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,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""istrain — Mill supervisor probe. Runs on the TrueNAS HOST via cron (every minute, root):
|
||||
docker state -> data/supervisor.json in the dashboard's snapshot shape, so the supervisor card
|
||||
tells Docker-era truth (it snapshotted the VM's systemd units until 2026-07-18). Read-only:
|
||||
can_ctl=false on every row — site control buttons are parked until a docker control relay exists
|
||||
(MILL-MIGRATION.md open item). Atomic write; stdlib only (host python3, no pip)."""
|
||||
import json, os, subprocess, tempfile, time
|
||||
|
||||
STACK = "/mnt/.ix-apps/app_mounts/dockge/stacks/istrain"
|
||||
OUT = os.path.join(STACK, "data", "supervisor.json")
|
||||
|
||||
# (container, name, job) — mirrors the VM card's rows; names kept identical where the role carried over.
|
||||
ROLES = [
|
||||
("istrain-web", "Dashboard", "serves this dashboard + data API (Mill :3456, the Cloudflare origin)"),
|
||||
("istrain-scanhop", "UHF · 1001", "dongle 1001: 452 ⇄ 457 retune-in-place hop → BOT/EOT/MID logs + clips + live meter"),
|
||||
("istrain-airband", "Voice · 1002", "dongle 1002: rail voice, 5 NS channels → mp3 clips + heard log"),
|
||||
("istrain-worker", "Transcribe", "voice clips → whisper on this box (:9000) → sidecars + transcripts log"),
|
||||
("istrain-eot-decode", "EOT decode", "eot_clips → unit/telemetry; enriches eot.jsonl (never culls)"),
|
||||
("istrain-bot-recover", "BOT recover", "bot_clips → clean head-end frame; enriches bot.jsonl (never culls)"),
|
||||
("istrain-trim", "Trim", "hourly voice-clip prune (>24h) — heard.jsonl ingest-gated, detections kept"),
|
||||
("istrain-whisper", "Whisper ASR", "faster-whisper small.en (istrain-whisper stack, :9000)"),
|
||||
]
|
||||
|
||||
DOTS = {"running": ("active", "listening"), "restarting": ("failed", "stalled"),
|
||||
"paused": ("inactive", "held"), "exited": ("inactive", "offline")}
|
||||
|
||||
def main():
|
||||
out = subprocess.run(["docker", "ps", "-a", "--format", "{{.Names}}\t{{.State}}\t{{.Status}}"],
|
||||
capture_output=True, text=True).stdout
|
||||
have = {}
|
||||
for ln in out.splitlines():
|
||||
p = ln.split("\t")
|
||||
if len(p) >= 3:
|
||||
have[p[0]] = (p[1], p[2])
|
||||
units = []
|
||||
for cid, name, job in ROLES:
|
||||
state, status = have.get(cid, ("absent", "no container"))
|
||||
st, dot = DOTS.get(state, ("inactive", "offline"))
|
||||
units.append({"id": cid, "name": name, "kind": "service", "job": job,
|
||||
"dot": dot, "state": st, "detail": status, "since": "", "can_ctl": False})
|
||||
snap = {"units": units, "ts": time.time(), "controls": False}
|
||||
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(OUT))
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(snap, f)
|
||||
os.replace(tmp, OUT)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user