559caead36
Public community release. The complete working system — DSP + decoders (scripts/), web dashboard + API (dashboard/), container stack (docker/), config templates (config/) — plus APOCALYPSE-EDITION.md, the full build guide for a human or a coding agent. GPLv3 (PyEOT dependency). Scrubbed of all secrets, internal IPs, hostnames, and location detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
241 lines
16 KiB
CSS
241 lines
16 KiB
CSS
/* istrain dashboard — frame. Color scheme carried over from the radio/marine dashboard
|
|
(the reusable radio engine). Signature look: each panel wears a colored left stripe. */
|
|
:root{
|
|
--bg:#0b1620; --panel:#13212e; --ink:#cfe3f0; --muted:#7d97a8;
|
|
--line:#243a4d; --accent:#37b3a4; --good:#2c9c6b; --bad:#e0413f;
|
|
/* per-panel left-stripe colors */
|
|
--c-dispatch:#e0b341; --c-detect:#37b3a4; --c-yard:#7fb3ff; --c-future:#5a7088;
|
|
--c-flow:#b48ce0; --c-stack:#57b6c9;
|
|
--c-captures:#e0884d; --c-activity:#b6c94d; --c-detectors:#e06b8a; --c-feeds:#8c9ee0;
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--ink)}
|
|
|
|
header{position:sticky;top:0;background:#0a131c;border-bottom:1px solid var(--line);padding:12px 16px;z-index:10;
|
|
display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
|
header h1{margin:0;font-size:20px;font-weight:700;letter-spacing:.02em}
|
|
header h1 .sub{font-size:12px;font-weight:400;color:var(--muted);margin-left:10px}
|
|
|
|
/* hits counter — header stat block */
|
|
.hdr-right{display:flex;align-items:center;gap:16px}
|
|
.controls{display:flex;gap:8px}
|
|
.btn{border:1px solid var(--line);background:#1b2c3b;color:var(--ink);padding:7px 12px;border-radius:6px;font:600 12px/1 system-ui;cursor:pointer}
|
|
.btn:hover{filter:brightness(1.18)}
|
|
.btn:active{transform:translateY(1px)}
|
|
.btn.reset{background:#3a1414;border-color:#7a2a28;color:#ff9a98}
|
|
.hits{display:flex;gap:18px;align-items:center}
|
|
.hits .stat{text-align:right}
|
|
.hits .num{font:700 20px/1 ui-monospace,monospace;color:var(--accent);font-variant-numeric:tabular-nums}
|
|
.hits .lab{font-size:10px;letter-spacing:.10em;text-transform:uppercase;color:var(--muted);margin-top:3px}
|
|
|
|
main{display:grid;gap:14px;padding:16px;max-width:1000px;margin:0 auto}
|
|
.panel{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--c-future);
|
|
border-radius:8px;padding:14px;min-width:0} /* min-width:0 lets a grid item shrink so a wide <pre> scrolls inside it instead of widening the whole page (mobile) */
|
|
.panel h2{margin:0 0 10px;font-size:14px;color:var(--accent);letter-spacing:.04em;text-transform:uppercase}
|
|
.panel h2 .tag{font-size:11px;color:var(--muted);font-weight:400;letter-spacing:.02em;text-transform:none;margin-left:8px}
|
|
|
|
/* left-stripe colors per panel role */
|
|
.panel.dispatch{border-left-color:var(--c-dispatch)}
|
|
.panel.dispatch h2{color:var(--c-dispatch)}
|
|
.panel.detect{border-left-color:var(--c-detect)}
|
|
.panel.yard{border-left-color:var(--c-yard)}
|
|
.panel.yard h2{color:var(--c-yard)}
|
|
.panel.flow{border-left-color:var(--c-flow)}
|
|
.panel.flow h2{color:var(--c-flow)}
|
|
.panel.stack{border-left-color:var(--c-stack)}
|
|
.panel.stack h2{color:var(--c-stack)}
|
|
/* per-PANEL distinct colors — id overrides the reused role-class so each panel reads as its own thing */
|
|
.panel#captures{border-left-color:var(--c-captures)} .panel#captures h2{color:var(--c-captures)}
|
|
.panel#activity{border-left-color:var(--c-activity)} .panel#activity h2{color:var(--c-activity)}
|
|
.panel#detectors{border-left-color:var(--c-detectors)} .panel#detectors h2{color:var(--c-detectors)}
|
|
.panel#feeds{border-left-color:var(--c-feeds)} .panel#feeds h2{color:var(--c-feeds)}
|
|
|
|
/* signal-flow ASCII map — hand-maintained <pre>; scrolls sideways if it can't fit */
|
|
.flow-chart{margin:0;overflow-x:auto;-webkit-overflow-scrolling:touch;
|
|
background:#0a131c;border:1px solid var(--line);border-radius:6px;padding:14px;
|
|
color:#c4dcea;font:11px/1.42 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
|
|
.placeholder{color:var(--muted);background:#0a131c;border:1px dashed var(--line);border-radius:6px;
|
|
padding:22px;text-align:center;line-height:1.7;font-size:13px}
|
|
.placeholder b{color:var(--ink)}
|
|
|
|
/* radios panel — per-dongle liveness table + status dots */
|
|
table.radios{width:100%;border-collapse:collapse;font-size:13px}
|
|
table.radios td{padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:middle}
|
|
table.radios tr:last-child td{border-bottom:none}
|
|
table.radios .sn{font-family:ui-monospace,monospace;color:var(--accent);font-weight:700;width:64px}
|
|
table.radios .role{color:var(--ink);text-transform:capitalize;width:80px}
|
|
table.radios .nm{color:var(--muted);font-size:12px}
|
|
table.radios .st{font-size:12px;text-align:right;font-variant-numeric:tabular-nums}
|
|
.dot{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle}
|
|
.dot.ready{background:#6f9bd9;box-shadow:0 0 7px #6f9bd9}
|
|
.dot.listening{background:var(--good);box-shadow:0 0 7px var(--good)}
|
|
.dot.blocked{background:#e0b341;box-shadow:0 0 7px #e0b341}
|
|
.dot.offline{background:var(--bad);box-shadow:0 0 7px var(--bad)}
|
|
/* the missing middle states (06-25 truth-by-data-flow): held-but-no-samples is the dangerous lie */
|
|
.dot.stalled{background:#e06a2e;box-shadow:0 0 8px #e06a2e}
|
|
.dot.held{background:#8a93a6;box-shadow:0 0 6px #8a93a6}
|
|
.st.ready{color:#6f9bd9} .st.listening{color:var(--good)} .st.blocked{color:#e0b341} .st.offline{color:var(--bad)}
|
|
.st.stalled{color:#e06a2e} .st.held{color:#8a93a6}
|
|
.sv-detail.stalled{color:#e06a2e;opacity:1;font-weight:600}
|
|
|
|
/* errors card — live issues + recent events (below WITH THANKS) */
|
|
.err-row{font-family:ui-monospace,monospace;font-size:12px;padding:4px 6px;border-bottom:1px solid var(--line);line-height:1.45}
|
|
.err-row:last-child{border-bottom:none}
|
|
.err-row.err-live{color:#e06a2e;font-weight:600}
|
|
.err-row.err-err{color:#d9705e}
|
|
.err-row.err-event{color:var(--muted)}
|
|
/* grid panel order (the owner's layout, 2026-06-23; supervisor moved OUT of <main> to the ops corner
|
|
below THANKS 2026-07-06): correlate → bot → mid-train → eot → voice activity → captures →
|
|
detectors → feeds → signal flow → system map. The signal panels run FRONT-to-BACK of a train
|
|
(bot=head · mid=DPU · eot=rear). Thanks + supervisor + errors + checklist follow after <main>. */
|
|
#correlate{order:-1} /* overview panel — pinned to the top, under the banner, before the train cards */
|
|
#live{order:0} #bot{order:3} #midtrain{order:4} #eot{order:5} #activity{order:6}
|
|
#captures{order:7} #detectors{order:8} #feeds{order:9} #flow{order:10} #stack{order:11}
|
|
#dispatch{order:12} /* Dispatch/Yard whisper placeholder — parked at the bottom; may use it later */
|
|
table.supervisor{width:100%;border-collapse:collapse;font-size:12.5px}
|
|
table.supervisor td{padding:4px 6px;border-bottom:1px solid rgba(255,255,255,.05);vertical-align:middle}
|
|
table.supervisor tr:last-child td{border-bottom:0}
|
|
.sv-grp td{font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);padding:8px 6px 2px;border-bottom:0}
|
|
.sv-name{font-weight:600}
|
|
.sv-detail{font-size:11.5px;opacity:.85}
|
|
.sv-since{font-size:11px;color:var(--muted);white-space:nowrap;text-align:right}
|
|
.sv-actions{text-align:right;white-space:nowrap}
|
|
.sv-ctl{background:none;border:1px solid rgba(255,255,255,.2);color:inherit;opacity:.55;cursor:pointer;font-size:11px;line-height:1;padding:2px 6px;border-radius:3px;margin-left:3px}
|
|
.sv-ctl:hover{opacity:1;border-color:var(--accent);color:var(--accent)}
|
|
|
|
/* NOAA per-radio sanity toggle — idle = quiet outline; on = live green (audio playing, hijacked) */
|
|
.noaa-btn{border:1px solid var(--line);background:#1b2c3b;color:var(--ink);
|
|
border-radius:6px;font:600 11px/1 system-ui;text-transform:none;letter-spacing:0;
|
|
cursor:pointer;white-space:nowrap}
|
|
.noaa-btn:hover{filter:brightness(1.18)}
|
|
.noaa-btn:active{transform:translateY(1px)}
|
|
.noaa-btn:disabled{opacity:.55;cursor:wait}
|
|
.noaa-btn.on{background:var(--good);border-color:var(--good);color:#eafff4;box-shadow:0 0 8px var(--good)}
|
|
.noaa-btn.mini{padding:4px 9px}
|
|
table.radios .noaa-cell{text-align:right;width:1%;white-space:nowrap}
|
|
.noaa-na{color:var(--muted);opacity:.5}
|
|
|
|
/* build checklist — rendered from checklist.md; below thanks. centered like thanks since it
|
|
sits outside <main>. checked items dim + strike; group headers rule off the sections. */
|
|
.panel.checklist{border-left-color:var(--good)}
|
|
/* every section OUTSIDE <main> (thanks / supervisor / errors / checklist) gets the SAME effective
|
|
width as the in-main panels (main = 1000px cap minus 16px side padding → 968px interior), so
|
|
the whole page reads one column — no full-window-wrapping strays. */
|
|
.thanks, .panel.checklist, #supervisor, #errors{
|
|
max-width:968px;margin:14px auto;width:calc(100% - 32px);box-sizing:border-box}
|
|
.panel.checklist h2{color:var(--good)}
|
|
.cl-intro{color:var(--muted);font-size:12px;line-height:1.5;margin-bottom:6px}
|
|
.cl-group{color:var(--ink);font-weight:700;font-size:12px;letter-spacing:.04em;text-transform:uppercase;
|
|
margin:12px 0 6px;padding-bottom:3px;border-bottom:1px solid var(--line)}
|
|
.cl-item{display:flex;gap:8px;align-items:flex-start;font-size:13px;padding:2px 0;color:var(--ink)}
|
|
.cl-item.cl-sub{margin-left:22px;font-size:12px;color:var(--muted)}
|
|
.cl-item.done{color:var(--muted)}
|
|
.cl-item.done .cl-text{text-decoration:line-through;opacity:.7}
|
|
.cl-box{flex-shrink:0;color:var(--muted);line-height:1.5}
|
|
.cl-item.done .cl-box{color:var(--good)}
|
|
.cl-text code,.cl-intro code{background:#0a131c;border:1px solid var(--line);border-radius:3px;
|
|
padding:0 4px;font-size:11px;color:var(--accent);word-break:break-word}
|
|
|
|
/* with thanks — carried from the first istrain, extended for the rebuild */
|
|
.thanks{padding:14px 16px;background:#0f1a24;border:1px solid var(--line);
|
|
border-left:4px solid var(--good);border-radius:8px;font:12px/1.65 ui-monospace,monospace;color:var(--muted)}
|
|
/* width/margin come from the shared outside-main rule above */
|
|
.thanks .th-h{color:var(--good);letter-spacing:.12em;font-weight:700;margin-bottom:8px}
|
|
.thanks p{margin:8px 0}
|
|
.thanks .th-grp b{color:var(--ink);display:inline-block;margin-bottom:2px}
|
|
.thanks a{color:#7ec8e3;text-decoration:none}
|
|
.thanks a:hover{text-decoration:underline}
|
|
.thanks strong{color:var(--ink)}
|
|
.thanks em{color:var(--ink);font-style:italic}
|
|
.thanks .dim{color:#5a7088}
|
|
|
|
footer{color:var(--muted);text-align:center;padding:16px;font-size:12px}
|
|
footer a{color:var(--accent);text-decoration:none}
|
|
|
|
/* ---- narrow / portrait (phone) ---- istrain will mostly be viewed on mobile.
|
|
Stack the header instead of fighting for one row, trim padding, and let the
|
|
radios table wrap instead of overflowing a ~360px screen. */
|
|
@media (max-width:560px){
|
|
body{font:13px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
|
|
/* header: title on its own line, sub beneath it, controls+hits on a full-width row */
|
|
header{padding:10px 12px;gap:10px}
|
|
header h1{font-size:17px;display:flex;flex-direction:column;gap:2px}
|
|
header h1 .sub{margin-left:0;font-size:11px}
|
|
.hdr-right{width:100%;justify-content:space-between;gap:10px}
|
|
.hits{gap:14px}
|
|
.hits .num{font-size:17px}
|
|
.btn{padding:7px 10px}
|
|
/* body: tighter gutters so panels get the width back */
|
|
main{padding:12px;gap:12px}
|
|
.panel{padding:12px}
|
|
.placeholder{padding:16px 12px;font-size:12.5px;line-height:1.6}
|
|
/* radios table: drop fixed cell widths, allow long product names to wrap */
|
|
table.radios{font-size:12.5px}
|
|
table.radios td{padding:6px 6px}
|
|
table.radios .sn,table.radios .role{width:auto}
|
|
table.radios .nm{word-break:break-word}
|
|
.thanks{padding:12px;font-size:11.5px}
|
|
.flow-chart{font-size:10px;padding:12px 10px}
|
|
footer{padding:14px 12px}
|
|
}
|
|
|
|
/* captures panel — rail-voice clip list (light: native <audio> preload=none, change-aware renders) */
|
|
.cap-row{display:flex;flex-direction:column;gap:4px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06)}
|
|
.cap-row:last-child{border-bottom:0}
|
|
.cap-meta{display:flex;justify-content:space-between;align-items:baseline;gap:10px;font-size:12.5px}
|
|
.cap-ch{font-weight:600}
|
|
.cap-time{opacity:.6;font-size:11px;white-space:nowrap}
|
|
.cap-audio{width:100%;height:30px}
|
|
.cap-text{font-size:12px;opacity:.85;font-style:italic;padding-left:2px}
|
|
.cap-right{display:flex;align-items:center;gap:8px;white-space:nowrap}
|
|
.cap-del{background:none;border:1px solid rgba(255,255,255,.18);color:inherit;opacity:.4;cursor:pointer;font-size:11px;line-height:1;padding:2px 6px;border-radius:3px}
|
|
.cap-del:hover{opacity:1;color:#e06b5c;border-color:#e06b5c}
|
|
.cap-play{background:none;border:1px solid rgba(255,255,255,.2);color:inherit;opacity:.6;cursor:pointer;font-size:10px;line-height:1;padding:2px 7px;border-radius:3px}
|
|
.cap-play:hover{opacity:1;color:var(--accent);border-color:var(--accent)}
|
|
.cap-play.on{opacity:.3;cursor:default}
|
|
/* activity panel — per-channel week strips (15-min buckets, canvas) */
|
|
.act-item{margin:0 0 9px}
|
|
.act-item:last-child{margin-bottom:0}
|
|
.act-head{display:flex;align-items:baseline;gap:8px;font-size:12.5px;line-height:1.4}
|
|
.act-head .act-ch{flex:1;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.act-head .act-n{font-variant-numeric:tabular-nums;opacity:.85}
|
|
.act-head .act-last{opacity:.55;font-size:11px;white-space:nowrap}
|
|
.act-strip{display:block;width:100%;height:32px;margin-top:3px;background:rgba(255,255,255,.045);border-radius:2px;cursor:crosshair}
|
|
.act-mission .act-ch{color:#37b3a4}
|
|
.act-tip{position:fixed;z-index:60;pointer-events:none;background:rgba(18,22,26,.97);border:1px solid rgba(255,255,255,.15);border-radius:3px;padding:3px 7px;font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap;color:#e8eef0;box-shadow:0 2px 10px rgba(0,0,0,.45)}
|
|
|
|
/* EOT panel — presence on 457.9375 (dongle 1001) */
|
|
.panel#eot{border-left-color:#e6aa46} .panel#eot h2{color:#e6aa46}
|
|
.panel#bot{border-left-color:#b482e6} .panel#bot h2{color:#b482e6} /* violet — head/front */
|
|
.panel#midtrain{border-left-color:#5ab4d2} .panel#midtrain h2{color:#5ab4d2} /* blue — mid-consist/DPU */
|
|
.eot-head{display:flex;align-items:baseline;gap:10px;font-size:12.5px;margin-bottom:2px}
|
|
.eot-state{font-weight:600;color:var(--muted)}
|
|
.eot-state.eot-near{color:#e6aa46}
|
|
.eot-last{color:var(--muted);font-size:11px}
|
|
.eot-n{margin-left:auto;color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}
|
|
.eot-strip{display:block;width:100%;height:32px;margin-top:3px;background:rgba(255,255,255,.045);border-radius:2px}
|
|
/* correlate panel — multi-band train-activity synthesis (teal = the mission/detection panel) */
|
|
.panel#correlate{border-left-color:#37b3a4} .panel#correlate h2{color:#37b3a4}
|
|
.corr-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12.5px;margin-bottom:4px}
|
|
.corr-state{font-weight:700;letter-spacing:.02em;padding:1px 7px;border-radius:4px;color:var(--muted)}
|
|
.corr-state.corr-single{color:#e6aa46} /* amber — one band, unconfirmed */
|
|
.corr-state.corr-candidate{color:#08121a;background:#37b3a4} /* teal solid — bands co-firing */
|
|
.corr-state.corr-error{color:#e06b6b}
|
|
.corr-key{display:inline-flex;align-items:center;gap:5px;color:var(--muted);font-size:11.5px}
|
|
.corr-key::before{content:'';width:8px;height:8px;border-radius:50%;background:rgb(var(--c))}
|
|
.corr-key b{color:var(--ink);font-weight:600}
|
|
.corr-strip{display:block;width:100%;height:92px;margin-top:2px;background:rgba(255,255,255,.045);border-radius:3px} /* 4-row heatmap */
|
|
.corr-why{margin-top:5px;font-size:11px;line-height:1.55;color:var(--muted);font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
.corr-why-line{white-space:pre-wrap}
|
|
/* live signal meter — compact bar, top of the grid */
|
|
.panel#live{padding:8px 12px;border-left-color:#6b7785}
|
|
.live-row{display:flex;align-items:center;gap:10px}
|
|
.live-label{font-weight:600;font-size:12px;min-width:118px;color:var(--muted)}
|
|
.live-track{position:relative;flex:1;height:14px;background:rgba(255,255,255,.06);border-radius:7px;overflow:hidden}
|
|
.live-fill{height:100%;width:0;background:#888;border-radius:7px;transition:width .12s linear,background .2s}
|
|
.live-floor{position:absolute;top:0;bottom:0;width:2px;background:rgba(255,255,255,.55);transition:left .3s;pointer-events:none} /* adaptive noise floor */
|
|
.live-db{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;min-width:74px;text-align:right}
|
|
.panel#live.live-on{border-left-color:#fff}
|
|
/* (detections panel removed 2026-06-19 — Activity absorbed it; heard.jsonl + /api/heard stay) */
|