diff --git a/migrations/0011_installationsmedium_wlan_quelle.sql b/migrations/0011_installationsmedium_wlan_quelle.sql new file mode 100644 index 0000000..998d92f --- /dev/null +++ b/migrations/0011_installationsmedium_wlan_quelle.sql @@ -0,0 +1,15 @@ +BEGIN; + +-- Einstieg vereinfachen, Schritt 3 (platform-docs roadmap/einstieg- +-- vereinfachen.md, 25.10.2026): Woher das Medium sein WLAN nimmt. "org" = +-- WLAN der Organisation (provisioning-server), "eigen" = die Felder +-- wifi_ssid/wifi_psk_encrypted (z. B. ein Einrichtungs-WLAN), "kabel" = +-- keins. + +ALTER TABLE installationsmedium_konfiguration + ADD COLUMN wlan_quelle TEXT NOT NULL DEFAULT 'eigen' + CHECK (wlan_quelle IN ('org', 'eigen', 'kabel')); + +UPDATE installationsmedium_konfiguration SET wlan_quelle = 'kabel' WHERE coalesce(wifi_ssid, '') = ''; + +COMMIT; diff --git a/routers/installationsmedium.py b/routers/installationsmedium.py index 9f6b1f3..4b76cc0 100644 --- a/routers/installationsmedium.py +++ b/routers/installationsmedium.py @@ -35,7 +35,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: """ SELECT wifi_ssid, wifi_psk_encrypted, bereitstellungsvorlage_id, max_devices, validity_days, enrollment_session_id, iso_build_id, - neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id + neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle FROM installationsmedium_konfiguration WHERE organization_id = %s """, @@ -48,7 +48,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: (wifi_ssid, wifi_psk_encrypted, bereitstellungsvorlage_id, max_devices, validity_days, enrollment_session_id, iso_build_id, - neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id) = row + neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle) = row # psycopg liefert TEXT-Spalten hier teils als bytes zurueck, nicht str - # dasselbe bekannte, nicht deterministische Verhalten wie in @@ -68,6 +68,7 @@ def fetch_konfiguration(organization_id: str) -> dict | None: "neuinstallation_ohne_rueckfrage": neuinstallation_ohne_rueckfrage, "offline_medium": offline_medium, "ziel_oe_id": str(ziel_oe_id) if ziel_oe_id else None, + "wlan_quelle": wlan_quelle, } @@ -127,16 +128,17 @@ def speichere_konfiguration_basis( def speichere_medium_optionen( organization_id: str, neuinstallation_ohne_rueckfrage: bool, offline_medium: bool, ziel_oe_id: str | None = None, + wlan_quelle: str = "eigen", ) -> None: with get_database_connection() as conn: with conn.cursor() as cur: cur.execute( """ UPDATE installationsmedium_konfiguration - SET neuinstallation_ohne_rueckfrage = %s, offline_medium = %s, ziel_oe_id = %s + SET neuinstallation_ohne_rueckfrage = %s, offline_medium = %s, ziel_oe_id = %s, wlan_quelle = %s WHERE organization_id = %s """, - (neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, organization_id), + (neuinstallation_ohne_rueckfrage, offline_medium, ziel_oe_id, wlan_quelle, organization_id), ) @@ -277,7 +279,21 @@ def _installationsmedium_kontext(user) -> dict: oes_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/organisationseinheiten") organisationseinheiten = oes_result.get("organisationseinheiten", []) if oes_result.get("success") else [] + wlan_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/org-wlan") + org_wlan = wlan_result.get("org_wlan") if wlan_result.get("success") else None + + # Vorauswahl der WLAN-Quelle: wie beim letzten Medium, sonst das WLAN der + # Organisation, sonst Kabel. + if konfiguration: + wlan_quelle = konfiguration["wlan_quelle"] + if wlan_quelle == "org" and not org_wlan: + wlan_quelle = "eigen" if konfiguration["wifi_ssid"] else "kabel" + else: + wlan_quelle = "org" if org_wlan else "kabel" + return { + "org_wlan": org_wlan, + "wlan_quelle": wlan_quelle, "user": user, "konfiguration": konfiguration, "vorlagen": vorlagen, @@ -316,41 +332,64 @@ def installationsmedium_erstellen_ansicht(request: Request, user=Depends(get_cur ) +def _erstellen_fehler(meldung: str): + return RedirectResponse("/installationsmedium/erstellen?" + urlencode({"fehler": meldung}), status_code=303) + + @router.post("/installationsmedium") def installationsmedium_erstellen( + wlan_quelle: str = Form(default=""), wifi_ssid: str = Form(default=""), wifi_psk: str = Form(default=""), - bereitstellungsvorlage_id: str = Form(...), + offenes_wlan: str = Form(default=""), + bereitstellungsvorlage_id: str = Form(default=""), max_devices: int = Form(...), validity_days: int = Form(...), neuinstallation_ohne_rueckfrage: str = Form(default=""), offline_medium: str = Form(default=""), ziel_oe_id: str = Form(default=""), - offenes_wlan: str = Form(default=""), user=Depends(get_current_user), ): + """ + Einstieg vereinfachen, Schritt 3 (25.10.2026): Der Kunde wählt nur noch, + über welches WLAN installiert wird, wie viele Geräte und wie lange. Die + Installation (Distro, Plattenaufteilung) wählt der Server passend zur + Ziel-OE; ein Profil von Hand gibt es nur noch für Tuxflotte-Admins. + """ + organization_id = str(user.organization_id) ohne_rueckfrage = neuinstallation_ohne_rueckfrage == "on" offline = offline_medium == "on" wifi_ssid = wifi_ssid.strip() - if offenes_wlan == "on" or not wifi_ssid: - wifi_psk = "" + if wlan_quelle not in ("org", "eigen", "kabel"): + # Formular vor Schritt 3: WLAN-Felder leer heißt Kabel. + wlan_quelle = "eigen" if wifi_ssid else "kabel" + if not user.is_superuser: + bereitstellungsvorlage_id = "" bisher = fetch_konfiguration(organization_id) - psk_entfernen = psk_verwerfen( - offenes_wlan == "on", wifi_ssid, wifi_psk, bisher["wifi_ssid"] if bisher else None - ) - - speichere_konfiguration_basis( - organization_id, wifi_ssid, wifi_psk, bereitstellungsvorlage_id, max_devices, validity_days, - psk_entfernen=psk_entfernen, - ) - speichere_medium_optionen(organization_id, ohne_rueckfrage, offline, ziel_oe_id or None) - - # Nur fuer den ISO-Bau gebraucht, nie ueber diesen Aufruf hinaus - # gespeichert (siehe Phase-4-Design von provisioning-server) - bei - # leerem Feld wird das zuvor gespeicherte PSK weiterverwendet. - psk_fuer_build = wifi_psk or ("" if psk_entfernen else (lade_gespeichertes_psk(organization_id) or "")) + psk_entfernen = False + if wlan_quelle == "org": + wlan = anode_request("GET", f"/api/v1/organizations/{organization_id}/org-wlan", params={"mit_passwort": "true"}) + org_wlan = wlan.get("org_wlan") if wlan.get("success") else None + if not org_wlan: + return _erstellen_fehler("Für die Organisation ist kein WLAN eingetragen (Organisation → WLAN der Organisation).") + build_ssid, psk_fuer_build = org_wlan["ssid"], org_wlan.get("passwort") or "" + # Die Felder "anderes WLAN" bleiben, wie sie waren. + wifi_ssid, wifi_psk = (bisher["wifi_ssid"] or "") if bisher else "", "" + elif wlan_quelle == "eigen": + if not wifi_ssid: + return _erstellen_fehler("Bitte den Namen des WLANs eingeben oder „Netzwerkkabel“ wählen.") + if offenes_wlan == "on": + wifi_psk = "" + psk_entfernen = psk_verwerfen(offenes_wlan == "on", wifi_ssid, wifi_psk, bisher["wifi_ssid"] if bisher else None) + build_ssid = wifi_ssid + # Nur für den ISO-Bau, nie über diesen Aufruf hinaus gespeichert - + # bei leerem Feld gilt das zuvor gespeicherte PSK. + psk_fuer_build = wifi_psk or ("" if psk_entfernen else (lade_gespeichertes_psk(organization_id) or "")) + else: + build_ssid = psk_fuer_build = "" + wifi_ssid, wifi_psk = (bisher["wifi_ssid"] or "") if bisher else "", "" expires_at = (datetime.now(timezone.utc) + timedelta(days=validity_days)).isoformat() @@ -358,7 +397,7 @@ def installationsmedium_erstellen( "POST", f"/api/v1/organizations/{organization_id}/enrollment-sessions", json={ - "bereitstellungsvorlage_id": bereitstellungsvorlage_id, + "bereitstellungsvorlage_id": bereitstellungsvorlage_id or None, "max_devices": max_devices, "expires_at": expires_at, "neuinstallation_ohne_rueckfrage": ohne_rueckfrage, @@ -366,24 +405,26 @@ def installationsmedium_erstellen( }, ) - # Ziel-OE und Profil passen nicht zusammen (andere Distro) - verständlich - # zurückmelden statt Fehlerseite (Workspace-Creator neu, Schritt F). - if not session_result.get("success") and session_result.get("error") == "ziel_oe_konflikt": - return RedirectResponse( - "/installationsmedium/erstellen?" + urlencode({"fehler": session_result["message"]}), status_code=303 - ) - + # Ziel-OE passt nicht zur Installation oder es gibt keine passende - + # verständlich zurückmelden statt Fehlerseite. if not session_result.get("success"): - raise HTTPException(status_code=400, detail=session_result.get("message", "Enrollment Session konnte nicht erstellt werden.")) + return _erstellen_fehler(session_result.get("message", "Das Medium konnte nicht angelegt werden.")) session_id = session_result["enrollment_session"]["id"] + vorlage_id = session_result["enrollment_session"].get("bereitstellungsvorlage_id") or bereitstellungsvorlage_id + + speichere_konfiguration_basis( + organization_id, wifi_ssid, wifi_psk, vorlage_id, max_devices, validity_days, + psk_entfernen=psk_entfernen, + ) + speichere_medium_optionen(organization_id, ohne_rueckfrage, offline, ziel_oe_id or None, wlan_quelle) build_result = anode_request( "POST", f"/api/v1/organizations/{organization_id}/iso-builds", json={ "activation_code": session_id, - "wifi_ssid": wifi_ssid, + "wifi_ssid": build_ssid, "wifi_psk": psk_fuer_build, # Diagnosezugang (02.10.2026): nur der Hash, und nur wenn der # Root-Admin ihn fuer diese Organisation eingeschaltet hat. diff --git a/templates/base.html b/templates/base.html index 5c95d98..fd49201 100644 --- a/templates/base.html +++ b/templates/base.html @@ -126,6 +126,8 @@ form.inline { display: inline; margin: 0; } form.inline button { width: auto; } button.klein, [role=button].klein { padding: 0.2rem 0.6rem; font-size: 0.85rem; width: auto; margin: 0; } + /* Felder, die zu einer Auswahl darüber gehören (Installationsmedium: anderes WLAN). */ + .einrueckung { margin-left: 1.75rem; } /* Merkmal-Seiten (15.10.2026, Nutzer-Wunsch): Aktiv/Inaktiv/ Zurücksetzen in den Kategorie-Tabellen so kompakt wie .klein. */ details.merkmal-kategorie button { padding: 0.2rem 0.6rem; font-size: 0.85rem; width: auto; margin: 0 0.25rem 0.25rem 0; } diff --git a/templates/installationsmedium.html b/templates/installationsmedium.html index 9ea74f3..93a6768 100644 --- a/templates/installationsmedium.html +++ b/templates/installationsmedium.html @@ -141,75 +141,87 @@
{% endif %}