# Status Panel settings page — engineering details (appendix to settings-plan.md) This is the reference for building the settings page; the plain-words plan is `settings-plan.md`. Every item here follows the decisions list of 2026-10-13: it keeps K1-K18 and leaves out everything C1-C13 cut (no SIGHUP partial reload or apply classes, no queued or pending jobs, no admin code or address allow-list, no training copy, no vendor ignore lists or Add/Ignore bookkeeping, no page-set probe, no live-signal triples, no wording test over the rendered DOM, no `then` chain executor, no stop flag or commands folder, no surfaces table). Where the second full revision of the plan and the final revision's sections 10-14 differed, the final revision won. It keeps the ATEM saved-file handling, the page markers, the rollback, the identity-keyed state, the Windows echo pairs, `EXPECTED_MODULE` and overnight live-service windows. Conventions used below: - **Apply words** (what Apply does for a changed value): **restart** = the watchdog is restarted (Apply always restarts it, about 5 s of purple, outside services); **pages** = the Status or Restart page is rebuilt and re-imported because the built page JSON, with the keys' notes stripped, differs from the last verified build (never a hand-kept list; a changed note alone never forces a re-import, D11), and the page-number buttons are re-run after any page change; **none** = page-only data (notes, gear notes): no restart, no import; **settings** = the settings agent restarts. - **Screens**: always Overview, Keys, Computers (Map, card, Gear filter, Setup tab), Logs, History, Help; then under a divider "Set up once": Site setup (Site . Schedules . Companion . Network & Dante . Spotify . ATEM) and Fixes; all eight are always visible and readable by anyone, and the editing code is what lets you change things (K3). The top bar holds the site name, the search box (finds any key, fix, computer, device or setting by name), the unsaved-changes badge, the Editing toggle and a "?". - **Status words**: one legend for the whole page, printed once under the top bar's "?": OK / Problem (+ one-line reason) / Can't see from here - ask at the computer / Not needed / Done by hand (who, when). Every light anywhere on the page uses these words. - **NEW** = a config key added by this plan (its default keeps today's behaviour). **DNE** = deliberately not editable (how the panel works, not what it watches; guideline 1), documented in Help > Glossary / Where things are. - Paths: the data dir is `~/Library/Application Support/status-panel/`; the watchdog agent is `org.waterschurch.status-panel`, the settings agent `org.waterschurch.status-settings`. ## A. Everything you can change Values are typed once: every string that names a program, path, task or helper lives in one owner (`hosts..runs`, `site.*`, `spotify.name`) and checks, fixes and scripts are regenerated from it. One owner screen per value; every other place shows it read-only with a link. | Thing | Screen | Field(s) | Plain-words help | Validation / apply | |---|---|---|---|---| | `site.id` NEW | Site setup > Site (Advanced) | Site id | Short name for folders and files ("na"). | `[a-z0-9_-]+`; restart | | `site.name`, `site.ingest_name`, `site.ingest_hostname`, `site.ingest_ips[]` NEW | Site setup > Site | Site name, this Mac's name, network name, addresses | Used in log headers, alert text, key notes and generated setup scripts (replaces "NA Ingest" and the IPs in windows-setup.ps1). [Check this Mac's network ports] fills the addresses. | non-empty; IPs valid; restart (a changed note alone never forces a re-import, D11) | | `site.settings_port` NEW | Site setup > Site (Advanced) | Port | Where this page listens: loopback plus every private-address port, narrowed by this Mac's LAN list. Default 8020. | 1024-65535; the dialog states the consequence (the page moves to the new address); settings | | `site.editing_code` NEW | Site setup > Site (Advanced) | Editing code | The code that switches Editing on. It is a setting in this campus's `config.json`, kept in the private git repo, so everyone with the repo has it (the user's choice, 2026-10-13); browsers remember it with the person's first name. Masked in every API response unless the request carries it. The one code: no admin code, no address list, no page-generated code. | non-empty; changing it: the dialog says every browser must enter the new code; none | | `site.live_services[]` NEW | Site setup > Schedules | Live services | When a service is on: Apply, Run now, Restart the watchdog, Re-import, Check the switcher and Install are refused and your draft stays saved; auto-fix may run; alarms are always on. Defaults to `auto_fix.windows`; the Phase 0 migration writes it. | days in mon..sun; `HH:MM`; end <= start = runs past midnight (allowed); warn if empty; restart (ServiceMode is built at start) | | built-in live signal: a watched HyperDeck that "follows the recording" is recording | Site setup > Schedules ("Also counts as live", read-only line with "Live now?") | - | Derived from every hyperdeck check with "follows the recording"; the watchdog writes `live-now.json` on change / every 5 s with a timestamp; older than 30 s = schedule only (said in Review). No connection/variable/value triple on the page; "OBS is streaming" is added as a built-in kind only if the Phase 1 spike finds the module variable. | DNE; warn when no watched deck follows the recording | | `site.contacts` NEW | Site setup > Site | Contacts | Printed on the cheat sheet. | free text; none | | `site.dvs_helper`, `site.solo_dir` NEW | Site setup > Site (Advanced) | DVS helper path, Studio B helper folder | One owner for paths that fixes, checks and scripts repeat; builders and templates regenerate from them. | absolute path; restart | | `alert_webhook`, `site.alert_prefix` NEW, `timing.alert_after_seconds` NEW | Site setup > Site / Schedules (Advanced) | Alert message (web hook), message starts with, alert after | Posts ": Companion has not answered for N s" and the all-clear; also a line for every command edit applied. Blank = off. Shown masked everywhere ("set, 44 chars"); [Change] [Send a test alert]. | http(s) or blank; 5-600; restart (captured in run()) | | `ssh_key` | Site setup > Site (Advanced) | Login key | Private key on this Mac for every other computer; light + public half with [Copy]. [Make the login key] on the Setup tab makes the panel's own pair at a new campus (asks first). | file exists, 0600; `.pub` readable; restart | | `companion.host/port/db` | Site setup > Companion (Advanced) | Address, port, settings database | Where Companion answers. The DB is read read-only for labels and addresses (module version only if the Phase 1 spike finds it trivially); blank = newest `vN.N/db.sqlite`. push_page and nav_keys read these too. | host non-empty; 1-65535; path exists; live probe; restart | | `variables.state/heartbeat/command`, `variables.flash` NEW | Site setup > Companion (Advanced) | Variable names (state `sp_state`, heartbeat `sp_hb`, command `sp_cmd`, blink `flash_tick`) | Must exist in Companion: a hard error when Companion answers, a warning ("could not check") when it does not. Renaming the blink variable means renaming it inside the trigger in Companion too (said beside the field). | `[A-Za-z0-9_]+`; exists; pages + restart | | `page`, `page_name`; `restart_page.page`, `restart_page.page_name` | Keys tab header (only home; the Companion section links here) | Page numbers and names | Name derived "\nStatus" / "\nRestart" unless overridden. Number change: the old page keeps its keys; Apply resets our marked keys on it (asks; if the spike shows `controls.resetControl` cannot blank a cell, Review lists "delete these N keys on page 19 by hand") and re-runs the page-number buttons. Name-only change on an existing page: rename it in Companion first (live name shown). "Pages exist" light. | int >= 1; distinct; name contains the number; live checks; pages | | `nav_keys[]`, `nav_hold_ms`, `nav_tap_page` | Site setup > Companion (Advanced) | Page-number buttons that follow ALL, hold ms, tap page | Which of up / page number / down are coloured; a tap goes to page N; a hold jumps to the Status page. "Use last page" surface note shown. | subset of up/pagenum/down; 200-5000; page exists; pages | | `page_hold_ms` NEW (today's HOLD_MS) | Site setup > Schedules (Advanced, Timing) | Holding a key takes | Guideline: 2 s. The watchdog reads it for the "HOLD 2s" text. | 500-5000; pages + restart | | `page_grid` NEW (GRID) | Site setup > Companion (Advanced) | Grid | Export grid for the surface (XL 8x4 inside 0-8 x 0-5); the Keys screen draws the 8x4 by default, "Show more" shows the whole grid. | ints; every position inside; pages | | `page_stale_seconds` NEW (STALE_SECONDS) | Site setup > Companion (Advanced) | Purple after | Keys turn purple when the watchdog is silent this long. | 5-120; pages | | `companion.export_version`, `companion.build` NEW | Site setup > Companion (Advanced) | Export stamps | What make_page writes into page files and which nav template it picks (`companion-objects/nav_templates/.json`); warning when Companion's live version differs; "Recapture the nav templates" recipe linked. Change only after a Companion upgrade. | int; string; pages | | `service_windows[]` + window shape | not on the page (Help: deliberately not editable) | Alarms are armed | Always every day all day (guideline 1). A narrowed value in an old file loads with a Review warning. | days in mon..sun; `HH:MM`; restart | | `auto_fix.windows[]` | derived from live services (not shown) | - | Same windows as the live services; "May run by itself on Sundays 06:30-13:30" is built from them. | derived; restart | | `auto_fix` on/off, `after_seconds`, `max_tries`, `retry_seconds`, `pause_after_network_seconds`, `forget_seconds`, `restart_connections`, `fixes[]`, `log_dir`, `keep_months` NEW | Site setup > Schedules | Auto-fix block: tick list and "also restart Companion connections by itself" on the screen; numbers, folder and months under Advanced with defaults beside them | Gentle fixes only; a fix that closes a program needs "may close"; a "never by itself" fix is refused. | ints; fix ids exist; load_config gentle rule; folder creatable; 1-12; restart (the Engine builds its auto state at start) | | `debounce_seconds`, `flap.count`, `flap.window_seconds` | Site setup > Schedules (Advanced, Timing) | A fault must last; flapping | One bad reading does not alarm; repeated faults show amber xN. Defaults 3 s, 3 in 600 s. | 0-60; ints; restart | | `timing.fix_cooldown_seconds` NEW (60) | Site setup > Schedules (Advanced, Timing) | The same fix never re-runs within | Guards against hammering a device; the watchdog reads it as one `self.cooldown` for the "WAIT 1 MIN" text. | 10-600; restart | | `network.interface/gateway/burst_connections/burst_groups/burst_window_seconds/remember_seconds` | Site setup > Network & Dante | NET key: port and gateway on the screen; blip rule under Advanced | Port from a live list; gateway blank = off; a "blip" is N connections from M groups dropping within W s; NET shows BLIP for remember_seconds. | exists in ifconfig; IP or blank; ints >= 1; restart (pollers built at start; NET note: pages) | | `dante_switches[]`, `dante_map_seconds`, `dante_max_shared` NEW (MAX_SHARED) | Site setup > Network & Dante | Switches, survey every, never power-cycle a port carrying more than | Netgear M4250 only; empty = no map, no PoE. | IPs with telnet light; 60-86400; 1-10; restart | | keychain `netgear-status-panel` | Site setup > Network & Dante | light + account | Present/absent and user name only; the Terminal line to store it is shown ([How to add it]). | never read or written by the page | | `hosts.` | Computers (card) | Short name used inside the settings | Shown below the display name. Delete shows what refers to it and removes those too (cascade, listed). | `[a-z0-9_]+` unique; rename updates references; restart | | `hosts..ssh/mac/wake_broadcast` | Computers (card) | Log in as, wake MAC, broadcast | How this Mac logs in; wake a PC that is off (key says WAKING PC). The MIDI stream and the batched checks for that computer restart with the new address. | `user@host`; MAC 6 bytes; IP; reach light; restart | | `hosts..name/os/role/notes/pingable/self` NEW | Computers (card) | Display name, kind, what it does for the panel, notes for operators, does not answer pings, this Mac | For people, logs, scripts and probes; `pingable:false` skips the ping light; exactly one `self` record (no ssh), refused as a host for fixes, remote checks and MIDI links (builders switch to local for it). | os in mac/windows/other; one self; none | | `hosts..runs[]` NEW | Computers (card: "What runs here") | What runs here | One entry per thing, with kind `sshd`, `task`, `service`, `launchagent`, `daemon`, `app`, `web`, `helper_dvs`, `helper_solo`, `midi_session`, `artnet`, `disabled` or `other`; fields per kind: name, exe, candidates, args, at_logon, label, path, http, source, dante_name, folder, restart_on_failure, `watched` (false = listed, no light); `helper_solo` also carries `pt_bundle_id` and `solo_menu_path` (defaults = today's Pro Tools values). Feeds probes, scripts, check and fix builders: a program is named once here. | per-kind required fields; task names `[A-Za-z0-9 ]+`; Windows values cmd-safe `^[A-Za-z0-9 ._()\\:*-]+$` (no `!` or `^` either); `PROBE_SAFE` whitelist of fields that may reach a probe line; none (dependent checks/fixes regenerate -> restart) | | `hosts..networks[]` NEW | Computers (Ingest card, Advanced) | Ports | iface + use (LAN / Dante / switch management), filled by [Check this Mac's network ports]; the page binds 127.0.0.1 plus the IPv4 address of each LAN-use port (resolved from ifconfig at start, re-checked every minute, rebound on change); an empty list = every private-address port and a grey notice. | iface exists (light); settings (for the `self` record) | | `hosts..manual_done{}` NEW | Computers > Setup | Done-by-hand ticks | BIOS WoL, Public profile, Vista autoload, Wi-Fi power settings: no probe can see them, you tick them and the light says "Done by hand (Chris, 2026-10-08)". | bool + who/when; none | | `gear..location/notes` NEW | Computers > Gear filter | Where it is, notes | Plain words for people; the device itself is defined by the check that watches it (the Gear filter is a view of the Computers list, not its own data model). | free text; none | | `fixes.` | Fixes (Advanced) | Id | Referenced by keys, restart keys and auto-fix; delete cascades with a list. | `[A-Za-z0-9_]+`; not restart-connection / dante-auto; restart | | `fixes..kind` | Fixes (Built as) | Built as | ssh (default), local, atem, reconnect, midi_connect, netgear_poe, hyperdeck, `spotify` NEW; `FIX_KINDS` exported and validated by load_config. | enum; restart | | `fixes..host` | Fixes | Computer | ssh kind; validated for every fix (restart keys, hold-always, MIDI links too): exists, has ssh, not the `self` record. | alias rules; restart | | `fixes..run`, `.template`, `.params` NEW | Fixes (run under Advanced) | Generated / Custom command | zsh on Macs, cmd.exe on PCs. The builder stores template + params beside the generated `run`; the watchdog runs only `run`; every Mac value is `shlex.quote`d, every Windows value regex-checked; the dvs_ingest -> prodcom step is rendered inline by the builder (no chain executor). A raw Custom command is a "command edit": red in Review, asks for the editing code again, never run before Apply. | non-empty; `zsh -n` for local and Mac ssh commands; pkill/taskkill/killall => not gentle unless "may close"; restart | | `fixes..timeout` | Fixes | Give up after | Default 60 s. The largest timeout sizes the watchdog's ExitTimeOut at install. | 5-600; restart | | `fixes..short`, `.label` | Fixes | Key text, plain words | "FIXING "; plain words required for every fix; the Play builder derives both from `spotify.name`. | <= 14; required; restart (a changed note alone never forces a re-import, D11) | | `fixes..reconnect`, `.conn` | Fixes | Reconnect Companion connection after | conn required for kind reconnect; picked from live labels. | warning if not live; restart | | `fixes..restart_ok` | Fixes | May close a running program | Lets a closing fix be gentle (atem_midi, prodcom, proddash). Removes "Run now". | bool; restart | | `fixes..never_auto` NEW | Fixes | Never runs by itself | Generic flag (bridge: never during a service). Removes "Run now". | bool; refused in auto_fix.fixes; restart | | `fixes..part` | Fixes | Switcher part | audio or camera. | enum; restart | | `fixes..verify_seconds/off_seconds/hung_note` | Fixes (More settings) | Wait before judging, off time, still-bad text | reconnect / hyperdeck kinds. | 5-120; 1-10; <= 2 lines; restart | | `fixes..switch/port/mac` | Fixes | PoE switch, port, MAC guard | netgear_poe kind. Prefer Dante auto. No Run now. | IP in switches; `\d+/\d+`; MAC; restart | | `spotify.open_wait`, `spotify.play_wait` NEW (read by the `spotify` fix kind) | Site setup > Spotify | Play fix: open Spotify and wait N s, then wait M s before checking it plays | The fix reads playlist and name from `spotify.*` (one owner); the same for the restart key, which has no check. | 1-60; restart | | built-ins `restart-connection`, `dante-auto` | key editor picker | "Restart its Companion connection", "Dante auto" | Always offered where they apply. No Run now. | - | | per-sub fix map `{sub: fix, "*": fix}` | key editor (Advanced) | table per check | Sub names per type (device names live; conn/disk/rec; chN_batt/chN_rf; chN_mute; conn/error/caution). | values are fix ids or built-ins; restart | | implicit "enable a disabled connection" | Help; HOLDING 2 s WILL line | documented | Any held key whose connection is off in Companion turns it on. | DNE | | `tiles[].id` | Keys (Advanced) | Id | Used inside Companion; master is `all`. | `[A-Za-z0-9_]+`; unique vs restart keys; pages | | `tiles[].label` | Keys | Label | First line and prefix of faults. | <= 10 warn; pages | | `tiles[].pos` | Keys (drag) | Position / tray | Blank = in the tray "Shown inside the RF group (no key of their own)". | in grid; not col 0 rows 0-2; no overlap incl. jump/back pos; pages | | `tiles[].kind` | Keys | Kind | single key / group; ALL and ALARMS are fixed chips (moved and edited, never created or deleted); NET may be removed at a campus that does not want it (at most one; D13). | exactly one ALL, one ALARMS, at most one NET; pages | | `tiles[].members[]` | Keys | Group members | Worst member shown ("worst member + N"); hold fixes all. | ids exist; not self; a hidden key must be in a group (warn); restart (pages when hold-ability changes, detected by the build) | | `tiles[].checks[]` | Keys | Checks | See per type. Changing a key's check list drops that key's remembered acknowledgements and auto-fix tries (said in Review). | type in CHECKS; restart (pages when the build differs) | | `tiles[].hold_always` | Keys > When held | also when the key is green | Even when green (HD, TASCAM, SPOTIFY). | fix exists with label; pages | | check `type` | "+ Add a check" | question first | The menu asks a question (Is a device answering? Is a program running? Is a MIDI link up? Is a recorder OK? Is a setting right? Is Spotify playing? Is Dante complete? Is the lighting console sending?) and picks the type from the answer; the 19 raw types (conn, hyperdeck, dante, shure, psm, tascam, ping, launchd, process, http, disk, midi_peer, pref, spotify, mdns, midi_rx, remote, artnet, atem) only under Advanced. | enum | | check `conn` | check row | Companion connection | Live dropdown filtered by `EXPECTED_MODULE` for the type; a mismatch warns in Review. | required for conn/hyperdeck/dante/shure/psm/tascam; restart | | check `fix`, `tag`, `service_only`, `debounce_seconds` | check row (When held / More settings) | Fix, tag, only during a service (Advanced; warned: guideline 1; no effect while alarms are armed all day), wait | | exists; <= 12; bool; 0-60; restart | | check `template`, `params` NEW (remote, process, pref) | check row | Built as (A program is running on a Mac / on a Windows PC / A helper file is fresh or says X / A program is running on this Mac / A macOS preference equals / Custom) | Regenerates `cmd` / `match` / `domain,key` from what runs on the computer and `site.solo_dir`, so renaming a program in Computers updates the checks that watch it. | regenerated on save; test_builders_regenerate; restart | | conn `ignore_warning` | check row | Ignore Companion's yellow warning | ProPresenter flicker. | bool; restart | | hyperdeck `conn/optional/disk_service_only/full_minutes/low_minutes/rec_sync/ip` | check row | Connection, this deck may have no disk, NO DISK only in service, FULL below, LOW below, follows the recording, address override | Which HyperDecks are in use = which HD keys exist (the Add or remove a HyperDeck job also asks "may this deck have no disk?" per kept deck); "follows the recording" decks feed the live-service guard. Expects bmd-hyperdeck. | ints; IP; restart | | dante `conn/devices{}/count_all` | Site setup > Network & Dante (only home; the check row links there) | Connection, device list, every device counts | Names from the live devices variable; key texts <= 14; the first dante check feeds the map. Expects audinate-dantecontroller. | non-empty; restart | | shure `conn/channels/battery_err_bars/battery_warn_bars` | check row | Receiver, channels, red at <=, amber at <= | | 1-8; 0-5; err <= warn; restart | | psm `conn/channels/muted_is_error` | check row | Receiver, channels, a muted channel is an error | | 1-8; bool; restart | | tascam `conn` | check row | Connection | Needs the quiet module (light on the Companion section). | restart | | ping `host/conn/name/msg/failures` | check row | Address or Companion connection, name, text, misses | With conn set, Companion's address wins. | host or conn required (fixes today's CHECK BUG); 1-10; restart | | launchd `label/name` | check row | Helper (picker), name | | restart | | process `match/name` (+ template/params) | check row | Program (picker from what runs on this Mac), name | | regex compiles; restart | | http `url/msg/name/interval_seconds` | check row | Address, text, name, every N s | Any answer counts (so it is never a live signal). | URL; >= 5; restart | | disk `path/warn_gb/err_gb` | check row | Folder, amber below, red below | | exists; err < warn; restart | | midi_peer `session/peer/host/address` | check row / Add a MIDI link job | Session, peer (live pickers, local or over ssh, or a typed name when the Mac is off), Mac, fallback address | Macs only as host. The identity (host, session, peer) keys acknowledgements and Run now. | alias exists, is a Mac, has ssh, not self; restart | | pref `domain/key/want/msg` (+ template/params) | check row | Domain and key (picker from `defaults domains`), wanted value, text | live `defaults read` shown | all required; restart | | spotify `playlist/name` -> `spotify.playlist`, `spotify.name` NEW (one owner) | Site setup > Spotify (only home; the SPOTIFY key's check row links) | one link field + name | The page derives the URI and fetches the name and song count from open.spotify.com only; `spotify.name` owns the display name used by the fix label, the key guide and the notes; one permission light from the watchdog's last successful Spotify read in its log. | playlist URI regex; song-list fetch warning (private playlist); restart | | mdns `instance/name` | check row | Bonjour MIDI name (picker) | RTP-MIDI only; the Bonjour type is fixed. | restart | | midi_rx `source/name/max_silence_seconds` | check row | Source (picker), name, amber after | | restart | | remote `host/cmd/msg/shell` NEW (+ template/params) | check row | Computer, built as, text | `shell` NEW: `sh` (Macs, batched with `; echo SPn=$?`) or `cmd` (Windows PCs, batched as `(cmd) >nul 2>&1 && echo SPn=0 \|\| echo SPn=1` pairs joined with `&`, never a bare `%ERRORLEVEL%`; verified once on the Lighting PC); `cmd` is generated; Custom only under Advanced with "cannot be checked before Apply"; a PC whose OpenSSH DefaultShell is not cmd.exe gets a light and cmd-batched checks are refused; a changed remote check is marked "not run before Apply" in Review. | alias exists, has ssh, not self; `sh -n` on each command and the batched line (Mac); cmd-safe regex (Windows); restart | | artnet `source/max_silence_seconds/msg` | check row | Sender, silence, text | Port 6454 is fixed. | IP; 1-60; restart | | atem check `part/msg` | check row | Part, text | Needs the ATEM section. | restart | | `atem.host/node/library/reference`, `atem.compare` NEW, `atem.source_folder` NEW | Site setup > ATEM | Switcher address, patterns (resolved paths shown), saved file (upload or pick the newest from the folder), folder, what to keep in line | The upload (capped at 20 MB; XML with a DOCTYPE or entity declarations refused; parsed in the settings process with the watchdog's own parser) replaces `atem-reference.xml` atomically together with the config; its hash goes into History; the old file is kept dated under `history/atem/`; refused during a live service; Review shows "product name matches / N audio and M camera values differ from the current file" and needs the tick "I checked this is the right file for this switcher"; [Check the switcher now] reads only (3 s, a second connection, guarded). | IP; globs resolve; XML parses with the watchdog's own parser, no DOCTYPE or entity declarations, <= 20 MB; compare is a subset of MIX_OPTIONS/CCU_PARAMS; folder exists (light); restart | | "this campus has no ATEM" switch (the `atem` section absent) | Site setup > ATEM | tick box | Hides the section; an atem check or a saved-switcher-file fix then fails validation with the key named. | cascade listed; restart | | `restart_page.jump_pos/back_pos`, `restart_page.keys[].id/label/pos/fix/watch[]` | Keys (Restart tab) | jump/back chips; restart keys: label, position, fix, "keep flashing HOLD TO FIX while these are down" | watch = fix ids and "conn: