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,143 @@
|
||||
# istrain — THE MILL stack (parallel stand-up started 2026-07-18; MILL-MIGRATION.md is the tracker).
|
||||
# Deployed by mill/deploy.sh into the Dockge stack dir (stacks/istrain) — Dockge owns the lifecycle,
|
||||
# the repo owns the truth. Web runs from day one on dual-pushed data (istrain-push-mill on the VM).
|
||||
#
|
||||
# airband + worker sit behind the "radio" profile until dongle day:
|
||||
# docker compose --profile radio up -d ← run this when 1002 is plugged into the Mill
|
||||
# (plain `up -d` starts web only; `--profile radio build` pre-builds the radio images now.)
|
||||
#
|
||||
# ONE CONF HOME: the capture dir mounts at /tmp/airband inside airband+worker, so the VM's canonical
|
||||
# airband/istrain.conf and transcribe-worker defaults run VERBATIM — no Mill fork of the channel plan.
|
||||
# Clips land in ./data/captures = web's CAPTURES_DIR, so the captures panel reads Mill-captured clips
|
||||
# the same way it reads VM-pushed ones. (Unpruned local growth: a DATA.md question for dongle day.)
|
||||
services:
|
||||
web:
|
||||
build: { context: ., dockerfile: web.Dockerfile }
|
||||
image: istrain-web:mill
|
||||
container_name: istrain-web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3456:3456"
|
||||
volumes:
|
||||
- ./app:/app:ro
|
||||
- ./data:/data
|
||||
environment:
|
||||
- BIND=0.0.0.0
|
||||
- PORT=3456
|
||||
# NO PUSH_KEY (security pass 2026-07-24): the Mill AUTHORS its own data (push retired 07-18),
|
||||
# so /api/push + /api/push-clip must stay DISABLED — an unset key makes both return 403. Setting
|
||||
# a key here would re-open a public, key-in-repo data-overwrite path behind the Cloudflare name.
|
||||
- EOT_LOG=/data/eot.jsonl
|
||||
- BOT_LOG=/data/bot.jsonl
|
||||
- MID_LOG=/data/midtrain.jsonl
|
||||
- HEARD_LOG=/data/heard.jsonl
|
||||
- LEVEL_LOG=/data/level.json
|
||||
- SUPERVISOR_SNAPSHOT=/data/supervisor.json
|
||||
- HITS_LOG=/data/hits.json
|
||||
- CAPTURES_DIR=/data/captures
|
||||
- TRANSCRIPTS_LOG=/data/transcripts.jsonl
|
||||
# NO CTL_FORWARD (2026-07-18): the old target was the VM dash, whose buttons now control
|
||||
# disabled units. Control is parked (403s gracefully; supervisor rows carry can_ctl=false via
|
||||
# supervisor-probe.py) until a docker control relay exists — MILL-MIGRATION.md open item.
|
||||
|
||||
airband:
|
||||
build: { context: ., dockerfile: airband.Dockerfile }
|
||||
image: istrain-airband:mill
|
||||
container_name: istrain-airband
|
||||
restart: unless-stopped
|
||||
profiles: ["radio"]
|
||||
# whole-bus bind + cgroup rule (189 = usb) so the dongle survives replug/re-enumeration without
|
||||
# a container restart; librtlsdr is built with DETACH_KERNEL_DRIVER=ON as the second line of
|
||||
# defense behind the host dvb blacklist. rtl_airband exits nonzero with no dongle — the restart
|
||||
# policy does the waiting (the 2026-07-18 boot-race lesson, baked in from day one).
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- ./conf/istrain.conf:/etc/rtl_airband/istrain.conf:ro
|
||||
- ./data/captures:/tmp/airband
|
||||
device_cgroup_rules:
|
||||
- "c 189:* rwm"
|
||||
environment:
|
||||
# the conf's localtime=true honors TZ; without it clip filenames stamp UTC (caught 2026-07-18,
|
||||
# first Mill clips named _1454xx against 10:54 local — breaks eyeball-parity with VM-era names)
|
||||
- TZ=America/New_York
|
||||
|
||||
worker:
|
||||
build: { context: ., dockerfile: worker.Dockerfile }
|
||||
image: istrain-worker:mill
|
||||
container_name: istrain-worker
|
||||
restart: unless-stopped
|
||||
profiles: ["radio"]
|
||||
volumes:
|
||||
- ./app:/app:ro
|
||||
- ./data:/data
|
||||
- ./data/captures:/tmp/airband
|
||||
environment:
|
||||
# whisper is the istrain-whisper stack on this same box; host tailnet IP reaches it (:9000)
|
||||
- MILL_ASR=http://MILL-IP:9000
|
||||
- TRANSCRIPTS_LOG=/data/transcripts.jsonl
|
||||
- TZ=America/New_York
|
||||
command: ["python3", "/app/scripts/transcribe-worker.py"]
|
||||
|
||||
trim:
|
||||
image: istrain-worker:mill
|
||||
container_name: istrain-trim
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/captures:/tmp/airband
|
||||
environment:
|
||||
# same policy as the VM era: voice audio >24h pruned AFTER heard.jsonl ingest (the script
|
||||
# aborts if the dash is unreachable — an unlogged clip can never be deleted); transcripts +
|
||||
# detections are the durable record. Keeps ./data/captures bounded on flash.
|
||||
- DASH_URL=http://web:3456
|
||||
- CAP_KEEP_HOURS=24
|
||||
- TZ=America/New_York
|
||||
command: ["sh", "-c", "while :; do python3 /app/scripts/trim-captures.py; sleep 3600; done"]
|
||||
depends_on: [web]
|
||||
|
||||
# ---- the 1001 set (profile radio1001) — start ONLY once 1001 is plugged into the Mill: ---------
|
||||
# docker compose --profile radio --profile radio1001 up -d
|
||||
# Separate profile from 1002's so a compose cycle can never point rtl_tcp at the wrong dongle
|
||||
# while only one radio has moved. The chain writes under ~/istrain (hardcoded in iq_channelize),
|
||||
# so ./data mounts at /root/istrain — the SAME files web serves; push-seeded history continues
|
||||
# in place. ⚠ istrain-push-mill on the VM must be DISABLED before first start (its 60s full-file
|
||||
# replace would fight the appends — the PUSH_SKIP lesson, now for every log).
|
||||
scanhop:
|
||||
build: { context: ., dockerfile: scanhop.Dockerfile }
|
||||
image: istrain-scanhop:mill
|
||||
container_name: istrain-scanhop
|
||||
restart: unless-stopped
|
||||
profiles: ["radio1001"]
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- ./app:/app:ro
|
||||
- ./data:/root/istrain
|
||||
device_cgroup_rules:
|
||||
- "c 189:* rwm"
|
||||
environment:
|
||||
- HOP_DWELL_452=28
|
||||
- HOP_DWELL_457=28
|
||||
- TZ=America/New_York
|
||||
|
||||
eot-decode:
|
||||
image: istrain-worker:mill
|
||||
container_name: istrain-eot-decode
|
||||
restart: unless-stopped
|
||||
profiles: ["radio1001"]
|
||||
volumes:
|
||||
- ./app:/app:ro
|
||||
- ./data:/root/istrain
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
command: ["python3", "/app/scripts/eot-decode.py"]
|
||||
|
||||
bot-recover:
|
||||
image: istrain-worker:mill
|
||||
container_name: istrain-bot-recover
|
||||
restart: unless-stopped
|
||||
profiles: ["radio1001"]
|
||||
volumes:
|
||||
- ./app:/app:ro
|
||||
- ./data:/root/istrain
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
command: ["python3", "/app/scripts/bot-recover.py"]
|
||||
Reference in New Issue
Block a user