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>
97 lines
5.4 KiB
JavaScript
97 lines
5.4 KiB
JavaScript
// main.js — istrain dashboard. Frame chrome (hit counter, refresh / hard-reset) + mounts live panels.
|
|
// Panels follow the engine's panels/<name>/mount(el) contract (see the radio engine). Static placeholder
|
|
// panels (dispatch, detect) stay in index.html until built.
|
|
//
|
|
// Refresh policy: slow background polls + change-aware renders so the page idles at ~0% CPU (no
|
|
// compositor stutter). Manual "refresh" re-pulls now; "hard reset" kills captures + restarts all helpers (/api/supervisor/reset).
|
|
|
|
import { mountSupervisor } from '/panels/supervisor/supervisor.js';
|
|
import { mountChecklist } from '/panels/checklist/checklist.js';
|
|
// captures RESTORED 2026-07-10 — the new antenna arrived + whisper transcripts went live; the public
|
|
// node gets voiced clips pushed. activity stays SHELVED (voice activity = a correlate-graph signal).
|
|
import { mountCaptures } from '/panels/captures/captures.js';
|
|
import { mountEot } from '/panels/eot/eot.js';
|
|
import { mountCorrelate } from '/panels/correlate/correlate.js';
|
|
import { mountBot } from '/panels/bot/bot.js';
|
|
import { mountMidtrain } from '/panels/midtrain/midtrain.js';
|
|
import { mountLive } from '/panels/live/live.js';
|
|
import { mountTrains } from '/panels/trains/trains.js';
|
|
import { mountBanner } from '/panels/banner/banner.js';
|
|
import { mountErrors } from '/panels/errors/errors.js';
|
|
|
|
// --- client-side error reporter → the Errors card (rate-limited + escaped server/panel side) ---
|
|
function reportErr(msg, src, line) {
|
|
try {
|
|
fetch('/api/errors/report', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ msg: String(msg).slice(0, 500), src: String(src || '').slice(0, 200), line: line || 0 }),
|
|
}).catch(() => {});
|
|
} catch { /* the reporter must never itself throw */ }
|
|
}
|
|
window.addEventListener('error', e => reportErr(e.message, e.filename, e.lineno));
|
|
window.addEventListener('unhandledrejection', e => reportErr('unhandledrejection: ' + ((e.reason && e.reason.message) || e.reason), location.pathname, 0));
|
|
|
|
// --- hit counter (change-aware; shows "—" until the server exists) ---
|
|
let lastHits = null;
|
|
async function loadHits() {
|
|
try {
|
|
const r = await fetch('/api/hits', { cache: 'no-store' });
|
|
if (!r.ok) return;
|
|
const { unique, total } = await r.json();
|
|
const key = unique + '/' + total;
|
|
if (key === lastHits) return; // no change → no DOM write
|
|
lastHits = key;
|
|
if (unique != null) document.getElementById('hits-unique').textContent = unique.toLocaleString();
|
|
if (total != null) document.getElementById('hits-total').textContent = total.toLocaleString();
|
|
} catch { /* no server yet — leave the dashes */ }
|
|
}
|
|
|
|
// --- build stamp: the commit this dashboard serves (VM: git; NAS: VERSION stamped by fetch-app.sh) ---
|
|
async function loadVersion() {
|
|
try {
|
|
const r = await fetch('/api/version', { cache: 'no-store' });
|
|
if (!r.ok) return;
|
|
const { commit } = await r.json();
|
|
if (commit) document.getElementById('ver').textContent = commit;
|
|
} catch { /* leave the dash */ }
|
|
}
|
|
|
|
// --- unique visitor: a persistent client token (robust behind the tunnel, where the real IP collapses) ---
|
|
function recordVisit() {
|
|
let vid = localStorage.getItem('istrain_vid');
|
|
if (!vid) {
|
|
vid = (window.crypto && crypto.randomUUID) ? crypto.randomUUID()
|
|
: (Date.now().toString(16) + '-' + Math.floor(Math.random() * 0xffffffff).toString(16));
|
|
localStorage.setItem('istrain_vid', vid);
|
|
}
|
|
fetch('/api/hit?vid=' + encodeURIComponent(vid), { cache: 'no-store' }).catch(() => {}).finally(loadHits);
|
|
}
|
|
|
|
// --- live panels ---
|
|
const supervisor = mountSupervisor(document.getElementById('supervisor'));
|
|
const checklist = mountChecklist(document.getElementById('checklist'));
|
|
const banner = mountBanner(document.getElementById('istrain-banner'));
|
|
const trains = mountTrains(document.getElementById('trains'));
|
|
const eot = mountEot(document.getElementById('eot'));
|
|
const correlate = mountCorrelate(document.getElementById('correlate'));
|
|
const bot = mountBot(document.getElementById('bot'));
|
|
const midtrain = mountMidtrain(document.getElementById('midtrain'));
|
|
const live = mountLive(document.getElementById('live'));
|
|
const captures = mountCaptures(document.getElementById('captures'));
|
|
const errors = mountErrors(document.getElementById('errors'));
|
|
|
|
// --- controls ---
|
|
document.getElementById('btn-refresh').onclick = () => { loadHits(); banner.refresh(); supervisor.refresh(); checklist.refresh(); trains.refresh(); eot.refresh(); correlate.refresh(); bot.refresh(); midtrain.refresh(); errors.refresh(); captures.refresh(); };
|
|
document.getElementById('btn-reset').onclick = async () => {
|
|
if (!confirm('Hard reset — kill the captures and restart all istrain helpers? Frees the dongles; device-level DVB resets still need cmd-palette.')) return;
|
|
const b = document.getElementById('btn-reset'), t = b.textContent;
|
|
b.disabled = true; b.textContent = 'resetting…';
|
|
try { await fetch('/api/supervisor/reset', { method: 'POST', headers: { 'X-Ops': '1' } }); } catch {}
|
|
setTimeout(() => { b.disabled = false; b.textContent = t; supervisor.refresh(); eot.refresh(); correlate.refresh(); }, 3500);
|
|
};
|
|
|
|
// initial pull + a slow safety poll (manual refresh drives the rest)
|
|
recordVisit(); // beacon once per load (mints/sends the visitor token), then shows counts
|
|
loadVersion(); // header build stamp — once per load
|
|
setInterval(loadHits, 600000); // 10 min
|