{% extends "base.html" %} {% block title %}Container {{container_name}}{% endblock %} {% block content %} <main> <header> <h1>Container <span class="container-name">{{container_name}}</span> {% match info.stack() %}{% when Some with (stack) %}({% match info.stack_folder() %}{% when Some with (stack_folder) %}<a class="stack-name" href="/stack/{{stack_folder}}/">{{stack}}</a>) {% when None %}{% endmatch %} {% when None %}{% endmatch %}</h1> <div class="actions"> <form action="./start" method="POST"><button type="submit">Start</button></form> <form action="./restart" method="POST"><button type="submit">Restart</button></form> <form action="./stop" method="POST"><button type="submit">Stop</button></form> <form action="./kill" method="POST"><button type="submit">Kill</button></form> <form action="./remove" method="POST"><button type="submit">Delete</button></form> </div> </header> <section class="status-section"> <h2>Status</h2> <article> <dl class="table"> <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> </dl> <dl class="list"> {% 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 %} {% match info.labels %} {% when Some with (labels) %} <dt>Labels</dt> {% for label in labels %} <dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{ label.1 }}</span></dd> {% endfor %} {% when None %} {% endmatch %} </dl> </article> </section> <section class="log-section"> <h2>Logs</h2> <code id="log"></code> </section> </main> <link rel="stylesheet" href="/static/css/resource.css" /> <style scoped> a.stack-name { text-decoration: none; } main { display: flex; flex-direction: column; gap: 1rem; } h2 { margin: 0; padding: 0; margin-bottom: 1rem; } pre { margin: 0; padding: 0; } .container-name { color: var(--text-accent); } .label { & .key { color: var(--text-accent); } } #log { background-color: var(--bg-raised); display: flex; display: block; max-height: 80vh; overflow-y: auto; & .error { padding: 4px 8px; background-color: var(--danger-bg); color: var(--danger-text); } & p { padding: 4px 8px; margin: 0; background-color: #1a1c38; word-break: break-all; &:nth-child(odd) { background-color: #212345; } } & time { padding-right: 1ch; color: #B1A9FF; } } main>header { grid-area: head; } .status-section { grid-area: info; } .log-section { grid-area: log; } @media (min-width: 1000px) { main { overflow: hidden; display: grid; grid-template-areas: "head head" "info log"; grid-template-rows: 120px 1fr; grid-template-columns: 30vw 1fr; } .status-section>article, #log { height: calc(100vh - 240px); max-height: none; overflow: auto; } } </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.append(document.createTextNode("No logs available after this (container not running)")); logEl.append(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.append(document.createTextNode(data.error)); logEl.append(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.append(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 %}