Gear can be added, edited and removed on Computers > Gear, with an optional watching key; Check now sits on the Map tab
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+18
-2
@@ -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)
|
||||
|
||||
+45
-4
@@ -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 = `<div class="tabs"><button class="${compTab === "map" ? "on" : ""}" onclick="location.hash='#computers'">Map</button><button class="${compTab === "gear" ? "on" : ""}" onclick="location.hash='#computers/gear'" data-hint="Devices that are not computers: switcher, decks, wireless, LED">Gear</button><button class="${compTab === "setup" ? "on" : ""}" onclick="location.hash='#computers/setup'" data-hint="The one-time set-up per computer, with the scripts">Setup</button>
|
||||
<button id="probe" ${m.probing ? "disabled" : ""} data-hint="Look at every computer over the network now (read-only)">${m.probing ? "Looking…" : "Check now"}</button>${editing() ? `<button class="small" data-job="computer" style="margin-left:auto">+ Computer</button>` : ""}</div>`;
|
||||
${editing() ? `<button class="small" data-job="computer" style="margin-left:auto">+ Computer</button>` : ""}</div>`;
|
||||
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) => `<tr class="click${selected === c.alias ? " sel" : ""}" onclick="location.hash='#computers/${esc(c.alias)}'"><td>${wordOf(c.word)}</td><td>${esc(c.name)}${c.self ? ' <span class="muted small">(this Mac)</span>' : ""}</td><td class="small">${esc(c.role)}</td><td class="small">${(c.keys || []).map((k) => `<span class="pill">${esc((cfg().tiles.find((t) => t.id === k) || { label: k }).label)}</span>`).join(" ")}</td><td class="muted small">${esc(c.reason)}${c.looked ? `<br>looked ${esc(ago(Date.now() / 1000 - c.looked))}` : ""}</td></tr>`).join("");
|
||||
body = `<div class="tablewrap"><table><tr><th>status</th><th>computer</th><th>does</th><th>keys${hint("The keys that turn red if it dies")}</th><th></th></tr>${rows}</table></div>`;
|
||||
const looked = Math.max(0, ...m.computers.map((c) => c.looked || 0));
|
||||
body = `<div class="row" style="justify-content:flex-end;margin-bottom:6px"><span class="muted small">${looked ? "looked " + esc(ago(Date.now() / 1000 - looked)) : "not looked yet"}</span><button id="probe" class="small" ${m.probing ? "disabled" : ""} data-hint="Log in to every computer now and refresh the lights (read-only)">${m.probing ? "Looking…" : "Check now"}</button></div>
|
||||
<div class="tablewrap"><table><tr><th>status</th><th>computer</th><th>does</th><th>keys${hint("The keys that turn red if it dies")}</th><th></th></tr>${rows}</table></div>`;
|
||||
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 `<div class="tablewrap"><table><tr><th></th><th>device</th><th>kind</th><th>address</th><th>where${hint("Where it is in the building, and notes")}</th><th>watched by</th></tr>
|
||||
${g.map((r) => `<tr><td>${lightDot(r.light, lightWord[r.light])}</td><td>${esc(r.name)}</td><td class="small">${esc(r.kind)}</td><td class="mono small">${esc(r.address || "")}${r.detail ? `<br><span class="muted">${esc(r.detail)}</span>` : ""}</td><td class="small">${editing() ? `<input data-loc value="${esc(r.location || "")}" placeholder="where it is" style="width:11em"> <input data-notes value="${esc(r.notes || "")}" placeholder="notes" style="width:11em"> <button class="small" data-gear="${esc(r.name)}">Save</button>` : `${esc(r.location || "")} ${esc(r.notes || "")}`} <span class="muted">${esc(r.reason || "")}</span></td><td class="small">${(r.watched_by || []).map((k) => `<a href="#keys/${esc(k)}">${esc((cfg().tiles.find((t) => t.id === k) || { label: k }).label)}</a>`).join(", ")}</td></tr>`).join("")}</table></div>`;
|
||||
${g.map((r) => `<tr><td>${lightDot(r.light, lightWord[r.light])}</td><td>${editing() ? `<a href="#" data-gearedit="${esc(r.name)}" data-hint="Change its kind, address or how it is watched">${esc(r.name)}</a>` : esc(r.name)}</td><td class="small">${esc(r.kind)}</td><td class="mono small">${esc(r.address || "")}${r.detail ? `<br><span class="muted">${esc(r.detail)}</span>` : ""}</td><td class="small">${editing() ? `<input data-loc value="${esc(r.location || "")}" placeholder="where it is" style="width:11em"> <input data-notes value="${esc(r.notes || "")}" placeholder="notes" style="width:11em"> <button class="small" data-gear="${esc(r.name)}">Save</button>` : `${esc(r.location || "")} ${esc(r.notes || "")}`} <span class="muted">${esc(r.reason || "")}</span></td><td class="small">${(r.watched_by || []).length ? r.watched_by.map((k) => `<a href="#keys/${esc(k)}">${esc((cfg().tiles.find((t) => t.id === k) || { label: k }).label)}</a>`).join(", ") : editing() && r.own ? `<button class="small danger" data-geardel="${esc(r.name)}">Remove</button>` : ""}</td></tr>`).join("")}</table></div>
|
||||
${editing() ? `<div class="row" style="margin-top:8px"><button class="small" id="addgear">+ Gear</button></div>` : ""}`;
|
||||
}
|
||||
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(`<h3>${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.")}</h3>
|
||||
<input id="gname" placeholder="name (Tascam, LED wall left, …)" value="${esc(cur.name || "")}" ${existing ? "readonly" : ""}>
|
||||
<select id="gkind">${GEAR_KINDS.map((k) => `<option ${cur.kind === k ? "selected" : ""}>${k}</option>`).join("")}</select>
|
||||
<input id="gaddr" placeholder="address (10.1.x.x), if it has one" value="${esc(cur.address || "")}">
|
||||
<input id="gloc" placeholder="where it is" value="${esc(cur.location || "")}"><input id="gnotes" placeholder="notes" value="${esc(cur.notes || "")}">
|
||||
<select id="gwatch"><option value="">not watched by a key</option><option value="ping">watch it: ping the address</option>${conns.map((n) => `<option value="conn:${esc(n)}">watch its Companion connection ${esc(n)}</option>`).join("")}</select>
|
||||
<select id="gkey"><option value="__new">on a new key</option>${plainKeys.map((t) => `<option value="${esc(t.id)}">on the ${esc(t.label)} key</option>`).join("")}</select>
|
||||
<div class="row"><button class="primary" id="dok">${existing ? "Save" : "Add"}</button><button id="dcancel">Cancel</button></div>`, (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) => {
|
||||
|
||||
Reference in New Issue
Block a user