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>
144 lines
6.0 KiB
YAML
144 lines
6.0 KiB
YAML
# 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"]
|