certbot richtet die automatische Verlaengerung (certbot.timer, zweimal
taeglich) zwar schon beim Paket-Install selbst ein -- das war fuer den
Admin in der Weboberflaeche bisher aber unsichtbar, reine Behauptung im
Code-Kommentar. Jetzt sichtbar/nachpruefbar:
- Neue Zeile "Automatische Verlaengerung" im Zertifikat-Status (nur bei
Let's-Encrypt-Zertifikaten): Timer aktiv/inaktiv + naechster geplanter
Lauf (_certbot_timer_status(), liest systemctl is-active/list-timers).
- "Verlaengerung testen (Dry-Run)" -- certbot renew --dry-run, aendert
nichts, zeigt nur ob die Verlaengerung im Ernstfall funktionieren wuerde.
- "Jetzt manuell verlaengern" -- certbot renew --force-renewal, fuer
Testzwecke oder eine sofortige Schluessel-Rotation (mit Warnhinweis auf
Let's Encrypts Rate-Limits).
- Hinweistext beim Anfordern zeigt den Timer-Status schon VOR der ersten
Anforderung.
VERSION 1.0.7 -> 1.0.8 (ab hier wieder normale fortlaufende
Versionierung statt erneutem Release derselben Versionsnummer).
Live auf Testsystem-Update-Restart verifiziert: Seite laedt weiterhin
fehlerfrei (_certbot_timer_status crasht nicht ohne aktives
Let's-Encrypt-Zertifikat), certbot.timer selbst bestaetigt aktiv
(naechster Lauf laut systemctl list-timers korrekt angezeigt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Umbenennung: Einstellungen -> SSL/TLS heisst jetzt Einstellungen -> NGINX
(Route /settings/ssl -> /settings/nginx, Recht settings_ssl.* ->
settings_nginx.*, Icon "server" statt "shield") -- die Seite verwaltet
den kompletten Reverse-Proxy, nicht mehr nur SSL.
Neu:
- Domain (server_name) und HTTP-/HTTPS-Port sind jetzt einstellbar, nicht
mehr hart "_" bzw. 80/443. Aendert man den HTTP-Port weg von 80, wird
automatisch zusaetzlich ein minimaler Nur-ACME-Block auf Port 80
mitgeschrieben, da Let's Encrypt (HTTP-01) protokollbedingt immer Port 80
braucht, unabhaengig vom konfigurierten HTTP-Port.
- "RAW-Konfiguration"-Button zeigt die komplette aktuelle nginx-Config im
selben Popup-Stil wie das Live-Log (Wiederverwendung von
window.openRawLogModal, kein neuer Code dafuer noetig).
- Let's Encrypt: Zertifikat per certbot anfordern (certonly --webroot,
bewusst NICHT das certbot-nginx-Plugin -- das wuerde die App-eigene,
komplett selbstgeschriebene Config direkt editieren und bei der
naechsten Aenderung ueber diese Seite wieder verlieren). Automatische
Verlaengerung laeuft ueber certbots eigenen systemd-Timer/Cron
(Standard-Paketverhalten), ein Deploy-Hook
(/etc/letsencrypt/renewal-hooks/deploy/tesm-reload-nginx.sh, wird wie
die logrotate-Config bei jedem App-Start neu geschrieben) laedt nginx
nach jeder Verlaengerung neu. Fehlgeschlagene Anforderung (kein
oeffentlicher DNS-Eintrag o.ae.) wird sauber abgefangen und als
Fehlermeldung angezeigt, ohne etwas zu veraendern -- live mit einer
ungueltigen Test-Domain verifiziert.
- Zertifikatsquelle (Upload vs. Let's Encrypt) wird als eigenes Setting
(ssl_source) verfolgt, das Anwenden/Anzeigen/Entfernen beruecksichtigt
beide Quellen jeweils passend (Entfernen einer LE-verwalteten
Installation laeuft ueber "certbot delete", nicht ueber rohes
Datei-Loeschen, damit certbots eigene Renewal-Konfiguration nicht
verwaist).
- certbot zu install.sh's Paketliste hinzugefuegt.
Flask/gunicorn bindet jetzt an 127.0.0.1 statt 0.0.0.0 (tesm.service) --
direkter Zugriff auf Port 5000 aus dem Netz ist damit nicht mehr moeglich,
nur noch ueber den nginx-Reverse-Proxy (der ohnehin nur ueber 127.0.0.1
zugreift). Beantwortet die Nachfrage, ob Flask unerreichbar gemacht werden
kann.
Live auf Testsystem-Update-Restart verifiziert: Domain/Port-Aenderung
(HTTP-Port 8080 inkl. automatischem Zusatz-Block auf Port 80),
RAW-Config-Anzeige, Zertifikat-Upload, Let's-Encrypt-Fehlschlag sauber
abgefangen, Rueckbau auf Standardwerte (_, 80, 443, kein SSL) ueber
denselben Anwenden/Bestaetigen-Sicherheitsnetz-Pfad wie zuvor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SSL/TLS (neue Seite Einstellungen -> SSL/TLS, eigenes Recht settings_ssl.view/.edit):
- Zertifikat+privater Schluessel per Upload hinterlegbar (PEM), Validierung
VOR dem Speichern: gueltiges Zertifikat, noch nicht abgelaufen, Schluessel
passt zum Zertifikat (Abgleich ueber die oeffentlichen Schluessel als
DER-Bytes -- funktioniert algorithmus-unabhaengig fuer RSA/EC/...).
Speicherort /etc/tesm/ssl (bewusst ausserhalb von /srv/tesm, damit
rsync --delete beim Update das Zertifikat nicht mitloescht), Key mit
chmod 600.
- HTTPS aktivieren schreibt/validiert (nginx -t) eine neue nginx-Konfiguration
und laedt erst danach neu; Port 80 leitet dann automatisch auf https weiter.
HSTS optional zuschaltbar, setzt aktives SSL voraus.
- Gleiches Sicherheitsnetz wie bei den Netzwerkeinstellungen: nach dem
Anwenden 45s Zeitfenster zum Bestaetigen, sonst automatischer Rueckroll
der nginx-Konfiguration UND der ssl_enabled/hsts_enabled-Einstellungen.
Beide Pfade (Timeout-Rollback und Bestaetigen-verhindert-Rollback) live
auf Testsystem-Update-Restart verifiziert, inklusive eines ECHTEN,
ungeplant ausgeloesten Rollbacks waehrend des Testens.
- Noch KEINE Let's-Encrypt-Automatisierung (siehe Kommentar im Code) --
alle bekannten Instanzen laufen intern ohne oeffentliche Domain, HTTP-01
wuerde dort nicht funktionieren; kann bei Bedarf separat ergaenzt werden.
install.sh: Update-Idempotenz ("nur anfassen, was sich geaendert hat"):
- install_if_changed()-Helfer: Custom-Skript/systemd-Units/MOTD-Banner
werden nur noch bei tatsaechlich geaendertem Inhalt neu geschrieben,
daemon-reload nur wenn sich mindestens eine Unit geaendert hat.
- nginx-Konfiguration wird nur noch bei einer Frischinstallation aus dem
Repo-Template geschrieben -- eine bereits vorhandene (egal ob unveraendert
oder von der App selbst um SSL erweitert) wird nie wieder ueberschrieben.
Voraussetzung dafuer, dass die neue SSL-Funktion ein Update ueberlebt.
update.sh: komplette Sonderbehandlung fuer die zwei TESM vorausgehenden
Generationen (rpi-check.service, poe_web.service/poe_manager) entfernt --
auf allen aktuellen Installationen laengst migriert. Deaktivierte
Unit-Dateien dieser Generationen wurden auf Testsystem-Update-Restart
zusaetzlich manuell endgueltig geloescht (waren dort seit einer Migration
im September 2025 nur deaktiviert, nicht entfernt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- _live_log_history_files() liefert jetzt zusätzlich generation 0 = die
aktuelle, noch laufende live.log selbst (nicht nur die rotierten
Kopien .1/.2/...) -- _resolve_log_history_file akzeptiert 'live.log'
als gültige Auswahl.
- Jeder Eintrag bekommt ein range_label ('von' - 'bis'): 'bis' ist das
eigene mtime (Zeitpunkt der Rotation) bzw. 'laufend' bei der aktuellen
Datei, 'von' ist das mtime der naechstaelteren Generation -- rename()
bei einer Rotation aendert mtime nicht, jede Generation behaelt also
dauerhaft ihren eigenen Entstehungszeitpunkt. Live verifiziert: die
Zeitraeume reihen sich lueckenlos aneinander (Ende von Generation N ==
Anfang von Generation N-1).
- Dropdown und Toolbar zeigen jetzt den Zeitraum statt des rohen
Dateinamens.
- logs_history.html: Farbcodierung (colorizeLine) entfernt, log-box durch
ein <pre class="raw-log-content"> ersetzt -- reine RAW-Darstellung
ohne Client-seitiges Span-pro-Zeile-Aufbauen, auf Wunsch zur weiteren
Reduktion der Ladezeit im Verlauf (Live-Ansicht selbst bleibt farbig).
- VERSION 1.0.6 -> 1.0.7.
Live getestet auf Testsystem-Update-Restart: 4 Eintraege (aktuell + 3
rotierte Generationen), Zeitraeume lueckenlos verkettet, RAW-Endpoint für
'file=live.log' funktioniert, Path-Traversal-Versuch weiterhin mit 404
abgewiesen.
Zusaetzlich untersucht (kein Code-Fix, siehe Konversation): der Verdacht
eines install.sh-Bugs, der tesm-check.service nach einem Update gestoppt
liesse. journalctl zeigt auf beiden Boxen durchgehend 'active (running)'
ueber den fraglichen Zeitraum -- keine Bestaetigung eines Defekts in der
Dienstverwaltung gefunden. Wahrscheinlichste Erklaerung: ein manuell zu
Testzwecken erzwungener logrotate-Lauf (logrotate -f) direkt nach dem
Update hat live.log geleert, wodurch die Live-Log-Seite und die
Naechste-Pruefung-Anzeige fuer die Dauer eines Pruefintervalls leer
wirkten, bis der naechste Zyklus neuen Inhalt schrieb.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root Cause (live auf POETEST reproduziert): /var/log/tesm war 0o777
(noch aus einer Zeit vor dem Umstieg auf root-laufende systemd-Units).
logrotate verweigert seit den aktuellen Haertungen jede Rotation in
einem group-/world-writable Verzeichnis ("insecure permissions") komplett
-- live.log wuchs auf POETEST seit der letzten Neuinstallation (9 Tage)
unbegrenzt, /var/lib/logrotate/status hatte fuer tesm ueberhaupt keinen
Eintrag. Direkte Folge: die neue Verlauf-Seite blieb leer, weil nie
live.log.1/.2/... entstanden.
Fix:
- os.chmod(TESM_LOG_DIR) und os.chmod(TESM_CLIENT_UPDATE_LOG_DIR) in
app.py: 0o777 -> 0o755 (tesm.service/tesm-check.service laufen beide
als root, kein anderer Dienstbenutzer schreibt hierher -- Keas eigenes
Log liegt separat unter /var/log/kea/). Laeuft bei jedem App-Start,
heilt bestehende Installationen also automatisch beim naechsten
Neustart/Update.
- install.sh: chmod 777 -> chmod 755 fuer /var/log/tesm bei einer
Frischinstallation, veraltete/falsche Kommentar-Begruendung (angeblich
auch fuer Kea noetig) korrigiert.
- _write_logrotate_config(): zusaetzlich 'su root root' in der
generierten Konfiguration als Absicherung, falls das Verzeichnis aus
irgendeinem Grund wieder zu weit geoeffnet wird.
- VERSION 1.0.5 -> 1.0.6.
Live verifiziert auf Testsystem-Update-Restart: logrotate -d zeigt nach
dem Fix keine 'insecure permissions'-Fehler mehr, ein erzwungener Lauf
(logrotate -f) erzeugt sauber live.log.1/live.log.2.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- /logs und /get_log liefern nur noch die letzten 300 Zeilen (_tail_lines,
effizientes Rueckwaerts-Lesen ohne komplette Datei einzulesen) statt der
kompletten live.log -- behebt Ladezeit-Probleme auf groesseren Umgebungen.
- Neuer Endpoint /logs/live/raw liefert das komplette Live-Log fuer ein
Popup im Stil des bestehenden 'Ungespeicherte Aenderungen'-Modals
(window.openRawLogModal in app.js, lazy nachgeladen erst bei Klick).
- Neue Unterseite 'Verlauf' (/logs/history, /logs/history/raw) fuer aeltere,
von logrotate rotierte Kopien des Live-Logs (live.log.1, .2, ...),
auswaehlbar per Dropdown, ebenfalls mit 300-Zeilen-Tail + RAW-Popup.
- Eigenes Recht logs_history.view (Kind von logs_group, direkt unter Live
einsortiert) statt an logs_live.view gekoppelt -- Live-Status sehen und
durch alte Logs blaettern sind bewusst getrennte Rechte.
- Dateiname-Validierung gegen Path-Traversal in _resolve_log_history_file
(striktes Regex + Verzeichnis-Check + Existenzpruefung).
- VERSION 1.0.4 -> 1.0.5.
Live getestet auf Testsystem-Update-Restart (192.168.82.51): Tail-Anzeige,
RAW-Popup, Verlauf-Auswahl inkl. echter live.log.1, Permission-Matrix
(Gruppen-Seite) sowie Rechtetrennung (Nutzer mit nur logs_live.view sieht
/logs, aber weder Verlauf-Link noch Zugriff auf /logs/history bzw.
/logs/history/raw) und Path-Traversal-Abwehr allesamt verifiziert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live gefunden: der 'Schema unveraendert -> kein Backup'-Kurzschluss aus
v1.0.3 liess einen rein CODE-seitigen Fehler im neuen Paket (z.B. ein
kaputtes app.py -- mit dem DB-Schema hat das nichts zu tun) komplett
ungeschuetzt durch. Backup + Health-Check + ggf. Rollback laufen jetzt
bei JEDEM Update einer bestehenden Installation, unabhaengig von
SCHEMA_VERSION -- die bleibt nur noch fuer den Wortlaut der Log-Meldung
relevant (In-Place-Update vs. Migration).
Zusaetzlich beim Rollback selbst: 'mv' durch 'cp -a' beim Zurueckspielen
ersetzt, plus die fehlgeschlagene Installation vorher separat beiseite
gelegt (/srv/tesm-failed-update-<timestamp>) statt geloescht -- vorher
wurde das Backup beim Zurueckspielen selbst zum neuen /srv/tesm und
verschwand dadurch als eigenstaendige Sicherung (widersprach der
eigenen Zusage 'Backup bleibt erhalten').
Live auf der Testbox in allen drei Pfaden verifiziert:
- Erfolgreiches Update: Backup erstellt, Health-Check bestanden,
Backup geloescht, Daten erhalten.
- Migration (kuenstlich abweichende SCHEMA_VERSION, echte Struktur
unveraendert): Backup erstellt, Health-Check bestanden, Backup
geloescht.
- Fehlgeschlagenes Update (absichtlich kaputte app.py): Backup
erstellt, Health-Check schlaegt korrekt fehl, automatischer Rollback
stellt den letzten funktionierenden Zustand wieder her (Dienst aktiv,
Health-Check 200, Datenbank-Canary erhalten), Backup UND
fehlgeschlagene Installation bleiben als zwei getrennte Verzeichnisse
zur Fehlersuche erhalten.
VERSION: 1.0.4 (v1.0.3 hatte die oben beschriebene Luecke und wurde
bereits ausgerollt, daher hier ein neues Release statt eines erneuten
Tags auf v1.0.3).
install.sh erkennt jetzt selbststaendig eine bestehende Installation
(an sqlite.db) und verhaelt sich entsprechend:
- Keine vorhandene Installation -> normale Frischinstallation (wie
bisher).
- Vorhanden, SCHEMA_VERSION unveraendert -> einfaches In-Place-Update,
kein Backup noetig (rsync schliesst sqlite.db ohnehin aus).
- Vorhanden, SCHEMA_VERSION unbekannt/abweichend -> Update MIT
Sicherheitsnetz: die KOMPLETTE bisherige Installation (Code, venv,
Datenbank, Schluessel) wird vorher 1:1 nach
/srv/tesm-backup-pre-migration-<timestamp> kopiert (bewusst nicht nur
die Datenbank -- ein reines DB-Rollback wuerde beim naechsten Start
mit dem NEUEN Code sofort denselben Migrationsfehler wiederholen).
Die eigentliche Schema-Migration uebernimmt danach die App selbst
beim Start (_ensure_schema() in app.py). Kommt der Dienst
anschliessend per HTTP-Check auf /login nachweislich NICHT gesund
hoch, wird automatisch die komplette Sicherung zurueckgespielt und
bleibt zur manuellen Pruefung erhalten. Kommt er gesund hoch, gilt
die Migration als erfolgreich und das Backup wird geloescht.
update.sh delegiert diese gesamte /srv/tesm-spezifische Logik jetzt an
install.sh und ist selbst nur noch fuer das Ablösen AELTERER, TESM
vorausgehender Installationsstufen zustaendig (PoE Manager unter
/srv/poe_manager, das urspruengliche reine Bash-Setup) -- bleibt aber
als bequemer Ein-Skript-Bootstrapper (Download + Entpacken + Aufruf von
install.sh) bestehen, wie gewuenscht. install.sh selbst ist dadurch
jetzt auch OHNE update.sh direkt aus einem entpackten Release-Paket
lauffaehig und erkennt ein Update trotzdem korrekt.
VERSION: 1.0.3 (SCHEMA_VERSION unveraendert, 1 -- keine echte Schema-
Aenderung in diesem Release, In-Place-Update-Pfad bleibt der Normalfall).
Reihenfolge in der Aktionen-Spalte getauscht (Neustart -> Update, statt
Update -> Neustart) und der Einzel-Update-Button auf btn-primary
(bereits vorhandene Akzentfarbe, orange) umgestellt, damit er sich vom
neutralen Neustart-Button abhebt. Der Bulk-Button unten heisst jetzt
'Bulk-Update' statt 'Update starten'.
Live end-to-end verifiziert: Testsystem-Update-Restart hat eine neue
MAC/IP bekommen (52:54:00:AD:B1:73 / 192.168.82.51, Geraete-Eintrag in
POETEST entsprechend aktualisiert) -- der neue Einzel-Update-Button hat
darueber ein komplettes Update ausgeloest und erfolgreich durchlaufen
(alle 6 Schritte einzeln bestaetigt: apt update, initramfs
MODULES=most, apt upgrade, apt full-upgrade, apt clean, apt
autoremove).
Papierkorb: jede der fuenf Kategorien (Clients/Switche/Zugangsdaten/
Benutzer/Gruppen) bekommt jetzt dasselbe Sortieren/Suchen wie die
uebrigen Listenseiten (data-sortable + Such-Eingabe je Tabelle, nur
wenn die Kategorie tatsaechlich Eintraege hat).
Wartung: Update liess sich bisher nur per Bulk (Checkbox + 'Update
starten') ausloesen. Jedes Geraet bekommt jetzt zusaetzlich einen
eigenen 'Update'-Button (analog zum bereits vorhandenen 'Neustart'-
Button, ueber ein eigenstaendiges <form> + form="update-<mac>"-
Referenz -- kein verschachteltes <form>). Zusaetzlich ein 'Bulk-
Neustart'-Button neben 'Update starten', der dieselben Checkboxen/
dasselbe #updateForm nutzt wie das Bulk-Update (Ziel-Route wird per JS
nur fuer diesen einen Submit umgebogen), inkl. Bestaetigungsdialog wie
beim Einzel-Neustart.
Serverseitig war dafuer keine Aenderung noetig -- _maintenance_dispatch()
verarbeitete beliebig viele MACs schon immer generisch, die bisherigen
Formulare haben nur zufaellig immer genau eine bzw. alle uebermittelt.
Live auf POETEST verifiziert: Papierkorb zeigt Sortieren/Suchen nur bei
tatsaechlich befuellten Kategorien; der neue Einzel-Update-Button loest
korrekt einen Job fuer genau dieses eine Geraet aus (SSH-Verbindungs-
versuch zum richtigen Ziel bestaetigt, sauber behandelter Fehler, da das
Testsystem gerade offline war).
Vorher: Sichtbarkeit einer Papierkorb-Sektion war an das jeweilige
Ressourcen-Leserecht gekoppelt (devices.view fuer Clients-Sektion usw.),
Wiederherstellen/endgueltig Loeschen an das jeweilige Ressourcen-
Bearbeiten-Recht (switches.edit fuer Switche im Papierkorb usw.).
Jetzt: papierkorb.view/papierkorb.edit als eigenstaendiges Rechtepaar
unter Einstellungen (vorher unter Geraete einsortiert), komplett
losgeloest von den einzelnen Ressourcen-Rechten:
- papierkorb.view zeigt ALLE fuenf Kategorien auf einmal, unabhaengig
davon, ob der Benutzer die jeweilige aktive Ressource sehen darf.
- papierkorb.edit erlaubt Wiederherstellen/endgueltig Loeschen JEDER
Kategorie, unabhaengig vom sonstigen Bearbeiten-Recht der jeweiligen
Ressource (z.B. switches.edit gewaehrt jetzt KEINEN Zugriff mehr auf
Switche im Papierkorb).
Live auf POETEST mit zwei eigens angelegten Testgruppen verifiziert:
ein Nutzer mit switches.edit (aber ohne papierkorb.edit) kann einen
Switch im Papierkorb nachweislich NICHT wiederherstellen (deleted_at
unveraendert vor/nach dem Versuch); ein Nutzer mit papierkorb.edit
(aber ohne switches.edit) kann es.
can_view_papierkorb leitete Sichtbarkeit bisher aus IRGENDEINEM der
fuenf Ressourcen-Leserechte (devices/switches/credentials/users/groups
.view) ab -- da die Standardgruppe 'Benutzer' bereits devices.view und
switches.view hat, konnte praktisch jeder Benutzer den Papierkorb sehen
(gemeldet: 'Papierkorb ist fuer alle sichtbar -> Rechtesystem').
Papierkorb hat jetzt ein eigenes, dediziertes Recht (papierkorb.view,
neuer Eintrag unter PERMISSIONS/devices_group/children, automatisch im
Gruppen-Editor sichtbar). Innerhalb der Seite bleibt jede Kachel
zusaetzlich einzeln je Ressource gated wie bisher -- papierkorb.view
schaltet nur die Seite selbst frei, keine automatischen Zusatzrechte.
Die Standardgruppe bekommt das neue Recht NICHT automatisch (das ist
der Fix) -- Admins behalten per is_admin-Bypass weiterhin vollen Zugriff.
Live auf POETEST verifiziert: ein Testnutzer mit exakt den
Standardgruppen-Rechten bekam vorher 200 auf /papierkorb, jetzt 302
(Zugriff verweigert), 'Papierkorb' verschwindet aus der Navigation,
/devices bleibt unveraendert erreichbar.
Gitea #1: LDAP-Login-Retry bei Domain-Controller in anderem Netzsegment
Gemeldet: bei einem DC in einem anderen (langsameren) Netzsegment war
ein doppelter Login noetig -- der erste Versuch scheiterte mit
'Benutzer/Passwort ungueltig', obwohl die Zugangsdaten korrekt waren.
Ursache: sowohl der Service- als auch der User-Bind nutzten ein hartes
5s-Timeout (connect_timeout/receive_timeout) ohne jeden Retry -- eine
erste, 'kalte' Verbindung zu einem entfernten/langsameren Netzsegment
kann das leicht ueberschreiten (ARP-/Routing-Aufwaermen, TCP-Handshake,
initiale Info-Abfrage), waehrend ein sofortiger zweiter Versuch dann
klappt, weil der Pfad bereits 'warm' ist.
Timeouts auf 10s angehoben und ein gezielter, genau EINMALIGER
automatischer Retry ergaenzt (_ldap_connect_with_retry) -- greift NUR
bei einem rein netzwerk-/timeout-bedingten Fehler (LDAPSocketOpenError/
LDAPSocketReceiveError/LDAPSocketSendError/LDAPResponseTimeoutError),
NICHT bei einer tatsaechlich abgelehnten Anmeldung (z.B. falsches
Passwort) -- ein Tippfehler soll keinen AD-Kontosperren-Zaehler
unnoetig doppelt hochzaehlen. Isoliert unit-getestet (transienter
Fehler -> 1 Retry -> Erfolg; echte Ablehnung -> kein Retry, sofortige
Exception) und auf POETEST regressionsgetestet (LDAP dort weiterhin
aktiviert und Dienst startet fehlerfrei).
Zusaetzlich: /login aktiv gegen SQL-Injection/Login-Bypass/SSTI-Payloads
getestet (13 Payload-Varianten inkl. UNION SELECT, DROP TABLE, Jinja-
SSTI) -- keine Auffaelligkeiten, durchgehend parametrisierte Queries
bestaetigt, users-Tabelle unveraendert.
Betrifft app.py, create_db.py, poe.sh sowie alle HTML-Templates mit
Kommentaren (Jinja {# #}, HTML <!-- -->, eingebettetes JavaScript //).
Docstrings bleiben unangetastet -- nur reine Kommentarzeilen/-fragmente
wurden entfernt, damit der Code selbst schlank bleibt, ohne die
dahinterliegende Begründung zu verlieren.
Umsetzung:
- app.py/create_db.py: per Pythons eigenem tokenize-Modul entfernt (sicher
gegen '#' innerhalb von String-Literalen, Shebang-Zeile ausgenommen).
- poe.sh: manuell, da einzelne expect-Prompt-Strings ('expect "#"')
ein literales '#' als Teil des Strings enthalten, kein Kommentar.
- Templates: {# #}/<!-- --> per Regex (eindeutige Delimiter, anschließend
mit Jinja2s eigenem Parser validiert), echte // -JS-Kommentare manuell
identifiziert (zwei Fundstellen mit '//' waren tatsächlich Code --
als Protocol-relative-URL-Template-
String -- und wurden bewusst nicht angefasst).
Verifiziert: alle Python-/Bash-/Jinja-Dateien syntaktisch weiterhin
gültig, live auf der Testbox deployt und alle 14 Kernrouten mit 200
bestätigt, keine Fehler im App-Log.
tesm.service startete die App bisher direkt per 'python3 app.py', was
Flasks eingebauten Werkzeug-Entwicklungsserver nutzt -- der meldet bei
jedem Start 'WARNING: This is a development server. Do not use it in a
production deployment.' und ist bewusst nicht für Dauerbetrieb gebaut.
Umgestellt auf Gunicorn, mit zwei bewusst gesetzten Einschraenkungen:
- '--workers 1': die App haelt laufende Wartungs-Jobs/Import-Vorschauen
nur im Arbeitsspeicher DES EINEN Prozesses (_maintenance_jobs,
_pending_imports, ...) -- mehrere Worker-Prozesse wuerden diesen
Zustand auf mehrere, sich nicht kennende Prozesse verteilen.
- '--worker-class gthread --threads 16' statt eines async-Workers: das
Browser-SSH-Terminal (flask-sock) belegt pro offener WebSocket-
Verbindung einen blockierenden Thread.
- '--timeout 0': deaktiviert Gunicorns eigenen 30s-Worker-Timeout, der
sonst auch eine offene, aber gerade inaktive SSH-Terminal-Verbindung
killen wuerde.
Live auf POETEST verifiziert: keine WARNING-Zeile mehr, genau 1 Master-
+ 1 Worker-Prozess, alle Kernseiten weiterhin 200, und als kritischer
Regressionstest das WebSocket-SSH-Terminal Ende-zu-Ende gegen einen
echten Host erfolgreich getestet (Host-Key/Passwort-Prompt/Login/Shell).
Dabei einen unabhaengigen Altlast-Fehler in POETESTs venv gefunden und
allgemein in install.sh gefixt: './venv/bin/pip' hat eine zur
Venv-Erstellung fest einprogrammierte Shebang-Zeile -- nach der
historischen Umbenennung 'poe_manager' -> 'tesm' zeigte sie auf einen
nicht mehr existierenden Pfad und schlug mit 'cannot execute: required
file not found' fehl. 'python3 -m pip' umgeht das, da nur der (korrekt
referenzierte) venv-Python-Interpreter selbst gebraucht wird -- betrifft
sonst auch jedes kuenftige In-Place-Update auf einem aehnlich
gewachsenen Host.
Fortlaufendes Release nach v1.0.0 -- Schema unveraendert (SCHEMA_VERSION
weiterhin 1), bereits installierte v1.0.0-Hosts aktualisieren sich beim
naechsten update.sh-Lauf per In-Place-Update (Datenbank bleibt erhalten).
_import_ldap()/_import_dhcp() riefen bislang set_setting() auf, das eine
EIGENE sqlite3-Verbindung oeffnet. import_apply() ruft mehrere
IMPORT_APPLIERS nacheinander auf DERSELBEN, noch offenen Verbindung auf
und committet erst am Ende -- war zu diesem Zeitpunkt (z.B. durch eine
vorher im selben Import verarbeitete Kategorie) bereits eine Schreib-
Transaktion offen, blockierte set_settings()' eigene Verbindung darauf,
bis 'sqlite3.OperationalError: database is locked' auftrat (live in
Produktion reproduziert). Beide Funktionen schreiben jetzt direkt auf
die uebergebene Verbindung. Isoliert reproduziert und verifiziert (alter
Code: Lock-Fehler: neuer Code: fehlerfrei).
Wartungs-Update fuer Linux-Clients (_apt_upgrade_command) außerdem
komplett neu aufgebaut: statt einer einzigen, mit '&&' verketteten
Befehlskette laufen jetzt einzeln benannte Schritte (apt update ->
initramfs MODULES=most dauerhaft in initramfs.conf setzen -> apt upgrade
-> apt full-upgrade -> apt clean -> apt autoremove), jeder mit eigenem
'Schritt N (...) -> erfolgreich/fehlgeschlagen (Exit-Code ...)'-Marker im
Live-Fenster (mit Zeitstempel je Zeile, wie gehabt). Bricht beim ersten
fehlgeschlagenen Schritt sofort ab. Bei einem mit Exit-Code
fehlgeschlagenen Update wird zusaetzlich eine Fehler-Logdatei unter
/var/log/tesm/clientupdate/<clientname>.error geschrieben (ueberschreibt
eine vorherige aus einem frueheren Fehlschlag desselben Clients).
Neue Datei SCHEMA_VERSION (neben VERSION) markiert den DB-Schema-Stand.
update.sh laedt/entpackt das Release-Paket jetzt VOR der Bestaetigungs-
abfrage und vergleicht die SCHEMA_VERSION der bestehenden Installation
mit der des neuen Pakets:
- Stimmen beide ueberein (und existiert bereits eine sqlite.db) --
In-Place-Update: /srv/tesm bleibt bestehen, install.sh rsynct nur den
neuen Code darueber (sqlite.db/fernet.key/secret.key waren dort schon
immer von rsync ausgeschlossen). Geraete/Switche/Zugangsdaten/Benutzer/
Einstellungen bleiben vollstaendig erhalten, kein manuelles Neuanlegen
oder Re-Import noetig.
- Unterscheiden sie sich (oder ist eine der beiden Versionen unbekannt,
z.B. eine Installation von vor dieser Aenderung) -- weiterhin der
bisherige sichere Weg: /srv/tesm wird beiseite verschoben (nicht
geloescht) und komplett frisch installiert.
/srv/poe_manager (voellig andere App/Schema) wird davon unabhaengig
immer beiseite verschoben, nie in-place uebernommen.
create_db.py bekommt einen Hinweis-Kommentar, SCHEMA_VERSION bei jeder
Schema-Aenderung mit hochzuzaehlen.
VERSION-Datei (neben app.py, wird von install.sh 1:1 nach /srv/tesm
mitkopiert) plus Context-Processor inject_tesm_version() -- die
zugehoerige app.py-Aenderung war bereits im vorherigen Commit
enthalten. Damit laesst sich der installierte Stand direkt in der
Oberflaeche ablesen, ohne Datei-Zeitstempel oder Commit-Hashes
vergleichen zu muessen.
_apt_upgrade_command() setzt jetzt zusaetzlich MODULES=most (initramfs-
tools) fuer den Update-Aufruf -- falls dabei ein neuer Kernel installiert
und ein initramfs neu gebaut wird, landen vorsorglich ALLE verfuegbaren
Treiber darin statt nur die per Abhaengigkeitserkennung ('dep') erkannten,
damit der Client nach einem Kernel-Wechsel zuverlaessig bootet. Am Ende
zusaetzlich 'apt-get autoremove'/'apt-get clean', damit nicht mehr
benoetigte Kernel/Pakete sowie der .deb-Cache nicht auf jedem Client
unbegrenzt Platz belegen. Live gegen einen echten Ubuntu-Client
verifiziert (Exit-Code 0, komplette Kette lief durch).
Bei einigen Switches/APs (u.a. ArubaOS-Varianten), die Server-seitig
sowohl 'password' als auch 'keyboard-interactive' anbieten, fuehrte
keyboard-interactive zu einem stillen Fehlschlag ganz ohne sichtbaren
Prompt im Browser-Terminal (RFC 4256 erlaubt eine erste Runde ganz ohne
Prompts -- fuer den Nutzer unsichtbar -- gefolgt von sofortigem
Scheitern statt einer zweiten Runde mit dem eigentlichen Passwort-
Prompt). Das erklaerte exakt das gemeldete Symptom: kein Prompt, direkt
'Zugangsdaten falsch'.
'password' wird jetzt bevorzugt, wenn der Server es ueberhaupt anbietet
(Ende-zu-Ende gegen einen echten Host verifiziert: Host-Key-Bestaetigung
-> Passwort-Prompt -> erfolgreicher Login, keine Regression). Die
keyboard-interactive-Handler-Funktion wurde zusaetzlich gehaertet:
title/instructions werden jetzt mit angezeigt (manche Geraete legen den
Hinweistext dort statt in prompt_list ab) und leere prompt_text-Strings
bekommen einen Passwort-Fallback-Text, falls keyboard-interactive doch
der einzig angebotene Weg ist.
Ein wirklich frisches Setup (create_db.py auf einer neuen Installation)
erzeugte bislang nur den ALLERERSTEN Schema-Stand aus der Projektgeschichte
-- alle seither per _ensure_schema()-Migration nachgerüsteten Spalten und
Tabellen fehlten komplett, bis diese Migration beim ersten App-Start
erfolgreich durchläuft. Live auf einem frisch installierten Produktivsystem
ist genau das nicht passiert: "sqlite3.OperationalError: no such column:
deleted_at" beim Dashboard-Aufruf, weil devices/switches/credentials/
users/groups ohne die Papierkorb-Spalte angelegt wurden. Fehlten außerdem
komplett: dhcp_subnets, dhcp_manual_reservations, service_accounts,
ldap_group_mappings (LDAP/DHCP-Funktionen wären auf einer solchen frischen
Installation gar nicht nutzbar gewesen) sowie dhcp_option_defs.is_standard.
- create_db.py jetzt deckungsgleich mit dem tatsächlichen, vollständig
migrierten Schema (per .schema-Dump von einer produktiv laufenden Instanz
verifiziert) -- category/deleted_at auf credentials, deleted_at auf
switches, credential_id/ssh_port/deleted_at auf devices, deleted_at auf
users/groups, is_standard auf dhcp_option_defs, plus die vier fehlenden
Tabellen.
- POE_DB_PATH → TESM_DB_PATH in create_db.py UND create_admin.py (weiteres
übersehenes Namensrelikt aus der TESM-Umbenennung), Docstring von
create_admin.py aktualisiert.
- Live verifiziert: komplett frische DB erzeugt, App auf separatem
Testport dagegen gestartet, alle wesentlichen Routen (Dashboard, Geräte,
Switche, Zugangsdaten, Wartung, Papierkorb, Benutzer, Gruppen, alle
Einstellungsseiten, Logs, Konto) liefern 200 ohne einen einzigen Fehler
im App-Log -- eine frische Installation hängt jetzt an keiner Stelle mehr
von der Laufzeit-Migration ab, um überhaupt benutzbar zu sein.
- Automatisches Schreiben+Neustart: jede relevante Änderung (Reservierung,
Option, Subnetz, globale Einstellung) ruft jetzt _dhcp_write_and_restart()
direkt nach dem Speichern auf -- schreibt die Kea-Config neu und startet
den Dienst neu, FALLS er gerade aktiv ist (kein ungefragtes Hochfahren
eines bewusst gestoppten Dienstes). Ein manuelles "Datei schreiben" +
"Neu starten" kann dadurch nicht mehr vergessen werden. Der bisherige
"Speichern"-Button (write_dhcp_file) restartet jetzt ebenfalls
automatisch statt nur zu schreiben.
- Leases-Übersicht dedupliziert: Keas memfile-Backend hängt bei jeder
Verlängerung eine neue Zeile an, statt die alte zu ersetzen -- dieselbe
Adresse stand oft dutzendfach in der CSV. _dhcp_read_leases() zeigt jetzt
nur noch die jeweils neueste (per "expire") Zeile pro IP.
- Einzelne Leases löschen (neuer Button + Route): entfernt alle Zeilen
einer Adresse aus der Lease-CSV und startet den Dienst neu, damit Kea sie
beim Neuladen nicht aus dem Speicher zurückschreibt.
- Eigenständiger Autostart-Slider (wie beim Aktivieren/Deaktivieren von
Clients) -- steuert nur systemctl enable/disable, entkoppelt vom
aktuellen Start/Stopp-Zustand.
- KRITISCH, live gefunden und behoben: die erste Version von
_dhcp_delete_lease() hat Kea beim Neustart zum Absturz gebracht --
Pythons csv-Modul schreibt standardmäßig "\r\n"-Zeilenenden, Kea erwartet
aber reines "\n" und lehnte die Datei mit "invalid column: 'pool_id^M'"
komplett ab. Zusätzlich hätte das Neuanlegen der Datei als root (der
App-Prozess) sie root:root statt dem Kea-Dienstnutzer gehören lassen,
wodurch Kea keine neuen Leases mehr hätte schreiben können. Beides
gefixt (lineterminator="\n", Eigentümer/Rechte der Originaldatei nach dem
Schreiben wiederhergestellt) und auf POETEST bestätigt: Dienst übersteht
den Neustart jetzt, Datei bleibt _kea:_kea, keine ^M mehr.
- Neue _run_ssh_command_streaming(): liest stdout/stderr zeilenweise WÄHREND
das SSH-Update noch läuft, statt alles erst nach Abschluss auf einmal zu
liefern. Jede Zeile bekommt sofort einen eigenen Zeitstempel und wird per
_maintenance_job_set() live in den Job-Status geschrieben (bereits alle
2s gepollt) -- Puffer auf die letzten 400 Zeilen begrenzt statt eines
harten Zeichen-Limits.
- Eindeutiger Abschluss-Marker als letzte Ausgabezeile: "✔ All Updates
Done" bei Erfolg, "✖ Update abgebrochen/fehlgeschlagen …" bei Fehler --
vorher endete die Ausgabe einfach mitten im rohen apt-Output.
- Frontend: Ausgabefenster scrollt jetzt automatisch mit, sobald neue
Zeilen reinkommen ("stick to bottom", solange man nicht selbst
hochgescrollt hat) und beim ersten Öffnen direkt ans Ende.
- Live auf POETEST verifiziert: Zeilen tragen korrekte Zeitstempel, Update
endet sichtbar mit "✔ All Updates Done".
- KRITISCH: usr/local/bin/custom/poe.sh wurde bei der TESM-Umbenennung
übersehen und zeigte weiterhin komplett auf die alten Pfade
(/srv/poe_manager/sqlite.db, /srv/poe_manager/generate_ips.py,
/var/log/poe-manager) -- der Ping-Check/PoE-Neustart-Loop
(tesm-check.service) lief dadurch seit dem Umzug nur noch auf Fehler
("No such file or directory"), das Live-Log stand seit gestern 19 Uhr
still und automatische PoE-Neustarts bei Geräteausfall liefen die
ganze Zeit ins Leere. Auf POETEST live behoben und verifiziert: Log
schreibt wieder frische Einträge, Checks laufen wieder durch.
- Wartung: "Update starten" nahm live IMMER zusätzlich das erste Gerät
mit, auch ganz ohne angehakte Checkbox. Ursache: der "Neustart"-Button
jeder Zeile saß in einem eigenen <form>, das INNERHALB des großen
#updateForm verschachtelt war -- ungültiges HTML, wodurch Browser das
äußere Formular vorzeitig schließen/aufspalten. Behoben, indem jedes
Neustart-<form> jetzt außerhalb von #updateForm steht und der Button
es nur per form="reboot-<mac>" referenziert. Live verifiziert: ein
gezielt einzeln ausgewähltes Gerät startet jetzt ausschließlich sich
selbst, keine leere Auswahl mehr möglich.
- Zusätzlich defensiv: Checkboxen werden beim Laden der Wartungsseite
jetzt explizit zurückgesetzt (manche Browser stellen den
Checked-Zustand nach einem Redirect/Reload sonst aus dem Verlauf
wieder her).
- Restliche "poe"-Relikte aus der TESM-Umbenennung bereinigt: Export-
Dateiname (poe_manager_export_… → tesm_export_…), Kommentare
(poe_web.service → tesm.service) sowie ein für den Nutzer sichtbarer
Hinweistext in Systemeinstellungen (rpi-check.service → tesm-check.service).
- Kritischer Bug behoben: _apply_network_config() baute die netplan-
Override-Datei bei JEDEM Speichern komplett neu auf (nur mit dem gerade
bearbeiteten Interface) statt bestehende Einträge anderer Interfaces zu
erhalten -- ein zweites Interface einzurichten hat dadurch die statische
Konfiguration des ersten aus der Datei geworfen (fiel auf DHCP zurück).
Jetzt wird die vorhandene Datei eingelesen und nur der Block des
gespeicherten Interfaces ersetzt/ergänzt.
- Live auf POETEST reproduziert und nach dem Fix verifiziert: enp1s0 verlor
durch den Bug seine statische 192.168.80.202/24 (fiel auf DHCP zurück),
während enp3s0 (zweites, neu eingerichtetes Interface) 192.168.82.202/24
behielt. Nach dem Fix enp1s0 über die echte GUI-Route wiederhergestellt
(Werte aus dem aktiven DHCP-Lease bestätigt) -- beide Interfaces stehen
jetzt nebeneinander in derselben netplan-Datei.
- Neuer JSON-Endpunkt /settings/network_state + JS: die Eingabemaske
(Modus/IP/Prefix/Gateway/DNS) lädt beim Umschalten des Interface-Dropdowns
jetzt dessen tatsächlichen Ist-Zustand nach, statt weiter die Werte des
zuvor angezeigten Interfaces zu zeigen.
- Die Übersicht ganz oben auf der Karte zeigt jetzt JEDES aktive Interface
gleichzeitig (eigene Zeile mit IP/Gateway/Modus/DNS) statt nur das gerade
im Formular ausgewählte.
- net_interface-Setting wird jetzt tatsächlich persistiert (war vorbereitet,
aber nie geschrieben), damit die Seite nach einem Reload das zuletzt
bearbeitete statt immer nur das alphabetisch erste Interface vorbelegt.
- Login-Seite: WiS-/TESM-Logo und "Melde dich an, um fortzufahren" aus der
Box entfernt; die TESM-Wortmarke mit Funktionszeile ist jetzt der
tatsächliche Seitenhintergrund (absolut positioniert, hinter allem), die
Box liegt zentriert davor. Nebenbei zwei echte Bugs gefunden und behoben:
fehlendes Cache-Busting auf login.html (asset_url() wie in base.html,
sonst hält nginx' 7-Tage-Cache eine alte style.css/app.js fest) und ein
durch den fehlenden align-items-Wert auf die volle Höhe gestrecktes
Login-Formular.
- Neuer SSH-Login-Banner (dynamisches MOTD) mit Live-Flottenstatus statt der
Standard-Ubuntu-Willkommensmeldung: srv/tesm/motd_banner.py liest
Online/Offline- und DHCP-Status aus denselben Quellen wie das
Web-Dashboard (get_device_status()/_dhcp_read_leases()), eingebunden über
etc/update-motd.d/50-tesm. install.sh deaktiviert dabei die Standard-
Ubuntu-MOTD-Skripte (chmod -x, reversibel) und installiert den neuen Hook.