From e9ff9b5112b94f2482bfc79eb4eb55bcce265039 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 20:33:58 +0000 Subject: [PATCH] Remove Categories management from dashboard, keep Rooms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Categories was a whole CRUD system (its own file, its own card, its own API routes) for something that only ever had one functional effect: tagging a device "mobile" to enable RTP NAT-keepalive tuning, plus a per-category auto-answer default. Replaced with a single "Mobile/cellular device" checkbox directly on the add-extension form and each extension row, writing the same underlying category string Easy Asterisk's device format already expects ("mobile" or "standard") without a separate category registry to manage. Removes ea_list_categories/ea_create_category/ea_delete_category/ ea_rename_category, their /api/ea-categories* routes, the Categories card, and the now-unused categories.conf sudoers grant. Rooms stays as-is — unlike Categories, it's the only actually-dialable ring/page group in this dashboard (a real Easy Asterisk extension that rings or pages members live), which the dashboard-only Groups feature cannot replace. --- services/security-dashboard.sh | 276 ++------------------------------- 1 file changed, 16 insertions(+), 260 deletions(-) diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index e165a5b..0f0143a 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -498,7 +498,6 @@ _secdash_write_sudoers() { # edits without a full container restart). if [ -n "$_ea_container" ]; then _ea_lines="$_svc_user ALL=(root) NOPASSWD: /usr/bin/docker exec -i $_ea_container tee /etc/asterisk/pjsip.conf -$_svc_user ALL=(root) NOPASSWD: /usr/bin/docker exec -i $_ea_container tee /etc/easy-asterisk/categories.conf $_svc_user ALL=(root) NOPASSWD: /usr/bin/docker exec -i $_ea_container tee /etc/easy-asterisk/rooms.conf $_svc_user ALL=(root) NOPASSWD: /usr/bin/docker exec $_ea_container asterisk -rx module\ reload\ res_pjsip.so $_svc_user ALL=(root) NOPASSWD: /usr/bin/docker exec $_ea_container asterisk -rx pjsip\ show\ endpoints @@ -1727,10 +1726,8 @@ ASTERISK_EA_CONFIG_DIR = os.environ.get("ASTERISK_EA_CONFIG_DIR", "") ASTERISK_EA_CONTAINER = os.environ.get("ASTERISK_EA_CONTAINER", "") EA_PJSIP_CONTAINER_PATH = "/etc/asterisk/pjsip.conf" -EA_CATEGORIES_CONTAINER_PATH = "/etc/easy-asterisk/categories.conf" EA_ROOMS_CONTAINER_PATH = "/etc/easy-asterisk/rooms.conf" EA_EXT_RE = re.compile(r"^\d{1,10}$") -EA_CATID_RE = re.compile(r"^[a-z0-9]+$") def ea_installed(): @@ -1741,10 +1738,6 @@ def _ea_pjsip_host_path(): return os.path.join(ASTERISK_CONFIG_DIR, "pjsip.conf") if ASTERISK_CONFIG_DIR else None -def _ea_categories_host_path(): - return os.path.join(ASTERISK_EA_CONFIG_DIR, "categories.conf") if ASTERISK_EA_CONFIG_DIR else None - - def _ea_rooms_host_path(): return os.path.join(ASTERISK_EA_CONFIG_DIR, "rooms.conf") if ASTERISK_EA_CONFIG_DIR else None @@ -2103,99 +2096,6 @@ def ea_change_device_category(extension, new_category): return True, "Category changed" -def ea_list_categories(): - path = _ea_categories_host_path() - categories = [] - if not path or not os.path.isfile(path): - return categories - with open(path) as f: - for line in f: - line = line.strip() - if line and not line.startswith("#"): - parts = line.split("|") - if len(parts) >= 3: - categories.append({"id": parts[0], "name": parts[1], "auto_answer": parts[2], - "description": parts[3] if len(parts) > 3 else ""}) - return categories - - -def ea_create_category(cat_id, name, auto_answer="", description=""): - path = _ea_categories_host_path() - if not path: - return False, "No Asterisk install detected on this box" - cat_id = (cat_id or "").strip().lower() - name = (name or "").strip() - if not EA_CATID_RE.match(cat_id): - return False, "Category ID must be lowercase letters/digits only" - if not name: - return False, "Name required" - - current = "" - if os.path.isfile(path): - with open(path) as f: - current = f.read() - else: - current = "# Format: id|name|auto_answer|description\n" - - for line in current.splitlines(): - line = line.strip() - if line and not line.startswith("#") and line.split("|")[0] == cat_id: - return False, "Category ID already exists" - - if not current.endswith("\n"): - current += "\n" - new_content = current + "%s|%s|%s|%s\n" % (cat_id, name, auto_answer, description) - ok, err = ea_docker_write(EA_CATEGORIES_CONTAINER_PATH, new_content) - return (True, "Category created") if ok else (False, err) - - -def ea_delete_category(cat_id): - path = _ea_categories_host_path() - if not path or not os.path.isfile(path): - return False, "Categories file not found" - with open(path) as f: - lines = f.readlines() - new_lines = [] - found = False - for line in lines: - stripped = line.strip() - if stripped and not stripped.startswith("#") and stripped.split("|")[0] == cat_id: - found = True - continue - new_lines.append(line) - if not found: - return False, "Category not found" - ok, err = ea_docker_write(EA_CATEGORIES_CONTAINER_PATH, "".join(new_lines)) - return (True, "Category deleted") if ok else (False, err) - - -def ea_rename_category(cat_id, new_name): - path = _ea_categories_host_path() - if not path or not os.path.isfile(path): - return False, "Categories file not found" - new_name = (new_name or "").strip() - if not new_name: - return False, "Name required" - with open(path) as f: - lines = f.readlines() - new_lines = [] - found = False - for line in lines: - stripped = line.strip() - if stripped and not stripped.startswith("#"): - parts = stripped.split("|") - if len(parts) >= 2 and parts[0] == cat_id: - parts[1] = new_name - new_lines.append("|".join(parts) + "\n") - found = True - continue - new_lines.append(line) - if not found: - return False, "Category not found" - ok, err = ea_docker_write(EA_CATEGORIES_CONTAINER_PATH, "".join(new_lines)) - return (True, "Category renamed") if ok else (False, err) - - def ea_list_rooms(): path = _ea_rooms_host_path() rooms = [] @@ -2691,23 +2591,22 @@ INDEX_HTML = """
- +
What these columns mean -

Name and Category are editable in place — click, type, then Save. Adding an extension generates a random password and reloads PJSIP + rebuilds the dialplan automatically; the password is shown once, at the top of this card.

+

Name is editable in place — click, type, then Save. Adding an extension generates a random password and reloads PJSIP + rebuilds the dialplan automatically; the password is shown once, at the top of this card. Mobile tags a device as a cellular/off-LAN softphone (like a phone app over wifi/data) so Asterisk sends RTP NAT-keepalive traffic to keep it reachable — leave it off for anything on the LAN.

PSTN sets how the outside phone network reaches this extension, and the Whitelist beside it is the one list of numbers that mode applies to:

-
Rooms (ring groups)
@@ -3052,9 +2925,8 @@ async function loadCrowdsecStatus() { document.getElementById("crowdsec-tab-btn").style.display = data.installed ? "" : "none"; } -// ── Extensions tab (extensions + categories + rooms + groups + trunk) ────── -let eaDevices = [], eaCategories = [], eaRooms = [], eaStatusMap = {}; -let eaCatSort = { key: null, dir: 1 }; +// ── Extensions tab (extensions + rooms + groups + trunk) ─────────────────── +let eaDevices = [], eaRooms = [], eaStatusMap = {}; let eaRoomSort = { key: null, dir: 1 }; async function initExtensionsTab() { @@ -3069,116 +2941,16 @@ async function initExtensionsTab() { await refreshExtensionsTab(); } -// Sequenced (not parallel) — extension rows render a category ${catOptions}` + const mobileCell = e.ea + ? `` : ''; const statusCell = e.ea ? `${esc(status)}` @@ -3312,7 +3081,7 @@ function renderExtensions() { return ` ${esc(e.ext)} - ${catCell} + ${mobileCell} ${statusCell} ${esc(e.transport)}${e.encryption && e.encryption !== "no" ? " / " + esc(e.encryption) : ""} ${restrictSelect(e.restrict)} @@ -3348,13 +3117,13 @@ function rowEdits(tr) { const model = extRows.find(e => e.ext === ext); if (!model) return null; const nameEl = tr.querySelector(".ext-name"); - const catEl = tr.querySelector(".ext-category"); + const mobileEl = tr.querySelector(".ext-mobile"); const modeEl = tr.querySelector(".ext-restrict"); const numsEl = tr.querySelector(".ext-numbers"); const msgEl = tr.querySelector(".ext-messaging"); const edits = {}; if (nameEl && !nameEl.disabled && nameEl.value.trim() !== model.name) edits.name = nameEl.value.trim(); - if (catEl && catEl.value !== model.category) edits.category = catEl.value; + if (mobileEl && mobileEl.checked !== (model.category === "mobile")) edits.category = mobileEl.checked ? "mobile" : "standard"; if (modeEl && modeEl.value !== model.restrict) edits.restrict = modeEl.value; if (numsEl && numsEl.value !== model.allowed_numbers) edits.allowed_numbers = numsEl.value; if (msgEl && msgEl.checked !== !!model.messaging) edits.messaging = msgEl.checked; @@ -3481,7 +3250,7 @@ document.getElementById("ea-dev-save").addEventListener("click", async () => { const data = await postJSON("/api/ea-devices", { name, extension, - category: document.getElementById("ea-dev-category").value, + category: document.getElementById("ea-dev-mobile").checked ? "mobile" : "standard", conn_type: document.getElementById("ea-dev-conn").value, auto_answer: document.getElementById("ea-dev-aa").value || null, }); @@ -3493,6 +3262,7 @@ document.getElementById("ea-dev-save").addEventListener("click", async () => { document.getElementById("ext-password-callout").classList.add("show"); document.getElementById("ea-dev-name").value = ""; document.getElementById("ea-dev-ext").value = ""; + document.getElementById("ea-dev-mobile").checked = false; extAddForm.classList.remove("open"); } else { toast(data.message || "Failed to add extension", "err"); @@ -3971,8 +3741,6 @@ class Handler(BaseHTTPRequestHandler): self._json({"installed": ea_installed()}) elif self.path == "/api/ea-devices": self._json({"devices": ea_list_devices(), "status": ea_get_status()}) - elif self.path == "/api/ea-categories": - self._json({"categories": ea_list_categories()}) elif self.path == "/api/ea-rooms": self._json({"rooms": ea_list_rooms()}) else: @@ -4043,18 +3811,6 @@ class Handler(BaseHTTPRequestHandler): elif self.path == "/api/ea-devices/category": ok, message = ea_change_device_category(payload.get("extension", ""), payload.get("category", "")) self._json({"ok": ok, "message": message}) - elif self.path == "/api/ea-categories": - ok, message = ea_create_category( - payload.get("id", ""), payload.get("name", ""), - payload.get("auto_answer", ""), payload.get("description", "") - ) - self._json({"ok": ok, "message": message}) - elif self.path == "/api/ea-categories/delete": - ok, message = ea_delete_category(payload.get("id", "")) - self._json({"ok": ok, "message": message}) - elif self.path == "/api/ea-categories/rename": - ok, message = ea_rename_category(payload.get("id", ""), payload.get("name", "")) - self._json({"ok": ok, "message": message}) elif self.path == "/api/ea-rooms": ok, message = ea_create_room( payload.get("extension", ""), payload.get("name", ""),