#!/usr/bin/env python3 """A status panel a location installs and runs on its own: no North Attleboro. python3 tools/standalone.py ab "Apollo Beach" -> dist/Apollo Beach Status Panel.zip Apollo Beach, 2026-10-21 (the user): "they want to take care of it on their own, so make a sorta standalone version that i can send them. i dont need to monitor AB from NA like i do for SC and WS". The zip holds the program (the same files a location downloads from NA, never NA's settings, plug keys or login keys), an installer and a read-me. Installed, the location sets itself up from what it finds, exactly as Southcoast and Woonsocket did, but it reports to nobody (campuses.json "standalone"), lets no other Mac in, and backs itself up every night into ~/Documents/Status Panel Backups. """ import os import re import sys import zipfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) from settings.campus import KIT_FILES, KIT_DIRS # noqa: E402 INSTALL = r"""#!/bin/zsh # Status panel for %(name)s: installs on the Mac that runs Companion and sets itself up from what it finds. # Safe to run again: it then updates the program and looks for new gear. set -e HERE=${0:A:h} if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then echo "First the Command Line Tools: a window opens - click Install, wait for it, then run this again." /usr/bin/xcode-select --install 2>/dev/null || true exit 1 fi DIR="$HOME/status-panel-install" rm -rf "$DIR" && mkdir -p "$DIR" /usr/bin/ditto "$HERE/program" "$DIR" /usr/bin/xattr -dr com.apple.quarantine "$DIR" 2>/dev/null || true [[ -f "$HOME/.ssh/id_status_panel" ]] || { mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" && /usr/bin/ssh-keygen -q -t ed25519 -N "" -C "status-panel@$(scutil --get LocalHostName)" -f "$HOME/.ssh/id_status_panel"; } cd "$DIR" zsh install.sh --site %(cid)s cd "$HOME/Library/Application Support/status-panel" /usr/bin/python3 -m settings.location setup --id %(cid)s --name %(qname)s --standalone # This Mac's wired address (the default route's port). IF=$(/sbin/route -n get default 2>/dev/null | awk '/interface:/{print $2}') ME=$(/usr/sbin/ipconfig getifaddr "$IF" 2>/dev/null || hostname) echo echo "Settings page: http://$ME:8020 (the editing code is printed above)" echo echo "Next, on each other computer here (ProPresenter, Tracks, Lighting, FOH...):" echo " Mac: open Terminal and paste curl -fsSL http://$ME:8020/join | sh" echo " (or send it to all of them at once in Apple Remote Desktop: Send UNIX Command, as root)" echo " PC: open PowerShell as administrator and paste irm http://$ME:8020/join.ps1 | iex" echo "Then on the settings page: Auto-setup > Look now." """ README = """%(name)s - Status Panel %(rule)s What it is A status page on the Stream Deck (Bitfocus Companion) that watches your gear - ProPresenter, the ATEM, Resi, consoles, Tracks, Lighting, MIDI links, the network - and fixes what it can. It runs on the Mac that runs Companion. It is yours: nothing is sent to any other location. Before you start - Companion 5.0.7 on that Mac, with its HTTP API on (Companion > Settings > Protocols). If yours is older, the installer says so and how to upgrade (download 5.0.7 for Mac from https://bitfocus.io/companion; your buttons come along). - Remote Login on for the other Macs (System Settings > General > Sharing), so the panel can check and restart programs on them. Install (about 5 minutes) 1. Copy this folder to the Companion Mac (Downloads is fine). 2. Open Terminal, type zsh and a space, drag "install.command" from this folder into the window, press Return. 3. It finds what you have, makes the Stream Deck pages (19 status, 20 restart, or the first free pages), and prints the settings page address and its editing code (PIN). Keep the PIN. 4. On each other computer, paste the line it prints (Mac: Terminal; PC: PowerShell as administrator). Then on the settings page: Auto-setup > Look now. New gear gets keys by itself. Using it (the Stream Deck) - Green means fine. Flashing red names the problem, for example "PROP OFFLINE". - Press a red key to acknowledge it: it stops flashing and stays dark red. - Hold any key for 2 seconds to fix it: it reconnects, restarts or reopens whatever is failing. - Purple means the panel itself is not running: restart the Companion Mac, or run the installer again. - ALARMS (bottom-left) silences alarms and stops automatic fixes until midnight. Use it during maintenance. - The page-number key on every page turns red when something is wrong. Hold it for 1 second to jump to the status page. - RESTART PAGE opens the restart page. Hold a key there for 2 seconds to restart that program even if it looks fine. - Automatic fixes run Tuesday 2-10 pm, Wednesday 9 am-9 pm and Sunday 5 am-2 pm. Change the hours on the settings page (Auto-fix) to match your services. - Nothing is ever restarted while the HyperDeck is recording. The settings page (http://:8020) Everything is changed there: keys, checks, fixes, auto-fix hours, the Restart page. Every Apply is kept, and History puts back any earlier one. Changing gear? Auto-setup > Look now adds what is new. Backups Every night at 4:30 am the settings are copied to ~/Documents/Status Panel Backups/%(cid)s on the Companion Mac (the newest 30 are kept). Copy that folder somewhere safe now and then, or point it at a Google Drive folder (ask whoever installed it). Run it again Running install.command again updates the program and looks for new gear; your settings stay. """ def build(cid, name, out_dir): cid = re.match(r"[a-z0-9]{1,8}", cid.lower()).group(0) top = "%s Status Panel" % name os.makedirs(out_dir, exist_ok=True) path = os.path.join(out_dir, top + ".zip") def add_file(z, src, arc, mode=0o644): info = zipfile.ZipInfo.from_file(src, arc) info.external_attr = (0o100000 | mode) << 16 info.compress_type = zipfile.ZIP_DEFLATED with open(src, "rb") as f: z.writestr(info, f.read()) def add_text(z, text, arc, mode=0o644): info = zipfile.ZipInfo(arc, date_time=(2026, 10, 21, 0, 0, 0)) info.external_attr = (0o100000 | mode) << 16 info.compress_type = zipfile.ZIP_DEFLATED z.writestr(info, text) with zipfile.ZipFile(path, "w") as z: prog = top + "/program/" for f in KIT_FILES + ("install.sh", "README.md"): p = os.path.join(ROOT, f) if os.path.exists(p): add_file(z, p, prog + f, 0o755 if f.endswith(".sh") else 0o644) for d in KIT_DIRS + ("sites/_template",): for base, dirs, files in os.walk(os.path.join(ROOT, d)): dirs[:] = [x for x in dirs if x != "__pycache__"] for f in files: if f.endswith(".pyc") or f.startswith("._") or f == ".DS_Store": continue p = os.path.join(base, f) add_file(z, p, prog + os.path.relpath(p, ROOT), 0o755 if f.endswith(".sh") else 0o644) q = "'" + name.replace("'", "'\"'\"'") + "'" add_text(z, INSTALL % {"name": name, "cid": cid, "qname": q}, top + "/install.command", 0o755) add_text(z, README % {"name": name, "cid": cid, "rule": "=" * (len(name) + 15)}, top + "/READ ME FIRST.txt") return path if __name__ == "__main__": if len(sys.argv) < 3: sys.exit(__doc__) print(build(sys.argv[1], sys.argv[2], os.path.join(ROOT, "dist")))