From 93325b84d85b35cd84648318c9f215bbbf06a0de Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sun, 11 Oct 2026 00:47:02 +0200 Subject: [PATCH] feat(web): Wartungsmodus, Wartezustand und E-Mail-Einstellungen; Doku 0.4.0 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 ++++++- DEVLOG.md | 30 ++++++++- ROADMAP.md | 13 ++-- compose.yml | 2 +- docs/ANLEITUNG.md | 59 +++++++++++++++-- pyproject.toml | 1 + src/netmover/__init__.py | 2 +- src/netmover/app_state.py | 3 +- src/netmover/settings_store.py | 11 ++++ src/netmover/web/app.py | 66 +++++++++++++++++++ src/netmover/web/static/app.css | 1 + src/netmover/web/templates/base.html | 1 + src/netmover/web/templates/macros.html | 4 +- .../web/templates/partials/status_body.html | 8 ++- src/netmover/web/templates/rules.html | 2 + src/netmover/web/templates/settings.html | 27 ++++++-- src/netmover/web/templates/status.html | 9 ++- 17 files changed, 233 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3cf23..530b599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ Die Version steht an genau einer Stelle: `src/netmover/__init__.py`. ## [Unreleased] +## [0.4.0] – 2026-10-11 + +### Hinzugefügt +- **Automatisch pausieren und fortsetzen:** Ist Quelle oder Ziel nicht erreichbar, + wartet die Regel („wartet seit …“ mit Grund) – ein Protokolleintrag statt eines + Fehlers alle 30 s, „Fehler in Folge“ zählt nicht hoch. Sobald die Anbindung zurück + ist, läuft die Regel von selbst weiter („Fortgesetzt nach … Wartezeit“). + Migration 0003: `rules.waiting_since`, `rules.waiting_reason`. +- **Wartungsmodus** (Knopf auf der Statusseite): alle geplanten Läufe pausiert, + „Jetzt ausführen“ geht weiter, kein Alarm für Regeln in `/healthz`; gelber Hinweis + in der Kopfzeile. +- **E-Mail-Benachrichtigung** per SMTP (STARTTLS, SSL/TLS oder ohne für interne + Relays; Zertifikate werden geprüft), Passwort verschlüsselt, mehrere Empfänger, + Test-Knopf. Webhook und E-Mail lassen sich gleichzeitig nutzen. + +### Geändert +- Ausfälle von Anbindungen werden **einmal pro Anbindung** gemeldet, mit der Liste + der betroffenen Regeln, und mit Entwarnung, wenn sie zurück sind – auch wenn eine + Anbindung von außen ausgehängt wurde (vorher keine Meldung). +- Einstellungen neu gegliedert: Benachrichtigungen · Webhook · E-Mail · Überwachung. +- `/healthz`: Eine wartende Regel ist ein Problem (mit Grund); im Wartungsmodus nicht. + ## [0.3.0] – 2026-10-11 ### Hinzugefügt @@ -191,7 +213,8 @@ Erste Version. - Löschen von Zugangsdaten, die noch von einem Mount benutzt werden, trennt den Mount nicht mehr stillschweigend ab, sondern wird von der Datenbank abgelehnt. -[Unreleased]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.3.0...HEAD +[Unreleased]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.4.0...HEAD +[0.4.0]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.3.0...v0.4.0 [0.3.0]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.2.1...v0.3.0 [0.2.1]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.2.0...v0.2.1 [0.2.0]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.1.1...v0.2.0 diff --git a/DEVLOG.md b/DEVLOG.md index 95a61a8..c655fdb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -17,10 +17,38 @@ Zeiten in Ortszeit (Europe/Berlin). | 2026-10-10 | 22:45 | 23:00 | 0:15 | – | Echtes NAS/Paperless anbinden | | 2026-10-10 | 23:31 | 23:58 | 0:27 | – | Schlüsselverwaltung, Update-Skript, 0.2.x | | 2026-10-11 | 00:13 | 00:32 | 0:19 | – | Menü, Anleitung, Roadmap, Uptime Kuma (0.3.0) | -| **Summe** | | | **2:36** | | | +| 2026-10-11 | 00:31 | 00:50 | 0:19 | – | Pausieren, Wartungsmodus, E-Mail (0.4.0) | +| **Summe** | | | **2:55** | | | --- +## 2026-10-11 · 00:31–00:50 · Pausieren, Wartungsmodus, E-Mail (0.4.0) + +**Erledigt** +- Wartezustand (`rules.waiting_since/_reason`, Migration 0003), Wartungsmodus, + E-Mail (`mail.py`, SMTP), eine Meldung pro Anbindung mit betroffenen Regeln. +- 0.4.0/0.5.0 aufgeteilt: kleinere Updates, schneller Nutzen. +- 21 neue Tests (299 gesamt), u. a. SMTP gegen echten Server (aiosmtpd) mit + STARTTLS, SSL, falschem Passwort und nicht vertrauenswürdigem Zertifikat. + +**Entscheidungen** +- Warten statt Deaktivieren; kein Fehlerzähler und nur ein Protokolleintrag beim + Eintritt; Ende des Wartens automatisch beim nächsten durchkommenden Lauf. +- Benachrichtigung pro Anbindung (nicht pro Regel) – sonst bei drei Regeln am NAS + drei gleiche Meldungen. Lücke geschlossen: Aushängen von außen meldete nichts. +- Wartungsmodus unterdrückt nur Regel-Alarme, Anbindungen werden weiter geprüft. +- SMTP prüft Zertifikate immer; für Tests ist nur `mail.tls_context()` ersetzbar. + +**Stolpersteine (alle im Testaufbau)** +- Globales Monkeypatchen von `ssl.create_default_context` traf auch den + Testserver → eigene Funktion `tls_context()`. +- aiosmtpd: `AuthResult(success=False)` ohne `handled=False` schickt keine Antwort → + Client hängt bis zum Zeitlimit. + +**Migrationen** – 0003 baut `rules` neu; getestet von 0001 und von 0002 mit allen +verknüpften Tabellen sowie an der befüllten Demo-Datenbank; vor dem Release an einer +Kopie der Live-Datenbank auf der VM. + ## 2026-10-11 · 00:13–00:32 · Menü, Anleitung, Roadmap, Uptime Kuma (0.3.0) **Erledigt** diff --git a/ROADMAP.md b/ROADMAP.md index c3413db..c01340c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,18 +6,12 @@ stehen mit Version im [CHANGELOG](CHANGELOG.md); Details zu Entscheidungen im Legende: ✅ erledigt · 🔨 in Arbeit · 📋 geplant · 💡 Idee (noch nicht entschieden) -## In Arbeit - -Gerade nichts – als Nächstes kommt 0.4.0. - -## Als Nächstes (0.4.0) +## In Arbeit (0.5.0) | Status | Thema | Warum | |---|---|---| -| 📋 | **SFTP als Quelle**: Scan, Stabilitätsprüfung und Löschen der Quelle über SFTP, mit denselben Garantien wie bei SMB/NFS | Quellen, die nur per SSH erreichbar sind (Wunsch aus dem Praxisbetrieb) | -| 📋 | **Gespeicherte Zugangsdaten** für SMB und SFTP-Passwörter, zusammen mit den SSH-Schlüsseln auf einer Seite „Zugangsdaten“ | Mehrere Freigaben mit demselben NAS-Benutzer an einer Stelle pflegen | -| 📋 | **Automatisch pausieren und fortsetzen**: Ist Quelle oder Ziel nicht erreichbar, wechselt die Regel in „wartet auf Anbindung X“ – ein Protokolleintrag und eine Benachrichtigung statt eines Fehlers alle 30 s; fortsetzen von selbst, sobald die Anbindung zurück ist. Dazu manuelles Pausieren (Wartungsmodus) | Weniger Rauschen im Protokoll und in Benachrichtigungen, kein Handgriff nach NAS-Neustarts | -| 📋 | **E-Mail-Benachrichtigung** (SMTP mit STARTTLS/SSL, verschlüsseltes Passwort, mehrere Empfänger) zusätzlich zum Webhook | Benachrichtigung ohne eigenen Webhook-Dienst | +| 🔨 | **SFTP als Quelle**: Scan, Stabilitätsprüfung und Löschen der Quelle über SFTP, mit denselben Garantien wie bei SMB/NFS | Quellen, die nur per SSH erreichbar sind | +| 🔨 | **Gespeicherte Zugangsdaten** für SMB und SFTP-Passwörter, zusammen mit den SSH-Schlüsseln auf einer Seite „Zugangsdaten“ | Mehrere Freigaben mit demselben NAS-Benutzer an einer Stelle pflegen | ## Geplant @@ -41,6 +35,7 @@ Gerade nichts – als Nächstes kommt 0.4.0. | Version | Thema | |---|---| +| 0.4.0 | Automatisch pausieren/fortsetzen bei nicht erreichbaren Anbindungen; Wartungsmodus; E-Mail-Benachrichtigung; eine Meldung pro ausgefallener Anbindung | | 0.3.0 | Neues Menü (gruppierte Seitenleiste, Statusampel, Handy-Menü); Anleitung im Repo und als Hilfe-Seite; Roadmap; Uptime Kuma (Einzel-Endpunkte, Push-Monitor) | | 0.2.1 | Migrationen in einer Transaktion und ohne Fremdschlüsselfehler auf befüllten Datenbanken | | 0.2.0 | Schlüsselverwaltung für SFTP; `netmover-update` mit Sicherung; Installation als Git-Klon | diff --git a/compose.yml b/compose.yml index 7688396..bbf4b16 100644 --- a/compose.yml +++ b/compose.yml @@ -4,7 +4,7 @@ services: netmover: build: . - image: netmover:0.3.0 + image: netmover:0.4.0 container_name: netmover restart: unless-stopped cap_add: diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md index 3c1c1fb..c2bcd66 100644 --- a/docs/ANLEITUNG.md +++ b/docs/ANLEITUNG.md @@ -9,7 +9,7 @@ stehen im [README](../README.md). **Inhalt:** [Begriffe](#begriffe) · [Erste Anmeldung](#erste-anmeldung) · [Anbindungen](#anbindungen) · [Schlüssel](#schluessel) · [Regeln](#regeln) · -[Probelauf](#probelauf) · [Status](#status) · [Protokoll](#protokoll) · +[Probelauf](#probelauf) · [Status](#status) · [Wartungsmodus](#wartungsmodus) · [Protokoll](#protokoll) · [Einstellungen](#einstellungen) · [Uptime Kuma](#uptime-kuma) · [Beispiele](#beispiele) · [Häufige Fragen](#fragen) ## Die vier Begriffe {#begriffe} @@ -99,6 +99,8 @@ Server neu aufgesetzt: bei der Anbindung **Hostschlüssel zurücksetzen** und ne Regeln, deren Anbindung nicht verfügbar ist, **brechen ab, ohne etwas anzufassen**. Es wird nie in ein leeres Verzeichnis geschrieben, nur weil die Freigabe fehlt. +Solche Regeln **warten** automatisch, bis die Anbindung zurück ist (siehe +[Status](#status)), und laufen dann von selbst weiter. ## Schlüssel {#schluessel} @@ -202,6 +204,28 @@ Eine Regel wird **gelb**, wenn ihr letzter erfolgreicher Lauf länger als das Dreifache ihres Intervalls zurückliegt. So fällt auf, wenn NetMover still ausfällt – statt dass es aussieht, als gäbe es nichts zu tun. +### Regel wartet auf eine Anbindung {#wartet} + +Ist die Quelle oder das Ziel einer Regel nicht erreichbar (NAS neu gestartet, Server +aus), **pausiert die Regel automatisch**: Sie zeigt „wartet seit …“ mit dem Grund. + +- Im Protokoll steht **ein** Eintrag „Pausiert, bis die Anbindung zurück ist“ – nicht + alle 30 Sekunden ein neuer Fehler. +- Es kommt **eine** Benachrichtigung pro Anbindung, mit der Liste der betroffenen + Regeln. +- Sobald die Anbindung zurück ist, **läuft die Regel von selbst weiter**; der nächste + Lauf vermerkt „Fortgesetzt nach … Wartezeit“, und es kommt eine Entwarnung. + +Man muss also nach einem NAS-Neustart nichts tun. + +### Wartungsmodus {#wartungsmodus} + +Der Knopf **Wartungsmodus** auf der Statusseite pausiert **alle geplanten Läufe** – +etwa während eines NAS-Updates. Oben erscheint dann gelb „Wartungsmodus“. „Jetzt +ausführen“ geht weiterhin. Für Regeln meldet `/healthz` in dieser Zeit keinen Alarm +(Anbindungen werden weiter geprüft). Mit **Wartungsmodus beenden** läuft alles +wieder nach Plan. + ## Protokoll {#protokoll} *Automatisierung → Protokoll* zeigt jeden Lauf: Zeitpunkt, Dauer, gesehen / @@ -218,7 +242,8 @@ Häufige Einträge: | *Quelle hat sich während der Übertragung geändert* | Abgebrochen, nichts gelöscht; nächster Lauf versucht es erneut | | *n bereits übertragen* | Kopiermodus: diese Dateiversionen sind schon kopiert | | *Termin übersprungen – voriger Lauf läuft noch* | Ein Lauf dauert länger als das Intervall | -| *nicht eingehängt – Lauf abgebrochen, nichts angefasst* | Anbindung fehlt; siehe [Zustände](#anbindung-zustaende) | +| *Pausiert, bis die Anbindung zurück ist* | Quelle oder Ziel nicht erreichbar; die Regel wartet, siehe [Regel wartet](#wartet) | +| *Fortgesetzt nach … Wartezeit* | Die Anbindung ist zurück, die Regel läuft wieder | Protokolle werden nach der eingestellten Frist gelöscht (Standard 30 Tage). @@ -228,15 +253,31 @@ Protokolle werden nach der eingestellten Frist gelöscht (Standard 30 Tage). ### Benachrichtigungen {#benachrichtigungen} -NetMover kann bei Fehlern eine Nachricht an einen **Webhook** schicken – z. B. an -ntfy, Gotify oder Home Assistant. +NetMover meldet Probleme per **E-Mail** und/oder an einen **Webhook** (z. B. ntfy, +Gotify, Home Assistant). Beides lässt sich gleichzeitig nutzen. + +| Feld | Bedeutung | +|---|---| +| **Benachrichtigen** | nie / bei jedem fehlgeschlagenen Lauf / erst nach N Fehlschlägen in Folge. Fällt eine **Anbindung** aus, kommt die Meldung immer sofort (einmal, mit den betroffenen Regeln). | +| **Entwarnung** | Nachricht, wenn eine Regel oder Anbindung wieder funktioniert | + +**E-Mail:** + +| Feld | Bedeutung | +|---|---| +| **SMTP-Server, Port, Verschlüsselung** | z. B. `smtp.example.org`, `587` mit STARTTLS oder `465` mit SSL/TLS. „keine“ nur für interne Relays. Zertifikate werden immer geprüft. | +| **Benutzer, Passwort** | Anmeldung am SMTP-Server; leer = ohne Anmeldung. Das Passwort wird verschlüsselt gespeichert und nie angezeigt. | +| **Absender, Empfänger** | Empfänger mit Komma getrennt | + +*Test-E-Mail senden* prüft die gespeicherten Einstellungen und zeigt bei Fehlern die +Antwort des Servers. + +**Webhook:** | Feld | Bedeutung | |---|---| -| **Benachrichtigen** | nie / bei jedem fehlgeschlagenen Lauf / erst nach N Fehlschlägen in Folge | | **Webhook-URL** | Adresse des Empfängers. Wird nur gekürzt angezeigt, weil sie oft ein Token enthält. | | **Content-Type, Vorlage** | Format der Nachricht; Platzhalter `$title`, `$message`, `$rule`, `$event`, `$time`, `$host` | -| **Entwarnung** | Nachricht, wenn eine Regel oder Anbindung wieder funktioniert | Beispiele: ntfy → URL `https://ntfy.sh/`, Content-Type `text/plain`, Vorlage `$title: $message`. Gotify → URL `https://gotify.lan/message?token=…`, Content-Type @@ -315,7 +356,11 @@ bei regulären Ausdrücken nicht), Datei wird noch geschrieben oder ist größer **Die Anbindung zeigt „Fehler“.** – Die Meldung darunter nennt die Ursache und meist die Abhilfe. Ist das NAS nur neu gestartet, hängt NetMover die Freigabe innerhalb -einer Minute selbst wieder ein. +einer Minute selbst wieder ein; die Regeln warten so lange und laufen dann weiter. + +**Ich mache gleich ein NAS-Update.** – Vorher auf der Statusseite *Wartungsmodus* +einschalten, danach wieder beenden. Nötig ist es nicht (die Regeln warten ohnehin +von selbst), aber so gibt es keine Alarme. **„mount error(95): Operation not supported“** – NAS und NetMover finden keine gemeinsame SMB-Version. Am NAS das maximale SMB-Protokoll auf SMB3 stellen diff --git a/pyproject.toml b/pyproject.toml index ec9dafa..2ab009b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dev = [ "types-paramiko", "types-Markdown", "shellcheck-py", + "aiosmtpd", ] [project.scripts] diff --git a/src/netmover/__init__.py b/src/netmover/__init__.py index b2455a9..b5b33d7 100644 --- a/src/netmover/__init__.py +++ b/src/netmover/__init__.py @@ -1,3 +1,3 @@ """NetMover – NFS/SMB-Mounts verwalten und Dateien zeitgesteuert verschieben.""" -__version__ = "0.3.0" +__version__ = "0.4.0" diff --git a/src/netmover/app_state.py b/src/netmover/app_state.py index f00b1dc..1f59756 100644 --- a/src/netmover/app_state.py +++ b/src/netmover/app_state.py @@ -50,7 +50,7 @@ class NetMover: self.session_factory = make_session_factory(self.db_engine) self.secrets = SecretBox(load_or_create_key(settings.secret_key_path)) self.backend = backend or make_backend(settings) - self.notifier = notifier or Notifier(self.session_factory) + self.notifier = notifier or Notifier(self.session_factory, secrets=self.secrets) self.mounts = MountManager( self.session_factory, self.backend, @@ -93,4 +93,5 @@ class NetMover: scheduler_running=self.scheduler.running, last_heartbeat=self.scheduler.last_heartbeat, service_started=self.started_at, + maintenance=load_settings(self.session_factory).maintenance, ) diff --git a/src/netmover/settings_store.py b/src/netmover/settings_store.py index d753784..a096015 100644 --- a/src/netmover/settings_store.py +++ b/src/netmover/settings_store.py @@ -35,6 +35,17 @@ class AppSettings: webhook_template: str = DEFAULT_TEMPLATE webhook_content_type: str = "application/json" kuma_push_url: str = "" + # Wartungsmodus: geplante Läufe pausiert, kein Alarm für Regeln + maintenance: bool = False + # E-Mail (SMTP); Passwort Fernet-verschlüsselt + email_enabled: bool = False + smtp_host: str = "" + smtp_port: int = 587 + smtp_security: str = "starttls" + smtp_user: str = "" + smtp_password_enc: str = "" + smtp_from: str = "" + smtp_to: str = "" _PREFIX = "app." diff --git a/src/netmover/web/app.py b/src/netmover/web/app.py index 5ae04d1..7ecf0cb 100644 --- a/src/netmover/web/app.py +++ b/src/netmover/web/app.py @@ -27,6 +27,7 @@ from fastapi import APIRouter, Depends, FastAPI, Request from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from markupsafe import escape from sqlalchemy import func, or_, select from starlette.concurrency import run_in_threadpool from starlette.datastructures import UploadFile @@ -52,6 +53,7 @@ from netmover.db import session_scope from netmover.keys import delete_key, generate_key, import_key, list_keys, rename_key from netmover.kuma import push as kuma_push from netmover.logging_setup import event, get_logger +from netmover.mail import SECURITY_MODES, MailConfigError, parse_recipients, valid_address from netmover.models import ( CollisionPolicy, Credential, @@ -185,6 +187,7 @@ class NavState: keys: int rules: int rules_active: int + maintenance: bool = False def _nav_state(request: Request) -> NavState | None: @@ -208,6 +211,7 @@ def _nav_state(request: Request) -> NavState | None: keys=keys or 0, rules=len(rules), rules_active=sum(1 for (en,) in rules if en), + maintenance=health.maintenance, ) @@ -459,6 +463,23 @@ async def status_partial(request: Request) -> Response: return render(request, "partials/status_body.html", **await _status_context(request)) +@protected.post("/maintenance") +async def toggle_maintenance(request: Request) -> Response: + state = nm(request) + cfg = load_settings(state.session_factory) + cfg.maintenance = not cfg.maintenance + save_settings(state.session_factory, cfg) + event(log, "maintenance", enabled=cfg.maintenance) + flash( + request, + "Wartungsmodus eingeschaltet – geplante Läufe sind pausiert." + if cfg.maintenance + else "Wartungsmodus beendet – Regeln laufen wieder nach Plan.", + "info", + ) + return redirect("/") + + @protected.post("/rules/{rule_id}/run") async def rule_run_now(request: Request, rule_id: int) -> Response: state = nm(request) @@ -1000,6 +1021,32 @@ async def settings_save(request: Request) -> Response: if url and not url.startswith(("http://", "https://")): errors["webhook_url"] = "URL muss mit http:// oder https:// beginnen." cfg.webhook_url = url + cfg.email_enabled = form.get("email_enabled") == "on" + cfg.smtp_host = (form.get("smtp_host") or "").strip() + cfg.smtp_user = (form.get("smtp_user") or "").strip() + cfg.smtp_from = (form.get("smtp_from") or "").strip() + cfg.smtp_to = (form.get("smtp_to") or "").strip() + cfg.smtp_security = form.get("smtp_security") or "starttls" + try: + cfg.smtp_port = int(form.get("smtp_port") or 587) + if not 1 <= cfg.smtp_port <= 65535: + raise ValueError + except ValueError: + errors["smtp_port"] = "Port zwischen 1 und 65535." + if cfg.smtp_security not in SECURITY_MODES: + errors["smtp_security"] = "Ungültig." + if cfg.smtp_host and not re.fullmatch(r"[A-Za-z0-9.-]+", cfg.smtp_host): + errors["smtp_host"] = "Hostname oder IP-Adresse." + if cfg.smtp_from and not valid_address(cfg.smtp_from): + errors["smtp_from"] = "Keine gültige E-Mail-Adresse." + try: + parse_recipients(cfg.smtp_to) + except MailConfigError as exc: + errors["smtp_to"] = str(exc) + if cfg.email_enabled and not (cfg.smtp_host and cfg.smtp_from and cfg.smtp_to): + errors["smtp_host"] = "Für E-Mail werden Server, Absender und Empfänger benötigt." + if form.get("smtp_password"): + cfg.smtp_password_enc = state.secrets.encrypt(form["smtp_password"]) kuma = (form.get("kuma_push_url") or "").strip() if kuma != auth_mask(cfg.kuma_push_url): if kuma and not kuma.startswith(("http://", "https://")): @@ -1050,6 +1097,25 @@ async def settings_webhook_test(request: Request) -> Response: return HTMLResponse('

Testnachricht gesendet.

') +@protected.post("/settings/mail-test", response_class=HTMLResponse) +async def settings_mail_test(request: Request) -> Response: + state = nm(request) + cfg = load_settings(state.session_factory) + if not (cfg.smtp_host and cfg.smtp_from and cfg.smtp_to): + return HTMLResponse('

Server, Absender und Empfänger speichern.

') + try: + await run_in_threadpool( + state.notifier.send_mail_now, + "NetMover: Testnachricht", + "Die E-Mail-Benachrichtigung funktioniert.", + cfg, + ) + except Exception as exc: + detail = escape(str(exc))[:300] or type(exc).__name__ + return HTMLResponse(f'

Fehlgeschlagen: {detail}

') + return HTMLResponse(f'

Gesendet an {escape(cfg.smtp_to)}.

') + + @protected.post("/settings/kuma-test", response_class=HTMLResponse) async def settings_kuma_test(request: Request) -> Response: state = nm(request) diff --git a/src/netmover/web/static/app.css b/src/netmover/web/static/app.css index b4594f1..c846de4 100644 --- a/src/netmover/web/static/app.css +++ b/src/netmover/web/static/app.css @@ -30,6 +30,7 @@ pre { white-space: pre-wrap; word-break: break-word; margin: 0; } .health .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; } .health.ok { color: var(--ok); background: var(--ok-bg); } .health.bad { color: var(--err); background: var(--err-bg); } +.health.warn { color: var(--warn); background: var(--warn-bg); } .help-link { display: inline-flex; align-items: center; gap: 6px; font-size: .9em; text-decoration: none; } .menu-toggle { display: none; align-items: center; gap: 6px; } .ico { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; diff --git a/src/netmover/web/templates/base.html b/src/netmover/web/templates/base.html index 7863c24..32554dd 100644 --- a/src/netmover/web/templates/base.html +++ b/src/netmover/web/templates/base.html @@ -23,6 +23,7 @@ {% if nav %} {{ 'Alles in Ordnung' if nav.healthy else (nav.problems ~ (' Problem' if nav.problems == 1 else ' Probleme')) }} + {% if nav.maintenance %}Wartungsmodus{% endif %} {% endif %} {% if help_anchor %}{{ icon('help') }}Hilfe zu dieser Seite{% endif %} diff --git a/src/netmover/web/templates/macros.html b/src/netmover/web/templates/macros.html index edc0766..7817631 100644 --- a/src/netmover/web/templates/macros.html +++ b/src/netmover/web/templates/macros.html @@ -21,8 +21,8 @@ {%- endmacro %} -{% macro check(name, label, checked, hint='') -%} -
+{% macro check(name, label, checked, hint='', wide=false) -%} +
{% if hint %}{{ hint }}{% endif %} diff --git a/src/netmover/web/templates/partials/status_body.html b/src/netmover/web/templates/partials/status_body.html index 8b0310c..534ad7d 100644 --- a/src/netmover/web/templates/partials/status_body.html +++ b/src/netmover/web/templates/partials/status_body.html @@ -33,17 +33,19 @@ RegelLetztes ErgebnisLetzter ErfolgNächster Lauf {% for v in rules %}{% set r = v.r %} - + {{ r.name }}{% if not r.enabled %} aus{% endif %}{% if r.dry_run %} Probelauf{% endif %}
{{ v.target }}
- {% if v.running_since %}läuft seit {{ v.running_since|ago|replace('vor ', '') }} + {% if r.waiting_since %}wartet seit {{ r.waiting_since|ago|replace('vor ', '') }} +
{{ r.waiting_reason }}
+ {% elif v.running_since %}läuft seit {{ v.running_since|ago|replace('vor ', '') }} {% elif v.last_run %}{{ run_badge(v.last_run.status) }} {{ v.last_run.finished_at|ago }}{% if v.last_run.seen %} · {{ v.last_run.moved }}/{{ v.last_run.seen }}{% endif %} {% if v.last_run.status in (RunStatus.ERROR, RunStatus.PARTIAL) and v.last_run.message %}
{{ v.last_run.message }}
{% endif %} {% else %}–{% endif %} {{ r.last_success_at|ago }}{% if v.stale %}
länger als 3 × Intervall!
{% endif %} - {% if r.consecutive_failures %}
{{ r.consecutive_failures }} Fehler in Folge
{% endif %} + {% if r.consecutive_failures and not r.waiting_since %}
{{ r.consecutive_failures }} Fehler in Folge
{% endif %} {{ v.next_run|ago if v.next_run else '–' }}
diff --git a/src/netmover/web/templates/rules.html b/src/netmover/web/templates/rules.html index 6c91ebc..87b46c1 100644 --- a/src/netmover/web/templates/rules.html +++ b/src/netmover/web/templates/rules.html @@ -22,6 +22,8 @@
{{ v.target }} · Filter {{ r.pattern }}{{ ' (rekursiv)' if r.recursive }}
+ {% if r.waiting_since %}

Wartet seit {{ r.waiting_since|localdt }} ({{ r.waiting_since|ago }}) – {{ r.waiting_reason }} + Die Regel läuft von selbst weiter, sobald die Anbindung zurück ist.

{% endif %}
Letzter Lauf: {% if v.running_since %}läuft seit {{ v.running_since|ago|replace('vor ', '') }}{% elif v.last_run %}{{ run_badge(v.last_run.status) }} {{ v.last_run.finished_at|ago }}{% else %}–{% endif %} · letzter Erfolg: {{ r.last_success_at|ago }} · nächster: {{ v.next_run|ago if v.next_run else '–' }}
diff --git a/src/netmover/web/templates/settings.html b/src/netmover/web/templates/settings.html index e4ba62f..f7a52a7 100644 --- a/src/netmover/web/templates/settings.html +++ b/src/netmover/web/templates/settings.html @@ -8,18 +8,37 @@
Protokoll
{{ field('retention_days', 'Aufbewahrung (Tage)', cfg.retention_days, type='number', errors=errors, attrs='min="1" max="3650"') }}
-
Benachrichtigung (Webhook) ?
- {{ select('notify_mode', 'Benachrichtigen', [('off', 'nie'), ('every', 'bei jedem fehlgeschlagenen Lauf'), ('after_n', 'nach N Fehlschlägen in Folge')], cfg.notify_mode.value, errors=errors) }} +
Benachrichtigungen ?
+ {{ select('notify_mode', 'Benachrichtigen', [('off', 'nie'), ('every', 'bei jedem fehlgeschlagenen Lauf'), ('after_n', 'nach N Fehlschlägen in Folge')], cfg.notify_mode.value, errors=errors, hint='Gilt für Webhook und E-Mail. Ausfälle von Anbindungen werden immer sofort gemeldet (außer bei „nie“).') }}
{{ field('notify_after', 'N', cfg.notify_after, type='number', errors=errors, attrs='min="1"') }}
- {{ field('webhook_url', 'Webhook-URL', mask_url(cfg.webhook_url), errors=errors, wide=true, placeholder='https://ntfy.sh/mein-topic', hint='Wird nur gekürzt angezeigt; unverändert lassen = behalten, leeren = entfernen.') }} + {{ check('notify_recovery', 'Entwarnung senden, wenn es wieder läuft', cfg.notify_recovery, wide=true) }} +
+ +
Webhook
+ {{ field('webhook_url', 'Webhook-URL', mask_url(cfg.webhook_url), errors=errors, wide=true, placeholder='https://ntfy.sh/mein-topic', hint='Leer = kein Webhook. Wird nur gekürzt angezeigt; unverändert lassen = behalten.') }} {{ field('webhook_content_type', 'Content-Type', cfg.webhook_content_type, errors=errors) }} - {{ check('notify_recovery', 'Entwarnung senden, wenn es wieder läuft', cfg.notify_recovery) }}
Platzhalter: $event $title $message $rule $time $host – bei JSON automatisch maskiert.
+ +
E-Mail
+ {{ check('email_enabled', 'E-Mail-Benachrichtigung einschalten', cfg.email_enabled, wide=true) }} + {{ field('smtp_host', 'SMTP-Server', cfg.smtp_host, errors=errors, placeholder='smtp.example.org') }} + {{ field('smtp_port', 'Port', cfg.smtp_port, type='number', errors=errors, attrs='min="1" max="65535"', hint='587 (STARTTLS) oder 465 (SSL)') }} + {{ select('smtp_security', 'Verschlüsselung', [('starttls', 'STARTTLS'), ('ssl', 'SSL/TLS'), ('none', 'keine (nur interne Relays)')], cfg.smtp_security, errors=errors) }} + {{ field('smtp_user', 'Benutzer', cfg.smtp_user, errors=errors, attrs='autocomplete="off"', hint='leer = ohne Anmeldung') }} +
+ + {{ 'Leer lassen = unverändert.' if cfg.smtp_password_enc else '' }} Wird verschlüsselt gespeichert.
+ {{ field('smtp_from', 'Absender', cfg.smtp_from, errors=errors, placeholder='netmover@example.org') }} + {{ field('smtp_to', 'Empfänger', cfg.smtp_to, errors=errors, wide=true, placeholder='ich@example.org, admin@example.org', hint='mehrere mit Komma getrennt') }} +
+
+
+
Überwachung (Uptime Kuma) ?
{{ field('kuma_push_url', 'Push-URL (Kuma-Monitor vom Typ „Push“)', mask_url(cfg.kuma_push_url), errors=errors, wide=true, placeholder='https://kuma.lan/api/push/AbCdEf?status=up&msg=OK&ping=', hint='NetMover meldet jede Minute den Zustand. Leer = aus. Wird nur gekürzt angezeigt.') }} diff --git a/src/netmover/web/templates/status.html b/src/netmover/web/templates/status.html index f2fee4d..e2e486d 100644 --- a/src/netmover/web/templates/status.html +++ b/src/netmover/web/templates/status.html @@ -2,7 +2,14 @@ {% block title %}Status{% endblock %} {% block content %}

Status

- aktualisiert sich alle 15 s
+
aktualisiert sich alle 15 s + + + +
+{% if nav and nav.maintenance %}

Wartungsmodus: Geplante Läufe sind pausiert. + „Jetzt ausführen“ geht weiterhin. Für Regeln schlägt /healthz keinen Alarm.

{% endif %}
{% include "partials/status_body.html" %}