feat(web): Wartungsmodus, Wartezustand und E-Mail-Einstellungen; Doku 0.4.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a8bf4ff67e
commit
5e00686e4f
+24
-1
@@ -8,6 +8,28 @@ Die Version steht an genau einer Stelle: `src/netmover/__init__.py`.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.3.0] – 2026-10-11
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
@@ -191,7 +213,8 @@ Erste Version.
|
|||||||
- 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.
|
||||||
|
|
||||||
[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.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.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
|
[0.2.0]: https://git.wilde-server.de/florian.wilde/netmover/compare/v0.1.1...v0.2.0
|
||||||
|
|||||||
@@ -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 | 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-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) |
|
| 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)
|
## 2026-10-11 · 00:13–00:32 · Menü, Anleitung, Roadmap, Uptime Kuma (0.3.0)
|
||||||
|
|
||||||
**Erledigt**
|
**Erledigt**
|
||||||
|
|||||||
+4
-9
@@ -6,18 +6,12 @@ stehen mit Version im [CHANGELOG](CHANGELOG.md); Details zu Entscheidungen im
|
|||||||
|
|
||||||
Legende: ✅ erledigt · 🔨 in Arbeit · 📋 geplant · 💡 Idee (noch nicht entschieden)
|
Legende: ✅ erledigt · 🔨 in Arbeit · 📋 geplant · 💡 Idee (noch nicht entschieden)
|
||||||
|
|
||||||
## In Arbeit
|
## In Arbeit (0.5.0)
|
||||||
|
|
||||||
Gerade nichts – als Nächstes kommt 0.4.0.
|
|
||||||
|
|
||||||
## Als Nächstes (0.4.0)
|
|
||||||
|
|
||||||
| Status | Thema | Warum |
|
| 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) |
|
| 🔨 | **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 |
|
| 🔨 | **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 |
|
|
||||||
|
|
||||||
## Geplant
|
## Geplant
|
||||||
|
|
||||||
@@ -41,6 +35,7 @@ Gerade nichts – als Nächstes kommt 0.4.0.
|
|||||||
|
|
||||||
| Version | Thema |
|
| 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.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.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 |
|
| 0.2.0 | Schlüsselverwaltung für SFTP; `netmover-update` mit Sicherung; Installation als Git-Klon |
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
services:
|
services:
|
||||||
netmover:
|
netmover:
|
||||||
build: .
|
build: .
|
||||||
image: netmover:0.3.0
|
image: netmover:0.4.0
|
||||||
container_name: netmover
|
container_name: netmover
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|||||||
+52
-7
@@ -9,7 +9,7 @@ stehen im [README](../README.md).
|
|||||||
|
|
||||||
**Inhalt:** [Begriffe](#begriffe) · [Erste Anmeldung](#erste-anmeldung) ·
|
**Inhalt:** [Begriffe](#begriffe) · [Erste Anmeldung](#erste-anmeldung) ·
|
||||||
[Anbindungen](#anbindungen) · [Schlüssel](#schluessel) · [Regeln](#regeln) ·
|
[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)
|
[Einstellungen](#einstellungen) · [Uptime Kuma](#uptime-kuma) · [Beispiele](#beispiele) · [Häufige Fragen](#fragen)
|
||||||
|
|
||||||
## Die vier Begriffe {#begriffe}
|
## 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**.
|
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.
|
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}
|
## 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
|
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.
|
– 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}
|
## Protokoll {#protokoll}
|
||||||
|
|
||||||
*Automatisierung → Protokoll* zeigt jeden Lauf: Zeitpunkt, Dauer, gesehen /
|
*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 |
|
| *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 |
|
| *n bereits übertragen* | Kopiermodus: diese Dateiversionen sind schon kopiert |
|
||||||
| *Termin übersprungen – voriger Lauf läuft noch* | Ein Lauf dauert länger als das Intervall |
|
| *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).
|
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}
|
### Benachrichtigungen {#benachrichtigungen}
|
||||||
|
|
||||||
NetMover kann bei Fehlern eine Nachricht an einen **Webhook** schicken – z. B. an
|
NetMover meldet Probleme per **E-Mail** und/oder an einen **Webhook** (z. B. ntfy,
|
||||||
ntfy, Gotify oder Home Assistant.
|
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 |
|
| 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. |
|
| **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` |
|
| **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/<thema>`, Content-Type `text/plain`, Vorlage
|
Beispiele: ntfy → URL `https://ntfy.sh/<thema>`, Content-Type `text/plain`, Vorlage
|
||||||
`$title: $message`. Gotify → URL `https://gotify.lan/message?token=…`, Content-Type
|
`$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 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
|
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
|
**„mount error(95): Operation not supported“** – NAS und NetMover finden keine
|
||||||
gemeinsame SMB-Version. Am NAS das maximale SMB-Protokoll auf SMB3 stellen
|
gemeinsame SMB-Version. Am NAS das maximale SMB-Protokoll auf SMB3 stellen
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ dev = [
|
|||||||
"types-paramiko",
|
"types-paramiko",
|
||||||
"types-Markdown",
|
"types-Markdown",
|
||||||
"shellcheck-py",
|
"shellcheck-py",
|
||||||
|
"aiosmtpd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""NetMover – NFS/SMB-Mounts verwalten und Dateien zeitgesteuert verschieben."""
|
"""NetMover – NFS/SMB-Mounts verwalten und Dateien zeitgesteuert verschieben."""
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.4.0"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class NetMover:
|
|||||||
self.session_factory = make_session_factory(self.db_engine)
|
self.session_factory = make_session_factory(self.db_engine)
|
||||||
self.secrets = SecretBox(load_or_create_key(settings.secret_key_path))
|
self.secrets = SecretBox(load_or_create_key(settings.secret_key_path))
|
||||||
self.backend = backend or make_backend(settings)
|
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.mounts = MountManager(
|
||||||
self.session_factory,
|
self.session_factory,
|
||||||
self.backend,
|
self.backend,
|
||||||
@@ -93,4 +93,5 @@ class NetMover:
|
|||||||
scheduler_running=self.scheduler.running,
|
scheduler_running=self.scheduler.running,
|
||||||
last_heartbeat=self.scheduler.last_heartbeat,
|
last_heartbeat=self.scheduler.last_heartbeat,
|
||||||
service_started=self.started_at,
|
service_started=self.started_at,
|
||||||
|
maintenance=load_settings(self.session_factory).maintenance,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ class AppSettings:
|
|||||||
webhook_template: str = DEFAULT_TEMPLATE
|
webhook_template: str = DEFAULT_TEMPLATE
|
||||||
webhook_content_type: str = "application/json"
|
webhook_content_type: str = "application/json"
|
||||||
kuma_push_url: str = ""
|
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."
|
_PREFIX = "app."
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from fastapi import APIRouter, Depends, FastAPI, Request
|
|||||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from markupsafe import escape
|
||||||
from sqlalchemy import func, or_, select
|
from sqlalchemy import func, or_, select
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
from starlette.datastructures import UploadFile
|
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.keys import delete_key, generate_key, import_key, list_keys, rename_key
|
||||||
from netmover.kuma import push as kuma_push
|
from netmover.kuma import push as kuma_push
|
||||||
from netmover.logging_setup import event, get_logger
|
from netmover.logging_setup import event, get_logger
|
||||||
|
from netmover.mail import SECURITY_MODES, MailConfigError, parse_recipients, valid_address
|
||||||
from netmover.models import (
|
from netmover.models import (
|
||||||
CollisionPolicy,
|
CollisionPolicy,
|
||||||
Credential,
|
Credential,
|
||||||
@@ -185,6 +187,7 @@ class NavState:
|
|||||||
keys: int
|
keys: int
|
||||||
rules: int
|
rules: int
|
||||||
rules_active: int
|
rules_active: int
|
||||||
|
maintenance: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _nav_state(request: Request) -> NavState | None:
|
def _nav_state(request: Request) -> NavState | None:
|
||||||
@@ -208,6 +211,7 @@ def _nav_state(request: Request) -> NavState | None:
|
|||||||
keys=keys or 0,
|
keys=keys or 0,
|
||||||
rules=len(rules),
|
rules=len(rules),
|
||||||
rules_active=sum(1 for (en,) in rules if en),
|
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))
|
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")
|
@protected.post("/rules/{rule_id}/run")
|
||||||
async def rule_run_now(request: Request, rule_id: int) -> Response:
|
async def rule_run_now(request: Request, rule_id: int) -> Response:
|
||||||
state = nm(request)
|
state = nm(request)
|
||||||
@@ -1000,6 +1021,32 @@ async def settings_save(request: Request) -> Response:
|
|||||||
if url and not url.startswith(("http://", "https://")):
|
if url and not url.startswith(("http://", "https://")):
|
||||||
errors["webhook_url"] = "URL muss mit http:// oder https:// beginnen."
|
errors["webhook_url"] = "URL muss mit http:// oder https:// beginnen."
|
||||||
cfg.webhook_url = url
|
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()
|
kuma = (form.get("kuma_push_url") or "").strip()
|
||||||
if kuma != auth_mask(cfg.kuma_push_url):
|
if kuma != auth_mask(cfg.kuma_push_url):
|
||||||
if kuma and not kuma.startswith(("http://", "https://")):
|
if kuma and not kuma.startswith(("http://", "https://")):
|
||||||
@@ -1050,6 +1097,25 @@ async def settings_webhook_test(request: Request) -> Response:
|
|||||||
return HTMLResponse('<p class="msg ok">Testnachricht gesendet.</p>')
|
return HTMLResponse('<p class="msg ok">Testnachricht gesendet.</p>')
|
||||||
|
|
||||||
|
|
||||||
|
@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('<p class="msg error">Server, Absender und Empfänger speichern.</p>')
|
||||||
|
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'<p class="msg error">Fehlgeschlagen: {detail}</p>')
|
||||||
|
return HTMLResponse(f'<p class="msg ok">Gesendet an {escape(cfg.smtp_to)}.</p>')
|
||||||
|
|
||||||
|
|
||||||
@protected.post("/settings/kuma-test", response_class=HTMLResponse)
|
@protected.post("/settings/kuma-test", response_class=HTMLResponse)
|
||||||
async def settings_kuma_test(request: Request) -> Response:
|
async def settings_kuma_test(request: Request) -> Response:
|
||||||
state = nm(request)
|
state = nm(request)
|
||||||
|
|||||||
@@ -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 .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
|
||||||
.health.ok { color: var(--ok); background: var(--ok-bg); }
|
.health.ok { color: var(--ok); background: var(--ok-bg); }
|
||||||
.health.bad { color: var(--err); background: var(--err-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; }
|
.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; }
|
.menu-toggle { display: none; align-items: center; gap: 6px; }
|
||||||
.ico { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 2;
|
.ico { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 2;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
{% if nav %}
|
{% if nav %}
|
||||||
<a class="health {{ 'ok' if nav.healthy else 'bad' }}" href="/" title="Gesundheitszustand">
|
<a class="health {{ 'ok' if nav.healthy else 'bad' }}" href="/" title="Gesundheitszustand">
|
||||||
<span class="dot"></span>{{ 'Alles in Ordnung' if nav.healthy else (nav.problems ~ (' Problem' if nav.problems == 1 else ' Probleme')) }}</a>
|
<span class="dot"></span>{{ 'Alles in Ordnung' if nav.healthy else (nav.problems ~ (' Problem' if nav.problems == 1 else ' Probleme')) }}</a>
|
||||||
|
{% if nav.maintenance %}<a class="health warn" href="/" title="Geplante Läufe sind pausiert"><span class="dot"></span>Wartungsmodus</a>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
{% if help_anchor %}<a class="help-link" href="/help#{{ help_anchor }}">{{ icon('help') }}<span>Hilfe zu dieser Seite</span></a>{% endif %}
|
{% if help_anchor %}<a class="help-link" href="/help#{{ help_anchor }}">{{ icon('help') }}<span>Hilfe zu dieser Seite</span></a>{% endif %}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro check(name, label, checked, hint='') -%}
|
{% macro check(name, label, checked, hint='', wide=false) -%}
|
||||||
<div class="field check">
|
<div class="field check{{ ' wide' if wide }}">
|
||||||
<input id="f-{{ name }}" type="checkbox" name="{{ name }}" {{ 'checked' if checked }}>
|
<input id="f-{{ name }}" type="checkbox" name="{{ name }}" {{ 'checked' if checked }}>
|
||||||
<label for="f-{{ name }}">{{ label }}</label>
|
<label for="f-{{ name }}">{{ label }}</label>
|
||||||
{% if hint %}<span class="hint">{{ hint }}</span>{% endif %}
|
{% if hint %}<span class="hint">{{ hint }}</span>{% endif %}
|
||||||
|
|||||||
@@ -33,17 +33,19 @@
|
|||||||
<thead><tr><th>Regel</th><th>Letztes Ergebnis</th><th>Letzter Erfolg</th><th>Nächster Lauf</th><th></th></tr></thead>
|
<thead><tr><th>Regel</th><th>Letztes Ergebnis</th><th>Letzter Erfolg</th><th>Nächster Lauf</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for v in rules %}{% set r = v.r %}
|
{% for v in rules %}{% set r = v.r %}
|
||||||
<tr class="{{ 'warn' if v.stale }}">
|
<tr class="{{ 'warn' if v.stale or r.waiting_since }}">
|
||||||
<td data-label="Regel"><strong>{{ r.name }}</strong>{% if not r.enabled %} <span class="badge">aus</span>{% endif %}{% if r.dry_run %} <span class="badge warn">Probelauf</span>{% endif %}
|
<td data-label="Regel"><strong>{{ r.name }}</strong>{% if not r.enabled %} <span class="badge">aus</span>{% endif %}{% if r.dry_run %} <span class="badge warn">Probelauf</span>{% endif %}
|
||||||
<div class="muted small mono">{{ v.target }}</div></td>
|
<div class="muted small mono">{{ v.target }}</div></td>
|
||||||
<td data-label="Letztes Ergebnis">
|
<td data-label="Letztes Ergebnis">
|
||||||
{% if v.running_since %}<span class="badge warn">läuft seit {{ v.running_since|ago|replace('vor ', '') }}</span>
|
{% if r.waiting_since %}<span class="badge warn">wartet seit {{ r.waiting_since|ago|replace('vor ', '') }}</span>
|
||||||
|
<div class="small text-warn">{{ r.waiting_reason }}</div>
|
||||||
|
{% elif v.running_since %}<span class="badge warn">läuft seit {{ v.running_since|ago|replace('vor ', '') }}</span>
|
||||||
{% elif v.last_run %}{{ run_badge(v.last_run.status) }}
|
{% elif v.last_run %}{{ run_badge(v.last_run.status) }}
|
||||||
<span class="small muted">{{ v.last_run.finished_at|ago }}{% if v.last_run.seen %} · {{ v.last_run.moved }}/{{ v.last_run.seen }}{% endif %}</span>
|
<span class="small muted">{{ v.last_run.finished_at|ago }}{% if v.last_run.seen %} · {{ v.last_run.moved }}/{{ v.last_run.seen }}{% endif %}</span>
|
||||||
{% if v.last_run.status in (RunStatus.ERROR, RunStatus.PARTIAL) and v.last_run.message %}<div class="small text-err">{{ v.last_run.message }}</div>{% endif %}
|
{% if v.last_run.status in (RunStatus.ERROR, RunStatus.PARTIAL) and v.last_run.message %}<div class="small text-err">{{ v.last_run.message }}</div>{% endif %}
|
||||||
{% else %}<span class="muted">–</span>{% endif %}</td>
|
{% else %}<span class="muted">–</span>{% endif %}</td>
|
||||||
<td data-label="Letzter Erfolg">{{ r.last_success_at|ago }}{% if v.stale %}<div class="small text-warn">länger als 3 × Intervall!</div>{% endif %}
|
<td data-label="Letzter Erfolg">{{ r.last_success_at|ago }}{% if v.stale %}<div class="small text-warn">länger als 3 × Intervall!</div>{% endif %}
|
||||||
{% if r.consecutive_failures %}<div class="small text-err">{{ r.consecutive_failures }} Fehler in Folge</div>{% endif %}</td>
|
{% if r.consecutive_failures and not r.waiting_since %}<div class="small text-err">{{ r.consecutive_failures }} Fehler in Folge</div>{% endif %}</td>
|
||||||
<td data-label="Nächster Lauf">{{ v.next_run|ago if v.next_run else '–' }}</td>
|
<td data-label="Nächster Lauf">{{ v.next_run|ago if v.next_run else '–' }}</td>
|
||||||
<td><form class="inline" method="post" action="/rules/{{ r.id }}/run">
|
<td><form class="inline" method="post" action="/rules/{{ r.id }}/run">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf }}">
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted small mono">{{ v.target }} · Filter {{ r.pattern }}{{ ' (rekursiv)' if r.recursive }}</div>
|
<div class="muted small mono">{{ v.target }} · Filter {{ r.pattern }}{{ ' (rekursiv)' if r.recursive }}</div>
|
||||||
|
{% if r.waiting_since %}<p class="msg warn small">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.</p>{% endif %}
|
||||||
<div class="small">Letzter Lauf: {% if v.running_since %}<span class="badge warn">läuft seit {{ v.running_since|ago|replace('vor ', '') }}</span>{% elif v.last_run %}{{ run_badge(v.last_run.status) }} {{ v.last_run.finished_at|ago }}{% else %}–{% endif %}
|
<div class="small">Letzter Lauf: {% if v.running_since %}<span class="badge warn">läuft seit {{ v.running_since|ago|replace('vor ', '') }}</span>{% elif v.last_run %}{{ run_badge(v.last_run.status) }} {{ v.last_run.finished_at|ago }}{% else %}–{% endif %}
|
||||||
· letzter Erfolg: <span class="{{ 'text-warn' if v.stale }}">{{ r.last_success_at|ago }}</span>
|
· letzter Erfolg: <span class="{{ 'text-warn' if v.stale }}">{{ r.last_success_at|ago }}</span>
|
||||||
· nächster: {{ v.next_run|ago if v.next_run else '–' }}</div>
|
· nächster: {{ v.next_run|ago if v.next_run else '–' }}</div>
|
||||||
|
|||||||
@@ -8,18 +8,37 @@
|
|||||||
<fieldset><legend>Protokoll</legend><div class="fields">
|
<fieldset><legend>Protokoll</legend><div class="fields">
|
||||||
{{ field('retention_days', 'Aufbewahrung (Tage)', cfg.retention_days, type='number', errors=errors, attrs='min="1" max="3650"') }}
|
{{ field('retention_days', 'Aufbewahrung (Tage)', cfg.retention_days, type='number', errors=errors, attrs='min="1" max="3650"') }}
|
||||||
</div></fieldset>
|
</div></fieldset>
|
||||||
<fieldset><legend>Benachrichtigung (Webhook) <a class="legend-help" href="/help#benachrichtigungen" title="Hilfe">?</a></legend><div class="fields">
|
<fieldset><legend>Benachrichtigungen <a class="legend-help" href="/help#benachrichtigungen" title="Hilfe">?</a></legend><div class="fields">
|
||||||
{{ 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) }}
|
{{ 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“).') }}
|
||||||
<div data-show-for="notify_mode:after_n">{{ field('notify_after', 'N', cfg.notify_after, type='number', errors=errors, attrs='min="1"') }}</div>
|
<div data-show-for="notify_mode:after_n">{{ field('notify_after', 'N', cfg.notify_after, type='number', errors=errors, attrs='min="1"') }}</div>
|
||||||
{{ 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) }}
|
||||||
|
</div></fieldset>
|
||||||
|
|
||||||
|
<fieldset><legend>Webhook</legend><div class="fields">
|
||||||
|
{{ 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) }}
|
{{ 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) }}
|
|
||||||
<div class="field wide"><label for="f-tpl">Vorlage</label>
|
<div class="field wide"><label for="f-tpl">Vorlage</label>
|
||||||
<textarea id="f-tpl" name="webhook_template" class="mono">{{ cfg.webhook_template }}</textarea>
|
<textarea id="f-tpl" name="webhook_template" class="mono">{{ cfg.webhook_template }}</textarea>
|
||||||
<span class="hint">Platzhalter: $event $title $message $rule $time $host – bei JSON automatisch maskiert.</span></div>
|
<span class="hint">Platzhalter: $event $title $message $rule $time $host – bei JSON automatisch maskiert.</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"><button type="button" hx-post="/settings/webhook-test" hx-target="#hook-result">Test senden (gespeicherte Einstellungen)</button><span id="hook-result"></span></div>
|
<div class="row"><button type="button" hx-post="/settings/webhook-test" hx-target="#hook-result">Test senden (gespeicherte Einstellungen)</button><span id="hook-result"></span></div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset><legend>E-Mail</legend><div class="fields">
|
||||||
|
{{ 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') }}
|
||||||
|
<div class="field{{ ' has-error' if errors.get('smtp_password') }}"><label for="f-smtp_password">Passwort</label>
|
||||||
|
<input id="f-smtp_password" type="password" name="smtp_password" autocomplete="new-password" placeholder="{{ '••••••' if cfg.smtp_password_enc }}">
|
||||||
|
<span class="hint">{{ 'Leer lassen = unverändert.' if cfg.smtp_password_enc else '' }} Wird verschlüsselt gespeichert.</span></div>
|
||||||
|
{{ 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') }}
|
||||||
|
</div>
|
||||||
|
<div class="row"><button type="button" hx-post="/settings/mail-test" hx-target="#mail-result">Test-E-Mail senden (gespeicherte Einstellungen)</button><span id="mail-result"></span></div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset><legend>Überwachung (Uptime Kuma) <a class="legend-help" href="/help#uptime-kuma" title="Hilfe">?</a></legend>
|
<fieldset><legend>Überwachung (Uptime Kuma) <a class="legend-help" href="/help#uptime-kuma" title="Hilfe">?</a></legend>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{{ 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.') }}
|
{{ 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.') }}
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
{% block title %}Status{% endblock %}
|
{% block title %}Status{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row spread"><h1>Status</h1>
|
<div class="row spread"><h1>Status</h1>
|
||||||
<span class="muted small">aktualisiert sich alle 15 s</span></div>
|
<div class="row"><span class="muted small">aktualisiert sich alle 15 s</span>
|
||||||
|
<form class="inline" method="post" action="/maintenance"
|
||||||
|
{% if not (nav and nav.maintenance) %}data-confirm="Alle geplanten Läufe pausieren?"{% endif %}>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf }}">
|
||||||
|
<button class="small">{{ 'Wartungsmodus beenden' if nav and nav.maintenance else 'Wartungsmodus' }}</button>
|
||||||
|
</form></div></div>
|
||||||
|
{% if nav and nav.maintenance %}<p class="msg warn"><strong>Wartungsmodus:</strong> Geplante Läufe sind pausiert.
|
||||||
|
„Jetzt ausführen“ geht weiterhin. Für Regeln schlägt <code>/healthz</code> keinen Alarm.</p>{% endif %}
|
||||||
<div id="status-body" hx-get="/partials/status" hx-trigger="every 15s" hx-swap="innerHTML">
|
<div id="status-body" hx-get="/partials/status" hx-trigger="every 15s" hx-swap="innerHTML">
|
||||||
{% include "partials/status_body.html" %}
|
{% include "partials/status_body.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user