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:
+53
-9
@@ -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)
|
||||
|
||||
@@ -16,6 +16,10 @@ at 99): the CAMPUS page's keys are cells ("c_1_3"), and the hub fills them from
|
||||
layout (sp_view says which). A campus that stops answering goes purple. Pages are only written
|
||||
where nothing a person made sits (push_page.safe_target): an empty page, else one added at the
|
||||
end. State: campuses.json in the data folder.
|
||||
|
||||
A standalone campus (Apollo Beach, 2026-10-21: "they want to take care of it on their own ... i dont
|
||||
need to monitor AB from NA") has "standalone" set instead of a home: it tells nobody, is no hub, and
|
||||
lists only itself.
|
||||
"""
|
||||
import gzip
|
||||
import logging
|
||||
@@ -271,7 +275,7 @@ class Mirror:
|
||||
|
||||
@staticmethod
|
||||
def is_hub(data):
|
||||
return not data.get("home")
|
||||
return not data.get("home") and not data.get("standalone")
|
||||
|
||||
def sync_registry(self):
|
||||
"""A campus: tell the hub where this settings page is (it learns nothing back). The hub: know itself."""
|
||||
|
||||
+6
-3
@@ -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)
|
||||
|
||||
+2
-2
@@ -949,8 +949,8 @@
|
||||
<div class="card"><h3 style="margin-top:0">Computers${hint("Each computer runs the join line once: it lets this Mac log in with its key and says it is here. The next look gives its programs keys.")}</h3>
|
||||
<div class="tablewrap"><table>${comps}${joined}${comps || joined ? "" : "<tr><td class='muted'>none yet</td></tr>"}</table></div>
|
||||
${line(d.join.mac, "Mac: Terminal, paste")}${line(d.join.pc, "PC: PowerShell as administrator, paste")}</div>
|
||||
<div class="card"><h3 style="margin-top:0">Set up another campus${hint("On that campus's Mac that runs Companion: Terminal, paste. It installs the panel from here, finds the gear and makes the pages. Then each computer there runs that Mac's join line.")}</h3>
|
||||
${campuses.map(([k, n]) => line(`curl -fsSL "${d.base}/campus/install?id=${k}" | zsh`, n)).join("")}</div>`;
|
||||
${d.hub ? `<div class="card"><h3 style="margin-top:0">Set up another campus${hint("On that campus's Mac that runs Companion: Terminal, paste. It installs the panel from here, finds the gear and makes the pages. Then each computer there runs that Mac's join line.")}</h3>
|
||||
${campuses.map(([k, n]) => line(`curl -fsSL "${d.base}/campus/install?id=${k}" | zsh`, n)).join("")}</div>` : ""}`;
|
||||
}
|
||||
function siteHtml(c) {
|
||||
const f = F("site.editing_code");
|
||||
|
||||
Reference in New Issue
Block a user