Apollo Beach on its own: a standalone panel to send them

- tools/standalone.py builds dist/<Name> Status Panel.zip: program, install.command, READ ME FIRST.txt
- setup --standalone: campuses.json standalone, tells nobody, no hub, no NA key
- Standalone backs itself up nightly to ~/Documents/Status Panel Backups/<id>
- upgrade-companion --from <.dmg/.zip from bitfocus.io> (no NA needed)
- Only the hub lists campuses or shows other campuses' install lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 13:12:30 -04:00
co-authored by Claude Opus 5.5
parent bea587cfad
commit 2f8e3d47a3
9 changed files with 314 additions and 21 deletions
+53 -9
View File
@@ -153,7 +153,7 @@ def wait_applied(applier, limit=240):
return applier.status or {}
def companion_version_problem(cfg):
def companion_version_problem(cfg, standalone=False):
"""None when this Companion can take the panel (5.x), else what to tell the person."""
try:
discover.companion_get(cfg, "/api/connections", 5)
@@ -162,6 +162,11 @@ def companion_version_problem(cfg):
pass
try:
discover.companion_get(cfg, "/socket.io/?EIO=4&transport=polling", 5)
if standalone:
return ("This Companion is older than version 5. The panel needs Companion 5.0.7: download it for Mac from\n"
" https://bitfocus.io/companion (5.0.7, the .dmg), then on this Mac run\n"
" cd \"$HOME/Library/Application Support/status-panel\" && /usr/bin/python3 -m settings.campus upgrade-companion --from ~/Downloads/<the .dmg>\n"
"(its settings come along; the old version stays installed), then run the installer again.")
return ("This Companion is older than version 5 (Woonsocket ran 4.0.3). The panel needs 5.0.7, as North "
"Attleboro runs. Upgrade it from NA's copy (its settings come along; the old version stays installed):\n"
" cd \"$HOME/Library/Application Support/status-panel\" && /usr/bin/python3 -m settings.campus upgrade-companion\n"
@@ -170,12 +175,18 @@ def companion_version_problem(cfg):
return "Companion does not answer on port 8000: open Companion (and its HTTP API, Settings > Protocols), then paste the line again."
def setup(data_dir, cid, name=None, say=print, home=None):
"""The first setup of a campus (or, when it is set up already, a look for what is new)."""
def setup(data_dir, cid, name=None, say=print, home=None, standalone=False):
"""The first setup of a campus (or, when it is set up already, a look for what is new).
Standalone: it reports to nobody and nobody else sees it (Apollo Beach runs its own)."""
from settings import server, campuses
if home: # where it was installed from: the campus list lives there
if home or standalone: # where it was installed from: the campus list lives there
data = campuses.load(data_dir)
data["home"] = home
if standalone:
data.pop("home", None)
data["standalone"] = True
else:
data["home"] = home
data.pop("standalone", None)
campuses.save(data_dir, data)
site = server.Site(data_dir)
cfg = site.load()
@@ -183,7 +194,7 @@ def setup(data_dir, cid, name=None, say=print, home=None):
say("%s is set up already: looking for what is new." % (cfg.get("site") or {}).get("name", "This campus"))
return look_and_add(site, say=say, force=True)
name = name or CAMPUSES.get(cid) or cid.upper()
problem = companion_version_problem(cfg)
problem = companion_version_problem(cfg, standalone)
if problem:
raise RuntimeError(problem)
for line in companion_prepare(cfg):
@@ -271,7 +282,7 @@ def clear_old_page(cfg, page):
return "cleared"
def upgrade_companion(base, say=print):
def upgrade_companion(base, say=print, source=None):
"""Companion 5.0.7 from `base` (North Attleboro's settings page), as Woonsocket was upgraded on 2026-10-20:
the running Companion is quit, the newest settings folder is copied for 5.0 (and marked newest, or the
launcher stops on "Config version mismatch"), 5.0.7 is installed beside the old one and started."""
@@ -281,6 +292,8 @@ def upgrade_companion(base, say=print):
import urllib.request
root = os.path.expanduser("~/Library/Application Support/companion")
app = "/Applications/Companion 5.0.7.app"
if not os.path.exists(app) and source: # standalone: the .dmg (or .zip) downloaded from bitfocus.io
install_companion_from(source, app, say)
if not os.path.exists(app):
say("Downloading Companion 5.0.7 from %s ..." % base)
tmp = "/tmp/companion-5.0.7.zip"
@@ -316,6 +329,35 @@ def upgrade_companion(base, say=print):
raise RuntimeError("Companion 5.0.7 did not start within 90 s: look at its window on this Mac")
def install_companion_from(source, app, say=print):
"""Companion copied from a downloaded .dmg or .zip to `app` (beside any older one)."""
import shutil
import subprocess
import tempfile
source = os.path.expanduser(source)
tmp = tempfile.mkdtemp(prefix="companion-")
try:
if source.lower().endswith(".dmg"):
mnt = os.path.join(tmp, "mnt")
subprocess.run(["/usr/bin/hdiutil", "attach", "-nobrowse", "-readonly", "-mountpoint", mnt, source], check=True, capture_output=True)
try:
found = [n for n in os.listdir(mnt) if n.startswith("Companion") and n.endswith(".app")]
if not found:
raise RuntimeError("no Companion app inside %s" % source)
subprocess.run(["/usr/bin/ditto", os.path.join(mnt, found[0]), app], check=True)
finally:
subprocess.run(["/usr/bin/hdiutil", "detach", mnt], capture_output=True)
else:
subprocess.run(["/usr/bin/ditto", "-x", "-k", source, tmp], check=True)
found = [n for n in os.listdir(tmp) if n.startswith("Companion") and n.endswith(".app")]
if not found:
raise RuntimeError("no Companion app inside %s" % source)
subprocess.run(["/usr/bin/ditto", os.path.join(tmp, found[0]), app], check=True)
say("Companion installed as %s" % app)
finally:
shutil.rmtree(tmp, ignore_errors=True)
def summary(inv):
known = set(autoconf.COMPANION)
return {"connections": len(inv.get("connections") or []),
@@ -564,13 +606,15 @@ def main(argv=None):
ap.add_argument("--data-dir", default=PARENT)
ap.add_argument("--home", help="the settings page this campus was installed from (the campus list lives there)")
ap.add_argument("--pages", help="rebuild onto these Status,Restart pages (19,20 on every campus)")
ap.add_argument("--standalone", action="store_true", help="a campus that runs on its own: reports to no other campus")
ap.add_argument("--from", dest="source", help="upgrade-companion: a Companion .dmg or .zip downloaded from bitfocus.io")
a = ap.parse_args(argv)
if a.what == "setup":
setup(a.data_dir, a.id, a.name, home=a.home)
setup(a.data_dir, a.id, a.name, home=a.home, standalone=a.standalone)
elif a.what == "rebuild":
rebuild(a.data_dir, pages=tuple(int(p) for p in a.pages.split(",")) if a.pages else None)
elif a.what == "upgrade-companion":
upgrade_companion(a.home or (load_campuses_home(a.data_dir) or "http://10.1.1.1:8020"))
upgrade_companion(a.home or (load_campuses_home(a.data_dir) or "http://10.1.1.1:8020"), source=a.source)
elif a.what == "look":
cfg = sp.load_config(os.path.join(a.data_dir, "config.json"))
inv = discover.look(cfg, a.data_dir)