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>
49 lines
2.7 KiB
Python
49 lines
2.7 KiB
Python
#!/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()
|