Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware, es gibt also
zwei Abzuege je Ziel. Die Station haelt jetzt beide vor - images/
migrate-nocreds (aus tools/build_nocreds.sh, Lampe startet im
Onboarding-AP) und images/migrate-wlan (aus build/, Lampe haengt sofort
im eigenen WLAN) - und der Bediener waehlt per Umschalter, welcher
ausgeliefert wird: "ninlight-station bildsatz" oder der Menueeintrag am
Taster.
Der Zustand liegt in /var/lib/ninlight-station/imageset, ueberlebt
Neustarts und steht dauerhaft in der Fusszeile des Panels - in jedem
Modus, auch bei ausgeschalteter Station: man soll vor dem Einschalten
sehen, was gilt.
Vorgabe und Rueckfallwert in JEDEM Zweifelsfall ist "ohne-wlan". Im
Automatikmodus wird jede Klyqa-Lampe in Reichweite migriert, auch
fremde; ein Abzug ohne Zugangsdaten ist dann ein harmloser Fehlgriff,
einer mit Zugangsdaten traegt das eigene WLAN-Passwort in ein fremdes
Geraet.
Fehlt der gewaehlte Satz (Verzeichnis weg oder leer), wird NICHTS
ausgeliefert - kein stilles Ausweichen auf den anderen Satz. Der Server
antwortet 404 mit Status "noimageset", CLI und Panel sagen "FEHLT".
server.py und die Stations-Unit muessen mit: die Auswahl der
ausgelieferten Datei ist genau die Aufgabe des Servers, und ein
Symlink-Tausch haette "gewaehlt, aber nicht vorhanden" von "dieses
Modell fehlt" nicht mehr unterscheidbar gemacht.
Fixrunde 1 zu Task 4:
- C1 (kritisch): die Unit leerte /run/ninlight-station bei jedem
systemctl restart, weil RuntimeDirectoryPreserve fehlte - genau der
Neustart, mit dem migrate/recovery on den Dienst hochfahren, wischte
die eben geschriebene Modus-/Steuerdatei wieder weg, und der Server
fiel still auf den Recovery-Default zurueck. Unit bekommt
RuntimeDirectoryPreserve=restart, und start_and_verify() prueft nach
dem Neustart zusaetzlich ausdruecklich nach, dass Modus und Phase noch
stimmen.
- I1: status las die Phase aus status.json, die arm/listen (die den
Dienst bewusst nicht neu starten) nie aktualisieren - jetzt kommt sie
aus control.json.
- I2: off pruefte nach dem Herunterfahren nichts nach und konnte
"Station aus" melden, waehrend ein Profil wegen haengendem
NetworkManager weiterhin aktiv war - ap_switch() verifiziert das jetzt
auch im Aus-Fall.
- I3: der Fehlerzweig von ap_switch (Mismatch nach dem Umschalten) war
im bisherigen Test nie erreichbar; der nmcli-Stub kann das jetzt
erzwingen.
- I4: neue Pflichtschluessel blockierten jeden Unterbefehl auf einem
bereits eingerichteten Pi. setup_pi.sh ergaenzt fehlende Schluessel in
einer bestehenden Konfiguration jetzt einzeln, statt sie unangetastet
zu lassen; dafuer musste das Skript per bash-s-Heredocs und
ueberschreibbaren Pfadvariablen erst testbar gemacht werden (neuer
test_setup_pi.sh).
Dazu vier kleinere Punkte aus derselben Review: grep -Fqx statt grep -qx
fuer Profilnamen, install-Stub durch tee+chmod ersetzt (POSIX, kein
Sonderfall mehr fuer macOS), Rechtepruefung auf control.json in den
arm/listen-Testfaellen, stderr sauber von stdout getrennt in den
Warnungs-/Fehlerfaellen.
ninlight-station bekommt recovery/migrate/arm/listen als Unterbefehle statt nur
on/off/status; "on" bleibt Kurzform fuer "recovery on". Eine gemeinsame
ap_switch()-Funktion faehrt vor jedem Wechsel erst jedes andere Stationsprofil
herunter und prueft per nmcli nach, dass auf wlan0 wirklich nur eines aktiv
ist - die beiden Zugangspunkte (Rettung/Migration) schliessen sich damit
gegenseitig aus. Der Modus wandert per /run/ninlight-station/mode an den
(ohnehin neu gestarteten) Dienst statt ueber die Konfigurationsdatei; arm/listen
schreiben die Steuerdatei aus Task 3 mit demselben atomaren 0644-Muster wie
server.py selbst. station.conf.example und die Pflichtschluesselpruefung
bekommen die neuen Migrations- und Anzeige-Schluessel dazu.
- 'on' fing bisher nur einen bereits laufenden, aber toten Dienst ab. Scheiterte
schon der Aufruf "systemctl start" selbst (z.B. start-limit-hit nach einer
Absturzserie durch Restart=always), beendete set -eu das Skript dort, bevor
die Lebendigkeitspruefung je erreicht wurde - der eben hochgefahrene
Zugangspunkt blieb offen ohne Server dahinter stehen. Der Startaufruf wird
jetzt selbst abgefangen und faehrt im Fehlerfall dieselbe Rueckabwicklung wie
die is-active-Pruefung. "systemctl reset-failed" vor dem Start verhindert,
dass eine fruehere Absturzserie den naechsten ehrlichen Versuch blockiert.
- Stirbt der Dienst nach einem erfolgreichen Start, stoppt das Skript ihn jetzt
explizit, bevor es den Zugangspunkt herunterfaehrt und "abgeschaltet" meldet -
sonst haette ein mit Restart=always weiterlaufender Dienst im Hintergrund
einen saubereren Zustand suggeriert, als tatsaechlich vorlag.
- ninlight-station.service setzt StartLimitIntervalSec/StartLimitBurst jetzt
explizit statt sich stillschweigend auf systemds Vorgabewerte zu verlassen,
mit Begruendung als Kommentar: ohne Zugangspunkt kann server.py nicht binden
und stirbt sofort, Restart=always soll das nach kurzer Zeit ehrlich aufgeben
statt das Journal zu fluten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 'on' prueft nach dem Start wirklich, ob der Dienst aktiv ist (Type=simple
meldet sonst schon Erfolg, wenn der Prozess nur gestartet wurde), und faehrt
den Zugangspunkt bei einem toten Server wieder herunter statt ihn offen mit
bekanntem Passwort ohne Server dahinter stehen zu lassen.
- 'off' faengt einen fehlschlagenden Wechsel zurueck ins Heim-WLAN ab statt
unter set -eu mit nmclis Rohausgabe abzubrechen.
- Fehlende Schluessel in der Konfiguration werden nach dem Sourcen explizit
geprueft und mit einem Verweis auf setup_pi.sh gemeldet statt mit
"parameter not set" abzubrechen.
- station.conf.example verwendet einfache statt doppelter Anfuehrungszeichen:
POSIX ". datei" wertet $ und ` auch in doppelten Anfuehrungszeichen aus,
ein Passwort mit "$1" waere sonst lautlos durch ein Shell-Argument ersetzt
worden. systemds EnvironmentFile= fuehrt ohnehin nie eine Shell aus und
entfernt einfache wie doppelte Anfuehrungszeichen gleichermassen.
- ninlight-station.service: BindsTo=NetworkManager.service entfernt (ein
dadurch ausgeloester Stop gilt fuer systemd als sauber und wird von keiner
Restart=-Politik erneut gestartet - ein NetworkManager-Neustart haette den
Server sonst still und dauerhaft mitgerissen); stattdessen nur
After=NetworkManager.service fuer die Startreihenfolge und Restart=always,
damit der Prozess nach einem Absturz (z.B. Bind-Fehler) automatisch
wiederkommt.
- Trailer-Reader im 'status'-Zweig nutzt jetzt errors="replace" wie
trailer_version() in server.py, statt bei nicht-ASCII-Bytes im
Versionsfeld abzustuerzen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Station ist bewusst nicht dauerhaft an - 'ninlight-station on'
schaltet Zugangspunkt und Dienst gemeinsam ein, 'off' schaltet wlan0
zurueck ins Heim-WLAN. Der Dienst laeuft als root (Port 80 und die
DHCP-Leases fuer 4c) und bekommt dafuer systemd-Einschraenkungen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>