diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 200f5d0..e165a5b 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -2430,6 +2430,17 @@ INDEX_HTML = """ } details.card > .card-body { padding: 0 var(--sp-4) var(--sp-4); } + /* Groups cards that belong to the same underlying system (Easy Asterisk + device provisioning vs. PSTN-trunk permissions) under one label, so the + page itself explains why e.g. "Rooms" and "Groups" both exist instead + of reading as duplicates. */ + .section-head { + margin: var(--sp-6) 0 var(--sp-2); padding-top: var(--sp-4); + border-top: 1px solid var(--line); + color: var(--text-faint); font-weight: 600; font-size: 0.75rem; + text-transform: uppercase; letter-spacing: 0.04em; + } + /* Inline "what does this mean" disclosure — keeps the explanation one click away instead of pushing every control below three paragraphs of prose. */ details.help { margin: 0 0 var(--sp-3); } @@ -2659,19 +2670,19 @@ INDEX_HTML = """
Changes may not be live yet.

- Edits are written to disk immediately, but Asterisk doesn't always pick them up without a restart — confirmed on personal-DID group reassignment specifically. Existing calls are never affected. + Edits are written to disk immediately, but Asterisk doesn't always pick them up without a restart. Saving never disturbs an active call by itself — but restarting does, so it isn't automatic; click below (or answer "Restart now?" right after a save) once it's a good time.

-
-
-

Extensions

- -
+
+ Extensions
+
+ +
@@ -2729,7 +2740,9 @@ INDEX_HTML = """
-
+ + +
Easy Asterisk device setup
Categories @@ -2784,12 +2797,15 @@ INDEX_HTML = """
+
Groups & personal numbers (PSTN permissions)
+
Groups
What groups do

Named sets of extensions for bulk actions — e.g. enable messaging for everyone in "Sales" at once. A management convenience only: applying an action writes the same per-extension setting each member's own Messaging checkbox above would, one time. It isn't a runtime concept the dialplan knows about, and membership changes never retroactively affect anything already applied. A group can also own a personal number below, which is evaluated live against current membership on every call.

+

Not the same thing as a Room above: a Room is a real, dialable ring-group extension that rings its members live on every call. A Group has no extension of its own and can't be dialed — it only exists to save you re-clicking the same setting on several extensions, and to optionally own a personal number.

@@ -2806,18 +2822,6 @@ INDEX_HTML = """
-
- Concurrent-call caps -
-

A call over either cap gets a busy signal (and an ntfy alert, if enabled) — existing calls are never affected. Usually live on the next call; if a call doesn't reflect a recent change, use "Commit changes" at the top.

-
- - - -
-
-
-
Personal numbers
@@ -3074,7 +3078,7 @@ async function refreshExtensionsTab() { await loadExtensions(); if (eaInstalled) await loadEaRooms(); await loadGroups(); - if (pstnInstalled) { await loadPstnLimits(); await loadPersonalDids(); } + if (pstnInstalled) await loadPersonalDids(); } function renderEaDeviceCategoryOptions() { @@ -3389,6 +3393,7 @@ document.getElementById("ext-save-all").addEventListener("click", async () => { btn.textContent = "Saving…"; let saved = 0; + let touchedPstn = false; const failures = []; for (const row of dirty) { const {ext, model, edits} = row; @@ -3419,7 +3424,7 @@ document.getElementById("ext-save-all").addEventListener("click", async () => { r = await postJSON("/api/pstn-messaging", {ext: ext, enabled: messaging}); } if (!r.ok) throw new Error(r.message || "permission save failed"); - if (pstnInstalled) markPstnDirty(); + if (pstnInstalled) { markPstnDirty(); touchedPstn = true; } } saved++; } catch (err) { @@ -3435,6 +3440,7 @@ document.getElementById("ext-save-all").addEventListener("click", async () => { toast(`Saved ${saved} extension${saved === 1 ? "" : "s"}`, "ok"); } await loadExtensions(); + if (touchedPstn) await offerPstnRestart(); }); function postJSON(url, body) { @@ -3637,13 +3643,6 @@ async function removeEaRoomMember(roomExt, device) { loadEaRooms(); } -async function loadPstnLimits() { - const res = await fetch("/api/pstn-limits"); - const data = await res.json(); - document.getElementById("limit-out").value = data.max_outbound; - document.getElementById("limit-in").value = data.max_inbound; -} - // The Groups card's membership picker. Driven by the same merged extension // list the table above renders, so a group can never offer an extension the // table doesn't show (or miss one it does). @@ -3726,7 +3725,7 @@ document.getElementById("grp-save").addEventListener("click", async () => { }); const data = await res.json(); toast(data.message || (data.ok ? "Group saved" : "Failed"), data.ok ? "ok" : "err"); - if (data.ok && pstnInstalled) markPstnDirty(); + if (data.ok && pstnInstalled) { markPstnDirty(); await offerPstnRestart(); } loadGroups(); }); @@ -3738,7 +3737,7 @@ async function applyGroupMessaging(name, enabled) { }); const data = await res.json(); toast(data.message || (data.ok ? "Applied to group" : "Failed"), data.ok ? "ok" : "err"); - if (data.ok && pstnInstalled) markPstnDirty(); + if (data.ok && pstnInstalled) { markPstnDirty(); await offerPstnRestart(); } loadExtensions(); } @@ -3750,23 +3749,10 @@ async function deleteGroup(name) { }); const data = await res.json(); toast(data.message || (data.ok ? "Group deleted" : "Failed"), data.ok ? "ok" : "err"); - if (data.ok && pstnInstalled) markPstnDirty(); + if (data.ok && pstnInstalled) { markPstnDirty(); await offerPstnRestart(); } loadGroups(); } -document.getElementById("limits-save").addEventListener("click", async () => { - const maxOut = document.getElementById("limit-out").value; - const maxIn = document.getElementById("limit-in").value; - const res = await fetch("/api/pstn-limits", { - method: "POST", headers: {"Content-Type": "application/json"}, - body: JSON.stringify({max_outbound: maxOut, max_inbound: maxIn}), - }); - const data = await res.json(); - toast(data.message || (data.ok ? "Caps saved" : "Failed"), data.ok ? "ok" : "err"); - if (data.ok) markPstnDirty(); - loadPstnLimits(); -}); - // "Commit Changes" — see restart_asterisk_container()'s comment in app.py // for why this button exists: AST_CONFIG() live-reads of these PSTN config // files have been confirmed to sometimes stay stale (returning what was on @@ -3789,7 +3775,7 @@ window.addEventListener("beforeunload", (e) => { e.preventDefault(); e.returnValue = ""; }); -document.getElementById("pstn-restart-btn").addEventListener("click", async () => { +async function commitPstnRestart() { const msg = document.getElementById("pstn-restart-msg"); msg.textContent = "Restarting Asterisk…"; const res = await fetch("/api/asterisk-restart", { method: "POST" }); @@ -3799,7 +3785,24 @@ document.getElementById("pstn-restart-btn").addEventListener("click", async () = pstnDirty = false; document.getElementById("pstn-restart-banner").style.display = "none"; } -}); + return data.ok; +} +document.getElementById("pstn-restart-btn").addEventListener("click", commitPstnRestart); + +// The banner above is easy to miss (confirmed live — the actual "phantom" +// this was built for: an admin saves a permission change, tests a call +// minutes later against Asterisk's still-stale read, and has no reason to +// suspect the save itself). Ask right at the moment of save instead of only +// leaving a passive banner — restart is still opt-in per prompt (not +// automatic) since it drops any calls in progress right now, which a save +// action alone never does. Call this once per logical user action (a +// single save/delete, or one whole batch), never inside a per-row loop, or +// multiple saves in one batch would each pop their own dialog. +async function offerPstnRestart() { + if (confirm("Saved. Asterisk won't use this change until it restarts, which will hang up any calls in progress right now. Restart Asterisk now?")) { + await commitPstnRestart(); + } +} // The personal-DID owner picker spans both lists, so it re-renders from // whichever of the two finished last rather than being owned by either. @@ -3879,6 +3882,7 @@ document.getElementById("pd-save").addEventListener("click", async () => { markPstnDirty(); } loadPersonalDids(); + if (data.ok) await offerPstnRestart(); }); async function removePersonalDid(did) { @@ -3889,7 +3893,7 @@ async function removePersonalDid(did) { }); const data = await res.json(); toast(data.message || (data.ok ? "Number removed" : "Failed"), data.ok ? "ok" : "err"); - if (data.ok) markPstnDirty(); + if (data.ok) { markPstnDirty(); await offerPstnRestart(); } loadPersonalDids(); }