From fa22779d619a039a6e13bfd5e22871a68b533de9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 19:49:22 +0000 Subject: [PATCH] Add member checkboxes to Ring Group creation Rooms' original flow was always create-empty-then-add-members-one-at- a-time in the table afterward; the merge kept that flow and dropped Groups' actual "check boxes, name it, done" creation UX, which is what was actually asked for. Add-form now has a member picker so a Ring Group can be built with its full membership in one step; existing groups still use the per-row add/remove control for later changes. --- services/security-dashboard.sh | 37 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 1063814..f42ca02 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -2100,7 +2100,7 @@ def ea_list_rooms(): return rooms -def ea_create_room(extension, name, room_type="ring", timeout="60"): +def ea_create_room(extension, name, room_type="ring", timeout="60", members=None): path = _ea_rooms_host_path() if not path: return False, "No Asterisk install detected on this box" @@ -2116,6 +2116,7 @@ def ea_create_room(extension, name, room_type="ring", timeout="60"): # this room gets assigned a personal number. if not GROUP_NAME_RE.match(name): return False, "Name must be 1-40 characters (letters, digits, spaces, - or _)" + clean_members = [str(m).strip() for m in (members or []) if EA_EXT_RE.match(str(m).strip())] current = "" if os.path.isfile(path): @@ -2131,12 +2132,12 @@ def ea_create_room(extension, name, room_type="ring", timeout="60"): if not current.endswith("\n"): current += "\n" - new_content = current + "%s|%s||%s|%s\n" % (extension, name, timeout, room_type) + new_content = current + "%s|%s|%s|%s|%s\n" % (extension, name, ",".join(clean_members), timeout, room_type) ok, err = ea_docker_write(EA_ROOMS_CONTAINER_PATH, new_content) if not ok: return False, err ea_rebuild_dialplan() - sync_room_group_mirror(name, []) + sync_room_group_mirror(name, clean_members) return True, "Room created" @@ -2653,7 +2654,7 @@ INDEX_HTML = """ Ring Groups

Pick extensions, give them a name, and they ring (or page/auto-answer) together as one dialable extension — and can optionally be assigned a personal number below, so an inbound call to that number rings every current member.

-
+
+
Members:
+
@@ -2981,9 +2984,26 @@ async function loadExtensions() { extRows = Array.from(byExt.values()); renderExtensions(); + renderRoomMemberPicker(); renderPersonalDidOwnerOptions(); } +// The new-ring-group form's member checkboxes — driven by the same merged +// extension list the table above renders, so it can never offer an +// extension the table doesn't show (or miss one it does). Existing rooms' +// membership is edited via the per-row add/remove control in the table +// instead — this picker is only for building a group's initial membership +// at creation time. +function renderRoomMemberPicker() { + const el = document.getElementById("ea-room-members"); + if (!el) return; + el.innerHTML = extRows.map(e => ` + + `).join("") || 'No extensions found'; +} + let extRows = []; let extSort = { key: null, dir: 1 }; @@ -3335,15 +3355,17 @@ document.getElementById("ea-room-save").addEventListener("click", async () => { const name = document.getElementById("ea-room-name").value.trim(); const type = document.getElementById("ea-room-type").value; const timeout = document.getElementById("ea-room-timeout").value.trim() || "60"; + const members = Array.from(document.querySelectorAll(".ea-room-member-cb:checked")).map(cb => cb.value); const res = await fetch("/api/ea-rooms", { method: "POST", headers: {"Content-Type": "application/json"}, - body: JSON.stringify({extension, name, type, timeout}), + body: JSON.stringify({extension, name, type, timeout, members}), }); const data = await res.json(); - toast(data.message || (data.ok ? "Room added" : "Failed"), data.ok ? "ok" : "err"); + toast(data.message || (data.ok ? "Ring group added" : "Failed"), data.ok ? "ok" : "err"); if (data.ok) { document.getElementById("ea-room-ext").value = ""; document.getElementById("ea-room-name").value = ""; + document.querySelectorAll(".ea-room-member-cb:checked").forEach(cb => { cb.checked = false; }); } loadEaRooms(); }); @@ -3697,7 +3719,8 @@ class Handler(BaseHTTPRequestHandler): elif self.path == "/api/ea-rooms": ok, message = ea_create_room( payload.get("extension", ""), payload.get("name", ""), - payload.get("type", "ring"), payload.get("timeout", "60") + payload.get("type", "ring"), payload.get("timeout", "60"), + payload.get("members", []) ) self._json({"ok": ok, "message": message}) elif self.path == "/api/ea-rooms/delete":
Ext