Plan for the settings page on NA Ingest (every setting editable, what runs where, multi-campus), built with a plan-and-audit workflow and consolidated against a decisions list; the audit's 157 findings and their dispositions; the 544-item inventory of everything configurable today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
227 KiB
227 KiB
Status Panel: inventory of everything configurable (2026-10-13)
Every setting, hard-coded value, installed piece, Companion-side object, secret and workflow step the system has today: 544 items in four lenses, gathered by four readers (the "what runs where" reader checked NA Ingest live, read-only). Each line: [kind] area :: name @ where | how it is changed today | values/validation | what another campus would change | whether it needs to be editable. Used by the settings-page plan (settings-plan.md) and its coverage table (settings-plan-details.md).
Lens: Operator workflow (every step a person does today to change something) and the Companion-side state the panel depends on (what another campus must recreate inside Companion)
- [workflow-step] workflow: edit :: Edit config.json in the repo on the MacBook (source of truth) @ /Users/christian/Documents/Status Indicators/config.json (README.md:139) | today: Open config.json in a text editor; every tile/check/fix/host/window change starts here. Nothing on NA Ingest is edited directly. | JSON only; no schema file; typos are caught only by install.sh's json.load or by load_config (unknown check type, unknown member/fix/host, duplicate tile id, restart key with a tile's id, auto_fix fix that pkill/taskkill/killall's without restart_ok). | other campus: Another campus needs its own config.json (and its own repo or branch); today there is one file for NA only.
- [workflow-step] workflow: edit :: Validate config.json before deploying (load_config) @ status_panel.py:2234-2276 load_config; .claude/settings.local.json allowlist shows the command | today: /usr/bin/python3 -c "import status_panel as sp; sp.load_config('config.json')" from the repo folder (also runs implicitly in make_page.py and in the tests' CONFIG = sp.load_config(...)). | Raises ValueError on: duplicate tile id; unknown check type (not in CHECKS); member not a tile id; check 'fix' not in fixes (except restart-connection / dante-auto); ssh fix whose host is not in hosts; remote check with unknown host; hold_always not in fixes; auto_fix fix unknown or containing pkill|taskkill|killall without restart_ok; restart key id equal to a tile id; restart key fix or watch entry unknown (watch may be 'conn:'). It does NOT check tile positions overlap (make_page.place does), connection labels exist in Companion, hosts answer, or playlist URIs. | other campus: Same validator for every site; a UI must run it (or an equivalent) before saving.
- [workflow-step] workflow: test :: Run the offline tests @ tests/test_status_panel.py (README.md:169) | today: /usr/bin/python3 -m unittest discover -s tests (from the repo folder on the MacBook; ~1771 lines, 9 test classes) | Classes: TestChecks, TestTextSize, TestBehaviour, TestFixes, TestHyperDeckProbe (FakeDeck TCP server on 9993), TestNetgearParsing, TestAgainstFakeCompanion (ThreadingHTTPServer imitating /api/connections, /api/variable, /api/custom-variable, /api/connections/:id/restart), TestPage (make_page.build/build_restart + push_page.safe_target), TestCoreMIDI (real CoreMIDI loopback). Stubs: engine.executor and poe_executor replaced with lambdas (no real fix ever runs); end-to-end test replaces poll_mdns, poll_system, poll_midi, poll_remote, poll_dante_map, start_remote_midi_streams, pops cfg['atem'], sets gateway None; AutoLog dir = tempfile; service_windows pinned to Sunday 07:00-12:30; two remote-MIDI tests run coremidi.py in a local /usr/bin/python3 subprocess. | other campus: Tests read the repo's config.json and hard-code NA facts: page 19/20, name '20\nRestart', RESTART key at [2,6], tiles[0].pos == [0,1], hd1 at row1 col1, ingest at 3/5, net at 3/4, REC at 0/2, 'ack hd1', 'force rs_tascam', ATEM connection label, 'x32' fallback check. A different layout breaks TestPage/TestFixes unless tests become config-driven. | (need not be editable)
- [workflow-step] workflow: test :: Poll once on NA Ingest without writing (status_panel.py --once) @ status_panel.py:2445-2476 run_once; :2483 arg; README.md:168 | today: ssh checklist-ingest 'cd ~/Library/Application\ Support/status-panel && /usr/bin/python3 status_panel.py --once' (prints rc tile level text, plus hidden tiles). --dry-run runs the loop but never writes to Companion. | Needs Companion at config.companion (127.0.0.1:8000) so it only works on NA Ingest; no debounce; pings run twice; MIDI errors are printed not raised; it does SSH to the other machines (poll_remote) and reads nothing from the ATEM/Dante map. | other campus: Same on any Ingest Mac.
- [workflow-step] workflow: deploy :: Copy the changed files to NA Ingest's staging folder @ README.md:142; .claude/settings.local.json (older 4-file variant) | today: scp status_panel.py coremidi.py netgear.py config.json install.sh checklist-ingest:status-panel-install/ | Staging folder ~/status-panel-install/ on NA Ingest (home of user 'Studio'). README's list omits atem_watch.js and atem-reference.xml although install.sh copies atem_watch.js unconditionally (cp fails under set -e if it is missing from the staging folder) and atem-reference.xml when present. Tests, make_page.py, push_page.py, nav_keys.py are not deployed (they run on the MacBook). | other campus: Each site has its own Ingest host alias and staging folder; the file list must be generated, not remembered.
- [external-setup] workflow: deploy :: SSH alias checklist-ingest (how the MacBook reaches NA Ingest) @ ~/.ssh/config:5-10 on the MacBook | today: Host checklist-ingest -> HostName 10.1.1.1, User Studio, IdentityFile ~/.ssh/id_ed25519_checklist_ingest, IdentitiesOnly yes, ConnectTimeout 10. Edited by hand in ~/.ssh/config. | NA Ingest also answers as na-ingest.local and 10.6.3.222 (second network). | other campus: One alias per campus Ingest Mac; the UI (if it runs ON the Ingest Mac) removes the need for this hop. | (need not be editable)
- [workflow-step] workflow: deploy :: Run the installer on NA Ingest @ install.sh:1-60; README.md:148 | today: ssh checklist-ingest 'zsh ~/status-panel-install/install.sh' | Steps: set -e; json.load config.json (refuses invalid JSON); mkdir DEST; if DEST/config.json exists and differs -> copy to DEST/config.json.bak-YYYYmmdd-HHMMSS; cp status_panel.py coremidi.py netgear.py config.json atem_watch.js (+ atem-reference.xml if present) to DEST; rewrite the LaunchAgent plist; launchctl bootout; wait up to 10 s for the old job to vanish (bootstrap too early -> error 5); launchctl bootstrap with 3 tries; prints 'installed ... log: ~/Library/Logs/status-panel.log'. No rollback of the Python files (only config is backed up); no 'is a service live right now' guard. | other campus: Same script; DEST/LABEL/log paths are hard-coded org.waterschurch names.
- [workflow-step] workflow: deploy :: install.sh --remove (stop the watchdog) @ install.sh:15-20; README.md:170 | today: ssh checklist-ingest 'zsh ~/status-panel-install/install.sh --remove' -> launchctl bootout + rm plist; files in DEST stay. | After removal every page key turns purple WATCHDOG DOWN within 15 s (sp_hb stale). | other campus: Same.
- [hardcoded] NA Ingest install :: Install folder (data dir) on NA Ingest @ install.sh:11 DEST="$HOME/Library/Application Support/status-panel" | today: Edit install.sh. Comment: App Support, not ~/Documents, because launchd-run python3 cannot read ~/Documents (TCC). | Holds status_panel.py, coremidi.py, netgear.py, atem_watch.js, config.json, config.json.bak-*, atem-reference.xml, atem-reference.json (generated), dante-map.json, service-mode.json. | other campus: Same path works on any Mac; keep it. | (need not be editable)
- [hardcoded] NA Ingest install :: LaunchAgent org.waterschurch.status-panel @ install.sh:9 LABEL; :12 PLIST=~/Library/LaunchAgents/$LABEL.plist; :30-49 plist body | today: Rewritten by install.sh every run: /usr/bin/python3 DEST/status_panel.py --data-dir DEST; RunAtLoad; KeepAlive; ThrottleInterval 10; StandardErrorPath ~/Library/Logs/status-panel.err.log; domain gui/ (must run in the logged-in GUI session: keychain, osascript to Spotify, CoreMIDI). | Restart by hand: launchctl kickstart -k gui/$(id -u)/org.waterschurch.status-panel. --config defaults to <script dir>/config.json; --log defaults to ~/Library/Logs/status-panel.log. | other campus: Label could carry the campus name but does not need to.
- [runtime-data] NA Ingest install :: Watchdog log files @ status_panel.py:2485 --log default; :2491 RotatingFileHandler 1 MB x3; install.sh:46 err log; README.md:173 | today: Read with ssh checklist-ingest 'tail ~/Library/Logs/status-panel.log' or grep HOLD|FIX|AUTO. Every fault/clear, ACK, HOLD, FIX, FORCE, AUTO line is timestamped. | ~/Library/Logs/status-panel.log (+ .1 .2 .3), ~/Library/Logs/status-panel.err.log (python tracebacks), ~/Desktop/Auto-Fix Logs/ Auto-Fix.log (auto_fix.log_dir default, one per day, deleted a calendar month later). | other campus: Same.
- [runtime-data] NA Ingest install :: Runtime state files in the data dir @ status_panel.py:2384 service-mode.json; :1840 dante-map.json; :2107 atem-reference.json; install.sh:25 config.json.bak-* | today: Written by the watchdog; never edited. service-mode.json = ALARMS override + last scheduled state; dante-map.json = name->ip->mac->switch/port/shared/poe/seen (every 30 s until first success then 600 s); atem-reference.json = parsed restore XML, rewritten at every start; config.json.bak- = previous config before each differing install. | No pruning of config.json.bak-* files; no history of what changed. | other campus: Same.
- [workflow-step] workflow: pages :: Rebuild the two Companion page files (make_page.py) @ make_page.py:296-304 main; README.md:154 | today: python3 make_page.py (on the MacBook, in the repo) -> writes status-page.companionconfig and restart-page.companionconfig (gzipped JSON). Only needed when a tile/key is added, moved, relabelled (stale text), when restart_page/nav settings change, or the expressions change; watchdog-only changes need no rebuild. | python3 make_page.py writes only the status page to . Refuses two buttons on one position (ValueError 'two buttons at row r column c'). Imports load_config so a bad config stops it. Both .companionconfig files are committed to git. | other campus: Same tool; output depends only on config.json.
- [workflow-step] workflow: pages :: Import the status page into Companion (push_page.py) @ push_page.py:168-224; README.md:160-163 | today: python3 push_page.py (from the MacBook; default --host na-ingest.local --port 8000, page and expected name from config.page/page_name, file status-page.companionconfig) | Refuses when: the built file's page name != config page_name ('builds page X, not Y'); the target page is not empty-but-nav-keys and not already named the same (use --force to overwrite anyway); Companion rejects the upload ('Companion could not read the file'); another upload session is open (waits up to 40x3 s for it to expire). Needs Companion's admin port reachable on the LAN with no auth. Page name is read before/after via /api/variable/internal/page_number_N_name/value. | other campus: Host must be that campus's Ingest; page numbers come from that site's config.
- [workflow-step] workflow: pages :: Import the Restart page (push_page.py --which restart) @ push_page.py:178-181; README.md:94 | today: python3 make_page.py && python3 push_page.py --which restart (page 20, name '20\nRestart', file restart-page.companionconfig) | Same refusals as the status page. Other flags: --page N, --file F, --host, --port, --force. | other campus: Same.
- [workflow-step] workflow: pages :: Alternative: import by hand in Companion's UI @ README.md:134,157; memory companion-status-page.md:20 | today: Copy the .companionconfig to NA Ingest's Desktop (use a NEW filename: overwriting in place keeps Finder's old date), then Companion web UI -> Import/Export -> Import -> 'import into page 19' (single page). | Never a full import: a full import resets ALL custom/expression variables and pages (memory :16). Trigger import only with replaceExisting=false. | other campus: Same. | (need not be editable)
- [hardcoded] workflow: pages :: push_page's safe-target rule (page emptiness) @ push_page.py:122-159 NAV_TYPES, NAV_MARKER, real_buttons, safe_target | today: Edit push_page.py. A page is safe if its name equals the expected name, or it holds nothing but pageup/pagenum/pagedown controls or buttons whose notes contain 'STATUS nav key'. | Pages 22 and 97 are named 'PAGE' yet hold real buttons, which is why the name alone is not trusted. | other campus: Same rule everywhere. | (need not be editable)
- [companion-side] workflow: pages :: tRPC import sequence used by push_page.py @ push_page.py:195-220 (WebSocket /trpc; importExport.prepareImport.start -> uploadChunk (256 KiB, base64) -> complete (sha1, userData null) -> importExport.importSinglePage {targetPage, sourcePage 1, connectionIdRemapping {}}) | today: Internal Companion 5.0.7 API, hand-implemented over a minimal WebSocket client (stdlib only). | Written against Companion 5.0.7; a Companion upgrade can change method names or the export format. | other campus: Each campus's Companion must be a compatible version. | (need not be editable)
- [companion-side] workflow: pages :: Page export fetch used by push_page/nav_keys @ push_page.py:126-134 GET /int/export/page/; nav_keys.py:32-43 same with ?includeSecrets=false (HTTPError = no such page) | today: Not changed; internal Companion endpoint returning gzipped page JSON. | nav_keys.py walks page 1, 2, 3... until the first HTTPError, so a gap in page numbers would stop the walk early. | other campus: Same. | (need not be editable)
- [workflow-step] workflow: nav keys :: Colour the page-number key on every page (nav_keys.py) @ nav_keys.py:78-122; README.md:96-104; guideline 9 | today: python3 nav_keys.py --dry-run (list), python3 nav_keys.py (apply), python3 nav_keys.py --revert (put Companion's built-in keys back). Re-run after adding or moving pages. | Scans every page except the status page; for each nav key of a kind in config.nav_keys it tRPC controls.copyControl's from the status page's row 1 col 0 (NAV_LOCATIONS) onto that location; kinds no longer listed get controls.resetControl back to the built-in type. Needs the status page already imported (it copies from it). Raises if a copy returns false. Default --host na-ingest.local. | other campus: Page count and nav key positions differ per site; run per campus.
- [runtime-data] workflow: nav keys :: nav_locations.json (record of converted nav keys) @ nav_locations.json (97 entries); nav_keys.py:28,89,111,117,121 | today: Written by nav_keys.py after every run (page/row/col -> kind), committed to git. Today: pagenum at 1/0 on pages 1-95 and 97 (96 absent), plus an extra pagenum at 15/0/1. | It is a record only; nav_keys.py re-scans Companion each run rather than trusting it. | other campus: One per campus (lives in the repo today, so a second campus would overwrite it).
- [workflow-step] workflow: git :: Commit and push to the private Gitea repo @ git remote origin http://chronicles-ab.waterschurch.org:3000/christian/status-indicators.git; .claude/settings.local.json credential helper line; memory companion-status-page.md:11; guidelines 13,16 | today: git add -A && git commit -m '...' (ends with the Co-Authored-By line) && git push, with credential helper reading the keychain item gitea-mcp (account christian). Commits normally touch README.md, config.json, status_panel.py, tests/, and the two .companionconfig binaries. | Repo must stay private. Branch main only. The two built pages are committed, so a rebuild without a commit leaves the repo stale. | other campus: One repo today; other campuses need their own config (branch, folder, or separate repo) or a sites/ layout. | (need not be editable)
- [secret] workflow: git :: Gitea credential in the MacBook keychain @ .claude/settings.local.json (security find-generic-password -s gitea-mcp -a christian -w) | today: Stored once in the login keychain; never printed or typed. | Used only by git push on the MacBook. | other campus: Not needed on the Ingest Macs. | (need not be editable)
- [workflow-step] workflow: docs :: Update README.md with every behaviour change @ README.md (216 lines); guideline 16 | today: Hand-edit the relevant table row (Buttons / What each button checks / Restart page / Files) in the same commit. | README is the only teaching material today; it is NA-specific (names, IPs, playlist, page numbers). | other campus: A per-site generated 'what is what' page would replace most of it.
- [workflow-step] workflow: docs :: Keep 'the artifact' current @ memory status-panel-guidelines.md:31 ('keep the artifact current'); settings-interface.md mentions docs/ in the repo 'once written' | today: Unknown: no artifact URL or docs/ folder exists in the repo or the memory files I read. | Could not determine what artifact is meant or where it lives. | other campus: n/a | (need not be editable)
- [workflow-step] workflow: verify :: Verify live after a change @ memory companion-status-page.md:20; .claude/settings.local.json verification commands | today: Open http://na-ingest.local:8000/tablet?pages=19&cols=8&rows=4 (web buttons, read-only look); ssh checklist-ingest 'grep -E "HOLD|FIX" ~/Library/Logs/status-panel.log | tail'; curl http://127.0.0.1:8000/api/custom-variable/sp_state/value; POST /api/location/19/3/0/press to toggle ALARMS; curl /api/variable/audinate-dantecontroller/devices/value. | Guideline 15: test it yourself before saying done; when the user tested, read the log. | other campus: Host and page number differ.
- [workflow-step] workflow: pages :: Replace the ATEM restore file @ README.md:114; install.sh:28; status_panel.py:2109-2115 (parsed into atem-reference.json at start) | today: Save a new restore .xml from ATEM Software Control (today a copy of '4k plus 2026-10-07 14-59-55.xml' from Google Drive Backup/NA/ATEM), copy it over atem-reference.xml in the repo, scp it to the staging folder, re-run install.sh (watchdog restart re-parses it). | config.atem.reference is a path relative to the script dir or absolute; 1.6 MB XML committed to git. | other campus: Every campus has its own switcher and restore file.
- [workflow-step] workflow: Companion modules :: Build and install the quiet Tascam module @ tascam-quiet/make_quiet_tascam.py; README.md:187-194 | today: python3 make_quiet_tascam.py '~/Library/Application Support/companion/modules/tascam-da-6400-1.3.0' -> tascam-da-6400-1.3.1-quiet.tgz; then Companion -> Modules -> Import module package; Connections -> tascam -> edit -> version 1.3.1-quiet -> save. | Patches the stock 1.3.0 minified source by string replacement; a newer stock module may not match the PATCHES. | other campus: Only where a DA-6400 exists. | (need not be editable)
- [companion-side] workflow: one-time :: Create the three custom variables in Companion by hand @ README.md:130-133; memory companion-status-page.md:16; config.variables; status_panel.py:2414-2425 warnings | today: Companion web UI -> Variables -> Custom Variables -> add sp_state, sp_hb, sp_cmd (start value empty, persist off). Done once on NA. | Cannot be created through the HTTP API (POST to a missing one -> 404); the watchdog logs 'cannot set custom: (HTTP 404) - create it in Companion' once. Names are config.variables.state/heartbeat/command and must match the names baked into the page expressions by make_page.py. Whether tRPC can create them was not determined. | other campus: Must be repeated on every campus's Companion (first thing on the checklist).
- [companion-side] companion-side :: Custom variable sp_state (tile JSON) @ config.variables.state; status_panel.py:2410 set_custom; make_page.py:51-66 jsonpath reads | today: Written by the watchdog twice a second when changed (and every 10 s anyway), as compact JSON {tile: {x text, b/f on-phase colours, b2/f2 off-phase colours, s font size}} including hidden tiles and 'all'; sent in the POST body (several KB). | Page expressions: jsonpath(
(custom:sp_state), '..x') etc. Tile ids therefore must be valid jsonpath names (letters/digits/underscore). | other campus: Same mechanism. | (need not be editable) - [companion-side] companion-side :: Custom variable sp_hb (heartbeat) and the 15 s stale rule @ config.variables.heartbeat; make_page.py:34 STALE_SECONDS=15, :53 '$(internal:time_unix) - $(custom:sp_hb) > 15' | today: Watchdog writes unix seconds every 0.5 s loop; every key turns PURPLE 'WATCHDOG DOWN ERR' (master) or '\n?' when older than 15 s. | STALE_SECONDS is a constant in make_page.py (needs page rebuild + re-import to change); PURPLE 0x6A1B9A constant. | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Custom variable sp_cmd (button command queue) and its grammar @ config.variables.command; make_page.py:83-97 command_action; status_panel.py:2341-2375 handle_commands, HOLD_MAX_AGE=15 | today: Buttons append '$(custom:sp_cmd);' as plain text (not an expression: '+' is numeric in Companion 5). Watchdog reads, clears, then handles: 'ack ', 'ackall', 'service toggle|on|off', 'restart ', 'force '. Holds older than 15 s or unstamped forces are dropped. | Verbs are hard-coded in both make_page.py and status_panel.py. | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Custom variable flash_tick + trigger flash_tick_500 (the blink clock) @ README.md:135; make_page.py:9,61-62,211-212 '$(custom:flash_tick) == 1'; tests FakeCompanion store has flash_tick | today: Pre-existing on NA (not created by this project): a trigger toggling custom variable flash_tick between 0 and 1 every 500 ms; it must stay enabled or nothing blinks. | Hard-coded name 'flash_tick' in make_page.py (not in config.variables). | other campus: Another campus must create both the variable and the trigger by hand (or import a trigger with replaceExisting=false); the name should become a config key.
- [config-key] companion-side :: Status page number and name (page 19, '19\nStatus') @ config.page=19, config.page_name='19\nStatus'; make_page.py:255-266 page_export; push_page.py:182,188 | today: Edit config.json, rebuild, push; the page was also renamed once in the admin UI (memory :20). push_page refuses unless the live page already has this exact name or holds only nav keys. | int page; string name with literal backslash-n; page must exist in Companion (Companion 5 pages are created in the UI). | other campus: Every campus picks its own free page numbers; nav hold jumps to this page.
- [config-key] companion-side :: Restart page number and name (page 20, '20\nRestart') + jump/back positions @ config.restart_page.page=20, page_name, jump_pos [2,6] (on the status page), back_pos [2,6] (on the restart page); make_page.py:274-288 | today: Edit config.json; rebuild both pages; push both. The RESTART PAGE key (0x37474F) on page 19 and the STATUS PAGE key on page 20 are plain set_page buttons. | Positions are [row, col]; place() refuses overlaps. | other campus: Per site.
- [hardcoded] companion-side :: Nav key positions (UP 0/0, page number 1/0, DOWN 2/0) and the ALARMS key at 3/0 @ make_page.py:195 NAV_LOCATIONS (hard-coded); config tiles 'service' pos [3,0] | today: Edit make_page.py. Column 0 is Companion's own nav column on an 8x4 XL; tiles may not be placed on 0/0, 1/0, 2/0. | nav_controls() puts {'type': 'pageup'/'pagenum'/'pagedown'} or the coloured copy at these spots on both pages. | other campus: Same on any Stream Deck XL layout. | (need not be editable)
- [config-key] companion-side :: Which nav keys follow ALL, tap page, hold time @ config.nav_keys=['pagenum'], nav_tap_page=1, nav_hold_ms=1000; make_page.py:199-245; README.md:98-104 | today: Edit config.json; rebuild; push page 19; re-run nav_keys.py. Tap (on release) -> set_page nav_tap_page; hold nav_hold_ms -> set_page config.page. | nav_keys subset of pageup/pagenum/pagedown; tap page must be named outright (the converted key's 'startup' means the current page on surfaces set to 'use last page'). | other campus: Per site (home page number differs).
- [hardcoded] companion-side :: nav_templates.json (Companion 5.0.7's converted nav keys) @ nav_templates.json (_comment; keys pageup/pagenum/pagedown, button-layered with inc_page/set_page startup/dec_page actions); make_page.py:207 | today: Captured once from page 19 after 'convert to regular button' in Companion 5.0.7; make_page.py recolours box/text/line layers with expressions and rewrites the pagenum steps. | Version-specific layer structure; a new Companion major may need a recapture. | other campus: Same file as long as Companion versions match. | (need not be editable)
- [hardcoded] companion-side :: Export format pin: EXPORT_VERSION 12, COMPANION_BUILD '5.0.7+9763-stable-cec2f88e6f', GRID 0-8 x 0-5 @ make_page.py:31-33 | today: Edit make_page.py when Companion changes its export format. | push_page/tRPC calls and nav_templates are also 5.0.7-specific. | other campus: Every campus must run a compatible Companion 5.0.x; record each site's version. | (need not be editable)
- [hardcoded] companion-side :: Hold time 2000 ms (HOLD_MS) and runWhileHeld @ make_page.py:36 HOLD_MS; :144-146, :181-182 | today: Edit make_page.py, rebuild, push both pages. Every status tile with a fix, every group, NET, hold_always tiles and every restart key use a 2000 ms action set. | Guideline 3: holding any key 2 s always fixes. | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Button expressions and internal variables the pages rely on @ make_page.py:51-66 (jsonpath(), $(internal:time_unix), $(custom:flash_tick)); :157 $(internal:instance_errors); template string for stale text | today: Baked by make_page.py. Gotchas (memory :18): '+' is numeric; join text with template strings; single-quoted strings process escapes, template strings do not. | Layers: canvas (border decoration), box (bg expression), image (empty), text (companion-sans, fontsize expression from sp_state.s, fontsizeAllowShrink). canModifyStyleInApis true on tiles. | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Button actions used (internal module) @ make_page.py:83-103: custom_variable_set_value (connectionId internal) and set_page (surfaceId self) | today: Baked by make_page.py; ids via stable_id(sha256) so re-imports keep the same ids. | Only the internal connection is referenced; no module connections are in the export ('instances': {}). | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Button notes as machine markers @ make_page.py:140,188,196,233 ('STATUS tile', 'RESTART key', NAV_MARKER 'STATUS nav key ()'); push_page.py:123,145; nav_keys.py:46-58 | today: Written into each button's notes field; push_page and nav_keys identify our keys by these strings. | Editing a note in Companion's UI would make a key invisible to the scripts. | other campus: Same. | (need not be editable)
- [config-key] companion-side :: Companion host/port the watchdog talks to @ config.companion {host 127.0.0.1, port 8000}; status_panel.py:2290 make_companion; optional config.companion.db | today: Edit config.json. HTTP API, no auth, persistent http.client connection, 3 s timeout, one connection object per poller (4). | push_page.py/nav_keys.py do NOT read this; they default to na-ingest.local:8000 (flags --host/--port). | other campus: Same on any Ingest Mac running Companion locally; the LAN name differs.
- [companion-side] companion-side :: Companion HTTP endpoints the watchdog uses @ status_panel.py:102-138: GET /api/connections; GET /api/variable///value; GET+POST /api/custom-variable//value (POST body text/plain; '?value=' to clear); POST /api/connections//restart|disable|enable | today: Not configurable; Companion 5.0.7 HTTP API. Connection ids are resolved from labels at call time. | A 400 is returned for an empty POST body; a 404 for a missing custom variable. | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Connection status fields read for every 'conn'-type check @ status_panel.py:1428-1436 poll_connections every 1 s: label -> {id, enabled, status.category, status.message} | today: Category good/warning/error; enabled false -> grey 'DISABLED' (amber) and a hold turns it back on; 'ignore_warning': true on a check ignores category warning (ProPresenter). | Labels are the only key: renaming a connection in Companion silently breaks its check (shows OFFLINE/unknown). | other campus: Each campus must use the same labels as its config.
- [companion-side] companion-side :: Connection addresses from Companion's settings database @ status_panel.py:1552-1593 companion_db_path (newest ~/Library/Application Support/companion/vN.N/db.sqlite, or config.companion.db), companion_conn_hosts (SELECT value FROM instances; JSON label + config.host), poll_conn_hosts every 60 s | today: Opened read-only (mode=ro, 2 s timeout) only when some ping check has a 'conn'; result overrides the check's 'host' fallback (Shure receivers, Tascam). Schema errors are logged and ignored. | Companion's private schema; a Companion upgrade that renames the table or folder breaks the lookup (falls back to config hosts). | other campus: Same path on any Mac; versions folder name differs. | (need not be editable)
- [companion-side] companion-side :: Module variables polled every 1 s (fast list) @ status_panel.py:552-573 needed_variables; :1438 poll_variables; Periodic 'variables' 1.0 s | today: Derived from config: hyperdeck -> status, slotId, recordingTime; dante -> devices; tascam -> error, caution. Each is one GET /api/variable///value. | Today: 6 decks x3 + devices + error + caution = 21 GETs/s on one connection. | other campus: Same derivation. | (need not be editable)
- [companion-side] companion-side :: Module variables polled every 3 s (slow list) @ status_panel.py:560-571; Periodic 'slow-variables' 3.0 s | today: hyperdeck -> ip; shure -> ch_N_name, ch_N_battery_bars, ch_N_interference_status for N in 1..channels; psm -> ch_N_name, ch_N_rf_mute. | channels per check (Axient 4, ULXD 1, PSM 2). Variable names are the Shure/PSM module's and are hard-coded in needed_variables and the checks (:325-365). | other campus: Same. | (need not be editable)
- [companion-side] companion-side: connections :: HyperDeck_1 .. HyperDeck_6 (Blackmagic HyperDeck module) @ config tiles hd1..hd6 {type hyperdeck, conn HyperDeck_N, optional on 2 and 4, fix {conn: hyperdeck}}, hold_always 'hyperdeck'; status_panel.py:255-290, :987 ip fallback | today: Reads variables status ('record'), slotId, recordingTime (h:m:s), ip; NO DISK / LOW <30 min / FULL <5 min / NOT REC when another deck records. Hold: disable+enable the connection, verify after 15 s; probes port 9993 (hyperdeck_probe) but never reboots. | 'optional': true on a deck = grey with no alarm when it has no disk. Which decks exist is simply which hd tiles are in config and which connections exist in Companion. | other campus: A campus with decks 2 and 4 only: remove/skip the other hd tiles (and REC members) and ensure the labels match its Companion.
- [companion-side] companion-side: connections :: tascam (Tascam DA-6400 module, version 1.3.1-quiet) @ config tile tascam {type tascam conn tascam fix tascam_reconnect; ping conn tascam host 10.1.11.74}; status_panel.py:375-384 error/caution; fixes.tascam_reconnect kind reconnect verify 20 s | today: Variables error and caution (text); ping address taken from Companion's connection config (DB) else 10.1.11.74. Hold always reconnects; 'HUNG - POWER CYCLE IT' when it pings but the control port is dead. | Needs the quiet module to keep Companion's log sane. | other campus: Per site.
- [companion-side] companion-side: connections :: audinate-dantecontroller (Dante module) and its 'devices' variable @ config tile dante {type dante conn audinate-dantecontroller, devices map of 19 Dante names -> short labels, fix map by device name + '*': dante-auto}; status_panel.py:297-320, :1880 | today: Variable devices = JSON array of device names present. Config lists the 19 expected names (FOH-DiGiCo-DMI-1/2, BOH-WING, BOH-DQ-Rack, BOH-AD4Q-A/B, STUDIO-A-X32, STUDIO-A-Resi-AJA-SDI, STUDIO-A-Ingest, NA-Translation, STUDIO-B-RedNet-TNX, STUDIO-B-TB, STUDIO-B-TB-MON, STUDIO-B-MON, STUDIO-B-SOLO-MON, STUDIO-B-Tascam, NA-Tracks, FOH-SuperRack-LiveBox, NA-ProPresenter-Mac). Per-device fixes dvs_prop/dvs_tracks/dvs_translation/dvs_ingest; everything else -> PoE cycle via the live Dante map or restart the Dante module. | Device names are Dante names (renaming in Dante Controller needs a config edit; re-patching does not). | other campus: Entire list is per site.
- [companion-side] companion-side: connections :: Axient_1, Axient_2 (Shure AD4Q, 4 channels), ULXD-4 (1 channel) @ config hidden tiles axient1/axient2/ulxd {type shure conn ..., channels; ping conn ... host 10.1.1.101/.102/.106}; status_panel.py:325-360 | today: Variables ch_N_name, ch_N_battery_bars (<=1 red, <=2 amber; thresholds battery_err_bars/battery_warn_bars per check), ch_N_interference_status (only while the transmitter is on). Receivers pinged every 3 s at Companion's address for them. Members of the RF group only (no own key). | channels int; ping host fallback. | other campus: Per site RF inventory.
- [companion-side] companion-side: connections :: PSM_1 .. PSM_3 (Shure PSM1000, 2 channels) @ config hidden tiles psm1..3 {type psm conn PSM_N channels 2; ping host 10.1.1.103-.105}; status_panel.py:363 | today: Variables ch_N_name, ch_N_rf_mute ('MUTED' = alarm). | Same ping-from-Companion-address rule. | other campus: Per site.
- [companion-side] companion-side: connections :: x32 (Behringer X32) @ config tile x32 {type conn conn x32} | today: Connection status only; hold = restart-connection. | other campus: Per site.
- [companion-side] companion-side: connections :: lighting-midi (Companion's MIDI connection to the lighting port) @ config tile midi check {type conn conn lighting-midi tag 'COMPANION MIDI'} | today: Connection status only, as one line of the MIDI key. | other campus: Per site.
- [companion-side] companion-side: connections :: Propresenter-API (ProPresenter module) @ config tile apps {type conn conn Propresenter-API tag PROPRES fix propresenter ignore_warning true} | today: Status only; its periodic yellow warning is ignored; hold restarts ProPresenter on the ProP Mac over SSH then the connection. | other campus: Per site.
- [companion-side] companion-side: connections :: ableton (OSC), BibleTV (OSC) @ config tile apps checks {conn ableton tag ABLETON}, {conn BibleTV tag 'BIBLE OSC'}; restart key rs_ableton watch ['ableton','conn:ableton'] | today: Status only; ableton_restart fix has conn ableton + reconnect so Companion reconnects after the app restarts. | other campus: Per site.
- [companion-side] companion-side: connections :: ATEM (bmd-atem module) and its bundled atem-connection library @ config tile atem {conn ATEM}; config.atem {host 10.1.10.2, node glob '/Applications/Companion [0-9].app/.../node22/bin/node', library glob '~/Library/Application Support/companion/modules/bmd-atem-/node_modules/atem-connection', reference atem-reference.xml}; status_panel.py:2089-2115 newest() | today: Connection status from Companion; audio/camera compared by atem_watch.js run with Companion's own node and the ATEM module's atem-connection library (highest version number chosen by glob). If either glob finds nothing: 'ATEM checks off' warning and the atem checks never report. | Depends on the Companion app name/location and the module being installed; a Companion update that moves node or the module breaks the watcher silently (warning only). | other campus: Per site switcher IP + restore file; globs may need per-site paths.
- [companion-side] companion-side: connections :: OBS-Youtube (OBS module) @ config tile obs {type conn conn OBS-Youtube service_only true fix obs}; hosts.livestream has mac + wake_broadcast | today: Status only; service_only = idle (grey) outside the service window instead of red. Hold starts OBS over SSH (Wake-on-LAN if the PC is off) then reconnects. | service_only is the one exception to 'every key flashes red any day' and service_windows are now 24/7, so it is moot today. | other campus: Per site.
- [companion-side] companion-side: connections :: RESI_Decoder (RossTalk) and resi-studio-cues @ config tile resi checks {conn RESI_Decoder tag DECODER}, {conn resi-studio-cues}; plus pings 10.1.10.203/.202, http://10.1.10.202/, https://central.resi.io/ | today: Status only. | other campus: Per site.
- [companion-side] companion-side: connections :: VX1000 and UHDJr (NovaStar modules) @ config tile led checks {conn VX1000}, {conn UHDJr}, pings 10.1.14.116/.115 | today: Status + ping, because the NovaStar module keeps saying Connected after power-off. | other campus: Per site.
- [companion-side] companion-side: connections :: arena (Resolume Arena module) @ config tile arena {type conn conn arena fix arena}; fixes.arena reconnect true | today: Status only; hold starts Arena on the Lighting PC via its StatusPanel task then reconnects. | other campus: Per site.
- [companion-side] companion-side :: Fix kinds that act on Companion itself @ status_panel.py:542 RESTARTABLE ('conn','hyperdeck','shure','psm','tascam'); :545 'restart-connection'; :1957 kind reconnect; :1983 kind hyperdeck (disable+enable); execute_fix 'reconnect': true -> restart_connection after a successful app start; disabled connection -> enable only | today: Built in; which applies is derived from the check type and the fix entry. | 60 s cooldown per fix (run_fixes cooldown=60). | other campus: Same. | (need not be editable)
- [companion-side] companion-side :: Stream Deck surfaces and the layout grid @ README.md:10-17 (XL 8x4; MK.2 and Mini show the top-left corner); make_page.py:33 GRID; tests:1657 master at [0,1] 'where the smallest Stream Deck can see it' | today: Surfaces are configured in Companion's Surfaces tab (not by this project); tiles use rows 0-3, cols 0-7; col 0 = UP/page/DOWN + ALARMS. | Surface setting 'use last page' affects the converted page-number key (hence tap page named outright). | other campus: A campus with only an MK.2 (5x3) would need the important keys in the top-left 5x3; layout is per site.
- [companion-side] companion-side :: Web buttons / tablet URL for a screen-only view @ memory companion-status-page.md:20: http://na-ingest.local:8000/tablet?pages=19&cols=8&rows=4 | today: Open in a browser on the LAN; read-only look at the page (presses also work there). | Page number and host per site. | other campus: Per site.
- [companion-side] companion-side :: Pressing a key over the API (for tests) @ memory :20: POST http://na-ingest.local:8000/api/location/19/3/0/press (ALARMS) | today: curl -X POST ...; same for any row/col. | other campus: Per site. | (need not be editable)
- [companion-side] companion-side :: Why only single-page import is used @ memory companion-status-page.md:16 | today: A full Companion import resets ALL custom/expression variables and pages; only importSinglePage into page 19/20 and trigger import with replaceExisting=false are safe. | other campus: Same caution everywhere. | (need not be editable)
- [companion-side] companion-side :: Companion pages 22 and 97 named 'PAGE' with real buttons @ push_page.py:10; tests:1683 | today: Observed NA fact that shaped the safety rule; nothing to change. | other campus: n/a | (need not be editable)
- [config-key] config: watchdog :: service_windows (when alarms are armed) and the ALARMS key @ config.service_windows [{days all, 00:00-23:59}]; status_panel.py:696 ServiceMode; tile 'service' kind service pos [3,0] | today: Edit config.json + install. Press ALARMS to override until the next window edge (persisted in service-mode.json). | days from mon..sun; start/end 'HH:MM' with start <= hm < end. | other campus: Per site, but guideline 1 says alarms should be 24/7 everywhere.
- [config-key] config: watchdog :: auto_fix window and list @ config.auto_fix {windows [{sun 06:30-13:30}], after_seconds 20, max_tries 2, retry_seconds 90, pause_after_network_seconds 60, restart_connections true, fixes [16 gentle fix ids], log_dir default ~/Desktop/Auto-Fix Logs} | today: Edit config.json + install; load_config refuses a fix that kills a program unless restart_ok. | Today's list: hyperdeck, tascam_reconnect, midi_reconnect, rtpmidi_start, vista, arena, obs, ableton, translate, dvs_prop, dvs_tracks, dvs_translation, solo_afl, atem_audio, atem_camera (+1 truncated in my dump). | other campus: Per site service times.
- [hardcoded] config: watchdog :: Poller periods (not configurable) @ status_panel.py:2299-2330 start_pollers: connections 1 s, variables 1 s, slow-variables 3 s, conn-hosts 60 s, spotify 3 s, pings 3 s, midi 0.5 s, system 10 s, bonjour 15 s, http 5 s, remote 10 s, dante-map 30 s (600 s after first success); remote MIDI streams continuous; ATEM watcher; Art-Net listener | today: Edit status_panel.py. | debounce_seconds=3 and flap {count 3, window 600} are config keys; network burst thresholds are config.network. | other campus: Same. | (need not be editable)
- [config-key] config: watchdog :: SSH identity and options for every other machine @ config.ssh_key '~/.ssh/id_status_panel'; status_panel.py:1748-1753 ssh_cmd (BatchMode, ConnectTimeout 5, IdentitiesOnly, LogLevel ERROR, StrictHostKeyChecking accept-new, ControlMaster auto, ControlPath /tmp/status-panel-ssh-%C, ControlPersist 120) | today: Key made once on NA Ingest; public key literal is pasted inside remote-access/mac-authorize.sh and windows-setup.ps1 ('ssh-ed25519 AAAAC3...bgY status-panel@NA-Ingest'). | Exit 255 = unreachable -> ' NOT REACHABLE' or Wake-on-LAN if the host has 'mac'. | other campus: Each Ingest Mac needs its own key pair and its own copies of the setup scripts with that public key embedded.
- [config-key] config: hosts :: hosts table (who the watchdog can SSH to) @ config.hosts: prop napropresenter@10.1.14.225; tracks waterschurchmedia@10.1.3.227; translation studio@10.1.7.39; bibletv media@10.6.7.13; lighting media@10.1.10.206; studiob studiob@10.1.13.175; livestream media@10.1.9.126 + mac 6c:24:08:d7:fb:57 + wake_broadcast 10.1.255.255 | today: Edit config.json + install. Referenced by fixes (host) and remote/midi_peer checks (host). | keys: ssh 'user@ip' (required), mac (Wake-on-LAN), wake_broadcast. | other campus: Entirely per site; this is the 'which computers' list the user wants visible.
- [external-setup] external-setup: Macs :: Authorize NA Ingest's key on each Mac (mac-authorize.sh via ARD as root) @ remote-access/mac-authorize.sh; memory :25-26 | today: Apple Remote Desktop -> select Macs -> Send UNIX Command as root -> paste the script. Appends the public key to the console user's ~/.ssh/authorized_keys and prints 'OK user=... mac=... ssh-group=...'. Remote Login must be on (System Settings > Sharing). | Done 2026-10-08 on ProP Mac, Tracks Mac, NA Translation; Studio B Mac Studio later. The user runs it; never passwords. | other campus: Repeat per Mac per campus with that campus's key.
- [external-setup] external-setup: Macs :: DVS start helper on each Dante Mac (mac-dvs-helper.sh via ARD as root) @ remote-access/mac-dvs-helper.sh -> /usr/local/sbin/status-panel-dvs-start (defaults write wantToRun 1; launchctl kickstart system/com.audinate.dante.DanteVirtualSoundcard) + /etc/sudoers.d/status-panel (NOPASSWD for that one script); fixes dvs_* run 'sudo -n /usr/local/sbin/status-panel-dvs-start && open -g -a Dante Virtual Soundcard' | today: ARD Send UNIX Command as root on ProP, Tracks, Translation and NA Ingest. Undo: rm both files. | Installed 2026-10-08 on all four Macs. | other campus: Per Mac that runs DVS.
- [external-setup] external-setup: Macs :: Studio B Pro Tools solo-mode helper @ remote-access/install-solo-mode.sh + solo-mode-agent.swift -> ~/Library/Application Support/status-panel/solo-mode-agent, LaunchAgent org.waterschurch.status-panel.solo-mode, writes solo-mode.json every second, acts on solo-mode.fix; config apps checks 'PT SOLO NOT AFL' / 'PT SOLO CHECK OFF' (file older than 20 s or trusted false); fix solo_afl | today: From NA Ingest: scp the two files to studiob@10.1.13.175:/tmp/ ; ssh ... 'zsh /tmp/install-solo-mode.sh' (needs swiftc on that Mac); then allow solo-mode-agent in System Settings > Privacy & Security > Accessibility and restart the helper. | Only Mac with a compiled helper; only campus-specific thing on Studio B. | other campus: Only where Pro Tools runs.
- [external-setup] external-setup: Windows :: windows-setup.ps1 on each Windows PC (OBS/LiveStream, Lighting, Bible TV) @ remote-access/windows-setup.ps1 (sshd install/auto-start + sc failure restart; firewall rule OpenSSH-Server-In-TCP, Profile Any, RemoteAddress 10.1.1.1 and 10.6.3.222 only; key into C:\ProgramData\ssh\administrators_authorized_keys and %USERPROFILE%.ssh\authorized_keys; scheduled tasks \StatusPanel\Start OBS|Vista|Arena|rtpMIDI|BibleApp (BibleApp also at logon +15 s; RUNASADMIN apps get RunLevel Highest); rtpMIDIService failure restart; Wake-on-LAN on the NIC) | today: On the PC: PowerShell as administrator, Set-ExecutionPolicy RemoteSigned -Scope CurrentUser, run the script, paste the RESULT block back; restart and rerun if it says so. Needs the user's OK (guideline 12). | $IngestIPs and the public key are hard-coded in the script; app paths are discovered (OBS on D:, Vista on G:/E:). | other campus: Per PC per campus; edit $IngestIPs and $Key for that campus.
- [external-setup] external-setup: Windows :: Lighting PC must stay on a Public network profile @ README.md:53; fixes.rtpmidi_start note | today: Manual Windows setting; its Bonjour firewall rules only allow Public; on Private it vanishes from the MIDI directory. | Nothing checks this automatically. | other campus: Same rule for any rtpMIDI PC. | (need not be editable)
- [external-setup] external-setup: Windows :: Vista autoload preference and show folder @ README.md:119: in E:\media\Documents\Vista 3.preferences -> Prop Takeover in E:\media\Documents\Vista 3\Shows (set 2026-10-08) | today: Set by hand on the Lighting PC. | other campus: Per site. | (need not be editable)
- [secret] external-setup: NA Ingest :: Netgear switch login in the login keychain @ netgear.py:4-8,22-35 (service 'netgear-status-panel', account = username, read with security find-generic-password); config.dante_switches ['10.66.1.100','10.66.1.102']; memory :33 | today: Once, in a GUI session on NA Ingest: security add-generic-password -s netgear-status-panel -a admin -w (prompts). Readable only from the GUI session (hence the watchdog runs as a gui/ LaunchAgent, not over SSH). | Missing/locked -> RuntimeError logged; Dante map and PoE cycling stop working. Never printed. | other campus: Per site switches and credentials.
- [external-setup] external-setup: NA Ingest :: Spotify Automation (AppleEvents) permission for the watchdog @ status_panel.py:1636 osascript 'tell application Spotify'; fixes.spotify_play; README.md:123 | today: macOS TCC prompt once for python3/osascript controlling Spotify in the GUI session; how it was granted on NA Ingest is not recorded. | If denied, poll_spotify returns state None with an error string (SPOTIFY key shows the error). The playlist list is fetched from open.spotify.com embed pages + spclient (anonymous token) hourly. | other campus: Per Ingest Mac; playlist URI per site.
- [external-setup] external-setup: NA Ingest :: Network MIDI sessions on NA Ingest ('NA Ingest', optional 'NA Ingest Listen') @ config midi checks session 'NA Ingest' peers 'Lighting PC' and 'SuperRack-LiveBox'; midi_rx source 'Network NA Ingest Listen'; README.md:175-185; coremidi.py sessions()/connect_peer() | today: Audio MIDI Setup -> MIDI Studio -> Network: sessions and peers are created by hand once; the watchdog only reads them (ctypes CoreMIDI) and re-adds a dropped peer at its last address or the check's 'address'. | Session and peer names must match exactly; Bonjour instance names ('Lighting PC', 'NA Tracks') for mdns checks likewise. | other campus: Per site session/peer names and addresses.
- [external-setup] external-setup: other Macs :: Remote MIDI sessions read over SSH ('NA Tracks' on the Tracks Mac, 'NA ProP' on the ProP Mac) @ config midi_peer checks with host tracks/prop; status_panel.py:1668-1746 (coremidi.py sent over stdin to /usr/bin/python3 on that Mac; one long-lived ssh stream per host) | today: Nothing installed on those Macs; needs only SSH + /usr/bin/python3. Sessions themselves are set up by hand in their Audio MIDI Setup. | Stream shows a drop within ~1-2 s; no verdict while the stream is down. | other campus: Per site.
- [external-setup] external-setup: NA Ingest :: Services the INGEST key watches (must exist as LaunchAgents/apps on the Ingest Mac) @ config ingest checks: launchd org.waterschurch.resi-propresenter (BRIDGE), org.waterschurch.lobby-captions (CAPTIONS), org.waterschurch.lobby-captions-display (CAPTION SCREEN); http 127.0.0.1:24480/api/v1/ping (ProdCom); process /Applications/ProdDash.app/... + http 127.0.0.1:24500/ (ProdDash); disk / warn 25 GB err 8 GB; fixes bridge/captions/caption_screen/prodcom/proddash/dvs_ingest kind local | today: Those agents/apps are installed separately (not by this repo); Church Translate's startup item on NA Ingest was moved to ~/Library/LaunchAgents.disabled/ (memory :25). | other campus: Per site set of local services.
- [external-setup] external-setup: NA Ingest :: ATEM Software Control on NA Ingest (midiEnabled pref) @ config atem check {type pref domain 'com.blackmagic-design.ATEM Software Control' key midiEnabled want '0'}; fix atem_midi kind local restart_ok (quits/reopens the app) | today: App installed by hand; the watchdog reads 'defaults read' every 10 s. | other campus: Per site.
- [config-key] config: tiles :: Hidden tiles (RF members with no key) @ config tiles axient1, axient2, ulxd, psm1, psm2, psm3 have no 'pos'; make_page skips them; run_once prints them as 'hidden' | today: Edit config.json; they show only through the RF group key. | Any tile without pos is allowed; must still be a member somewhere to be visible. | other campus: Per site.
- [config-key] config: tiles :: Group keys and their members @ config tiles rec/audio/rf/show/video/stream with 'members' lists | today: Edit config.json, rebuild, push (members changes alone need only install; position/label changes need a page rebuild). | Members must be tile ids; a group shows the first problem + '+N'; hold fixes all members. | other campus: Per site.
- [config-key] config: tiles :: Restart page keys @ config.restart_page.keys: 13 keys {id rs_*, label, pos, fix, optional watch [fix ids or 'conn:']} | today: Edit config.json, python3 make_page.py, python3 push_page.py --which restart, install (watchdog reads the keys for FORCE and the flashing 'watch'). | id must not collide with tile ids; fix must exist; watch entries must be fix ids or conn:. | other campus: Per site.
- [config-key] config: fixes :: Fix catalogue (26 entries) and their kinds @ config.fixes; status_panel.py:1921-1990 execute_fix; tests:1355 valid kinds ssh/local/hyperdeck/midi_connect/reconnect/atem (netgear_poe exists in code but is not in the test's list) | today: Edit config.json + install. Keys: kind (default ssh), host, run (shell: zsh on Macs, cmd.exe on Windows), short, label, conn, reconnect, timeout (60 default), restart_ok, verify_seconds, hung_note, part. | 'run' strings are long shell one-liners (ProPresenter newest version, Ableton last set from Live's Log.txt, Windows schtasks /Run \StatusPanel\Start X, etc.). | other campus: Per site, but most run strings are reusable verbatim if hosts and task names match.
- [config-key] config: watchdog :: network (NET key) settings @ config.network {interface en0, gateway 10.1.0.1, burst_connections 4, burst_groups 3, burst_window_seconds 10, remember_seconds 300} | today: Edit config.json + install. | gateway may be null (tests) to disable the gateway ping. | other campus: Per site.
- [config-key] config: watchdog :: alert_webhook (Companion unreachable 30 s) @ config.alert_webhook ''; status_panel.py:2333-2341 post_webhook | today: Edit config.json; empty = off. Posts {text} JSON (Slack-style). | URL string. | other campus: Per site.
- [hardcoded] hardcoded :: Colours and display levels @ status_panel.py:42-60 ORDER/COLORS (ok green 0x007A2E, warn amber, notice flashing amber, quiet, ack dark red, err red/black flash, off grey, idle, service blue, fixing blue, failed orange); make_page.py:35 PURPLE | today: Edit status_panel.py (no rebuild: colours travel in sp_state). | other campus: Same. | (need not be editable)
- [hardcoded] hardcoded :: Fix cooldown 60 s, hold max age 15 s, auto-fix defaults @ status_panel.py:1041 run_fixes cooldown=60; :2338 HOLD_MAX_AGE=15 | today: Edit status_panel.py. | other campus: Same. | (need not be editable)
- [workflow-step] hardcoded :: Project folder on the MacBook and its layout @ /Users/christian/Documents/Status Indicators: status_panel.py, coremidi.py, netgear.py, atem_watch.js, atem_state.js, atem-reference.xml, config.json, make_page.py, push_page.py, nav_keys.py, nav_templates.json, nav_locations.json, install.sh, status-page.companionconfig, restart-page.companionconfig, README.md, remote-access/, tascam-quiet/, tests/, .claude/settings.local.json, .gitignore (pycache, *.pyc, .DS_Store) | today: All editing happens here; NA Ingest only receives copies. | other campus: Needs a sites// split or per-campus repos. | (need not be editable)
- [companion-side] runtime-data :: Companion custom variable values are not persisted @ README.md:130 ('persist' off) | today: On Companion restart the three variables start empty; the watchdog refills within a second; keys are purple until then. | other campus: Same. | (need not be editable)
- [workflow-step] workflow: order of operations :: Full change sequence for a layout change (today) @ README.md:137-171; guideline 16 | today: 1 edit config.json; 2 run tests; 3 python3 make_page.py; 4 scp files; 5 ssh install.sh; 6 python3 push_page.py (and --which restart); 7 python3 nav_keys.py if nav settings/pages changed; 8 verify tablet + log; 9 README; 10 git commit + push; 11 memory/artifact. Watchdog-only changes: 1,2,4,5,8,9,10. | Nothing stops this during a live Sunday service except the user (guideline 10). | other campus: Per site.
Lens: Site-specific and layout-specific things that are HARD-CODED in .py/.js/.sh/.ps1/.swift files (or exist only as one-time setup outside the repo) rather than in config.json: what a second campus with similar-but-different gear would have to change by editing code today.
- [hardcoded] hardcoded: site names in status_panel.py :: "NA Ingest" in module docstring and check docstrings @ /Users/christian/Documents/Status Indicators/status_panel.py:2, :427, :449, :1701, :1749 | today: edit the .py, re-run install.sh | free text; cosmetic only | other campus: rename to the campus's Ingest Mac or make the wording generic ("this Mac") | (need not be editable)
- [hardcoded] hardcoded: site names in status_panel.py :: Auto-fix daily log header text: "(NA Ingest fixes these by itself on Sundays 06:30-13:30; ...)" @ /Users/christian/Documents/Status Indicators/status_panel.py:680-683 | today: edit AutoLog.write in status_panel.py, re-run install.sh | literal string; the window is duplicated here even though auto_fix.windows is config | other campus: machine name and window must be built from config (site name + auto_fix.windows) or the header lies at the other campus | (need not be editable)
- [hardcoded] hardcoded: site names in status_panel.py :: Auto-fix log line "NA Ingest's view of Dante or the Dante network" @ /Users/christian/Documents/Status Indicators/status_panel.py:1198-1202 | today: edit status_panel.py | literal string | other campus: use a configured site/machine name | (need not be editable)
- [hardcoded] hardcoded: site names in status_panel.py :: Webhook texts "NA Ingest: Companion is answering again." / "NA Ingest: Companion has not answered for 30 seconds." and the 30 s threshold @ /Users/christian/Documents/Status Indicators/status_panel.py:2431, :2437-2438 | today: edit status_panel.py (alert_webhook URL itself is config.json:26, empty today) | text + 30 s; payload format {"text": ...} (Slack-style) at :2338 | other campus: site name in the message; threshold and webhook URL per campus
- [hardcoded] hardcoded: logging/paths in status_panel.py :: Watchdog log path default ~/Library/Logs/status-panel.log (CLI --log) and rotation 1,000,000 bytes x 3 backups @ /Users/christian/Documents/Status Indicators/status_panel.py:2485, :2491; echoed by install.sh:60 | today: edit status_panel.py or pass --log in the LaunchAgent plist (install.sh) | path; int bytes; int count | other campus: same path works anywhere; the UI should show/tail it | (need not be editable)
- [hardcoded] hardcoded: logging/paths in status_panel.py :: Default --config = <script dir>/config.json and --data-dir = <script dir> @ /Users/christian/Documents/Status Indicators/status_panel.py:2481-2482 (install.sh:40-41 passes --data-dir) | today: CLI flags set by install.sh | paths | other campus: unchanged; a UI needs to know the live data dir (~/Library/Application Support/status-panel) | (need not be editable)
- [runtime-data] hardcoded: logging/paths in status_panel.py :: service-mode.json (ALARMS override state) in the data dir @ /Users/christian/Documents/Status Indicators/status_panel.py:2384 | today: written by the watchdog; filename fixed in code | {override, last_sched} | other campus: unchanged; UI could show current ALARMS state | (need not be editable)
- [runtime-data] hardcoded: logging/paths in status_panel.py :: dante-map.json (self-updating Dante name->ip->mac->switch/port/poe map) in the data dir @ /Users/christian/Documents/Status Indicators/status_panel.py:1839-1840, :1902-1906 | today: written by the watchdog every dante_map_seconds; filename fixed | {name: {ip, mac, switch, port, shared, poe, seen}} | other campus: unchanged mechanism; UI should display it (it is the only place the Dante wiring is visible)
- [runtime-data] hardcoded: logging/paths in status_panel.py :: atem-reference.json (generated from the restore .xml at start) in the data dir @ /Users/christian/Documents/Status Indicators/status_panel.py:2107, :2114-2115 | today: regenerated each watchdog start from atem.reference | {audio: {input/source: {mixOption, faderGain}}, camera: {cam/cat/param: [values]}} | other campus: unchanged; derived from that campus's restore file | (need not be editable)
- [config-default] hardcoded: logging/paths in status_panel.py :: Auto-fix log folder default ~/Desktop/Auto-Fix Logs, file name pattern "YYYY-MM-DD Weekday Auto-Fix.log", one-calendar-month retention @ /Users/christian/Documents/Status Indicators/status_panel.py:630, :637-663, :666, :775 | today: auto_fix.log_dir key exists in code but is not in config.json; pattern and retention are code | path (default ~/Desktop/Auto-Fix Logs); retention fixed at 1 month | other campus: other campus may want a different folder/retention; expose log_dir and retention
- [hardcoded] hardcoded: colours and display in status_panel.py :: COLORS table: 11 display levels (ok, warn, notice, quiet, ack, err, off, idle, service, fixing, failed) each with (bg,fg) for flash-on and flash-off phases @ /Users/christian/Documents/Status Indicators/status_panel.py:49-61 | today: edit the dict, re-run install.sh (no page re-import needed) | 24-bit RGB ints, e.g. ok 0x007A2E/0xFFFFFF, err 0xFF0000/0xFFFFFF then 0x000000/0xFF3030 | other campus: same by guideline (every key green/flashing red), but should be editable in one place
- [hardcoded] hardcoded: colours and display in status_panel.py :: ORDER / RANK level priority (off < ok < idle < warn < notice < quiet < ack < err) @ /Users/christian/Documents/Status Indicators/status_panel.py:45-46 | today: edit status_panel.py | ordered list | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: colours and display in status_panel.py :: Key text fitting: LINE_CAPACITY=145, LINE_HEIGHT=1.15, fit_size largest=30 smallest=12, 95% height budget @ /Users/christian/Documents/Status Indicators/status_panel.py:1331-1356 | today: edit constants | measured on Companion 5.0.7 web buttons for a Stream Deck XL key | other campus: a different surface (Mini/+/plus) or Companion build may need re-measuring | (need not be editable)
- [hardcoded] hardcoded: colours and display in status_panel.py :: Stale-watchdog size 20 and purple used by the page expressions when sp_hb is old @ /Users/christian/Documents/Status Indicators/make_page.py:35, :62, :65 | today: edit make_page.py, rebuild and re-import both pages | PURPLE 0x6A1B9A, fg 0xFFFFFF, size 20 | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: key wording in status_panel.py :: Hold-feedback texts: NOTHING TO FIX, NO AUTO FIX, JUST TRIED WAIT 1 MIN, CHECKING, FIXING, AUTO-FIXING, WAKING PC, FIX FAILED, RESTARTING, DONE, FAILED, JUST DONE WAIT 1 MIN, STARTING WAIT 1 MIN, RECORDING LEFT ALONE, NOTHING TO CLEAR, CLEARED @ /Users/christian/Documents/Status Indicators/status_panel.py:1045-1070, :1115-1145, :1269-1294 | today: edit status_panel.py | English literals, with their display seconds (4, 15, 20, 30, 60, 120, 180) | other campus: same unless the other campus wants other words/language | (need not be editable)
- [hardcoded] hardcoded: key wording in status_panel.py :: Rendered texts: ALL OK, "%d DOWN", "%d WARN", OK, OFF, ON, manual, HOLD TO FIX, HOLD 2s, "+%d" @ /Users/christian/Documents/Status Indicators/status_panel.py:1372-1421 | today: edit render() | "HOLD 2s" assumes make_page HOLD_MS=2000 | other campus: keep in sync with hold duration if it changes | (need not be editable)
- [hardcoded] hardcoded: key wording in status_panel.py :: Check fault words: NO DATA, NOT IN COMPANION, DISABLED, STARTING, WARNING, OFFLINE, NO DISK, FULL x, LOW x, NOT REC, GONE, BATT, INTERF, RF MUTE, NO PING, NOT LOADED, STOPPED, UNREACH, SPOTIFY CLOSED, NOT PLAYING, WRONG PLAYLIST, PLAYING, DISK nGB, NO SESSION, MIDI LOST, MIDI , SILENT, LINK DOWN, NO GATEWAY, n DROPPED, BLIP HH:MM, CHECK BUG, "x%d" flapping @ /Users/christian/Documents/Status Indicators/status_panel.py:200-520, :616, :803, :817-860 | today: edit each check function | English literals; Shure channel names cut to 9 chars (:326), Tascam messages to 14 (:378,:382) | other campus: unchanged; a UI glossary should list them so operators can be taught | (need not be editable)
- [hardcoded] hardcoded: key wording in status_panel.py :: short_reason mapping of Companion messages (ETIMEDOUT/TIMED OUT/TIMEOUT -> NO ANSWER, ECONNREFUSED -> REFUSED, EHOSTUNREACH/ENETUNREACH/EHOSTDOWN -> UNREACHABLE, AUTH/LOGIN/PASSWORD/401/403 -> LOGIN) @ /Users/christian/Documents/Status Indicators/status_panel.py:186-195 | today: edit the tuple | pattern -> word | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: timing in status_panel.py :: Poller intervals: connections 1 s, variables 1 s, slow-variables 3 s, conn-hosts 60 s, spotify 3 s, pings 3 s, midi 0.5 s, system 10 s, bonjour 15 s, http 5 s, remote 10 s, dante-map 30 s @ /Users/christian/Documents/Status Indicators/status_panel.py:2304-2315 | today: edit start_pollers | seconds (float) | other campus: a campus with more SSH hosts or slower links may need different intervals
- [hardcoded] hardcoded: timing in status_panel.py :: Main loop: 2.5 s start grace, redraw every 0.5 s, sp_state rewritten at least every 10 s, loop sleep 0.25 s @ /Users/christian/Documents/Status Indicators/status_panel.py:2387, :2400, :2417, :2440 | today: edit run() | seconds | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: timing in status_panel.py :: Companion HTTP timeout 3 s; connections considered fresh for 10 s @ /Users/christian/Documents/Status Indicators/status_panel.py:83, :181 | today: edit Companion.init / Snapshot.conns_fresh | seconds | other campus: unchanged | (need not be editable)
- [config-default] hardcoded: timing in status_panel.py :: Debounce overrides: network 0 s, midi_peer 1 s (per-check debounce_seconds key allowed); general debounce from config (default 3) @ /Users/christian/Documents/Status Indicators/status_panel.py:748, :882-884 | today: debounce_seconds in config.json or per check; midi/network specials are code | seconds | other campus: expose global and per-check debounce
- [hardcoded] hardcoded: timing in status_panel.py :: Remote-Mac MIDI report considered stale after 12 s (no verdict) @ /Users/christian/Documents/Status Indicators/status_panel.py:472 | today: edit check_midi_peer | 12 s | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: timing in status_panel.py :: Ping: /sbin/ping -c 1 -t 1 -q, 3 s subprocess timeout; a host alarms after 2 consecutive misses (ping check default failures=2; gateway fixed at 2) @ /Users/christian/Documents/Status Indicators/status_panel.py:396, :820, :1445-1449 | today: edit ping_once / check_ping (per-check "failures" key exists) | int misses; the -t 1 wait is macOS ping syntax | other campus: expose misses; Linux ping would need -W instead of -t
- [hardcoded] hardcoded: timing in status_panel.py :: Link check: /sbin/ifconfig <network.interface> must print "status: active" @ /Users/christian/Documents/Status Indicators/status_panel.py:1467-1472 | today: interface name is config (network.interface = en0); the method is code | interface name | other campus: en0 vs enX differs per Mac; the UI should list the Mac's interfaces to pick from
- [hardcoded] hardcoded: timing in status_panel.py :: Bonjour browse: /usr/bin/dns-sd -B _apple-midi._udp local. collected for 3 s @ /Users/christian/Documents/Status Indicators/status_panel.py:1510-1518 | today: edit poll_mdns | only RTP-MIDI instances can be an "mdns" check; other service types need code | other campus: a campus wanting to watch another Bonjour type (e.g. _dante) needs the type made configurable | (need not be editable)
- [hardcoded] hardcoded: timing in status_panel.py :: HTTP checks: HEAD request, 6 s timeout, TLS unverified, any HTTP answer = OK; default interval 30 s @ /Users/christian/Documents/Status Indicators/status_panel.py:1521-1541, :2291 | today: interval_seconds per check is config; method/timeout are code | seconds | other campus: unchanged
- [hardcoded] hardcoded: timing in status_panel.py :: Hold freshness: HOLD_MAX_AGE 15 s (older queued holds are dropped) @ /Users/christian/Documents/Status Indicators/status_panel.py:2345 | today: edit constant | 15 s | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: timing in status_panel.py :: Fix cooldown 60 s per fix identity (holds, restart keys and auto-fix share it); auto_fix throttle 5 s; Dante-wide warning suppressed 600 s @ /Users/christian/Documents/Status Indicators/status_panel.py:1041, :1074, :1161, :1197, :1219, :1256 | today: edit defaults | seconds | other campus: expose cooldown
- [config-default] hardcoded: timing in status_panel.py :: auto_fix defaults in code: after_seconds 20, max_tries 2, retry_seconds 90, pause_after_network_seconds 60, forget_seconds 600, restart_connections, fixes list, windows @ /Users/christian/Documents/Status Indicators/status_panel.py:1164-1246 (config.json:98-105 sets most; forget_seconds only in code) | today: config.json auto_fix block, re-run install.sh | ints; windows [{days,start,end}]; fixes = list of fix ids that are 'gentle' | other campus: service times and gentle-fix list differ per campus
- [config-default] hardcoded: timing in status_panel.py :: network defaults in code: burst_connections 4, burst_groups 3, burst_window_seconds 10, remember_seconds 300, gateway, interface @ /Users/christian/Documents/Status Indicators/status_panel.py:815-865 (config.json:17-24) | today: config.json network block | ints, IP, interface | other campus: gateway and interface differ; thresholds may
- [config-default] hardcoded: timing in status_panel.py :: flap defaults: count 3 within window_seconds 600 @ /Users/christian/Documents/Status Indicators/status_panel.py:749-750 (config.json:15) | today: config.json flap | ints | other campus: same
- [config-default] hardcoded: check thresholds in status_panel.py :: HyperDeck defaults: full_minutes 5, low_minutes 30, rec_sync True, disk_service_only True, optional False; variables status/slotId/recordingTime/ip from Companion's hyperdeck module @ /Users/christian/Documents/Status Indicators/status_panel.py:254-289, :558-560 | today: per-check keys in config.json; variable names are code | minutes; booleans | other campus: a campus with decks 2 and 4 only: remove hd1/3/5/6 tiles from config AND the REC group members list; thresholds per deck
- [config-default] hardcoded: check thresholds in status_panel.py :: Shure defaults: channels 1, battery_err_bars 1, battery_warn_bars 2; variables ch_N_name / ch_N_battery_bars / ch_N_interference_status (shure-wireless module) @ /Users/christian/Documents/Status Indicators/status_panel.py:324-352, :565-568 | today: per-check keys; variable names are code | ints 0-5 | other campus: channel counts per receiver differ (4/4/1 at NA)
- [config-default] hardcoded: check thresholds in status_panel.py :: PSM defaults: channels 1, muted_is_error False; variable ch_N_rf_mute == MUTED @ /Users/christian/Documents/Status Indicators/status_panel.py:355-366, :569-571 | today: per-check keys | ints; bool | other campus: channel counts differ
- [hardcoded] hardcoded: check thresholds in status_panel.py :: Tascam: variables error/caution; OK strings "no error"/"no caution" @ /Users/christian/Documents/Status Indicators/status_panel.py:369-385, :563-564 | today: edit check_tascam | tascam-da-6400 module variable names | other campus: unchanged if same recorder | (need not be editable)
- [config-default] hardcoded: check thresholds in status_panel.py :: Disk defaults: err_gb 5, warn_gb 20 @ /Users/christian/Documents/Status Indicators/status_panel.py:456-465 | today: per-check keys (config.json:216 sets 25/8) | GB ints | other campus: same
- [hardcoded] hardcoded: check thresholds in status_panel.py :: Dante check: Companion variable "devices" (JSON list) and "_ip" from the audinate-dantecontroller module; first dante check's conn is the one the map uses @ /Users/christian/Documents/Status Indicators/status_panel.py:292-321, :562, :1861-1866, :1880-1886 | today: edit code | module-specific variable names | other campus: unchanged if the same Companion Dante module is used | (need not be editable)
- [hardcoded] hardcoded: check thresholds in status_panel.py :: Art-Net listener: UDP 6454 on all interfaces with SO_REUSEPORT, magic "Art-Net\0", default max_silence_seconds 5 @ /Users/christian/Documents/Status Indicators/status_panel.py:510-520, :2184-2208 | today: source IP and max_silence per check in config; port in code | port 6454; seconds | other campus: source IP of the lighting PC differs
- [hardcoded] hardcoded: check thresholds in status_panel.py :: ATEM check: watcher report older than 30 s = no verdict; fix wait 8 s; watcher restart wait 5 s @ /Users/christian/Documents/Status Indicators/status_panel.py:2145, :2147, :2173 | today: edit code | seconds | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: check thresholds in status_panel.py :: midi_rx: "max_silence_seconds" only alarms while service mode is on; shows last-message age @ /Users/christian/Documents/Status Indicators/status_panel.py:496-507 | today: per-check key | seconds | other campus: source name differs ("Network NA Ingest Listen" is the NA session name)
- [hardcoded] hardcoded: check registry in status_panel.py :: CHECKS registry: conn, hyperdeck, dante, shure, psm, tascam, ping, launchd, process, http, disk, midi_peer, pref, spotify, mdns, midi_rx, remote, artnet, atem @ /Users/christian/Documents/Status Indicators/status_panel.py:534-539, :2181 | today: a new check type is Python code + a test | the UI's "add check" menu must offer exactly these with each one's keys | other campus: same set; a campus wanting e.g. a UniFi or OSC check needs code
- [hardcoded] hardcoded: check registry in status_panel.py :: Per-check keys the code understands: type, conn, fix (string or {sub: id, "*": id}), tag, name, msg, service_only, ignore_warning, optional, full_minutes, low_minutes, rec_sync, disk_service_only, ip, channels, battery_err_bars, battery_warn_bars, muted_is_error, host, failures, label, match, url, interval_seconds, path, warn_gb, err_gb, session, peer, address, domain, key, want, playlist, instance, source, max_silence_seconds, cmd, part, devices, count_all, debounce_seconds @ /Users/christian/Documents/Status Indicators/status_panel.py:244-531, :883, :987-999 | today: known only by reading the code | schema per check type; no JSON schema exists today | other campus: the UI must carry this schema so another campus can build checks without reading Python
- [hardcoded] hardcoded: check registry in status_panel.py :: RESTARTABLE check types (conn, hyperdeck, shure, psm, tascam) whose failing connection a hold restarts without a "fix" @ /Users/christian/Documents/Status Indicators/status_panel.py:542, :1005-1009; make_page.py:167 | today: edit tuple | tuple of type names | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: check registry in status_panel.py :: Reserved fix ids "restart-connection" and "dante-auto" @ /Users/christian/Documents/Status Indicators/status_panel.py:545, :549, :963-977 | today: edit constants | two magic strings a check's "fix" may name besides entries in "fixes" | other campus: UI fix picker must offer them
- [hardcoded] hardcoded: fix engine in status_panel.py :: Fix kinds: ssh (default), local, atem, reconnect, midi_connect, netgear_poe, hyperdeck; and their keys host, run, short, label, timeout(60), reconnect, conn, restart_ok, part, verify_seconds(20), off_seconds(2), hung_note("STILL DOWN"), switch, port, mac @ /Users/christian/Documents/Status Indicators/status_panel.py:1921-2037 | today: fixes live in config.json; the kinds and their meaning are code | enum + per-kind schema | other campus: UI "add fix" must offer these kinds; a new kind (e.g. UniFi PoE) is code
- [hardcoded] hardcoded: fix engine in status_panel.py :: "local" fixes run with /bin/zsh -c; "ssh" fixes run the raw string on the remote's default shell (cmd.exe on the Windows PCs) @ /Users/christian/Documents/Status Indicators/status_panel.py:1928 | today: edit code | shell choice | other campus: Windows fixes must be written in cmd syntax; a PowerShell-default sshd would break them | (need not be editable)
- [hardcoded] hardcoded: fix engine in status_panel.py :: ssh exit 255 handling: with hosts[h].mac -> Wake-on-LAN and WAKING; else " NOT REACHABLE" @ /Users/christian/Documents/Status Indicators/status_panel.py:1932-1942 | today: edit code; mac/wake_broadcast per host in config | MAC, broadcast IP per host | other campus: each wakeable PC's MAC and subnet broadcast
- [hardcoded] hardcoded: fix engine in status_panel.py :: Wake-on-LAN: UDP port 9, sends to wake_broadcast and 255.255.255.255 @ /Users/christian/Documents/Status Indicators/status_panel.py:1759-1769 | today: edit code | port 9 | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: fix engine in status_panel.py :: reconnect_and_verify outcome strings: STILL OFF IN COMPANION, DISABLED IN COMPANION, STILL CONNECTING, NO NETWORK CHECK POWER, default hung_note STILL DOWN @ /Users/christian/Documents/Status Indicators/status_panel.py:2012, :2019, :2031, :2035, :2037 | today: edit code (hung_note per fix is config) | literals | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: fix engine in status_panel.py :: hyperdeck_probe: control port 9993, 3 s, "transport info" protocol (present in code; no longer used by the hyperdeck fix kind) @ /Users/christian/Documents/Status Indicators/status_panel.py:1802-1834 | today: dead-ish code | port 9993 | other campus: n/a; candidate for removal or a 'probe' check type | (need not be editable)
- [hardcoded] hardcoded: fix engine in status_panel.py :: load_config validation: unknown check type/fix/host/member, duplicate ids, restart key ids, watch entries, and refusal of pkill/taskkill/killall in auto_fix fixes unless restart_ok @ /Users/christian/Documents/Status Indicators/status_panel.py:2234-2274 | today: the only validator; runs at start and in install.sh (JSON parse only) | rules | other campus: the UI must run the same validation before saving (R8)
- [hardcoded] hardcoded: tiles/layout logic in status_panel.py :: Tile keys the code understands: id, label, pos [row,col], kind (master | service | network), members [ids], checks [], hold_always (fix id); tiles without pos are hidden members @ /Users/christian/Documents/Status Indicators/status_panel.py:744-783, :1364-1423; make_page.py:151-192, :269-278 | today: config.json tiles; schema only in code | ids unique; members must be tile ids; pos must not collide (make_page.place raises) | other campus: the whole tiles list is the layout; UI needs a grid editor
- [hardcoded] hardcoded: tiles/layout logic in status_panel.py :: restart_page keys schema: page, page_name, jump_pos, back_pos, keys [{id, label, pos, fix, watch [fix ids or "conn:"]}] @ /Users/christian/Documents/Status Indicators/status_panel.py:771, :1012-1027, :1256-1296, :1367-1376; make_page.py:131-148, :281-288 | today: config.json restart_page | restart key ids must not equal tile ids | other campus: different restart keys per campus
- [hardcoded] hardcoded: tiles/layout logic in status_panel.py :: Group tiles repeat members' faults; master counts detail tiles; service kind draws ON/OFF/manual; network kind draws link/gateway/burst @ /Users/christian/Documents/Status Indicators/status_panel.py:1383-1423, :815-865 | today: code | three special kinds | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: tiles/layout logic in status_panel.py :: Service windows / auto-fix windows day names DAYS = mon..sun, times "HH:MM" local, compared as strings @ /Users/christian/Documents/Status Indicators/status_panel.py:622, :689-693, :711-715 | today: config.json service_windows / auto_fix.windows | days list; "HH:MM"; end exclusive; no overnight windows | other campus: service times differ per campus
- [hardcoded] hardcoded: OS tools in status_panel.py :: Absolute tool paths: /sbin/ping, /sbin/ifconfig, /bin/launchctl, /usr/bin/pgrep, /usr/bin/defaults, /usr/bin/dns-sd, /usr/sbin/arp, /usr/bin/ssh, /usr/bin/osascript, /bin/zsh, /usr/bin/python3 (remote) @ /Users/christian/Documents/Status Indicators/status_panel.py:1447, :1469, :1478, :1488, :1494, :1511, :1632, :1636, :1708, :1741, :1750, :1857, :1928 | today: edit code | macOS paths | other campus: unchanged on any Mac; the Ingest machine must be a Mac | (need not be editable)
- [hardcoded] hardcoded: OS tools in status_panel.py :: launchd check reads
launchctl listof the GUI user; process check uses pgrep -f ; pref check usesdefaults read <domain> <key>@ /Users/christian/Documents/Status Indicators/status_panel.py:1475-1497 | today: labels/domains per check in config | LaunchAgent labels (org.waterschurch.* at NA), process paths, defaults domains | other campus: the Ingest Mac's own helper agents (resi bridge, captions, ProdCom, ProdDash) are NA-specific; another campus has its own list - [companion-side] hardcoded: Companion API in status_panel.py :: Companion HTTP API paths: GET /api/connections, GET /api/variable///value, GET/POST /api/custom-variable//value (?value= to clear), POST /api/connections//restart|disable|enable @ /Users/christian/Documents/Status Indicators/status_panel.py:102-138 | today: code; Companion 5.0.7 API | host/port from config companion (127.0.0.1:8000 default at :2296) | other campus: same if the same Companion major version; the UI should show Companion version | (need not be editable)
- [companion-side] hardcoded: Companion API in status_panel.py :: Companion settings DB read: ~/Library/Application Support/companion/v<X.Y>/db.sqlite (newest), table instances, JSON field config.host, read-only, 2 s timeout (override key companion.db) @ /Users/christian/Documents/Status Indicators/status_panel.py:1552-1582 | today: companion.db key (not set) or code | path | other campus: Companion must run on the same Mac as the watchdog; schema may change with Companion versions | (need not be editable)
- [companion-side] hardcoded: Companion API in status_panel.py :: Custom variables sp_state / sp_hb / sp_cmd (names from config "variables") must be CREATED BY HAND in Companion; the API returns 404 for a missing one (code only logs "create it in Companion") @ /Users/christian/Documents/Status Indicators/status_panel.py:2418-2427; config.json:8 | today: Companion admin UI > Variables > Custom | three names | other campus: one-time step at every campus; the UI should detect and say which are missing
- [hardcoded] hardcoded: Companion API in status_panel.py :: Command protocol over sp_cmd: ack ; ackall; service [on|off]; restart ; force ; ';'-separated @ /Users/christian/Documents/Status Indicators/status_panel.py:2348-2376; make_page.py:83-97, :144-146, :166-182 | today: code on both sides | grammar | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: Spotify in status_panel.py :: AppleScript SPOTIFY_STATE: tell application "Spotify" ... player state, id/name/artist of current track; only when
pgrep -xq Spotify(never opens it) @ /Users/christian/Documents/Status Indicators/status_panel.py:1593-1594, :1631-1641 | today: edit code | app name "Spotify" fixed; needs macOS Automation permission python3 -> Spotify (external) | other campus: a campus using Apple Music or another player needs a different check type | (need not be editable) - [hardcoded] hardcoded: Spotify in status_panel.py :: Playlist reader: https://open.spotify.com/embed/playlist/ (NEXT_DATA, first 100 songs, anonymous token) then https://spclient.wg.spotify.com/playlist/v2/playlist/?from=0&length=1000; UA Mozilla/5.0; refresh every 3600 s and on an unknown song after 60 s; last-3-songs rule @ /Users/christian/Documents/Status Indicators/status_panel.py:1597-1616, :1629, :1643, :156, :436-444 | today: edit code | undocumented Spotify endpoints; playlist must be public | other campus: same mechanism; fragile if Spotify changes its embed page | (need not be editable)
- [config-key] hardcoded: Spotify in status_panel.py :: Spotify playlist URI appears TWICE: check "playlist" and inside the spotify_play fix's run command (play track "spotify:playlist:2uTpJVYQLFnKF1WnSOuco8"), plus the name "Christian EDM 2026" in the fix label and check name @ /Users/christian/Documents/Status Indicators/config.json:75-76, :173; README.md:92, :123 | today: edit both places in config.json by hand, re-run install.sh | spotify:playlist:<22 chars> | other campus: one field in the UI must write both the check and the fix command (or the fix should read the check's playlist)
- [config-key] hardcoded: Spotify in status_panel.py :: spotify_play fix: pgrep Spotify || open -g -a Spotify; sleep 8; osascript play track; sleep 3; verify playing @ /Users/christian/Documents/Status Indicators/config.json:76 | today: config.json fixes.spotify_play.run | shell string with embedded AppleScript and the playlist URI | other campus: template it: UI generates the run string from playlist + delays
- [hardcoded] hardcoded: ATEM in status_panel.py :: MIX_OPTIONS Off=1 On=2 AudioFollowVideo=4 and CCU_PARAMS: the 13 camera settings compared (Lens/ApertureFstop, Video/ManualWhiteBalance, Exposure, DetailLevel, SensorGainInDecibels, SensorNDFilter, ColorCorrection Lift/Gamma/Gain/Offset/Contrast/LumaMix/ColorAdjust) with category/parameter ids and value order @ /Users/christian/Documents/Status Indicators/status_panel.py:2042-2058 | today: edit the dict | which camera settings count as 'wrong' | other campus: a campus that lets operators change e.g. iris live must drop ApertureFstop; should be a checklist in the UI
- [hardcoded] hardcoded: ATEM in status_panel.py :: Restore-file parser: expects ATEM Software Control XML with FairlightAudioMixer/AudioInput/AudioSource (mixOption, faderGain dB -> x100) and CameraControl entries (device, category, parameter, fields) @ /Users/christian/Documents/Status Indicators/status_panel.py:2061-2086 | today: code | XML schema of ATEM Software Control 9.x | other campus: unchanged for any ATEM with Fairlight; a classic-audio ATEM would need another parser | (need not be editable)
- [config-key] hardcoded: ATEM in status_panel.py :: atem config keys and their Companion-version-bound globs: node "/Applications/Companion [0-9].app/Contents/Resources/node-runtimes/node22/bin/node", library "~/Library/Application Support/companion/modules/bmd-atem-/node_modules/atem-connection", reference "atem-reference.xml" (relative to script dir), host @ /Users/christian/Documents/Status Indicators/config.json:91-96; status_panel.py:2101-2115, :2089-2095 | today: config.json atem block | globs; newest version picked by numbers in the path | other campus: switcher IP differs; node22 / app-name globs break on a Companion upgrade; the UI should show which node/library resolved
- [external-setup] hardcoded: ATEM in status_panel.py :: atem-reference.xml: a copy of "4k plus 2026-10-07 14-59-55.xml" from Google Drive Backup/NA/ATEM (product "ATEM 4 M/E Constellation 4K Plus"), 1.6 MB committed to git, copied by install.sh @ /Users/christian/Documents/Status Indicators/atem-reference.xml; README.md:114; install.sh:28 | today: copy the new restore file over atem-reference.xml, re-run install.sh (watchdog rebuilds the JSON at start) | file upload | other campus: each campus has its own restore file; the UI needs an 'upload new restore file' action and shows its date/product
- [hardcoded] hardcoded: atem_watch.js :: CLI argv order <reference.json>; report every 1 s, repeat every 10 s; float tolerance 0.002; MIX labels Off/On/AFV; CCdP command name; type 128 = float; disableMultithreaded @ /Users/christian/Documents/Status Indicators/atem_watch.js:13-21, :39, :76, :96-100, :113 | today: edit the .js, re-run install.sh | constants | other campus: unchanged | (need not be editable)
- [hardcoded] hardcoded: atem_state.js :: Read-only snapshot tool: 15 s connect timeout, default 3 s CCU collection; not deployed by install.sh @ /Users/christian/Documents/Status Indicators/atem_state.js:6, :26, :39 | today: run by hand with Companion's node | argv [secs] | other campus: useful as a UI 'what does the switcher look like now' button | (need not be editable)
- [hardcoded] hardcoded: SSH in status_panel.py :: ssh_cmd options: -i <ssh_key> (default ~/.ssh/id_status_panel), BatchMode=yes, ConnectTimeout=5, IdentitiesOnly=yes, LogLevel=ERROR, StrictHostKeyChecking=accept-new, ControlMaster=auto, ControlPath=/tmp/status-panel-ssh-%C, ControlPersist=120; target string hosts[h].ssh @ /Users/christian/Documents/Status Indicators/status_panel.py:1748-1753 | today: ssh_key is config.json:28; the options are code | OpenSSH options | other campus: unchanged; the key file must exist on that campus's Ingest Mac | (need not be editable)
- [config-key] hardcoded: SSH in status_panel.py :: hosts schema: {alias: {ssh: "user@ip", mac?, wake_broadcast?}} (7 aliases at NA: prop, tracks, translation, bibletv, lighting, studiob, livestream) @ /Users/christian/Documents/Status Indicators/config.json:31-39; status_panel.py:1931-1935, :2254-2258 | today: config.json hosts | alias; user@host; MAC; broadcast | other campus: entirely per campus; these aliases are what fixes/checks/midi_peer refer to (R5 machine list)
- [hardcoded] hardcoded: SSH in status_panel.py :: Remote MIDI over SSH: coremidi.py source is sent over stdin to /usr/bin/python3 on the other Mac (streamed; ServerAliveInterval=2/CountMax=3; 0.5 s poll, 5 s keepalive print, 3 s reconnect wait; one-shot 20 s timeout; connect_peer 30 s) @ /Users/christian/Documents/Status Indicators/status_panel.py:1663-1746, :1959-1976 | today: code | requires macOS python3 + CoreMIDI on the remote Mac (Macs only; a Windows rtpMIDI host cannot be a midi_peer "host") | other campus: any Mac whose sessions are watched needs Remote Login on and the key authorized | (need not be editable)
- [hardcoded] hardcoded: SSH in status_panel.py :: remote checks batched per host every 10 s as one shell line "(cmd) >/dev/null 2>&1; echo SPn=$?" with 20 s timeout @ /Users/christian/Documents/Status Indicators/status_panel.py:1772-1799 | today: code | POSIX-sh syntax; would not work on a cmd.exe host | other campus: remote checks only for Macs/Linux today | (need not be editable)
- [hardcoded] hardcoded: Dante/Netgear in netgear.py :: SWITCHES default list ["10.66.1.100", "10.66.1.102"] used by the
lookupCLI (the watchdog passes config dante_switches) @ /Users/christian/Documents/Status Indicators/netgear.py:21, :74 | today: edit netgear.py (duplicate of config.json:30) | list of IPs | other campus: other campus's switch IPs; CLI default should read config - [secret] hardcoded: Dante/Netgear in netgear.py :: KEYCHAIN_SERVICE "netgear-status-panel" (login keychain item; account = switch username, e.g. admin) @ /Users/christian/Documents/Status Indicators/netgear.py:22-34 | today: once in Terminal on the Ingest Mac: security add-generic-password -s netgear-status-panel -a admin -w (never in files; only readable in the GUI session, so the watchdog must be a LaunchAgent) | service name fixed; username/password in keychain | other campus: one-time step per campus; the UI should show present/missing and the account name, never the password
- [hardcoded] hardcoded: Dante/Netgear in netgear.py :: Netgear M4250 CLI driver: telnet port 23, 8 s timeout, prompt regex [>#], User:/Password: prompts,
enablewith blank password,terminal length 0, commands show version / show mac-addr-table [interface X] / show poe port info X / configure / interface X / poe reset / logout @ /Users/christian/Documents/Status Indicators/netgear.py:37-68, :77-131 | today: code | vendor-specific | other campus: a campus on UniFi/Cisco/other PoE switches needs a different driver (fix kind netgear_poe and the map survey both depend on it) | (need not be editable) - [hardcoded] hardcoded: Dante/Netgear in netgear.py :: MAX_SHARED = 2 (a port with more MACs is an uplink and is never power-cycled); port format \d+/\d+ @ /Users/christian/Documents/Status Indicators/netgear.py:118, :143, :185, :201 | today: edit constant | int | other campus: same; expose as a safety setting
- [external-setup] hardcoded: Dante/Netgear in status_panel.py :: Dante map survey uses this Mac's ARP cache (
arp -an), i.e. the Ingest Mac must sit on the Dante VLAN (NA: Dante port en17, switch management via en20 10.66.1.2 per memory); those interfaces are not in config @ /Users/christian/Documents/Status Indicators/status_panel.py:1855-1858, :1869-1906; memory companion-status-page.md | today: physical/network setup; nothing to edit | interface names for display | other campus: the UI should show which interface reaches the Dante subnet and whether the switches answer - [config-default] hardcoded: Dante/Netgear in status_panel.py :: dante_map_seconds default 600; first map attempted every 30 s until success; map entries kept forever for devices that vanish @ /Users/christian/Documents/Status Indicators/status_panel.py:1875-1877, :2315 | today: dante_map_seconds key (not in config.json) | seconds | other campus: same
- [hardcoded] hardcoded: coremidi.py :: NETWORK_DRIVERS = (com.apple.AppleMIDINetworkDriver, com.apple.AppleMIDIRTPDriver) - macOS 26.5 vs 26.2 names @ /Users/christian/Documents/Status Indicators/coremidi.py:57-59 | today: edit tuple | driver bundle ids | other campus: a Mac on another macOS release may need a third name; the UI should show 'no network MIDI driver found' clearly | (need not be editable)
- [hardcoded] hardcoded: coremidi.py :: CoreMIDI client/port names "status-panel" / "status-panel in"; LIVE_STATES running/connected; session property apple.midirtp.session / dns-sd-name; clock/active-sensing (0xF8/0xFE) ignored @ /Users/christian/Documents/Status Indicators/coremidi.py:120, :151, :163, :251, :300, :330 | today: code | constants | other campus: unchanged | (need not be editable)
- [config-key] hardcoded: coremidi.py :: midi_peer identity = (host alias, session name, peer name) + address "ip:5004" or Bonjour "\tName\tlocal." form; session and peer names are the human names set in Audio MIDI Setup / rtpMIDI ("NA Ingest", "NA Tracks", "NA ProP", "Lighting PC", "SuperRack-LiveBox", "Studio B Mac Studio") @ /Users/christian/Documents/Status Indicators/config.json:161-166; coremidi.py:171-193; status_panel.py:468-487, :993-1000 | today: config.json midi checks | session (must exist on that Mac), peer, address | other campus: all names/addresses differ; the UI should list live sessions/peers read from coremidi to pick from
- [hardcoded] config: layout in make_page.py :: EXPORT_VERSION 12 and COMPANION_BUILD "5.0.7+9763-stable-cec2f88e6f" written into every page export @ /Users/christian/Documents/Status Indicators/make_page.py:31-32, :257-259 | today: edit make_page.py, rebuild, re-import | must be acceptable to the campus's Companion | other campus: a campus on another Companion build may need these bumped; UI should read Companion's version and warn | (need not be editable)
- [hardcoded] config: layout in make_page.py :: GRID {minColumn 0, maxColumn 8, minRow 0, maxRow 5} in the page export (Stream Deck XL shows 8x4 of it) @ /Users/christian/Documents/Status Indicators/make_page.py:33, :260; README.md:10-16 | today: edit make_page.py | grid bounds | other campus: a campus with a different surface (Stream Deck +, Mini, two XLs) needs different bounds and a different tile map
- [hardcoded] config: layout in make_page.py :: STALE_SECONDS 15 (sp_hb older -> purple WATCHDOG DOWN) in the button expressions @ /Users/christian/Documents/Status Indicators/make_page.py:34, :53, :209 | today: edit, rebuild, re-import both pages and re-run nav_keys.py | seconds | other campus: unchanged | (need not be editable)
- [hardcoded] config: layout in make_page.py :: HOLD_MS 2000 (hold duration for restart/force) and nav hold default 1000 ms / tap page 1 @ /Users/christian/Documents/Status Indicators/make_page.py:36, :144-146, :181-182, :199; config.json:6-7 (nav_hold_ms, nav_tap_page) | today: HOLD_MS is code (rebuild + re-import); nav values are config | ms | other campus: same by guideline; if changed, status_panel's "HOLD 2s" text must follow
- [companion-side] config: layout in make_page.py :: Stale texts "WATCHDOG\nDOWN\n<instance_errors> ERR" (master) and "\n?" (others); uses $(internal:instance_errors) and $(custom:flash_tick) @ /Users/christian/Documents/Status Indicators/make_page.py:9, :61-62, :157, :161 | today: code; flash_tick custom variable + its 500 ms trigger must already exist in Companion | flash_tick must be created by hand at a new campus (trigger "flash_tick_500") | other campus: one-time Companion step; UI should detect a missing flash_tick
- [hardcoded] config: layout in make_page.py :: Button style: font companion-sans, outlineColor 4278190080, decoration border, fontsizeAllowShrink, per-key size from sp_state 's' @ /Users/christian/Documents/Status Indicators/make_page.py:66-80, :107-118 | today: edit, rebuild, re-import | Companion 5 layered-button fields | other campus: unchanged | (need not be editable)
- [hardcoded] config: layout in make_page.py :: Static jump keys RESTART PAGE / STATUS PAGE: colour 0x37474F, white text, positions from restart_page.jump_pos / back_pos (both [2,6] at NA) @ /Users/christian/Documents/Status Indicators/make_page.py:274-277, :284-285; config.json:108 | today: colour/text in code; positions in config | pos; colour | other campus: positions depend on the campus's tile map
- [hardcoded] config: layout in make_page.py :: NAV_LOCATIONS pageup (0,0), pagenum (1,0), pagedown (2,0): column 0 is reserved for nav keys on both pages; NAV_MARKER "STATUS nav key" @ /Users/christian/Documents/Status Indicators/make_page.py:195-196, :238-245; push_page.py:122-123; nav_keys.py:29 | today: edit make_page.py (and the tile map must avoid those cells) | row/col tuples | other campus: Companion default; a campus that moved its nav keys needs this changed | (need not be editable)
- [hardcoded] config: layout in make_page.py :: nav_templates.json: Companion 5.0.7's converted UP / page number / DOWN keys captured from page 19 (ids like izGK69mOF_3K_vast-75-) @ /Users/christian/Documents/Status Indicators/nav_templates.json; make_page.py:207 | today: re-capture from Companion after 'convert to regular button' if the version changes | JSON blob | other campus: re-capture at a campus on a different Companion version | (need not be editable)
- [hardcoded] config: layout in make_page.py :: Hold decision per tile: hold exists only if a check is RESTARTABLE with a conn, or any check has a fix, or members, or hold_always, or kind network @ /Users/christian/Documents/Status Indicators/make_page.py:167-169 | today: code; a tile gaining/losing a hold requires rebuild + re-import | rule | other campus: UI must know when a config change needs a page re-import (R8)
- [hardcoded] config: layout in make_page.py :: Button notes text mentioning "status_panel.py on NA Ingest" and "clears itself after 5 minutes" (remember_seconds is config) @ /Users/christian/Documents/Status Indicators/make_page.py:140-141, :171, :188 | today: code | help text seen in Companion's editor | other campus: site name; '5 minutes' should come from config | (need not be editable)
- [hardcoded] config: layout in make_page.py :: Output file names status-page.companionconfig / restart-page.companionconfig (gzipped JSON, committed to git) @ /Users/christian/Documents/Status Indicators/make_page.py:298-303; push_page.py:180-184 | today: python3 make_page.py | paths | other campus: per-site outputs would collide in one repo | (need not be editable)
- [companion-side] config: layout in make_page.py :: Companion internal action ids custom_variable_set_value and set_page (surfaceId self), command appended as plain text "$(custom:sp_cmd);" @ /Users/christian/Documents/Status Indicators/make_page.py:83-103 | today: code | Companion 5 internals | other campus: unchanged | (need not be editable)
- [hardcoded] config: pages in push_page.py :: Defaults --host na-ingest.local, --port 8000; page/page_name from config (19 "19\nStatus", 20 "20\nRestart") @ /Users/christian/Documents/Status Indicators/push_page.py:172-173; config.json:3-4, :108 | today: CLI flags; config.json page numbers (page_name embeds the number so both must change together) | hostname; port; page ints | other campus: other campus's Ingest hostname and free page numbers
- [companion-side] config: pages in push_page.py :: Import mechanics: tRPC over WebSocket /trpc, importExport.prepareImport.start/uploadChunk(256 KB)/complete(sha1)/importSinglePage(sourcePage 1), 40 x 3 s retry on an open upload; GET /int/export/page/N; /api/variable/internal/page_number_N_name/value @ /Users/christian/Documents/Status Indicators/push_page.py:32, :103-134, :162-165, :195-224 | today: code; Companion 5.0.7 internal API | internal API | other campus: may break on a Companion upgrade; UI should fall back to 'import by hand' instructions | (need not be editable)
- [hardcoded] config: pages in push_page.py :: Safety rule: refuse to import unless the target page holds only nav keys or is already the page of that name (--force overrides) @ /Users/christian/Documents/Status Indicators/push_page.py:137-159, :191-193 | today: code | rule | other campus: UI must surface the refusal reason and never auto-force
- [hardcoded] config: pages in nav_keys.py :: Defaults --host na-ingest.local --port 8000; walks pages 1..N until export 404; copies from status page col 0 via controls.copyControl / controls.resetControl; ?includeSecrets=false @ /Users/christian/Documents/Status Indicators/nav_keys.py:32-33, :61-75, :81-82, :110-115 | today: CLI; run after every page move (guideline 9) | hostname; nav_keys list in config (pagenum only today) | other campus: page count and nav key positions differ
- [runtime-data] config: pages in nav_keys.py :: nav_locations.json: record of every page/row/col converted (96 entries at NA incl. the odd 15/0/1), committed to git @ /Users/christian/Documents/Status Indicators/nav_locations.json; nav_keys.py:28, :89, :121 | today: written by nav_keys.py | {"page/row/col": kind} | other campus: site-specific; must not be shared between campuses in one repo (UI: 'which pages were touched' view + revert button)
- [hardcoded] NA Ingest install: install.sh :: LaunchAgent label org.waterschurch.status-panel; plist ~/Library/LaunchAgents/.plist; domain gui/ @ /Users/christian/Documents/Status Indicators/install.sh:9, :12-13, :35 | today: edit install.sh | reverse-DNS label | other campus: another org/campus may want its own label; UI (R5) must show this agent's state on the Ingest Mac
- [hardcoded] NA Ingest install: install.sh :: DEST ~/Library/Application Support/status-panel (not ~/Documents: launchd python3 cannot read it); backup config.json.bak- only on install when changed @ /Users/christian/Documents/Status Indicators/install.sh:10-11, :24-26 | today: edit install.sh | path; one-shot .bak files (no history viewer) | other campus: same path; the UI's history/backups (R8) should live here
- [hardcoded] NA Ingest install: install.sh :: Deployed file list: status_panel.py coremidi.py netgear.py config.json atem_watch.js (+ atem-reference.xml if present); atem_state.js, make_page.py, push_page.py, nav_keys.py, nav_templates.json are NOT deployed @ /Users/christian/Documents/Status Indicators/install.sh:27-28 | today: edit the cp line whenever a file is added | file list | other campus: a UI server file must be added here, and page-building tools must be deployed if the UI runs on Ingest | (need not be editable)
- [hardcoded] NA Ingest install: install.sh :: Plist: /usr/bin/python3 status_panel.py --data-dir DEST; RunAtLoad; KeepAlive; ThrottleInterval 10; StandardErrorPath ~/Library/Logs/status-panel.err.log; bootout/wait-up-to-10 s/bootstrap x3 @ /Users/christian/Documents/Status Indicators/install.sh:30-58 | today: edit install.sh | launchd keys | other campus: unchanged | (need not be editable)
- [workflow-step] NA Ingest install: workflow :: Deploy path from the MacBook: scp files to checklist-ingest:status-panel-install/ then ssh checklist-ingest 'zsh ~/status-panel-install/install.sh'; the alias "checklist-ingest" lives in the user's own ~/.ssh/config (not in the repo) @ /Users/christian/Documents/Status Indicators/README.md:142, :148; .claude/settings.local.json; memory companion-status-page.md | today: by hand from the MacBook | ssh alias; staging folder name | other campus: each campus's Ingest needs its own alias; an on-Ingest UI removes the need to scp at all
- [workflow-step] NA Ingest install: workflow :: Page import workflow: python3 make_page.py -> push_page.py (--which restart) -> nav_keys.py; or by hand via Import/Export into page 19/20 (README says the file is on NA Ingest's Desktop) @ /Users/christian/Documents/Status Indicators/README.md:134, :157-163; make_page.py:19; push_page.py:12-14 | today: three commands on the MacBook against na-ingest.local | rebuild only when layout/hold-ability changed | other campus: UI 'Apply layout' button runs all three locally on Ingest
- [secret] NA Ingest install: external-setup :: NA Ingest SSH key ~/.ssh/id_status_panel (+.pub, comment status-panel@NA-Ingest) generated once by hand; config ssh_key points to it @ /Users/christian/Documents/Status Indicators/config.json:28; status_panel.py:1750; memory companion-status-page.md | today: ssh-keygen by hand on the Ingest Mac | private key never leaves Ingest; public key is copied into mac-authorize.sh / windows-setup.ps1 | other campus: new key per campus; UI should show the public key to paste and test each host
- [external-setup] NA Ingest install: external-setup :: macOS Automation permission: python3 (LaunchAgent) allowed to control Spotify (osascript) @ /Users/christian/Documents/Status Indicators/status_panel.py:1636 (implied); README/memory | today: macOS prompt once on the Ingest screen; System Settings > Privacy & Security > Automation | allowed/denied | other campus: one-time per campus; UI should test with a harmless osascript and report
- [external-setup] NA Ingest install: external-setup :: Keychain item netgear-status-panel must be stored in the Ingest login keychain (GUI session only) @ /Users/christian/Documents/Status Indicators/netgear.py:4-10 | today: security add-generic-password in Terminal on Ingest | present/absent | other campus: per campus (different switch login)
- [external-setup] NA Ingest install: external-setup :: NA Ingest's own DVS helper + sudoers (mac-dvs-helper.sh run on Ingest too) for the dvs_ingest local fix @ /Users/christian/Documents/Status Indicators/config.json:51; remote-access/mac-dvs-helper.sh:1 | today: ARD Send UNIX Command as root | installed/not | other campus: only if that campus's Ingest runs DVS
- [companion-side] NA Ingest install: external-setup :: Companion app itself on Ingest: pages 19/20 free, custom variables sp_state/sp_hb/sp_cmd/flash_tick, trigger flash_tick_500, connections with the labels config names, bmd-atem module installed, quiet Tascam module imported @ /Users/christian/Documents/Status Indicators/README.md:130-165; make_page.py:5-12; tascam-quiet/make_quiet_tascam.py:14-15 | today: Companion admin UI by hand | checklist | other campus: every item re-done per campus; UI should verify each (variables exist? labels exist? module versions?)
- [companion-side] Companion side: labels referenced by config :: Connection labels config refers to (HyperDeck_1..6, tascam, x32, audinate-dantecontroller, lighting-midi, Propresenter-API, ableton, BibleTV, ATEM, OBS-Youtube, RESI_Decoder, resi-studio-cues, VX1000, UHDJr, arena, Axient_1, Axient_2, ULXD-4, PSM_1..3) must match Companion's labels exactly (case-sensitive) @ /Users/christian/Documents/Status Indicators/config.json:135-237; status_panel.py:198-214 (NOT IN COMPANION) | today: rename in config.json to match Companion | strings | other campus: UI should offer a dropdown of live labels from GET /api/connections
- [hardcoded] Windows PC: windows-setup.ps1 :: $Key literal: NA Ingest's public key "ssh-ed25519 AAAAC3...thbgY status-panel@NA-Ingest"; the comment is also the grep marker @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:17, :52, :59 | today: edit the .ps1 before copying it to each PC | one public key | other campus: other campus's Ingest key; UI should generate this script with the right key filled in
- [hardcoded] Windows PC: windows-setup.ps1 :: $IngestIPs = 10.1.1.1, 10.6.3.222 (firewall RemoteAddress allowlist for port 22) @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:18, :46 | today: edit the .ps1 | list of IPs | other campus: that campus's Ingest addresses on every network it touches
- [hardcoded] Windows PC: windows-setup.ps1 :: App discovery list: OBS \Program Files\obs-studio\bin\64bit\obs64.exe (--disable-shutdown-check); Vista \Vista3\Vista.exe; Resolume Arena*\Arena.exe; Tobias Erichsen\rtpMIDI\rtpMIDI.exe; Bible app = first *.exe matching 'bible' (AtLogon PT15S) @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:64-82 | today: edit the .ps1 | name, exe candidates, args, at-logon flag | other campus: other PCs run other apps (e.g. ProPresenter on Windows, Chamsys); the UI should hold this app table and emit the script
- [hardcoded] Windows PC: windows-setup.ps1 :: Scheduled task folder \StatusPanel\ and names "Start OBS", "Start Vista", "Start Arena", "Start rtpMIDI", "Start BibleApp" - referenced verbatim by config fixes (schtasks /Run /TN "\StatusPanel\Start OBS" etc.) @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:102; config.json:57, :59, :61, :63, :65 | today: both files by hand | task names must agree in two places | other campus: UI should derive the fix's schtasks name from the app table
- [hardcoded] Windows PC: windows-setup.ps1 :: Elevated task when the exe has RUNASADMIN in AppCompatFlags\Layers (OBS on the LiveStream PC); otherwise Limited interactive principal; settings no time limit, IgnoreNew @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:83-108 | today: script logic | auto-detected | other campus: unchanged | (need not be editable)
- [external-setup] Windows PC: windows-setup.ps1 :: sshd: capability install, service Automatic + failure restart 5s/5s/30s; firewall rule OpenSSH-Server-In-TCP port 22 profile Any; keys written to C:\ProgramData\ssh\administrators_authorized_keys and %USERPROFILE%.ssh\authorized_keys @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:21-61 | today: run once per PC as admin (needs the user's OK: system/firewall change) | done/not per PC | other campus: per PC per campus; UI shows per-PC status (ssh reachable? tasks exist?)
- [external-setup] Windows PC: windows-setup.ps1 :: rtpMIDIService failure-restart policy and Wake-on-LAN adapter setting (BIOS WoL must be enabled by hand) @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:112-125 | today: same script | per PC | other campus: per PC
- [workflow-step] Windows PC: windows-setup.ps1 :: Manual steps: save as C:\Users<you>\windows-setup.ps1, PowerShell as admin, Set-ExecutionPolicy RemoteSigned -Scope CurrentUser, run, paste RESULT block back (pc, user, domain, sshd, nic/mac/ip, tasks) @ /Users/christian/Documents/Status Indicators/remote-access/windows-setup.ps1:1-14, :127-134 | today: by hand on each PC | RESULT block feeds hosts..ssh user and .mac | other campus: UI should accept the RESULT block and fill hosts
- [hardcoded] Mac helpers: mac-authorize.sh :: Public key literal "ssh-ed25519 AAAAC3...thbgY status-panel@NA-Ingest" appended to the console user's ~/.ssh/authorized_keys via ARD Send UNIX Command as root; prints user/ComputerName/ssh-group @ /Users/christian/Documents/Status Indicators/remote-access/mac-authorize.sh:1-5 | today: edit the key in the file, paste into ARD | key; requires Remote Login on each Mac (com.apple.access_ssh) | other campus: other campus's key; UI generates the one-liner and records the printed user name into hosts
- [external-setup] Mac helpers: mac-dvs-helper.sh :: /usr/local/sbin/status-panel-dvs-start (defaults write /Library/Preferences/com.audinate.dante.dvs wantToRun 1; launchctl kickstart -k system/com.audinate.dante.DanteVirtualSoundcard) + /etc/sudoers.d/status-panel ( NOPASSWD for that script only); target Macs listed in comment: NA ProPresenter Mac, NA Tracks, NA Translation, NA Ingest @ /Users/christian/Documents/Status Indicators/remote-access/mac-dvs-helper.sh:1-29 | today: paste into ARD as root on each DVS Mac | installed? (ls the two files over ssh) | other campus: any Mac running DVS at the other campus; path must match the dvs_* fixes' "sudo -n /usr/local/sbin/status-panel-dvs-start" in config.json:48-51
- [external-setup] Mac helpers: Studio B solo-mode agent :: install-solo-mode.sh: LABEL org.waterschurch.status-panel.solo-mode, DIR ~/Library/Application Support/status-panel, compiles with /usr/bin/swiftc (needs Xcode CLT on that Mac), scp/ssh target studiob@10.1.13.175 in the comment @ /Users/christian/Documents/Status Indicators/remote-access/install-solo-mode.sh:4-12 | today: scp + ssh by hand from Ingest | installed? agent running? (launchctl print over ssh) | other campus: only a campus with a Pro Tools room; host alias differs
- [hardcoded] Mac helpers: Studio B solo-mode agent :: solo-mode-agent.swift: bundle id com.avid.ProTools; menu path Options > Solo Mode > AFL* / Latch; state file solo-mode.json and trigger file solo-mode.fix in ~/Library/Application Support/status-panel; 1 s loop; needs Accessibility permission (prompted once on that Mac's screen) @ /Users/christian/Documents/Status Indicators/remote-access/solo-mode-agent.swift:15-17, :29, :34-41, :53-55, :86 | today: edit .swift, re-run install-solo-mode.sh | the two file paths are duplicated in config.json:69, :188-189 (remote checks + solo_afl fix) and must agree; 20 s staleness in config | other campus: menu titles differ per Pro Tools version/language; Accessibility is a per-Mac one-time step the UI should show as trusted/untrusted
- [workflow-step] tascam-quiet :: make_quiet_tascam.py: VERSION 1.3.1-quiet; stock module path ~/Library/Application Support/companion/modules/tascam-da-6400-1.3.0; two string patches anchored on the minified stock main.js; HELP.md note "Waters Church build"; output tascam-da-6400-1.3.1-quiet.tgz imported by hand in Companion > Modules @ /Users/christian/Documents/Status Indicators/tascam-quiet/make_quiet_tascam.py:9-10, :24-36, :56, :59 | today: run once, import package, pick 1.3.1-quiet on the tascam connection | module version selection is Companion-side | other campus: only a campus with a DA-6400; a newer stock module version breaks the patch anchors
- [hardcoded] .gitignore / repo hygiene :: .gitignore only ignores pycache/, *.pyc, .DS_Store: config.json, nav_locations.json, atem-reference.xml (1.6 MB), both .companionconfig outputs and .claude/settings.local.json are committed; dante-map.json / service-mode.json / .bak files exist only in the Ingest data dir @ /Users/christian/Documents/Status Indicators/.gitignore:1-3 | today: git | three patterns | other campus: one repo cannot hold two campuses' config.json/nav_locations.json/atem-reference.xml without per-site folders or branches; the UI's site identity must decide which files are per-site | (need not be editable)
- [hardcoded] hardcoded: data model gaps :: No site/campus name, Ingest hostname or machine display names anywhere in config (hosts have aliases only); machine names exist only in comments, README, button notes and log strings @ /Users/christian/Documents/Status Indicators/config.json:31-39; status_panel.py:2, :681; make_page.py:141, :188; remote-access/*:1-2 | today: n/a | needs a new top-level "site" block (name, ingest hostname, networks) and per-host display name/role/OS | other campus: this is the root of R3/R5: without it the UI cannot label machines or logs per campus
- [hardcoded] hardcoded: data model gaps :: No per-host inventory of what runs there (helpers, LaunchAgents, scheduled tasks, DVS helper, solo agent, sshd, WoL); that knowledge is spread across fixes' run strings, remote checks, the two helper scripts and the memory file @ /Users/christian/Documents/Status Indicators/config.json:41-89, :178-190; remote-access/* | today: in the user's head / README | needs hosts..{os, role, helpers:[...], tasks:[...]} plus live probes (ssh ok? task exists? agent running?) | other campus: R5 requirement; each campus's map differs
- [hardcoded] hardcoded: data model gaps :: Duplicated values that must agree but are edited separately: Spotify URI (check + fix), schtasks names (ps1 + fixes), DVS helper path (sh + fixes), solo-mode paths (swift + checks + fix), switch IPs (netgear.py + config), public key (two scripts), page number (page + page_name), 5-minute blip text (make_page + remember_seconds), window text in AutoLog header @ see the individual items above | today: by hand in each place | single source of truth per value with generated outputs | other campus: the UI should own each value once and generate scripts/commands from it
Lens: EVERY key of config.json and every key the code reads from it (status_panel.py, make_page.py, push_page.py, nav_keys.py), with type/default/validation, plus the hardcoded constants that change what those keys mean
- [config-key] config: companion :: companion.host @ config.json:2; status_panel.py:2296,1948,2000 | today: edit config.json, zsh install.sh (watchdog restarts) | string host/IP; default "127.0.0.1" (cfg["companion"].get("host")); no validation; the "companion" object itself is required (KeyError otherwise) | other campus: same value if the watchdog runs on the Companion Mac; NOTE push_page.py/nav_keys.py do NOT read it (they use --host na-ingest.local)
- [config-key] config: companion :: companion.port @ config.json:2; status_panel.py:2296,1948,2000 | today: edit config.json, zsh install.sh | int; default 8000; no validation | other campus: only if that site's Companion HTTP port differs
- [config-default] config: companion :: companion.db @ status_panel.py:1553-1560 (companion_db_path) | today: not set today; would be edit config.json, zsh install.sh | optional path (~ expanded) to Companion's db.sqlite; default = newest ~/Library/Application Support/companion/vN.N/db.sqlite; read read-only every 60 s for ping checks with "conn" and for reconnect_and_verify's ping; silently falls back to check "host" on any error | other campus: only if Companion keeps its data elsewhere
- [config-key] config: pages :: page @ config.json:3; make_page.py:242,278,284; push_page.py:182; nav_keys.py:86 | today: edit config.json, python3 make_page.py, python3 push_page.py (and --which restart), python3 nav_keys.py | int Companion page number (19); required; no validation (push_page refuses a target page that holds real buttons unless already this page) | other campus: each site picks a free page number
- [config-key] config: pages :: page_name @ config.json:4; make_page.py:278; push_page.py:182,188-190 | today: edit config.json, make_page.py, push_page.py | string, literal backslash-n for a line break on the key ("19\nStatus"); required; push_page.py refuses import if the built file's page name differs and uses it to recognise its own page | other campus: should match the page number
- [config-key] config: nav keys :: nav_keys @ config.json:5; make_page.py:240; nav_keys.py:88 | today: edit config.json, make_page.py, push_page.py, then python3 nav_keys.py (colours every page) / --revert | list of "pageup"|"pagenum"|"pagedown"; default all three when the key is absent; unknown names silently ignored; today ["pagenum"] | other campus: same
- [config-key] config: nav keys :: nav_hold_ms @ config.json:6; make_page.py:242,199 | today: edit config.json, make_page.py, push_page.py, nav_keys.py | int milliseconds; default 1000; hold on the page-number key jumps to the status page | other campus: same
- [config-key] config: nav keys :: nav_tap_page @ config.json:7; make_page.py:243,227 | today: edit config.json, make_page.py, push_page.py, nav_keys.py | int page number; default 1; tap on the page-number key (on release) goes there | other campus: site's home page
- [config-key] config: variables :: variables.state @ config.json:8; status_panel.py:2381,2418-2423; make_page.py:52,56,210 | today: edit config.json + create the custom variable in Companion by hand, make_page.py, push_page.py, install.sh | string custom-variable name ("sp_state"); required (KeyError); the variable must already exist in Companion or the watchdog logs "cannot set custom:... create it in Companion" and the page stays purple | other campus: same names fine; must be created in each Companion
- [config-key] config: variables :: variables.heartbeat @ config.json:8; status_panel.py:2424-2427; make_page.py:53,209 | today: same as variables.state | string ("sp_hb"); required; unix time written every redraw; page turns purple when older than 15 s (STALE_SECONDS) | other campus: same
- [config-key] config: variables :: variables.command @ config.json:8; status_panel.py:2395-2398; make_page.py:144,177,181 | today: same as variables.state | string ("sp_cmd"); required; buttons append "ack " / "ackall" / "service toggle" / "restart " / "force " separated by ";" | other campus: same
- [companion-side] hardcoded: Companion side :: custom variable flash_tick + trigger flash_tick_500 @ make_page.py:9,61-62,211-212 ($(custom:flash_tick) == 1) | today: exists in Companion already (1 Hz blink clock); not configurable | hardcoded name; every key's flash phase depends on it; without it keys never flash | other campus: each site's Companion needs the same trigger/variable, or make the name a config key
- [config-key] config: windows :: service_windows @ config.json:10-12; status_panel.py:752,711-715 (ServiceMode.scheduled) | today: edit config.json, zsh install.sh | list of {days,start,end}; default [] (= never scheduled on; only the ALARMS button override); today every day 00:00-23:59 | other campus: per site; guideline says alarms any day so keep all-day
- [config-key] config: windows :: window shape: days / start / end (service_windows and auto_fix.windows) @ status_panel.py:622 (DAYS), 689-693 (in_windows), 711-715 | today: edit config.json, install.sh | days: list of lowercase "mon".."sun" (anything else never matches, no error); start/end: "HH:MM" strings compared as text, inside when start <= now < end (so "23:59" excludes the last minute; no overnight windows); no validation at all | other campus: per site service times
- [config-key] config: timing :: debounce_seconds @ config.json:14; status_panel.py:748,878-884 | today: edit config.json, install.sh | number of seconds a fault must persist before it is confirmed; default 3; overridden per check by check.debounce_seconds (midi_peer defaults to 1, network checks 0) | other campus: same
- [config-key] config: timing :: flap.count @ config.json:15; status_panel.py:749,615-616 | today: edit config.json, install.sh | int; default 3; that many confirmed fault starts inside flap.window_seconds shows amber " xN" | other campus: same
- [config-key] config: timing :: flap.window_seconds @ config.json:15; status_panel.py:750,606 | today: edit config.json, install.sh | int seconds; default 600 | other campus: same
- [config-key] config: network :: network.interface @ config.json:18; status_panel.py:2302,1467-1472 | today: edit config.json, install.sh | string interface name ("en0"); default none = no LINK DOWN check; ifconfig must print "status: active" | other campus: per Mac (en0 vs en1)
- [config-key] config: network :: network.gateway @ config.json:19; status_panel.py:818-820,2281-2282 | today: edit config.json, install.sh | IP string; default none = no NO GATEWAY check; pinged every 3 s, 2 misses = NO GATEWAY (hardcoded 2) | other campus: per site subnet
- [config-key] config: network :: network.burst_connections @ config.json:20; status_panel.py:844 | today: edit config.json, install.sh | int; default 4; watched Companion connections that must drop together (within burst_window_seconds) to call it a network burst | other campus: same
- [config-key] config: network :: network.burst_groups @ config.json:21; status_panel.py:844 | today: edit config.json, install.sh | int; default 3; distinct group tiles those drops must span | other campus: same
- [config-key] config: network :: network.burst_window_seconds @ config.json:22; status_panel.py:839 | today: edit config.json, install.sh | int seconds; default 10 | other campus: same
- [config-key] config: network :: network.remember_seconds @ config.json:23; status_panel.py:857 | today: edit config.json, install.sh | int seconds; default 300; how long NET shows the flashing "BLIP HH:MM" after a burst clears (hold NET clears it early) | other campus: same
- [config-key] config: alerts :: alert_webhook @ config.json:26; status_panel.py:2334-2342,2431,2438 | today: edit config.json, install.sh | URL string; "" or absent = off; POST JSON {"text": ...} when Companion has not answered for 30 s and when it answers again; message text hardcoded "NA Ingest: ..." | other campus: per site URL; the "NA Ingest" wording needs a site-name key
- [config-key] config: ssh :: ssh_key @ config.json:28; status_panel.py:1750 | today: edit config.json, install.sh; the key itself made once (remote-access/mac-authorize.sh) | path string, ~ expanded; default "~/.ssh/id_status_panel"; used with BatchMode=yes IdentitiesOnly=yes for every ssh | other campus: same path per ingest Mac; a new key pair per site
- [config-key] config: dante :: dante_switches @ config.json:30; status_panel.py:1872,1913; netgear.py:174,193 | today: edit config.json, install.sh; switch login stored once in keychain | list of switch IP strings; absent/empty = Dante map and PoE power-cycle off (dante-auto then only restarts the Companion connection); telnet port 23 hardcoded | other campus: per site switches; netgear.py's own SWITCHES list (netgear.py:21) is only for its CLI
- [config-default] config: dante :: dante_map_seconds @ status_panel.py:1877 | today: not set; edit config.json, install.sh | int seconds between switch surveys; default 600; the poller runs every 30 s regardless | other campus: same
- [config-key] config: hosts :: hosts (map alias -> machine) @ config.json:31-39; status_panel.py:1753,1931,2255,2257 | today: edit config.json, install.sh; plus one-time ssh authorisation on that machine | object keyed by alias (prop, tracks, translation, bibletv, lighting, studiob, livestream); aliases referenced by fixes..host, remote checks' host, midi_peer checks' host | other campus: the core per-site list; every machine the panel talks to
- [config-key] config: hosts :: hosts..ssh @ config.json:32-38; status_panel.py:1753 | today: edit config.json, install.sh | "user@host" string; required for every alias; validated to exist only for ssh-kind fixes referenced by a tile check (:2254-2256) and remote checks (:2257); a midi_peer "host" or a restart-page-only ssh fix with a missing alias fails at runtime (KeyError in ssh_cmd) | other campus: per site users/IPs
- [config-key] config: hosts :: hosts..mac @ config.json:38; status_panel.py:1931-1937,1759-1769 | today: edit config.json, install.sh | MAC string (any separator); optional; when an ssh fix exits 255 a Wake-on-LAN packet is sent and the key says WAKING PC; wake_on_lan raises ValueError if not 6 bytes (fix then reports FAILED) | other campus: per PC that may be asleep
- [config-key] config: hosts :: hosts..wake_broadcast @ config.json:38; status_panel.py:1935,1767 | today: edit config.json, install.sh | IP string; optional; packet also always goes to 255.255.255.255; UDP port 9 hardcoded | other campus: per site subnet broadcast
- [config-key] config: fixes :: fixes (map id -> fix) @ config.json:41-89; status_panel.py:983,1100,1924,2252-2273 | today: edit config.json, install.sh | object keyed by fix id; ids referenced by check.fix, tile.hold_always, auto_fix.fixes, restart_page.keys[].fix and .watch; load_config checks every reference exists | other campus: largely reusable; host aliases and app names differ
- [config-key] config: fixes :: fixes..kind @ status_panel.py:1925,1927-1987 | today: edit config.json, install.sh | string; default "ssh"; allowed ssh | local | atem | reconnect | midi_connect | netgear_poe | hyperdeck; an unknown kind logs a warning and the key shows FIX FAILED; NOT validated by load_config | other campus: same
- [config-key] config: fixes :: fix kind = ssh (fields) @ status_panel.py:1927-1952 | today: edit config.json, install.sh | uses host (required, hosts alias), run (required), timeout (60), reconnect (false), conn, short, label, restart_ok; runs
ssh <hosts.alias.ssh> '<run>'in the remote login shell (cmd.exe on Windows PCs); exit 255 + hosts.mac -> WAKING, exit 255 otherwise -> " NOT REACHABLE"; exit 0 = success | other campus: same mechanics - [config-key] config: fixes :: fix kind = local (fields) @ status_panel.py:1928-1952 | today: edit config.json, install.sh | uses run (required), timeout (60), reconnect (false), conn, short, label, restart_ok; runs
/bin/zsh -c <run>on the ingest Mac as the logged-in user | other campus: same - [config-key] config: fixes :: fix kind = atem (fields) @ status_panel.py:1953-1956,2147-2164 | today: edit config.json, install.sh | uses part (required: "audio"|"camera"), short, label; sends "fix " to atem_watch.js and waits up to 8 s (hardcoded) for the switcher to match the restore file; fails if the ATEM watcher is not running | other campus: needs that site's atem section
- [config-key] config: fixes :: fix kind = reconnect (fields) @ status_panel.py:1957-1958,1993-2037 | today: edit config.json, install.sh | uses conn (required Companion label), verify_seconds (20), hung_note ("STILL DOWN"), short, label; restarts the Companion connection (or enables it if disabled), waits verify_seconds, then True if good, "STILL CONNECTING" if still starting, hung_note if the device still pings, else "NO NETWORK CHECK POWER" | other campus: same
- [config-key] config: fixes :: fix kind = midi_connect (fields) @ status_panel.py:1959-1976,993-1000,1107-1112 | today: edit config.json, install.sh | no fields of its own beyond short/label; session, peer, host and address come from the midi_peer check it is attached to; address = where the peer was last seen else check.address; local via coremidi.connect_peer, remote via ssh + /usr/bin/python3 - ; ok when result is asked|already|kicked; several run one after another | other campus: same
- [config-key] config: fixes :: fix kind = netgear_poe (fields) @ status_panel.py:1977-1982; netgear.py:110 | today: not used in config.json today; edit config.json, install.sh | uses switch (required IP), port (required "0/14" form), mac (optional expected MAC; refuses if that MAC is elsewhere); power-cycles one PoE port over telnet with the keychain login | other campus: per site switch/port
- [config-key] config: fixes :: fix kind = hyperdeck (fields) @ status_panel.py:1983-1986,981-992,1057-1063,1271-1272 | today: edit config.json, install.sh | uses verify_seconds (20), off_seconds (2), hung_note, short, label; conn and ip come from the hyperdeck check (ip: check.ip, else Companion variable "ip", else last remembered); disables then enables the Companion connection; per-deck 60 s cooldown; never runs while that deck is recording; never reboots the deck | other campus: same
- [config-key] config: fixes :: fixes..host @ status_panel.py:1928,1931,1936,1942,2254-2256 | today: edit config.json, install.sh | hosts alias string; required when kind is ssh (default kind); validated only when the fix is referenced from a tile check; fixes used only by restart_page keys or hold_always are not validated (runtime KeyError -> FIX FAILED) | other campus: per site alias
- [config-key] config: fixes :: fixes..run @ status_panel.py:1928,2264 | today: edit config.json, install.sh | shell command string; required for ssh/local; remote shell for ssh, /bin/zsh -c for local; if the fix is in auto_fix.fixes it must not contain pkill|taskkill|killall unless restart_ok is true (load_config raises); today also embeds site data: app names, LaunchAgent labels, \StatusPanel\ task names, the Spotify playlist URI, /usr/local/sbin/status-panel-dvs-start | other campus: commands change per site; the UI should expose the embedded values (playlist, labels, task names) as fields
- [config-key] config: fixes :: fixes..timeout @ config.json:81,83; status_panel.py:1929 | today: edit config.json, install.sh | seconds; default 60; ssh/local only; subprocess killed after it (fix reports FAILED) | other campus: same
- [config-key] config: fixes :: fixes..short @ config.json:42-88; status_panel.py:1102,1106,1112,1942 | today: edit config.json, install.sh | short key text; default fix id upper-cased; shown as "FIXING " / " NOT REACHABLE"; on a group button prefixed to the outcome | other campus: same
- [config-key] config: fixes :: fixes..label @ status_panel.py:1101,1253,1281,1925; make_page.py:173 | today: edit config.json, install.sh (+ make_page/push_page for hold_always notes) | plain-words description; default fix id in the watchdog (logs, auto-fix log); REQUIRED by make_page.py for any fix used as a tile's hold_always (KeyError otherwise) | other campus: wording mentions site machines ("NA Translation"); per site
- [config-key] config: fixes :: fixes..reconnect @ config.json:58,60,81; status_panel.py:1945-1951 | today: edit config.json, install.sh | bool; default false; ssh/local only: after exit 0 restart the Companion connection named by the check's conn, else fixes..conn | other campus: same
- [config-key] config: fixes :: fixes..conn @ config.json:81,85; status_panel.py:1945,1998-2001 | today: edit config.json, install.sh | Companion connection label; required for kind reconnect; fallback target for reconnect:true when the check has no conn; ignored by kind hyperdeck (uses the check's conn); not validated against Companion | other campus: per site connection labels
- [config-key] config: fixes :: fixes..restart_ok @ config.json:72; status_panel.py:2264 | today: edit config.json, install.sh | bool; default false; lets a run containing pkill/taskkill/killall be listed in auto_fix.fixes (atem_midi today) | other campus: same
- [config-key] config: fixes :: fixes..part @ config.json:70-71; status_panel.py:1954 | today: edit config.json, install.sh | "audio"|"camera"; required for kind atem; not validated (anything else just never matches) | other campus: same
- [config-key] config: fixes :: fixes..verify_seconds @ config.json:85,87; status_panel.py:2023,2030 | today: edit config.json, install.sh | seconds; default 20; reconnect and hyperdeck kinds: wait before judging the connection | other campus: same
- [config-default] config: fixes :: fixes..off_seconds @ status_panel.py:2009 | today: not set; edit config.json, install.sh | seconds; default 2; hyperdeck kind: time the connection stays disabled before re-enabling | other campus: same
- [config-key] config: fixes :: fixes..hung_note @ config.json:85,87; status_panel.py:2035 | today: edit config.json, install.sh | key text (\n allowed); default "STILL DOWN"; shown when the connection is still bad but the device answers ping | other campus: same
- [config-default] config: fixes :: fixes..switch / .port / .mac (netgear_poe) @ status_panel.py:1980 | today: not set today | switch: IP string required; port: "unit/port" string required; mac: optional MAC guard; no load-time validation | other campus: per site
- [config-key] config: fixes :: special fix id "restart-connection" (RESTART_CONNECTION) @ status_panel.py:545,972-977,1005-1009,2252 | today: use the literal string as a check's fix (or a dict value); install.sh | allowed in check.fix instead of a fixes entry; restarts the check's own Companion connection (needs check.conn); checks of type conn/hyperdeck/shure/psm/tascam (RESTARTABLE :542) get this automatically when they have no fix | other campus: same
- [config-key] config: fixes :: special fix id "dante-auto" (DANTE_AUTO) @ config.json:151; status_panel.py:549,963-971,1909-1918 | today: edit config.json, install.sh | allowed in check.fix (dante checks); if dante-map.json says the device is on a PoE port (found by MAC) its port is power-cycled, else falls back to restart-connection on the Dante connection; one PoE cycle per MAC per plan | other campus: needs dante_switches + keychain login
- [config-key] config: fixes :: check.fix as a per-sub map {sub: fix id, "": fallback} @ config.json:135,149-151; status_panel.py:961-962,1021-1022,2250-2253 | today: edit config.json, install.sh | keys are the check's sub-result names: dante = device names; hyperdeck = conn|disk|rec; shure = chN_batt|chN_rf; psm = chN_mute; tascam = conn|error|caution; "" matches every other sub; each value validated like a string fix | other campus: dante device names differ per site
- [hardcoded] config: fixes :: implicit "enable" plan for a disabled Companion connection @ status_panel.py:953-959,1083-1088 | today: not configurable | any failing check whose conn is disabled in Companion is fixed by enabling it (TURN ON), never by its fix; good to document in the UI | other campus: same | (need not be editable)
- [config-key] config: tiles :: tiles (list) @ config.json:126-238; status_panel.py:747,755-760; make_page.py:271-273 | today: edit config.json, make_page.py, push_page.py, install.sh | ordered list of tile objects; order matters only for rendering/iteration; today 7 groups + 1 master + service + network + 25 detail tiles (6 hidden RF tiles) | other campus: the main per-site layout
- [config-key] config: tiles :: tiles[].id @ status_panel.py:2238-2241,2267; make_page.py:56-65 (jsonpath
.<id>.x) | today: edit config.json, make_page.py, push_page.py, install.sh | string; required; unique among tiles and not equal to any restart key id (validated); used as a JSON key in sp_state and inside Companion jsonpath expressions, so it must be a plain identifier (letters/digits/_) - NOT validated; the master tile's id must be "all" (make_page.py:210 nav keys read.all.*, status_panel.py:2357 ackall acks "all") | other campus: same - [config-key] config: tiles :: tiles[].label @ status_panel.py:886,1381,1385,1407-1421; make_page.py:154 | today: edit config.json, make_page.py, push_page.py, install.sh | string; required; first line of the key, prefix of fault messages ("VISTA SILENT"), name in logs; keep short (fit_size shrinks text to 12%) | other campus: same
- [config-key] config: tiles :: tiles[].pos @ config.json:127-224; make_page.py:272,248-252; status_panel.py:2466-2475 | today: edit config.json, make_page.py, push_page.py | [row, col] ints; optional - a tile without pos is hidden and only shows through a group's members (axient1.., psm1..); make_page raises on two buttons at one position; grid is GRID minColumn 0..8, minRow 0..5 (make_page.py:33); column 0 rows 0-2 are the nav keys; restart_page.jump_pos also occupies a slot; no range validation | other campus: per site Stream Deck layout
- [config-key] config: tiles :: tiles[].kind @ config.json:127,192,208; status_panel.py:912,1042,1383,1388; make_page.py:155-171 | today: edit config.json, make_page.py, push_page.py, install.sh | absent (normal/group) | "master" (ALL: press = ackall, shows ALL OK / N DOWN, no hold) | "service" (ALARMS toggle, press = service toggle) | "network" (NET: link/gateway/burst, hold clears a blip); not validated; exactly one of each expected | other campus: same
- [config-key] config: tiles :: tiles[].members @ config.json:128-133; status_panel.py:756,914,942,1402,2246-2248 | today: edit config.json, make_page.py, push_page.py, install.sh | list of tile ids; validated to exist; makes a group button (worst member, "+N"); hold fixes every member; a tile may be in several groups (first match wins for burst grouping) | other campus: per site
- [config-key] config: tiles :: tiles[].checks @ config.json:135-237; status_panel.py:789-810,2242-2244 | today: edit config.json, install.sh (make_page/push_page only if the hold-ability changes) | list of check objects; each type must be in CHECKS (19 types); a tile with checks or kind network is a "detail" tile counted by ALL; a check that throws shows CHECK BUG | other campus: per site
- [config-key] config: tiles :: tiles[].hold_always @ config.json:135-141,172; status_panel.py:1053-1063,2259-2260; make_page.py:169,173 | today: edit config.json, make_page.py, push_page.py, install.sh | fix id; validated to exist; hold runs it even when the button is green (HD1-6, TASCAM, SPOTIFY); for a hyperdeck-kind fix the first check with a conn supplies conn/ip and a recording deck is left alone; make_page needs fixes[hold_always].label | other campus: same
- [config-key] config: checks (common) :: checks[].type @ status_panel.py:534-539,2181,2243 | today: edit config.json, install.sh | one of conn, hyperdeck, dante, shure, psm, tascam, ping, launchd, process, http, disk, midi_peer, pref, spotify, mdns, midi_rx, remote, artnet, atem; validated | other campus: same
- [config-key] config: checks (common) :: checks[].conn @ status_panel.py:245,259,294,331,357,370,391,760,932,953,2256 | today: edit config.json, install.sh | Companion connection label (case-sensitive, as shown in Companion); required for conn/hyperdeck/dante/shure/psm/tascam, optional for ping; any check with conn is a "watched" connection for burst detection and gets the enable/restart plans; NOT validated against Companion (shows NOT IN COMPANION live) | other campus: per site labels; the UI should list Companion's live connections to pick from
- [config-key] config: checks (common) :: checks[].fix @ status_panel.py:960-962,1020-1022,2250-2256; make_page.py:167 | today: edit config.json, install.sh (+ make_page/push_page when it changes whether the key has a hold) | fix id string | "restart-connection" | "dante-auto" | per-sub dict; validated; absent = restart its Companion connection for RESTARTABLE types, nothing otherwise (hold says NO AUTO FIX) | other campus: same
- [config-key] config: checks (common) :: checks[].tag @ config.json:161-167,179-187,201,219-220; status_panel.py:805-808 | today: edit config.json, install.sh | string; optional; prefixed to fault text that does not already name its device ("PROPRES OFFLINE"); use on tiles with several devices | other campus: same
- [config-key] config: checks (common) :: checks[].service_only @ config.json:199; status_panel.py:245,313,398,1306 | today: edit config.json, install.sh | bool; default false; read only by conn, dante, ping; the fault shows grey "idle" while ALARMS is off instead of dark red "quiet" | other campus: same
- [config-default] config: checks (common) :: checks[].debounce_seconds @ status_panel.py:882-884 | today: not set; edit config.json, install.sh | seconds; default = global debounce_seconds (3), except midi_peer = 1; network sub-checks always 0 | other campus: same
- [config-key] config: check conn :: conn.ignore_warning @ config.json:179; status_panel.py:246-250 | today: edit config.json, install.sh | bool; default false; treats Companion's yellow "warning" state as OK (ProPresenter API flicker); error/offline/disabled still alarm | other campus: same
- [hardcoded] config: check conn :: conn fault texts (hardcoded) @ status_panel.py:186-214 | today: edit source | NO DATA (no Companion data for 10 s), NOT IN COMPANION, DISABLED (warn), NO ANSWER/REFUSED/UNREACHABLE/LOGIN mapped from the module message, OFFLINE, WARNING, STARTING | other campus: same | (need not be editable)
- [config-key] config: check hyperdeck :: hyperdeck.conn + polled variables @ status_panel.py:254-289,558-560 | today: edit config.json, install.sh | Companion label (HyperDeck_1..6); reads module variables status, slotId, recordingTime every 1 s and ip every 3 s; conn must be a Blackmagic HyperDeck module instance | other campus: use only the decks that site has (e.g. HyperDeck_2 and _4) - just list fewer tiles
- [config-key] config: check hyperdeck :: hyperdeck.optional @ config.json:136,138; status_panel.py:267-270 | today: edit config.json, install.sh | bool; default false; true = a deck with no disk/no recordingTime sits grey "NO DISK" with no alarm and is excluded from REC sync until a disk is in | other campus: per site
- [config-default] config: check hyperdeck :: hyperdeck.disk_service_only @ status_panel.py:272 | today: not set; edit config.json, install.sh | bool; default true; NO DISK on a non-optional deck is idle outside service | other campus: same
- [config-default] config: check hyperdeck :: hyperdeck.full_minutes @ status_panel.py:276 | today: not set; edit config.json, install.sh | int minutes left; default 5; below = red "FULL " | other campus: same
- [config-default] config: check hyperdeck :: hyperdeck.low_minutes @ status_panel.py:278 | today: not set; edit config.json, install.sh | int minutes left; default 30; below = amber "LOW " | other campus: same
- [config-default] config: check hyperdeck :: hyperdeck.rec_sync @ status_panel.py:283-287,794-796 | today: not set; edit config.json, install.sh | bool; default true; when any rec_sync deck is recording, every other rec_sync deck that is not shows red NOT REC | other campus: same
- [config-default] config: check hyperdeck :: hyperdeck.ip @ status_panel.py:987,1062,1986 | today: not set; edit config.json, install.sh | IP string; optional override of the deck's address used by the hyperdeck fix's ping; default = Companion variable "ip" of the connection, remembered if it disappears | other campus: normally not needed
- [config-key] config: check dante :: dante.conn + polled variables @ status_panel.py:292-303,562,1880-1886 | today: edit config.json, install.sh | Companion label of the audinate-dantecontroller module; reads variable "devices" (JSON list) every 1 s and "_ip" for the Dante map; only the FIRST dante check's conn feeds the Dante map (dante_label) | other campus: same module per site
- [config-key] config: check dante :: dante.devices @ config.json:152-158; status_panel.py:304-313,791 | today: edit config.json, install.sh | dict {Dante device name: short alias} or list of names (alias = name); required; each missing device = red " GONE"; count "seen/total" shown when >1 device; aliases should be short (14 chars) | other campus: the per-site Dante device list; the UI should offer the live list from Companion's devices variable
- [config-default] config: check dante :: dante.count_all @ status_panel.py:314-318 | today: not set; edit config.json, install.sh | bool; default false; true = the count covers every Dante device watched on any tile (so several dante checks can split devices across tiles) | other campus: same
- [config-key] config: check shure :: shure.conn + polled variables @ status_panel.py:329-352,565-568 | today: edit config.json, install.sh | Companion label of a Shure module (Axient_1, Axient_2, ULXD-4); reads ch_N_name, ch_N_battery_bars, ch_N_interference_status every 3 s | other campus: per site receivers
- [config-key] config: check shure :: shure.channels @ config.json:226-230; status_panel.py:335,566 | today: edit config.json, install.sh | int; default 1; channels 1..N read | other campus: per receiver
- [config-default] config: check shure :: shure.battery_err_bars @ status_panel.py:340 | today: not set; edit config.json, install.sh | int 0-5; default 1; bars <= this = red " BATT" | other campus: same
- [config-default] config: check shure :: shure.battery_warn_bars @ status_panel.py:342 | today: not set; edit config.json, install.sh | int 0-5; default 2; bars <= this = amber | other campus: same
- [config-key] config: check psm :: psm.conn + polled variables @ status_panel.py:355-366,569-571 | today: edit config.json, install.sh | Companion label (PSM_1..3); reads ch_N_name, ch_N_rf_mute every 3 s | other campus: per site
- [config-key] config: check psm :: psm.channels @ config.json:232-236; status_panel.py:362,570 | today: edit config.json, install.sh | int; default 1 | other campus: per unit
- [config-default] config: check psm :: psm.muted_is_error @ status_panel.py:361 | today: not set; edit config.json, install.sh | bool; default false (RF MUTE is amber); true = red | other campus: same
- [config-key] config: check tascam :: tascam.conn + polled variables @ status_panel.py:369-385,563-564 | today: edit config.json, install.sh | Companion label; reads variables error and caution every 1 s; no other fields; "no error"/"no caution" = OK, else text upper-cased to 14 chars | other campus: same
- [config-key] config: check ping :: ping.host @ config.json:143,184,202-203,221-222,227-237; status_panel.py:388-399,2277-2282 | today: edit config.json, install.sh | IP/hostname; the address pinged (every 3 s, 1 s timeout, /sbin/ping); when conn is set, Companion's own host setting for that connection wins and host is only the fallback; chk["host"] is read for the default message so a ping check with neither host nor name/msg crashes that check (CHECK BUG) | other campus: per site IPs
- [config-key] config: check ping :: ping.conn @ config.json:143,227-237; status_panel.py:388-391,1585-1590 | today: edit config.json, install.sh | optional Companion label; its "host" setting is read from Companion's db.sqlite every 60 s so an address changed in Companion needs no config change | other campus: same
- [config-key] config: check ping :: ping.name @ status_panel.py:397 | today: edit config.json, install.sh | string; default = host; fault text " NO PING" | other campus: same
- [config-default] config: check ping :: ping.msg @ status_panel.py:397 | today: not set; edit config.json, install.sh | string; optional full fault text replacing " NO PING" | other campus: same
- [config-default] config: check ping :: ping.failures @ status_panel.py:396 | today: not set; edit config.json, install.sh | int consecutive misses; default 2 (~6 s at the 3 s poll) | other campus: same
- [config-key] config: check launchd :: launchd.label @ config.json:210-212; status_panel.py:402-410,1477-1484 | today: edit config.json, install.sh | LaunchAgent label string (org.waterschurch.lobby-captions); required;
launchctl listevery 10 s: missing = " NOT LOADED", loaded with no pid = " STOPPED" | other campus: per site agents - [config-key] config: check launchd :: launchd.name @ status_panel.py:405 | today: edit config.json, install.sh | string; default = label | other campus: same
- [config-key] config: check process :: process.match @ config.json:214; status_panel.py:413-417,1486-1489 | today: edit config.json, install.sh |
pgrep -fpattern (regex against the full command line); required; polled every 10 s | other campus: per app path - [config-key] config: check process :: process.name @ status_panel.py:416 | today: edit config.json, install.sh | string; default "PROCESS"; fault " STOPPED" | other campus: same
- [config-key] config: check http :: http.url @ config.json:185-186,204,206,213,215; status_panel.py:420-423,1527-1541,2285-2292 | today: edit config.json, install.sh | URL string; required; HEAD request, 6 s timeout, TLS not verified; any HTTP answer (even 4xx/5xx) = up | other campus: per site
- [config-key] config: check http :: http.msg @ status_panel.py:422 | today: edit config.json, install.sh | string; default " UNREACH" | other campus: same
- [config-key] config: check http :: http.name @ config.json:206; status_panel.py:422 | today: edit config.json, install.sh | string; default "WEB" | other campus: same
- [config-key] config: check http :: http.interval_seconds @ config.json:185,204,213,215; status_panel.py:2291,2313 | today: edit config.json, install.sh | seconds; default 30; the smallest value wins when the same URL appears twice; the http poller runs every 5 s so values below 5 behave as 5 | other campus: same
- [config-key] config: check disk :: disk.path @ config.json:216; status_panel.py:456-465,1490-1492 | today: edit config.json, install.sh | filesystem path; required; free GB read every 10 s; shows "GB free" | other campus: same
- [config-key] config: check disk :: disk.warn_gb @ config.json:216; status_panel.py:463 | today: edit config.json, install.sh | number; default 20; below = amber "DISK nGB" | other campus: same
- [config-key] config: check disk :: disk.err_gb @ config.json:216; status_panel.py:461 | today: edit config.json, install.sh | number; default 5; below = red | other campus: same
- [config-key] config: check midi_peer :: midi_peer.session @ config.json:161-166; status_panel.py:476-478,995,1961 | today: edit config.json, install.sh | network MIDI session name as in Audio MIDI Setup ("NA Ingest", "NA Tracks"); required; missing session = red NO SESSION | other campus: per Mac session names
- [config-key] config: check midi_peer :: midi_peer.peer @ config.json:161-166; status_panel.py:479-487,995,1961 | today: edit config.json, install.sh | participant name as the session lists it ("Lighting PC"); required; state running|connected = OK, other = "MIDI ", absent = MIDI LOST | other campus: per site
- [config-key] config: check midi_peer :: midi_peer.host @ config.json:163-166; status_panel.py:471-473,1663-1746,1962-1966 | today: edit config.json, install.sh; the other Mac needs the ssh key authorised | optional hosts alias; the session is read on that Mac over one long-lived ssh running coremidi.py with /usr/bin/python3 (nothing installed there); no verdict if it has not reported for 12 s; NOT validated by load_config (a bad alias kills that stream thread) | other campus: per site Macs
- [config-key] config: check midi_peer :: midi_peer.address @ config.json:161-166; status_panel.py:996-999 | today: edit config.json, install.sh | "ip:port" or Bonjour "\tName\tlocal." form; optional fallback for the midi_reconnect fix when the peer was never seen; last-seen address wins | other campus: per site
- [config-default] config: check midi_peer :: midi_peer.debounce_seconds (implicit) @ status_panel.py:881-884 | today: not set | default 1 s for this type (a peer leaving is a clean event) | other campus: same
- [config-key] config: check pref :: pref.domain @ config.json:197; status_panel.py:448-453,1493-1496 | today: edit config.json, install.sh | macOS defaults domain string; required;
defaults read <domain> <key>every 10 s on the ingest Mac | other campus: same app = same domain - [config-key] config: check pref :: pref.key @ config.json:197; status_panel.py:450,1494 | today: edit config.json, install.sh | defaults key string; required | other campus: same
- [config-key] config: check pref :: pref.want @ config.json:197; status_panel.py:451 | today: edit config.json, install.sh | value compared as a string to what defaults prints ("0"); required; unreadable key = OK (no verdict) | other campus: same
- [config-key] config: check pref :: pref.msg @ config.json:197; status_panel.py:453 | today: edit config.json, install.sh | fault text; required (KeyError otherwise) | other campus: same
- [config-key] config: check spotify :: spotify.playlist @ config.json:173; status_panel.py:1619-1630,1597-1616 | today: edit config.json (TWICE: here and inside fixes.spotify_play.run), install.sh | "spotify:playlist:" URI; required; only the FIRST spotify check's playlist is fetched (public embed page + anonymous token, hourly and on an unknown song); WRONG PLAYLIST when the current song is not in it (3 strangers in a row if only part of the list is known) | other campus: per site playlist; the UI must write both places (or the fix should read it from the check)
- [config-key] config: check spotify :: spotify.name @ config.json:173 | today: edit config.json | string; NOT read by any code today (documentation only); the fix label hardcodes "Christian EDM 2026" | other campus: per site
- [hardcoded] config: check spotify :: spotify hardcoded behaviour @ status_panel.py:426-445,1593-1594,1631-1645,156-158 | today: edit source | polled every 3 s via pgrep + osascript (never opens Spotify); CLOSED/NOT PLAYING red; refresh 3600 s; remembers last 3 tracks; needs Automation permission for Spotify on the ingest Mac | other campus: same | (need not be editable)
- [config-key] config: check mdns :: mdns.instance @ config.json:168-169; status_panel.py:490-493,1510-1518 | today: edit config.json, install.sh | Bonjour _apple-midi._udp instance name; required; browsed 3 s every 15 s; missing = " GONE" | other campus: per site
- [config-key] config: check mdns :: mdns.name @ status_panel.py:492 | today: edit config.json, install.sh | string; default = instance | other campus: same
- [config-key] config: check midi_rx :: midi_rx.source @ config.json:170; status_panel.py:496-507,1652-1658 | today: edit config.json, install.sh | CoreMIDI source name on the ingest Mac ("Network NA Ingest Listen"); required; a coremidi.Listener is attached; grey OFF while not connected, else " " | other campus: per Mac source name
- [config-key] config: check midi_rx :: midi_rx.name @ status_panel.py:498 | today: edit config.json, install.sh | string; default "MIDI" | other campus: same
- [config-default] config: check midi_rx :: midi_rx.max_silence_seconds @ status_panel.py:504-506; README.md:185 | today: not set; edit config.json, install.sh | seconds; default none = informational only; set = amber " SILENT" when no MIDI for that long, only while service mode is on | other campus: same
- [config-key] config: check remote :: remote.host @ config.json:180-189; status_panel.py:523-531,1782-1799,2257 | today: edit config.json, install.sh | hosts alias; required; validated; all remote cmds for one host are batched into one ssh every 10 s | other campus: per site
- [config-key] config: check remote :: remote.cmd @ config.json:180-189; status_panel.py:524,1772-1774 | today: edit config.json, install.sh | shell command run on that machine; required; exit 0 = OK, non-zero = fault, unreachable = no verdict; duplicates per host collapsed | other campus: per site
- [config-key] config: check remote :: remote.msg @ status_panel.py:530 | today: edit config.json, install.sh | fault text; required (KeyError otherwise) | other campus: same
- [config-key] config: check artnet :: artnet.source @ config.json:176; status_panel.py:510-520,2184-2208 | today: edit config.json, install.sh | IP string of the Art-Net sender; required; passive listener on UDP 6454 (SO_REUSEPORT), started only when an artnet check exists | other campus: per site lighting PC IP
- [config-key] config: check artnet :: artnet.max_silence_seconds @ config.json:176; status_panel.py:512 | today: edit config.json, install.sh | seconds; default 5 | other campus: same
- [config-default] config: check artnet :: artnet.msg @ status_panel.py:519 | today: not set; edit config.json, install.sh | string; default "SILENT" (tile label is prefixed); a custom msg is shown as-is | other campus: same
- [config-key] config: check atem :: atem check .part @ config.json:195-196; status_panel.py:2170-2178 | today: edit config.json, install.sh | "audio"|"camera"; required; no verdict unless atem_watch.js reported within 30 s and is connected | other campus: same
- [config-key] config: check atem :: atem check .msg @ config.json:195-196; status_panel.py:2177 | today: edit config.json, install.sh | fault text; required (KeyError otherwise); info shows the first 3 differences | other campus: same
- [config-key] config: atem section :: atem (section presence) @ config.json:91-96; status_panel.py:2320-2328 | today: edit config.json, install.sh | object; the watcher starts only when the section exists AND at least one atem check exists; if node/library are not found the ATEM checks are silently off (warning in log) | other campus: omit at a site without an ATEM
- [config-key] config: atem section :: atem.host @ config.json:92; status_panel.py:2120 | today: edit config.json, install.sh | switcher IP; required | other campus: per site
- [config-key] config: atem section :: atem.node @ config.json:93; status_panel.py:2108,2089-2095 | today: edit config.json, install.sh | glob pattern to Companion's bundled node binary; required; newest version match wins; ~ expanded | other campus: same unless Companion is installed elsewhere
- [config-key] config: atem section :: atem.library @ config.json:94; status_panel.py:2108 | today: edit config.json, install.sh | glob pattern to the atem-connection module inside Companion's bmd-atem module; required; newest wins | other campus: same
- [config-key] config: atem section :: atem.reference @ config.json:95; status_panel.py:2111-2115,2061-2086; install.sh:28 | today: copy a new ATEM Software Control .xml over atem-reference.xml, zsh install.sh | path to the restore .xml; relative paths are relative to the script folder, ~ expanded; converted to atem-reference.json in the data dir at start; which settings are compared is hardcoded (MIX_OPTIONS/CCU_PARAMS :2042-2058) | other campus: per site restore file (today from Google Drive Backup/NA/ATEM)
- [config-key] config: auto_fix :: auto_fix (section) @ config.json:98-105; status_panel.py:772-775,1154-1246 | today: edit config.json, install.sh | object; absent or empty = auto-fix completely off; runs every 5 s while inside a window AND service mode is on | other campus: per site
- [config-key] config: auto_fix :: auto_fix.windows @ config.json:99; status_panel.py:1164-1174 | today: edit config.json, install.sh | list of {days,start,end} like service_windows; default []; today Sunday 06:30-13:30 | other campus: per site service times
- [config-key] config: auto_fix :: auto_fix.after_seconds @ config.json:100; status_panel.py:1206 | today: edit config.json, install.sh | seconds a red fault must have lasted; default 20 | other campus: same
- [config-key] config: auto_fix :: auto_fix.max_tries @ config.json:100; status_panel.py:1211-1218,1234 | today: edit config.json, install.sh | int; default 2; then "gave up" in the auto-fix log until the fault is forgotten | other campus: same
- [config-key] config: auto_fix :: auto_fix.retry_seconds @ config.json:100; status_panel.py:1219 | today: edit config.json, install.sh | seconds between tries; default 90 (plus the global 60 s fix cooldown) | other campus: same
- [config-key] config: auto_fix :: auto_fix.pause_after_network_seconds @ config.json:100; status_panel.py:1178-1186 | today: edit config.json, install.sh | seconds; default 60; no auto-fixes while link/gateway are down or a burst is on/just cleared | other campus: same
- [config-key] config: auto_fix :: auto_fix.restart_connections @ config.json:101; status_panel.py:1207 | today: edit config.json, install.sh | bool; default false; true = plain Companion reconnects are also done automatically | other campus: same
- [config-key] config: auto_fix :: auto_fix.fixes @ config.json:102-104; status_panel.py:1187,1207,2261-2265 | today: edit config.json, install.sh | list of fix ids that auto-fix may run ("gentle"); validated to exist and to not contain pkill/taskkill/killall in run unless restart_ok; everything else stays a hold | other campus: per site
- [config-default] config: auto_fix :: auto_fix.forget_seconds @ status_panel.py:1240-1245 | today: not set; edit config.json, install.sh | seconds; default 600; a fault's try count resets once it has been gone this long | other campus: same
- [config-default] config: auto_fix :: auto_fix.log_dir @ status_panel.py:775,625-686 | today: not set; edit config.json, install.sh | directory, ~ expanded; default "~/Desktop/Auto-Fix Logs"; one "YYYY-MM-DD Day Auto-Fix.log" per day, deleted after a calendar month; header text hardcodes "NA Ingest ... Sundays 06:30-13:30" (:680-682) | other campus: per site; header wording needs a site-name key
- [config-key] config: restart_page :: restart_page (section) @ config.json:107-124; status_panel.py:771; make_page.py:274-277,281-288,301-304; push_page.py:179 | today: edit config.json, make_page.py, push_page.py --which restart, install.sh | optional object; when present make_page also builds restart-page.companionconfig and places a RESTART PAGE jump key on the status page | other campus: per site
- [config-key] config: restart_page :: restart_page.page @ config.json:108; make_page.py:277,288; push_page.py:179 | today: edit config.json, make_page.py, push_page.py --which restart | int Companion page number (20); required when the section exists | other campus: per site
- [config-key] config: restart_page :: restart_page.page_name @ config.json:108; make_page.py:288; push_page.py:179,188 | today: edit config.json, make_page.py, push_page.py --which restart | string with literal \n ("20\nRestart"); required; push_page matches on it | other campus: per site
- [config-key] config: restart_page :: restart_page.jump_pos @ config.json:108; make_page.py:276 | today: edit config.json, make_page.py, push_page.py | [row,col] on the STATUS page for the grey "RESTART PAGE" key; required; must not collide with a tile pos | other campus: per layout
- [config-key] config: restart_page :: restart_page.back_pos @ config.json:108; make_page.py:284 | today: edit config.json, make_page.py, push_page.py --which restart | [row,col] on the RESTART page for the "STATUS PAGE" key; required; must not collide with a restart key pos | other campus: per layout
- [config-key] config: restart_page :: restart_page.keys[].id @ config.json:110-122; status_panel.py:771,1260,2266-2268; make_page.py:137,144-145 | today: edit config.json, make_page.py, push_page.py --which restart, install.sh | string; required; must not equal a tile id (validated); same identifier rules as tile ids (jsonpath); command "force " | other campus: same
- [config-key] config: restart_page :: restart_page.keys[].label @ config.json:110-122; status_panel.py:1372-1376; make_page.py:134 | today: edit config.json, make_page.py, push_page.py --which restart, install.sh | key text; required; shows "\nHOLD 2s" / "HOLD TO FIX" / RESTARTING / DONE / FAILED | other campus: same
- [config-key] config: restart_page :: restart_page.keys[].pos @ config.json:110-122; make_page.py:287 | today: edit config.json, make_page.py, push_page.py --which restart | [row,col] on the restart page; required; collision raises | other campus: per layout
- [config-key] config: restart_page :: restart_page.keys[].fix @ config.json:110-122; status_panel.py:1264,1369,2269-2270; make_page.py:141 | today: edit config.json, make_page.py, push_page.py --which restart, install.sh | fix id; required; validated to exist; runs whatever the status says (shares the 60 s guard with holds) | other campus: per site
- [config-key] config: restart_page :: restart_page.keys[].watch @ config.json:115-116,120,122; status_panel.py:1265-1270,1369-1374,2271-2273 | today: edit config.json, install.sh | list of fix ids or "conn:"; default []; the key flashes HOLD TO FIX when its fix or any watched fix/connection is failing; a watched fix that ran <60 s ago makes the key say "STARTING WAIT 1 MIN"; fix ids validated, conn labels not | other campus: per site
- [hardcoded] config: restart_page :: make_page "options/steps/style" are NOT restart-key config @ make_page.py:207-232 (nav_key), nav_templates.json | today: n/a | key["style"], key["steps"], key["options"] in make_page.py are read from nav_templates.json (Companion's own pageup/pagenum/pagedown exports), not from restart_page.keys; restart keys accept only id, label, pos, fix, watch | other campus: same | (need not be editable)
- [hardcoded] hardcoded: page build :: HOLD_MS = 2000 @ make_page.py:36,144-146,181-182 | today: edit make_page.py, make_page.py, push_page.py | hold time in ms for every status tile and restart key | other campus: same; candidate config key
- [hardcoded] hardcoded: page build :: STALE_SECONDS = 15 and PURPLE = 0x6A1B9A @ make_page.py:34-35,53,61-65,209-212 | today: edit make_page.py | heartbeat age after which every key turns purple WATCHDOG DOWN | other campus: same | (need not be editable)
- [hardcoded] hardcoded: page build :: GRID = columns 0..8, rows 0..5 @ make_page.py:33 | today: edit make_page.py | page grid size written into the export; a site with a different surface needs it changed | other campus: per surface; candidate config key
- [hardcoded] hardcoded: page build :: NAV_LOCATIONS pageup (0,0) pagenum (1,0) pagedown (2,0) @ make_page.py:195,241-244; nav_keys.py:24,113-114 | today: edit make_page.py | column 0 rows 0-2 are always the nav keys; tiles cannot use them | other campus: same | (need not be editable)
- [hardcoded] hardcoded: page build :: EXPORT_VERSION 12 / COMPANION_BUILD 5.0.7 @ make_page.py:31-32,257-259 | today: edit make_page.py when Companion's export format changes | export format stamps; push_page/nav_keys written against Companion 5.0.7's tRPC API | other campus: same Companion version needed per site | (need not be editable)
- [hardcoded] hardcoded: page build :: push_page.py / nav_keys.py --host na-ingest.local --port 8000 @ push_page.py:172-173; nav_keys.py:84-85 | today: pass --host/--port on the command line | CLI defaults; these scripts read config.json only for page numbers/names and ignore companion.host/port | other campus: another site must pass --host; should read companion.host
- [hardcoded] hardcoded: page build :: "on NA Ingest" wording in key notes and docstrings @ make_page.py:141,188; status_panel.py:2,427,680-682,2431,2438 | today: edit source | site name appears in Companion button notes, auto-fix log header and webhook text | other campus: needs a site_name config key
- [hardcoded] hardcoded: watchdog timing :: poller intervals @ status_panel.py:2303-2316 | today: edit source | connections 1 s, fast variables 1 s, slow variables 3 s, Companion db hosts 60 s, spotify 3 s, pings 3 s, local MIDI 0.5 s, system (launchd/process/disk/pref) 10 s, Bonjour 15 s, http 5 s, remote ssh 10 s, dante-map 30 s; redraw 0.5 s (:2400), command poll 0.25 s (:2440) | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog timing :: fix cooldown 60 s @ status_panel.py:1041,1074,1256,1219 | today: edit source | the same fix (per deck / per MIDI peer) never re-runs within 60 s; key says JUST TRIED WAIT 1 MIN | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog timing :: HOLD_MAX_AGE 15 s @ status_panel.py:2345,2365-2370 | today: edit source | a hold/force stamped more than 15 s ago (queued while the watchdog was down) is dropped | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog timing :: freshness limits @ status_panel.py:181 (conns 10 s), 472 (remote MIDI 12 s), 2173 (ATEM 30 s), 2387 (2.5 s start grace), 2437 (webhook after 30 s) | today: edit source | NO DATA when Companion data is older than 10 s; remote MIDI no verdict after 12 s; ATEM no verdict after 30 s | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: COLORS table / display levels @ status_panel.py:41-61,1299-1310 | today: edit source | ok green, warn amber, notice flashing amber, quiet dark red (fault while ALARMS off), ack dark red, err flashing red, off/idle grey, service blue, fixing blue, failed orange | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: RESTARTABLE = conn, hyperdeck, shure, psm, tascam @ status_panel.py:542,932,1005; make_page.py:29,167 | today: edit source | check types whose Companion connection a hold restarts when they have no fix; also decides whether make_page gives the key a hold action | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: ssh options @ status_panel.py:1748-1753,1707 | today: edit source | /usr/bin/ssh -i <ssh_key> BatchMode=yes ConnectTimeout=5 IdentitiesOnly=yes StrictHostKeyChecking=accept-new ControlMaster=auto ControlPath=/tmp/status-panel-ssh-%C ControlPersist=120; remote python is /usr/bin/python3 (:1708,1741,1963) | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: ports: WoL 9, Art-Net 6454, HyperDeck 9993, telnet 23 @ status_panel.py:1759,2184,1802; netgear.py:38 | today: edit source | hyperdeck_probe (9993) is defined but only used by tests, not by any fix path | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: ATEM compared settings (MIX_OPTIONS, CCU_PARAMS) @ status_panel.py:2042-2058 | today: edit source | Fairlight mixOption + faderGain per source; camera iris, white balance, exposure, detail, gain, ND, lift/gamma/gain/offset, contrast, luma mix, colour | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: text fitting LINE_CAPACITY 145 / sizes 30..12 @ status_panel.py:1331-1356 | today: edit source | key text size chosen per key so words never break; measured on Companion web buttons | other campus: same | (need not be editable)
- [hardcoded] hardcoded: watchdog :: CLI: --config, --data-dir, --log, --once, --dry-run @ status_panel.py:2479-2497; install.sh:33-39 | today: LaunchAgent passes --data-dir "$HOME/Library/Application Support/status-panel"; --config defaults to config.json beside the script; --log ~/Library/Logs/status-panel.log (1 MB x3) | the running config is the COPY in App Support, not the repo file; a UI must edit that copy (or re-run install.sh) | other campus: same
- [runtime-data] runtime-data :: data dir files: service-mode.json, dante-map.json, atem-reference.json, config.json.bak-* @ status_panel.py:2384-2385,1839,2107; install.sh:24-26 | today: written by the watchdog / install.sh | ALARMS override + last schedule; Dante name->ip/mac/switch/port/poe/seen; converted ATEM restore; timestamped config backups on each install | other campus: same
- [companion-side] companion-side :: Companion connection labels referenced by checks @ config.json tiles (HyperDeck_1-6, tascam, x32, audinate-dantecontroller, lighting-midi, Propresenter-API, ableton, BibleTV, ATEM, OBS-Youtube, RESI_Decoder, resi-studio-cues, VX1000, UHDJr, arena, Axient_1/2, ULXD-4, PSM_1-3) | today: rename in Companion AND in config.json | exact label match; a renamed connection shows NOT IN COMPANION; the UI should read /api/connections to offer the live list and flag mismatches | other campus: per site
- [companion-side] companion-side :: custom variables sp_state, sp_hb, sp_cmd must be created by hand @ status_panel.py:2421-2427; README.md:128 | today: Companion web UI once | names come from variables.*; watchdog only warns when missing | other campus: per Companion
- [secret] secret :: Netgear switch login in the login keychain (service "netgear-status-panel") @ netgear.py:4-10,22,25-35 | today: security add-generic-password -s netgear-status-panel -a -w (in Terminal on the ingest Mac, logged-in session) | account = username, password never printed; needed for dante_switches survey and PoE cycle; the UI should only show present/absent and never the value | other campus: per site switches
- [external-setup] external-setup referenced from config strings :: things fixes.run strings depend on @ config.json:48-50 (/usr/local/sbin/status-panel-dvs-start via sudo -n), 53-55,67 (LaunchAgents org.waterschurch.*), 57-65 (schtasks \StatusPanel\Start BibleApp/OBS/Arena/Vista/rtpMIDI), 69,188-189 (~/Library/Application Support/status-panel/solo-mode.json on studiob), 76 (Spotify Automation permission) | today: remote-access/mac-dvs-helper.sh, windows-setup.ps1, install-solo-mode.sh; by hand | each is a plain string inside run; nothing verifies it exists until the fix fails | other campus: per machine; the UI should show per host which helpers are installed
- [workflow-step] workflow-step :: install.sh copies status_panel.py coremidi.py netgear.py config.json atem_watch.js atem-reference.xml to ~/Library/Application Support/status-panel and restarts LaunchAgent org.waterschurch.status-panel @ install.sh:9-11,22-28,41-60 | today: zsh install.sh on the ingest Mac after any config change | validates config.json is JSON only (no load_config); backs up the old config; atem_state.js/nav_templates.json not needed at runtime | other campus: same per ingest Mac
Lens: WHAT RUNS WHERE: every machine the status panel touches, what is installed/configured on each, how it got there, how to tell it is alive, and which panel keys depend on it
- [workflow-step] Your MacBook (dev) :: Project folder (source of truth) + git @ MacBook: /Users/christian/Documents/Status Indicators (git, branch main) | today: edit files here, git commit, git push | Verify: git status clean; git log. Panel: nothing at runtime (NA Ingest runs copies) | other campus: one repo per site, or one repo with per-site config files; code must stay shared | (need not be editable)
- [external-setup] Your MacBook (dev) :: Gitea remote christian/status-indicators (private) @ MacBook: origin http://chronicles-ab.waterschurch.org:3000/christian/status-indicators.git | today: git push with credential helper reading keychain item gitea-mcp (see .claude/settings.local.json) | Verify: git remote -v; git ls-remote origin | other campus: same repo; keep private (guideline 13) | (need not be editable)
- [external-setup] Your MacBook (dev) :: SSH alias checklist-ingest -> NA Ingest @ MacBook: ~/.ssh/config Host checklist-ingest (HostName 10.1.1.1, User Studio, IdentityFile ~/.ssh/id_ed25519_checklist_ingest) | today: edited by hand in ~/.ssh/config; key authorized on NA Ingest by hand | Verify: ssh -o BatchMode=yes checklist-ingest 'hostname'. Used by scp/install deploy and all remote checks from the MacBook | other campus: one alias per site Ingest Mac (checklist-) | (need not be editable)
- [workflow-step] Your MacBook (dev) :: Memory files (project history + guidelines) @ MacBook: ~/.claude/projects/-Users-christian-Documents-Status-Indicators/memory/companion-status-page.md and status-panel-guidelines.md | today: written by Claude sessions | Verify: files exist. Panel: none | other campus: n/a | (need not be editable)
- [external-setup] NA Ingest: watchdog :: LaunchAgent org.waterschurch.status-panel (the watchdog) @ NA Ingest (Studio@10.1.1.1):
/Library/LaunchAgents/org.waterschurch.status-panel.plist; runs /usr/bin/python3 '/Library/Application Support/status-panel/status_panel.py' --data-dir ; RunAtLoad, KeepAlive, ThrottleInterval 10 | today: install.sh:30-58 writes the plist and bootout/bootstraps gui/; 'zsh install.sh --remove' stops it | Verify: launchctl list | grep status-panel (PID shown, e.g. 99852); curl 127.0.0.1:8000/api/custom-variable/sp_hb/value within 15 s of now; page turns PURPLE 'WATCHDOG DOWN' when it stops. Panel: everything | other campus: same install.sh per site; label could carry the site name - [workflow-step] NA Ingest: watchdog :: Install staging folder @ NA Ingest: ~/status-panel-install/ (status_panel.py, coremidi.py, netgear.py, atem_watch.js, config.json, atem-reference.xml, install.sh) | today: scp from the MacBook (README 'Changing things'), then ssh checklist-ingest 'zsh ~/status-panel-install/install.sh' | Verify: ls ~/status-panel-install; cmp with App Support copies | other campus: same path per site | (need not be editable)
- [external-setup] NA Ingest: watchdog :: Data dir ~/Library/Application Support/status-panel @ NA Ingest: that folder (install.sh:11 DEST; App Support because launchd python cannot read ~/Documents) | today: install.sh:23-28 copies status_panel.py, coremidi.py, netgear.py, config.json, atem_watch.js, atem-reference.xml into it | Verify: ls -la the folder; mtime of config.json matches the deployed one | other campus: same
- [external-setup] NA Ingest: watchdog :: /usr/bin/python3 3.9.6 (macOS Xcode python, stdlib only) @ NA Ingest: /usr/bin/python3 (Xcode Python3.framework 3.9); macOS 26.5.1 | today: comes with Xcode / Command Line Tools; nothing installed (R9) | Verify: /usr/bin/python3 --version. Panel: whole watchdog; telnetlib (netgear.py) needs 3.9-era stdlib | other campus: every Ingest Mac needs Xcode CLT python3; telnetlib is removed in newer pythons
- [runtime-data] NA Ingest: watchdog :: Watchdog log (rotating) @ NA Ingest: ~/Library/Logs/status-panel.log (1 MB x 3, status_panel.py:2485,2491) and ~/Library/Logs/status-panel.err.log (launchd stderr, install.sh:46) | today: written by the watchdog; every fault/clear/HOLD/FIX line | Verify: tail -f ~/Library/Logs/status-panel.log; err.log should stay 0 bytes | other campus: same
- [runtime-data] NA Ingest: watchdog :: Auto-Fix daily logs @ NA Ingest: ~/Desktop/Auto-Fix Logs/ Auto-Fix.log (AutoLog status_panel.py:625-686; dir from auto_fix.log_dir default
/Desktop/Auto-Fix Logs) | today: written by auto_fix(); files deleted a calendar month after their date | Verify: ls '/Desktop/Auto-Fix Logs' (today: 2026-10-11 Sunday, 2026-10-13 Tuesday). Double-click opens Console | other campus: same; folder name configurable - [runtime-data] NA Ingest: watchdog :: service-mode.json (ALARMS override) @ NA Ingest: ~/Library/Application Support/status-panel/service-mode.json ({override, last_sched}; ServiceMode status_panel.py:696-739) | today: written when ALARMS key is pressed or a window edge passes | Verify: cat the file (now {"override": null, "last_sched": true}). Panel: ALARMS key, auto-fix on/off | other campus: same
- [runtime-data] NA Ingest: watchdog :: dante-map.json (self-updating Dante device map) @ NA Ingest: ~/Library/Application Support/status-panel/dante-map.json (poll_dante_map status_panel.py:1869-1906; every 30 s until first success then dante_map_seconds=600) | today: built automatically from Companion Dante vars + arp -an + Netgear MAC/PoE tables | Verify: python3 -c to print it (19 devices today; PoE: STUDIO-B-MON 0/15, SOLO-MON 0/14, TB 0/16, TB-MON 0/17 on 10.66.1.102); log line 'dante map: N devices'. Panel: DANTE hold (dante-auto) | other campus: automatic per site
- [runtime-data] NA Ingest: watchdog :: atem-reference.json (derived from the restore XML) @ NA Ingest: ~/Library/Application Support/status-panel/atem-reference.json (AtemWatcher.init status_panel.py:2107-2115 writes it on every start from atem-reference.xml) | today: regenerated at watchdog start; source XML copied by install.sh:28 | Verify: file mtime == last watchdog start; ATEM key green. Panel: ATEM AUDIO/CAMERA WRONG | other campus: each site has its own restore XML
- [runtime-data] NA Ingest: watchdog :: config.json backups @ NA Ingest: ~/Library/Application Support/status-panel/config.json.bak-YYYYmmdd-HHMMSS (install.sh:24-26; 46 of them today) | today: install.sh makes one whenever the config differs | Verify: ls config.json.bak-*. Useful as the history/rollback source the UI needs (R8) | other campus: same
- [runtime-data] NA Ingest: watchdog :: SSH multiplex sockets @ NA Ingest: /tmp/status-panel-ssh-%C (ssh_cmd status_panel.py:1748-1753: ControlMaster=auto, ControlPersist=120, BatchMode, ConnectTimeout=5, IdentitiesOnly, StrictHostKeyChecking=accept-new) | today: created by ssh on first use per host | Verify: ls /tmp/status-panel-ssh-*; pgrep -lf 'ssh.*id_status_panel'. Panel: all remote checks/fixes | other campus: same | (need not be editable)
- [runtime-data] NA Ingest: watchdog :: Long-lived MIDI session streams to NA Tracks and ProPresenter Mac @ NA Ingest: one ssh per host (stream_remote_midi status_panel.py:1700-1734) running /usr/bin/python3 -u - with coremidi.py piped over stdin; nothing installed on the far Mac | today: started by start_pollers for every midi_peer check with a 'host' | Verify: pgrep -lf 'ServerAliveInterval=2' on NA Ingest (one per host, e.g. PID 6558); MIDI key shows no 'NO SESSION'. Panel: MIDI (FX LIGHTS, TRK LIVEBOX, TRK STUDIO B, PROP LIGHTS) | other campus: any Mac with Remote Login + /usr/bin/python3
- [runtime-data] NA Ingest: watchdog :: ATEM watcher subprocess (atem_watch.js on Companion's node) @ NA Ingest: <Companion 5.0.7.app>/Contents/Resources/node-runtimes/node22/bin/node + ~/Library/Application Support/companion/modules/bmd-atem-3.19.1/node_modules/atem-connection (config atem.node / atem.library globs; newest() status_panel.py:2089; AtemWatcher 2098-2164) | today: globs resolve to the newest Companion app and newest bmd-atem module; restarts itself every 5 s if it dies | Verify: pgrep -lf atem_watch.js; log 'ATEM watcher stopped' absent; ATEM key green. Panel: ATEM AUDIO/CAMERA; atem_audio/atem_camera fixes | other campus: depends on the Companion app version and bmd-atem module being installed there
- [runtime-data] NA Ingest: watchdog :: Art-Net listener UDP 6454 @ NA Ingest: watchdog socket on 0.0.0.0:6454 with SO_REUSEPORT (start_artnet_listener status_panel.py:2184-2208) | today: started when any 'artnet' check exists | Verify: lsof -nP -iUDP:6454; log 'Art-Net listener could not start' absent. Panel: VISTA (SILENT) | other campus: the lighting console's Art-Net must reach the Ingest Mac's subnet
- [runtime-data] NA Ingest: watchdog :: Bonjour browse of _apple-midi._udp @ NA Ingest: /usr/bin/dns-sd -B _apple-midi._udp local. every 15 s for 3 s (poll_mdns status_panel.py:1510) | today: built in | Verify: dns-sd -B _apple-midi._udp local. lists 'Lighting PC' and 'NA Tracks'. Panel: MIDI (LIGHT PC MIDI GONE, TRACKS MIDI GONE) | other campus: instance names differ per site (config mdns.instance)
- [external-setup] NA Ingest: watchdog :: Local CoreMIDI session read + 'NA Ingest Listen' session (optional) @ NA Ingest: coremidi.py via ctypes reads the network driver's apple.midirtp.session; session 'NA Ingest' (peers Lighting PC, SuperRack-LiveBox); optional 'NA Ingest Listen' session created in Audio MIDI Setup (README 'Optional') | today: sessions created/connected by hand in Audio MIDI Setup > MIDI Studio > Network | Verify: python3 -c 'import coremidi; print(coremidi.sessions())' in the data dir. Panel: MIDI (LIGHT PC, SUPERRACK), TRACKS IN line | other campus: session and peer names per site
- [runtime-data] NA Ingest: watchdog :: Local shell probes used by checks @ NA Ingest: /sbin/ping, /sbin/ifconfig en0, /bin/launchctl list, /usr/bin/pgrep, /usr/bin/defaults read, /usr/sbin/arp -an, shutil.disk_usage (poll_pings 1453, poll_system 1475, arp_table 1855) | today: built in | Verify: python3 status_panel.py --once prints every tile. Panel: NET, INGEST, ATEM SW MIDI, DANTE map | other campus: interface name (network.interface) and gateway per site | (need not be editable)
- [runtime-data] NA Ingest: watchdog :: Spotify control via osascript @ NA Ingest: /usr/bin/osascript 'tell application "Spotify"' every 3 s (poll_spotify status_panel.py:1619-1645, SPOTIFY_STATE 1593) + playlist list from open.spotify.com/embed and spclient.wg.spotify.com (playlist_tracks 1597) | today: built in; playlist URI in config tiles.spotify.checks[0].playlist and fixes.spotify_play.run (hardcoded twice) | Verify: SPOTIFY key green 'PLAYING'; log 'Spotify playlist list' warnings absent. Panel: SPOTIFY, rs_spotify | other campus: playlist URI per site; needs internet for the song list
- [runtime-data] NA Ingest: watchdog :: Read-only read of Companion's settings database @ NA Ingest: ~/Library/Application Support/companion/v5.0/db.sqlite opened mode=ro every 60 s (companion_db_path 1552, companion_conn_hosts 1563; table instances) | today: built in; override with config companion.db | Verify: log 'could not read connection addresses' absent. Panel: Shure/Tascam/LED pings and HyperDeck/Tascam hung-vs-off verdicts use the address set on the Companion connection | other campus: path follows the newest vN.N folder automatically | (need not be editable)
- [external-setup] NA Ingest: Companion :: Companion 5.0.7 (login item) @ NA Ingest: /Applications/Companion 5.0.7.app (also 5.0.3/5.0.4/5.0.5 and Companion.app present); HTTP API 127.0.0.1:8000; web UI http://na-ingest.local:8000 | today: installed by hand; macOS login item 'Companion' | Verify: curl -s 127.0.0.1:8000/api/connections | head -c 100; pgrep -lf 'Companion 5.0.7'. Panel: all keys; watchdog logs 'Companion unreachable' and posts alert_webhook after 30 s | other campus: version pinned in make_page.py (EXPORT_VERSION 12, COMPANION_BUILD 5.0.7+9763); old app copies could be deleted
- [external-setup] NA Ingest: Companion :: Custom variables sp_state, sp_hb, sp_cmd @ NA Ingest Companion: Variables > Custom Variables (names from config variables.state/heartbeat/command) | today: created BY HAND (README 'One-time Companion setup'); the API cannot create them (POST to a missing one -> 404) | Verify: curl -s -o /dev/null -w '%{http_code}' 127.0.0.1:8000/api/custom-variable/sp_hb/value -> 200 (all three 200 today). Panel: everything | other campus: create the three by hand on each site's Companion
- [external-setup] NA Ingest: Companion :: Custom variable flash_tick + trigger flash_tick_500 (blink clock) @ NA Ingest Companion: custom var flash_tick, trigger 'flash_tick_500' (controls table id trigger:324fEl4_yJyr6Uu71jvId, enabled) | today: pre-existing trigger; must stay enabled (README step 3) | Verify: custom-variable/flash_tick/value -> 200 and toggles 0/1; trigger enabled in Triggers page. Panel: blinking of every key and nav key | other campus: recreate the trigger on each site (not exported by make_page.py)
- [external-setup] NA Ingest: Companion :: Page 19 'Status' (8x4 keys) @ NA Ingest Companion page 19 (config page, page_name); built by make_page.py -> status-page.companionconfig; imported by push_page.py (tRPC importSinglePage) or by hand | today: python3 make_page.py; python3 push_page.py (refuses unless page empty or already the status page) | Verify: http://na-ingest.local:8000/tablet?pages=19&cols=8&rows=4; page_export via /int/export/page/19. Panel: the whole status page | other campus: page number per site in config
- [external-setup] NA Ingest: Companion :: Page 20 'Restart' @ NA Ingest Companion page 20 (config restart_page.page); restart-page.companionconfig | today: python3 make_page.py; python3 push_page.py --which restart | Verify: tablet view pages=20. Panel: restart keys rs_* | other campus: page number per site
- [external-setup] NA Ingest: Companion :: Coloured page-number nav key on all other pages (97 pages) @ NA Ingest Companion: location 1/0 on every page replaced by a copy of page 19's converted pagenum key; record in repo nav_locations.json; templates nav_templates.json; marker 'STATUS nav key' | today: python3 nav_keys.py (tRPC controls.copyControl); --revert resets; re-run after adding pages (guideline 9) | Verify: nav_keys.py --dry-run lists nothing to change; cat nav_locations.json. Panel: ALL colour visible from every page; tap -> page nav_tap_page, hold nav_hold_ms -> page 19 | other campus: run nav_keys.py on each site; nav_locations.json is per site
- [external-setup] NA Ingest: Companion :: Tascam quiet module 1.3.1-quiet @ NA Ingest: ~/Library/Application Support/companion/modules/tascam-da-6400-1.3.1-quiet (built by tascam-quiet/make_quiet_tascam.py from tascam-da-6400-1.3.0; tgz imported under Modules > Import module package; connection 'tascam' set to that version) | today: by hand in the Companion UI | Verify: ls the modules folder; Connections > tascam shows 1.3.1-quiet. Panel: TASCAM key (log flood otherwise) | other campus: import the tgz on each site that has a DA-6400
- [external-setup] NA Ingest: Companion :: Companion module folders used by the panel @ NA Ingest: ~/Library/Application Support/companion/modules/ (bmd-atem-3.19.1 used by atem_watch.js; bmd-hyperdeck-3.2.0, shure-wireless-2.3.1, shure-psm1000-2.0.3, audinate-dantecontroller-2.0.3-heartbeat-auto, novastar-controller-2.6.4, resi-decoders-2.0.0, resi-studio-cues-0.1.5, obs-studio-3.15.3, resolume-arena-3.12.0, generic-midi-1.4.0, ableton-osc-2.0.0, renewedvision-propresenter-api-1.2.0-rc4 etc.) | today: Companion module store / manual imports | Verify: ls that folder; each connection's version in the UI. Panel: all conn/variable checks | other campus: module set differs per site
- [external-setup] NA Ingest: Companion :: Companion Satellite (login item) @ NA Ingest: ~/Applications/Companion Satellite.app (running, PID 874) | today: installed by hand; login item | Verify: pgrep -lf 'Companion Satellite'. Panel: not checked today; surfaces it feeds are unknown | other campus: not determined
- [external-setup] NA Ingest: Companion :: Stream Deck surfaces (XL; MK.2 and Mini show top-left corner) @ NA Ingest Companion: Surfaces page; db tables surfaces/surface_groups | today: plugged in / configured in Companion UI (surface 'use last page' setting matters for the nav key, memory note) | Verify: Companion > Surfaces lists them (not read in this audit). Panel: which physical keys show pages 19/20 | other campus: per site
- [external-setup] NA Ingest: Companion connections :: HyperDeck_1..HyperDeck_6 (bmd-hyperdeck) + tcp-HyperDeck-1..6 (generic-tcp-udp) @ NA Ingest Companion connections; deck IPs on each connection (watchdog reads var 'ip' / db); control port 9993 probed by hyperdeck_probe status_panel.py:1802 | today: Companion UI; config tiles hd1..hd6 (HD2/HD4 optional:true) | Verify: /api/connections shows all 6 'good'; HD keys green with record time. Panel: HD1-6, REC group | other campus: which decks exist and which are optional is config; tcp-HyperDeck-n connections are separate (purpose not determined)
- [external-setup] NA Ingest: Companion connections :: tascam (tascam-da-6400 1.3.1-quiet) -> DA-6400 10.1.11.74 @ NA Ingest Companion connection 'tascam'; ping 10.1.11.74 (config fallback host) | today: Companion UI; config tiles.tascam | Verify: connection good; ping 10.1.11.74. Panel: TASCAM (HUNG - POWER CYCLE IT / NO NETWORK), rs_tascam | other campus: address per site
- [external-setup] NA Ingest: Companion connections :: x32 (behringer-x32) [m32 connection exists but OFF] @ NA Ingest Companion connection 'x32' | today: Companion UI | Verify: connection good. Panel: X32, AUDIO group | other campus: console model/label per site
- [external-setup] NA Ingest: Companion connections :: ATEM (bmd-atem) -> switcher 10.1.10.2 @ NA Ingest Companion connection 'ATEM'; config atem.host 10.1.10.2 | today: Companion UI; config atem | Verify: connection good; atem_watch reports connected. Panel: ATEM, VIDEO group | other campus: switcher IP per site
- [external-setup] NA Ingest: Companion connections :: audinate-dantecontroller (module 2.0.3-heartbeat-auto) -> 19 Dante devices @ NA Ingest Companion connection 'audinate-dantecontroller'; vars devices, _ip; config tiles.dante.devices (19 names -> short labels) | today: Companion UI; config dante check 'devices' map and 'fix' map | Verify: /api/variable/audinate-dantecontroller/devices/value lists 19. Panel: DANTE (19/19, X GONE), AUDIO group | other campus: device list per site
- [external-setup] NA Ingest: Companion connections :: Axient_1 10.1.1.101, Axient_2 10.1.1.102, ULXD-4 10.1.1.106 (shure-wireless); PSM_1 .103, PSM_2 .104, PSM_3 .105 (shure-psm1000) @ NA Ingest Companion connections; addresses read from Companion db every 60 s, config host = fallback | today: Companion UI; config hidden tiles axient1/2, ulxd, psm1-3 (channels 4/4/1/2/2/2) | Verify: connections good; ping each. Panel: RF group (battery, interference, NO PING) | other campus: RF inventory per site
- [external-setup] NA Ingest: Companion connections :: ableton (ableton-osc), AbletonOSC (generic-osc), AbleSet (OFF) -> Tracks Mac @ NA Ingest Companion connections | today: Companion UI | Verify: 'ableton' good. Panel: APPS (ABLETON), rs_ableton watch conn:ableton | other campus: per site
- [external-setup] NA Ingest: Companion connections :: Propresenter-API (renewedvision-propresenter-api) -> ProPresenter Mac @ NA Ingest Companion connection 'Propresenter-API' (ignore_warning:true in config) | today: Companion UI | Verify: connection good. Panel: APPS (PROPRES OFFLINE) | other campus: per site
- [external-setup] NA Ingest: Companion connections :: BibleTV (generic-osc) -> Bible TV PC @ NA Ingest Companion connection 'BibleTV' | today: Companion UI | Verify: connection good. Panel: APPS (BIBLE OSC) | other campus: per site
- [external-setup] NA Ingest: Companion connections :: lighting-midi (generic-midi) -> Lighting PC @ NA Ingest Companion connection 'lighting-midi' | today: Companion UI | Verify: connection good. Panel: MIDI (COMPANION MIDI) | other campus: per site
- [external-setup] NA Ingest: Companion connections :: OBS-Youtube (obs-studio) -> LiveStream PC @ NA Ingest Companion connection 'OBS-Youtube' (service_only:true) | today: Companion UI | Verify: connection good. Panel: OBS, STREAM group; obs fix reconnects it | other campus: per site
- [external-setup] NA Ingest: Companion connections :: arena (resolume-arena) -> Lighting PC :8080 @ NA Ingest Companion connection 'arena' | today: Companion UI | Verify: connection good. Panel: ARENA, VIDEO group | other campus: per site
- [external-setup] NA Ingest: Companion connections :: RESI_Decoder (resi-decoders, RossTalk) 10.1.10.203 and resi-studio-cues @ NA Ingest Companion connections | today: Companion UI | Verify: connections good; ping 10.1.10.203. Panel: RESI, STREAM group | other campus: per site
- [external-setup] NA Ingest: Companion connections :: VX1000 10.1.14.116 and UHDJr 10.1.14.115 (novastar-controller) @ NA Ingest Companion connections | today: Companion UI | Verify: connections good AND ping (module keeps saying Connected when off). Panel: LED, VIDEO group | other campus: LED processors per site
- [runtime-data] NA Ingest: Companion connections :: Connections NOT watched by the panel @ NA Ingest Companion: 7 Govee lights, DiGiCo_OSC (OFF), m32 (OFF), AbleSet (OFF), tcp-HyperDeck-1..6 | today: Companion UI | Verify: /api/connections. Panel: none (candidates to add via the UI) | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: Resi bridge LaunchAgent org.waterschurch.resi-propresenter @ NA Ingest:
/Library/LaunchAgents/org.waterschurch.resi-propresenter.plist -> /usr/bin/python3 '/Library/Application Support/resi-propresenter/resi_to_propresenter.py' (KeepAlive, RunAtLoad) | today: separate project, installed by hand; fix 'bridge' = launchctl kickstart -k / bootstrap | Verify: launchctl list | grep resi-propresenter (PID 843). Panel: INGEST (BRIDGE), rs_bridge. Never restart during a service (guideline 10) | other campus: only where ProPresenter stage countdown to Resi cues is used - [external-setup] NA Ingest: apps the panel watches :: Lobby captions server LaunchAgent org.waterschurch.lobby-captions @ NA Ingest: ~/Library/LaunchAgents/org.waterschurch.lobby-captions.plist -> /usr/bin/python3 ~/LobbyCaptions/server.py --channels Speaker,MC | today: separate project; fix 'captions' kickstarts both captions agents | Verify: launchctl list | grep lobby-captions (PID 837). Panel: INGEST (CAPTIONS), rs_captions | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: Lobby caption screen LaunchAgent org.waterschurch.lobby-captions-display @ NA Ingest: ~/Library/LaunchAgents/org.waterschurch.lobby-captions-display.plist -> ~/LobbyCaptions/LobbyCaptionsDisplay.app --config ~/LobbyCaptions/display.json | today: separate project; fix 'caption_screen' | Verify: launchctl list (PID 838). Panel: INGEST (CAPTION SCREEN) | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: ProdCom.app (transcription, HTTP :24480) @ NA Ingest: /Applications/ProdCom.app (running PID 548); check http://127.0.0.1:24480/api/v1/ping every 5 s; fix 'prodcom' pkill/open; also restarted after dvs_ingest | today: how it starts at login NOT determined (not a login item, not a LaunchAgent) | Verify: curl 127.0.0.1:24480/api/v1/ping; pgrep -x ProdCom. Panel: INGEST (PRODCOM DOWN), rs_prodcom | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: ProdDash.app (login item, HTTP :24500) @ NA Ingest: /Applications/ProdDash.app (login item; launchd shows application.org.waterschurch.proddash.*); checks process match + http://127.0.0.1:24500/ every 10 s; fix 'proddash' | today: macOS login item | Verify: curl -I 127.0.0.1:24500; pgrep -x ProdDash. Panel: INGEST (PRODDASH), rs_proddash | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: Spotify.app + playlist Christian EDM 2026 @ NA Ingest: ~/Applications/Spotify.app (NOT /Applications); playlist spotify:playlist:2uTpJVYQLFnKF1WnSOuco8 | today: installed by hand; playlist URI hardcoded in config twice (check + fix run string) | Verify: pgrep -x Spotify; SPOTIFY key 'PLAYING'. Panel: SPOTIFY, rs_spotify, SHOW group | other campus: playlist per site; open -g -a Spotify works wherever the app is
- [external-setup] NA Ingest: apps the panel watches :: ATEM Software Control (login item) + midiEnabled preference @ NA Ingest: /Applications/Blackmagic ATEM Switchers/ATEM Software Control.app; defaults domain 'com.blackmagic-design.ATEM Software Control' key midiEnabled must be 0 (pref check; fix atem_midi quits/reopens the app) | today: installed with ATEM Switchers package; login item | Verify: defaults read 'com.blackmagic-design.ATEM Software Control' midiEnabled -> 0. Panel: ATEM (ATEM SW MIDI ON) | other campus: per site
- [external-setup] NA Ingest: apps the panel watches :: Dante Virtual Soundcard on NA Ingest (root LaunchDaemon) @ NA Ingest: /Library/LaunchDaemons com.audinate.dante.DanteVirtualSoundcard (dvsd, apec, conmon_server, ptp running); state wantToRun in /Library/Preferences/com.audinate.dante.dvs.plist; Dante name STUDIO-A-Ingest on en17 | today: Audinate installer; fix dvs_ingest = sudo -n /usr/local/sbin/status-panel-dvs-start && open -g -a DVS, then ProdCom restart | Verify: pgrep -f DanteVirtualSoundcard/apec; defaults read /Library/Preferences/com.audinate.dante.dvs wantToRun -> 1. Panel: DANTE (INGEST GONE), rs_dvs_ingest | other campus: per Mac with DVS
- [external-setup] NA Ingest: apps the panel watches :: Church Translate DISABLED on NA Ingest @ NA Ingest: ~/Library/LaunchAgents.disabled/org.waterschurch.translate.plist (open -a Church Translate; must NOT run here, it runs on NA Translation) | today: moved out of ~/Library/LaunchAgents by hand 2026-10-08 | Verify: launchctl list | grep translate -> nothing; pgrep -x 'Church Translate' -> nothing on NA Ingest | other campus: document which Mac owns each app
- [external-setup] NA Ingest: apps the panel watches :: Google Drive (ATEM restore file source) @ NA Ingest: ~/Library/CloudStorage/GoogleDrive-media@waterschurch.org (login item 'Google Drive'); restore file Backup/NA/ATEM/'4k plus 2026-10-07 14-59-55.xml' copied to repo atem-reference.xml | today: copy a new restore XML over atem-reference.xml in the repo, re-run install.sh | Verify: ls that CloudStorage folder; cmp repo atem-reference.xml with the Drive file. Panel: ATEM AUDIO/CAMERA reference | other campus: each site keeps its own restore XML
- [runtime-data] NA Ingest: apps the panel watches :: Free disk on / @ NA Ingest: shutil.disk_usage('/') every 10 s; warn_gb 25, err_gb 8 | today: config tiles.ingest disk check | Verify: df -g /. Panel: INGEST (DISK xGB) | other campus: thresholds per site
- [runtime-data] NA Ingest: other background tasks (not the panel) :: org.waterschurch.checklist-build-runner (Gitea act-runner daemon) @ NA Ingest: ~/Library/LaunchAgents/org.waterschurch.checklist-build-runner.plist -> ~/act-runner/gitea-runner daemon (KeepAlive) | today: other project (checklist app); not touched by the panel | Verify: launchctl list (PID 839). Shows on the machine map only | other campus: n/a
- [runtime-data] NA Ingest: other background tasks (not the panel) :: org.waterschurch.checklist-drive-backup (calendar) and checklist-weekly-health (calendar) @ NA Ingest: ~/Library/LaunchAgents/... -> ~/checklist-app/backup-to-drive.sh and ~/checklist-app/scripts/weekly-health.sh (StartCalendarInterval, RunAtLoad false) | today: other project | Verify: launchctl list shows them with '-' (idle). Machine map only | other campus: n/a
- [runtime-data] NA Ingest: other background tasks (not the panel) :: Other login items / agents: Itsycal, Alfred 5, Google Drive, Google Updater/keystone, Signiant (local.launch.signiantapp) @ NA Ingest: login items list + ~/Library/LaunchAgents | today: by hand | Verify: osascript 'tell application "System Events" to get the name of every login item'. Machine map only | other campus: n/a
- [external-setup] NA Ingest: network :: en0 10.1.1.1 (main LAN, gateway 10.1.0.1) and second address 10.6.3.222 @ NA Ingest: config network.interface en0, network.gateway 10.1.0.1; windows-setup.ps1:18 allows SSH only from 10.1.1.1 and 10.6.3.222 (which interface carries 10.6.3.222 NOT determined) | today: macOS network settings; IPs hardcoded in windows-setup.ps1 | Verify: ipconfig getifaddr en0; ifconfig | grep 10.6.3.222; ping 10.1.0.1. Panel: NET (link, gateway, blips) | other campus: Ingest IPs per site must be edited in windows-setup.ps1 and config
- [external-setup] NA Ingest: network :: en17 Dante port 169.254.144.168 (STUDIO-A-Ingest) @ NA Ingest: en17; arp -an on it feeds the Dante map | today: physical cabling + DVS interface choice | Verify: ipconfig getifaddr en17; dante-map.json lists STUDIO-A-Ingest on 10.66.1.102 0/3 | other campus: per site
- [external-setup] NA Ingest: network :: en20 10.66.1.2 Netgear management link @ NA Ingest: en20 -> switches 10.66.1.100 and 10.66.1.102 (telnet 23); NETGEAR Engage.app also on this Mac | today: physical link + static IP by hand; config dante_switches | Verify: ipconfig getifaddr en20; nc -z 10.66.1.100 23. Panel: DANTE PoE power-cycle, map | other campus: switch IPs per site
- [external-setup] ProPresenter Mac :: Host 'prop' napropresenter@10.1.14.225 ('NA ProPresenter Mac (2)', macOS 26.2) @ config hosts.prop.ssh; Remote Login ON (System Settings > Sharing) | today: Remote Login by hand; key via mac-authorize.sh (ARD Send UNIX Command as root) | Verify: ssh -i ~/.ssh/id_status_panel napropresenter@10.1.14.225 hostname (ok today). Panel: APPS (PROPRES NOT RUNNING, PROP TV DOWN), MIDI (PROP LIGHTS), DANTE (PROP MAC) | other campus: user@ip per site
- [secret] ProPresenter Mac :: authorized_keys entry status-panel@NA-Ingest @ ProPresenter Mac: ~napropresenter/.ssh/authorized_keys (1 line today) | today: remote-access/mac-authorize.sh pasted into ARD (public key hardcoded in the script) | Verify: grep -c status-panel ~/.ssh/authorized_keys -> 1 | other campus: new site = new key pair -> edit the hardcoded key in mac-authorize.sh and windows-setup.ps1
- [secret] ProPresenter Mac :: DVS helper /usr/local/sbin/status-panel-dvs-start + /etc/sudoers.d/status-panel @ ProPresenter Mac: root-owned 755 script (defaults write wantToRun 1; launchctl kickstart -k system/com.audinate.dante.DanteVirtualSoundcard) + sudoers NOPASSWD for that one script (installed 2026-10-08 12:05) | today: remote-access/mac-dvs-helper.sh via ARD as root; undo: rm both files | Verify: sudo -n -l /usr/local/sbin/status-panel-dvs-start (sudo-ok today); ls -l both. Panel: fix dvs_prop, rs_dvs_prop | other campus: run the same script on each DVS Mac
- [external-setup] ProPresenter Mac :: Dante Virtual Soundcard engine (NA-ProPresenter-Mac) @ ProPresenter Mac: root LaunchDaemon com.audinate.dante.DanteVirtualSoundcard; wantToRun=1; engine running today | today: Audinate installer | Verify: pgrep -f DanteVirtualSoundcard/apec; Companion Dante devices list has NA-ProPresenter-Mac. Panel: DANTE PROP MAC GONE | other campus: per Mac
- [external-setup] ProPresenter Mac :: ProPresenter app (newest /Applications/ProPresenter*.app, 21.4) + API :1025 @ ProPresenter Mac: fix propresenter = pkill + open newest; checks pgrep -xq ProPresenter (SSH every 10 s) and http://10.1.14.225:1025/ every 5 s | today: installed by hand; API enabled in ProPresenter prefs | Verify: curl -I http://10.1.14.225:1025/; ssh ... pgrep -x ProPresenter. Panel: APPS, rs_propres | other campus: per site
- [external-setup] ProPresenter Mac :: Network MIDI session 'NA ProP' with peer Lighting PC (10.1.10.206:5004) @ ProPresenter Mac: CoreMIDI network driver (read via the ssh python stream; reconnect via connect_peer over ssh) | today: Audio MIDI Setup on that Mac by hand; config tiles.midi check PROP LIGHTS | Verify: MIDI key has no 'PROP LIGHTS'; log of the stream. Panel: MIDI | other campus: session/peer names per site
- [external-setup] Tracks Mac :: Host 'tracks' waterschurchmedia@10.1.3.227 (Ableton Live 11 Suite + DVS) @ config hosts.tracks.ssh; Remote Login turned ON 2026-10-08 (was off) | today: Remote Login by hand; key via mac-authorize.sh (ARD) | Verify: ssh ... hostname. TODAY: 'Host is down' at 10.1.3.227 (Mac off or asleep). Panel: APPS (ABLETON NOT RUNNING, TRACKS MAC ping), MIDI (FX LIGHTS, TRK LIVEBOX, TRK STUDIO B, TRACKS MIDI), DANTE (TRACKS) | other campus: per site
- [secret] Tracks Mac :: authorized_keys entry + DVS helper + sudoers @ Tracks Mac: ~/.ssh/authorized_keys; /usr/local/sbin/status-panel-dvs-start; /etc/sudoers.d/status-panel (verified installed 2026-10-08 per memory; NOT re-verified today, host down) | today: mac-authorize.sh and mac-dvs-helper.sh via ARD as root | Verify when up: grep -c status-panel ~/.ssh/authorized_keys; sudo -n -l /usr/local/sbin/status-panel-dvs-start. Panel: fix dvs_tracks, rs_dvs_tracks | other campus: same scripts
- [external-setup] Tracks Mac :: Ableton Live 11 Suite + last-set lookup @ Tracks Mac: /Applications/Ableton Live 11 Suite.app; last set parsed from ~/Library/Preferences/Ableton/Live*/Log.txt ('Loading document', templates skipped) by fixes ableton / ableton_restart | today: app installed by hand; app name hardcoded in both fix strings | Verify: ssh ... pgrep -x Live; Companion 'ableton' good. Panel: APPS, rs_ableton | other campus: Live version/app name per site
- [external-setup] Tracks Mac :: Network MIDI session 'NA Tracks' (peers Lighting PC 10.1.10.206:5004, SuperRack-LiveBox 10.1.13.140:5004, Studio B Mac Studio via Bonjour) + Bonjour advert 'NA Tracks' @ Tracks Mac: CoreMIDI network driver; read by the ssh stream; mDNS instance seen from NA Ingest | today: Audio MIDI Setup on the Tracks Mac by hand; config midi_peer checks with host tracks | Verify: dns-sd -B _apple-midi._udp shows 'NA Tracks'; MIDI key clean. Panel: MIDI (FX LIGHTS, TRK LIVEBOX, TRK STUDIO B, TRACKS MIDI GONE) | other campus: per site
- [external-setup] Tracks Mac :: Dante Virtual Soundcard (NA-Tracks) @ Tracks Mac: root LaunchDaemon com.audinate.dante.DanteVirtualSoundcard | today: Audinate installer | Verify: Dante devices list has NA-Tracks. Panel: DANTE TRACKS GONE | other campus: per Mac
- [external-setup] NA Translation Mac mini :: Host 'translation' studio@10.1.7.39 ('NA Translation', macOS 26.5.1) @ config hosts.translation.ssh; Remote Login ON | today: Remote Login by hand; key via mac-authorize.sh | Verify: ssh ... hostname (ok today). Panel: APPS (TRANSLATE NOT RUNNING), DANTE (TRANSL) | other campus: per site
- [secret] NA Translation Mac mini :: authorized_keys + DVS helper + sudoers @ NA Translation: ~/.ssh/authorized_keys (1); /usr/local/sbin/status-panel-dvs-start; /etc/sudoers.d/status-panel (installed 2026-10-08 12:05, sudo-ok today) | today: mac-authorize.sh and mac-dvs-helper.sh via ARD as root | Verify: sudo -n -l /usr/local/sbin/status-panel-dvs-start. Panel: fix dvs_translation, rs_dvs_transl | other campus: same scripts
- [external-setup] NA Translation Mac mini :: Church Translate.app (+ its translation server) @ NA Translation: /Applications/Church Translate.app; checks pgrep -xq 'Church Translate'; fixes translate (open if closed) / translate_restart (kills process group then open) | today: installed by hand | Verify: ssh ... pgrep -x 'Church Translate'. Panel: APPS, rs_translate | other campus: per site
- [external-setup] NA Translation Mac mini :: Dante Virtual Soundcard (NA-Translation) @ NA Translation: root LaunchDaemon; wantToRun=1, engine running today | today: Audinate installer | Verify: pgrep -f DanteVirtualSoundcard/apec. Panel: DANTE TRANSL GONE | other campus: per Mac
- [external-setup] Studio B Mac Studio :: Host 'studiob' studiob@10.1.13.175 ('NA Studio B (4)', macOS 26.5) @ config hosts.studiob.ssh; Remote Login ON; authorized_keys (1) | today: Remote Login by hand; key via mac-authorize.sh | Verify: ssh ... hostname (ok today). Panel: APPS (PT SOLO NOT AFL, PT SOLO CHECK OFF), MIDI (TRK STUDIO B) | other campus: per site
- [external-setup] Studio B Mac Studio :: Solo-mode helper LaunchAgent org.waterschurch.status-panel.solo-mode @ Studio B: ~/Library/LaunchAgents/org.waterschurch.status-panel.solo-mode.plist -> ~/Library/Application Support/status-panel/solo-mode-agent (compiled with /usr/bin/swiftc from remote-access/solo-mode-agent.swift; KeepAlive); running PID 1051 | today: scp solo-mode-agent.swift + install-solo-mode.sh to /tmp on that Mac, then ssh 'zsh /tmp/install-solo-mode.sh' (needs swiftc = Xcode CLT there) | Verify: launchctl list | grep solo-mode; solo-mode.json younger than 20 s with trusted:true. Panel: APPS PT SOLO checks, fix solo_afl | other campus: only where Pro Tools solo mode must be watched
- [runtime-data] Studio B Mac Studio :: solo-mode.json (state) and solo-mode.fix (trigger file) @ Studio B: ~/Library/Application Support/status-panel/solo-mode.json written every 1 s ({afl, latch, bad, running, trusted, time, error}); touching solo-mode.fix makes the helper tick AFL+Latch | today: helper writes json; fix solo_afl touches .fix over ssh then greps json | Verify: cat solo-mode.json (today afl:true latch:true trusted:true). Panel: APPS | other campus: same
- [secret] Studio B Mac Studio :: Accessibility permission for solo-mode-agent (TCC) @ Studio B: System Settings > Privacy & Security > Accessibility > solo-mode-agent allowed | today: allowed by hand once on that Mac's screen (the helper prompts via AXIsProcessTrustedWithOptions), then helper restart | Verify: solo-mode.json trusted:true (true today); otherwise panel says PT SOLO CHECK OFF | other campus: must be re-granted after recompiling the binary
- [external-setup] Studio B Mac Studio :: Pro Tools.app @ Studio B: /Applications/Pro Tools.app (bundle com.avid.ProTools); Options > Solo Mode must be AFL + Latch | today: installed by hand | Verify: solo-mode.json running:true. Panel: APPS | other campus: per site
- [external-setup] Studio B Mac Studio :: MIDI peer 'Studio B Mac Studio' in the NA Tracks session (Bonjour address '\tStudio B Mac Studio\tlocal.') @ config tiles.midi check TRK STUDIO B; reconnect via Tracks Mac | today: Audio MIDI Setup on Studio B / Tracks by hand | Verify: MIDI key clean. Panel: MIDI | other campus: per site
- [external-setup] Lighting PC :: Host 'lighting' media@10.1.10.206 (LIGHTING-PC, Windows; firewall blocks ping) @ config hosts.lighting.ssh; no MAC (no Wake-on-LAN entry) | today: windows-setup.ps1 run by hand as administrator (Set-ExecutionPolicy RemoteSigned CurrentUser first) | Verify: ssh ... 'echo %COMPUTERNAME%' (ok today). Panel: VISTA, ARENA, MIDI (LIGHT PC, FX LIGHTS, PROP LIGHTS, LIGHT PC MIDI GONE) | other campus: per site
- [external-setup] Lighting PC :: OpenSSH Server service sshd (Automatic, restarts on failure) @ Lighting PC: Windows capability OpenSSH.Server; sc failure sshd restart/5000 x3 (windows-setup.ps1:21-40) | today: windows-setup.ps1 step 1 | Verify: sc query sshd -> RUNNING (yes today) | other campus: same script
- [external-setup] Lighting PC :: Firewall rule OpenSSH-Server-In-TCP (Profile Any, RemoteAddress 10.1.1.1,10.6.3.222) @ Lighting PC: Windows Defender Firewall inbound rule Name OpenSSH-Server-In-TCP / DisplayName 'OpenSSH Server (sshd)' (windows-setup.ps1:41-47) | today: windows-setup.ps1 step 1 (needs the user's OK for firewall changes, guideline 12) | Verify: PowerShell Get-NetFirewallRule -Name OpenSSH-Server-In-TCP | Get-NetFirewallAddressFilter (netsh by display name found nothing today, so NOT confirmed) | other campus: Ingest IPs hardcoded in the script
- [secret] Lighting PC :: authorized keys: C:\ProgramData\ssh\administrators_authorized_keys + %USERPROFILE%.ssh\authorized_keys @ Lighting PC: both files hold status-panel@NA-Ingest; ACL Administrators+SYSTEM only (windows-setup.ps1:49-61) | today: windows-setup.ps1 step 2 | Verify: key login works (it does today) | other campus: public key hardcoded in the script
- [external-setup] Lighting PC :: Scheduled task \StatusPanel\Start Vista @ Lighting PC: task runs \Vista3\Vista.exe (G:) as the interactive user, Limited; fix 'vista' = schtasks /Run (Status: Running today; Vista.exe up) | today: windows-setup.ps1 step 3 (app auto-discovered) | Verify: schtasks /Query /TN \StatusPanel\Start Vista; tasklist | findstr Vista.exe. Panel: VISTA hold + auto-fix | other campus: the script finds the apps present on that PC
- [external-setup] Lighting PC :: Vista autoload preference (opens Prop Takeover) @ Lighting PC: E:\media\Documents\Vista 3.preferences -> E:\media\Documents\Vista 3\Shows (set by hand 2026-10-08) | today: edited by hand on the PC (not in any script) | Verify: open the .preferences file; starting Vista loads the show. Panel: VISTA fix opens the right show (guideline 6) | other campus: per site show path
- [external-setup] Lighting PC :: Scheduled task \StatusPanel\Start Arena (Resolume Arena, web :8080) @ Lighting PC: C:\Program Files\Resolume Arena*\Arena.exe; fix 'arena' then Companion reconnect (Running today) | today: windows-setup.ps1 step 3 | Verify: schtasks /Query /TN \StatusPanel\Start Arena; tasklist | findstr Arena.exe; Companion arena good. Panel: ARENA | other campus: same
- [external-setup] Lighting PC :: Scheduled task \StatusPanel\Start rtpMIDI (tray app) + rtpMIDIService (restarts itself on failure) @ Lighting PC: C:\Program Files (x86)\Tobias Erichsen\rtpMIDI\rtpMIDI.exe; service rtpMIDIService sc failure restart/5000 (windows-setup.ps1:112-116); fix rtpmidi_start = net start service, then task; NEVER restart a running rtpMIDI | today: windows-setup.ps1 step 3 | Verify: sc query rtpMIDIService -> RUNNING (yes); tasklist rtpMIDI.exe -> NOT running today (tray app closed; sessions live in the service). Panel: MIDI LIGHT PC MIDI GONE fix | other campus: same
- [external-setup] Lighting PC :: Network profile must stay Public (rtpMIDISvc Bonjour firewall rules are Public-only) @ Lighting PC: Windows network category; firewall rules named rtpMIDISvc (26 rules seen today) | today: by hand; README MIDI row (2026-10-08) | Verify: Get-NetConnectionProfile NetworkCategory = Public; dns-sd on NA Ingest shows 'Lighting PC' | other campus: same gotcha on any Windows rtpMIDI PC
- [runtime-data] Lighting PC :: Art-Net polls from 10.1.10.206 to UDP 6454 (Vista heartbeat) @ config tiles.vista artnet check source 10.1.10.206 max_silence 5 s | today: Vista sends ArtPoll every 0.5 s by itself | Verify: VISTA key green. Panel: VISTA SILENT | other campus: source IP per site
- [external-setup] Lighting PC :: rtpMIDI session peer 'Lighting PC' 10.1.10.206:5004 in NA Ingest / NA Tracks / NA ProP sessions @ config tiles.midi midi_peer checks LIGHT PC, FX LIGHTS, PROP LIGHTS (address 10.1.10.206:5004) | today: rtpMIDI UI on the PC + Audio MIDI Setup on the Macs by hand | Verify: MIDI key clean. Panel: MIDI, hold = midi_reconnect from the Mac side | other campus: peer name/address per site
- [external-setup] LiveStream PC :: Host 'livestream' media@10.1.9.126 (LIVESTREAM, Windows) + Wake-on-LAN mac 6c:24:08:d7:fb:57 broadcast 10.1.255.255 @ config hosts.livestream (ssh, mac, wake_broadcast); wake_on_lan status_panel.py:1759 sends UDP 9 magic packet when ssh exits 255 | today: windows-setup.ps1 (sshd, key, WoL adapter) + BIOS WoL by hand | Verify: ssh ... 'echo %COMPUTERNAME%' (ok today); Get-NetAdapterPowerManagement Ethernet WakeOnMagicPacket Enabled (yes today). Panel: OBS (WAKING PC) | other campus: MAC/broadcast per site
- [secret] LiveStream PC :: sshd + firewall rule + authorized keys @ LiveStream PC: sshd RUNNING today; OpenSSH-Server-In-TCP rule; administrators_authorized_keys + user authorized_keys | today: windows-setup.ps1 steps 1-2 | Verify: sc query sshd; key login works | other campus: same script
- [external-setup] LiveStream PC :: Scheduled task \StatusPanel\Start OBS (RunLevel Highest, --disable-shutdown-check) @ LiveStream PC: D:\Program Files\obs-studio\bin\64bit\obs64.exe ticked 'Run as administrator' so the task runs elevated (windows-setup.ps1:84-86,101); fix 'obs' then Companion reconnect (task Ready, obs64.exe running today) | today: windows-setup.ps1 step 3 | Verify: schtasks /Query /TN \StatusPanel\Start OBS; tasklist obs64.exe; Companion OBS-Youtube good. Panel: OBS | other campus: same
- [external-setup] Bible TV PC :: Host 'bibletv' media@10.6.7.13 (SANCTUARYTV, Windows 11, Wi-Fi only, blocks ping) @ config hosts.bibletv.ssh; checked via http://10.6.7.13:8080/ every 5 s | today: windows-setup.ps1 by hand | Verify: ssh ... (ok today); curl -I http://10.6.7.13:8080/. Panel: APPS (BIBLE APP DOWN, BIBLE OSC) | other campus: per site
- [secret] Bible TV PC :: sshd + firewall rule + authorized keys @ Bible TV PC: sshd RUNNING today; rule + keys per windows-setup.ps1 | today: windows-setup.ps1 steps 1-2 | Verify: sc query sshd; key login works | other campus: same script
- [external-setup] Bible TV PC :: Scheduled task \StatusPanel\Start BibleApp (also AtLogOn + 15 s) @ Bible TV PC: 'Bible App.exe' found by the script; task Running today (4 Bible App.exe processes); fix bibleapp = taskkill then schtasks /Run | today: windows-setup.ps1 step 3 (AtLogon true) | Verify: schtasks /Query /TN \StatusPanel\Start BibleApp; tasklist | findstr Bible. Panel: APPS hold (never auto) | other campus: same
- [external-setup] Bible TV PC :: Wi-Fi power settings (power plan wireless = max performance; adapter PnPCapabilities=24) @ Bible TV PC: powercfg + registry (set by hand 2026-10-11; not in any script) | today: by hand | Verify: powercfg /q (wireless adapter settings); Ethernet recommended instead | other campus: only Wi-Fi machines
- [external-setup] Devices: video :: ATEM switcher 10.1.10.2 @ Companion 'ATEM' connection + atem_watch.js direct connection; reference atem-reference.xml | today: config atem.host; restore file copied by hand | Verify: ATEM key green; ping 10.1.10.2. Panel: ATEM | other campus: IP + XML per site
- [external-setup] Devices: video :: HyperDecks 1-6 (IPs set on the Companion connections; control port 9993) @ Companion HyperDeck_n; watchdog reads var 'ip' and the db; hold = connection off/on + 15 s check; never reboots | today: Companion UI; config tiles hd1..hd6 (optional:true on HD2/HD4) | Verify: nc -z 9993; HD keys green. Panel: HD1-6 | other campus: deck count/optional flags per site
- [external-setup] Devices: video :: LED processors VX1000 10.1.14.116, UHDJr 10.1.14.115 @ Companion novastar connections + pings | today: config tiles.led | Verify: ping both. Panel: LED | other campus: per site
- [external-setup] Devices: audio/RF :: Tascam DA-6400 10.1.11.74 @ Companion 'tascam' + ping | today: config tiles.tascam | Verify: ping; Companion good. Panel: TASCAM | other campus: per site
- [external-setup] Devices: audio/RF :: X32 (STUDIO-A-X32 on Dante too) @ Companion 'x32' (address on the connection) | today: Companion UI | Verify: Companion good. Panel: X32 | other campus: per site
- [external-setup] Devices: audio/RF :: Shure AD4Q x2 (10.1.1.101/.102), ULXD4 (.106), PSM1000 x3 (.103-.105) @ Companion Shure connections + 3 s pings | today: config hidden RF tiles | Verify: ping each; RF key green. Panel: RF | other campus: per site
- [external-setup] Devices: audio/RF :: SuperRack LiveBox 10.1.13.140:5004 (MIDI peer) / FOH-SuperRack-LiveBox (Dante) @ config midi_peer SUPERRACK + TRK LIVEBOX; Dante map 10.66.1.102 0/9 | today: config | Verify: MIDI key clean; Dante list. Panel: MIDI, DANTE | other campus: per site
- [external-setup] Devices: Resi/LED/network :: Resi decoder 10.1.10.203 (RossTalk) and encoder 10.1.10.202 (LAOEncoder web), Resi cloud https://central.resi.io/ @ Companion RESI_Decoder + resi-studio-cues; pings; HEAD http checks every 10 s / 30 s | today: config tiles.resi | Verify: ping .203 .202; curl -I http://10.1.10.202/; curl -I https://central.resi.io/. Panel: RESI | other campus: per site
- [external-setup] Devices: Resi/LED/network :: Gateway 10.1.0.1 + NA Ingest link en0 @ config network (gateway, interface, burst_* thresholds, remember_seconds 300) | today: config | Verify: ping 10.1.0.1; ifconfig en0 'status: active'. Panel: NET (BLIP) | other campus: per site
- [external-setup] Dante network :: Netgear M4250 switches 10.66.1.100 (8G2XF-PoE+) and 10.66.1.102 (26G4XF-PoE+), telnet CLI port 23, fw 13.0.5.24 @ config dante_switches; netgear.py (Switch class, survey, poe_cycle_device; MAX_SHARED 2) | today: switches managed in NETGEAR Engage; IPs in config | Verify (GUI session on NA Ingest, not over ssh): python3 netgear.py lookup ; python3 netgear.py poe-status 10.66.1.102 0/14. Panel: DANTE hold power-cycle (dante-auto), dante-map | other campus: switch IPs + keychain login per site
- [runtime-data] Dante network :: Studio B PoE-powered Dante boxes (MON 0/15, SOLO-MON 0/14, TB 0/16, TB-MON 0/17 on 10.66.1.102) + other 15 hardware devices @ dante-map.json; config dante check fix map ('*': dante-auto) | today: automatic by MAC; no config edit when re-patched | Verify: dante-map.json shows poe:true on those four. Panel: DANTE hold = PoE reset for them, Companion module restart for the rest | other campus: automatic
- [secret] Secrets & permissions :: SSH key pair id_status_panel (private on NA Ingest) @ NA Ingest: ~/.ssh/id_status_panel + .pub (config ssh_key); comment status-panel@NA-Ingest; public key HARDCODED in remote-access/mac-authorize.sh:5 and windows-setup.ps1:17 | today: generated 2026-10-08 by hand; never leaves NA Ingest | Verify: ls -l ~/.ssh/id_status_panel; ssh-keygen -lf id_status_panel.pub matches the key in the scripts | other campus: each site Ingest Mac makes its own key; the UI must generate/show the public key and feed it into the setup scripts instead of the hardcoded line
- [secret] Secrets & permissions :: known_hosts (accept-new) @ NA Ingest: ~/.ssh/known_hosts (+ .old); ssh_cmd uses StrictHostKeyChecking=accept-new | today: first connection records the host key automatically | Verify: ssh-keygen -F 10.1.14.225 etc. A reinstalled machine with a new host key makes every check fail until the old line is removed | other campus: same
- [secret] Secrets & permissions :: Netgear login in NA Ingest login keychain (service netgear-status-panel, account admin) @ NA Ingest: login keychain generic password; read by netgear.credentials() via /usr/bin/security; only readable in the GUI session (LaunchAgent), not over ssh | today: security add-generic-password -s netgear-status-panel -a admin -w, typed by the user in Terminal on NA Ingest | Verify: security find-generic-password -s netgear-status-panel (attributes only; exists today, acct admin); dante-map log line appears. Panel: DANTE PoE, dante-map | other campus: store the item on each site's Ingest Mac; never typed by Claude (guideline 11)
- [secret] Secrets & permissions :: sudoers NOPASSWD for the DVS helper (4 Macs) @ NA Ingest, ProPresenter Mac, Tracks Mac, NA Translation: /etc/sudoers.d/status-panel ' ALL=(root) NOPASSWD: /usr/local/sbin/status-panel-dvs-start' (visudo-checked) | today: mac-dvs-helper.sh via ARD as root (NA Ingest 2026-10-08 12:38, others 12:05) | Verify: sudo -n -l /usr/local/sbin/status-panel-dvs-start on each (ok on Ingest, ProP, Translation today; Tracks not reachable). Panel: DVS fixes | other campus: same script
- [secret] Secrets & permissions :: Accessibility (TCC) for solo-mode-agent on Studio B @ Studio B: Privacy & Security > Accessibility | today: granted by hand on that Mac's screen | Verify: solo-mode.json trusted:true | other campus: per Mac; re-grant after rebuild
- [secret] Secrets & permissions :: Automation (TCC) for the watchdog to control Spotify via osascript @ NA Ingest: Privacy & Security > Automation (python3 / launchd context -> Spotify); prompt appears on NA Ingest's screen | today: NOT determined how/when it was granted; Spotify hold works per the user, and poll_spotify logs an error string if osascript is refused | Verify: log has no 'spotify' osascript errors; SPOTIFY key shows PLAYING not '?'. Panel: SPOTIFY | other campus: grant once per Ingest Mac
- [secret] Secrets & permissions :: Remote Login (SSH) enabled on each Mac + ssh access group membership @ ProPresenter Mac, Tracks Mac, NA Translation, Studio B, NA Ingest: System Settings > General > Sharing > Remote Login (com.apple.access_ssh group; mac-authorize.sh prints ssh-group) | today: by hand (Tracks turned on 2026-10-08) | Verify: ssh from NA Ingest works; sudo systemsetup -getremotelogin on the Mac | other campus: per Mac
- [workflow-step] Secrets & permissions :: Apple Remote Desktop (ARD) used to push root commands to the Macs @ User's admin Mac: ARD > Manage > Send UNIX Command as root (used for mac-authorize.sh and mac-dvs-helper.sh) | today: user pastes the script text by hand | Verify: script prints 'OK user=... mac=...'. The UI can only show the text to paste, never run it | other campus: same
- [secret] Secrets & permissions :: Windows: run PowerShell as administrator + Set-ExecutionPolicy RemoteSigned (CurrentUser) @ Lighting, LiveStream, Bible TV PCs (windows-setup.ps1 header steps 2-3) | today: user does it at the PC; system/firewall changes need the user's OK (guideline 12) | Verify: the script's RESULT block (pc, user, sshd, nic mac/ip/network, tasks) | other campus: same
- [secret] Secrets & permissions :: MacBook keychain item gitea-mcp (git push credential) and key id_ed25519_checklist_ingest @ MacBook: login keychain; ~/.ssh/ | today: by hand | Verify: git push works; ssh checklist-ingest works | other campus: n/a | (need not be editable)
- [config-key] Secrets & permissions :: alert_webhook (empty today) @ config alert_webhook ''; post_webhook status_panel.py:2334 posts JSON {text} when Companion is down >30 s | today: config edit | string URL or ''. Verify: log 'webhook failed' absent | other campus: per site
- [workflow-step] Workflow steps :: Deploy the watchdog: scp + install.sh @ MacBook -> NA Ingest: scp status_panel.py coremidi.py netgear.py config.json install.sh atem_watch.js atem-reference.xml checklist-ingest:status-panel-install/; ssh checklist-ingest 'zsh ~/status-panel-install/install.sh' | today: by hand from the MacBook (README 'Changing things'); install.sh validates JSON, backs up config, restarts the agent | Verify: 'installed org.waterschurch.status-panel' echo; sp_hb fresh | other campus: same per site; the UI on the Ingest Mac should replace the scp step
- [workflow-step] Workflow steps :: Rebuild + import pages when the layout changes @ python3 make_page.py (imports status_panel.load_config); python3 push_page.py [--which restart] (tRPC WebSocket /trpc, Companion 5.0.7; refuses non-empty foreign pages) | today: by hand; only needed for tile positions/labels/new keys, not for check or fix edits | Verify: tablet view of pages 19/20; page_export name '19\nStatus' | other campus: same
- [workflow-step] Workflow steps :: Re-colour nav keys after pages move @ python3 nav_keys.py (--dry-run / --revert); nav_locations.json committed | today: by hand (guideline 9) | Verify: --dry-run reports nothing | other campus: same
- [workflow-step] Workflow steps :: Offline tests @ MacBook: python3 -m unittest discover -s tests (tests/test_status_panel.py, fake Companion, MIDI loopback; stubs ssh/fixes) | today: by hand before deploy (guideline 15) | Verify: all tests pass | other campus: same | (need not be editable)
- [workflow-step] Workflow steps :: One-shot poll without writing @ NA Ingest or MacBook: python3 status_panel.py --once (and --dry-run) | today: by hand | prints every tile row/col/level/text; useful as the UI's 'test this config' button | other campus: same
- [workflow-step] Workflow steps :: New ATEM restore file @ Copy the ATEM Software Control .xml from Google Drive Backup/NA/ATEM over repo atem-reference.xml, run install.sh (watchdog rebuilds atem-reference.json on start) | today: by hand | Verify: ATEM key green after restart | other campus: per site
- [workflow-step] Workflow steps :: Companion one-time setup per site @ Companion UI: create sp_state/sp_hb/sp_cmd, keep flash_tick_500 enabled, import pages, import tascam quiet module, name page '19\nStatus' | today: by hand (README 'One-time Companion setup') | Verify: custom-variable endpoints 200; trigger enabled | other campus: repeat per site