From 4706870774a581a3653c2e28a0af587767ccd687 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Fri, 11 Sep 2026 11:10:33 +0200 Subject: [PATCH] =?UTF-8?q?Phase=204:=20Webapp-Kategorie-Men=C3=BC-Einordn?= =?UTF-8?q?ung=20+=20Kiosk-Modus,=20Chromium+Edge-Merkmale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Webapps bekommen zwei neue, optionale Eigenschaften (Migration 0043): kategorie_id (FK auf kategorien, nullable) und kiosk_modus (Boolean). Kein eigener "Webapps"-Menüpunkt (per Nutzer-Feedback in der Plan-Runde verworfen) - stattdessen ordnet sich eine Webapp optional einer der bestehenden App-Kategorien zu, KATEGORIE_ZU_XDG_CATEGORIES übersetzt das in eine echte Freedesktop-Categories=-Zeile fürs .desktop-Menü (Default unverändert Network;WebBrowser; ohne gewählte Kategorie). Kiosk-Modus: bei aktiviertem kiosk_modus löst der Server (nicht das Gerät) anhand der Workspace-Merkmal-Zuordnung auf, welcher Browser das Kiosk-Fenster öffnet (Chrome bevorzugt, Chromium Fallback, sonst normaler Tab) - _resolve_kiosk_browser() in fetch_workspace_webapps(). Migration 0044: Chromium + Microsoft Edge als neue Browser-Merkmale (Google Chrome existiert bereits aus Migration 0031). Chromium bewusst NUR für debian-image/mint-image blueprint-verknüpft, nicht ubuntu-image - recherchiert: Ubuntus "chromium"-Paket ist seit 19.10 nur ein transitionelles Paket, das im Hintergrund Snap nachzieht (passt nicht zu unserem Modell), während Debian und Mint ein echtes .deb bauen. Microsoft Edge über das offizielle, distributionsunabhängige Microsoft-Repo auf allen drei Backends. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9 --- app.py | 157 ++++++++++++++++----- migrations/0043_webapp_kategorie_kiosk.sql | 24 ++++ migrations/0044_chromium_edge_merkmale.sql | 33 +++++ 3 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 migrations/0043_webapp_kategorie_kiosk.sql create mode 100644 migrations/0044_chromium_edge_merkmale.sql diff --git a/app.py b/app.py index c888897..44329c8 100644 --- a/app.py +++ b/app.py @@ -213,12 +213,16 @@ class CreateWebappRequest(BaseModel): name: str url: str sort_order: int = 0 + kategorie_id: str | None = None + kiosk_modus: bool = False class UpdateWebappRequest(BaseModel): name: str url: str sort_order: int = 0 + kategorie_id: str | None = None + kiosk_modus: bool = False class CreateWebappTemplateRequest(BaseModel): @@ -804,6 +808,60 @@ def fetch_favicon(page_url: str) -> tuple[bytes, str] | None: return _fetch_url_bytes(fallback_url, FAVICON_MAX_BYTES) +# Phase 4 des GUI-Ausbaus (11.09.2026): Kategorie-Key -> echter Freedesktop- +# Categories=-Wert fuers .desktop-Menue einer Webapp (siehe kategorien- +# Tabelle). Unsere eigenen Kategorie-Namen sind keine gueltigen XDG-Menue- +# Kategorien, diese Abbildung uebersetzt dazwischen. Mehrfachwerte (durch +# Semikolon getrennt, mit abschliessendem Semikolon - Pflicht im +# .desktop-Format) helfen Menues, die eine Kategorie nicht kennen, noch +# eine plausible Alternative zu finden. +KATEGORIE_ZU_XDG_CATEGORIES = { + "bildung-mathe-naturwissenschaft": "Education;Science;", + "bildung-sprache-schreiben": "Education;", + "kreativ-grafik": "Graphics;", + "musik-audio": "AudioVideo;Audio;", + "buero-produktivitaet": "Office;", + "programmieren-making": "Development;", + "internet-kommunikation": "Network;", + "unterrichtswerkzeuge": "Education;", + "individuelle-systemkonfiguration": "Settings;", + "konfig-identitaet-netzwerk": "Settings;", + "konfig-system": "Settings;", + "haertung": "Settings;", + "app-store": "Network;", +} +# Unveraenderter alter Default (keine Kategorie gewaehlt) - landet wie +# bisher meist im "Internet"-Menueordner. +WEBAPP_XDG_CATEGORIES_DEFAULT = "Network;WebBrowser;" + +# Kiosk-Modus (Phase 4): Chrome bevorzugt, Chromium Fallback - beide nur +# nutzbar, wenn das jeweilige Browser-Merkmal im Workspace aktiv ist +# (Nutzer-Entscheidung aus der Plan-Klaerungsrunde: "vermutlich Chrome, +# weil Chromium etwas abgespeckter ist"). Ohne einen der beiden faellt +# die .desktop-Vorlage auf den normalen xdg-open-Start zurueck. +KIOSK_BROWSER_PRIORITAET = ["google-chrome", "chromium"] + + +def _resolve_kiosk_browser(workspace_id: str) -> str | None: + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + """ + SELECT m.key + FROM workspace_merkmale wm + JOIN merkmale m ON m.id = wm.merkmal_id + WHERE wm.workspace_id = %s AND m.key = ANY(%s) + """, + (workspace_id, KIOSK_BROWSER_PRIORITAET), + ) + vorhandene = {row[0] for row in cur.fetchall()} + + for browser in KIOSK_BROWSER_PRIORITAET: + if browser in vorhandene: + return browser + return None + + def fetch_workspace_webapps(device_id: str): with get_database_connection() as conn: with conn.cursor() as cur: @@ -825,27 +883,38 @@ def fetch_workspace_webapps(device_id: str): cur.execute( """ - SELECT id, name, url, icon_filename - FROM workspace_webapps - WHERE workspace_id = %s - ORDER BY sort_order, name + SELECT w.id, w.name, w.url, w.icon_filename, w.kiosk_modus, k.key + FROM workspace_webapps w + LEFT JOIN kategorien k ON k.id = w.kategorie_id + WHERE w.workspace_id = %s + ORDER BY w.sort_order, w.name """, (workspace_id,), ) - return [ - { - "id": str(webapp_id), - "name": name, - "url": url, - # Relativer Pfad, kein voller URL - der Server kennt - # seine eigene oeffentliche Adresse nirgends (siehe - # ANODE_URL in tuxflotte-installer/provisioning-agent, - # bewusst nur client-seitig bekannt, gleiches Muster wie - # bei GOLDEN_IMAGE_URL). Der Agent haengt ANODE_URL davor. - "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None, - } - for webapp_id, name, url, icon_filename in cur.fetchall() - ] + zeilen = cur.fetchall() + + kiosk_browser = _resolve_kiosk_browser(workspace_id) if any(row[4] for row in zeilen) else None + + return [ + { + "id": str(webapp_id), + "name": name, + "url": url, + # Relativer Pfad, kein voller URL - der Server kennt + # seine eigene oeffentliche Adresse nirgends (siehe + # ANODE_URL in tuxflotte-installer/provisioning-agent, + # bewusst nur client-seitig bekannt, gleiches Muster wie + # bei GOLDEN_IMAGE_URL). Der Agent haengt ANODE_URL davor. + "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None, + "categories": KATEGORIE_ZU_XDG_CATEGORIES.get(kategorie_key, WEBAPP_XDG_CATEGORIES_DEFAULT), + # None, wenn kein Kiosk-Modus gewuenscht ODER kein Kiosk- + # faehiger Browser im Workspace verfuegbar ist - die + # .desktop-Vorlage faellt in beiden Faellen auf den normalen + # xdg-open-Start zurueck (siehe webapp-shortcuts-Rolle). + "kiosk_browser": kiosk_browser if kiosk_modus else None, + } + for webapp_id, name, url, icon_filename, kiosk_modus, kategorie_key in zeilen + ] def fetch_assigned_blueprints(device_id: str): @@ -2719,10 +2788,12 @@ def fetch_webapps_for_workspace(workspace_id: str): with conn.cursor() as cur: cur.execute( """ - SELECT id, name, url, icon_filename, sort_order - FROM workspace_webapps - WHERE workspace_id = %s - ORDER BY sort_order, name + SELECT w.id, w.name, w.url, w.icon_filename, w.sort_order, + w.kategorie_id, k.name, w.kiosk_modus + FROM workspace_webapps w + LEFT JOIN kategorien k ON k.id = w.kategorie_id + WHERE w.workspace_id = %s + ORDER BY w.sort_order, w.name """, (workspace_id,), ) @@ -2733,12 +2804,18 @@ def fetch_webapps_for_workspace(workspace_id: str): "url": url, "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None, "sort_order": sort_order, + "kategorie_id": str(kategorie_id) if kategorie_id else None, + "kategorie_name": kategorie_name, + "kiosk_modus": kiosk_modus, } - for webapp_id, name, url, icon_filename, sort_order in cur.fetchall() + for webapp_id, name, url, icon_filename, sort_order, kategorie_id, kategorie_name, kiosk_modus in cur.fetchall() ] -def create_webapp(workspace_id: str, name: str, url: str, sort_order: int): +def create_webapp( + workspace_id: str, name: str, url: str, sort_order: int, + kategorie_id: str | None = None, kiosk_modus: bool = False, +): webapp_id = uuid4() favicon = fetch_favicon(url) icon_filename = _write_webapp_icon(webapp_id, favicon[0], favicon[1]) if favicon else None @@ -2748,16 +2825,20 @@ def create_webapp(workspace_id: str, name: str, url: str, sort_order: int): with conn.cursor() as cur: cur.execute( """ - INSERT INTO workspace_webapps (id, workspace_id, name, url, icon_filename, icon_content_type, sort_order) - VALUES (%s, %s, %s, %s, %s, %s, %s) + INSERT INTO workspace_webapps + (id, workspace_id, name, url, icon_filename, icon_content_type, sort_order, kategorie_id, kiosk_modus) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """, - (webapp_id, workspace_id, name, url, icon_filename, icon_content_type, sort_order), + (webapp_id, workspace_id, name, url, icon_filename, icon_content_type, sort_order, kategorie_id, kiosk_modus), ) return {"id": str(webapp_id), "name": name, "url": url, "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None} -def update_webapp(webapp_id: str, name: str, url: str, sort_order: int): +def update_webapp( + webapp_id: str, name: str, url: str, sort_order: int, + kategorie_id: str | None = None, kiosk_modus: bool = False, +): # Favicon bei jeder Bearbeitung neu holen (einfacher als zu erkennen, # ob sich nur die URL geaendert hat - Fetch ist billig, siehe # fetch_favicon()) - alte Icon-Datei wird durch die neue ID-basierte @@ -2776,10 +2857,10 @@ def update_webapp(webapp_id: str, name: str, url: str, sort_order: int): """ UPDATE workspace_webapps SET name = %s, url = %s, sort_order = %s, icon_filename = %s, - icon_content_type = %s, updated_at = now() + icon_content_type = %s, kategorie_id = %s, kiosk_modus = %s, updated_at = now() WHERE id = %s """, - (name, url, sort_order, icon_filename, icon_content_type, webapp_id), + (name, url, sort_order, icon_filename, icon_content_type, kategorie_id, kiosk_modus, webapp_id), ) return {"id": webapp_id, "name": name, "url": url, "icon_path": f"/webapp-icons/{icon_filename}" if icon_filename else None} @@ -4287,7 +4368,13 @@ def create_workspace_webapp(workspace_id: str, payload: CreateWebappRequest, aut if not require_service_token(authorization): return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} - return {"success": True, "webapp": create_webapp(workspace_id, payload.name, payload.url, payload.sort_order)} + return { + "success": True, + "webapp": create_webapp( + workspace_id, payload.name, payload.url, payload.sort_order, + payload.kategorie_id, payload.kiosk_modus, + ), + } @app.put("/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}") @@ -4295,7 +4382,13 @@ def update_workspace_webapp(workspace_id: str, webapp_id: str, payload: UpdateWe if not require_service_token(authorization): return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} - return {"success": True, "webapp": update_webapp(webapp_id, payload.name, payload.url, payload.sort_order)} + return { + "success": True, + "webapp": update_webapp( + webapp_id, payload.name, payload.url, payload.sort_order, + payload.kategorie_id, payload.kiosk_modus, + ), + } @app.delete("/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}") diff --git a/migrations/0043_webapp_kategorie_kiosk.sql b/migrations/0043_webapp_kategorie_kiosk.sql new file mode 100644 index 0000000..921016e --- /dev/null +++ b/migrations/0043_webapp_kategorie_kiosk.sql @@ -0,0 +1,24 @@ +BEGIN; + +-- Phase 4 des GUI-Ausbaus (11.09.2026, siehe +-- project_kundenplattform_gui_umbau_2026-09-11_dashboard_workspace-Memory): +-- Webapps bekommen zwei neue, optionale Eigenschaften. +-- +-- kategorie_id: statt eines eigenen "Webapps"-Menüpunkts (bewusst +-- verworfen, siehe Nutzer-Feedback in der Plan-Runde) ordnet sich eine +-- Webapp optional einer der BESTEHENDEN App-Kategorien zu - die +-- .desktop-Datei bekommt dadurch die passende XDG-Categories=-Zeile +-- (Mapping in app.py) statt immer "Network;WebBrowser;" und landet im +-- Menü in derselben Kategorie-Schublade wie reguläre Apps. NULL = alter, +-- unveränderter Default (Internet/Netzwerk). +-- +-- kiosk_modus: bei true startet die Webapp im Chrome-/Chromium-Kiosk- +-- Fenster (--app=, keine Adressleiste/Tabs) statt im normalen +-- Standardbrowser - welcher der beiden Browser verwendet wird (Chrome +-- bevorzugt, Chromium Fallback, sonst normaler Tab), wird serverseitig +-- anhand der Workspace-Merkmal-Zuordnung aufgelöst (siehe +-- fetch_workspace_webapps() in app.py), nicht zur Laufzeit auf dem Gerät. +ALTER TABLE workspace_webapps ADD COLUMN kategorie_id UUID REFERENCES kategorien(id) ON DELETE SET NULL; +ALTER TABLE workspace_webapps ADD COLUMN kiosk_modus BOOLEAN NOT NULL DEFAULT FALSE; + +COMMIT; diff --git a/migrations/0044_chromium_edge_merkmale.sql b/migrations/0044_chromium_edge_merkmale.sql new file mode 100644 index 0000000..d8f8582 --- /dev/null +++ b/migrations/0044_chromium_edge_merkmale.sql @@ -0,0 +1,33 @@ +BEGIN; + +-- Phase 4 des GUI-Ausbaus (11.09.2026): zwei neue Browser-Merkmale, wie +-- vom Nutzer in der Plan-Klärungsrunde gewünscht ("wollte sowieso noch +-- Chrome, Chromium und Edge ergänzen") - Google Chrome existiert bereits +-- aus Migration 0031. +-- +-- Chromium bewusst NUR auf debian-image/mint-image blueprint-verknüpft, +-- NICHT auf ubuntu-image - siehe ausführlicher Kommentar in +-- ansible-content/roles/chromium/tasks/main.yml (Ubuntus "chromium"-Paket +-- ist seit 19.10 nur noch ein Snap-Wrapper, kein echtes .deb). +-- Microsoft Edge läuft (wie Google Chrome) über ein eigenes, distributions- +-- unabhängiges Microsoft-Repo - auf allen drei Backends verfügbar. +INSERT INTO merkmale (id, key, name, description, kategorie_id, im_auftragskatalog) VALUES + (gen_random_uuid(), 'chromium', 'Chromium', + 'Freie Basis von Google Chrome. Auf Ubuntu bewusst nicht angeboten (dortiges Paket zieht Snap nach) - dort steht Chrome zur Verfügung.', + (SELECT id FROM kategorien WHERE key = 'internet-kommunikation'), TRUE), + (gen_random_uuid(), 'microsoft-edge', 'Microsoft Edge', NULL, + (SELECT id FROM kategorien WHERE key = 'internet-kommunikation'), TRUE); + +INSERT INTO blueprints (id, merkmal_id, backend_id, ansible_role) +SELECT gen_random_uuid(), m.id, b.id, 'chromium' +FROM merkmale m +JOIN backends b ON b.key IN ('mint-image', 'debian-image') +WHERE m.key = 'chromium'; + +INSERT INTO blueprints (id, merkmal_id, backend_id, ansible_role) +SELECT gen_random_uuid(), m.id, b.id, 'microsoft-edge' +FROM merkmale m +JOIN backends b ON b.key IN ('mint-image', 'debian-image', 'ubuntu-image') +WHERE m.key = 'microsoft-edge'; + +COMMIT;