istrain-public: from-scratch build of a passive RF train detector
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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user