feat: netmover serve und reset-password, ruhigere Scheduler-Logs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-10 19:33:00 +02:00
co-authored by Claude Opus 5.5
parent 2b0b039aeb
commit 2770c2e204
5 changed files with 92 additions and 6 deletions
+16
View File
@@ -67,7 +67,23 @@ Die Version steht an genau einer Stelle: `src/netmover/__init__.py`.
auch für Anbindungen, deren Wiederherstellung scheitert. auch für Anbindungen, deren Wiederherstellung scheitert.
- Dienststart: Migration → Schlüssel → Anbindungen wiederherstellen → Scheduler; - Dienststart: Migration → Schlüssel → Anbindungen wiederherstellen → Scheduler;
beim Beenden werden Mounts nicht ausgehängt. 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 ### 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 - Löschen von Zugangsdaten, die noch von einem Mount benutzt werden, trennt den
Mount nicht mehr stillschweigend ab, sondern wird von der Datenbank abgelehnt. Mount nicht mehr stillschweigend ab, sondern wird von der Datenbank abgelehnt.
+35 -1
View File
@@ -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: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 | 18:53 | 19:07 | 0:14 | 3 | Mover-Logik |
| 2026-10-10 | 19:10 | 19:17 | 0:07 | 4 | Scheduler, Protokoll, Benachrichtigung | | 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 `<meter>`; 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 ## 2026-10-10 · 19:10–19:17 · Schritt 4: Scheduler und Protokollierung
**Erledigt** **Erledigt**
+1 -1
View File
@@ -3,7 +3,7 @@
Selbst gehosteter Dienst mit Web-Oberfläche, der SMB/CIFS- und NFS-Freigaben Selbst gehosteter Dienst mit Web-Oberfläche, der SMB/CIFS- und NFS-Freigaben
verwaltet und Dateien zeitgesteuert zwischen ihnen verschiebt. 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 ## Entwicklung
+37 -4
View File
@@ -1,7 +1,5 @@
"""Kommandozeile: ``netmover migrate`` / ``netmover version``. """Kommandozeile: ``netmover serve`` / ``netmover migrate`` / ``netmover version``
/ ``netmover reset-password``."""
``netmover serve`` folgt mit dem Web-Teil (Schritt 5).
"""
from __future__ import annotations 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 = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("version", help="Version ausgeben") sub.add_parser("version", help="Version ausgeben")
sub.add_parser("migrate", help="Datenbank auf den neuesten Stand bringen") 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) args = parser.parse_args(argv)
settings = get_settings() settings = get_settings()
@@ -35,6 +38,36 @@ def main(argv: Sequence[str] | None = None) -> int:
upgrade(settings.effective_database_url) upgrade(settings.effective_database_url)
event(log, "db.migrated", url=settings.effective_database_url) event(log, "db.migrated", url=settings.effective_database_url)
return 0 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 return 2
+3
View File
@@ -62,6 +62,9 @@ def setup_logging(level: str = "INFO", fmt: str = "logfmt") -> None:
lg = logging.getLogger(name) lg = logging.getLogger(name)
lg.handlers[:] = [] lg.handlers[:] = []
lg.propagate = True 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: def get_logger(name: str) -> logging.Logger: