feat: netmover serve und reset-password, ruhigere Scheduler-Logs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2b0b039aeb
commit
2770c2e204
@@ -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.
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user