Let an existing Ring Group's type/timeout be edited, not just set at creation

Rename, member add/remove, and DID assignment could all already be
changed after a room existed -- type and timeout could only be set once,
at creation, with no way to flip an existing Ring group to Page (or back)
without deleting and recreating it, losing its members/DID assignment in
the process. Reported directly: "I can't edit the ring group to change
it to a page group."

Turns the Timeout/Type columns into inline-editable controls (matching
the same select/input the creation form already uses) with a Save button
per row, backed by a new ea_update_room_settings() that rewrites just
those two fields in rooms.conf, leaving name/members/DID untouched --
same read-modify-write pattern ea_rename_room() already uses.

Verified: bash -n, py_compile on the extracted embedded app.py, node
--check on the extracted embedded JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDyKC6Kdg7tofmYSmRtgww
This commit is contained in:
Claude
2026-07-29 03:03:15 +00:00
parent ca750f5432
commit 646b7b6fde
+62 -2
View File
@@ -2721,6 +2721,44 @@ def ea_rename_room(extension, new_name):
return True, "Room renamed"
def ea_update_room_settings(extension, room_type, timeout):
"""Changes an existing room's type (ring/page) and timeout without
touching its name, members, or any DID assignment — the one thing
creating or renaming a room couldn't already do: change these two
fields after the room exists, rather than only at creation time."""
path = _ea_rooms_host_path()
if not path or not os.path.isfile(path):
return False, "Rooms file not found"
room_type = (room_type or "").strip()
if room_type not in ("ring", "page"):
return False, "Type must be 'ring' or 'page'"
timeout = str(timeout or "").strip()
if not timeout.isdigit() or int(timeout) <= 0:
return False, "Timeout must be a positive number of seconds"
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) >= 5 and parts[0] == extension:
parts[3] = timeout
parts[4] = room_type
new_lines.append("|".join(parts) + "\n")
found = True
continue
new_lines.append(line)
if not found:
return False, "Room not found"
ok, err = ea_docker_write(EA_ROOMS_CONTAINER_PATH, "".join(new_lines))
if not ok:
return False, err
ea_rebuild_dialplan()
return True, "Room settings updated"
def _ea_update_room_members(extension, new_members):
path = _ea_rooms_host_path()
if not path or not os.path.isfile(path):
@@ -4038,10 +4076,14 @@ function renderEaRooms() {
<td>${esc(r.extension)}</td>
<td>${esc(r.name)}</td>
<td>${memberChips || '<span class="muted">none</span>'}<br>${addPicker}</td>
<td>${esc(r.timeout)}</td>
<td>${esc(r.type)}</td>
<td><input type="text" class="room-timeout-input" value="${esc(r.timeout)}" style="width:5rem"></td>
<td><select class="room-type-select">
<option value="ring" ${r.type === "ring" ? "selected" : ""}>Ring</option>
<option value="page" ${r.type === "page" ? "selected" : ""}>Page</option>
</select></td>
<td class="pstn-only">${didCell}</td>
<td class="actions">
<button class="action" onclick="saveEaRoomSettings('${esc(r.extension)}', this)">Save</button>
<button class="action" onclick="renameEaRoom('${esc(r.extension)}')">Rename</button>
<button class="action danger" onclick="deleteEaRoom('${esc(r.extension)}')">Delete</button>
</td>
@@ -4049,6 +4091,19 @@ function renderEaRooms() {
}).join("") || '<tr><td colspan=7 class=empty>No rooms yet.</td></tr>';
}
async function saveEaRoomSettings(ext, btn) {
const row = btn.closest("tr");
const type = row.querySelector(".room-type-select").value;
const timeout = row.querySelector(".room-timeout-input").value.trim() || "60";
const res = await fetch("/api/ea-rooms/settings", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({extension: ext, type, timeout}),
});
const data = await res.json();
toast(data.message || (data.ok ? "Room settings saved" : "Failed"), data.ok ? "ok" : "err");
loadEaRooms();
}
document.querySelectorAll("#ea-room-table th.sortable").forEach(th => {
th.addEventListener("click", () => {
const key = th.dataset.sort;
@@ -4507,6 +4562,11 @@ class Handler(BaseHTTPRequestHandler):
elif self.path == "/api/ea-rooms/rename":
ok, message = ea_rename_room(payload.get("extension", ""), payload.get("name", ""))
self._json({"ok": ok, "message": message})
elif self.path == "/api/ea-rooms/settings":
ok, message = ea_update_room_settings(
payload.get("extension", ""), payload.get("type", ""), payload.get("timeout", "")
)
self._json({"ok": ok, "message": message})
elif self.path == "/api/ea-rooms/members/add":
ok, message = ea_add_room_member(payload.get("room", ""), payload.get("device", ""))
self._json({"ok": ok, "message": message})