From 26e26e1b3b42a91e6753c1c96ca80e3d8403ca72 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Tue, 13 Oct 2026 15:10:30 +0200 Subject: [PATCH] Standardbrowser je Workspace-Version (Migration 0058) Vorgabe im Entwurf setzen (Firefox oder ein im Entwurf enthaltener Browser), wird mit kopiert, faellt beim Entfernen des Browsers zurueck und steht in der Wirkungsvorschau. Der Check-in liefert sie als eigenen Auftrag "standardbrowser" (kein Katalog-Merkmal); dessen Rueckmeldung erzeugt keine Ereigniszeile. Co-Authored-By: Claude Opus 5.5 --- app.py | 119 +++++++++++++++++- migrations/0058_workspace_standardbrowser.sql | 14 +++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 migrations/0058_workspace_standardbrowser.sql diff --git a/app.py b/app.py index c9cf4ae..f1e2120 100644 --- a/app.py +++ b/app.py @@ -1033,6 +1033,49 @@ WEBAPP_XDG_CATEGORIES_DEFAULT = "Network;WebBrowser;" KIOSK_BROWSER_PRIORITAET = ["google-chrome", "chromium"] +# Standardbrowser je Workspace-Version (13.10.2026, Migration 0058): Firefox +# ist auf den Images vorinstalliert, die übrigen sind Merkmale und nur +# wählbar, wenn sie in derselben Version enthalten sind. Der Server gibt die +# Vorgabe als eigenen Auftrag an den Agenten weiter (Rolle "standardbrowser", +# Option "browser"); es gibt dafür bewusst kein Katalog-Merkmal, damit die +# Vorgabe nicht zusätzlich in der Merkmal-Auswahl auftaucht. +STANDARDBROWSER_AUFTRAG = "standardbrowser" +STANDARDBROWSER_FIREFOX = "firefox" +STANDARDBROWSER_MERKMALE = ["google-chrome", "chromium", "browser-brave", "microsoft-edge"] + + +def standardbrowser_auswahl(cur, version_id) -> list[dict]: + """Was in dieser Version als Standardbrowser wählbar ist.""" + + cur.execute( + """ + SELECT m.key, m.name + FROM workspace_version_merkmale vm + JOIN merkmale m ON m.id = vm.merkmal_id + WHERE vm.workspace_version_id = %s AND m.key = ANY(%s) + ORDER BY m.name + """, + (version_id, STANDARDBROWSER_MERKMALE), + ) + return [{"key": STANDARDBROWSER_FIREFOX, "name": "Firefox (vorinstalliert)"}] + [ + {"key": key, "name": name} for key, name in cur.fetchall() + ] + + +def set_workspace_standardbrowser(workspace_id: str, browser: str | None): + """Setzt die Vorgabe im Entwurf. Rückgabe: None oder Fehlermeldung.""" + + with get_database_connection() as conn: + with conn.cursor() as cur: + vid = entwurf_id(cur, workspace_id) + if vid is None: + return "Änderungen gehen nur in einem Entwurf - bitte zuerst einen Entwurf anlegen." + if browser is not None and browser not in {b["key"] for b in standardbrowser_auswahl(cur, vid)}: + return "Dieser Browser ist im Workspace nicht enthalten." + cur.execute("UPDATE workspace_versionen SET standardbrowser = %s WHERE id = %s", (browser, vid)) + return None + + def _resolve_kiosk_browser(version_id) -> str | None: with get_database_connection() as conn: with conn.cursor() as cur: @@ -1513,6 +1556,10 @@ def fetch_auftragskatalog_state(device_id: str): ) rows = cur.fetchall() + cur.execute("SELECT standardbrowser FROM workspace_versionen WHERE id = %s", (version_id,)) + standardbrowser_row = cur.fetchone() + standardbrowser = standardbrowser_row[0] if standardbrowser_row else None + overrides = resolve_katalog_overrides(device_id) ergebnis = [] @@ -1530,6 +1577,16 @@ def fetch_auftragskatalog_state(device_id: str): "optionen": decrypt_sensitive_optionen(optionen), }) + # Standardbrowser der Workspace-Version: immer mitschicken, auch als + # "absent" - sonst liefe die Rolle nach dem Zurücknehmen der Vorgabe nie + # wieder und der alte Eintrag bliebe stehen. + ergebnis.append({ + "merkmal": STANDARDBROWSER_AUFTRAG, + "ansible_role": STANDARDBROWSER_AUFTRAG, + "state": "present" if standardbrowser else "absent", + "optionen": {"browser": standardbrowser} if standardbrowser else {}, + }) + return ergebnis def fetch_auftragskatalog_listing(device_id: str): @@ -3663,6 +3720,15 @@ def _version_kopieren(cur, quelle_version_id, ziel_version_id, ziel_workspace_id "INSERT INTO workspace_version_merkmale (workspace_version_id, merkmal_id) VALUES (%s, %s)", (ziel_version_id, merkmal_id), ) + # Standardbrowser mitnehmen, sofern der Browser im Ziel noch enthalten ist + # (merkmal_filter kann ihn beim Kopieren auf eine andere Distro entfernen). + cur.execute("SELECT standardbrowser FROM workspace_versionen WHERE id = %s", (quelle_version_id,)) + quelle_row = cur.fetchone() + if quelle_row and quelle_row[0] in {b["key"] for b in standardbrowser_auswahl(cur, ziel_version_id)}: + cur.execute( + "UPDATE workspace_versionen SET standardbrowser = %s WHERE id = %s", + (quelle_row[0], ziel_version_id), + ) cur.execute( """ INSERT INTO workspace_webapps (id, workspace_id, workspace_version_id, name, url, icon_filename, @@ -3892,8 +3958,16 @@ def workspace_wirkung(workspace_id: str) -> dict | None: cur.execute("SELECT name, url FROM workspace_webapps WHERE workspace_version_id = %s", (vid,)) return set(cur.fetchall()) + def standardbrowser(vid): + if vid is None: + return None + cur.execute("SELECT standardbrowser FROM workspace_versionen WHERE id = %s", (vid,)) + key = cur.fetchone()[0] + return next((b["name"] for b in standardbrowser_auswahl(cur, vid) if b["key"] == key), key) + m_alt, m_neu = merkmale(alt), merkmale(neu) w_alt, w_neu = webapps(alt), webapps(neu) + sb_alt, sb_neu = standardbrowser(alt), standardbrowser(neu) def namen(ids, quelle, art): return sorted(quelle[i][0] for i in ids if quelle[i][1] == art) @@ -3906,7 +3980,10 @@ def workspace_wirkung(workspace_id: str) -> dict | None: "richtlinien_weg": namen(weg, m_alt, "richtlinie"), "webapps_neu": sorted(n for n, _u in w_neu - w_alt), "webapps_weg": sorted(n for n, _u in w_alt - w_neu), - "unveraendert": not (hinzu or weg or w_neu != w_alt), + "standardbrowser_geaendert": sb_alt != sb_neu, + "standardbrowser_alt": sb_alt, + "standardbrowser_neu": sb_neu, + "unveraendert": not (hinzu or weg or w_neu != w_alt or sb_alt != sb_neu), "nutzung": workspace_nutzung(workspace_id), } @@ -4143,6 +4220,10 @@ def fetch_workspace_detail(workspace_id: str): ) version_row = cur.fetchone() + cur.execute("SELECT standardbrowser FROM workspace_versionen WHERE id = %s", (version_id,)) + standardbrowser_row = cur.fetchone() + browser_auswahl = standardbrowser_auswahl(cur, version_id) + cur.execute( """ SELECT m.id, m.key, m.name @@ -4173,6 +4254,8 @@ def fetch_workspace_detail(workspace_id: str): if version_row else None ), "merkmale": merkmale, + "standardbrowser": standardbrowser_row[0] if standardbrowser_row else None, + "standardbrowser_auswahl": browser_auswahl, } def add_merkmal_to_workspace(workspace_id: str, merkmal_id: str): @@ -4553,9 +4636,18 @@ def activate_webapp_template(workspace_id: str, template_id: str): def remove_merkmal_from_workspace(workspace_id: str, merkmal_id: str): with get_database_connection() as conn: with conn.cursor() as cur: + version_id = arbeitsversion_id(cur, workspace_id) cur.execute( "DELETE FROM workspace_version_merkmale WHERE workspace_version_id = %s AND merkmal_id = %s", - (arbeitsversion_id(cur, workspace_id), merkmal_id), + (version_id, merkmal_id), + ) + # War das der Standardbrowser, gilt wieder "keine Vorgabe". + cur.execute( + """ + UPDATE workspace_versionen SET standardbrowser = NULL + WHERE id = %s AND standardbrowser = (SELECT key FROM merkmale WHERE id = %s) + """, + (version_id, merkmal_id), ) def fetch_backends(): @@ -5596,6 +5688,10 @@ def agent_report( with conn.cursor() as cur: merkmal_ids = {} + # Der Standardbrowser-Auftrag ist kein Merkmal und hat keine + # Ereigniszeile (siehe STANDARDBROWSER_AUFTRAG). + payload.results = [e for e in payload.results if e.merkmal != STANDARDBROWSER_AUFTRAG] + for entry in payload.results: if entry.merkmal in merkmal_ids: continue @@ -6353,6 +6449,25 @@ def remove_workspace_merkmal(workspace_id: str, merkmal_id: str, authorization: return {"success": True} +class SetStandardbrowserRequest(BaseModel): + browser: str | None = None + + +@app.put("/api/v1/workspaces/{workspace_id}/standardbrowser") +def put_workspace_standardbrowser( + workspace_id: str, + payload: SetStandardbrowserRequest, + authorization: str | None = Header(default=None), +): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + + if fehler := set_workspace_standardbrowser(workspace_id, payload.browser or None): + return {"success": False, "error": "invalid_request", "message": fehler} + + return {"success": True} + + @app.get("/api/v1/workspaces/{workspace_id}/webapps") def list_workspace_webapps(workspace_id: str, authorization: str | None = Header(default=None)): if not require_service_token(authorization): diff --git a/migrations/0058_workspace_standardbrowser.sql b/migrations/0058_workspace_standardbrowser.sql new file mode 100644 index 0000000..82119b8 --- /dev/null +++ b/migrations/0058_workspace_standardbrowser.sql @@ -0,0 +1,14 @@ +BEGIN; + +-- Standardbrowser je Workspace-Version (Nutzer-Entscheidung 13.10.2026). +-- Webapps ohne Kiosk-Modus und Links aus anderen Programmen öffnen sich im +-- Standardbrowser des Systems; den legte bisher niemand fest. NULL = keine +-- Vorgabe. Wert: 'firefox' (vorinstalliert) oder der Key eines +-- Browser-Merkmals, das in derselben Version enthalten ist (der Server +-- prüft das, siehe STANDARDBROWSER_MERKMALE in app.py). Umsetzung auf dem +-- Gerät: ansible-content Rolle standardbrowser. + +ALTER TABLE workspace_versionen ADD COLUMN standardbrowser TEXT + CHECK (standardbrowser IN ('firefox', 'google-chrome', 'chromium', 'browser-brave', 'microsoft-edge')); + +COMMIT;