homelab-showcase/08-monitoring.md

235 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Monitoring — Rebuild-Runbook
Neue LXC `CT104 monitoring` (`<LAN_IP_MONITORING>`, Tailscale-Mitglied `monitoring.<TAILNET>`) hostet als Docker-Compose-Stack: InfluxDB (Metrik-Datenbank), Grafana (Dashboards), Uptime Kuma (Erreichbarkeit + Alert-Routing), ntfy (Push-Benachrichtigungen). Ergänzt um PVE-Metric-Server-Export, Telegraf auf dem VPS, ein HDD-SMART-Tracking-Script und Login-/Zugriffs-Tracking je Dienst.
## 1. CT104 anlegen
```bash
pct create 104 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
--hostname monitoring --memory 1536 --swap 512 --cores 2 \
--rootfs local-lvm:12 \
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_MONITORING>/24,gw=<LAN_IP_ROUTER> \
--nameserver <LAN_IP_ADGUARD> --unprivileged 1 --features nesting=1 \
--onboot 1 --timezone Europe/Berlin --ostype debian
pct start 104
pct stop 104
cat >> /etc/pve/lxc/104.conf <<'EOF'
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0
EOF
pct start 104
```
Danach Docker (`curl -fsSL https://get.docker.com | sh`) und Tailscale (`curl -fsSL https://tailscale.com/install.sh | sh && tailscale up --hostname=monitoring --accept-routes=false`) installieren.
⚠️ **NICHT vergessen:** CT104 ins nächtliche `vzdump`-Backup aufnehmen (`crontab -e` auf dem PVE-Host, `104` zur ID-Liste ergänzen) — sonst ist der Monitoring-Stack selbst nicht gesichert.
## 2. Docker-Compose-Stack
`/opt/monitoring/docker-compose.yml` (Auszug, vollständige Datei im laufenden System): vier Services `influxdb`, `grafana`, `uptime-kuma`, `ntfy`, gemeinsamer `x-logging`-Anchor (10m×3 Dateien, bestehende Konvention). Ports: InfluxDB 8086, Grafana 3000, Uptime Kuma 3001, ntfy 8090→80.
⚠️ **NICHT TUN:** Grafana-Datenverzeichnis (`./grafana`) beim ersten Start nicht vor-chownen — der Container läuft als UID 472, ein per `pct exec`/root angelegtes Bind-Mount-Verzeichnis gehört aber root:root. Fix: `chown -R 472:472 /opt/monitoring/grafana` **vor** dem ersten Start von Grafana, sonst Crash-Loop ("not writable").
```bash
mkdir -p /opt/monitoring/{influxdb,grafana,uptime-kuma,ntfy}
chown -R 472:472 /opt/monitoring/grafana
cd /opt/monitoring && docker compose up -d
```
## 3. InfluxDB: Setup, Buckets, Retention, Downsampling
```bash
docker exec influxdb influx setup --username admin --password '<PW>' \
--org homelab --bucket raw --retention 14d --force
docker exec influxdb influx bucket create --name aggregated --org homelab --retention 730d
```
Zwei Buckets statt vieler kleiner: `raw` (14 Tage, volle Auflösung — deckt die längste Rohdaten-Anforderung aller Datentypen ab) und `aggregated` (2 Jahre, stündliche Mittelwerte). Downsampling-Task (`influx task create -f downsample.flux`):
```flux
option task = {name: "downsample-hourly", every: 1h}
from(bucket: "raw")
|> range(start: -task.every)
|> filter(fn: (r) => r._field != "name" and r._field != "status" and r._field != "type" and r._field != "tags" and r._field != "uptime_format" and r._field != "content")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> to(bucket: "aggregated", org: "homelab")
```
⚠️ **NICHT TUN:** Den `raw`-Bucket ohne Feld-Filter blind mit `aggregateWindow(fn: mean)` downsamplen. Proxmox' `system`-Measurement (LXC- und Storage-Objekte) enthält Text-Felder (`name`, `status`, `type`, `tags`, `uptime_format`, `content`) — `mean` auf einer String-Spalte lässt den Task mit `unsupported input type for mean aggregate: string` fehlschlagen. Vor dem Anlegen des Tasks die tatsächlichen Feldnamen je Measurement prüfen (`schema.fieldKeys`), nicht raten.
Retention-Prinzip (Details/Begründung siehe Plan-Historie): Rohdaten kurz (Tage), Aggregate lang (Jahre) — hält die Gesamtgröße über Jahre konstant klein statt linear mit der Laufzeit zu wachsen.
## 4. PVE Metric Server → InfluxDB
```bash
pvesh create /cluster/metrics/server/monitoring \
--type influxdb --server <LAN_IP_MONITORING> --port 8086 \
--influxdbproto http --organization homelab --bucket raw --token '<TOKEN>'
```
Pusht Host- und LXC-Metriken automatisch alle ~10s, kein Agent in den LXCs nötig. Guest-Metriken landen unter Measurement `system` (Felder `cpu`, `mem`, `maxmem`, …, Tag `object=lxc`), Host-Metriken unter `cpustat`/`memory`/`blockstat`/`nics` (Tag `object=nodes`) — **unterschiedliches Schema**, beim Dashboard-Bau beide Fälle abdecken.
## 5. Telegraf auf dem VPS
VPS ist kein PVE-Cluster-Mitglied, braucht eigenen Agent:
```bash
apt install -y gnupg
curl -sL -o /tmp/telegraf.deb https://dl.influxdata.com/telegraf/releases/telegraf_1.39.1-1_amd64.deb
dpkg -i /tmp/telegraf.deb
```
⚠️ **NICHT TUN:** Das offizielle InfluxData-apt-Repo (`repos.influxdata.com/debian`) auf Debian 13 (trixie) einbinden — die GPG-Signatur wird von `apt`/`sqv` mit "Missing key" abgelehnt (Schlüssel-Rotation nicht mit dem dokumentierten `_compat.key` synchron). Stattdessen das `.deb` direkt von `dl.influxdata.com/telegraf/releases/` laden und mit `dpkg -i` installieren.
⚠️ **NICHT vergessen:** Minimale Debian-13-Installationen haben oft kein `cron`-Paket (`crontab: command not found`) — `apt install -y cron && systemctl enable --now cron` vor dem ersten `crontab -e`.
`/etc/telegraf/telegraf.conf`: `outputs.influxdb_v2` auf `http://<CT104-Tailscale-IP>:8086`, Bucket `raw`, Org `homelab`; Inputs `cpu`, `mem`, `disk`, `net`, `system`.
## 6. HDD-SMART-Tracking (PVE-Host)
`/usr/local/bin/hdd-smart-to-influx.sh` liest `smartctl -A` für beide Platten (Attribute `Load_Cycle_Count`, `Start_Stop_Count`, `Temperature_Celsius` — Raw-Wert ist das erste Feld vor evtl. Klammertext), `zfs list -H -o name,used,avail,refer -p` für `nas`/`nas/freigabe`/`nas/backups` (Measurement `zfs_usage`, `/` im Dataset-Namen zu `_` normalisiert für den Tag-Wert) **und** NVMe-Belegung (Measurement `nvme_usage`) — die VG `pve` deckt praktisch die gesamte Intenso-NVMe ab (nur EFI-/Boot-Partitionen liegen außerhalb, vernachlässigbar). Schreibt alles per InfluxDB-Line-Protocol via `curl` in den `raw`-Bucket. systemd-Timer `hdd-smart-to-influx.timer`, alle 5 Minuten:
⚠️ **NICHT TUN:** Bei LVM-Thin-Provisioning (hier: LV `pve/data`, 137GB Pool für alle LXC-Disks) `vgs -o vg_size,vg_free` als "wie voll ist die Platte" interpretieren. Das misst nur, wie viel VG-Fläche noch **unallokiert** ist (also Platz für neue LVs) — nicht, wie viel der bereits allokierten LVs tatsächlich mit Daten beschrieben ist. Bei diesem Setup zeigte `vgs` 93% "belegt", obwohl real nur ~8% Daten auf der Platte lagen (root-LV zu 68GB allokiert, aber nur 6GB genutzt; Thin-Pool zu 137GB allokiert, aber nur 8,88% beschrieben). Für eine ehrliche Nutzungsanzeige: `df --output=used -B1 /` (root-FS) + `lvs -o lv_size,data_percent pve/data` (Thin-Pool-Ist-Nutzung) addieren → Feld `actual_used_gb`, getrennt von `total_gb`/`free_gb` (VG-Allokation, weiterhin nützlich um zu wissen, wie viel Raum für neue LVs übrig ist).
```ini
# /etc/systemd/system/hdd-smart-to-influx.timer
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
```
Grafana-Panel "HDD Load-Cycle-Rate" (`derivative(unit: 1h, nonNegative: true)` auf `load_cycle_count`) ist die eigentliche Verifikation, ob ein Spindown-Timeout in der Praxis zu häufiges Spin-up/down verursacht.
⚠️ **NICHT TUN:** Bei Prozent-Berechnungen aus zwei InfluxDB-Feldern (z.B. `used_bytes / (used_bytes + avail_bytes)`) auf automatische Int→Float-Konvertierung verlassen. Line-Protocol-Felder mit `i`-Suffix sind in Flux `int`, eine Division mit einem Float-Literal (`* 100.0`) scheitert dann mit `type conflict: float != int`. Fix: `float(v: r.used_bytes) / float(v: r.used_bytes + r.avail_bytes) * 100.0`.
## 7. Grafana: Datasource + Dashboard per Provisioning
`/opt/monitoring/grafana-provisioning/datasources/influxdb.yaml` (InfluxDB2/Flux, fester `uid: influxdb-homelab`) und `/opt/monitoring/grafana-provisioning/dashboards/{provider.yaml,homelab.json}`, gemountet nach `/etc/grafana/provisioning`. Dashboard-JSON referenziert die Datasource über die feste `uid`, nicht per Name.
Dashboard-Struktur (per `"type": "row"`-Panels gruppiert, jede Zeile bewusst `"collapsed": false`): **Übersicht** (Esprimo/PVE-Host CPU+RAM als Graph + Intenso-NVMe frei/gesamt/Belegung-%, direkt darunter dasselbe Muster für den VPS: CPU+RAM als Graph + Root-Filesystem frei/gesamt/Belegung-%), **Ressourcen** (CPU/RAM/Speicherbelegung nur LXCs — PVE-Host und VPS bewusst nicht doppelt, stehen schon in der Übersicht), **Festplatten** (ZFS-Belegung in Bytes, HDD-Load-Cycle-Count/-Rate, Temperatur), **Logins & Zugriffe** (erfolgreiche/fehlgeschlagene Logins je Dienst, gestapelte Balken). `refresh: "1m"` auf Dashboard-Ebene gesetzt.
VPS-Speicherbelegung braucht kein neues Tracking — Telegrafs `disk`-Input-Plugin (bereits für die Ressourcen-Metriken konfiguriert, siehe Abschnitt 5) liefert `free`/`total`/`used_percent` für `/` schon fertig aufbereitet, kein manueller Prozent-Umweg wie bei NVMe/ZFS nötig.
**LXC-Speicherbelegung** (Panel "Speicherbelegung: LXCs", Ressourcen-Abschnitt): braucht kein neues Tracking-Script — Felder `disk`/`maxdisk` kommen schon über den PVE-Metric-Export im `system`-Measurement mit (Rootfs-Füllstand je Container, nicht zu verwechseln mit der NVMe- oder ZFS-Belegung).
⚠️ **NICHT TUN:** Bei Stat-Panels mit `pivot()`+`map()`-Berechnung (z.B. Prozent aus zwei Feldern) das Ergebnis ohne abschließendes `|> keep(columns: ["_time", "_value"])` zurückgeben. Grafana zeigt bei `reduceOptions.fields: ""` (Auto-Erkennung) sonst **jedes** numerische Feld der Tabelle als eigene Kachel-Zahl an — inklusive der Zwischenwerte (`used_bytes`, `avail_bytes`) neben dem eigentlichen `_value`. Gleiches Prinzip für Legenden bei Mehrserien-Timeseries-Panels: ohne `keep(columns: ["_time", "host", "_value"])` zeigt die Legende alle Tags (`nodename`, `object`, `vmid`, …) statt nur des relevanten (`host`).
⚠️ **NICHT TUN:** Bei Stat-Panels mit reinen Referenzzahlen (z.B. "frei (GB)", "gesamt (GB)") `colorMode: "value"` ohne explizite Thresholds stehen lassen. Grafanas Default-Threshold färbt ab Wert 80 automatisch rot — bei einer GB-Zahl wie 213,9 (viel freier Platz, eigentlich gut) sieht das wie ein Alarm aus, obwohl "hoch" hier positiv ist. Für reine Referenzzahlen `colorMode: "none"` setzen; Ampelfarben nur bei tatsächlich alarmwürdigen Metriken (z.B. Belegungs-Prozent, wo hoch = schlecht stimmt) verwenden.
⚠️ **NICHT vergessen:** `GF_SECURITY_ADMIN_PASSWORD` wirkt nur bei der **ersten** DB-Initialisierung. Ist Grafana schon einmal mit Default-Credentials (`admin`/`admin`) gestartet, hilft nur `docker exec grafana grafana cli admin reset-admin-password '<PW>'` (neuere Grafana-Versionen: `grafana cli`, nicht mehr das alte `grafana-cli`-Binary).
## 8. Uptime Kuma: Monitore + ntfy-Notification
Kein offizielles REST-API für Monitor-Verwaltung — Setup über die Python-Bibliothek `uptime-kuma-api` (nutzt das interne Socket.IO-Protokoll):
```bash
python3 -m venv /opt/monitoring/kuma-venv
/opt/monitoring/kuma-venv/bin/pip install uptime-kuma-api
```
```python
from uptime_kuma_api import UptimeKumaApi, MonitorType, NotificationType
api = UptimeKumaApi('http://localhost:3001')
api.setup('admin', '<PW>') # nur beim allerersten Mal
api.login('admin', '<PW>')
api.add_notification(name='ntfy-homelab', type=NotificationType.NTFY, isDefault=True,
ntfyserverurl='http://ntfy:80', ntfytopic='homelab-alerts',
ntfyPriority=4, ntfyAuthenticationMethod='none')
api.add_monitor(type=MonitorType.HTTP, name='...', url='...', interval=60, maxretries=2)
api.add_monitor(type=MonitorType.PING, name='...', hostname='...', interval=60, maxretries=2)
```
⚠️ **NICHT TUN:** Sich auf `isDefault=True` bei `add_notification` verlassen, um die Notification automatisch an alle (auch später erstellte) Monitore zu hängen — greift über die API nicht zuverlässig. Stattdessen nach dem Anlegen explizit für jeden Monitor `api.edit_monitor(id, notificationIDList={'<notif_id>': True})` setzen.
⚠️ **NICHT TUN:** `retries=` als Kwarg für `add_monitor` verwenden — die Bibliothek erwartet `maxretries`, sonst `TypeError`.
Angelegte Monitore: 5× HTTP (Guacamole öffentlich, Vaultwarden/PVE-Web über Tailnet-Namen, AdGuard/Filebrowser über LAN-IP — bewusst IP statt `*.pve`-Name bei AdGuard, um keine Zirkularität zu erzeugen, falls AdGuard selbst der Ausfall ist), 6× Ping (VPS über Tailscale-IP, PVE-Host + 4 LXCs über LAN-IP).
## 9. ntfy: Erreichbarkeit + tailscale serve
```bash
tailscale serve --bg http://localhost:8090
```
⚠️ **NICHT TUN:** `https+insecure://localhost:8090` verwenden (das Muster aus `01-pve-host.md`/PVE-Webinterface) — dort war es richtig, weil das Backend (`pveproxy`) selbst HTTPS mit selbstsigniertem Zertifikat spricht. ntfy spricht intern nur **plain HTTP**; `https+insecure://` gegen einen HTTP-Backend führt zu `502 Bad Gateway`. Korrekt: `http://localhost:8090`.
Nach Einrichtung `NTFY_BASE_URL` in der Compose-Datei auf `https://monitoring.<TAILNET>` setzen und `docker compose up -d ntfy` (Recreate).
## 10. Backup-Cronjobs: Fehlschlag-Alerts
Bestehende Cronjobs (`vzdump`, `backup-guacamole.sh`, rsync-Pull) auf Wrapper-Skripte umgestellt, die bei Fehlschlag (`if ! <command>; then curl ntfy; fi` bzw. `trap ... ERR`) eine Push-Nachricht senden. Bewusst nur bei Fehlschlag, kein täglicher Erfolgs-Spam. Ersetzt die bis dahin wirkungslose `vzdump`-Mail-Benachrichtigung (Postfix auf dem PVE-Host ist `inet_interfaces = loopback-only`, kein Relay, kein root-Postfach — die Mail ging faktisch ins Leere).
## 11. Login-/Zugriffs-Tracking
Kleine Skripte (alle 5 Min. per Cron) statt eines Loki/Promtail-Log-Stacks — schreiben Erfolgs-/Fehlschlag-Zähler nach InfluxDB, pushen bei Schwellenwert (>3 Fehlschläge/5 Min.) einen ntfy-Alert. Zusätzlich pusht jedes Skript bei `SUCCESS -gt 0` eine niedrigprioritäre ntfy-Meldung für erfolgreiche Logins (eigener Block je Skript, `Priority: default` statt `high`, Tag `white_check_mark` statt `warning`).
⚠️ **Bekannte Rauschquelle:** Vaultwardens Clients nutzen denselben `/identity/connect/token`-Endpunkt auch für OAuth-Token-Refreshes, nicht nur für echte Logins — der Erfolgs-Alert kann dadurch öfter feuern als ein tatsächlicher neuer Login stattfindet. Bewusst so belassen (Nutzeranforderung), aber beim nächsten Mal ggf. auf `grant_type=password` in den Logs filtern, um echte Logins von Refreshes zu trennen.
| Dienst | Quelle |
|---|---|
| Guacamole (VPS) | Docker-Logs (`InMemoryAuthenticationFailureTracker`) + `guacamole_user_history`-Tabelle für Erfolge |
| Vaultwarden (CT103) | Docker-Logs, `(login) POST /identity/connect/token => <Status>` |
| PVE-Weboberfläche (Host) | `/var/log/pveproxy/access.log`, byte-offset-basiertes Tailing (kein natives `--since`) |
| SSH (PVE-Host) | `journalctl -u ssh --since "5 min ago"` |
| SSH/fail2ban (VPS) | `fail2ban-client status sshd` (Currently/Total failed/banned) — kein Log-Parsing nötig, fail2ban liefert die Zahlen direkt; Alert nur wenn „Currently banned" gegenüber letzter Messung steigt |
| AdGuard-Web-UI (CT100) | **nicht umgesetzt** — AdGuard loggt fehlgeschlagene Web-UI-Logins nachweislich nicht (getestet: weder Journal noch mit `verbose`-Flag), trotz eingebautem Lockout (`auth_attempts: 5`, `block_auth_min: 15`). Bewusste, dokumentierte Lücke statt einer fragilen Behelfslösung |
**Update 2026-07-19 — Wer/IP statt nur Zahlen:** Alle fünf Skripte (außer fail2ban, das schon Zahlen liefert) hängen den echten Login-Kontext an den ntfy-Text an:
- **SSH (Host):** `user@ip` per `sed -nE` aus `Accepted`/`Failed password`/`Invalid user`-Zeilen extrahiert
- **PVE-Web:** Quell-IP (erstes Feld in `access.log`, `::ffff:`-Präfix entfernt)
- **fail2ban:** von reinem Status-Polling auf `fail2ban.log`-Tailing (byte-offset) umgestellt — liefert die tatsächlich gesperrte(n) IP(s) aus den `NOTICE [sshd] Ban <ip>`-Zeilen statt nur "eine neue IP"
- **Vaultwarden:** brauchte `IP_HEADER: "X-Forwarded-For"` in der Compose-Datei (Vaultwarden loggt IPs sonst gar nicht) — `tailscale serve` setzt den Header bereits automatisch, nur Vaultwarden vertraute ihm nicht. Danach erscheint bei Fehlschlägen `Username or password is incorrect... IP: X. Username: Y.` im Log, extrahierbar. Bei **Erfolg** bleibt die IP unverfügbar (dieser Log-Pfad existiert nur im Fehlerfall)
- **Guacamole:** brauchte `REMOTE_IP_VALVE_ENABLED: "true"` in der Compose-Datei (Tomcats RemoteIpValve, liest `X-Forwarded-For` von Caddy). Vorher zeigte der Fehlschlag-Tracker nur die interne Docker-Gateway-IP (`172.18.0.1`), nach dem Fix die echte Client-IP. Bei **Erfolg** bleibt `guacamole_user_history.remote_host` trotzdem auf der internen Adresse — der DB-Verlauf nutzt offenbar einen anderen internen Pfad als der Live-Fehlschlag-Tracker, der die Valve-korrigierte Adresse honoriert. Nicht weiter debuggt (Aufwand/Nutzen), Username bei Erfolg ist trotzdem neu und nützlich
⚠️ **NICHT TUN:** `paste -sd', ' -` verwenden, um mehrere Werte mit `, ` zu verbinden. `paste -s -d LISTE` zykelt durch die **einzelnen Zeichen** der Delimiter-Liste statt sie als einen mehrzeichigen Trenner zu nutzen — bei 3+ Werten entsteht `a,b c,d e,f` (abwechselnd Komma und Leerzeichen) statt `a, b, c, d, e, f`. Fällt bei nur 1-2 Werten nicht auf (Zufallstreffer), erst bei mehreren wird der Bug sichtbar. Fix: `paste -sd',' - | sed 's/,/, /g'`.
## 12. HDD-Tracking hat sich selbst sabotiert (kritischer Fund)
Das 5-Minuten-Tracking-Script (`hdd-smart-to-influx.sh`) weckte mit jedem Lauf die schlafenden Platten — `smartctl -A` liest per Default auch bei einer Platte im Standby, was sie aufweckt. Da Check-Takt (5 Min.) und Spindown-Timeout (5 Min., siehe `01-pve-host.md`) identisch sind, hat das Monitoring den gerade erst eingerichteten Spindown fast vollständig ausgehebelt — bewiesen durch manuellen Test (Platte per `hdparm -y` schlafen gelegt, `smartctl -A` drüber, Status sofort wieder `active/idle`).
**Fix:** `smartctl -n standby -A <dev>` — überspringt (ohne Weckvorgang) die Abfrage, wenn die Platte schon schläft; Skript erkennt das am Exit-Code/der Meldung `Device is in STANDBY mode` und schreibt für diesen Durchlauf einfach keinen Datenpunkt für die schlafende Platte (InfluxDB/Grafana kommen mit Lücken problemlos klar).
⚠️ **NICHT TUN:** `smartctl -A` (ohne `-n standby`) in einem Monitoring-Script verwenden, das eine Platte im Standby beobachten soll — das ist ein Widerspruch in sich, das Skript verhindert genau das, was es messen will.
## 13. Esprimo-Hardware-Sensoren (CPU-Temp + Lüfter)
Fujitsu-eigener Sensorchip, nicht automatisch aktiv:
```bash
apt install -y lm-sensors
yes | sensors-detect --auto
echo -e "ftsteutates\ncoretemp" > /etc/modules-load.d/ftsteutates.conf
modprobe ftsteutates coretemp
```
Liefert `coretemp-isa-0000` (`Package id 0` = CPU-Temperatur) und `ftsteutates-i2c-0-73` (`fan2` = einziger belegter Lüfterkanal von 8, Rest `FAULT`/nicht verbaut). Werte per `sensors <chip>` parsen, ins `hdd-smart-to-influx.sh`-Script integriert (Measurement `esprimo_hw`, Felder `cpu_temp_c`/`fan_rpm`) — läuft im selben 5-Min-Timer mit statt einen eigenen zu brauchen.
Grafana-Panel kombiniert beide Metriken in einer Kachel mit zwei Y-Achsen (Temperatur links, RPM rechts, per `byFrameRefID`-Field-Override) — zeigt auf einen Blick, ob und wann der Lüfter bei steigender Last einsetzt (Ziel: möglichst viel passive Kühlung).
## 14. Load-Cycle-Verifikation: tägliche Momentaufnahme statt Dauerkurve
Zusätzlich zur laufenden 5-Min-Erfassung (jetzt korrekt, siehe Punkt 12) schreibt `vzdump-backup.sh` einmal täglich (03:00, während die Platten wegen des Snapshots ohnehin wach sind — kein zusätzlicher Weckvorgang) den rohen `Load_Cycle_Count` nach InfluxDB. Grafana-Panel "Differenz zum Vortag":
```flux
from(bucket: "aggregated")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "smart" and r._field == "load_cycle_count")
|> aggregateWindow(every: 1d, fn: last, createEmpty: false)
|> difference(nonNegative: true)
```
Beantwortet direkt "wie viele Spin-ups gab es seit gestern" — eindeutiger als die stündliche Rate-Kurve, die bei Datenlücken (Platte war lange am Stück wach oder lange am Stück im Standby) leicht misszuinterpretieren ist.
## Guacamole-SFTP: bekanntes, ungelöstes Problem
Die SFTP-Begleitverbindung (Datei-Transfer-Feature, `enable-sftp`) der VNC-Verbindung "Management-Desktop (Browser)" ist aktuell **deaktiviert** (`enable-sftp: false` in der DB). Ursache war zunächst ein reines Passwort-Drift zwischen dem in Guacamole hinterlegten Passwort und dem tatsächlichen System-Passwort des `transfer`-Users auf CT102 (behoben, `chpasswd`) — aber selbst mit korrektem Passwort scheiterte `guacd`s eingebauter SSH-Client (`libssh2`) weiterhin an der Authentifizierung, während derselbe Login mit dem normalen OpenSSH-Client (von PVE-Host **und** vom VPS aus, exakt derselbe Netzwerkpfad wie `guacd`) anstandslos funktionierte. Deutet auf eine Algorithmus-/Protokoll-Inkompatibilität zwischen `libssh2` und CT102s OpenSSH-Server hin (z.B. moderne KEX-Defaults, die `libssh2` nicht unterstützt), nicht auf ein Credential-Problem.
VNC selbst funktioniert einwandfrei (davon unabhängig). Für eine vollständige Lösung: `sshd_config` auf CT102 um ältere, `libssh2`-kompatible `KexAlgorithms`/`HostKeyAlgorithms` erweitern (Kompatibilitäts-Fallback, nicht global schwächen) und erneut mit aktiviertem `enable-sftp` testen — noch nicht gemacht (Priorität war, den Nutzer schnell wieder reinzulassen).
## Referenz: Zugänge
| Dienst | URL | Hinweis |
|---|---|---|
| Grafana | `http://<LAN_IP_MONITORING>:3000` | Admin-Passwort in Vaultwarden |
| Uptime Kuma | `http://<LAN_IP_MONITORING>:3001` | Admin-Passwort in Vaultwarden |
| InfluxDB | `http://<LAN_IP_MONITORING>:8086` | Admin-Passwort + Token in Vaultwarden |
| ntfy (Push) | `https://monitoring.<TAILNET>`, Topic `homelab-alerts` | Android/iOS-App auf den self-hosted Server zeigen lassen |
| NAS-Backups (SMB) | `\\nas.pve\backups` | Read-only, gleicher Nutzer `<PRIMARY_USER>` wie `freigabe` |