"""The registry of every setting the status panel has. One record per config key (path, type, default, plain-words help, which screen of the settings page owns it, whether it sits under Advanced), the check types with their fields, the fix kinds, the words a key can show, and the rules a config must pass. The settings page draws its forms from this file; tests/test_schema.py keeps it in step with the code (every key the watchdog reads must be declared here, every default in the code must equal the one here). Paths: "companion.host", "tiles[].checks[].conn" (a list element), "fixes.*.run" (a map entry). """ import copy import re import status_panel as sp # --- Field records ----------------------------------------------------------- # type: str | int | float | bool | list | dict | enum | path | ip | mac | url | hhmm | days | id # apply: what changes when the value does: "restart" (the watchdog restarts), "pages" (the Companion # pages are rebuilt and re-imported when the built page differs), "none" (page-only data), "settings". F = {} def field(path, type, default=None, help="", screen="", advanced=False, secret=False, apply="restart", required=False, options=None, lo=None, hi=None, regex=None, example=None, shows=None): F[path] = {"path": path, "type": type, "default": default, "help": help, "screen": screen, "advanced": advanced, "secret": secret, "apply": apply, "required": required, "options": options, "lo": lo, "hi": hi, "regex": regex, "example": example, "shows": shows} # -- site -- field("site", "dict", {}, "This campus: its name, this Mac, and when changes must wait.", "Site") field("site.id", "id", "na", "Short name used for folders and files.", "Site", advanced=True, regex=r"[a-z0-9_-]+") field("site.name", "str", "", "The campus, as people say it.", "Site", example="North Attleboro", apply="none") field("site.ingest_name", "str", "NA Ingest", "What this Mac is called in logs, notes and alerts.", "Site", apply="pages") field("site.ingest_hostname", "str", "", "This Mac's network name, used in the page address and by the page tools.", "Site", example="na-ingest.local", apply="none") field("site.ingest_ips", "list", [], "This Mac's addresses, for the PCs' firewall allow-lists.", "Site", apply="none") field("site.contacts", "str", "", "Printed on the cheat sheet: who to call.", "Site", apply="none") field("site.alert_prefix", "str", "", "The first words of an alert message; blank = this Mac's name.", "Site", advanced=True) field("site.live_services", "list", [], "Clock windows during which changes to the panel wait (a recording " "HyperDeck always counts as live, whatever the clock says).", "Schedules") field("site.live_services[].days", "days", None, "Days of the week.", "Schedules", required=True) field("site.live_services[].start", "hhmm", None, "Start time.", "Schedules", required=True) field("site.live_services[].end", "hhmm", None, "End time; earlier than the start means it runs past midnight.", "Schedules", required=True) field("site.editing_code", "str", "", "The code that switches Editing on. Kept here, in the repo, so everyone " "with the repo has it.", "Site", advanced=True, secret=True, apply="none") field("site.settings_port", "int", 8020, "Where the settings page listens.", "Site", advanced=True, lo=1024, hi=65535, apply="settings") field("site.dvs_helper", "path", "/usr/local/sbin/status-panel-dvs-start", "The DVS helper on each Mac that runs " "Dante Virtual Soundcard.", "Site", advanced=True) field("site.solo_dir", "path", "~/Library/Application Support/status-panel", "The Pro Tools helper's folder on " "the Studio B Mac.", "Site", advanced=True) # -- companion -- field("companion", "dict", {}, "Where Companion answers.", "Companion", required=True) field("companion.host", "str", "127.0.0.1", "Companion's address from this Mac.", "Companion", advanced=True) field("companion.port", "int", 8000, "Companion's HTTP port.", "Companion", advanced=True, lo=1, hi=65535) field("companion.db", "path", None, "Companion's settings database; blank = the newest one found.", "Companion", advanced=True) field("companion.export_version", "int", sp.__dict__.get("EXPORT_VERSION", 12), "Companion's page export format " "number (changes with a Companion upgrade).", "Companion", advanced=True, apply="pages") field("companion.build", "str", "5.0.7+9763-stable-cec2f88e6f", "The Companion build the pages are written for.", "Companion", advanced=True, apply="pages") field("page", "int", 19, "The Status page's number in Companion.", "Keys", lo=1, apply="pages") field("page_name", "str", "19\\nStatus", "The Status page's name in Companion.", "Keys", advanced=True, apply="pages") field("nav_keys", "list", ["pagenum"], "Which of Companion's page keys (page up, page number, page down) are " "coloured like the ALL key on every page.", "Companion", advanced=True, options=["pageup", "pagenum", "pagedown"], apply="pages") field("nav_hold_ms", "int", 1000, "Holding the page-number key this long jumps to the Status page.", "Companion", advanced=True, lo=200, hi=5000, apply="pages") field("nav_tap_page", "int", 1, "Tapping the page-number key goes to this page.", "Companion", advanced=True, lo=1, apply="pages") field("variables", "dict", {}, "The custom variables the panel uses inside Companion.", "Companion", required=True) field("variables.state", "str", "sp_state", "The variable the keys are drawn from.", "Companion", advanced=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("variables.heartbeat", "str", "sp_hb", "The variable that says the watchdog is alive.", "Companion", advanced=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("variables.command", "str", "sp_cmd", "The variable key presses are written to.", "Companion", advanced=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("variables.flash", "str", "flash_tick", "The blink clock. Renaming it means renaming it inside the trigger " "in Companion too.", "Companion", advanced=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("page_grid", "dict", {"minColumn": 0, "maxColumn": 8, "minRow": 0, "maxRow": 5}, "The export grid " "(the deck is the 8x4 inside it).", "Companion", advanced=True, apply="pages") field("page_stale_seconds", "int", 15, "Keys turn purple when the watchdog has been silent this long.", "Companion", advanced=True, lo=5, hi=120, apply="pages") field("page_hold_ms", "int", 2000, "How long a key is held to run its fix.", "Schedules", advanced=True, lo=500, hi=5000, apply="pages", shows="HOLD 2s") # -- schedules and timing -- field("service_windows", "list", [{"days": list(sp.DAYS), "start": "00:00", "end": "23:59"}], "When alarms are armed: always (the ALARMS key silences them until midnight).", "Schedules", advanced=True) field("service_windows[].days", "days", None, "", "Schedules", required=True) field("service_windows[].start", "hhmm", None, "", "Schedules", required=True) field("service_windows[].end", "hhmm", None, "", "Schedules", required=True) field("debounce_seconds", "int", 3, "A fault must last this long before it shows (one bad reading is not an " "alarm).", "Schedules", advanced=True, lo=0, hi=60) field("flap", "dict", {}, "Repeated faults show amber 'xN'.", "Schedules", advanced=True) field("flap.count", "int", 3, "This many faults...", "Schedules", advanced=True, lo=1, hi=20) field("flap.window_seconds", "int", 600, "...within this many seconds.", "Schedules", advanced=True, lo=60, hi=86400) field("timing", "dict", {}, "Timing numbers.", "Schedules", advanced=True) field("timing.fix_cooldown_seconds", "int", 60, "The same fix never re-runs within this.", "Schedules", advanced=True, lo=10, hi=600, shows="WAIT 1 MIN") field("timing.alert_after_seconds", "int", 30, "Send the alert message when Companion has not answered for this " "long.", "Schedules", advanced=True, lo=5, hi=600) field("alert_webhook", "url", "", "Where alert messages go (a Slack-style web hook); blank = off.", "Site", secret=True) field("auto_fix", "dict", {}, "When the panel may fix things by itself.", "Schedules") field("auto_fix.windows", "list", [], "When auto-fix may act.", "Schedules") field("auto_fix.windows[].days", "days", None, "", "Schedules", required=True) field("auto_fix.windows[].start", "hhmm", None, "", "Schedules", required=True) field("auto_fix.windows[].end", "hhmm", None, "", "Schedules", required=True) field("auto_fix.after_seconds", "int", 20, "A fault must last this long before auto-fix acts.", "Schedules", advanced=True, lo=5, hi=600) field("auto_fix.max_tries", "int", 2, "Tries per fault, then it is yours to hold.", "Schedules", advanced=True, lo=1, hi=10) field("auto_fix.retry_seconds", "int", 90, "Between tries.", "Schedules", advanced=True, lo=30, hi=3600) field("auto_fix.pause_after_network_seconds", "int", 60, "Wait this long after a network blip.", "Schedules", advanced=True, lo=0, hi=3600) field("auto_fix.forget_seconds", "int", 600, "Forget a fault's tries once it has been gone this long.", "Schedules", advanced=True, lo=60, hi=86400) field("auto_fix.restart_connections", "bool", False, "Also reconnect Companion connections by itself.", "Schedules") field("auto_fix.fixes", "list", [], "The fixes auto-fix may run (gentle ones only).", "Schedules") field("auto_fix.log_dir", "path", "~/Desktop/Auto-Fix Logs", "One file per day of everything auto-fix does.", "Schedules", advanced=True) field("auto_fix.keep_months", "int", 1, "Daily log files are deleted after this many months.", "Schedules", advanced=True, lo=1, hi=12) # -- network and Dante -- field("network", "dict", {}, "The NET key.", "Network & Dante") field("network.interface", "str", "en0", "The network port to watch for link.", "Network & Dante") field("network.gateway", "ip", "", "The router to ping; blank = off.", "Network & Dante") field("network.burst_connections", "int", 4, "A blip is this many connections...", "Network & Dante", advanced=True, lo=1, hi=50) field("network.burst_groups", "int", 3, "...from this many groups...", "Network & Dante", advanced=True, lo=1, hi=20) field("network.burst_window_seconds", "int", 10, "...dropping within this many seconds.", "Network & Dante", advanced=True, lo=1, hi=300) field("network.remember_seconds", "int", 300, "NET shows BLIP this long after.", "Network & Dante", advanced=True, lo=10, hi=3600, apply="pages") field("ssh_key", "path", "~/.ssh/id_status_panel", "The login key this Mac uses for the other computers.", "Site", advanced=True) field("dante_switches", "list", [], "The Netgear switches on the Dante network.", "Network & Dante") field("dante_map_seconds", "int", 600, "Survey the switches this often.", "Network & Dante", advanced=True, lo=60, hi=86400) field("dante_max_shared", "int", 2, "Never power-cycle a port carrying more than this many devices (an uplink).", "Network & Dante", advanced=True, lo=1, hi=10) # -- Spotify and ATEM -- field("spotify", "dict", {}, "The service playlist.", "Spotify") field("spotify.playlist", "str", "", "The playlist Spotify must be playing.", "Spotify", regex=r"spotify:playlist:[A-Za-z0-9]+", required=True) field("spotify.name", "str", "", "The playlist's name, as the keys and the key guide say it.", "Spotify") field("spotify.open_wait", "int", 8, "Seconds to wait after opening Spotify.", "Spotify", advanced=True, lo=1, hi=60) field("spotify.play_wait", "int", 3, "Seconds to wait before checking it plays.", "Spotify", advanced=True, lo=1, hi=60) field("atem", "dict", {}, "The switcher and its saved file.", "ATEM") field("atem.host", "ip", "", "The switcher's address.", "ATEM", required=True) field("atem.node", "path", "/Applications/Companion [0-9]*.app/Contents/Resources/node-runtimes/node22/bin/node", "Companion's node (pattern; the newest match is used).", "ATEM", advanced=True) field("atem.library", "path", "~/Library/Application Support/companion/modules/bmd-atem-*/node_modules/atem-connection", "Companion's ATEM library (pattern).", "ATEM", advanced=True) field("atem.reference", "path", "atem-reference.xml", "The saved switcher file to keep in line with.", "ATEM") field("atem.compare", "dict", None, "What to keep in line: audio fields and camera settings; blank = everything.", "ATEM", advanced=True) field("atem.source_folder", "path", "", "Where ATEM Software Control saves its files (for 'pick the newest').", "ATEM", apply="none") # -- hosts -- field("hosts", "dict", {}, "The computers this Mac logs in to.", "Computers") field("hosts.*.ssh", "str", None, "How this Mac logs in: user@address.", "Computers", regex=r"[^@\s]+@[^@\s]+") field("hosts.*.mac", "mac", None, "The PC's hardware address, to wake it when it is off.", "Computers") field("hosts.*.wake_broadcast", "ip", None, "The broadcast address the wake-up goes to.", "Computers", advanced=True) field("hosts.*.name", "str", None, "The computer, as people say it.", "Computers", apply="none") field("hosts.*.os", "enum", "mac", "Mac or Windows (a PC's checks are written for cmd.exe).", "Computers", options=["mac", "windows", "other"]) field("hosts.*.role", "str", None, "What it is for.", "Computers", apply="none") field("hosts.*.notes", "str", None, "Notes for operators.", "Computers", apply="none") field("hosts.*.pingable", "bool", True, "Off for a PC whose firewall blocks ping.", "Computers", apply="none") field("hosts.*.self", "bool", False, "This Mac itself.", "Computers", apply="none") field("hosts.*.runs", "list", [], "What runs on it (programs, tasks, services, helpers).", "Computers", apply="none") field("hosts.*.networks", "list", [], "Its network ports and what each is for (LAN, Dante, switch management).", "Computers", advanced=True, apply="none") field("hosts.*.manual_done", "dict", {}, "Set-up steps ticked by hand (who, when).", "Computers", apply="none") field("gear", "dict", {}, "Devices that are not computers: where each one is.", "Computers", apply="none") field("gear.*.location", "str", None, "Where it is in the building.", "Computers", apply="none") field("gear.*.notes", "str", None, "Notes for operators.", "Computers", apply="none") # -- fixes -- field("fixes", "dict", {}, "Everything a hold can run.", "Fixes") field("fixes.*.kind", "enum", "ssh", "How it is built.", "Fixes", options=list(sp.FIX_KINDS)) field("fixes.*.host", "str", None, "The computer it runs on (ssh fixes).", "Fixes") field("fixes.*.run", "str", None, "The command (zsh on Macs, cmd.exe on PCs).", "Fixes", advanced=True) field("fixes.*.timeout", "int", 60, "Give up after this many seconds.", "Fixes", advanced=True, lo=5, hi=600) field("fixes.*.short", "str", None, "The word the key shows while it runs (FIXING ...).", "Fixes", apply="pages") field("fixes.*.label", "str", None, "Plain words: what it does.", "Fixes", required=True, apply="pages") field("fixes.*.reconnect", "bool", False, "Reconnect the Companion connection afterwards.", "Fixes") field("fixes.*.conn", "str", None, "The Companion connection it reconnects.", "Fixes") field("fixes.*.restart_ok", "bool", False, "May close a running program (and still run by itself).", "Fixes") field("fixes.*.never_auto", "bool", False, "Never runs by itself.", "Fixes") field("fixes.*.part", "enum", None, "Which part of the switcher.", "Fixes", options=["audio", "camera"]) field("fixes.*.verify_seconds", "int", 20, "Wait this long before judging a reconnect.", "Fixes", advanced=True, lo=5, hi=120) field("fixes.*.template", "str", None, "How it is built (the builder); custom = typed by hand under Advanced.", "Fixes", apply="none") field("fixes.*.params", "dict", None, "The builder's fields; the command is made from them.", "Fixes", apply="none") field("tiles[].checks[].template", "str", None, "How the check's command is built.", "Keys", apply="none") field("tiles[].checks[].params", "dict", None, "The builder's fields.", "Keys", apply="none") field("fixes.*.off_seconds", "int", 2, "How long a HyperDeck's connection stays off.", "Fixes", advanced=True, lo=1, hi=10) field("fixes.*.hung_note", "str", None, "What the key says when the device answers ping but not Companion.", "Fixes", advanced=True) field("fixes.*.switch", "ip", None, "The PoE switch.", "Fixes") field("fixes.*.port", "str", None, "The PoE port (e.g. 0/14).", "Fixes", regex=r"\d+/\d+") field("fixes.*.mac", "mac", None, "The device expected on that port.", "Fixes") field("fixes.*.playlist", "str", None, "A playlist other than the site's (rare).", "Fixes", advanced=True) field("fixes.*.template", "str", None, "The builder this fix was made with.", "Fixes", advanced=True, apply="none") field("fixes.*.params", "dict", None, "The builder's fields.", "Fixes", advanced=True, apply="none") # -- tiles (keys) -- field("tiles", "list", [], "The keys on the Status page.", "Keys", required=True) field("tiles[].id", "id", None, "Used inside Companion.", "Keys", advanced=True, required=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("tiles[].label", "str", None, "The first line on the key.", "Keys", required=True, apply="pages") field("tiles[].pos", "list", None, "[row, column]; absent = no key of its own (shown inside a group).", "Keys", apply="pages") field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARMS), network (NET).", "Keys", advanced=True, options=["master", "service", "network"], apply="pages") field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys") field("tiles[].checks", "list", [], "What the key watches.", "Keys") field("tiles[].hold_always", "str", None, "A fix a hold runs even when the key is green.", "Keys", apply="pages") field("tiles[].checks[].type", "enum", None, "What kind of check.", "Keys", required=True) field("tiles[].checks[].conn", "str", None, "The Companion connection.", "Keys") field("tiles[].checks[].fix", "str", None, "What holding the key runs for this check (a fix, or a table per " "sub-check).", "Keys") field("tiles[].checks[].tag", "str", None, "Names the device when one key covers several.", "Keys") field("tiles[].checks[].service_only", "bool", False, "Legacy: only during a service (has no effect while alarms " "are armed all day).", "Keys", advanced=True) field("tiles[].checks[].debounce_seconds", "int", None, "Wait before this check's fault shows.", "Keys", advanced=True, lo=0, hi=60) field("tiles[].checks[].ignore_warning", "bool", False, "Ignore Companion's yellow warning state.", "Keys") field("tiles[].checks[].optional", "bool", False, "A HyperDeck that may have no disk (grey, no alarm).", "Keys") field("tiles[].checks[].disk_service_only", "bool", True, "NO DISK only during a service.", "Keys", advanced=True) field("tiles[].checks[].full_minutes", "int", 5, "FULL below this many minutes left.", "Keys", advanced=True) field("tiles[].checks[].low_minutes", "int", 30, "LOW below this many minutes left.", "Keys", advanced=True) field("tiles[].checks[].rec_sync", "bool", True, "Must record whenever another deck records.", "Keys") field("tiles[].checks[].ip", "ip", None, "The deck's address, instead of what Companion reports.", "Keys", advanced=True) field("tiles[].checks[].devices", "dict", None, "Dante devices: name -> key text.", "Keys") field("tiles[].checks[].count_all", "bool", False, "Count every watched Dante device on this key.", "Keys") field("tiles[].checks[].channels", "int", 1, "Channels on the receiver.", "Keys", lo=1, hi=8) field("tiles[].checks[].battery_err_bars", "int", 1, "Red at this many bars or fewer.", "Keys", advanced=True, lo=0, hi=5) field("tiles[].checks[].battery_warn_bars", "int", 2, "Amber at this many bars or fewer.", "Keys", advanced=True, lo=0, hi=5) field("tiles[].checks[].muted_is_error", "bool", False, "RF mute is red, not amber.", "Keys") field("tiles[].checks[].host", "str", None, "The computer (an address for ping, a computer name for the rest).", "Keys") field("tiles[].checks[].name", "str", None, "The name the key shows.", "Keys") field("tiles[].checks[].msg", "str", None, "The words the key shows when it fails.", "Keys") field("tiles[].checks[].failures", "int", 2, "Pings missed before NO PING.", "Keys", advanced=True, lo=1, hi=10) field("tiles[].checks[].label", "str", None, "The launchd helper.", "Keys") field("tiles[].checks[].match", "str", None, "The program (pgrep -f pattern).", "Keys") field("tiles[].checks[].url", "url", None, "The web address to fetch.", "Keys") field("tiles[].checks[].interval_seconds", "int", 30, "Fetch it this often.", "Keys", advanced=True, lo=5, hi=3600) field("tiles[].checks[].path", "path", None, "The disk to watch.", "Keys") field("tiles[].checks[].warn_gb", "int", 20, "Amber below this many GB free.", "Keys", advanced=True) field("tiles[].checks[].err_gb", "int", 5, "Red below this many GB free.", "Keys", advanced=True) field("tiles[].checks[].session", "str", None, "The network MIDI session.", "Keys") field("tiles[].checks[].peer", "str", None, "The peer that must be in it.", "Keys") field("tiles[].checks[].address", "str", None, "Where to reconnect the peer if it was never seen.", "Keys", advanced=True) field("tiles[].checks[].domain", "str", None, "The macOS preference domain.", "Keys") field("tiles[].checks[].key", "str", None, "The preference key.", "Keys") field("tiles[].checks[].want", "str", None, "The value it must have.", "Keys") field("tiles[].checks[].playlist", "str", None, "A playlist other than the site's (rare).", "Keys", advanced=True) field("tiles[].checks[].instance", "str", None, "The Bonjour MIDI name.", "Keys") field("tiles[].checks[].source", "str", None, "The MIDI source, or the Art-Net sender's address.", "Keys") field("tiles[].checks[].max_silence_seconds", "int", None, "Amber/red after this much silence.", "Keys", lo=1, hi=600) field("tiles[].checks[].cmd", "str", None, "The command run on that computer.", "Keys", advanced=True) field("tiles[].checks[].shell", "enum", None, "sh (Macs) or cmd (PCs); blank = from the computer's kind.", "Keys", advanced=True, options=["sh", "cmd"]) field("tiles[].checks[].part", "enum", None, "Which part of the switcher.", "Keys", options=["audio", "camera"]) field("tiles[].checks[].template", "str", None, "The builder this check was made with.", "Keys", advanced=True, apply="none") field("tiles[].checks[].params", "dict", None, "The builder's fields.", "Keys", advanced=True, apply="none") # -- restart page -- field("restart_page", "dict", None, "The Restart page; absent = this campus has none.", "Keys", apply="pages") field("restart_page.page", "int", 20, "Its number in Companion.", "Keys", lo=1, apply="pages") field("restart_page.page_name", "str", "20\\nRestart", "Its name in Companion.", "Keys", advanced=True, apply="pages") field("restart_page.jump_pos", "list", None, "Where the 'go to Restart page' key sits on the Status page.", "Keys", apply="pages") field("restart_page.back_pos", "list", None, "Where the 'back to Status' key sits on the Restart page.", "Keys", apply="pages") field("restart_page.keys", "list", [], "The restart keys.", "Keys", apply="pages") field("restart_page.keys[].id", "id", None, "", "Keys", advanced=True, required=True, regex=r"[A-Za-z0-9_]+", apply="pages") field("restart_page.keys[].label", "str", None, "", "Keys", required=True, apply="pages") field("restart_page.keys[].pos", "list", None, "[row, column].", "Keys", required=True, apply="pages") field("restart_page.keys[].fix", "str", None, "What holding it runs.", "Keys", required=True) field("restart_page.keys[].watch", "list", [], "Keep flashing HOLD TO FIX while these fixes' checks or " "'conn: