diff --git a/settings/server.py b/settings/server.py
index 9fc6ce8..fa424e7 100644
--- a/settings/server.py
+++ b/settings/server.py
@@ -497,6 +497,16 @@ class Site:
for s in cfg.get("dante_switches", []):
rows.setdefault("Netgear " + s, {"name": "Netgear " + s, "kind": "Dante switch", "address": s, "watched_by": {"dante"}, "light": "unknown",
"reason": "", "location": gear.get("Netgear " + s, {}).get("location", ""), "notes": ""})
+ for name, g in gear.items(): # gear added by hand, watched by no key
+ if not isinstance(g, dict):
+ continue
+ row = rows.setdefault(name, {"name": name, "kind": g.get("kind") or "device", "address": g.get("address"), "watched_by": set(), "light": "not_needed",
+ "reason": "not watched", "location": g.get("location", ""), "notes": g.get("notes", "")})
+ if g.get("kind"):
+ row["kind"] = g["kind"]
+ if g.get("address") and not row.get("address"):
+ row["address"] = g["address"]
+ row["own"] = True
out = []
for r in rows.values():
r["watched_by"] = sorted(r["watched_by"])
@@ -877,8 +887,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
if path == "/api/gear":
cfg = site.load()
gear = dict(cfg.get("gear") or {})
- name = str(body.get("name", ""))[:80]
- gear[name] = {"location": str(body.get("location", ""))[:120], "notes": str(body.get("notes", ""))[:300]}
+ name = str(body.get("name", "")).strip()[:80]
+ if not name:
+ return self.send(400, {"error": "a name is needed"})
+ if body.get("remove"):
+ gear.pop(name, None)
+ else:
+ entry = {k: str(body.get(k, "")).strip()[:300] for k in ("location", "notes", "kind", "address")}
+ gear[name] = {k: v for k, v in entry.items() if v}
draft = site.draft.read() or {"config": cfg}
draft_cfg = dict(draft["config"], gear=gear)
d, _ = site.draft.update(draft_cfg, who, site.cfg_sha)
diff --git a/settings/ui/app.js b/settings/ui/app.js
index 9ba7ac4..3ec9c56 100644
--- a/settings/ui/app.js
+++ b/settings/ui/app.js
@@ -491,13 +491,15 @@
const selected = rest[0] && rest[0] !== "gear" && rest[0] !== "setup" ? rest[0] : null;
compTab = rest[0] === "gear" || rest[0] === "setup" ? rest[0] : "map";
const tabs = `
- ${editing() ? `` : ""}
`;
+ ${editing() ? `` : ""}`;
let body = "";
if (compTab === "gear") body = await gearHtml();
else if (compTab === "setup") body = setupHtml(m, (await api("/api/setup").catch(() => ({ computers: [] }))).computers);
else {
const rows = m.computers.slice().sort((a, b) => (a.word === "Problem" ? 0 : 1) - (b.word === "Problem" ? 0 : 1)).map((c) => `| ${wordOf(c.word)} | ${esc(c.name)}${c.self ? ' (this Mac)' : ""} | ${esc(c.role)} | ${(c.keys || []).map((k) => `${esc((cfg().tiles.find((t) => t.id === k) || { label: k }).label)}`).join(" ")} | ${esc(c.reason)}${c.looked ? ` looked ${esc(ago(Date.now() / 1000 - c.looked))}` : ""} |
`).join("");
- body = `| status | computer | does | keys${hint("The keys that turn red if it dies")} | |
${rows}
`;
+ const looked = Math.max(0, ...m.computers.map((c) => c.looked || 0));
+ body = `${looked ? "looked " + esc(ago(Date.now() / 1000 - looked)) : "not looked yet"}
+ | status | computer | does | keys${hint("The keys that turn red if it dies")} | |
${rows}
`;
const c = m.computers.find((x) => x.alias === selected);
if (c) body += computerCard(c);
}
@@ -507,7 +509,10 @@
for (const b of main.querySelectorAll("[data-job]")) b.onclick = () => job(b.dataset.job);
const pb = document.getElementById("probe");
if (pb) pb.onclick = async () => { pb.disabled = true; pb.textContent = "Looking…"; try { await post("/api/machines/probe"); } catch (e) { alertBox(e.message); } setTimeout(() => computers(rest, true).then(() => wireInputs(main)), 4000); };
- for (const b of main.querySelectorAll("[data-gear]")) b.onclick = async () => { const row = b.closest("tr"); try { await post("/api/gear", { name: b.dataset.gear, location: row.querySelector("[data-loc]").value, notes: row.querySelector("[data-notes]").value }); const d = await api("/api/draft"); state.draft = d.draft; badge(); b.textContent = "Saved"; } catch (e) { alertBox(e.message); } };
+ const ag = main.querySelector("#addgear"); if (ag) ag.onclick = () => gearDialog(null);
+ for (const a of main.querySelectorAll("[data-gearedit]")) a.onclick = (e) => { e.preventDefault(); gearDialog(a.dataset.gearedit); };
+ for (const b of main.querySelectorAll("[data-geardel]")) b.onclick = () => confirmBox(`Remove ${b.dataset.geardel} from the gear list?`, "Remove", async () => { try { const r = await post("/api/gear", { name: b.dataset.geardel, remove: true }); state.draft = r.draft; badge(); route(); } catch (e) { alertBox(e.message); } });
+ for (const b of main.querySelectorAll("[data-gear]")) b.onclick = async () => { const row = b.closest("tr"); try { const prev = (cfg().gear || {})[b.dataset.gear] || {}; await post("/api/gear", { name: b.dataset.gear, location: row.querySelector("[data-loc]").value, notes: row.querySelector("[data-notes]").value, kind: prev.kind || "", address: prev.address || "" }); const d = await api("/api/draft"); state.draft = d.draft; badge(); b.textContent = "Saved"; } catch (e) { alertBox(e.message); } };
const runs = main.querySelector("#runs"); if (runs) runs.onchange = () => { if (!ensureDraft()) return; try { state.draft.config.hosts[selected].runs = runs.value.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)); saveDraft(); } catch (err) { alertBox("Each line must be a valid {...} entry."); } };
}
function computerCard(c) {
@@ -549,10 +554,46 @@
saveDraft(); location.hash = "#computers";
});
}
+ const GEAR_KINDS = ["recorder", "switcher", "Dante device", "Dante switch", "wireless receiver", "in-ear transmitter", "LED processor", "camera", "network switch", "display", "device"];
async function gearHtml() {
const g = await api("/api/gear");
return ``;
+ ${g.map((r) => `| ${lightDot(r.light, lightWord[r.light])} | ${editing() ? `${esc(r.name)}` : esc(r.name)} | ${esc(r.kind)} | ${esc(r.address || "")}${r.detail ? ` ${esc(r.detail)}` : ""} | ${editing() ? ` ` : `${esc(r.location || "")} ${esc(r.notes || "")}`} ${esc(r.reason || "")} | ${(r.watched_by || []).length ? r.watched_by.map((k) => `${esc((cfg().tiles.find((t) => t.id === k) || { label: k }).label)}`).join(", ") : editing() && r.own ? `` : ""} |
`).join("")}
+ ${editing() ? `` : ""}`;
+ }
+ async function gearDialog(existing) {
+ const c = cfg();
+ const cur = existing ? Object.assign({ name: existing }, (c.gear || {})[existing] || {}) : {};
+ let conns = [];
+ try { const cc = await api("/api/companion"); conns = (cc.connections || []).filter((r) => !(r.watched_by || []).length).map((r) => r.label); } catch (e) {}
+ const plainKeys = c.tiles.filter((t) => !t.members && t.kind !== "master");
+ dialog(`${existing ? "Gear" : "New gear"}${hint("A device that is not a computer. Give it a kind and an address; choose whether a key watches it.")}
+
+
+
+
+
+
+ `, (b) => {
+ b.querySelector("#dcancel").onclick = closeDialog;
+ b.querySelector("#dok").onclick = async () => {
+ const name = b.querySelector("#gname").value.trim(), kind = b.querySelector("#gkind").value, address = b.querySelector("#gaddr").value.trim();
+ const location = b.querySelector("#gloc").value.trim(), notes = b.querySelector("#gnotes").value.trim(), watch = b.querySelector("#gwatch").value, keyId = b.querySelector("#gkey").value;
+ if (!name) return alertBox("A name, please.");
+ if (watch === "ping" && !/^[\w.:-]+$/.test(address)) return alertBox("To ping it, give it an address.");
+ if (!ensureDraft()) return;
+ try { const r = await post("/api/gear", { name, kind, address, location, notes }); state.draft = r.draft; } catch (e) { return alertBox(e.message); }
+ if (watch) {
+ const d = state.draft.config;
+ const chk = watch === "ping" ? { type: "ping", host: address, name: tagOf(name) } : { type: "conn", conn: watch.slice(5), fix: "restart-connection" };
+ let t = d.tiles.find((x) => x.id === keyId);
+ if (!t) { let id = slug(name) || "gear", n = 2; while (d.tiles.some((x) => x.id === id)) id = slug(name) + "_" + n++; t = { id, label: tagOf(name), checks: [] }; const pos = firstEmpty(d, "status"); if (pos) t.pos = pos; d.tiles.push(t); }
+ t.checks = t.checks || []; t.checks.push(chk);
+ saveDraft(); closeDialog(); location.hash = "#keys/" + t.id; return;
+ }
+ badge(); closeDialog(); route();
+ };
+ });
}
function setupHtml(m, generated) {
return m.computers.map((c) => {