Files
status-indicators/install.sh
T
christianandClaude Fable 5.1 01c18ba5ef Step 1: the settings page, read-only, live on NA Ingest at port 8020
Overview (problems in the room with the hold action, the live deck, one line per computer),
Keys (both pages, what each key will say and what holding does), Computers (map with the keys
that turn red, cards with a light per thing that runs there from the watchdog's snapshot and a
read-only probe, Gear, Setup), Logs, History (the installer's copies for now), Help (four cards,
the generated key guide, cheat sheet and Where things are, glossary, Companion facts), Site setup
and Fixes read-only. The editing code is checked (with a lockout) but nothing can change yet.

Also: faults are attributed to the computers their check is about; the installer refuses while a
HyperDeck is recording (--now overrides); the Lighting PC's MIDI session is listed; the probes read
sudo's answer correctly and expand $HOME outside the quotes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 12:18:36 -04:00

187 lines
7.7 KiB
Bash

#!/bin/zsh
# Install or update the status panel watchdog on this Mac (the Ingest Mac) as a LaunchAgent.
# Re-run after changing any file in this folder.
#
# zsh install.sh install / update and (re)start
# zsh install.sh --keep-config update the code but leave the live config.json as it is
# zsh install.sh --rollback put the previous code back (kept in prev/) and restart
# zsh install.sh --remove stop it and remove the LaunchAgent (files stay)
#
# The config is validated with the NEW code before anything is touched, so a stricter check can
# never leave the watchdog crash-looping. The previous code is kept in prev/ for --rollback.
set -e
SRC=${0:A:h}
LABEL=org.waterschurch.status-panel
SETTINGS_LABEL=org.waterschurch.status-settings
# App Support, not ~/Documents: launchd-run python3 cannot read ~/Documents.
DEST="$HOME/Library/Application Support/status-panel"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
DOMAIN="gui/$(id -u)"
CODE=(status_panel.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json)
KEEP_CONFIG=0
for arg in "$@"; do
case "$arg" in
--keep-config) KEEP_CONFIG=1 ;;
--remove|--rollback|--now) ;;
*) echo "unknown option $arg" >&2; exit 2 ;;
esac
done
restart_agent() { # $1 = label, $2 = plist
local label=${1:-$LABEL} plist=${2:-$PLIST}
launchctl bootout "$DOMAIN/$label" 2>/dev/null || true
# bootout returns before the old job is gone; bootstrapping too early fails with "5: Input/output error".
for i in {1..50}; do launchctl print "$DOMAIN/$label" >/dev/null 2>&1 || break; sleep 0.2; done
for attempt in 1 2 3; do
launchctl bootstrap "$DOMAIN" "$plist" && break
[[ $attempt == 3 ]] && { echo "launchctl bootstrap failed for $label" >&2; exit 1; }
sleep 1
done
}
SETTINGS_PLIST="$HOME/Library/LaunchAgents/$SETTINGS_LABEL.plist"
write_settings_plist() {
cat > "$SETTINGS_PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>$SETTINGS_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>-m</string>
<string>settings.server</string>
<string>--data-dir</string>
<string>$DEST</string>
</array>
<key>WorkingDirectory</key><string>$DEST</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
<key>AbandonProcessGroup</key><true/>
<key>StandardErrorPath</key><string>$HOME/Library/Logs/status-settings.err.log</string>
</dict>
</plist>
EOF
}
write_plist() { # $1 = exit timeout in seconds
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>$LABEL</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>$DEST/status_panel.py</string>
<string>--data-dir</string>
<string>$DEST</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
<key>ExitTimeOut</key><integer>$1</integer>
<key>StandardErrorPath</key><string>$HOME/Library/Logs/status-panel.err.log</string>
</dict>
</plist>
EOF
}
wait_for_start() { # $1 = install time (unix): wait for a start stamp newer than that
for i in {1..60}; do
if [[ -f "$DEST/started.json" ]] && /usr/bin/python3 - "$DEST/started.json" "$1" <<'PY' 2>/dev/null
import json, sys
d = json.load(open(sys.argv[1]))
sys.exit(0 if d.get("started_at", 0) >= float(sys.argv[2]) else 1)
PY
then
echo "running (code $(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version"))' "$DEST/started.json"))"
return 0
fi
sleep 0.5
done
echo "WARNING: no start stamp after 30 s - check ~/Library/Logs/status-panel.log and status-panel.err.log" >&2
return 1
}
if [[ "$1" == "--remove" ]]; then
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
launchctl bootout "$DOMAIN/$SETTINGS_LABEL" 2>/dev/null || true
rm -f "$PLIST" "$SETTINGS_PLIST"
echo "removed $LABEL and $SETTINGS_LABEL (files left in $DEST)"
exit 0
fi
if [[ "$1" == "--rollback" ]]; then
[[ -d "$DEST/prev" ]] || { echo "nothing to roll back to ($DEST/prev is missing)" >&2; exit 1; }
for f in "$DEST"/prev/*(N); do cp "$f" "$DEST/"; done
/usr/bin/python3 "$DEST/status_panel.py" --check --config "$DEST/config.json"
T=$(date +%s)
write_plist "$(/usr/bin/python3 "$DEST/status_panel.py" --exit-timeout --config "$DEST/config.json")"
restart_agent
echo "rolled back to the previous code (version $(/usr/bin/python3 -c 'import sys; sys.path.insert(0, sys.argv[1]); import status_panel; print(status_panel.VERSION)' "$DEST"))"
wait_for_start "$T" || true
exit 0
fi
# 0. Not while a HyperDeck is recording (the watchdog restarts for a few seconds); --now overrides.
if [[ -f "$DEST/live-now.json" && " $* " != *" --now "* ]] && /usr/bin/python3 - "$DEST/live-now.json" <<'PY'
import json, sys, time
d = json.load(open(sys.argv[1]))
sys.exit(0 if d.get("live") and time.time() - d.get("time", 0) < 60 else 1)
PY
then
echo "a HyperDeck is recording right now ($(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("reason"))' "$DEST/live-now.json")) - not installing; use --now to override" >&2
exit 3
fi
# 1. The config that will run, checked with the code that will run it.
mkdir -p "$DEST"
if [[ $KEEP_CONFIG == 1 && -f "$DEST/config.json" ]]; then
CFG="$DEST/config.json"
else
CFG="$SRC/config.json"
fi
/usr/bin/python3 "$SRC/status_panel.py" --check --config "$CFG"
EXIT_TIMEOUT=$(/usr/bin/python3 "$SRC/status_panel.py" --exit-timeout --config "$CFG")
# 2. Keep the code that runs now, for --rollback.
mkdir -p "$DEST/prev"
for f in $CODE; do [[ -f "$DEST/$f" ]] && cp "$DEST/$f" "$DEST/prev/"; done
# 3. Files. The live config is backed up when it changes; --keep-config leaves it alone.
if [[ $KEEP_CONFIG == 0 ]]; then
if [[ -f "$DEST/config.json" ]] && ! cmp -s "$SRC/config.json" "$DEST/config.json"; then
cp "$DEST/config.json" "$DEST/config.json.bak-$(date +%Y%m%d-%H%M%S)"
echo "config.json replaced (the old one is kept as config.json.bak-...; use --keep-config to leave it)"
fi
cp "$SRC/config.json" "$DEST/"
fi
for f in $CODE; do [[ -f "$SRC/$f" ]] && cp "$SRC/$f" "$DEST/"; done
[[ -f "$SRC/atem-reference.xml" ]] && cp "$SRC/atem-reference.xml" "$DEST/"
# The settings page (its own LaunchAgent) and the record of coloured page-number keys (site-owned: never overwritten).
if [[ -d "$SRC/settings" ]]; then
rm -rf "$DEST/settings" && cp -R "$SRC/settings" "$DEST/" && find "$DEST/settings" -name "__pycache__" -prune -exec rm -rf {} + 2>/dev/null
fi
[[ -f "$SRC/nav_locations.json" && ! -f "$DEST/nav_locations.json" ]] && cp "$SRC/nav_locations.json" "$DEST/"
if [[ -d "$SRC/companion-objects" ]]; then
rm -rf "$DEST/companion-objects" && cp -R "$SRC/companion-objects" "$DEST/"
fi
# 4. The LaunchAgent, with the exit timeout this config needs (a running fix finishes before a restart).
T=$(date +%s)
write_plist "$EXIT_TIMEOUT"
restart_agent
echo "installed $LABEL -> $DEST (exit timeout ${EXIT_TIMEOUT}s)"
echo "log: ~/Library/Logs/status-panel.log"
wait_for_start "$T" || true
if [[ -d "$DEST/settings" ]]; then
write_settings_plist
restart_agent "$SETTINGS_LABEL" "$SETTINGS_PLIST"
PORT=$(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("site",{}).get("settings_port",8020))' "$DEST/config.json")
echo "settings page: http://$(hostname -s).local:$PORT/ (log: ~/Library/Logs/status-settings.log)"
fi