From 2770c2e204ff8bed806486acbe261690d7607355 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 10 Oct 2026 19:33:00 +0200 Subject: [PATCH] feat: netmover serve und reset-password, ruhigere Scheduler-Logs Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ++++++++++++++ DEVLOG.md | 36 +++++++++++++++++++++++++++++- README.md | 2 +- src/netmover/cli.py | 41 +++++++++++++++++++++++++++++++---- src/netmover/logging_setup.py | 3 +++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed5e2b4..3cdaf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,23 @@ Die Version steht an genau einer Stelle: `src/netmover/__init__.py`. auch für Anbindungen, deren Wiederherstellung scheitert. - Dienststart: Migration → Schlüssel → Anbindungen wiederherstellen → Scheduler; beim Beenden werden Mounts nicht ausgehängt. +- Weboberfläche (FastAPI, Jinja2, HTMX ohne Build-Schritt, handyfähig, Hell/Dunkel): + Status, Anbindungen, Regeln, Protokoll, Einstellungen. +- Ersteinrichtung mit Pflicht-Passwort (≥ 10 Zeichen) und einmaligem + Einrichtungscode aus dem Log; Anmeldung mit Argon2, Sperre nach 5 Fehlversuchen. +- Session-Cookie `HttpOnly`, `SameSite=Lax` (optional `Secure`), CSRF-Token bei + jedem POST, strikte Content-Security-Policy ohne Inline-Skripte/-Styles. +- Mount-Formular mit „Testen“ vor dem Speichern; Passwörter werden nie + angezeigt (leer = unverändert); SFTP-Schlüsselpaar wird erzeugt und der + öffentliche Schlüssel zum Kopieren angezeigt. +- Probelauf zeigt in einer Tabelle, welche Datei wohin ginge. +- Protokoll filterbar nach Regel und Status, Einzeldateien aufklappbar. +- Export/Import der Konfiguration als JSON (ohne Zugangsdaten und Webhook-URL). +- `GET /healthz`, `netmover serve`, `netmover reset-password`. ### Behoben +- Import-Upload wurde nie erkannt (falsche `UploadFile`-Klasse geprüft). +- Hinweise nach Aktionen gingen verloren, wenn schon ein Hinweis in der + Session stand (Session erkennt nur Neuzuweisungen als Änderung). - Löschen von Zugangsdaten, die noch von einem Mount benutzt werden, trennt den Mount nicht mehr stillschweigend ab, sondern wird von der Datenbank abgelehnt. diff --git a/DEVLOG.md b/DEVLOG.md index 7a5ca7e..70c9236 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -11,10 +11,44 @@ Zeiten in Ortszeit (Europe/Berlin). | 2026-10-10 | 18:35 | 18:48 | 0:13 | 2 | Mount-Schicht, Validierung, SFTP | | 2026-10-10 | 18:53 | 19:07 | 0:14 | 3 | Mover-Logik | | 2026-10-10 | 19:10 | 19:17 | 0:07 | 4 | Scheduler, Protokoll, Benachrichtigung | -| **Summe** | | | **0:40** | | | +| 2026-10-10 | 19:18 | 19:34 | 0:16 | 5 | Weboberfläche und Anmeldung | +| **Summe** | | | **0:56** | | | --- +## 2026-10-10 · 19:18–19:34 · Schritt 5: Weboberfläche und Anmeldung + +**Erledigt** +- `admin.py` (Formulare prüfen/speichern, frameworkunabhängig), `transfer.py` + (Export/Import), `web/auth.py`, `web/app.py`, 14 Templates, `app.css`, `app.js`. +- htmx 2.0.4 lokal eingebunden (SHA-256 + `e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447`). +- 25 Web-Tests (232 gesamt); Rauchtest mit echtem `netmover serve`. + +**Entscheidungen** +- Einmaliger Einrichtungscode im Log, damit nicht irgendwer im LAN beim ersten + Aufruf das Admin-Passwort setzt. +- Strikte CSP (`script-src 'self'; style-src 'self'`) – deshalb keine + Inline-Styles; Füllstand als ``; htmx-Indikator-Styles abgeschaltet. +- Jede Anbindung pflegt eigene Zugangsdaten (Datenmodell erlaubt Teilen, die + Oberfläche bleibt dadurch einfacher). +- Verbindungsdaten nur im ausgehängten Zustand änderbar; SFTP-Hostschlüssel wird + bei Server-/Portwechsel verworfen. +- „Jetzt ausführen“ startet im Hintergrund (Ergebnis im Protokoll); „Probelauf“ + wartet und zeigt die Tabelle. +- Webhook-URL wird nur gekürzt angezeigt; unverändert zurückgeschickte Maske = behalten. +- Import: vorhandene Namen überspringen, importierte Regeln deaktiviert, + CIFS-Anbindungen ohne `auto_mount` (Zugangsdaten fehlen). +- APScheduler-Logs auf WARNING (sonst alle 30 s eine Zeile pro Job). + +**Stolpersteine** +- Starlette-Session erkennt `setdefault(...).append()` nicht als Änderung → + zweite Flash-Meldung ging verloren. Jetzt Neuzuweisung. +- `request.form()` liefert `starlette.datastructures.UploadFile`, nicht die + FastAPI-Unterklasse → Import erkannte nie eine Datei. +- Settings-Import öffnete eine zweite DB-Verbindung in offener Schreib- + transaktion (SQLite: „database is locked“) → Einstellungen nach dem Commit. + ## 2026-10-10 · 19:10–19:17 · Schritt 4: Scheduler und Protokollierung **Erledigt** diff --git a/README.md b/README.md index 7ec2d39..4b16a4b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Selbst gehosteter Dienst mit Web-Oberfläche, der SMB/CIFS- und NFS-Freigaben verwaltet und Dateien zeitgesteuert zwischen ihnen verschiebt. -> Status: in Entwicklung (Schritt 4 von 6). Vollständige Doku folgt mit Schritt 6. +> Status: in Entwicklung (Schritt 5 von 6). Vollständige Doku folgt mit Schritt 6. ## Entwicklung diff --git a/src/netmover/cli.py b/src/netmover/cli.py index 5ae39b6..b7fe7de 100644 --- a/src/netmover/cli.py +++ b/src/netmover/cli.py @@ -1,7 +1,5 @@ -"""Kommandozeile: ``netmover migrate`` / ``netmover version``. - -``netmover serve`` folgt mit dem Web-Teil (Schritt 5). -""" +"""Kommandozeile: ``netmover serve`` / ``netmover migrate`` / ``netmover version`` +/ ``netmover reset-password``.""" from __future__ import annotations @@ -22,6 +20,11 @@ def main(argv: Sequence[str] | None = None) -> int: sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("version", help="Version ausgeben") sub.add_parser("migrate", help="Datenbank auf den neuesten Stand bringen") + sub.add_parser("serve", help="Dienst mit Weboberfläche starten") + sub.add_parser( + "reset-password", + help="Administratorpasswort löschen – beim nächsten Aufruf startet die Einrichtung", + ) args = parser.parse_args(argv) settings = get_settings() @@ -35,6 +38,36 @@ def main(argv: Sequence[str] | None = None) -> int: upgrade(settings.effective_database_url) event(log, "db.migrated", url=settings.effective_database_url) return 0 + if args.cmd == "serve": + import uvicorn + + from netmover.web.app import create_app + + uvicorn.run( + create_app(settings), + host=settings.host, + port=settings.port, + log_config=None, # unser strukturiertes Logging + proxy_headers=True, + forwarded_allow_ips=settings.forwarded_allow_ips, + server_header=False, + ) + return 0 + if args.cmd == "reset-password": + from netmover.db import make_engine, make_session_factory + from netmover.models import Setting + from netmover.web.auth import PASSWORD_KEY + + upgrade(settings.effective_database_url) + engine = make_engine(settings.effective_database_url) + with make_session_factory(engine)() as s: + row = s.get(Setting, PASSWORD_KEY) + if row is not None: + s.delete(row) + s.commit() + engine.dispose() + print("Passwort gelöscht. Dienst neu starten; der Einrichtungscode steht im Log.") + return 0 return 2 diff --git a/src/netmover/logging_setup.py b/src/netmover/logging_setup.py index e60c63a..f923c21 100644 --- a/src/netmover/logging_setup.py +++ b/src/netmover/logging_setup.py @@ -62,6 +62,9 @@ def setup_logging(level: str = "INFO", fmt: str = "logfmt") -> None: lg = logging.getLogger(name) lg.handlers[:] = [] lg.propagate = True + # APScheduler meldet jeden Job-Start auf INFO – bei 30-s-Intervallen nur Rauschen. + if level != "DEBUG": + logging.getLogger("apscheduler").setLevel(logging.WARNING) def get_logger(name: str) -> logging.Logger: