initial POC
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Layerlight | Timelapse</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; --ink: #f4f1e8; --muted: #a8aaa2; --line: #303531; --panel: #171b19; --accent: #e2b36d; --good: #8fca98; --bad: #e47f6d; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: #0d100f; color: var(--ink); font: 15px/1.5 Georgia, serif; }
|
||||
main { width: min(1100px, calc(100% - 40px)); margin: 0 auto; padding: 42px 0 64px; }
|
||||
header { display: flex; justify-content: space-between; align-items: end; gap: 24px; margin-bottom: 38px; border-bottom: 1px solid var(--line); padding-bottom: 24px; }
|
||||
h1, h2, p { margin: 0; } h1 { font-size: clamp(2.3rem, 6vw, 4.5rem); line-height: .95; font-weight: 400; letter-spacing: -.04em; }
|
||||
h1 span { color: var(--accent); } h2 { font: 600 12px/1.2 monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
|
||||
.eyebrow { color: var(--accent); font: 11px monospace; letter-spacing: .18em; text-transform: uppercase; margin-bottom: 12px; }
|
||||
.connection { color: var(--muted); font: 12px monospace; text-align: right; } .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--bad); margin-right: 7px; } .dot.good { background: var(--good); }
|
||||
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); margin-bottom: 36px; }
|
||||
.metric { padding: 22px; border-right: 1px solid var(--line); } .metric:last-child { border-right: 0; }
|
||||
.value { display: block; margin-top: 7px; font: 28px monospace; color: var(--ink); } .unit { color: var(--muted); font-size: 12px; }
|
||||
section { margin-top: 32px; } .section-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 14px; }
|
||||
.videos { display: grid; gap: 10px; } .video { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 17px 20px; background: var(--panel); border: 1px solid var(--line); }
|
||||
.video strong { font: 14px monospace; } .video small { display: block; margin-top: 5px; color: var(--muted); } a { color: var(--accent); font: 12px monospace; text-decoration: none; border-bottom: 1px solid var(--accent); padding-bottom: 2px; }
|
||||
.preview { position: relative; min-height: 180px; background: var(--panel); border: 1px solid var(--line); overflow: hidden; } .preview img { display: block; width: 100%; max-height: 560px; object-fit: contain; } .preview p { padding: 70px 20px; color: var(--muted); text-align: center; font: 12px monospace; }
|
||||
.live { background: #050706; border: 1px solid var(--line); overflow: hidden; } .live img { display: block; width: 100%; min-height: 220px; max-height: 620px; object-fit: contain; }
|
||||
.empty { color: var(--muted); padding: 22px 0; } .error { color: var(--bad); font: 12px monospace; margin-top: 16px; min-height: 18px; }
|
||||
@media (max-width: 700px) { main { width: min(100% - 24px, 560px); padding-top: 28px; } header { display: block; } .connection { text-align: left; margin-top: 20px; } .metrics { grid-template-columns: repeat(2, 1fr); } .metric:nth-child(2) { border-right: 0; } .metric:nth-child(-n+2) { border-bottom: 1px solid var(--line); } .video { align-items: start; flex-direction: column; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div><p class="eyebrow">Moonraker / layer capture</p><h1>Layer<span>light</span></h1></div>
|
||||
<p class="connection"><i class="dot" id="dot"></i><span id="connection">Connecting...</span><br><span id="updated">Waiting for worker</span></p>
|
||||
</header>
|
||||
<div class="metrics">
|
||||
<div class="metric"><h2>Printer</h2><span class="value" id="state">--</span></div>
|
||||
<div class="metric"><h2>Current Z</h2><span class="value" id="z">-- <span class="unit">mm</span></span></div>
|
||||
<div class="metric"><h2>Frames</h2><span class="value" id="frames">0</span></div>
|
||||
<div class="metric"><h2>Print</h2><span class="value" id="session">--</span></div>
|
||||
</div>
|
||||
<section><div class="section-head"><h2>Latest snapshot</h2><h2 id="snapshot-time">Waiting for first layer</h2></div><div class="preview"><img id="snapshot" alt="Latest captured layer snapshot" hidden><p id="snapshot-empty">No snapshot captured yet.</p></div></section>
|
||||
<section><div class="section-head"><h2>Live printer view</h2><h2>Camera stream</h2></div><div class="live"><img src="/api/camera" alt="Live printer camera view" onerror="this.alt='Camera stream unavailable';"></div></section>
|
||||
<section><div class="section-head"><h2>Rendered timelapses</h2><h2 id="count">0 files</h2></div><div class="videos" id="videos"><p class="empty">No completed prints yet.</p></div><p class="error" id="error"></p></section>
|
||||
</main>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const formatBytes = (bytes) => bytes > 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
||||
async function refresh() {
|
||||
try {
|
||||
const [status, videos] = await Promise.all([fetch('/api/status').then(r => r.json()), fetch('/api/videos').then(r => r.json())]);
|
||||
$('dot').className = `dot ${status.connected ? 'good' : ''}`;
|
||||
$('connection').textContent = status.connected ? 'Moonraker connected' : 'Moonraker offline';
|
||||
$('updated').textContent = status.updated_at ? new Date(status.updated_at).toLocaleTimeString() : 'Waiting for worker';
|
||||
$('state').textContent = status.state || '--'; $('z').firstChild.textContent = status.z_height == null ? '-- ' : `${status.z_height.toFixed(2)} `;
|
||||
$('frames').textContent = status.frames || 0; $('session').textContent = status.print_name || status.session || '--'; $('error').textContent = status.last_error || '';
|
||||
if (status.frames > 0) { $('snapshot').src = `/api/snapshot?frame=${status.frames}`; $('snapshot').hidden = false; $('snapshot-empty').hidden = true; $('snapshot-time').textContent = `Layer ${status.frames}`; }
|
||||
$('count').textContent = `${videos.length} file${videos.length === 1 ? '' : 's'}`;
|
||||
$('videos').innerHTML = videos.length ? videos.map(video => `<article class="video"><div><strong>${video.name}</strong><small>${formatBytes(video.size_bytes)} · ${new Date(video.created_at).toLocaleString()}</small></div><a href="${video.url}" download>Download MP4</a></article>`).join('') : '<p class="empty">No completed prints yet.</p>';
|
||||
} catch (error) { $('connection').textContent = 'Panel unavailable'; }
|
||||
}
|
||||
refresh(); setInterval(refresh, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user