Files
status-indicators/install.sh
T
christianandClaude Fable 5.1 2343a27529 Step 1 in progress: the settings page server, probes, teaching pages and UI (not deployed yet)
- settings/server.py: read-only routes (overview, keys, computers, gear, companion, logs, history,
  help facts, key guide / cheat sheet / where things are), the editing-code check with a lockout,
  LAN-only binding with a per-minute rebind
- settings/probes.py: fixed read-only probes per computer; whole lists fetched and matched locally
- settings/keyguide.py: the generated teaching pages
- settings/ui: the page, every screen read-only
- status_panel.py: snapshot.json every 5 s for the page; the Companion database lookup prefers the
  running build's folder and skips a database that will not open (a stale v5.1 folder was breaking it)
- config.json: every computer's record (what runs there, network ports, done-by-hand ticks), the
  editing code (kept in the repo, the user's choice), an empty gear list
- install.sh: the settings page as a second LaunchAgent; settings/ and schema.py deployed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 16:23:59 -04:00

176 lines
7.1 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) ;;
*) 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
# 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