From c54d3a6792c5ab1b431311c14255be1d1886eb52 Mon Sep 17 00:00:00 2001 From: Docker Config Backup Date: Sun, 7 Jun 2026 10:32:18 +0200 Subject: [PATCH] feat: align keyboard LocalID to GlobalID and preserve server credentials Two operator requirements for MBeg keyboards and server connections: 1. align_keyboard_local_ids(): on every keyboard interface under Clients/GeViIO/GeViIO_01, set each video input/output LocalID equal to its GlobalID so operators can address cameras/monitors by global id. Virtual decoders/servers under GeViIO_Virtual keep their sequential local ids. Runs after prune in /api/set/export and /api/batch/build. 2. preserve_server_credentials(): server User/Password are entered by hand and must survive re-importing the server list from Excel. Matched by alias, an existing non-empty credential overrides the imported value. Applied to G-Core and GeViScope servers in /api/batch/build. Co-Authored-By: Claude Opus 4.8 --- backend/app/geviset.py | 58 ++++++++++++++++++++++++++++++++++++++++++ backend/app/main.py | 4 +++ 2 files changed, 62 insertions(+) diff --git a/backend/app/geviset.py b/backend/app/geviset.py index fd6222f..3d95a59 100644 --- a/backend/app/geviset.py +++ b/backend/app/geviset.py @@ -1120,6 +1120,64 @@ def disable_keyboard_ptz(tree): return cleared +def align_keyboard_local_ids(tree): + """Make LocalID equal GlobalID for every video input and output on every + MBeg keyboard interface (Clients/GeViIO/GeViIO_01). + + Operators address cameras/monitors on the keyboards by their global id, + so the local id must match it. Other interfaces (virtual decoders and + servers under GeViIO_Virtual) keep their own sequential local ids. + """ + geviio = _find_folder(tree, ["Clients", "GeViIO", "GeViIO_01"]) + if not geviio: + return 0 + changed = 0 + for interface in geviio.get("children", []): + if not isinstance(interface, dict) or interface.get("type") != "folder": + continue + for sub in ("VideoInputs", "VideoOutputs"): + container = _child_by_name(interface, sub) + if container is None: + continue + for entry in container.get("children", []): + if entry.get("type") != "folder": + continue + local_node = _child_by_name(entry, "LocalID") + global_node = _child_by_name(entry, "GlobalID") + if local_node is None or global_node is None: + continue + global_id = global_node.get("value") + if isinstance(global_id, int) and local_node.get("value") != global_id: + local_node["value"] = global_id + changed += 1 + return changed + + +def preserve_server_credentials(new_servers, existing_servers): + """Keep User/Password that were already entered on the matching server. + + Server credentials are filled in by hand and must not be wiped when the + server list is re-imported from Excel. Servers are matched by alias; an + existing non-empty user or password overrides the imported value. + """ + by_alias = {} + for server in existing_servers or []: + alias = str(server.get("alias", "")).strip() + if alias: + by_alias[alias] = server + preserved = 0 + for server in new_servers or []: + old = by_alias.get(str(server.get("alias", "")).strip()) + if not old: + continue + if old.get("user"): + server["user"] = old["user"] + if old.get("password"): + server["password"] = old["password"] + preserved += 1 + return preserved + + def prune_video_inputs(tree, global_ids): target_ids = {int(x) for x in global_ids if isinstance(x, int) and x > 0} if not target_ids: diff --git a/backend/app/main.py b/backend/app/main.py index b18ac4a..3447bf3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -111,6 +111,7 @@ async def export_set(payload: dict): geviset.ensure_vx3_video_inputs(tree, camera_ids, ptz_by_id) geviset.prune_video_inputs(tree, camera_ids) geviset.disable_keyboard_ptz(tree) + geviset.align_keyboard_local_ids(tree) print( f"EXPORT camera_ids={len(camera_ids)} contains_101027={101027 in camera_ids}", flush=True, @@ -303,6 +304,7 @@ async def build_from_excel( geviset.ensure_vx3_video_inputs(tree, camera_ids, ptz_by_id) geviset.prune_video_inputs(tree, camera_ids) geviset.disable_keyboard_ptz(tree) + geviset.align_keyboard_local_ids(tree) if servers and servers.filename: if not servers.filename.lower().endswith(".xlsx"): @@ -316,6 +318,8 @@ async def build_from_excel( s["id"] = str(idx) bundle_gcore = geviset.extract_gcore(tree) bundle_gsc = geviset.extract_gsc(tree) + geviset.preserve_server_credentials(gcore_list, bundle_gcore["servers"]) + geviset.preserve_server_credentials(gsc_list, bundle_gsc["servers"]) bundle_gcore["servers"] = gcore_list bundle_gsc["servers"] = gsc_list geviset.apply_gcore(tree, bundle_gcore)