1. Generischer Ja/Nein-Dialog bei ungespeicherten Aenderungen (kein Opt-in pro Seite noetig): initUnsavedChangesGuard() erkennt automatisch jedes Formular mit echten Eingabefeldern, das seit dem Laden der Seite veraendert wurde, und fragt beim Klick auf einen Link (z.B. Sidebar- Navigation), ob zuerst gespeichert werden soll -- eigener Dialog (confirmSaveDiscard) statt des bestehenden confirmAction (dort ist "Abbrechen" = Bleiben, hier gibt es bewusst nur die zwei angefragten Optionen: speichern oder verwerfen, beide verlassen die Seite). "Ja, speichern" sendet das Formular ganz normal ab (echter POST) und merkt sich das eigentliche Ziel in sessionStorage, um danach automatisch dorthin weiterzuleiten. Nachtrag noch in dieser Session gefixt: ein normales Speichern ueber den Seiten-eigenen Button loeste zusaetzlich die native Browser-Warnung (beforeunload) aus, obwohl die Aenderung bereits gespeichert wurde -- navigatingAway-Flag wird jetzt bei jedem Submit eines beobachteten Formulars (eigener Button ODER eigener Ja/Nein-Dialog) gesetzt, die generische Browser-Warnung erscheint dadurch nur noch bei tatsaechlich ungefragter Navigation (Tab schliessen, Reload, Adresszeile). Live per Playwright in drei Szenarien verifiziert (direktes Speichern, Ja via eigenem Dialog, Nein via eigenem Dialog) -- kein Browser-Popup in den ersten beiden Faellen, korrektes Redirect zum urspruenglichen Ziel nach "Ja". 2. DHCP-Options-Dropdown (Reservierungs-Dialog) verschiebt ein neu ausgewaehltes Options-Feld jetzt direkt unter das Dropdown (in einen .poe-added-options-Container) statt es nur an seiner urspruenglichen Stelle in der bis zu 57 Eintraege langen Liste einzublenden -- lag dort oft ausserhalb des sichtbaren Modal-Ausschnitts und wirkte, als waere nichts passiert. Bereits gesetzte Overrides stehen von Anfang an im selben Container, weitere haengen sich beim Auswaehlen unten an. 3. Cache-Busting fuer app.js/style.css (asset_url()-Helper, haengt die Aenderungszeit der Datei als ?v=... an) -- ein im Browser gecachtes, veraltetes app.js konnte nach einem Update sonst den Eindruck erwecken, eine neue Funktion wuerde "nicht funktionieren", obwohl der Code auf dem Server laengst aktuell war. Alle drei live auf der Test-VM deployt und per Playwright verifiziert, keine Fehler im journalctl-Log. (Testartefakt aufgeraeumt: der echte Host-Hostname wurde durch die Playwright-Tests versehentlich auf "directsave-test"/"navsave-test" geaendert und wieder auf POETEST zurueckgesetzt.)
PoE Manager — Modern UI Rebuild
Webbasierte Verwaltung und Monitoring von PoE-Devices und Aruba-Switchen — funktionales Rebuild von Aruba-PoE mit komplett neuem, modernem Frontend und einem granularen Rechtesystem.
Das Projekt ist ausschließlich für Linux ausgelegt (Zielsystem: eine Linux-VM). Entwickelt wird unter WSL2 (Ubuntu) unter Windows — siehe Entwicklung unter WSL weiter unten.
Die App ermöglicht:
- Öffentliches Kachel-Dashboard ohne Login — zeigt nur aktive (nicht deaktivierte) Geräte, nur Online/Offline/Gesamt-Statistik; volle Verwaltung, Aktivieren/Deaktivieren und PoE-Neustart bleiben hinter dem Login. Eingeloggt sind zusätzlich deaktivierte Geräte sichtbar (eigene Kachel-Sektion). Alle Kacheln sind durchsuchbar und in drei Abschnitten sortiert: Offline, Online, Deaktiviert — jeweils alphabetisch
- Verwaltung von Devices und Switches
- Zugangsdaten als eigene, wiederverwendbare Entität: SSH-Logins werden einmal angelegt und können mehreren Switchen zugeordnet werden; beim Anlegen eines Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen, ohne vorher zu „Zugangsdaten“ wechseln zu müssen
- Browser-SSH-Terminal beim Anlegen/Bearbeiten eines Switches, um den Host-Key zu bestätigen und die Zugangsdaten direkt zu prüfen, bevor der Switch gespeichert wird — funktioniert auf jedem Betriebssystem, die SSH-Verbindung wird dabei immer vom PoE-Server aus aufgebaut
- Manueller PoE-Neustart einzelner Geräte per Klick; bei deaktivierten Geräten bietet das Detail-Popup stattdessen direkt eine Aktivieren-Option
- Einstellbares Prüfintervall (steuert den Hintergrund-Check-Dienst) — der Countdown „Nächste Prüfung“ wird aus dem tatsächlichen letzten Log-Eintrag berechnet, nicht bei jedem Seitenaufruf neu gestartet, und läuft global in der Topbar auf jeder Seite (nicht nur im Dashboard)
- Live-Log-Ansicht mit farblicher Hervorhebung (online/offline/Neustart); Dashboard und Live-Log aktualisieren sich automatisch im Hintergrund (per AJAX, ohne vollen Seiten-Reload). Ein „Jetzt prüfen"-Icon neben dem Timer (nur Admins) startet manuell einen sofortigen Prüf-Durchlauf — der Countdown setzt sich dabei auf den manuell ausgelösten Zeitpunkt zurück
- Einheitliches Seiten-Layout: die Topbar zeigt rechts ausschließlich den „Nächste Prüfung“-Timer; Anlegen-Buttons und eine kurze Beschreibung sitzen stattdessen direkt über der jeweiligen Tabelle. Die Listen (Geräte, Switche, Zugangsdaten, Benutzer, Gruppen) sind per Klick auf die Spaltenüberschrift sortierbar
- Rechtesystem mit Gruppen: Admins dürfen immer alles (als Gruppe „Admin“ überall mit aufgeführt); normalen Benutzern lassen sich über Gruppen einzelne Verwaltungsrechte gezielt zuweisen, inkl. reiner Ansichtsrechte (siehe Rechtesystem)
- Einklappbare Sidebar (Desktop: dauerhaft, über Neuladen hinweg gemerkt; mobil: temporäres Überlagern)
- Änderungslog: jede Anlage/Bearbeitung/Löschung sowie Aktivieren/ Deaktivieren von Geräten, Switchen, Zugangsdaten, Benutzern und Gruppen wird protokolliert (wer, wann, was) — einsehbar unter „Logs → Änderungslog“; Geräte und Switche merken sich zusätzlich direkt am Datensatz, wer sie zuletzt geändert hat. PoE-Neustarts werden bewusst nicht geloggt (siehe Änderungslog)
- Verschlüsselter Import/Export: Geräte, Switche und Zugangsdaten lassen sich unter „Einstellungen → Im-/Export" als ein mit selbstgewählter Passphrase verschlüsseltes Bundle exportieren und auf einer neuen Umgebung wieder importieren (siehe Import/Export)
- Eigenes Konto: das Zahnrad-Symbol neben dem eigenen Namen unten in der Sidebar führt zu „Mein Konto“ — Vor-/Nachname, eigenes Passwort und Profilbild pflegen (siehe Eigenes Konto)
- Anpassbare Navigation: Admins legen dort zusätzlich die Reihenfolge der Sidebar-Hauptpunkte per Auf-/Ab-Buttons fest; jeder Benutzer sieht davon ohnehin nur, wofür er berechtigt ist — auch pro Unterpunkt innerhalb einer Gruppe (siehe Navigation anpassen)
Was ist neu gegenüber dem Original?
- Komplett neues Frontend: eigenes Design-System (kein Bootstrap mehr),
Logo mittig in der Topbar (auf jeder Seite, skaliert auf die Balkenhöhe),
Sidebar-Navigation (nur für eingeloggte Nutzer sichtbar), Dark/Light-
Umschalter, Karten-Dashboard mit Live-Zählern, moderne Modals/Toasts statt
Browser-
confirm()und Bootstrap-Alerts. - Dashboard jetzt teilweise öffentlich:
/ist ohne Login erreichbar und zeigt eine reduzierte Lese-Ansicht (Hostname, IP, Status, letzte Prüfung) mit Login-Button; alle Verwaltungsseiten und der PoE-Neustart bleiben@login_requiredund zusätzlich rechtebasiert (s.u.). - Browser-SSH-Terminal für Switche, implementiert mit paramiko (reines
Python) statt eines lokalen pty + System-
ssh-Client — läuft dadurch plattformunabhängig. Die SSH-Verbindung zum Switch wird immer serverseitig (vom PoE-Server) aufgebaut, nie vom Browser/Client aus. - Granulares Rechtesystem mit Gruppen (neu, siehe unten) statt der ursprünglichen reinen Admin/User-Unterscheidung.
- Gleiche Kernlogik: Datenbankschema (erweitert um Gruppen/Rechte),
Login, PoE-Restart-Mechanismus (
poe.shper SSH/expectauf die Aruba-Switche) sind ansonsten unverändert. - Aufgeräumtes Backend: konsistente, konfigurierbare Pfade (
POE_DB_PATH,POE_SCRIPT,POE_LOG_GLOB, …) statt hart codierter/inkonsistenter Pfade; Secret-Key und Fernet-Key werden beim ersten Start automatisch generiert statt im Repo zu liegen.
⚠️ Sicherheitshinweis zum Original-Repo: Im Original sind
sqlite.db(Nutzer-Passwort-Hashes, verschlüsselte Switch-Passwörter) undfernet.key(der zugehörige Schlüssel) mit im Git-Repo eingecheckt — die.gitignoredort schließt sie über die!-Regeln versehentlich wieder ein. In diesem Rebuild sind beide Dateien in.gitignoreausgeschlossen und werden beim ersten Start frisch erzeugt. Empfehlung fürs Original-Repo: beide Dateien aus der Git-Historie entfernen und Fernet-Key sowie alle Switch-/User- Passwörter rotieren.
Rechtesystem (Gruppen & Berechtigungen)
Admins dürfen immer alles. Zusätzlich gibt es Gruppen mit einem vollständig granularen, an der Navbar gespiegelten Rechtebaum — jeder Bereich der App (nicht nur Devices/Switches) lässt sich einzeln freischalten. Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich (Vereinigung, nicht Schnittmenge).
Zweistufiger Rechtebaum
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen
(Geräte/Logs/Einstellungen). Auf der Gruppen-Seite stehen sie als drei
kompakte Spalten nebeneinander, mit einer kurzen Legende direkt darüber.
Jede Spalte ist eine kleine Tabelle: die Kopfzeile enthält in EINER Zeile
sowohl den Bereichsnamen mit seinem Kästchen (dem Kill-Switch, dicht
nebeneinander statt an den Rand gestellt) als auch die Buchstaben-Spalten
R(ead/Lesen), W(rite/Schreiben, Anlegen), E(dit/Ändern) — nur
die Rechte, die dieser Bereich tatsächlich nutzt. Darunter je eine
eingerückte Zeile pro Unterpunkt. „Ändern“ deckt Löschen überall mit
ab — es gibt bewusst kein eigenes Löschen-Recht. Eine vierte Spalte D
gibt es nur bei Geräte → Clients, und sie bedeutet dort nicht Löschen,
sondern PoE-Neustart über das Dashboard-Popup auslösen (automatische
Neustarts bei Ausfall über poe.sh sind davon komplett unabhängig und
laufen immer). Für Zeilen ohne passendes Recht (z.B. „D“ bei Switche/
Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht anklickbare
Checkbox angezeigt statt die Zelle wegzulassen:
☑ Geräte R W E D ☑ Logs R W E ☑ Einstellungen R W E
Clients ☑ ☑ ☑ ☑ Live ☑ ☑ ☑ Benutzer ☑ ☑ ☑
Switche ☑ ☑ ☑ ▢ Änd. ☑ ☑ ☑ Gruppen ☑ ☑ ☑
Zugangsd. ☑ ☑ ☑ ▢ Systemeinst. ☑ ▢ ☑
Im-/Export ☑ ▢ ☑
Das jeweilige „Bereich sichtbar“-Recht (devices_group.view /
logs_group.view / settings_group.view, das Kästchen in der Kopfzeile
einer Spalte) wirkt als Kill-Switch: ist es für eine Gruppe nicht
gesetzt, greift kein einziges Recht darunter mehr — selbst wenn z.B.
devices.view einzeln noch angehakt ist. So lässt sich ein ganzer Bereich
mit einem Klick sperren, ohne jedes Unterrecht einzeln zurücknehmen zu
müssen (User.has_permission() in app.py). Auf der Gruppen-Seite sind
die Unterpunkt-Zeilen einer Spalte per JavaScript so lange gesperrt UND
sichtbar ausgegraut (nicht nur das dezente native disabled-Styling), bis
das Kopfzeilen-Kästchen angehakt ist.
Systemgruppen
Zwei Gruppen sind fest und weder umbenennbar noch in ihren Rechten änderbar (auch nicht durch Admins über die UI) — Mitgliedschaft bleibt bei beiden frei verwaltbar:
- Admin — virtuell (kein echter
groups-Datensatz), intern über denis_admin-Schalter der Benutzer gesteuert, immer alle Rechte. Mindestens ein Admin muss bestehen bleiben (serverseitig erzwungen). - Benutzer — die Standardgruppe (
is_system-Flag), der jeder neu angelegte Nicht-Admin automatisch zugeordnet wird. Fester Rechtesatz: Geräte-Bereich + Clients/Switche lesen, Logs-Bereich + Live-Log lesen.
Alle anderen Gruppen sind vom jeweiligen Rechteinhaber (groups.edit, das
auch Anlegen und Löschen von Gruppen umfasst) frei konfigurierbar.
Eskalationsschutz
Da jetzt auch Benutzer- und Gruppenverwaltung delegierbar sind (z.B.
eine Gruppe mit nur users.edit, ohne Admin zu sein), gelten zusätzlich zu
den granularen Rechten diese fest einprogrammierten Schranken, unabhängig
davon, was eine Gruppe an Rechten hat:
- Ein Benutzer als Admin anlegen/dazu machen (
group_id=adminbei Anlegen/Zuweisen) bleibt echten Admins vorbehalten. - Ein bestehendes Admin-Konto bearbeiten, löschen oder ihm die Gruppe ändern bleibt echten Admins vorbehalten.
- Wer Admin ist, wird ausschließlich über „Admin-Mitglieder verwalten“ auf
der Gruppen-Seite gesteuert — bleibt admin-exklusiv, unabhängig von
groups.edit.
Sowohl das Anzeigen jeder Seite als auch jede einzelne Aktion (Buttons, Formulare) ist an das jeweilige Recht gekoppelt — im Frontend ausgeblendet und im Backend serverseitig durchgesetzt, unabhängig vom Frontend.
Datenmodell: groups (inkl. is_default- und is_system-Flag),
group_permissions (Gruppe → Recht), user_groups (Benutzer → Gruppe).
Bestehende Datenbanken werden beim App-Start automatisch migriert
(_ensure_schema() in app.py): devices.toggle → devices.edit,
switches.* → gespiegelte credentials.*, nachträgliches Setzen der neuen
Bereichs-Rechte für bereits vergebene Unterrechte, sowie (spätere Revision)
sämtliche *.delete-Rechte → *.edit (Löschen wurde nachträglich überall in
Ändern gefaltet). Jede dieser Migrationen läuft nur einmalig über einen
eigenen Guard in der settings-Tabelle, damit z.B. ein bewusst deaktiviertes
Bereichs-Recht nicht bei jedem Neustart automatisch wieder gesetzt wird.
Zugangsdaten (wiederverwendbare SSH-Logins)
Switch-Zugangsdaten (Username + verschlüsseltes Passwort) sind eine eigene
Entität (credentials-Tabelle) statt direkt am Switch zu hängen — mehrere
Switche mit demselben Login teilen sich einen Eintrag. Beim Anlegen/Bearbeiten
eines Switches lässt sich per Dropdown ein bestehender Eintrag auswählen oder
über „+ Neue Zugangsdaten anlegen“ direkt im selben Formular ein neuer
anlegen, ohne vorher zur eigenen Zugangsdaten-Seite wechseln zu müssen.
Löschen ist nur möglich, wenn kein Switch mehr auf den Eintrag verweist.
Bestehende Datenbanken aus einer älteren Version (Username/Passwort direkt
am Switch) werden beim ersten Start automatisch migriert.
Jeder Switch kann außerdem einen individuellen SSH-Port hinterlegen
(Feld „SSH-Port“ beim Anlegen/Bearbeiten) — bleibt er leer, wird überall
automatisch Port 22 angenommen (SWITCH_DEFAULT_SSH_PORT in app.py).
Der Port wird konsistent an allen drei Stellen verwendet, an denen die App
sich per SSH mit einem Switch verbindet: dem Web-Terminal (Verbindungstest),
generate_ips.py (liefert ihn als eigenes Feld an poe.sh) und den
expect-Aufrufen in poe.sh selbst (ssh -p <port> ...) für automatische
wie manuelle PoE-Neustarts.
DHCP-Reservierungen & eigene Options (Systemeinstellungen → DHCP)
Erzeugt aus den vorhandenen Client-Stammdaten (MAC + IP + Name) automatisch
feste DHCP-Reservierungen inkl. eigener/herstellerspezifischer Options, ohne
die Daten ein zweites Mal pflegen zu müssen. Eigenes Rechtepaar
settings_dhcp.view/settings_dhcp.edit, gespiegelt in der Navbar unter
Einstellungen.
Backend: Kea DHCP (kea-dhcp4-server), nicht das ältere
isc-dhcp-server — ISC hat isc-dhcp-server 2022 als EOL erklärt, Kea ist
der aktiv weiterentwickelte Nachfolger und bildet „globaler Wert, pro Client
überschreibbar“ nativ ab (siehe unten).
- Status-Erkennung (rein lesend):
shutil.which("kea-dhcp4")+systemctl is-active/is-enabled. Zeigt Version, Lauf- und Autostart-Status. - Installation & Dienststeuerung: eigene, jeweils einzeln bestätigte
Buttons — „kea-dhcp4-server installieren“ (
apt-get install -y), „Aktivieren & (neu) starten“ (systemctl enable+restart) und „Stoppen“ (systemctl stop+disable, damit ein bewusst gestoppter Dienst nach einem Host-Neustart nicht trotzdem automatisch wieder hochfährt — ein Host-Neustart übernimmt so genau den zuletzt bewusst gewählten Zustand, an oder aus). Bewusst nicht automatisch an „Konfiguration speichern“ oder „In Datei schreiben“ gekoppelt — ein unerwartet aktiver oder fehlkonfigurierter DHCP-Server kann ein ganzes Netzsegment stören, jede dieser Aktionen soll ein bewusster, separater Klick bleiben. - Mehrere Subnetze gleichzeitig: ein Host kann mehrere IPs/Interfaces
mit jeweils eigenem Netz haben, für die alle DHCP angeboten werden soll
(
dhcp_subnets-Tabelle statt einer einzelnen globalen Range) — jedes Subnetz hat eigene Interface/Range sowie optional eigenes Gateway/DNS, generiert wird ein eigenersubnet4-Block je Subnetz in der Kea-Config. Domain, Lease-Zeiten und Ausgabepfad bleiben global (Kea-weit gültig). - Netzwerk-Kopplung: Subnet und Netzmaske werden nie manuell
eingegeben, sondern bei jeder Anzeige/Generierung live aus der
System-Netzwerkkonfiguration des jeweiligen Interfaces gelesen
(
_detect_interface_network()/_detect_interface_networks():ip -4 addr show dev <iface>+ip route show default) — berücksichtigt dabei ALLE IPv4-Adressen eines Interfaces, nicht nur die erste (ein Interface kann mehrere IPs/Subnetze gleichzeitig tragen, z.B. Alias-IPs). Die generierte Kea-Konfiguration ist dadurch immer an das Netz gekoppelt, in dem der Host tatsächlich hängt, auch wenn sich dessen IP/Subnet mal ändert. - Range ist Pflicht und muss zu einem echten Netz passen: kein
vorausgefüllter Default für Range/DNS (nur Platzhaltertext) — ein
Subnetz ohne zu einem tatsächlich vorhandenen Netz passende Range wird
beim Anlegen/Bearbeiten abgelehnt (
_dhcp_matching_network()), da Kea ohne eine echte, physisch vorhandene IP im gewünschten Bereich keine gültige Subnet-CIDR zugeordnet werden kann. Dieselbe Prüfung läuft zusätzlich bei „In Datei schreiben“ (Subnetze, die inzwischen zu keinem echten Netz mehr passen — z.B. nach einem Netzwechsel des Hosts — werden übersprungen und in der Erfolgsmeldung benannt, statt die ganze Aktion abzubrechen) und bei „Aktivieren & (neu) starten“ (verweigert den Start, falls kein einziges gültiges Subnetz mehr existiert). Live auf einer echten Fehlkonfiguration reproduziert und verifiziert. - Subnetze einzeln deaktivierbar: ein Subnetz lässt sich ohne Löschen
pausieren (
enabled-Flag) — wird dann bei „In Datei schreiben“ und „Aktivieren & (neu) starten“ komplett ignoriert (kein Fehler, bewusst kein "ungültig", da das Fehlen des Netzes hier Absicht ist), statt es jedes Mal neu anlegen zu müssen. - DHCP-Options (
dhcp_option_defs/dhcp_option_values): eine kuratierte Auswahl an 57 Standard-Optionen (analog den „Vordefinierten Optionen“ eines Windows-DHCP-Servers — u.a. Hostname erzwingen, NTP-/WINS-/TFTP-/ SMTP-Server, Boot-Datei, Domain-Search, IP-Forwarding) ist automatisch vorbefüllt und nicht löschbar (Backend lehnt es ab, UI zeigt statt eines Löschen-Buttons ein Schloss-Symbol) — Router/DNS-Server/Domain-Name/ Lease-Zeiten sind bewusst ausgenommen, da dafür bereits eigene, dedizierte Felder existieren (Subnetze bzw. globale Einstellungen oben); ebenso client-/protokollinterne Optionen (message-type, parameter-request-list, requested-address, ...), die serverseitig nicht sinnvoll setzbar sind. Alle 57 Namen sind live gegen eine echte Kea-2.4.1-Instanz verifiziert (kea-dhcp4 -t, jede Option einzeln getestet — u.a. „host-name“ mit Bindestrich statt „hostname“ als Kea-Name richtiggestellt). Standard- Optionen bekommen bewusst kein eigenesoption-defin der generierten Konfiguration — Kea kennt sie bereits nativ, eine Neudefinition würde die eingebaute duplizieren; nur zusätzlich angelegte, wirklich eigene/ herstellerspezifische Options (z.B. eine Terminal-Boot-URL) bekommen eines und bleiben frei löschbar. In der Tabelle sind grundsätzlich nur tatsächlich genutzte Options sichtbar (eigene immer, Standard-Optionen nur mit gesetztem globalen Wert) — weitere Standard-Optionen kommen über ein Dropdown „+ Standard-Option hinzufügen“ dazu, statt alle 57 dauerhaft als leere Zeilen anzuzeigen. Jede Option hat einen globalen Wert sowie optional einen Wert pro Client — ein Client-Override überschreibt den globalen Wert ausschließlich für dieses eine Gerät (technisch: globale Werte landen im Top-Leveloption-data, Overrides imoption-datader jeweiligen Kea-Reservierung, was Kea automatisch pro Client Vorrang geben lässt). Im Reservierungs-Options-Dialog gilt dasselbe Prinzip: nur Options mit gesetztem Override sind sichtbar, weitere kommen über ein Dropdown dazu. Das ausgewählte Feld wird dabei direkt unter das Dropdown verschoben und fokussiert, statt an seiner ursprünglichen Stelle irgendwo in der u.U. langen Liste (bis zu 57 mögliche Options) zu erscheinen — sonst lag es oft außerhalb des sichtbaren Modal-Ausschnitts und wirkte, als wäre nichts passiert (live genau so reproduziert und behoben). - Reservierungen aus zwei Quellen: automatisch für jedes aktive Gerät mit
gültiger MAC + IP (Client-Stammdaten dieser App) sowie zusätzlich manuell
anlegbar („Reservierung hinzufügen“, MAC/IP/Name) — für Geräte, die
nicht als Client gepflegt werden (z.B. ein externes Gerät), sodass ihnen
bewusst und mit voller Kontrolle eine feste IP gegeben werden kann, statt
sie unüberwacht automatisch anzubinden. Automatische Reservierungen können
in dieser Tabelle nicht gelöscht werden (Schloss-Symbol statt Button —
sie entstehen/verschwinden mit den Client-Stammdaten), manuelle schon. Bei
einer MAC in beiden Quellen gewinnt immer die automatische. Jede
Reservierung wird ins passende Subnetz einsortiert (die IP entscheidet,
zu welchem der ggf. mehreren konfigurierten Subnetze sie gehört); der
Hostname wird aus dem Namen abgeleitet (nur
a-z0-9-, Kollisionen innerhalb desselben Subnetzes bekommen automatisch ein-2/-3-Suffix). Einträge ohne MAC/IP oder mit einer IP außerhalb aller konfigurierten Subnetze werden mit Hinweis übersprungen — Kea lehnt eine Reservierung außerhalb ihres Subnets als Konfigurationsfehler ab (mit einer echten Kea-Instanz verifiziert, inkl. einer Mischung aus automatischer und manueller Reservierung im selben Subnetz). - Aktive Leases: eigene Karte, liest die tatsächlich vergebenen Adressen
direkt aus Keas Lease-Datenbank (
kea-leases4.csv, memfile-Backend — kein Control-Agent nötig, reiner Lesezugriff auf die Datei). Zeigt damit auch Clients ohne eigene Reservierung, die sich einfach eine freie IP aus dem Pool genommen haben — deutlich als „Ohne Reservierung“ markiert, statt nur die (unvollständige) Reservierungsliste zu zeigen. Die Ablaufzeit wird explizit als UTC ausgewiesen (Kea arbeitet intern in UTC-Epoch) und zusätzlich als live tickender Countdown angezeigt — eine reine Absolutzeit auf einer nicht selbst aktualisierenden Seite wirkte bei den kurzen Standard-Lease-Zeiten (600s) schnell „schon abgelaufen“, sobald etwas Zeit vergangen war, bis man tatsächlich hinschaut (live auf der Test-VM als genau dieses Missverständnis reproduziert). - Schreiben: „Speichern“ bei den Reservierungen (nur mit
settings_dhcp.edit) schreibt die vollständige, generierte Kea-JSON- Konfiguration an den konfigurierten Ausgabepfad. Wirksam wird sie erst nach einem Dienst-Neustart über den separaten Button. - Ampel im Topbar: neben dem Prüfintervall-Timer zeigt ein grüner/roter
Punkt, ob der Kea-Dienst läuft — nur sichtbar mit
settings_dhcp.view. - Reservierungen mit aktiven eigenen Options zeigen in der Tabelle, welche Option greift (global oder Client-Override).
Live gegen eine echte, frisch installierte Kea-Instanz verifiziert:
Installation, generierte Konfiguration besteht kea-dhcp4 -t, Dienststart
übernimmt sie fehlerfrei (DHCP4_CONFIG_COMPLETE), globaler Options-Wert
sowie Client-spezifischer Override erscheinen korrekt getrennt in der
Konfiguration.
Die Mehrfach-Subnetz-Unterstützung selbst (Anlegen/Bearbeiten/Löschen samt
Validierung, mehrere generierte subnet4-Blöcke mit korrekten Pools/
Interfaces, Migration einer bestehenden Einzel-Range) ist live auf einem
echten Host verifiziert. Zwei tatsächlich unabhängige Netze (z.B. über
zwei physische NICs) standen beim Test nicht zur Verfügung — verifiziert
wurde daher zusätzlich zwei Ranges innerhalb desselben real vorhandenen
Netzes (identischer subnet-CIDR, unterschiedliche Pools); ob Kea zwei
subnet4-Blöcke mit identischem subnet-Wert akzeptiert oder als Fehler
ablehnt, wurde nicht gegen eine echte Kea-Instanz geprüft — bei tatsächlich
unterschiedlichen Netzen (der eigentliche Anwendungsfall) besteht dieses
Risiko nicht. Die Leases-Anzeige wurde vollständig End-to-End im echten
Betrieb bestätigt: ein tatsächlicher, unreservierter Client im Testnetz
(„shellyhtg3-...“) hat sich selbstständig eine IP aus dem Pool geholt und
wurde korrekt mit der „Ohne Reservierung“-Markierung angezeigt.
Netzwerkeinstellungen des Hosts (Systemeinstellungen)
Eigene, klar getrennte Funktion von obigem DHCP-Server: hier geht es um
die Netzwerkkonfiguration des Hosts, auf dem der PoE Manager selbst läuft
(IP-Adresse, DNS-Server, DHCP-Client vs. statisch, Hostname) — nicht um
Adressen, die er an andere Geräte vergibt. Der Hostname wird separat und
risikolos über hostnamectl set-hostname gesetzt (kein Revert-Timer nötig,
da eine Änderung nicht die Erreichbarkeit kappt).
- Backend-Erkennung: prüft, ob NetworkManager oder dhcpcd aktiv ist
(
systemctl is-active); ist keins von beiden aktiv, abersystemd-networkdaktiv undnetplaninstalliert, wird netplan angenommen — der Standard auf einem frisch installierten Ubuntu (Server wie Cloud-Image). Ist keins der drei erkennbar, bleibt die Seite bewusst rein lesend (Status wird trotzdem angezeigt) — ohne zuverlässige Erkennung des Backends keine Änderung, um nicht ins Blaue zu raten. - Anzeige: aktuelles Interface, IP/Prefix, Gateway, DNS-Server sowie
Modus (DHCP/Statisch) — Modus wird backend-spezifisch ermittelt
(
nmcli ipv4.method, einstatic ip_address=-Eintrag im betreffendeninterface-Block von/etc/dhcpcd.conf, bzw. bei netplan amdynamic-Flag des Kernels auf der aktuellen Adresse — zuverlässiger als die u.U. mehreren überlagernden YAML-Dateien unter/etc/netplan/selbst zu parsen). - Änderung: Interface, Modus (DHCP/Statisch), bei Statisch zusätzlich
IP/Prefix/Gateway, sowie unabhängig davon eigene DNS-Server (funktioniert
in beiden Modi — z.B. IP per DHCP, aber feste DNS-Server). Bei netplan
landet die Änderung in einer eigenen, niedrig priorisierten Override-Datei
(
/etc/netplan/90-poe-manager.yaml) statt in der vom Distributor bzw. cloud-init mitgelieferten — netplan wendet Dateien alphabetisch an, spätere Werte gewinnen pro Schlüssel, damit bleibt die Original-Datei unangetastet. - Sicherheitsnetz (wie
netplan try): vor jeder Änderung wird die vorherige Konfiguration gesichert. Nach dem Anwenden läuft ein Hintergrund-Timer (Standard 45s) — wird die Änderung nicht explizit bestätigt (nur möglich, wenn die Seite über die neue Konfiguration noch erreichbar ist), rollt die App automatisch auf die gesicherte Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den Host von der Web-UI/SSH abschneidet. - DHCP-Server wird vorsorglich gestoppt UND deaktiviert: eine vorher passende Range (siehe DHCP-Konfiguration oben) kann nach einer IP-/Subnetzänderung plötzlich zum falschen Netz gehören. Läuft Kea beim Anwenden oder beim Auto-Rollback, wird der Dienst deshalb gestoppt und deaktiviert (nicht automatisch neu gestartet — die Range muss der Admin aktiv auf der DHCP-Seite prüfen und den Dienst dort bewusst wieder starten). Das zusätzliche Deaktivieren verhindert, dass ein Host-Neustart vor dieser Prüfung den Dienst trotzdem automatisch mit der ggf. falschen Range wieder hochfährt, statt dass die App mit einer möglicherweise falschen Konfiguration weiterläuft oder blind rät.
- Live gegen eine echte Ubuntu-24.04-VM verifiziert (Standard-Installation,
Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die
Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der
eigenen Override-Datei. NetworkManager und dhcpcd sind zusätzlich per
gemockten
subprocess.run/Dateizugriffen verifiziert (inkl. eines echtendhcpcd.conf-Rewrites und eines vollständigen Anwenden-dann-Auto- Rollback-Durchlaufs), da in der WSL-Entwicklungsumgebung keins von beiden verfügbar ist.
Änderungslog
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
Geräten, Switchen, Zugangsdaten, Benutzern und Gruppen wird in der Tabelle
audit_log protokolliert (Zeitpunkt, Benutzer, Aktion, Ziel, Details) —
einsehbar unter Logs → Änderungen (Recht logs_activity.view). Zusätzlich merken
sich Geräte und Switche direkt am Datensatz (last_modified_by,
last_modified_at), wer sie zuletzt geändert hat, damit man das nicht erst
im Log nachschlagen muss.
PoE-Neustarts werden bewusst nicht im Änderungslog erfasst — die stehen
bereits im Live-Log von poe.sh. Eine Ausnahme ist die Änderung des
Prüfintervalls selbst (eine Einstellungsänderung, kein Neustart), die wird
mitprotokolliert.
Import/Export
Unter Einstellungen lassen sich Geräte, Switche und Zugangsdaten
(inkl. entschlüsselter Passwörter) als ein einziges JSON-Bundle exportieren.
Das Bundle wird mit einem aus einer selbstgewählten Passphrase abgeleiteten
Schlüssel verschlüsselt (PBKDF2-HMAC-SHA256 + Fernet) — unabhängig vom
fernet.key dieser Installation, damit die Datei auf einer komplett neuen
Umgebung (mit eigenem, neu generiertem fernet.key) wieder eingelesen
werden kann. Gedacht für einen Umzug auf einen neuen Server.
Beim Import wird dieselbe Passphrase erneut abgefragt; Zugangsdaten werden nach Name, Switche nach Hostname und Geräte nach MAC-Adresse abgeglichen — bestehende Einträge mit gleichem Schlüssel werden aktualisiert, neue werden angelegt. Switche referenzieren ihre Zugangsdaten dabei über den Namen (nicht die interne ID), damit der Import auch bei abweichender ID-Vergabe auf dem Zielsystem korrekt verknüpft.
Eigenes Konto & Profilbild
Das Zahnrad-Symbol neben dem eigenen Namen unten in der Sidebar führt zu
„Mein Konto“ (/account): Vor-/Nachname ändern, ein Profilbild
hochladen (PNG/JPG/GIF/WEBP) und das eigene Passwort ändern (erfordert
Eingabe des aktuellen Passworts). Das ist bewusst getrennt vom Admin-Bereich
„Benutzer“, der andere Konten verwaltet. Ist ein Profilbild hinterlegt,
erscheint es statt der Initialen sowohl in der Sidebar als auch im
Änderungslog neben dem Benutzernamen. Admins sehen auf derselben Seite
zusätzlich die Navbar-Reihenfolge (siehe unten).
Warnung bei ungespeicherten Änderungen
Generischer, app-weiter Mechanismus (kein Opt-in pro Seite nötig): erkennt automatisch, ob ein Formular mit echten Eingabefeldern (nicht nur versteckten Aktions-Feldern wie bei Löschen-/Toggle-Buttons) seit dem Laden der Seite verändert wurde, und fragt beim Verlassen (Klick auf einen Link, z.B. in der Navigation) per eigenem Ja/Nein-Dialog, ob zuerst gespeichert werden soll, statt Änderungen stillschweigend zu verwerfen:
- „Ja, speichern“: sendet das betroffene Formular ganz normal ab (echter
POST, kein AJAX-Nachbau) und merkt sich das eigentliche Navigationsziel in
sessionStorage, um nach dem Speichern automatisch dorthin weiterzuleiten — der Link muss dafür nicht ein zweites Mal angeklickt werden. - „Nein, verwerfen“: verwirft die Änderungen und wechselt direkt zur Zielseite.
- Klickt man stattdessen auf das eigene „Speichern“/„Anlegen“ der Seite direkt (ohne über einen Link zu einer anderen Seite zu gehen), passiert nichts zusätzlich — kein Dialog, kein Browser-Popup, es wird einfach ganz normal gespeichert.
- Für Browser-eigene Navigation (Tab schließen, Neuladen, Adresszeile) gibt
es zusätzlich
beforeunload— dort erlaubt der Browser aus Sicherheitsgründen nur eine generische Warnung ohne eigene Buttons, kein Ja/Nein/Speichern-Dialog. Sie wird bewusst unterdrückt, sobald die Seite ohnehin schon bewusst verlassen wird (eigenes Speichern oder der Ja/Nein-Dialog) — sie soll ausschließlich bei tatsächlich ungefragter Navigation erscheinen, nicht zusätzlich bei einem ganz normalen Speichern (live als genau dieses Doppel-Popup reproduziert und behoben). - Ausnahmen über
data-no-unsaved-guardauf Formular oder Link (z.B. das Login-Formular — Zugangsdaten eintippen ist keine "Einstellung").
Live per Playwright auf mehreren Seiten (Systemeinstellungen, DHCP-Options, Gruppen anlegen) verifiziert: eigener Dialog erscheint nur bei tatsächlich ungespeicherten Änderungen, „Ja“ speichert und leitet danach automatisch zum ursprünglichen Ziel weiter, „Nein“ verwirft und wechselt direkt, ein normales Speichern über den Seiten-eigenen Button zeigt weder eigenen noch Browser-Dialog.
Nebenbei wurde ein generisches Cache-Busting für app.js/style.css
ergänzt (asset_url()-Helper, hängt die Änderungszeit der Datei als
?v=... an) — vorher konnte ein bereits gecachtes, veraltetes app.js im
Browser nach einem Update scheinbar "nicht funktionierende" neue Funktionen
verursachen, obwohl der Code auf dem Server längst aktuell war.
Navigation anpassen
Die Sidebar ist in drei aufklappbare Gruppen strukturiert:
- Geräte — Clients (die eigentlichen Devices), Switche, Zugangsdaten
- Einstellungen — Benutzer, Gruppen, Systemeinstellungen (Prüfintervall), Im-/Export
- Logs — Live, Änderungen (Änderungslog)
Die Reihenfolge dieser drei Gruppen (und von „Dashboard“) ist für Admins
unter Mein Konto (Zahnrad-Symbol in der Sidebar) per Auf-/Ab-Buttons
konfigurierbar (gespeichert als JSON-Liste unter settings.nav_order) und
gilt global für alle Benutzer. Unabhängig von der Reihenfolge sieht jeder
Benutzer ohnehin nur, wofür er berechtigt ist — sowohl auf Gruppenebene als
auch pro Unterpunkt: hat ein Benutzer z.B. nur devices.view, erscheint die
Gruppe „Geräte“ nur mit dem Unterpunkt „Clients“ (ohne Switche/
Zugangsdaten); eine Gruppe verschwindet komplett, wenn kein Unterpunkt
sichtbar ist.
Entwicklung unter WSL
Da das Projekt ausschließlich für Linux gebaut ist, empfiehlt sich die
Entwicklung direkt unter WSL2 (Ubuntu), auch wenn der Editor unter Windows
läuft — das Projektverzeichnis liegt dabei ganz normal unter
C:\...\aruba-poe-modern und wird aus WSL über /mnt/c/... erreicht:
wsl --install -d Ubuntu # einmalig, falls noch nicht vorhanden
wsl -d Ubuntu
sudo apt update && sudo apt install -y python3 python3-venv python3-pip sqlite3 expect openssh-client
cd /mnt/c/Users/<dich>/aruba-poe-modern/srv/poe_manager
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
./venv/bin/python create_db.py
./venv/bin/python create_admin.py
./venv/bin/python app.py # http://localhost:5000 — via WSL2-Portweiterleitung
# auch aus dem Windows-Browser erreichbar
Hinweis zu rpi-check.service: Der „Speichern & Service neustarten“-Button
unter Systemeinstellungen sowie das „Jetzt prüfen“-Icon funktionieren nur,
wenn rpi-check.service tatsächlich als echter systemd-Service läuft (viele
aktuelle WSL2-Installationen unterstützen systemd, siehe wsl.conf
[boot] systemd=true). Für lokale Tests reicht es, etc/systemd/system/ rpi-check.service sowie usr/local/bin/custom/poe.sh per Symlink an ihre
Produktivpfade (/etc/systemd/system/, /usr/local/bin/custom/) zu legen
und /srv/poe_manager auf den Checkout zu verlinken, dann systemctl daemon-reload && systemctl enable --now rpi-check.service. Ohne systemd
(z.B. systemctl nicht vorhanden) scheitert der Neustart-Aufruf lautlos —
das Intervall in der DB wird trotzdem aktualisiert, greift dann aber erst
nach einem manuellen Neustart des Prozesses.
Lokal starten (Entwicklung)
cd srv/poe_manager
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python create_db.py # legt sqlite.db + Tabellen an
python create_admin.py # legt ersten Admin-Benutzer an
python app.py # http://localhost:5000
Produktiv-Deployment (auf der Ziel-VM, App landet unter /srv/poe_manager)
Das Repo wird bewusst nicht direkt nach /srv/poe_manager geklont —
install.sh kopiert (rsync) den Inhalt von srv/poe_manager/ aus dem Repo
gezielt dorthin und lässt Quell-Checkout und Zielverzeichnis damit sauber
getrennt (kein doppeltes /srv/poe_manager/srv/poe_manager/…):
sudo git clone <dieses-repo> /opt/aruba-poe-modern-src
cd /opt/aruba-poe-modern-src
sudo bash install.sh
sudo /srv/poe_manager/venv/bin/python3 /srv/poe_manager/create_admin.py
Nach der Installation liegt die laufende App vollständig unter
/srv/poe_manager (so wie es poe_web.service, rpi-check.service und
POE_SCRIPT/POE_DB_PATH als Default erwarten) — /opt/aruba-poe-modern-src
wird nur als Checkout-Quelle für install.sh gebraucht und kann danach
liegen bleiben (für spätere Updates: git pull dort, dann install.sh
erneut ausführen) oder gelöscht werden.
install.sh installiert Systempakete, richtet die venv ein, initialisiert die
Datenbank (falls nicht vorhanden), kopiert poe.sh nach
/usr/local/bin/custom/, registriert die systemd-Units
(poe_web.service, rpi-check.service, rpi-check-restart.timer) und
konfiguriert nginx als Reverse-Proxy auf Port 80.
Relevante Umgebungsvariablen (optional)
| Variable | Zweck | Default |
|---|---|---|
POE_DB_PATH |
Pfad zur SQLite-DB | <app-dir>/sqlite.db |
POE_SCRIPT |
Pfad zum PoE-Restart-Skript | /usr/local/bin/custom/poe.sh |
POE_LOG_GLOB |
Glob-Pattern der Check-Logs | /var/log/rpi-*.log |
POE_FERNET_KEY |
Pfad zum Fernet-Schlüssel | <app-dir>/fernet.key (auto-erzeugt) |
POE_KNOWN_HOSTS |
Pfad zur SSH-known_hosts-Datei des Terminals | <app-dir>/known_hosts (auto-erzeugt) |
POE_SECRET_KEY |
Flask Secret Key (überschreibt Datei) | zufällig, in secret.key gespeichert |
PORT, FLASK_DEBUG |
Flask-Serverport / Debug-Modus | 5000 / aus |
Browser-SSH-Terminal (Switch-Verbindungstest)
„Verbindung testen“ im Switch-Modal öffnet eine WebSocket-Verbindung zu
/ws/ssh_terminal. Serverseitig baut paramiko eine echte SSH-Sitzung
zum Switch auf (inkl. interaktiver Host-Key-Bestätigung beim Erstkontakt,
danach dauerhaft in known_hosts gemerkt) und reicht eine interaktive Shell
1:1 an ein xterm.js-Terminal im Browser durch. Die Zugangsdaten aus dem
Formular werden direkt für die SSH-Authentifizierung verwendet (nicht als
Tastatureingabe in die Shell getippt) — Erfolg/Fehlschlag der Anmeldung wird
sofort und eindeutig im Terminal angezeigt. Ein Klick auf „Passwort
einfügen“ sendet den aktuell im Formular eingetragenen Wert zusätzlich als
Tastatureingabe (z.B. für ein sudo-Passwort in der geöffneten Sitzung).
Voraussetzungen:
- Läuft auf jedem Betriebssystem (paramiko ist reines Python) — die SSH-Verbindung wird dabei immer vom PoE-Server aus aufgebaut, nie vom Browser/Client.
- Nur für eingeloggte Admins erreichbar.
- Der Flask-Entwicklungsserver muss mit
threaded=Truelaufen (ist bereits so konfiguriert) — für einen echten Produktivbetrieb empfiehlt sich ohnehin ein WSGI-Server mit WebSocket-Unterstützung (z.B. Gunicorn mitgevent- odereventlet-Worker) statt des eingebauten Dev-Servers. - nginx muss den WebSocket-Upgrade für
/ws/durchreichen (Upgrade/Connection-Header, sieheetc/nginx/sites-available/poe_manager) — ohne das lehnt Flask den Handshake mit400 Bad Requestab und das Terminal bleibt leer auf „Getrennt“ hängen, obwohl derselbe Aufruf direkt gegen Port 5000 (an nginx vorbei) funktioniert. War auf einer frisch perinstall.shaufgesetzten Ubuntu-VM live reproduzierbar, da alle bisherigen Terminal-Tests ausschließlich direkt gegen Flask liefen, nie durch nginx. - Da
poe_web.service(wie im Original) alsrootläuft, kann über dieses Terminal im Prinzip jeder erreichbare Host per SSH kontaktiert werden — bewusst so gewählt, da Admins über die App ohnehin schon vollen Zugriff auf alle hinterlegten Switch-Zugangsdaten haben. Für eine strengere Trennung empfiehlt es sich,poe_web.servicekünftig unter einem eigenen, unprivilegierten Systembenutzer laufen zu lassen (im Original wie auch hier aktuellroot, um Änderungen am produktiven Setup nicht zu vergrößern).
Projektstruktur
srv/poe_manager/ Flask-App (Backend + Templates + Static-Assets)
usr/local/bin/custom/ poe.sh — Ping-Check & PoE-Restart per SSH (expect)
etc/systemd/system/ Service-/Timer-Units (Web-App + Check-Dienst)
etc/nginx/ Reverse-Proxy-Konfiguration
install.sh Produktiv-Installer