diff --git a/internal/server/index.html b/internal/server/index.html index 91abfcf..85612a5 100644 --- a/internal/server/index.html +++ b/internal/server/index.html @@ -20,6 +20,7 @@ padding:clamp(1.25rem,4vw,2.5rem); max-width:920px; margin:0 auto; } a { color:var(--cyan); } + code { font-family:var(--mono); } /* header */ header { display:flex; align-items:center; gap:1rem; margin-bottom:1.75rem; } @@ -36,40 +37,72 @@ .card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:1.4rem 1.5rem; margin-bottom:1.25rem; } .eyebrow { font-family:var(--mono); font-size:.7rem; letter-spacing:.16em; text-transform:uppercase; color:var(--dim); margin:0 0 .9rem; } - /* current job */ - .job-head { display:flex; align-items:baseline; gap:.75rem; flex-wrap:wrap; margin-bottom:.2rem; } - .job-file { font-size:1.15rem; font-weight:600; color:#e7edf4; word-break:break-word; } - .badge { font-family:var(--mono); font-size:.66rem; letter-spacing:.08em; text-transform:uppercase; padding:.2rem .5rem; border-radius:5px; border:1px solid var(--line); color:var(--muted); } + /* ---- hero ---- */ + .job-head { display:flex; align-items:baseline; gap:.6rem; flex-wrap:wrap; margin-bottom:1rem; } + .job-title { font-size:1.2rem; font-weight:600; color:#e7edf4; word-break:break-word; } + .job-title .se { color:var(--amber); font-family:var(--mono); font-size:1rem; margin-right:.45rem; } + .job-title .ep { color:var(--muted); font-weight:400; } + .badge { font-family:var(--mono); font-size:.66rem; letter-spacing:.08em; text-transform:uppercase; padding:.2rem .5rem; border-radius:5px; border:1px solid var(--line); color:var(--muted); margin-left:auto; white-space:nowrap; } .badge[data-phase="encoding"] { color:var(--amber); border-color:var(--amber); } .badge[data-phase="probing"], .badge[data-phase="audio"] { color:var(--cyan); border-color:var(--cyan); } - .badge[data-phase="idle"] { color:var(--dim); } - .pct { font-family:var(--mono); font-size:2.6rem; font-weight:600; line-height:1; margin:.6rem 0 .5rem; color:var(--amber); } - .pct.idle { color:var(--dim); font-size:1.4rem; } + /* ETA is the hero answer */ + .eta-line { display:flex; align-items:baseline; gap:.7rem; flex-wrap:wrap; margin-bottom:.9rem; } + .eta-big { font-family:var(--mono); font-size:2.6rem; font-weight:600; line-height:1; color:var(--amber-soft); } + .eta-big.dim { color:var(--dim); font-size:1.6rem; } + .eta-at { font-family:var(--mono); color:var(--muted); font-size:.95rem; } + .eta-at b { color:var(--text); font-weight:500; } - .bar { height:10px; border-radius:6px; background:var(--panel-2); overflow:hidden; border:1px solid var(--line); } + .bar { position:relative; height:12px; border-radius:6px; background:var(--panel-2); overflow:hidden; border:1px solid var(--line); } .bar > i { display:block; height:100%; width:0; background:linear-gradient(90deg,var(--amber-soft),var(--amber)); border-radius:6px; transition:width .6s ease; } .bar.indet > i { width:35%; background:linear-gradient(90deg,transparent,var(--cyan),transparent); animation:slide 1.3s linear infinite; transition:none; } @keyframes slide { 0%{transform:translateX(-120%);} 100%{transform:translateX(340%);} } - @media (prefers-reduced-motion:reduce){ .dot.ok{animation:none;} .bar.indet>i{animation:none; width:100%; opacity:.4;} } - - .stats { display:flex; flex-wrap:wrap; gap:1.5rem; margin-top:1.1rem; } - .stat { font-family:var(--mono); } - .stat .v { font-size:1.15rem; color:var(--text); } - .stat .k { font-size:.66rem; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); margin-top:.1rem; } - + .pct { font-family:var(--mono); font-size:.8rem; color:var(--muted); margin-top:.45rem; } + .pct b { color:var(--text); } .idle-msg { color:var(--muted); } - /* grid: queue + events */ + /* ---- signature: throughput sparkline ---- */ + .pulse { margin-top:1.3rem; padding-top:1.2rem; border-top:1px solid var(--line); display:grid; grid-template-columns:1fr auto; gap:1rem 1.5rem; align-items:center; } + .spark-wrap { grid-column:1; min-width:0; } + .spark-head { display:flex; justify-content:space-between; align-items:baseline; font-family:var(--mono); font-size:.66rem; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); margin-bottom:.35rem; } + svg.spark { width:100%; height:54px; display:block; overflow:visible; } + .ref-label { font-family:var(--mono); font-size:.6rem; fill:var(--dim); } + + .gauges { grid-column:2; display:flex; gap:1.6rem; } + .gauge { font-family:var(--mono); text-align:right; } + .gauge .v { font-size:1.5rem; line-height:1; } + .gauge .v.warn { color:var(--amber); } + .gauge .v.bad { color:var(--red); } + .gauge .v.good { color:var(--green); } + .gauge .k { font-size:.62rem; letter-spacing:.12em; text-transform:uppercase; color:var(--dim); margin-top:.35rem; } + .gauge .sub { font-size:.6rem; color:var(--dim); margin-top:.15rem; } + + .substats { display:flex; flex-wrap:wrap; gap:1.4rem; margin-top:1.2rem; font-family:var(--mono); } + .substats .s { font-size:.8rem; color:var(--muted); } + .substats .s b { color:var(--text); font-weight:500; } + + /* stream chips */ + .streams { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:1rem; } + .chip { font-family:var(--mono); font-size:.72rem; padding:.2rem .5rem; border-radius:5px; background:var(--panel-2); border:1px solid var(--line); color:var(--text); } + .chip .lab { color:var(--dim); } + .chip .ch { color:var(--amber); } + + /* grid */ .grid { display:grid; grid-template-columns:1fr 1fr; gap:1.25rem; } - @media (max-width:680px){ .grid { grid-template-columns:1fr; } } + @media (max-width:680px){ .grid { grid-template-columns:1fr; } .pulse{ grid-template-columns:1fr; } .gauges{ grid-column:1; justify-content:flex-start; } .gauge{ text-align:left; } } - .count { font-family:var(--mono); color:var(--amber); } - ul.list { list-style:none; margin:0; padding:0; font-family:var(--mono); font-size:.85rem; } - ul.list li { padding:.4rem 0; border-bottom:1px solid var(--line); color:var(--text); word-break:break-word; } + /* queue — collapsed aggregate */ + .qsum { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; margin-bottom:.8rem; } + .qsum .qn { font-family:var(--mono); font-size:1.4rem; color:var(--amber); line-height:1; } + .qsum .ql { color:var(--muted); font-size:.85rem; } + ul.list { list-style:none; margin:0; padding:0; font-family:var(--mono); font-size:.82rem; } + ul.list li { padding:.35rem 0; border-bottom:1px solid var(--line); color:var(--muted); word-break:break-word; } ul.list li:last-child { border-bottom:0; } - ul.list li.empty { color:var(--dim); border:0; } + ul.list li .pre { color:var(--dim); } + ul.list li .key { color:var(--text); } + .qmore { font-family:var(--mono); font-size:.74rem; color:var(--dim); padding-top:.5rem; } + /* events */ .ev { display:flex; gap:.6rem; padding:.4rem 0; border-bottom:1px solid var(--line); font-size:.82rem; } .ev:last-child { border-bottom:0; } .ev time { font-family:var(--mono); font-size:.7rem; color:var(--dim); white-space:nowrap; padding-top:.1rem; } @@ -78,20 +111,7 @@ .day { text-align:center; font-family:var(--mono); font-size:.64rem; letter-spacing:.12em; text-transform:uppercase; color:var(--dim); margin:.8rem 0 .2rem; } .day:first-child { margin-top:0; } - /* job metadata */ - .meta { margin-top:1.1rem; padding-top:1.1rem; border-top:1px solid var(--line); } - .meta .mtitle { font-size:1.1rem; color:#e7edf4; font-weight:600; word-break:break-word; } - .meta .msub { color:var(--amber-soft); font-size:.92rem; margin-top:.15rem; } - .meta .mrow { color:var(--muted); font-size:.85rem; margin-top:.3rem; } - .meta .mrow b { color:var(--text); font-weight:500; } - - /* stream chips */ - .streams { display:flex; flex-direction:column; gap:.5rem; margin-top:1rem; } - .srow { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; } - .srow .lab { font-family:var(--mono); font-size:.64rem; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); width:4rem; flex:none; padding-top:.25rem; } - .chip { font-family:var(--mono); font-size:.74rem; padding:.2rem .5rem; border-radius:5px; background:var(--panel-2); border:1px solid var(--line); color:var(--text); } - .chip .ch { color:var(--amber); } - .chip .cd { color:var(--dim); } + @media (prefers-reduced-motion:reduce){ .dot.ok{animation:none;} .bar.indet>i{animation:none; width:100%; opacity:.4;} }
@@ -102,14 +122,13 @@Current job
Queue
-Queue
+Recent events
@@ -127,105 +146,175 @@ ({ "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[c])); } - function fmtDur(sec) { - sec = Math.max(0, Math.floor(sec || 0)); - if (sec <= 0) return "--"; - const h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), s = sec % 60; - if (h) return `${h}h${String(m).padStart(2,"0")}m`; - if (m) return `${m}m${String(s).padStart(2,"0")}s`; - return `${s}s`; - } - - // "17:49 (3h52m)" — wall-clock finish time plus remaining duration. - function etaText(sec) { - if (!sec || sec <= 0) return "--"; - const done = new Date(Date.now() + sec * 1000); - const p = n => String(n).padStart(2, "0"); - return `${p(done.getHours())}:${p(done.getMinutes())} (${fmtDur(sec)})`; - } - - function chLabel(n) { return ({1:"mono",2:"2.0",6:"5.1",8:"7.1"})[n] || (n ? n+"ch" : ""); } function base(path) { return (path || "").split("/").pop(); } function pad2(s) { return String(s || "").padStart(2, "0"); } - function jobHeading(c) { + // "3h 53m" / "53m 20s" / "20s" — remaining time, spelled for the hero. + function fmtDurLong(sec) { + sec = Math.max(0, Math.floor(sec || 0)); + if (sec <= 0) return "—"; + const h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), s = sec % 60; + if (h) return `${h}h ${pad2(m)}m`; + if (m) return `${m}m ${pad2(s)}s`; + return `${s}s`; + } + // wall-clock finish time, "21:35" + function clockAt(sec) { + if (!sec || sec <= 0) return "—"; + const d = new Date(Date.now() + sec * 1000), p = n => String(n).padStart(2, "0"); + return `${p(d.getHours())}:${p(d.getMinutes())}`; + } + function chLabel(n) { return ({1:"mono",2:"2.0",6:"5.1",8:"7.1"})[n] || (n ? n+"ch" : ""); } + + // ---- signature: throughput sparkline, fed by the 1 s poll ---- + let speedHist = [], histFile = null; + function pushSpeed(c) { + if (!c || c.file !== histFile) { speedHist = []; histFile = c ? c.file : null; } + if (c && c.phase === "encoding" && c.speed > 0) { + speedHist.push(c.speed); + if (speedHist.length > 90) speedHist.shift(); // ~90 s window + } + } + // Trace of recent speed against a fixed 1.0× realtime reference. The reference + // stays fixed on purpose — autoscaling would hide how far below realtime we run. + function sparkSVG(hist, ref) { + const W = 300, H = 54; + if (hist.length < 2) + return ``; + const max = Math.max(ref * 1.3, ...hist); + const y = v => H - (v / max) * H; + const pts = hist.map((v, i) => `${(i/(hist.length-1)*W).toFixed(1)},${y(v).toFixed(1)}`).join(" "); + const refY = y(ref).toFixed(1); + const below = hist[hist.length-1] < ref; + const col = below ? "var(--red)" : "var(--green)"; + return ``; + } + function trendLabel(hist) { + if (hist.length < 4) return ["—", "var(--dim)"]; + const last = hist[hist.length-1], prev = hist[hist.length-3]; + if (last < prev - 0.01) return ["slowing", "var(--red)"]; + if (last > prev + 0.01) return ["recovering", "var(--green)"]; + return ["holding", "var(--dim)"]; + } + function speedClass(v) { return v <= 0 ? "" : v < 0.5 ? "bad" : v < 1 ? "warn" : "good"; } + + function jobTitleHTML(c) { const m = c.meta; if (m && m.is_series && m.collection) { const se = (m.season && m.episode) ? `S${pad2(m.season)}E${pad2(m.episode)}` : ""; - return { title: m.collection, sub: [se, m.title].filter(Boolean).join(" · ") }; + const ep = (m.title && m.title !== "Unknown") ? ` — “${esc(m.title)}”` : ""; + return `${se ? `${se}` : ""}${esc(m.collection)}${ep}`; } - if (m && m.title && m.title !== "Unknown") return { title: m.title, sub: "" }; - return { title: base(c.file), sub: "" }; + if (m && m.title && m.title !== "Unknown") return esc(m.title); + return esc(base(c.file)); } - function renderStreams(streams) { + function streamsHTML(streams) { if (!streams || !streams.length) return ""; - const audio = streams.filter(s => s.kind === "audio"); - const subs = streams.filter(s => s.kind === "subtitle"); - let html = ""; - if (audio.length) { - html += `Waiting for files in input/.
Waiting for files in input/.