{% extends "base.html" %} {% block title %}Container {{container_name}}{% endblock %} {% block content %} <main> <h1>Container <span class="container-name">{{container_name}}</span></h1> <section class="status-section"> <h2>Status</h2> <dl> <dt>ID</dt> <dd>{{ info.id }}</dd> <dt>Status</dt> <dd data-state="{{ info.state }}">{{ info.state }}</dd> <dt>Image</dt> <dd>{{ info.image }}</dd> <dt>Image ID</dt> <dd>{{ info.image_id }}</dd> <dt>Created at</dt> <dd>{{ info.created_at }}</dd> {% match info.volumes %} {% when Some with (volumes) %} <dt>Volumes</dt> {% for volume in volumes %} <dd> {{ volume.source.clone().unwrap_or_default() }} → {{ volume.destination.clone().unwrap_or_default() }} </dd> {% endfor %} {% when None %} {% endmatch %} {% match info.env %} {% when Some with (env) %} <dt>Environment</dt> {% for var in env %} <dd>{{ var }}</dd> {% endfor %} {% when None %} {% endmatch %} </dl> </section> <section class="log-section"> <h2>Logs</h2> <code id="log"></code> </section> </main> <style scoped> main { display: flex; flex-direction: column; gap: 1rem; } h2 { text-transform: uppercase; margin: 0; padding: 0; margin-bottom: 1rem; } pre { margin: 0; padding: 0; } .container-name { color: #E796F3; } #log { background-color: #171625; display: flex; display: block; max-height: 80vh; overflow-y: auto; & .error { padding: 4px 8px; background-color: #3B1219; color: #FF9592; } & p { padding: 4px 8px; margin: 0; background-color: #1a1c38; &:nth-child(odd) { background-color: #212345; } } & time { padding-right: 1ch; color: #B1A9FF; } } dl { border: 1px solid #3D3E82; background-color: #202248; margin: 0; } dt, dd { box-sizing: border-box; padding: 4px; } dd~dt, dd~dd { border-top: 1px solid #262A65; } dt { float: left; width: 25%; padding-bottom: 0; } dd { margin-left: 25%; border-left: 1px dotted #3D3E82; background-color: #171625; padding: 4px 8px; word-break: break-all; } dd[data-state] { background-color: #202248; } dd[data-state="running"] { background-color: #113B29; color: #3DD68C; } dd[data-state="stopped"], dd[data-state="exited"], dd[data-state="dead"] { background-color: #500F1C; color: #FF9592; } dd:after { content: ""; display: block; clear: both; } main>h1 { grid-area: head; } .status-section { grid-area: info; } .log-section { grid-area: log; } @media (min-width: 1000px) { main { display: grid; grid-template-areas: "head head head" "info log log" "info log log"; grid-template-rows: 80px 1fr; grid-template-columns: 30vw 1fr; } } </style> <script type="module"> import ansiFmt from 'https://esm.run/ansi-to-html'; const convert = new ansiFmt({ fg: '#E0DFFE', bg: '#171625', newline: false, escapeXML: true, stream: false, colors: ["#282c34", "#abb2bf", "#e06c75", "#be5046", "#98c379", "#d19a66", "#61afef", "#c678dd", "#56b6c2", "#4b5263", "#5c6370"] }); const logEl = document.querySelector("#log"); const logSource = new EventSource(`${location.origin}${location.pathname}/log`); logSource.addEventListener("error", (ev) => { logSource.close(); const err = document.createElement("div"); err.className = "error"; err.appendChild(document.createTextNode("No logs available after this (container not running)")); logEl.appendChild(err); err.scrollIntoView(); return; }); logSource.addEventListener("message", (ev) => { const data = JSON.parse(ev.data); // If an error is received stop listening if ("error" in data) { logSource.close(); const err = document.createElement("div"); err.className = "error"; err.appendChild(document.createTextNode(data.error)); logEl.appendChild(err); err.scrollIntoView(); return; } // Received lines of log if ("lines" in data) { data.lines.split("\n").forEach(line => { if (!line) { return; } // Extract timestamp const firstSpace = line.indexOf(' ') const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)]; const lineEl = document.createElement("p"); lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim(); logEl.appendChild(lineEl); lineEl.scrollIntoView(); }); } }); // Fix copying from log (remove double newlines) logEl.addEventListener("copy", (event) => { const selection = document.getSelection(); event.clipboardData.setData("text/plain", selection.toString().replace(/\n\n/g, '\n')); event.preventDefault(); }); </script> {% endblock %}