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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-13 15:10:30 +02:00
1 parent 381e0e55c1
commit 26e26e1b3b
2 files changed
+131 -2

No files matched your search

+117 -2
View File
@@ -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):