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
+6 -3
View File
@@ -722,8 +722,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
if path == "/api/campuses":
# The hub's own view (its settings page). A campus only ever lists itself.
data = campuses.load(site.data_dir)
return self.send(200, {"list": data.get("list") or {}, "pages": data.get("pages") or {} if not data.get("home") else {},
"error": data.get("error"), "hub": not data.get("home")})
hub = campuses.Mirror.is_hub(data)
return self.send(200, {"list": data.get("list") or {}, "pages": data.get("pages") or {} if hub else {},
"error": data.get("error"), "hub": hub, "standalone": bool(data.get("standalone"))})
if path == "/api/fleet":
return self.send(200, {"machines": list((site.read_json("fleet.json", {}) or {}).values())})
if path == "/api/discover":
@@ -732,7 +733,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
return self.send(200, {"auto": bool((cfg.get("discover") or {}).get("auto")), "last": campus.load_last(site.data_dir),
"joined": discover.registered(site.data_dir),
"join": {"mac": "curl -fsSL %s/join | sh" % base, "pc": "irm %s/join.ps1 | iex" % base},
"base": base})
"base": base, "hub": campuses.Mirror.is_hub(campuses.load(site.data_dir))})
if path == "/api/config":
cfg = site.load()
return self.send(200, {"config": schema.mask_secrets(cfg), "sha": site.cfg_sha, "mtime": site.cfg_mtime})
@@ -911,6 +912,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
or not allowed_client(url[7:].split(":")[0]):
return self.send(400, {"error": "an id like ws and an address like http://10.5.1.1:8020"})
data = campuses.load(site.data_dir)
if not campuses.Mirror.is_hub(data):
return self.send(403, {"error": "this settings page keeps no list of campuses"})
lst = data.setdefault("list", {})
for other in [k for k, e in lst.items() if k != cid and (e or {}).get("url") == url]:
lst.pop(other) # the same Mac under its old name (before its first setup)