${restart && !c.restart_page ? "
No Restart page.
" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)}
+
${restart && !c.restart_page ? "
No Restart page.
" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)}
${!restart ? `
In a group only${hint("Keys with no spot of their own: they show inside their group key. Drop a key here to hide it.")}: ${tray || "none "}
` : ""}
${adders}
${panel}
`;
wireKeyPanel(sel, restart);
@@ -412,10 +412,13 @@
const k = await api("/api/keys");
const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] };
const fixOptions = (cur) => `
none reconnect its Companion connection Dante auto (PoE port or the Dante module) ${Object.entries(c.fixes).map(([id, f]) => `
${esc(f.short || id)} – ${esc(f.label || "")} `).join("")}`;
+ const short = (text, n) => { text = String(text || ""); return text.length > n ? text.slice(0, n - 1) + "…" : text; };
const checks = (t.checks || []).map((chk, j) => {
const spec = state.schema.checks[chk.type] || { fields: [], question: "" };
const sentence = (kt.checks[j] || {}).sentence || chk.type;
- if (!editing()) return `
${esc(sentence)} `;
+ const what = chk.conn || chk.peer || chk.name || chk.msg || (chk.params || {}).process || chk.host || chk.label || "";
+ const summary = `
${esc(spec.question || chk.type)} ${esc(short(what || sentence, 40))} ${hint(sentence)}`;
+ if (!editing()) return `
${summary}${esc(sentence)}
`;
const base = `tiles.${i}.checks.${j}.`;
let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("");
if (chk.type === "remote") {
@@ -427,21 +430,22 @@
const fixSel = typeof chk.fix === "object" && chk.fix ? `
fix per device${hint('A table: device → fix, "*" for everything else')}
`
: `
fix${hint("What holding the key runs for this check")} ${fixOptions(chk.fix)}
`;
const run = typeof chk.fix === "string" && chk.fix && canRunNow(chk.fix) ? `
Run the fix ` : "";
- return `
${esc(spec.question || chk.type)} ${esc(sentence)} ${fields}${fixSel}
+ return `
${summary}${fields}${fixSel}
more ${["tag", "service_only", "debounce_seconds"].filter((f) => !(spec.fields || []).includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("")}type ${esc(chk.type)}
-
${run}Remove
`;
+ ${run}Remove
`;
}).join("");
const plain = !t.members && t.kind !== "master";
- const members = t.members ? (editing() ? `group members ${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => ` ${esc(x.label)} `).join(" ")}
` : `Sums up: ${t.members.map((m) => `${esc((c.tiles.find((x) => x.id === m) || {}).label || m)} `).join(", ")}
`) : "";
- const addMenu = editing() && plain ? `add a check – pick a question – ${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `${types.map((ty) => `${esc(state.schema.checks[ty].words || ty)} `).join("")} `).join("")}
` : "";
+ const members = t.members ? (editing() ? `group members ${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => ` ${esc(x.label)} `).join(" ")}
` : `Sums up ${t.members.map((m) => `${esc((c.tiles.find((x) => x.id === m) || {}).label || m)} `).join(", ")}
`) : "";
+ const addMenu = editing() && plain ? `+ check… ${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `${types.map((ty) => `${esc(state.schema.checks[ty].words || ty)} `).join("")} `).join("")} ` : "";
const label = editing() ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : "";
const holdAlways = editing() && plain ? `hold also when green${hint("A fix that runs on every hold, even with nothing failing")} no ${Object.entries(c.fixes).map(([id, f]) => `${esc(f.short || id)} – ${esc(f.label || "")} `).join("")}
` : "";
- const pos = `more id ${esc(t.id)} ${editing() && !t.kind ? `position${hint("row, column; blank = inside its group only")}
` : ""} `;
- return `${esc(t.label)} ${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}${t.kind ? ` · ${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}` : ""}
- ${kt.says && kt.says.length ? `
Can say ${esc(kt.says.join(", "))}
` : ""}
-
Hold ${esc(hold)} ${esc(kt.holding)}${t.hold_always ? " also when green " : ""}
- ${label}${members}${holdAlways}${checks ? `
${checks} ` : ""}${addMenu}${pos}
- ${editing() && (!t.kind || t.kind === "network") ? `
Delete key
` : ""}
`;
+ const holdList = (kt.holding || "").split(/;\s*/).filter(Boolean);
+ const more = `more id ${esc(t.id)} ${t.kind ? `kind ${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)} ` : ""} ${editing() && !t.kind ? `position${hint("row, column; blank = inside its group only")}
` : ""}${editing() && (!t.kind || t.kind === "network") ? `Delete key
` : ""} `;
+ return `${esc(t.label)} ${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}
+ ${label}${members}${holdAlways}
+ ${holdList.length ? `
Hold ${esc(hold)}: ${esc(short(holdList[0], 48))}${holdList.length > 1 ? ` +${holdList.length - 1} ` : ""}${t.hold_always ? " also when green " : ""} ${holdList.map((h) => `${esc(h)} `).join("")} ` : ""}
+ ${kt.says && kt.says.length ? `
Can say ${kt.says.length} things ${esc(kt.says.join(" · "))}
` : ""}
+ ${checks ? `
${checks}
` : ""}${addMenu}${more}
`;
}
function groupBy(obj, fn) { const out = {}; for (const [k, v] of Object.entries(obj)) (out[fn(v)] = out[fn(v)] || []).push(k); return out; }
function wireKeyPanel(sel, restart) {
diff --git a/settings/ui/style.css b/settings/ui/style.css
index 2f70bea..fd51f06 100644
--- a/settings/ui/style.css
+++ b/settings/ui/style.css
@@ -124,3 +124,14 @@ details.script textarea { width: 100%; font-size: .8rem; }
.top { flex-wrap: wrap; gap: 6px; } .nav { order: 3; width: 100%; justify-content: space-between; margin: 0; } .nav a { padding: 4px 4px; } #search { max-width: none; margin-left: 0; }
main { padding: 12px 14px 40px; } .brand .muted { display: none; }
}
+/* the key editor: a narrow column that never overflows; checks fold */
+.field > * { min-width: 0; } .field input, .field select, .field textarea { max-width: 100%; }
+.keycols { grid-template-columns: minmax(0, 620px) minmax(300px, 1fr); }
+.keycard .field { grid-template-columns: minmax(90px, 130px) minmax(0, 1fr); }
+.chk { border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px; margin: 6px 0; background: var(--bg); }
+.chk > summary { list-style: none; cursor: pointer; display: flex; gap: 6px; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
+.chk > summary::-webkit-details-marker { display: none; } .chk > summary::before { content: "▸"; color: var(--muted); font-size: .8rem; } .chk[open] > summary::before { content: "▾"; }
+.chk > summary .muted { overflow: hidden; text-overflow: ellipsis; }
+.chkbody { margin-top: 6px; } .chkbody .field { grid-template-columns: 1fr; gap: 2px; margin: 4px 0; }
+.fold { margin: 4px 0; } .fold > summary { color: var(--fg); }
+.checks { margin-top: 8px; }