diff --git a/settings/ui/app.js b/settings/ui/app.js index 68d2430..762542b 100644 --- a/settings/ui/app.js +++ b/settings/ui/app.js @@ -1225,10 +1225,23 @@ return `
${!cc ? "

Companion is not answering; the list is from the keys.

" : ""}
${rows || ""}
in use${hint("Tick a deck and it gets an HD key in the REC group; untick and the key goes (its cell stays empty)")}key
No HyperDeck connections in Companion.
${editing() && rows ? `
` : ""}
`; } + // The wired network(s) MIDI must ride: the MIDI key's wired check (empty = no rule). + function midiNets(c) { return c.tiles.concat((c.ready_page || {}).keys || []).flatMap((t) => (t.checks || []).filter((ch) => ch.type === "midi_wired" && !ch.disabled).flatMap((ch) => ch.nets || [])); } + function ip4(a) { const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?::\d+)?$/.exec(String(a || "").trim()); return m ? m.slice(1, 5).reduce((n, x) => n * 256 + Number(x), 0) : null; } + function onWired(nets, a) { const v = ip4(a); return v !== null && nets.some((n) => { const [b, bits] = String(n).split("/"); const k = Number(bits || 32), base = ip4(b); return base !== null && Math.floor(v / 2 ** (32 - k)) === Math.floor(base / 2 ** (32 - k)); }); } function midiHtml(c) { + const nets = midiNets(c); + let rule = null; + c.tiles.forEach((t, ti) => (t.checks || []).forEach((ch, j) => { if (!rule && ch.type === "midi_wired") rule = { ti, j, ch }; })); + const netRow = rule + ? `
wired network${hint("Every MIDI link must come over this network (several: comma between). A link on Wi-Fi or Dante, or joined by name, turns the MIDI key red; holding MIDI moves it here.")}
` + : `

No wired-network rule: MIDI links may come over any network.${editing() ? ` ` : ""}

`; const rows = []; - c.tiles.forEach((t, ti) => (t.checks || []).forEach((ch, j) => { if (ch.type === "midi_peer") rows.push(`${onOff(`tiles.${ti}.checks.${j}`, ch, "this MIDI link")}${esc(t.label)}${esc(ch.host ? (c.hosts[ch.host] || {}).name || ch.host : (c.site || {}).ingest_name || "this Mac")}${esc(ch.session)}${esc(ch.peer)}${esc(ch.tag || "")}${editing() ? `` : ""}`); })); - return `
${rows.join("") || ""}
keyMacsessionpeerkey text
none
+ c.tiles.forEach((t, ti) => (t.checks || []).forEach((ch, j) => { if (ch.type === "midi_peer") { + const bad = nets.length && !onWired(nets, ch.address); + const addr = editing() ? `` : `${esc(ch.address || "–")}`; + rows.push(`${onOff(`tiles.${ti}.checks.${j}`, ch, "this MIDI link")}${esc(t.label)}${esc(ch.host ? (c.hosts[ch.host] || {}).name || ch.host : (c.site || {}).ingest_name || "this Mac")}${esc(ch.session)}${esc(ch.peer)}${addr}${bad ? ` not wired` : ""}${esc(ch.tag || "")}${editing() ? `` : ""}`); } })); + return `
${netRow}
${rows.join("") || ""}
keyMacsessionpeeraddress${hint("Where a reconnect joins it. Must be on the wired network.")}key text
none
${editing() ? `
` : ""}
`; } function computersHtml(c) { @@ -1444,6 +1457,17 @@ bx.querySelector("#dok").onclick = () => { const g = d.tiles.find((x) => x.id === bx.querySelector("#ngroup").value); d.tiles.push(t); if (g) g.members.push(id); saveDraft(); closeDialog(); location.hash = "#keys/" + id; }; }); }; for (const cb of main.querySelectorAll("[data-autofix]")) cb.onchange = () => { if (!ensureDraft()) return; const d = state.draft.config, id = cb.dataset.autofix, f = d.fixes[id]; const set = new Set(d.auto_fix.fixes || []); if (cb.checked) { set.add(id); if (closes(f)) f.restart_ok = true; if (f.never_auto) delete f.never_auto; } else set.delete(id); setPath("auto_fix.fixes", [...set]); }; + for (const el of main.querySelectorAll("[data-midiaddr]")) el.onchange = () => { + if (!ensureDraft()) return; + const d = state.draft.config, [ti, j] = el.dataset.midiaddr.split("|").map(Number), ch = d.tiles[ti].checks[j], v = el.value.trim(); + for (const t of d.tiles.concat((d.ready_page || {}).keys || [])) for (const x of t.checks || []) + if (x.type === "midi_peer" && (x.host || "") === (ch.host || "") && x.session === ch.session && x.peer === ch.peer) { if (v) x.address = v; else delete x.address; } + saveDraft(); route(); + }; + const aw = main.querySelector("#addwired"); + if (aw) aw.onclick = () => { if (!ensureDraft()) return; const d = state.draft.config; const t = d.tiles.find((x) => (x.checks || []).some((ch) => ch.type === "midi_peer")); if (!t) return alertBox("Add a MIDI link first."); + const own = d.hosts && Object.values(d.hosts).find((h) => h.self); const ip = own && (own.ip || ""); const guess = /^(\d+\.\d+)\./.exec(ip || ((d.site || {}).ingest_ips || [])[0] || ""); + t.checks.push({ type: "midi_wired", nets: [guess ? guess[1] + ".0.0/16" : "10.1.0.0/16"], fix: Object.keys(d.fixes).find((id) => d.fixes[id].kind === "midi_connect") }); saveDraft(); route(); }; for (const b of main.querySelectorAll("[data-rmcheck]")) b.onclick = () => confirmBox("Remove this MIDI link from its key?", "Remove", () => { if (!ensureDraft()) return; const [tid, j] = b.dataset.rmcheck.split("|"); const t = state.draft.config.tiles.find((x) => x.id === tid); t.checks.splice(+j, 1); saveDraft(); route(); }); const ds = q("#decksave"); if (ds) ds.onclick = () => applyDecks(main); const dc = () => danteCheck(state.draft.config); @@ -1502,9 +1526,7 @@ const midiKey = c.tiles.find((t) => (t.checks || []).some((ch) => ch.type === "midi_peer")); const fixId = Object.keys(c.fixes).find((id) => c.fixes[id].kind === "midi_connect"); // The wired network(s) MIDI must ride (the MIDI key's wired check): suggest and insist on an address on it. - const nets = c.tiles.concat((c.ready_page || {}).keys || []).flatMap((t) => (t.checks || []).filter((ch) => ch.type === "midi_wired").flatMap((ch) => ch.nets || [])); - const ip4 = (a) => { const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?::\d+)?$/.exec(String(a || "").trim()); return m ? m.slice(1, 5).reduce((n, x) => n * 256 + Number(x), 0) : null; }; - const wired = (a) => { const v = ip4(a); return v !== null && nets.some((n) => { const [b, bits] = String(n).split("/"); const k = Number(bits || 32), base = ip4(b); if (base === null) return false; const mask = k ? (2 ** 32 - 2 ** (32 - k)) : 0; return Math.floor(v / 2 ** (32 - k)) === Math.floor(base / 2 ** (32 - k)) && (mask >= 0); }); }; + const nets = midiNets(c), wired = (a) => onWired(nets, a); const wiredFor = (peer) => { for (const src of [live.local || {}].concat(Object.values(live.remote || {}))) for (const s of Object.values(src)) for (const p of s.peers || []) if (p.name === peer && wired(p.address)) return p.address; const h = Object.values(c.hosts).find((x) => x.name === peer || (x.midi_name || "") === peer); const ip = h && String(h.ssh || "").split("@").pop(); return ip && wired(ip) ? ip + ":5004" : ""; }; const sessionsOf = (alias) => { const h = c.hosts[alias]; const src = h.self ? (live.local || {}) : ((live.remote || {})[alias] || {}); return Object.entries(src).map(([name, v]) => [name, (v.peers || []).map((p) => p.name)]); }; diff --git a/settings/ui/style.css b/settings/ui/style.css index cb19cab..6f693f3 100644 --- a/settings/ui/style.css +++ b/settings/ui/style.css @@ -64,6 +64,7 @@ th { color: var(--muted); font-weight: 600; font-size: .78rem; text-transform: u .problems { list-style: none; padding: 0; margin: 0; } .problems li { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line); } .problems .lvl { display: inline-block; width: 10px; height: 10px; border-radius: 3px; } .pill { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--accent-soft); font-size: .82rem; font-family: var(--mono); } +.pill.err { background: transparent; color: var(--problem); border: 1px solid var(--problem); } .chips { display: flex; flex-wrap: wrap; gap: 6px; } .chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); font-size: .85rem; } .chip.ok { border-color: var(--ok); } .chip.bad { border-color: var(--problem); color: var(--problem); } .deck { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 5px; background: var(--deck); padding: 8px; border-radius: 12px; max-width: 640px; width: 100%; }