"Location", never "campus", in everything people read; SC FOH joined with its MIDI links
- pages (NA 98 Locations / 99 Location, re-imported via PAGES_VERSION), keys, settings page, setup and install messages, alerts, docs, the Apollo Beach kit; python3 -m settings.location and /location/... addresses (the old names still answer for installed Macs) - Apply reads the before settings from the file: an in-place edit skipped the watchdog restart - SC: SUPERRACK (+SoundGrid) and X32 EDIT from SC FOH; FOH TRACKS and TRACKS LIGHTS MIDI links watched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -499,8 +499,11 @@ class Applier:
|
||||
|
||||
def _pipeline(self, new_cfg, note, by, address, atem_file):
|
||||
site = self.site
|
||||
old_cfg = site.load()
|
||||
site.load()
|
||||
sha_before = site.cfg_sha
|
||||
# From the file, not the cached object: a caller that edited site.load()'s result in place would
|
||||
# otherwise compare the new settings with themselves and skip the watchdog restart (2026-10-21).
|
||||
old_cfg = sp.load_config(site.config_path)
|
||||
if self.status.get("emergency"):
|
||||
self.log("EMERGENCY apply by %s: %s" % (by, self.status["emergency"].get("reason")))
|
||||
# 1. check
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
"""Auto-setup: what a campus has (settings/discover.py's inventory) becomes its keys, checks, fixes
|
||||
"""Auto-setup: what a location has (settings/discover.py's inventory) becomes its keys, checks, fixes
|
||||
and page layout - the same building blocks North Attleboro's were made of by hand.
|
||||
|
||||
Pure functions, no I/O:
|
||||
|
||||
build(base, inv, campus, pin) a whole config for a new campus, from the empty template
|
||||
extend(cfg, inv, seen) a running campus: only what is new (never removes anything)
|
||||
build(base, inv, location, pin) a whole config for a new location, from the empty template
|
||||
extend(cfg, inv, seen) a running location: only what is new (never removes anything)
|
||||
|
||||
Everything that comes out passes schema.validate and can be changed on the settings page like any
|
||||
hand-made setting. Gear auto-setup does not know (a generic connection, an app it has no recipe
|
||||
@@ -20,7 +20,7 @@ import status_panel as sp
|
||||
|
||||
# --- what auto-setup knows -------------------------------------------------------------
|
||||
|
||||
# Group keys sum up their members on a big campus, in this order along the top row.
|
||||
# Group keys sum up their members on a big location, in this order along the top row.
|
||||
GROUPS = ["REC", "AUDIO", "RF", "SHOW", "VIDEO", "STREAM"]
|
||||
# Who gets a cell of its own when not everything fits (the rest show inside their group key only):
|
||||
# a row of wireless receivers folds first, as at North Attleboro.
|
||||
@@ -86,7 +86,7 @@ SHARED_FIXES = {
|
||||
}
|
||||
|
||||
STATUS_ROWS, COLS = 4, 8
|
||||
# North Attleboro's auto-fix hours, for every campus: Tuesday and Wednesday rehearsals, Sunday services.
|
||||
# North Attleboro's auto-fix hours, for every location: Tuesday and Wednesday rehearsals, Sunday services.
|
||||
DEFAULT_AUTO_FIX = {"after_seconds": 20, "max_tries": 2, "retry_seconds": 90, "pause_after_network_seconds": 60,
|
||||
"restart_connections": True, "windows": [{"days": ["tue"], "start": "14:00", "end": "22:00"},
|
||||
{"days": ["wed"], "start": "09:00", "end": "21:00"},
|
||||
@@ -408,7 +408,7 @@ def layout(cfg, new_tiles, restart_items):
|
||||
|
||||
|
||||
def place_new(cfg, tile):
|
||||
"""One more key on a running campus: the first free cell, else inside its group, else on ALL only."""
|
||||
"""One more key on a running location: the first free cell, else inside its group, else on ALL only."""
|
||||
taken = {tuple(t["pos"]) for t in cfg["tiles"] if t.get("pos")}
|
||||
if cfg.get("restart_page"):
|
||||
taken.add(tuple(cfg["restart_page"]["jump_pos"]))
|
||||
@@ -423,7 +423,7 @@ def place_new(cfg, tile):
|
||||
|
||||
|
||||
def restart_layout(cfg, restart_items):
|
||||
"""The Restart page always exists (a campus set up before its computers joined gets their keys later)."""
|
||||
"""The Restart page always exists (a location set up before its computers joined gets their keys later)."""
|
||||
rp = cfg.get("restart_page")
|
||||
if not rp:
|
||||
return
|
||||
@@ -446,7 +446,7 @@ def nets_of(inv):
|
||||
|
||||
|
||||
def build(base, inv, campus, pin, pages=(19, 20)):
|
||||
"""A whole config for a new campus: the template, this Mac, and a key for everything found."""
|
||||
"""A whole config for a new location: the template, this Mac, and a key for everything found."""
|
||||
cfg = copy.deepcopy(base)
|
||||
ing = inv.get("ingest") or {}
|
||||
wired = ing.get("wired") or {}
|
||||
@@ -485,7 +485,7 @@ def build(base, inv, campus, pin, pages=(19, 20)):
|
||||
|
||||
|
||||
def extend(cfg, inv, seen):
|
||||
"""What is new since the last look, added to a running campus. Never removes or changes what is
|
||||
"""What is new since the last look, added to a running location. Never removes or changes what is
|
||||
there; a thing seen before (and so deliberately taken off by someone) is not added again.
|
||||
Returns (new config, [what was added, in words], [identities now seen])."""
|
||||
cfg = copy.deepcopy(cfg)
|
||||
@@ -570,7 +570,7 @@ def keep_from(new, old):
|
||||
cfg = copy.deepcopy(new)
|
||||
have = referenced(cfg)
|
||||
kept = []
|
||||
# Switch keys made for this campus by hand (Southcoast's LIGHTS) and an AUTO-FIX key: auto-setup makes neither.
|
||||
# Switch keys made for this location by hand (Southcoast's LIGHTS) and an AUTO-FIX key: auto-setup makes neither.
|
||||
for t in old.get("tiles", []):
|
||||
if t.get("kind") not in ("bypass", "autofix") or t["id"] in {x["id"] for x in cfg["tiles"]}:
|
||||
continue
|
||||
|
||||
+20
-20
@@ -1,15 +1,15 @@
|
||||
"""A campus that sets itself up.
|
||||
"""A location that sets itself up.
|
||||
|
||||
On the campus's Mac that runs Companion, one line installs the panel from another campus's settings
|
||||
On the location's Mac that runs Companion, one line installs the panel from another location's settings
|
||||
page and runs `setup`: Companion gets the panel's custom variables and blink clock, two free pages
|
||||
are picked, the gear is found (settings/discover.py) and turned into keys (settings/autoconf.py),
|
||||
and that config is applied like any Apply on the page. Each other computer runs one join line
|
||||
(Terminal on a Mac, an administrator PowerShell on a PC); from then on the settings page looks again
|
||||
every ten minutes and adds what is new by itself (Settings > Site: "Add new gear by itself").
|
||||
|
||||
python3 -m settings.campus setup --id ab [--name "Apollo Beach"]
|
||||
python3 -m settings.campus look what it finds (nothing changes)
|
||||
python3 -m settings.campus add add what is new now
|
||||
python3 -m settings.location setup --id ab [--name "Apollo Beach"]
|
||||
python3 -m settings.location look what it finds (nothing changes)
|
||||
python3 -m settings.location add add what is new now
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
@@ -35,7 +35,7 @@ VARIABLES = ("sp_state", "sp_hb", "sp_cmd", "flash_tick")
|
||||
TRIGGER_NAME = "flash_tick_500"
|
||||
LOOK_EVERY = 600
|
||||
SEEN, LAST = "discover-seen.json", "discover-last.json"
|
||||
# What the program kit carries (no settings, keys or history of the campus that serves it).
|
||||
# What the program kit carries (no settings, keys or history of the location that serves it).
|
||||
KIT_FILES = ("status_panel.py", "readiness.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")
|
||||
KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup")
|
||||
@@ -165,21 +165,21 @@ def companion_version_problem(cfg, standalone=False):
|
||||
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"
|
||||
" cd \"$HOME/Library/Application Support/status-panel\" && /usr/bin/python3 -m settings.location 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"
|
||||
" cd \"$HOME/Library/Application Support/status-panel\" && /usr/bin/python3 -m settings.location upgrade-companion\n"
|
||||
"then paste the install line again.")
|
||||
except OSError:
|
||||
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, standalone=False):
|
||||
"""The first setup of a campus (or, when it is set up already, a look for what is new).
|
||||
"""The first setup of a location (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 or standalone: # where it was installed from: the campus list lives there
|
||||
if home or standalone: # where it was installed from: the location list lives there
|
||||
data = campuses.load(data_dir)
|
||||
if standalone:
|
||||
data.pop("home", None)
|
||||
@@ -191,7 +191,7 @@ def setup(data_dir, cid, name=None, say=print, home=None, standalone=False):
|
||||
site = server.Site(data_dir)
|
||||
cfg = site.load()
|
||||
if not is_fresh(cfg):
|
||||
say("%s is set up already: looking for what is new." % (cfg.get("site") or {}).get("name", "This campus"))
|
||||
say("%s is set up already: looking for what is new." % (cfg.get("site") or {}).get("name", "This location"))
|
||||
return look_and_add(site, say=say, force=True)
|
||||
name = name or CAMPUSES.get(cid) or cid.upper()
|
||||
problem = companion_version_problem(cfg, standalone)
|
||||
@@ -223,7 +223,7 @@ def setup(data_dir, cid, name=None, say=print, home=None, standalone=False):
|
||||
|
||||
|
||||
def rebuild(data_dir, say=print, pages=None):
|
||||
"""A campus's setup made again from what is found now (today's recipes), keeping its name, PIN,
|
||||
"""A location's setup made again from what is found now (today's recipes), keeping its name, PIN,
|
||||
pages and site details. Applied like any Apply (it waits for a recording; history keeps the old one)."""
|
||||
from settings import server
|
||||
site = server.Site(data_dir)
|
||||
@@ -236,7 +236,7 @@ def rebuild(data_dir, say=print, pages=None):
|
||||
if not rp:
|
||||
rp = next(p for p in pick_pages(cfg, 3) if p != cfg["page"])
|
||||
pages = tuple(pages or (cfg["page"], rp))
|
||||
if pages != (cfg["page"], rp): # moving (19/20 like every campus): only onto pages that are free or ours
|
||||
if pages != (cfg["page"], rp): # moving (19/20 like every location): only onto pages that are free or ours
|
||||
comp = cfg.get("companion") or {}
|
||||
for p in pages:
|
||||
ok, why = push_page.safe_target(push_page.fetch_page(comp.get("host", "127.0.0.1"), int(comp.get("port", 8000)), p))
|
||||
@@ -390,7 +390,7 @@ def blocked(cfg, applier, now=None, asked=False):
|
||||
|
||||
|
||||
def look_and_add(site, say=lambda s: None, force=False):
|
||||
"""Look, and add what is new (when the campus allows it, and not during a service)."""
|
||||
"""Look, and add what is new (when the location allows it, and not during a service)."""
|
||||
cfg = site.load()
|
||||
inv = discover.look(cfg, site.data_dir)
|
||||
seen = load_seen(site.data_dir)
|
||||
@@ -488,7 +488,7 @@ def shq(s):
|
||||
|
||||
|
||||
def install_script(base, cid, support_key=None):
|
||||
"""What a new campus's Companion Mac runs: install from `base` (this page), then set itself up."""
|
||||
"""What a new location's Companion Mac runs: install from `base` (this page), then set itself up."""
|
||||
m = re.match(r"[a-z0-9]+", str(cid).lower())
|
||||
cid = m.group(0)[:8] if m else "new"
|
||||
name = CAMPUSES.get(cid, cid.upper())
|
||||
@@ -506,7 +506,7 @@ fi
|
||||
DIR="$HOME/status-panel-install"
|
||||
mkdir -p "$DIR" && cd "$DIR"
|
||||
echo "Downloading the status panel from $SRC ..."
|
||||
/usr/bin/curl -fsSL "$SRC/campus/program.tgz" | /usr/bin/tar -xz
|
||||
/usr/bin/curl -fsSL "$SRC/location/program.tgz" | /usr/bin/tar -xz
|
||||
[[ -f "$HOME/.ssh/id_status_panel" ]] || { mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" && /usr/bin/ssh-keygen -q -t ed25519 -N "" -C "status-panel@$(scutil --get LocalHostName)" -f "$HOME/.ssh/id_status_panel"; }
|
||||
SUPPORT=%(key)s
|
||||
if [[ -n "$SUPPORT" ]]; then
|
||||
@@ -515,7 +515,7 @@ if [[ -n "$SUPPORT" ]]; then
|
||||
fi
|
||||
zsh install.sh --site %(cid)s
|
||||
cd "$HOME/Library/Application Support/status-panel"
|
||||
/usr/bin/python3 -m settings.campus setup --id %(cid)s --name %(qname)s --home "$SRC"
|
||||
/usr/bin/python3 -m settings.location setup --id %(cid)s --name %(qname)s --home "$SRC"
|
||||
if ! /usr/bin/nc -z -G 2 127.0.0.1 22 >/dev/null 2>&1; then
|
||||
echo
|
||||
echo "One more thing: turn on Remote Login (System Settings > General > Sharing), so North Attleboro can help remotely."
|
||||
@@ -604,9 +604,9 @@ def main(argv=None):
|
||||
ap.add_argument("--id", default="new")
|
||||
ap.add_argument("--name")
|
||||
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("--home", help="the settings page this location was installed from (the location list lives there)")
|
||||
ap.add_argument("--pages", help="rebuild onto these Status,Restart pages (19,20 on every location)")
|
||||
ap.add_argument("--standalone", action="store_true", help="a location that runs on its own: reports to no other location")
|
||||
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":
|
||||
|
||||
+47
-42
@@ -1,23 +1,23 @@
|
||||
"""Every campus's status, on North Attleboro's Companion only.
|
||||
"""Every location's status, on North Attleboro's Companion only.
|
||||
|
||||
Each other campus's settings page tells North Attleboro's (the hub: a campus with no "home") where it
|
||||
is, and learns nothing back: no other campus ever sees another campus (2026-10-20, the user: "NA
|
||||
Each other location's settings page tells North Attleboro's (the hub: a location with no "home") where it
|
||||
is, and learns nothing back: no other location ever sees another location (2026-10-20, the user: "NA
|
||||
should be the only one that sees every single location's status"). Only the hub, every two seconds,
|
||||
copies each campus's key states (what that campus's watchdog draws, /api/state) into its own
|
||||
copies each location's key states (what that location's watchdog draws, /api/state) into its own
|
||||
Companion as custom variables sp_state_<id> / sp_hb_<id>, and keeps two kinds of page there:
|
||||
|
||||
CAMPUSES page one key per campus: that campus's ALL key, with its name; tap = its status page
|
||||
CAMPUS page whichever campus was tapped, as its own status page looks (same places, same
|
||||
colours, live), view only: holds and presses are for that campus's own deck.
|
||||
The ALARMS corner goes back to the CAMPUSES page.
|
||||
LOCATIONS page one key per location: that location's ALL key, with its name; tap = its status page
|
||||
LOCATION page whichever location was tapped, as its own status page looks (same places, same
|
||||
colours, live), view only: holds and presses are for that location's own deck.
|
||||
The ALARMS corner goes back to the LOCATIONS page.
|
||||
|
||||
Two pages, whatever the number of campuses (NA's Companion has 97 pages of its own; Companion stops
|
||||
at 99): the CAMPUS page's keys are cells ("c_1_3"), and the hub fills them from the tapped campus's
|
||||
layout (sp_view says which). A campus that stops answering goes purple. Pages are only written
|
||||
Two pages, whatever the number of locations (NA's Companion has 97 pages of its own; Companion stops
|
||||
at 99): the LOCATION page's keys are cells ("c_1_3"), and the hub fills them from the tapped location's
|
||||
layout (sp_view says which). A location 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
|
||||
A standalone location (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.
|
||||
"""
|
||||
@@ -70,17 +70,22 @@ def me(cfg, url=None):
|
||||
import pwd
|
||||
site = cfg.get("site") or {}
|
||||
# The account this panel runs as: the hub logs in with it for the nightly backup.
|
||||
return {"id": site.get("id") or "new", "name": site.get("name") or "This campus", "url": url or my_url(cfg),
|
||||
return {"id": site.get("id") or "new", "name": site.get("name") or "This location", "url": url or my_url(cfg),
|
||||
"user": pwd.getpwuid(os.getuid()).pw_name}
|
||||
|
||||
|
||||
# Bumped when the pages are built differently (2: "campus" became "location", 2026-10-21), so the hub
|
||||
# re-imports them.
|
||||
PAGES_VERSION = 2
|
||||
|
||||
|
||||
def layout_sha(cfg):
|
||||
keys = [(t.get("id"), t.get("label"), t.get("pos"), t.get("kind"), t.get("members")) for t in cfg.get("tiles", [])]
|
||||
return hashlib.sha256(json.dumps([keys, cfg.get("page_grid"), cfg.get("variables")], sort_keys=True, default=str).encode()).hexdigest()[:16]
|
||||
return hashlib.sha256(json.dumps([keys, cfg.get("page_grid"), cfg.get("variables"), PAGES_VERSION], sort_keys=True, default=str).encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def state_now(site):
|
||||
"""What /api/state answers: this campus's key states as its watchdog draws them, right now."""
|
||||
"""What /api/state answers: this location's key states as its watchdog draws them, right now."""
|
||||
cfg = site.load()
|
||||
names = cfg.get("variables") or {}
|
||||
comp = site.comp
|
||||
@@ -100,8 +105,8 @@ def http_json(url, body=None, timeout=3):
|
||||
|
||||
VIEW = {"state": "sp_state_view", "heartbeat": "sp_hb_view"}
|
||||
SELECT = "sp_view"
|
||||
VIEW_CMD = "sp_view_cmd" # a tap or hold on the CAMPUS page, passed on to that campus's own watchdog
|
||||
MODE = "sp_view_mode" # "" = that campus's status page, "RESTART" = its Restart page
|
||||
VIEW_CMD = "sp_view_cmd" # a tap or hold on the LOCATION page, passed on to that location's own watchdog
|
||||
MODE = "sp_view_mode" # "" = that location's status page, "RESTART" = its Restart page
|
||||
CELLS = [(r, c) for r in range(4) for c in range(1, 8)]
|
||||
|
||||
|
||||
@@ -110,7 +115,7 @@ def cell_id(r, c):
|
||||
|
||||
|
||||
def view_state(state, keys):
|
||||
"""A campus's key states, moved onto the cells its keys sit in (empty cells blank)."""
|
||||
"""A location's key states, moved onto the cells its keys sit in (empty cells blank)."""
|
||||
out = {cell_id(r, c): {"x": "", "b": 0, "b2": 0, "f": 0xFFFFFF, "f2": 0xFFFFFF, "s": 14} for r, c in CELLS}
|
||||
for k in keys:
|
||||
r, c = k["pos"]
|
||||
@@ -120,8 +125,8 @@ def view_state(state, keys):
|
||||
|
||||
|
||||
def build_view(local_cfg, page, overview_page):
|
||||
"""The CAMPUS page: a key per cell, drawn from sp_state_view; tap = acknowledge, hold = fix, at
|
||||
that campus (2026-10-20: the user wants to test and fix Woonsocket from NA). The ALARMS corner goes back."""
|
||||
"""The LOCATION page: a key per cell, drawn from sp_state_view; tap = acknowledge, hold = fix, at
|
||||
that location (2026-10-20: the user wants to test and fix Woonsocket from NA). The ALARMS corner goes back."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
hold_ms = opts["hold_ms"]
|
||||
@@ -132,17 +137,17 @@ def build_view(local_cfg, page, overview_page):
|
||||
make_page.place(controls, (r, c), {
|
||||
"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus view %d/%d' - the tapped campus's key here. Press: acknowledge it there. "
|
||||
"Hold %g s: fix it there, like a hold on that campus's own deck." % (r, c, hold_ms / 1000.0)},
|
||||
"notes": "STATUS tile 'location view %d/%d' - the tapped location's key here. Press: acknowledge it there. "
|
||||
"Hold %g s: fix it there, like a hold on that location's own deck." % (r, c, hold_ms / 1000.0)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": [tap], str(hold_ms): [hold]},
|
||||
"options": {"runWhileHeld": [hold_ms]}}}, "localVariables": []})
|
||||
back = make_page.static_button("campus_back", "ALL\\nSITES", 0x37474F, 0xFFFFFF, overview_page,
|
||||
"Tap: back to the CAMPUSES page (page %d)." % overview_page, "STATUS jump key")
|
||||
"Tap: back to the LOCATIONS page (page %d)." % overview_page, "STATUS jump key")
|
||||
for layer in back["style"]["layers"]:
|
||||
if layer["type"] == "text": # which campus this page shows, then the way back
|
||||
if layer["type"] == "text": # which location this page shows, then the way back
|
||||
layer["text"] = make_page.prop("`${$(custom:%s)} ${$(custom:%s)}\\nALL SITES`" % (SELECT, MODE), True)
|
||||
make_page.place(controls, (3, 0), back)
|
||||
return make_page.page_export("%d\\nCampus" % page, page, controls, local_cfg)
|
||||
return make_page.page_export("%d\\nLocation" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def select_action(cid, mode=""):
|
||||
@@ -153,7 +158,7 @@ def select_action(cid, mode=""):
|
||||
|
||||
|
||||
def campus_key(cid, name, names, actions, note, local_cfg):
|
||||
"""One CAMPUSES key: that campus's ALL key with its name on top."""
|
||||
"""One LOCATIONS key: that location's ALL key with its name on top."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
tag = short(cid, name)
|
||||
stale_text = "`%s\\n?`" % tag
|
||||
@@ -164,13 +169,13 @@ def campus_key(cid, name, names, actions, note, local_cfg):
|
||||
text["fontsize"] = make_page.prop("auto")
|
||||
return {"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus %s' - %s's ALL key. %s" % (cid, name, note)},
|
||||
"notes": "STATUS tile 'location %s' - %s's ALL key. %s" % (cid, name, note)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": actions}, "options": {"runWhileHeld": []}}},
|
||||
"localVariables": []}
|
||||
|
||||
|
||||
def build_overview(local_cfg, campuses, page, view_page):
|
||||
"""Row 0: each campus's ALL key (tap: its status page). Row 1: its RESTART key (tap: its Restart page)."""
|
||||
"""Row 0: each location's ALL key (tap: its status page). Row 1: its RESTART key (tap: its Restart page)."""
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
mine = (local_cfg.get("site") or {}).get("id")
|
||||
rp = (local_cfg.get("restart_page") or {}).get("page")
|
||||
@@ -181,7 +186,7 @@ def build_overview(local_cfg, campuses, page, view_page):
|
||||
key = campus_key(cid, name, local_cfg["variables"], [make_page.set_page_action("campus_" + cid, "go", local_cfg["page"])],
|
||||
"Tap: its status page (page %d)." % local_cfg["page"], local_cfg)
|
||||
restart = make_page.static_button("campus_rs_" + cid, "%s\\nRESTART" % tag, 0x37474F, 0xFFFFFF, rp,
|
||||
"Tap: this campus's Restart page (page %s)." % rp, "STATUS jump key") if rp else None
|
||||
"Tap: this location's Restart page (page %s)." % rp, "STATUS jump key") if rp else None
|
||||
else:
|
||||
key = campus_key(cid, name, {"state": "sp_state_" + cid, "heartbeat": "sp_hb_" + cid},
|
||||
select_action(cid) + [make_page.set_page_action("campus_" + cid, "go", view_page)],
|
||||
@@ -193,7 +198,7 @@ def build_overview(local_cfg, campuses, page, view_page):
|
||||
make_page.place(controls, (0, col), key)
|
||||
if restart:
|
||||
make_page.place(controls, (1, col), restart)
|
||||
return make_page.page_export("%d\\nCampuses" % page, page, controls, local_cfg)
|
||||
return make_page.page_export("%d\\nLocations" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def ensure_variables(cfg, ids):
|
||||
@@ -232,10 +237,10 @@ def free_page(cfg, taken, start=21):
|
||||
# None empty: a new page at the end (Companion's own page keys on it), never in between.
|
||||
last = max(p for p in range(1, 100) if exists(host, port, p))
|
||||
if last >= 99:
|
||||
raise RuntimeError("Companion has 99 pages and none is empty: free one for the campuses")
|
||||
raise RuntimeError("Companion has 99 pages and none is empty: free one for the locations")
|
||||
ws = push_page.WebSocket(host, port, "/trpc")
|
||||
try:
|
||||
push_page.Trpc(ws).mutation("pages.insert", {"asPageNumber": last + 1, "pageNames": ["CAMPUSES"]})
|
||||
push_page.Trpc(ws).mutation("pages.insert", {"asPageNumber": last + 1, "pageNames": ["LOCATIONS"]})
|
||||
finally:
|
||||
ws.close()
|
||||
return last + 1
|
||||
@@ -278,7 +283,7 @@ class Mirror:
|
||||
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."""
|
||||
"""A location: tell the hub where this settings page is (it learns nothing back). The hub: know itself."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
mine = me(cfg)
|
||||
@@ -300,7 +305,7 @@ class Mirror:
|
||||
return {p for p in t if p}
|
||||
|
||||
def refresh_pages(self):
|
||||
"""The hub: each campus's layout (when it changed), the CAMPUS page once, the CAMPUSES page when the roster changes."""
|
||||
"""The hub: each location's layout (when it changed), the LOCATION page once, the LOCATIONS page when the roster changes."""
|
||||
cfg = self.site.load()
|
||||
data = self.sync_registry()
|
||||
if not self.is_hub(data):
|
||||
@@ -308,7 +313,7 @@ class Mirror:
|
||||
mine = (cfg.get("site") or {}).get("id")
|
||||
others = {cid: e for cid, e in data.get("list", {}).items() if cid != mine}
|
||||
if not others and not (data.get("pages") or {}).get("overview"):
|
||||
return data # no other campus yet: no pages needed
|
||||
return data # no other location yet: no pages needed
|
||||
pages = data.setdefault("pages", {})
|
||||
layouts = data.setdefault("layouts", {})
|
||||
for cid, e in sorted(others.items()):
|
||||
@@ -337,7 +342,7 @@ class Mirror:
|
||||
return data
|
||||
|
||||
def pass_on(self, pick, others, restart=False):
|
||||
"""A tap or hold on the CAMPUS page goes to the shown campus's watchdog, through its Companion."""
|
||||
"""A tap or hold on the LOCATION page goes to the shown location's watchdog, through its Companion."""
|
||||
text = (self.site.comp.get_custom(VIEW_CMD) or "").strip()
|
||||
if not text:
|
||||
return
|
||||
@@ -351,12 +356,12 @@ class Mirror:
|
||||
remote = sp.Companion(host, 8000, timeout=3)
|
||||
before = (remote.get_custom("sp_cmd") or "").strip()
|
||||
remote.set_custom("sp_cmd", ";".join([before] + cmds if before else cmds))
|
||||
log.info("CAMPUS %s: %s (from %s's CAMPUS page)", pick, "; ".join(cmds), (self.site.cfg.get("site") or {}).get("name", "here"))
|
||||
log.info("LOCATION %s: %s (from %s's LOCATION page)", pick, "; ".join(cmds), (self.site.cfg.get("site") or {}).get("name", "here"))
|
||||
except OSError as e:
|
||||
log.warning("CAMPUS %s: could not pass on %s: %s", pick, cmds, e)
|
||||
log.warning("LOCATION %s: could not pass on %s: %s", pick, cmds, e)
|
||||
|
||||
def poll(self):
|
||||
"""The hub: copy each other campus's key states into this Companion."""
|
||||
"""The hub: copy each other location's key states into this Companion."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
if not self.is_hub(data):
|
||||
@@ -380,10 +385,10 @@ class Mirror:
|
||||
threads.append(t)
|
||||
for t in threads:
|
||||
t.join(2)
|
||||
# The CAMPUS page shows the tapped campus: its keys moved onto their cells.
|
||||
# The LOCATION page shows the tapped location: its keys moved onto their cells.
|
||||
pick = (self.site.comp.get_custom(SELECT) or "").strip().lower() # held as "WS": the back key shows it
|
||||
if pick not in others and others:
|
||||
# Nothing tapped yet (or a campus that left): show the first one rather than a purple page.
|
||||
# Nothing tapped yet (or a location that left): show the first one rather than a purple page.
|
||||
pick = sorted(others)[0]
|
||||
self.site.comp.set_custom(SELECT, pick.upper())
|
||||
restart = (self.site.comp.get_custom(MODE) or "").strip().upper() == "RESTART"
|
||||
@@ -400,7 +405,7 @@ class Mirror:
|
||||
|
||||
|
||||
def campus_command(keys, text, now, restart=False):
|
||||
"""The CAMPUS page's taps and holds ("tap 1 3 <time>;hold 0 2 <time>") as that campus's own
|
||||
"""The LOCATION page's taps and holds ("tap 1 3 <time>;hold 0 2 <time>") as that location's own
|
||||
commands ("ack hd1", "restart hd1 <time>"; on its Restart page a hold is "force rs_x <time>" and a
|
||||
tap does nothing, as on its own deck), for the keys at those cells. Stale ones are dropped."""
|
||||
at = {tuple(k["pos"]): k["id"] for k in keys or []}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Auto-setup's eyes: what this campus has, read from Companion, Dante, the network MIDI sessions,
|
||||
"""Auto-setup's eyes: what this location has, read from Companion, Dante, the network MIDI sessions,
|
||||
this Mac's own network, and the computers that joined (each one ran the join line once).
|
||||
|
||||
look(cfg, data_dir) the inventory autoconf.py turns into keys
|
||||
@@ -143,7 +143,7 @@ def this_mac():
|
||||
info["exes"] = {os.path.basename(p.strip()): p.strip() for p in ps if ".app/Contents/MacOS/" in p}
|
||||
if wired:
|
||||
ip, net = ifs[wired]
|
||||
# A /24 or smaller on a campus usually sits inside a /16 the rest of the gear shares: use what is configured.
|
||||
# A /24 or smaller on a location usually sits inside a /16 the rest of the gear shares: use what is configured.
|
||||
info["wired"] = {"interface": wired, "ip": ip, "net": net, "gateway": gateway_of(wired)}
|
||||
return info
|
||||
|
||||
@@ -303,7 +303,7 @@ def computers(cfg, data_dir):
|
||||
|
||||
def look(cfg, data_dir):
|
||||
inv = {"ingest": this_mac(), "at": int(time.time())}
|
||||
# A campus whose MIDI rule names its wired network keeps it (it may be wider than this Mac's subnet).
|
||||
# A location whose MIDI rule names its wired network keeps it (it may be wider than this Mac's subnet).
|
||||
rule = [n for t in sp.all_tiles(cfg) for c in t.get("checks", []) if c.get("type") == "midi_wired" for n in c.get("nets", [])]
|
||||
if rule and inv["ingest"].get("wired"):
|
||||
inv["ingest"]["wired"]["net"] = rule[0]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""What people type: python3 -m settings.location setup | look | add | rebuild | upgrade-companion
|
||||
(the same as settings.campus, the module's older name; installed Macs may still call that one)."""
|
||||
from settings.campus import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smart outlets the panel may switch (a hung recorder's mains, say), through Tuya's cloud with the
|
||||
campus's own project keys. Credentials and the outlet list live in outlets.json beside the config
|
||||
location's own project keys. Credentials and the outlet list live in outlets.json beside the config
|
||||
(0600, never in the repo). Also a command for the watchdog's fixes:
|
||||
|
||||
outlet.py list the outlets on the account (name, id, on/off)
|
||||
|
||||
+15
-15
@@ -687,14 +687,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(200, keyguide.cheat_sheet(site.load(), self.my_address()), "text/html; charset=utf-8")
|
||||
if path == "/where.html":
|
||||
return self.send(200, keyguide.where_things_are(site.load(), site.data_dir, self.my_address()), "text/html; charset=utf-8")
|
||||
# ---- auto-setup: the install line for a new campus, the join lines for its computers ----
|
||||
if path == "/campus/install":
|
||||
# ---- auto-setup: the install line for a new location, the join lines for its computers ----
|
||||
if path in ("/campus/install", "/location/install"):
|
||||
key, _ = scripts.public_key(site.load())
|
||||
return self.send(200, campus.install_script(self.base_url(), q.get("id", ["new"])[0], key), "text/plain; charset=utf-8")
|
||||
if path == "/campus/program.tgz":
|
||||
if path in ("/campus/program.tgz", "/location/program.tgz"):
|
||||
return self.send(200, campus.program_kit(site.data_dir), "application/gzip")
|
||||
if path == "/campus/companion.zip":
|
||||
# Companion 5.0.7 as this Mac has it, for `settings.campus upgrade-companion` on a campus
|
||||
if path in ("/campus/companion.zip", "/location/companion.zip"):
|
||||
# Companion 5.0.7 as this Mac has it, for `settings.campus upgrade-companion` on a location
|
||||
# still on an older one (packed once, kept beside the config, streamed).
|
||||
app = "/Applications/Companion 5.0.7.app"
|
||||
pack = os.path.join(site.data_dir, "companion-5.0.7.zip")
|
||||
@@ -721,7 +721,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if path == "/api/state":
|
||||
return self.send(200, campuses.state_now(site))
|
||||
if path == "/api/campuses":
|
||||
# The hub's own view (its settings page). A campus only ever lists itself.
|
||||
# The hub's own view (its settings page). A location only ever lists itself.
|
||||
data = campuses.load(site.data_dir)
|
||||
hub = campuses.Mirror.is_hub(data)
|
||||
return self.send(200, {"list": data.get("list") or {}, "pages": data.get("pages") or {} if hub else {},
|
||||
@@ -824,7 +824,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if os.path.exists(fp):
|
||||
z.write(fp, name)
|
||||
z.writestr("README.txt", "Site bundle of %s, exported %s from the settings page.\nDrop it in the repo as sites/%s/ or feed it to "
|
||||
"'New campus from a bundle' on another campus's page.\n" % ((cfg.get("site") or {}).get("name", "?"), time.strftime("%Y-%m-%d %H:%M"), (cfg.get("site") or {}).get("id", "site")))
|
||||
"'New location from a bundle' on another location's page.\n" % ((cfg.get("site") or {}).get("name", "?"), time.strftime("%Y-%m-%d %H:%M"), (cfg.get("site") or {}).get("id", "site")))
|
||||
data = buf.getvalue()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/zip")
|
||||
@@ -906,7 +906,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
site.discover_poke.set()
|
||||
return self.send(200, {"ok": True})
|
||||
if path == "/api/campus/hello":
|
||||
# Another campus says where its settings page is (the hub keeps the list for everyone).
|
||||
# Another location says where its settings page is (the hub keeps the list for everyone).
|
||||
cid = str(body.get("id") or "")
|
||||
url = str(body.get("url") or "")
|
||||
if not re.fullmatch(r"[a-z0-9]{1,8}", cid) or not re.fullmatch(r"http://\d{1,3}(\.\d{1,3}){3}:\d{2,5}", url) \
|
||||
@@ -914,7 +914,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
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"})
|
||||
return self.send(403, {"error": "this settings page keeps no list of locations"})
|
||||
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)
|
||||
@@ -924,7 +924,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
campuses.save(site.data_dir, data)
|
||||
return self.send(200, {"ok": True})
|
||||
if path == "/api/fleet/hello":
|
||||
# A machine anywhere (any campus) says it now takes the admin laptop's key: kept for the
|
||||
# A machine anywhere (any location) says it now takes the admin laptop's key: kept for the
|
||||
# laptop's SSH shortcuts (tools/fleet.py). Nothing here acts on it.
|
||||
try:
|
||||
entry = discover.clean_hello(body, self.client_address[0])
|
||||
@@ -1098,7 +1098,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(400, {"error": "not a site bundle or settings file: %s" % e})
|
||||
cfg = site.load()
|
||||
mine = cfg.get("site") or {}
|
||||
src = (new_cfg.get("site") or {}).get("name") or "the other campus"
|
||||
src = (new_cfg.get("site") or {}).get("name") or "the other location"
|
||||
new_cfg["site"] = dict(new_cfg.get("site") or {}, **{k: mine[k] for k in ("id", "name", "ingest_name", "ingest_hostname", "ingest_ips", "editing_code", "settings_port") if k in mine})
|
||||
flag = "copied from %s - check it" % src
|
||||
for alias, h in (new_cfg.get("hosts") or {}).items():
|
||||
@@ -1110,7 +1110,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
f["label"] = f.get("label", "")
|
||||
errors, warnings = schema.validate(new_cfg)
|
||||
draft, _ = site.draft.update(new_cfg, who, site.cfg_sha)
|
||||
log.info("%s started a new campus from a bundle of %s", who, src)
|
||||
log.info("%s started a new location from a bundle of %s", who, src)
|
||||
return self.send(200, {"draft": self.masked_draft(draft), "from": src, "errors": errors, "warnings": warnings})
|
||||
if path == "/api/atem-reference/pick-newest":
|
||||
folder = os.path.expanduser((site.cfg.get("atem") or {}).get("source_folder") or "")
|
||||
@@ -1148,7 +1148,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def names_check(self, cfg):
|
||||
"""Every device name the keys use, against what this Companion, the Dante network and the Macs report now:
|
||||
the mapping step of a copied campus."""
|
||||
the mapping step of a copied location."""
|
||||
site = self.site
|
||||
try:
|
||||
live = {c["label"]: c.get("moduleId") for c in site.comp.connections()}
|
||||
@@ -1388,7 +1388,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(404, {"error": "missing %s" % name})
|
||||
|
||||
def base_url(self):
|
||||
"""This page as the caller reached it (an address, across campuses: names do not cross subnets)."""
|
||||
"""This page as the caller reached it (an address, across locations: names do not cross subnets)."""
|
||||
host = (self.headers.get("Host") or "").strip() or "%s:%d" % (self.server.server_address[0], self.server.server_address[1])
|
||||
if not re.fullmatch(r"[A-Za-z0-9.\-]+(:\d+)?", host):
|
||||
host = "%s:%d" % (self.server.server_address[0], self.server.server_address[1])
|
||||
@@ -1506,7 +1506,7 @@ def main(argv=None):
|
||||
stop = threading.Event()
|
||||
threading.Thread(target=rebind_loop, args=(site, servers, port, stop), daemon=True).start()
|
||||
threading.Thread(target=campus.loop, args=(site, stop, site.discover_poke), name="auto-setup", daemon=True).start()
|
||||
threading.Thread(target=campuses.loop, args=(site, stop), name="campuses", daemon=True).start()
|
||||
threading.Thread(target=campuses.loop, args=(site, stop), name="locations", daemon=True).start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
|
||||
+12
-12
@@ -858,7 +858,7 @@
|
||||
<li>Settings › MIDI links: add one to the MIDI key; Run the fix on that check.</li>
|
||||
<li>Keys › Fixes › VISTA: change the program file to Vista2.exe, watch the command rebuild; put it back.</li>
|
||||
<li>Computers › Setup › Lighting PC: open the script, Copy. Computers › Lighting PC › Remove (read, then Cancel).</li>
|
||||
<li>Settings › Switcher: Pick the newest file; Discard. Settings › Program & campus: Export bundle.</li>
|
||||
<li>Settings › Switcher: Pick the newest file; Discard. Settings › Program & location: Export bundle.</li>
|
||||
<li>A real change (a note on Computers › Gear): APPLY, then History › Undo, APPLY again.</li></ol></details>
|
||||
<details><summary><b>The words a key can show</b></summary>${glossary}</details>
|
||||
<details><summary><b>Only a person can</b></summary><ul class="small"><li>Run the set-up script on a new computer, allow Accessibility on the Studio B Mac, click Allow on the Spotify prompt once.</li><li>Create Companion's custom variables and the blink trigger, add pages, install modules, name the Stream Decks.</li><li>Type the Netgear login into the keychain; keep Google Drive signed in.</li><li>Save a new switcher file from ATEM Software Control when the show changes, then pick it under Settings › Switcher.</li></ul></details>
|
||||
@@ -882,9 +882,9 @@
|
||||
["outlets", "Outlets", "Smart plugs the panel may switch, for a device that only a power cycle brings back"],
|
||||
["companion", "Companion", "Connections, and names the keys use that are not here"],
|
||||
["network", "Network", "This Mac's network port and the Dante switches"],
|
||||
["setup", "Auto-setup", "What this Mac found, the line each computer runs to join, and the line that sets up another campus"],
|
||||
["site", "Site & PIN", "The campus, contacts, this Mac, the PIN"],
|
||||
["program", "Program & campus", "Update the code, export or start a campus"],
|
||||
["setup", "Auto-setup", "What this Mac found, the line each computer runs to join, and the line that sets up another location"],
|
||||
["site", "Site & PIN", "The location, contacts, this Mac, the PIN"],
|
||||
["program", "Program & location", "Update the code, export or start a location"],
|
||||
["advanced", "Advanced", "Every number and name, by screen"],
|
||||
];
|
||||
async function settings(rest) {
|
||||
@@ -917,7 +917,7 @@
|
||||
wireSettings(which);
|
||||
}
|
||||
function readySettingsHtml(c) {
|
||||
if (!c.ready_page) return "<p class='muted'>This campus has no READY page.</p>";
|
||||
if (!c.ready_page) return "<p class='muted'>This location has no READY page.</p>";
|
||||
const lists = JSON.stringify((c.ready || {}).prop_lists || [], null, 1);
|
||||
return `<div class="card"><p style="margin:0">${onOff("ready", c.ready || {}, "the READY page")} ${(c.ready || {}).disabled ? "<b>The READY page is off:</b> its keys show OFF and nothing is looked at. Everything is kept." : "<b>The READY page</b> looks at this weekend every 2 minutes."}</p></div><div class="card"><h3 style="margin-top:0">When gaps turn red${hint("Before this, a READY key that is not done is amber; inside it, still missing is red and blinks")}</h3>${calendarHtml("ready.due", (c.ready || {}).due)}</div>`
|
||||
+ fieldsHtml("Ready")
|
||||
@@ -935,7 +935,7 @@
|
||||
const screens = [...new Set(Object.values(state.schema.fields).map((f) => f.screen))];
|
||||
return screens.map((s) => { const h = fieldsHtml(s, true); return h ? `<h3>${esc(s)}</h3>${h}` : ""; }).join("") || "<p class='muted'>Nothing.</p>";
|
||||
}
|
||||
// Auto-setup: what was found and added, the join lines, and the install line for another campus.
|
||||
// Auto-setup: what was found and added, the join lines, and the install line for another location.
|
||||
async function setupHtml(c) {
|
||||
const d = await api("/api/discover").catch(() => null);
|
||||
if (!d) return "<p class='muted'>Auto-setup is not answering.</p>";
|
||||
@@ -944,7 +944,7 @@
|
||||
const comps = Object.entries(inv.computers || {}).map(([a, x]) => `<tr><td>${lightDot(x.reachable ? "ok" : "problem", x.reachable ? "logs in" : x.error || "does not let this Mac in yet")}${esc(x.name || a)}</td><td class="small">${esc(x.os || "")}</td><td class="mono small">${esc(x.ip || "")}</td><td class="small muted">${x.reachable ? "" : esc(x.error || "run the join line on it")}</td></tr>`).join("");
|
||||
const joined = Object.entries(d.joined || {}).filter(([n]) => !Object.values(inv.computers || {}).some((x) => x.name === n)).map(([n, j]) => `<tr><td>${lightDot("warn", "joined; the next look sets it up")}${esc(n)}</td><td class="small">${esc(j.os)}</td><td class="mono small">${esc((j.ips || [])[0] || "")}</td><td class="small muted">joined ${esc(when(j.at))}</td></tr>`).join("");
|
||||
const list = (xs) => xs && xs.length ? `<ul class="small">${xs.map((x) => `<li>${esc(x)}</li>`).join("")}</ul>` : "<p class='small muted'>nothing</p>";
|
||||
const campuses = [["ab", "Apollo Beach"], ["sc", "Southcoast"], ["ws", "Woonsocket"], ["na", "North Attleboro"]].filter(([k]) => k !== (c.site || {}).id);
|
||||
const locations = [["ab", "Apollo Beach"], ["sc", "Southcoast"], ["ws", "Woonsocket"], ["na", "North Attleboro"]].filter(([k]) => k !== (c.site || {}).id);
|
||||
return `<div class="card"><p style="margin-top:0">${inputHtml("discover.auto", F("discover.auto"), (c.discover || {}).auto)}</p>
|
||||
<dl class="kv small"><dt>last look</dt><dd>${last.at ? esc(when(last.at)) : "not yet"}${last.error ? ` · <span style="color:var(--problem)">${esc(last.error)}</span>` : ""}</dd>
|
||||
<dt>found</dt><dd>${inv.connections != null ? `${inv.connections} Companion connections · ${inv.dante || 0} Dante devices · ${inv.midi_sessions || 0} MIDI sessions · wired network ${esc(inv.wired || "?")}` : "–"}</dd></dl>
|
||||
@@ -954,8 +954,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>
|
||||
${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>` : ""}`;
|
||||
${d.hub ? `<div class="card"><h3 style="margin-top:0">Set up another location${hint("On that location'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>
|
||||
${locations.map(([k, n]) => line(`curl -fsSL "${d.base}/location/install?id=${k}" | zsh`, n)).join("")}</div>` : ""}`;
|
||||
}
|
||||
function siteHtml(c) {
|
||||
const f = F("site.editing_code");
|
||||
@@ -1286,7 +1286,7 @@
|
||||
return `<div class="card"><dl class="kv"><dt>running</dt><dd>code ${esc(p.running_version || "?")}</dd><dt>staged</dt><dd>${staged}${same ? " <span class='muted'>· the same</span>" : ""}</dd><dt>folder</dt><dd class="mono small">${esc(p.folder)}</dd></dl>
|
||||
<div class="row" style="margin-top:8px"><button id="installbtn" ${editing() && p.present ? "" : "disabled"} data-hint="Copy the new folder here with scp first. install.sh runs by itself, the config stays, the watchdog restarts, this page comes back.">Install the staged folder</button>${p.job === "running" ? " <span class='muted'>running…</span>" : ""}</div>
|
||||
${p.log && p.log.length ? `<details><summary>last install log${p.log_when ? " (" + esc(when(p.log_when)) + ")" : ""}</summary><pre class="log small">${esc(p.log.join("\n"))}</pre></details>` : ""}</div>
|
||||
<div class="card"><div class="row"><a class="btn" href="/api/site/bundle" download data-hint="A zip of the settings, the saved switcher file and the page-key record: drop it in the repo as sites/${esc((cfg().site || {}).id || "site")}/ or feed it to another campus">Export campus bundle</a>${editing() ? `<label class="btn" data-hint="This campus's name, addresses and PIN stay; every copied computer is flagged until you touch it">New campus from a bundle <input type="file" id="bundlefile" accept=".zip,.json" hidden></label>` : ""}</div></div>`;
|
||||
<div class="card"><div class="row"><a class="btn" href="/api/site/bundle" download data-hint="A zip of the settings, the saved switcher file and the page-key record: drop it in the repo as sites/${esc((cfg().site || {}).id || "site")}/ or feed it to another location">Export location bundle</a>${editing() ? `<label class="btn" data-hint="This location's name, addresses and PIN stay; every copied computer is flagged until you touch it">New location from a bundle <input type="file" id="bundlefile" accept=".zip,.json" hidden></label>` : ""}</div></div>`;
|
||||
}
|
||||
async function outletsHtml() {
|
||||
const o = await api("/api/outlets");
|
||||
@@ -1318,7 +1318,7 @@
|
||||
const conns = n.connections.filter((c) => !c.live).map((c) => `<tr><td>${lightDot("problem")}<span class="mono">${esc(c.name)}</span></td><td class="small">${esc(c.keys.join(", "))}</td><td class="small">${n.companion_ok ? "not in this Companion" : "Companion not answering"}${c.module ? ` <span class="muted">(needs ${esc(c.module)})</span>` : ""}</td><td>${sel("conn", c.name, c.candidates)}</td></tr>`).join("");
|
||||
const dante = n.dante.filter((d) => !d.seen).map((d) => `<tr><td>${lightDot("warn")}<span class="mono">${esc(d.name)}</span></td><td class="small">DANTE</td><td class="small">not seen on the Dante network</td><td>${sel("dante", d.name, d.candidates)}</td></tr>`).join("");
|
||||
const midi = n.midi.filter((m) => !m.session_seen || !m.peer_seen).map((m) => `<tr><td>${lightDot("warn")}<span class="mono">${esc(m.session)} › ${esc(m.peer)}</span></td><td class="small">${esc(m.key)}</td><td class="small">${!m.session_seen ? `session not seen${m.host ? " on " + esc((cfg().hosts[m.host] || {}).name || m.host) : ""}${m.sessions_seen.length ? " (seen: " + esc(m.sessions_seen.join(", ")) + ")" : ""}` : `peer not in the session now${m.peers_seen.length ? " (in it: " + esc(m.peers_seen.join(", ")) + ")" : ""}`}</td><td class="small muted">change it on the key</td></tr>`).join("");
|
||||
return `<div class="card"><h3 style="margin-top:0">Names the keys use that are not here (${n.missing})${hint("A copied campus, or something renamed. Map each name to what this campus has; everything that uses it follows.")}</h3>
|
||||
return `<div class="card"><h3 style="margin-top:0">Names the keys use that are not here (${n.missing})${hint("A copied location, or something renamed. Map each name to what this location has; everything that uses it follows.")}</h3>
|
||||
<div class="tablewrap"><table><tr><th>name</th><th>used by</th><th></th><th></th></tr>${conns}${dante}${midi}</table></div></div>`;
|
||||
}
|
||||
function renameConn(from, to) {
|
||||
@@ -1511,7 +1511,7 @@
|
||||
const spname = q("#spname"); if (spname) spname.onchange = () => setPath("spotify.name", spname.value.trim());
|
||||
const splink = q("#splink"); if (splink) splink.onchange = () => { const m = splink.value.match(/(?:playlist[/:])([A-Za-z0-9]{22})/); if (m) setPath("spotify.playlist", "spotify:playlist:" + m[1]); else if (splink.value.trim()) alertBox("That is not a playlist link."); };
|
||||
const ib = q("#installbtn"); if (ib) ib.onclick = () => confirmBox("Install the staged folder? The watchdog restarts (the deck goes purple for a few seconds) and this page restarts too.", "Install", async () => { try { const r = await post("/api/program/install", {}); alertBox(r.ok ? r.detail : "Not started: " + r.error); setTimeout(() => location.reload(), 25000); } catch (e) { alertBox(e.message); } });
|
||||
const bf = q("#bundlefile"); if (bf) bf.onchange = async () => { const f = bf.files[0]; if (!f) return; confirmBox(`Start a new campus from ${f.name}? Your current draft is replaced.`, "Start", async () => { try { const r = await fetch("/api/site/import", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/octet-stream" }, body: await f.arrayBuffer() }).then((x) => x.json()); if (r.error) return alertBox(r.error); state.draft = r.draft; state.atemPending = true; badge(); alertBox(`Copied from ${r.from}. ${r.errors.length ? "Not applicable yet: " + r.errors.slice(0, 3).join("; ") : "Look through Computers, Keys and the Companion names, then Apply."}`); location.hash = "#settings/companion"; } catch (e) { alertBox(e.message); } }); };
|
||||
const bf = q("#bundlefile"); if (bf) bf.onchange = async () => { const f = bf.files[0]; if (!f) return; confirmBox(`Start a new location from ${f.name}? Your current draft is replaced.`, "Start", async () => { try { const r = await fetch("/api/site/import", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/octet-stream" }, body: await f.arrayBuffer() }).then((x) => x.json()); if (r.error) return alertBox(r.error); state.draft = r.draft; state.atemPending = true; badge(); alertBox(`Copied from ${r.from}. ${r.errors.length ? "Not applicable yet: " + r.errors.slice(0, 3).join("; ") : "Look through Computers, Keys and the Companion names, then Apply."}`); location.hash = "#settings/companion"; } catch (e) { alertBox(e.message); } }); };
|
||||
const af = q("#atemfile"); if (af) af.onchange = async () => { const f = af.files[0]; if (!f) return; try { const r = await fetch("/api/atem-reference", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/xml" }, body: await f.arrayBuffer() }).then((x) => x.json()); showAtem(r); } catch (e) { alertBox(e.message); } };
|
||||
const an = q("#atemnewest"); if (an) an.onclick = async () => { an.disabled = true; an.textContent = "Reading…"; try { showAtem(await post("/api/atem-reference/pick-newest")); } catch (e) { alertBox(e.message); route(); } };
|
||||
const ad = q("#atemdiscard"); if (ad) ad.onclick = async () => { try { await post("/api/atem-reference/discard"); state.atemPending = false; badge(); route(); } catch (e) { alertBox(e.message); } };
|
||||
|
||||
Reference in New Issue
Block a user