Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cc5a1e222 | |||
| d85972d6ab | |||
| bd0deea90d | |||
| 5a6ab2cc37 | |||
| 30c3435ddf | |||
| b236eaeeaa | |||
| 4cf9e3226e | |||
| 699b1f118e | |||
| db886c9eb2 | |||
| 2a342614db | |||
| 2bb6eaa267 | |||
| cb80e2d2c0 | |||
| 201b201657 | |||
| 725e3b0125 | |||
| 1de6ffc5ac | |||
| 5559aa3f24 | |||
| ed61fda0ec | |||
| 85d8270898 | |||
| a904955d11 | |||
| 882ea5ad01 | |||
| e3ae97bbaf | |||
| 64976e0c0e | |||
| 424772dcfa | |||
| c8380b5755 | |||
| 83d7988c72 | |||
| 1a4593110a | |||
| f296338530 | |||
| df2e308c65 | |||
| 3861eaa0d1 | |||
| 7ff6a24c9d | |||
| ac1fa5b8e9 | |||
| f0735265eb | |||
| d99082a3a7 | |||
| 536a6fd0cd | |||
| dee9b102bd | |||
| 03e7f882d3 | |||
| c7663779bb | |||
| 8379657446 | |||
| c39ae5cdfa | |||
| 80385c4560 | |||
| 7587ee4e77 | |||
| a3c5610934 | |||
| bc9ace315a | |||
| 5171059dd1 | |||
| 0ecb2aceca | |||
| 1160f50663 | |||
| 88c48faab1 | |||
| ec8e915a56 | |||
| 861f70da58 | |||
| fc9e4aad8e | |||
| 410b20d74f | |||
| 15b351fa25 | |||
| e8cde1e2e0 | |||
| f236bfec00 | |||
| fd1b7001f6 | |||
| d45a49d648 | |||
| 1255863a4e | |||
| 26fc96a7af | |||
| e18720d1f8 | |||
| a1e6a03f79 | |||
| 8200697258 | |||
| 05b12c4802 | |||
| 8d01c3537a | |||
| 230e0cc9dc | |||
| c9bd4af2a8 | |||
| 5927b478fa | |||
| ee69bbf730 | |||
| d908d967d4 | |||
| 606779d342 | |||
| 0fabed4d1a | |||
| 76b9ffa140 | |||
| 170a7dcc1f | |||
| 0f5045ea8e | |||
| dfa3acc21e | |||
| 2eb8da1cd4 | |||
| 2acbc1adde | |||
| 342d0a0a27 | |||
| a28d3bbc33 | |||
| 4ab6dcefd2 | |||
| c24b792808 | |||
| 25a4ada891 | |||
| 6e6005aefd | |||
| af4b7015ee |
+2
-1
@@ -6,7 +6,8 @@
|
|||||||
!**/stack.env.example
|
!**/stack.env.example
|
||||||
|
|
||||||
# Secrets and certificate material
|
# Secrets and certificate material
|
||||||
**/secrets/
|
**/secrets/*
|
||||||
|
!**/secrets/*.example
|
||||||
**/letsencrypt/
|
**/letsencrypt/
|
||||||
**/acme.json
|
**/acme.json
|
||||||
**/*.key
|
**/*.key
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
||||||
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
||||||
|
|
||||||
**Stand:** 2026-06-11 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
|
**Stand:** 2026-06-13 | **Aktueller Schwerpunkt:** Home Assistant Tibber / Energie-Kosten
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,7 +88,8 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme
|
|||||||
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
|
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
|
||||||
| `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt |
|
| `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt |
|
||||||
| `mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt |
|
| `mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt |
|
||||||
| `immich_default` | Compose-intern, `internal: true` | internes Immich-Netz | ✅ umgesetzt |
|
| `immich_default` | Compose-intern, `internal: true` | internes Immich-Netz (Server, Postgres, Redis, ML) | ✅ umgesetzt |
|
||||||
|
| `immich_egress` | Compose-intern, bridge (nicht `internal`) | Outbound-only fuer `immich_machine_learning` (Modell-Download huggingface) | ✅ umgesetzt |
|
||||||
| `nextcloud_internal` | bridge, `internal: true` | internes Netz nur fuer `nextcloud` + `nextcloud-postgres` + `nextcloud-redis` | ✅ vorbereitet |
|
| `nextcloud_internal` | bridge, `internal: true` | internes Netz nur fuer `nextcloud` + `nextcloud-postgres` + `nextcloud-redis` | ✅ vorbereitet |
|
||||||
| `monitoring_net` | Compose-intern, bridge | zentraler Observability-Stack fuer Prometheus, Loki, Grafana, Promtail, Exporter und InfluxDB | Zielzustand |
|
| `monitoring_net` | Compose-intern, bridge | zentraler Observability-Stack fuer Prometheus, Loki, Grafana, Promtail, Exporter und InfluxDB | Zielzustand |
|
||||||
| `monitoring_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | Zielzustand |
|
| `monitoring_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | Zielzustand |
|
||||||
@@ -264,7 +265,7 @@ Legende Status:
|
|||||||
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
|
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
|
||||||
| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — |
|
| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — |
|
||||||
| `nextcloud-redis` | ✅ | `nextcloud_internal` | intern | app-eigener Cache fuer File Locking / Sessions | — |
|
| `nextcloud-redis` | ✅ | `nextcloud_internal` | intern | app-eigener Cache fuer File Locking / Sessions | — |
|
||||||
| `smarthome-mosquitto` | ✅ vorbereitet | `smarthome_net` | intern `1883`, kein Host-Port in Phase 1 | MQTT-Datenbus fuer Home Assistant, spaeter ESPHome und Zigbee2MQTT; Passwortdatei und ACLs in `/mnt/user/appdata/mosquitto/config` | LAN-Port erst in ESPHome-Phase mit ACLs/per-Device-Usern |
|
| `smarthome-mosquitto` | ✅ | `smarthome_net` | intern `1883`, kein Host-Port in Phase 1 | MQTT-Datenbus fuer Home Assistant, spaeter ESPHome und Zigbee2MQTT; Passwortdatei und ACLs in `/mnt/user/appdata/mosquitto/config`; MQTT-Smoke und HA-MQTT-Integration am 2026-06-13 erfolgreich | LAN-Port erst in ESPHome-Phase mit ACLs/per-Device-Usern |
|
||||||
|
|
||||||
### 7.4 Produktive Apps
|
### 7.4 Produktive Apps
|
||||||
|
|
||||||
@@ -276,9 +277,9 @@ Legende Status:
|
|||||||
| `ntfy` | ✅ | `frontend_net` | Traefik | aktiv via `ntfy.kaleschke.info`, Git-Stack | — |
|
| `ntfy` | ✅ | `frontend_net` | Traefik | aktiv via `ntfy.kaleschke.info`, Git-Stack | — |
|
||||||
| `gitea` | ✅ | `frontend_net` | Traefik + SSH-Port 222 | Web via Traefik, SSH direkt gebunden | — |
|
| `gitea` | ✅ | `frontend_net` | Traefik + SSH-Port 222 | Web via Traefik, SSH direkt gebunden | — |
|
||||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
| `immich_machine_learning` | ✅ | `immich_default`, `immich_egress` | intern (keine Traefik-Route) | `immich_default` fuer Server-Erreichbarkeit + dediziertes `immich_egress` (nicht-internal) fuer einmaligen Modell-Download (CLIP/buffalo_l) nach `model-cache`; bewusst nicht `frontend_net`, da unauth. ML-API | — |
|
||||||
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
||||||
| `homeassistant` | ✅ vorbereitet | `frontend_net`, `smarthome_net` | Traefik via `home.kaleschke.info`, native HA-Auth | Home Assistant Container im GitOps-Stack `smart-home/`; kein HAOS, kein Supervised; Fach-YAML kommt aus `smart-home-kalli`, `.storage` bleibt in `/mnt/user/appdata/homeassistant` | Deploy, Onboarding, Restore-Probe, Cloud-Integrationen |
|
| `homeassistant` | ✅ | `frontend_net`, `smarthome_net` | Traefik via `home.kaleschke.info`, native HA-Auth | Home Assistant Container im GitOps-Stack `smart-home/`; kein HAOS, kein Supervised; Fach-YAML kommt aus `smart-home-kalli`, `.storage` bleibt in `/mnt/user/appdata/homeassistant`; Komodo-Stack und Gitea-Webhook aktiv; HA-native Backup-Erzeugung, Restore-Probe, HA-MQTT-Integration, SolarEdge Local und Energy Dashboard am 2026-06-13 erfolgreich | Tibber, Energie-Kosten, spaeter Energie-Automationen |
|
||||||
| `plex` | ✅ | `host` | Traefik via `plex.kaleschke.info` + Plex native Auth; LAN direkt `:32400` | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Traefik routet per File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden; kein direkter WAN-Port 32400 und Plex Remote Access bleibt aus; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
| `plex` | ✅ | `host` | Traefik via `plex.kaleschke.info` + Plex native Auth; LAN direkt `:32400` | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Traefik routet per File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden; kein direkter WAN-Port 32400 und Plex Remote Access bleibt aus; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
||||||
| `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen |
|
| `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen |
|
||||||
| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen |
|
| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen |
|
||||||
@@ -399,7 +400,8 @@ Die Blockmigration aus der Portainer-/Dockerman-Phase ist abgeschlossen: Traefik
|
|||||||
| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) |
|
| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) |
|
||||||
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
||||||
| `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. |
|
| `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. |
|
||||||
| `Ecowitt` | spaetere HTTP-Ausnahme offen | Ecowitt kann nur HTTP. Wegen globalem Traefik-HTTP-Redirect wird in Phase 2 entschieden, ob Traefik eine selektive Webhook-Ausnahme bekommt oder ob ein LAN-only HA-Port `8123` als dokumentierte Host-Port-Ausnahme noetig wird. |
|
| `homeassistant` (Ecowitt) | LAN-only Host-Port `8123` auf `192.168.178.58` | Ecowitt-GW3000 kann kein HTTPS und pusht per HTTP an den HA-Webhook. HA bekommt einen Host-Bind nur auf der LAN-IP (`192.168.178.58:8123:8123`, nicht `0.0.0.0`/WAN), analog InfluxDB 8181. Kein Traefik-Umbau des globalen HTTP-Redirects noetig, da Ecowitt rein im LAN pusht. Webhook nicht `local_only`, geschuetzt durch 128-bit-Zufalls-ID. Siehe `docs/DECISIONS.md` (2026-06-13). |
|
||||||
|
| `homeassistant` (SolarEdge Local) | HACS/Custom-Integration `solaredge_modbus_multi` | Lokaler SolarEdge-Zugriff laeuft ueber Modbus TCP `192.168.178.111:1502`, Device-ID `1`. Das ist bewusst lokal statt Cloud-API, weil kein SolarEdge-API-Key verfuegbar ist und der Wechselrichter Modbus-Daten fuer Inverter, Smart Meter und Batterie liefert. Custom-Integration-Warnungen bei HA-Core-Upgrades beachten. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=dawarich
|
||||||
|
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
DAWARICH_HOST=dawarich.kaleschke.info
|
||||||
|
APPLICATION_HOSTS=dawarich.kaleschke.info,dawarich_app,localhost,127.0.0.1,::1
|
||||||
|
|
||||||
|
POSTGRES_USER=dawarich
|
||||||
|
POSTGRES_DB=dawarich_production
|
||||||
|
GRAFANA_DB_USER=dawarich_grafana_ro
|
||||||
|
|
||||||
|
PHOTON_API_HOST=photon.komoot.io
|
||||||
|
PHOTON_API_USE_HTTPS=true
|
||||||
|
|
||||||
|
METRICS_USERNAME=prometheus
|
||||||
|
BACKGROUND_PROCESSING_CONCURRENCY=5
|
||||||
|
RAILS_MAX_THREADS=10
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Dawarich Stack
|
||||||
|
|
||||||
|
Produktionsvorlage fuer Dawarich im KalliLab-Homelab mit GitOps ueber Gitea und Komodo.
|
||||||
|
|
||||||
|
## Gepruefter Stand
|
||||||
|
|
||||||
|
- Dawarich Release: `1.8.1` (GitHub latest am 2026-06-11)
|
||||||
|
- Docker Image: `freikin/dawarich:1.8.1`
|
||||||
|
- Hinweis: `freika/dawarich` existiert auf Docker Hub nicht; das offizielle Image aus dem Upstream-Compose ist `freikin/dawarich`.
|
||||||
|
- Dawarich Tracking-Endpoint fuer OwnTracks: `/api/v1/owntracks/points?api_key=<api-key>`
|
||||||
|
- Dawarich Prometheus ab 1.7.7: Web-Service `/metrics`; Port `9394` ist intern fuer Sidekiq-Metriken.
|
||||||
|
|
||||||
|
Quellen:
|
||||||
|
|
||||||
|
- https://github.com/Freika/dawarich/releases/tag/1.8.1
|
||||||
|
- https://dawarich.app/docs/getting-started/track-your-location/
|
||||||
|
- https://dawarich.app/docs/self-hosting/monitoring/prometheus/
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/dawarich/
|
||||||
|
|-- docker-compose.yml
|
||||||
|
|-- .env.example
|
||||||
|
|-- prometheus-scrape.snippet.yml
|
||||||
|
|-- homeassistant-dawarich.example.yaml
|
||||||
|
|-- grafana/
|
||||||
|
| |-- datasource-dawarich.yml
|
||||||
|
| `-- dashboard-dawarich.json
|
||||||
|
|-- postgres/initdb/20-grafana-readonly.sh
|
||||||
|
`-- secrets/*.txt.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup-Reihenfolge
|
||||||
|
|
||||||
|
1. Stack-Verzeichnis nach Komodo/Gitea uebernehmen: `apps/dawarich`.
|
||||||
|
2. `.env.example` als nicht versionierte Stack-`.env` oder Komodo Stack Environment anlegen.
|
||||||
|
3. Secret-Dateien auf dem Unraid-Host erstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install -d -m 700 /mnt/user/appdata/secrets
|
||||||
|
openssl rand -base64 48 > /mnt/user/appdata/secrets/dawarich_postgres_password.txt
|
||||||
|
openssl rand -base64 48 | tr -dc 'A-Za-z0-9._~-' | head -c 48 > /mnt/user/appdata/secrets/dawarich_redis_password.txt
|
||||||
|
openssl rand -hex 64 > /mnt/user/appdata/secrets/dawarich_secret_key_base.txt
|
||||||
|
openssl rand -base64 48 > /mnt/user/appdata/secrets/dawarich_metrics_password.txt
|
||||||
|
openssl rand -base64 48 > /mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt
|
||||||
|
chmod 600 /mnt/user/appdata/secrets/dawarich_*.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Bind-Volume-Zielpfade vor dem ersten Deploy anlegen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install -d -m 750 \
|
||||||
|
/mnt/user/appdata/dawarich/postgres17 \
|
||||||
|
/mnt/user/appdata/dawarich/redis \
|
||||||
|
/mnt/user/appdata/dawarich/shared \
|
||||||
|
/mnt/user/appdata/dawarich/public \
|
||||||
|
/mnt/user/appdata/dawarich/watched \
|
||||||
|
/mnt/user/appdata/dawarich/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
5. In Komodo als Compose-Stack deployen. `frontend_net` und `backend_net` muessen bereits existieren.
|
||||||
|
6. Ersten Login in Dawarich durchfuehren und den API-Key im Account-Bereich erzeugen.
|
||||||
|
7. Home Assistant `homeassistant-dawarich.example.yaml` in das Smart-Home-Fachrepo uebernehmen und `device_tracker.your_phone` ersetzen.
|
||||||
|
|
||||||
|
## Traefik und Authelia
|
||||||
|
|
||||||
|
Die UI liegt auf `https://dawarich.kaleschke.info` und nutzt `authelia@file,secure-headers@file`.
|
||||||
|
|
||||||
|
Der Healthcheck und die Tracking-API-Routen fuer OwnTracks, Overland und Traccar sind separat und priorisiert ohne Authelia geroutet, weil Mobile Clients per Dawarich-API-Key authentifizieren und keine Browser-ForwardAuth-Challenge verarbeiten koennen.
|
||||||
|
|
||||||
|
## Prometheus
|
||||||
|
|
||||||
|
`prometheus-scrape.snippet.yml` ist die dienstnahe Referenz. Produktiv ist der Job bereits in `monitoring/prometheus/prometheus.yml` eingetragen.
|
||||||
|
|
||||||
|
Der Monitoring-Stack ist dafuer bereits vorbereitet:
|
||||||
|
|
||||||
|
- `/mnt/user/appdata/secrets/dawarich_metrics_password.txt` ist in Dawarich und Prometheus eingebunden.
|
||||||
|
|
||||||
|
Nicht `dawarich_app:9394` scrapen: das ist nach aktueller Dawarich-Doku veraltet. Der Web-Service aggregiert App- und Sidekiq-Metriken unter `/metrics`. Im KalliLab scrapt Prometheus intern `http://dawarich_app:3000/metrics` ueber `backend_net` und setzt `X-Forwarded-Proto: https`, damit Dawarich mit `APPLICATION_PROTOCOL=https` keinen HTTPS-Redirect erzeugt.
|
||||||
|
|
||||||
|
Verifikation aus dem Prometheus-Container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PW="$(cat /run/secrets/dawarich_metrics_password)"
|
||||||
|
curl -i -u "prometheus:${PW}" http://dawarich_app:3000/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- `200`: Scrape ist direkt funktionsfaehig.
|
||||||
|
- `301`/`308` nach HTTPS: `http_headers` mit `X-Forwarded-Proto: https` im Prometheus-Job beibehalten.
|
||||||
|
- `403 Blocked host`: `dawarich_app` in `APPLICATION_HOSTS` aufnehmen.
|
||||||
|
|
||||||
|
## Grafana
|
||||||
|
|
||||||
|
Der Read-only-User `dawarich_grafana_ro` wird beim ersten DB-Init durch `postgres/initdb/20-grafana-readonly.sh` angelegt.
|
||||||
|
|
||||||
|
Bei einer bereits initialisierten DB das Script einmal manuell im DB-Container ausfuehren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec dawarich_db /docker-entrypoint-initdb.d/20-grafana-readonly.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die produktive Provisionierung ist bereits in den vorhandenen Monitoring-Stack integriert:
|
||||||
|
|
||||||
|
- Datasource: `monitoring/grafana/provisioning/datasources/dawarich.yml`
|
||||||
|
- Dashboard: `monitoring/grafana/dashboards/dawarich.json`
|
||||||
|
- Grafana haengt an `backend_net`, damit `dawarich_db:5432` erreichbar ist.
|
||||||
|
- `DAWARICH_GRAFANA_RO_PASSWORD` wird beim Grafana-Start aus `/mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt` exportiert.
|
||||||
|
|
||||||
|
## Home Assistant
|
||||||
|
|
||||||
|
Dawarich akzeptiert OwnTracks-kompatible Location-Punkte per:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://dawarich.kaleschke.info/api/v1/owntracks/points?api_key=<dawarich-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
`homeassistant-dawarich.example.yaml` enthaelt:
|
||||||
|
|
||||||
|
- `rest_command.dawarich_push_owntracks`
|
||||||
|
- Automation fuer `device_tracker`-State-Changes
|
||||||
|
- API-Key aus HA `secrets.yaml` als `dawarich_api_key`
|
||||||
|
|
||||||
|
Alternativ existiert eine HACS-Integration `dawarich-home-assistant`; die YAML-Variante hier bleibt absichtlich transparent und GitOps-lesbar.
|
||||||
|
|
||||||
|
## Backup mit Borg
|
||||||
|
|
||||||
|
Borg-relevante Daten liegen unter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/appdata/dawarich/postgres17
|
||||||
|
/mnt/user/appdata/dawarich/redis
|
||||||
|
/mnt/user/appdata/dawarich/shared
|
||||||
|
/mnt/user/appdata/dawarich/public
|
||||||
|
/mnt/user/appdata/dawarich/watched
|
||||||
|
/mnt/user/appdata/dawarich/storage
|
||||||
|
/mnt/user/appdata/secrets/dawarich_*.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Primaerer Restore-Weg fuer die DB sollte ein logischer Dump plus Appdaten sein. Raw-Postgres-Verzeichnisse sind nur fuer gleiches Major/PostGIS-Image und sauberen Shutdown geeignet.
|
||||||
|
|
||||||
|
Empfohlener Dump vor Borg:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec dawarich_db pg_dump -U dawarich -d dawarich_production -Fc > /mnt/user/backups/borg/dumps/latest/dawarich.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
- Kein `latest` verwenden.
|
||||||
|
- Vor jedem Update Release Notes lesen, besonders bei Dawarich und PostGIS.
|
||||||
|
- Dawarich App und Sidekiq muessen immer dasselbe Image-Tag nutzen.
|
||||||
|
- PostGIS-Major-/Minor-Wechsel getrennt planen und vorher Dump plus Restore-Probe erstellen.
|
||||||
|
- Image-Digests nach Review bewusst aktualisieren.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
1. Komodo Stack stoppen.
|
||||||
|
2. Vorherigen Git-Commit mit altem Image-Tag/Digest deployen.
|
||||||
|
3. Falls nur App-Code gewechselt wurde: Stack starten und Healthchecks pruefen.
|
||||||
|
4. Falls DB-Migrationen gelaufen sind: DB aus `dawarich.dump` in einen frischen PostGIS-17-Container restoren; kein blindes Zurueckkopieren eines Live-Postgres-Verzeichnisses.
|
||||||
|
5. Dawarich UI, `/api/v1/health`, Prometheus-Scrape und HA-Push testen.
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
name: dawarich
|
||||||
|
|
||||||
|
x-dawarich-image: &dawarich_image freikin/dawarich:1.8.1@sha256:7c70f2169e848ed77ae1cec01dd10ec4a73a70a785d4e4d248db1735c0bc25ed
|
||||||
|
|
||||||
|
services:
|
||||||
|
dawarich_db:
|
||||||
|
image: postgis/postgis:17-3.5-alpine@sha256:fc07e7a034e013d50ada575673b798ca6277e000b8364e39e217f612d94bd9a5
|
||||||
|
container_name: dawarich_db
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 1G
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/dawarich_postgres_password
|
||||||
|
GRAFANA_DB_USER: ${GRAFANA_DB_USER}
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- dawarich_db_data:/var/lib/postgresql/data
|
||||||
|
- dawarich_shared:/var/shared
|
||||||
|
- ./postgres/initdb:/docker-entrypoint-initdb.d:ro
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
|
secrets:
|
||||||
|
- dawarich_postgres_password
|
||||||
|
- dawarich_grafana_ro_password
|
||||||
|
expose:
|
||||||
|
- "5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
dawarich_redis:
|
||||||
|
image: redis:7-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||||
|
container_name: dawarich_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -lc
|
||||||
|
- |
|
||||||
|
exec redis-server \
|
||||||
|
--save 900 1 \
|
||||||
|
--save 300 10 \
|
||||||
|
--appendonly no \
|
||||||
|
--requirepass "$$(cat /run/secrets/dawarich_redis_password)"
|
||||||
|
volumes:
|
||||||
|
- dawarich_redis_data:/data
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
|
secrets:
|
||||||
|
- dawarich_redis_password
|
||||||
|
expose:
|
||||||
|
- "6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli -a \"$$(cat /run/secrets/dawarich_redis_password)\" --raw incr ping >/dev/null"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
dawarich_app:
|
||||||
|
image: *dawarich_image
|
||||||
|
container_name: dawarich_app
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -lc
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
export DATABASE_PASSWORD="$$(cat /run/secrets/dawarich_postgres_password)"
|
||||||
|
export REDIS_URL="redis://:$$(cat /run/secrets/dawarich_redis_password)@dawarich_redis:6379/0"
|
||||||
|
export SECRET_KEY_BASE="$$(cat /run/secrets/dawarich_secret_key_base)"
|
||||||
|
export METRICS_PASSWORD="$$(cat /run/secrets/dawarich_metrics_password)"
|
||||||
|
exec web-entrypoint.sh bin/rails server -p 3000 -b ::
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ}
|
||||||
|
RAILS_ENV: production
|
||||||
|
DATABASE_HOST: dawarich_db
|
||||||
|
DATABASE_PORT: "5432"
|
||||||
|
DATABASE_USERNAME: ${POSTGRES_USER}
|
||||||
|
DATABASE_NAME: ${POSTGRES_DB}
|
||||||
|
APPLICATION_HOSTS: ${APPLICATION_HOSTS}
|
||||||
|
APPLICATION_PROTOCOL: https
|
||||||
|
TIME_ZONE: ${TZ}
|
||||||
|
SELF_HOSTED: "true"
|
||||||
|
STORE_GEODATA: "true"
|
||||||
|
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
||||||
|
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
||||||
|
RAILS_LOG_TO_STDOUT: "true"
|
||||||
|
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||||
|
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||||
|
SIDEKIQ_METRICS_URL: http://dawarich_sidekiq:9394/metrics
|
||||||
|
BACKGROUND_PROCESSING_CONCURRENCY: ${BACKGROUND_PROCESSING_CONCURRENCY}
|
||||||
|
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS}
|
||||||
|
volumes:
|
||||||
|
- dawarich_public:/var/app/public
|
||||||
|
- dawarich_watched:/var/app/tmp/imports/watched
|
||||||
|
- dawarich_storage:/var/app/storage
|
||||||
|
- dawarich_db_data:/dawarich_db_data:ro
|
||||||
|
networks:
|
||||||
|
- frontend_net
|
||||||
|
- backend_net
|
||||||
|
secrets:
|
||||||
|
- dawarich_postgres_password
|
||||||
|
- dawarich_redis_password
|
||||||
|
- dawarich_secret_key_base
|
||||||
|
- dawarich_metrics_password
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO - --header=\"Host: ${DAWARICH_HOST}\" --header=\"X-Forwarded-Proto: https\" http://127.0.0.1:3000/api/v1/health | grep -q '\"status\"[[:space:]]*:[[:space:]]*\"ok\"'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 30
|
||||||
|
start_period: 30s
|
||||||
|
depends_on:
|
||||||
|
dawarich_db:
|
||||||
|
condition: service_healthy
|
||||||
|
dawarich_redis:
|
||||||
|
condition: service_healthy
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=frontend_net
|
||||||
|
|
||||||
|
# Public API-key endpoints for mobile apps and Home Assistant pushes.
|
||||||
|
- traefik.http.routers.dawarich-api.rule=Host(`${DAWARICH_HOST}`) && (Path(`/api/v1/health`) || Path(`/api/v1/owntracks/points`) || Path(`/api/v1/overland/batches`) || Path(`/api/v1/traccar/points`))
|
||||||
|
- traefik.http.routers.dawarich-api.entrypoints=websecure
|
||||||
|
- traefik.http.routers.dawarich-api.tls=true
|
||||||
|
- traefik.http.routers.dawarich-api.tls.certresolver=le
|
||||||
|
- traefik.http.routers.dawarich-api.priority=100
|
||||||
|
- traefik.http.routers.dawarich-api.middlewares=secure-headers@file
|
||||||
|
- traefik.http.routers.dawarich-api.service=dawarich
|
||||||
|
|
||||||
|
# UI and all other routes require Authelia ForwardAuth.
|
||||||
|
- traefik.http.routers.dawarich.rule=Host(`${DAWARICH_HOST}`)
|
||||||
|
- traefik.http.routers.dawarich.entrypoints=websecure
|
||||||
|
- traefik.http.routers.dawarich.tls=true
|
||||||
|
- traefik.http.routers.dawarich.tls.certresolver=le
|
||||||
|
- traefik.http.routers.dawarich.priority=10
|
||||||
|
- traefik.http.routers.dawarich.middlewares=authelia@file,secure-headers@file
|
||||||
|
- traefik.http.routers.dawarich.service=dawarich
|
||||||
|
- traefik.http.services.dawarich.loadbalancer.server.port=3000
|
||||||
|
|
||||||
|
dawarich_sidekiq:
|
||||||
|
image: *dawarich_image
|
||||||
|
container_name: dawarich_sidekiq
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -lc
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
export DATABASE_PASSWORD="$$(cat /run/secrets/dawarich_postgres_password)"
|
||||||
|
export REDIS_URL="redis://:$$(cat /run/secrets/dawarich_redis_password)@dawarich_redis:6379/0"
|
||||||
|
export SECRET_KEY_BASE="$$(cat /run/secrets/dawarich_secret_key_base)"
|
||||||
|
export METRICS_PASSWORD="$$(cat /run/secrets/dawarich_metrics_password)"
|
||||||
|
exec sidekiq-entrypoint.sh sidekiq
|
||||||
|
environment:
|
||||||
|
TZ: ${TZ}
|
||||||
|
RAILS_ENV: production
|
||||||
|
DATABASE_HOST: dawarich_db
|
||||||
|
DATABASE_PORT: "5432"
|
||||||
|
DATABASE_USERNAME: ${POSTGRES_USER}
|
||||||
|
DATABASE_NAME: ${POSTGRES_DB}
|
||||||
|
APPLICATION_HOSTS: ${APPLICATION_HOSTS}
|
||||||
|
APPLICATION_PROTOCOL: https
|
||||||
|
TIME_ZONE: ${TZ}
|
||||||
|
SELF_HOSTED: "true"
|
||||||
|
STORE_GEODATA: "true"
|
||||||
|
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
||||||
|
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
||||||
|
RAILS_LOG_TO_STDOUT: "true"
|
||||||
|
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||||
|
PROMETHEUS_EXPORTER_PORT: "9394"
|
||||||
|
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||||
|
BACKGROUND_PROCESSING_CONCURRENCY: "5"
|
||||||
|
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS}
|
||||||
|
volumes:
|
||||||
|
- dawarich_public:/var/app/public
|
||||||
|
- dawarich_watched:/var/app/tmp/imports/watched
|
||||||
|
- dawarich_storage:/var/app/storage
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
|
secrets:
|
||||||
|
- dawarich_postgres_password
|
||||||
|
- dawarich_redis_password
|
||||||
|
- dawarich_secret_key_base
|
||||||
|
- dawarich_metrics_password
|
||||||
|
expose:
|
||||||
|
- "9394"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pgrep -f sidekiq >/dev/null"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 30
|
||||||
|
start_period: 30s
|
||||||
|
depends_on:
|
||||||
|
dawarich_db:
|
||||||
|
condition: service_healthy
|
||||||
|
dawarich_redis:
|
||||||
|
condition: service_healthy
|
||||||
|
dawarich_app:
|
||||||
|
condition: service_healthy
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
|
backend_net:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dawarich_db_data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/postgres17
|
||||||
|
dawarich_redis_data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/redis
|
||||||
|
dawarich_shared:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/shared
|
||||||
|
dawarich_public:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/public
|
||||||
|
dawarich_watched:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/watched
|
||||||
|
dawarich_storage:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/user/appdata/dawarich/storage
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
dawarich_postgres_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_postgres_password.txt
|
||||||
|
dawarich_redis_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_redis_password.txt
|
||||||
|
dawarich_secret_key_base:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_secret_key_base.txt
|
||||||
|
dawarich_metrics_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_metrics_password.txt
|
||||||
|
dawarich_grafana_ro_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": false,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 16,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"basemap": {
|
||||||
|
"config": {},
|
||||||
|
"name": "Layer 0",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"mouseWheelZoom": true,
|
||||||
|
"showAttribution": true,
|
||||||
|
"showDebug": false,
|
||||||
|
"showMeasure": false,
|
||||||
|
"showScale": true,
|
||||||
|
"showZoom": true
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"showLegend": true,
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "dark-green"
|
||||||
|
},
|
||||||
|
"opacity": 0.55,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 4,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"latitude": "latitude",
|
||||||
|
"longitude": "longitude",
|
||||||
|
"mode": "coords"
|
||||||
|
},
|
||||||
|
"name": "Location points",
|
||||||
|
"tooltip": true,
|
||||||
|
"type": "markers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "details"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"allLayers": true,
|
||||||
|
"id": "fit",
|
||||||
|
"lat": 51,
|
||||||
|
"lon": 10,
|
||||||
|
"zoom": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Location Points",
|
||||||
|
"type": "geomap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 70,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "km"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Kilometers per Month",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 70,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Points per Day",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"refresh": "5m",
|
||||||
|
"schemaVersion": 41,
|
||||||
|
"tags": [
|
||||||
|
"dawarich",
|
||||||
|
"location"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-30d",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "Dawarich",
|
||||||
|
"uid": "dawarich",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Dawarich PostgreSQL
|
||||||
|
uid: dawarich-postgres
|
||||||
|
type: postgres
|
||||||
|
access: proxy
|
||||||
|
url: dawarich_db:5432
|
||||||
|
database: dawarich_production
|
||||||
|
user: dawarich_grafana_ro
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
sslmode: disable
|
||||||
|
postgresVersion: 1700
|
||||||
|
timescaledb: false
|
||||||
|
secureJsonData:
|
||||||
|
password: $DAWARICH_GRAFANA_RO_PASSWORD
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Add `dawarich_api_key` to Home Assistant `secrets.yaml`.
|
||||||
|
# The endpoint is the current OwnTracks-compatible Dawarich endpoint:
|
||||||
|
# https://<host>/api/v1/owntracks/points?api_key=<api-key>
|
||||||
|
|
||||||
|
rest_command:
|
||||||
|
dawarich_push_owntracks:
|
||||||
|
url: "https://dawarich.kaleschke.info/api/v1/owntracks/points?api_key={{ api_key }}"
|
||||||
|
method: POST
|
||||||
|
content_type: "application/json"
|
||||||
|
payload: >-
|
||||||
|
{
|
||||||
|
"_type": "location",
|
||||||
|
"lat": {{ latitude }},
|
||||||
|
"lon": {{ longitude }},
|
||||||
|
"tst": {{ timestamp }},
|
||||||
|
"acc": {{ accuracy | default(0) }},
|
||||||
|
"alt": {{ altitude | default(0) }},
|
||||||
|
"batt": {{ battery | default(0) }},
|
||||||
|
"tid": "{{ tracker_id[:2] }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
automation:
|
||||||
|
- id: dawarich_push_device_tracker_location
|
||||||
|
alias: Dawarich - push device tracker location
|
||||||
|
mode: queued
|
||||||
|
max: 20
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- device_tracker.your_phone
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: >-
|
||||||
|
{{ trigger.to_state is not none
|
||||||
|
and state_attr(trigger.entity_id, 'latitude') is number
|
||||||
|
and state_attr(trigger.entity_id, 'longitude') is number }}
|
||||||
|
action:
|
||||||
|
- service: rest_command.dawarich_push_owntracks
|
||||||
|
data:
|
||||||
|
api_key: !secret dawarich_api_key
|
||||||
|
tracker_id: "{{ trigger.entity_id.split('.')[1] }}"
|
||||||
|
latitude: "{{ state_attr(trigger.entity_id, 'latitude') }}"
|
||||||
|
longitude: "{{ state_attr(trigger.entity_id, 'longitude') }}"
|
||||||
|
accuracy: "{{ state_attr(trigger.entity_id, 'gps_accuracy') | default(0, true) }}"
|
||||||
|
altitude: "{{ state_attr(trigger.entity_id, 'altitude') | default(0, true) }}"
|
||||||
|
battery: "{{ state_attr(trigger.entity_id, 'battery_level') | default(0, true) }}"
|
||||||
|
timestamp: "{{ as_timestamp(trigger.to_state.last_updated) | int }}"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
GRAFANA_USER="${GRAFANA_DB_USER:-dawarich_grafana_ro}"
|
||||||
|
GRAFANA_PASSWORD="$(cat /run/secrets/dawarich_grafana_ro_password)"
|
||||||
|
|
||||||
|
sql_ident() {
|
||||||
|
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_literal() {
|
||||||
|
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
|
||||||
|
}
|
||||||
|
|
||||||
|
DB_IDENT="$(sql_ident "$POSTGRES_DB")"
|
||||||
|
USER_IDENT="$(sql_ident "$GRAFANA_USER")"
|
||||||
|
USER_LITERAL="$(sql_literal "$GRAFANA_USER")"
|
||||||
|
PASSWORD_LITERAL="$(sql_literal "$GRAFANA_PASSWORD")"
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = ${USER_LITERAL}) THEN
|
||||||
|
EXECUTE 'CREATE ROLE ${USER_IDENT} LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
||||||
|
ELSE
|
||||||
|
EXECUTE 'ALTER ROLE ${USER_IDENT} WITH LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
GRANT CONNECT ON DATABASE ${DB_IDENT} TO ${USER_IDENT};
|
||||||
|
GRANT USAGE ON SCHEMA public TO ${USER_IDENT};
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${USER_IDENT};
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${USER_IDENT};
|
||||||
|
EOSQL
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Dawarich 1.8.1 serves the Prometheus endpoint on the web service at
|
||||||
|
# /metrics. Port 9394 is the internal Sidekiq metrics endpoint consumed by
|
||||||
|
# dawarich_app via SIDEKIQ_METRICS_URL.
|
||||||
|
#
|
||||||
|
# Prerequisites in monitoring/docker-compose.yml:
|
||||||
|
# - mount Docker secret/file `/mnt/user/appdata/secrets/dawarich_metrics_password.txt`
|
||||||
|
# into the Prometheus container at `/run/secrets/dawarich_metrics_password`
|
||||||
|
|
||||||
|
- job_name: dawarich
|
||||||
|
metrics_path: /metrics
|
||||||
|
basic_auth:
|
||||||
|
username: prometheus
|
||||||
|
password_file: /run/secrets/dawarich_metrics_password
|
||||||
|
http_headers:
|
||||||
|
X-Forwarded-Proto:
|
||||||
|
values:
|
||||||
|
- https
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- dawarich_app:3000
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
replace-with-a-long-random-grafana-readonly-password
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
replace-with-a-long-random-metrics-password
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
replace-with-a-long-random-postgres-password
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
replace-with-a-long-random-url-safe-redis-password
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
replace-with-output-of-openssl-rand-hex-64
|
||||||
@@ -48,7 +48,18 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
networks:
|
networks:
|
||||||
|
# immich_default (internal) = Erreichbarkeit durch immich-server.
|
||||||
|
# immich_egress (nicht-internal) = Outbound zu huggingface, damit ML die
|
||||||
|
# Modelle (CLIP ViT-B-32, buffalo_l) einmalig nach model-cache laedt.
|
||||||
|
# Ohne dieses Netz scheitert der Modell-Download an der DNS-Aufloesung
|
||||||
|
# (immich_default ist internal: true) -> Smart Search/Gesichtserkennung tot.
|
||||||
- immich_default
|
- immich_default
|
||||||
|
- immich_egress
|
||||||
|
dns:
|
||||||
|
# Egress-Netz braucht externe Aufloesung (huggingface.co); explizit nach
|
||||||
|
# docs/WORKFLOW.md "DNS-Regeln fuer Container", analog traefik/ddns-updater.
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
@@ -86,5 +97,10 @@ networks:
|
|||||||
name: immich_default
|
name: immich_default
|
||||||
internal: true
|
internal: true
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
immich_egress:
|
||||||
|
# Bewusst NICHT internal: nur fuer den ML-Modell-Download (Outbound).
|
||||||
|
# Nur immich_machine_learning haengt hier; DB/Redis bleiben in immich_default.
|
||||||
|
name: immich_egress
|
||||||
|
driver: bridge
|
||||||
frontend_net:
|
frontend_net:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
mail-archiver:
|
mail-archiver:
|
||||||
image: s1t5/mailarchiver@sha256:4ea7ecc47ad1dd2c523b85c3967574b61e39def1b6fd26edf874e21733c4018c
|
image: s1t5/mailarchiver@sha256:9ab6f51fa036c7869f64cb052a18f7bb8b9951a120ce1c03df43a273a20d3f59
|
||||||
container_name: mail-archiver
|
container_name: mail-archiver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ services:
|
|||||||
- traefik.http.services.mealie.loadbalancer.server.port=9000
|
- traefik.http.services.mealie.loadbalancer.server.port=9000
|
||||||
|
|
||||||
mealie-postgres:
|
mealie-postgres:
|
||||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
||||||
container_name: mealie-postgres
|
container_name: mealie-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
n8n:
|
n8n:
|
||||||
image: docker.n8n.io/n8nio/n8n:2.26.2@sha256:61ba01bc5e39304bbc928c9dbecd938c3a5cc1331b68affba6a34d0f654c43d9
|
image: docker.n8n.io/n8nio/n8n:2.27.3@sha256:a772d24e6b4f9b3848be5a57c5e45437eed1965bbbcefa2f9a93f4835b6639fa
|
||||||
container_name: n8n
|
container_name: n8n
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: nextcloud:33.0.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d
|
image: nextcloud:34.0.0-apache@sha256:851ca6ef9da101ce3c8a32ec7b6fc65a726b380b5f466307a54c17d32fb77c9a
|
||||||
container_name: nextcloud
|
container_name: nextcloud
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
|
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
|
||||||
|
|
||||||
nextcloud-postgres:
|
nextcloud-postgres:
|
||||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
||||||
container_name: nextcloud-postgres
|
container_name: nextcloud-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
super-productivity:
|
super-productivity:
|
||||||
image: johannesjo/super-productivity:v18.9.1@sha256:773760107344e739f4c29409f7842db66a1b167d50eb2c40248cb5b5b328652e
|
image: johannesjo/super-productivity:v18.12.0@sha256:2c84668a961b090dd931f6e117dde5195b7c674d8453e0d511b777c23c242bc8
|
||||||
container_name: super-productivity
|
container_name: super-productivity
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
unbound:
|
unbound:
|
||||||
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b
|
image: shaanmajid/unbound:1.25.1@sha256:6fa3d5257ff6d95ab16153c62fabfe256edc0db515f94755f5edeb1f2a2258ab
|
||||||
container_name: unbound
|
container_name: unbound
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
gitea:
|
gitea:
|
||||||
image: docker.gitea.com/gitea:1.26.2@sha256:7d13848af12645600a5f9d93ee2560daa9c6fa6b5b859b7bff3a5e1c0b661031
|
image: docker.gitea.com/gitea:1.26.4@sha256:8e25c717b8f748445e15ec46e0390f577cb628101184cb0a150d1dae126c1f39
|
||||||
container_name: gitea
|
container_name: gitea
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
+9
-1
@@ -1,6 +1,6 @@
|
|||||||
# Alert Rules
|
# Alert Rules
|
||||||
|
|
||||||
Stand: 2026-06-05
|
Stand: 2026-06-18
|
||||||
|
|
||||||
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
||||||
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
||||||
@@ -36,6 +36,14 @@ Skripten unter `services/posture-check/`.
|
|||||||
| `HomelabBorgBackupStale` | letztes Borg-Backup >30h | warning | Backup-Lauf nachholen/pruefen |
|
| `HomelabBorgBackupStale` | letztes Borg-Backup >30h | warning | Backup-Lauf nachholen/pruefen |
|
||||||
| `HomelabBorgLastJobFailed` | letzter Borg-Job fehlgeschlagen | critical | Borg-UI-Job-Log pruefen |
|
| `HomelabBorgLastJobFailed` | letzter Borg-Job fehlgeschlagen | critical | Borg-UI-Job-Log pruefen |
|
||||||
| `HomelabBorgLastJobCompletedWithWarnings` | letzter Borg-Job mit Warnungen | warning | Warnung im Borg-UI-Job lesen |
|
| `HomelabBorgLastJobCompletedWithWarnings` | letzter Borg-Job mit Warnungen | warning | Warnung im Borg-UI-Job lesen |
|
||||||
|
| `HomelabBorgDumpMissing` | erwartetes Dump-Artefakt fehlt im aktuellen Dump-Set | critical | `pre-backup-dumps.sh`/User-Script pruefen |
|
||||||
|
| `HomelabBorgDumpStale` | Dump-Artefakt >30h alt (Borg laeuft, Dumps eingefroren) | critical | `pre-backup-dumps.sh`/User-Script pruefen, nicht nur den Borg-Job |
|
||||||
|
| `HomelabBorgScopeSourceListMissing` | Repo-Quellliste fuer Borg-Drift-Check fehlt | critical | Borg-UI-Mount `/local/services/homelab-infra` und Repo-Pfad pruefen |
|
||||||
|
| `HomelabBorgScopeMissingSources` | Borg UI enthaelt nicht alle Pfade aus `ops/borg-ui/all-important-sources.txt` | critical | Live-Borg-Scope an Repo-Quelle angleichen |
|
||||||
|
| `HomelabBorgScopeExtraSources` | Borg UI enthaelt Pfade ausserhalb der Repo-Quellliste | warning | Doku oder Live-Scope bereinigen |
|
||||||
|
| `HomelabBorgRepositoryCheckStale` | letzter Borg-Check >14 Tage alt | warning | Borg-Repository-Check ausfuehren oder Scheduler pruefen |
|
||||||
|
| `HomelabBorgRetentionDisabled` | Scheduled Job fuehrt kein Prune aus | warning | Retention-Einstellung in Borg UI pruefen |
|
||||||
|
| `HomelabBorgCompactDisabled` | Scheduled Job fuehrt kein Compact aus | warning | Compact-Einstellung in Borg UI pruefen |
|
||||||
| `HomelabCriticalContainerDown` | kritischer Container fehlt | critical | Komodo/Docker-Status pruefen |
|
| `HomelabCriticalContainerDown` | kritischer Container fehlt | critical | Komodo/Docker-Status pruefen |
|
||||||
| `HomelabPrometheusTargetDown` | Scrape-Ziel down | critical | node-exporter/cadvisor/blackbox/traefik pruefen |
|
| `HomelabPrometheusTargetDown` | Scrape-Ziel down | critical | node-exporter/cadvisor/blackbox/traefik pruefen |
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Authelia OIDC fuer Apps - Plan & Runbook
|
# Authelia OIDC fuer Apps - Plan & Runbook
|
||||||
|
|
||||||
Stand: 2026-06-06. Authelia-Version: **v4.39.20**.
|
Stand: 2026-06-17. Authelia-Version: **v4.39.20**.
|
||||||
|
|
||||||
Ziel: App-uebergreifendes Single-Sign-On ueber Authelia als OpenID-Connect-Provider
|
Ziel: App-uebergreifendes Single-Sign-On ueber Authelia als OpenID-Connect-Provider
|
||||||
(`https://auth.kaleschke.info`). Statt pro App eigener Logins meldet man sich einmal
|
(`https://auth.kaleschke.info`). Statt pro App eigener Logins meldet man sich einmal
|
||||||
bei Authelia an (inkl. 2FA) und wird per OIDC an die App durchgereicht.
|
bei Authelia an (inkl. 2FA) und wird per OIDC an die App durchgereicht.
|
||||||
|
|
||||||
> **Status:** aktives Runbook. Grafana und Mealie sind seit 2026-06-06 live
|
> **Status:** aktives Runbook. Grafana und Mealie sind seit 2026-06-06 live
|
||||||
> und per Login-Smoke verifiziert. Der weitere Rollout bleibt additiv: lokale
|
> und per Login-Smoke verifiziert. Paperless ist seit 2026-06-17 technisch
|
||||||
> App-Logins bleiben als Fallback aktiv.
|
> verdrahtet (Authelia-Client + Stack-ENV-Secret + Service-Smoke gruen);
|
||||||
|
> finaler Browser-Login mit Operator-Account bleibt offen. Der Rollout bleibt
|
||||||
|
> additiv: lokale App-Logins bleiben als Fallback aktiv.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ docker exec authelia authelia crypto hash generate pbkdf2 \
|
|||||||
| 2 | Immich | `immich.kaleschke.info` | nativ (Admin-UI/Config-File) | s. u. (Familie) | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** nur `micha` hat Authelia-Account, Familien-SSO-Nutzen entsteht erst mit Familien-Accounts; Immich ist mobil-lastig (hoechste Stoeranfaelligkeit) und braucht UI/Config-File. Erst nach Onboarding gezielt. Runbook bereit. |
|
| 2 | Immich | `immich.kaleschke.info` | nativ (Admin-UI/Config-File) | s. u. (Familie) | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** nur `micha` hat Authelia-Account, Familien-SSO-Nutzen entsteht erst mit Familien-Accounts; Immich ist mobil-lastig (hoechste Stoeranfaelligkeit) und braucht UI/Config-File. Erst nach Onboarding gezielt. Runbook bereit. |
|
||||||
| 3 | Nextcloud | `cloud.kaleschke.info` | App `user_oidc` (+occ) | s. u. | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** wie Immich; braucht `user_oidc`-App-Install + `occ`. Lokaler Login bleibt. Erst nach Onboarding. Runbook bereit. |
|
| 3 | Nextcloud | `cloud.kaleschke.info` | App `user_oidc` (+occ) | s. u. | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** wie Immich; braucht `user_oidc`-App-Install + `occ`. Lokaler Login bleibt. Erst nach Onboarding. Runbook bereit. |
|
||||||
| **4 ERLEDIGT 2026-06-06** | Mealie | `mealie.kaleschke.info` | nativ | `one_factor` | niedrig | **Live + Login verifiziert.** OIDC-Env additiv (lokaler Login bleibt), Secret als Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}`, `extra_hosts` noetig (s. Gotchas) |
|
| **4 ERLEDIGT 2026-06-06** | Mealie | `mealie.kaleschke.info` | nativ | `one_factor` | niedrig | **Live + Login verifiziert.** OIDC-Env additiv (lokaler Login bleibt), Secret als Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}`, `extra_hosts` noetig (s. Gotchas) |
|
||||||
| 5 | Paperless-ngx | `paperless.kaleschke.info` | `django-allauth` (Umgebungsvariablen) | `two_factor` | mittel | dokumentenlastig, Operator-nah |
|
| **5 TEILWEISE ERLEDIGT 2026-06-17** | Paperless-ngx | `paperless.kaleschke.info` | `django-allauth` (Umgebungsvariablen) | `one_factor` (hostseitiger Ist-Stand; `two_factor` spaeter moeglich) | mittel | **Authelia-Client + `${PAPERLESS_OIDC_SECRET}` in Stack-ENV gesetzt, Authelia-Config validiert, Paperless HTTP-Smoke `200`.** Lokaler Login bleibt Fallback; finaler Browser-Login mit Operator-Account offen. |
|
||||||
|
|
||||||
**Nicht OIDC:** Vaultwarden hat kein Standard-Endnutzer-OIDC (SSO ist Enterprise/Bitwarden-Feature) -> bleibt eigener Login. ntfy bleibt wie gehabt.
|
**Nicht OIDC:** Vaultwarden hat kein Standard-Endnutzer-OIDC (SSO ist Enterprise/Bitwarden-Feature) -> bleibt eigener Login. ntfy bleibt wie gehabt.
|
||||||
|
|
||||||
@@ -175,7 +177,8 @@ GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
|
|||||||
E-Mail-Claim. Stimmt die Authelia-E-Mail mit dem App-Account, wird verknuepft;
|
E-Mail-Claim. Stimmt die Authelia-E-Mail mit dem App-Account, wird verknuepft;
|
||||||
sonst legt die App (bei aktivem Signup) einen neuen User an.
|
sonst legt die App (bei aktivem Signup) einen neuen User an.
|
||||||
- **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret),
|
- **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret),
|
||||||
Mealie Stack-ENV `${...}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo.
|
Mealie Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}`, Paperless Stack-ENV
|
||||||
|
`${PAPERLESS_OIDC_SECRET}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo.
|
||||||
|
|
||||||
## Spaetere Feinschliffe vor breitem Rollout
|
## Spaetere Feinschliffe vor breitem Rollout
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,128 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-16 - Immich ML bekommt dediziertes Egress-Netz (Modell-Download)
|
||||||
|
|
||||||
|
**Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default`
|
||||||
|
(`internal: true`) in einem neuen, bewusst **nicht**-internen Compose-Netz
|
||||||
|
`immich_egress` und bekommt explizit `dns: 1.1.1.1/8.8.8.8`. Damit kann ML die
|
||||||
|
Modelle (CLIP `ViT-B-32__openai`, Gesichtserkennung `buffalo_l`) einmalig von
|
||||||
|
huggingface nach `model-cache` laden. DB und Redis bleiben unveraendert in
|
||||||
|
`immich_default` isoliert.
|
||||||
|
|
||||||
|
**Kontext:** Am 2026-06-16 live gemessen: ML lief zwar `healthy`, aber
|
||||||
|
`/cache` war 0 B und die Logs zeigten in Schleife
|
||||||
|
`NameResolutionError: Failed to resolve 'huggingface.co'`. Ursache: ML hing nur
|
||||||
|
im `internal: true`-Netz `immich_default` ohne Egress/DNS. Folge: Smart Search
|
||||||
|
und Gesichtserkennung waren faktisch tot, trotz gesundem Container (Healthcheck
|
||||||
|
prueft nur den HTTP-Endpoint, nicht die Modellverfuegbarkeit). Das Zielbild §7.4
|
||||||
|
sagte vorher "bleibt intern" und widersprach damit dem eigenen Einordnungsschema
|
||||||
|
§6 Schritt 2 ("braucht Internet -> nicht-internal").
|
||||||
|
|
||||||
|
**Alternativen:** (a) ML an `frontend_net` haengen — verworfen, weil die
|
||||||
|
unauthentifizierte ML-API dann im geteilten Web-Netz erreichbar waere.
|
||||||
|
(b) `immich_default` auf nicht-internal umstellen — verworfen, weil dann auch
|
||||||
|
Postgres/Redis Outbound-Internet bekaemen (Least-Privilege-Verlust). Das
|
||||||
|
dediziertes Egress-Netz isoliert den Internetbedarf auf genau ML.
|
||||||
|
|
||||||
|
**Review-Trigger:** Immich-Update, das ML-Modelle anders bezieht; Wunsch nach
|
||||||
|
vollstaendigem Air-Gap (dann Modelle offline vorseeden statt Egress); oder wenn
|
||||||
|
ML weitere Modelle braucht (Egress bleibt dafuer noetig).
|
||||||
|
|
||||||
|
## 2026-06-13 - Wetter-/Langzeitarchiv: HA schreibt nach InfluxDB 3 Core
|
||||||
|
|
||||||
|
**Entscheidung:** Home Assistant schreibt die Ecowitt-Sensoren
|
||||||
|
(`sensor.gw3000a_*`) dauerhaft nach InfluxDB 3 Core (DB `homeassistant`),
|
||||||
|
visualisiert im Grafana-Dashboard `ha-weather-archive` ("Wetterarchiv
|
||||||
|
KalliHome"). HA wurde dem bestehenden `monitoring_net` als zusaetzliches Netz
|
||||||
|
hinzugefuegt und schreibt intern an `monitoring-influxdb3-core:8181`
|
||||||
|
(v2-Write-API) - kein Host-Port, keine LAN-Exposition. Das war die im
|
||||||
|
Ecowitt-Eintrag offen gelassene Reachability-Entscheidung (Alternative:
|
||||||
|
LAN-Bind 8181).
|
||||||
|
|
||||||
|
**Kontext:** Gewuenscht war ein echtes Langzeit-Wetterarchiv unabhaengig von
|
||||||
|
HAs kurzer SQLite-Historie. HAs eingebaute Langzeit-Statistiken decken den Fall
|
||||||
|
stuendlich bereits ab; InfluxDB liefert volle Aufloesung und eigene Grafana-
|
||||||
|
Dashboards. InfluxDB 3 Core kennt nur Admin-Tokens (keine feingranularen
|
||||||
|
Scopes), daher hat der HA-Schreibtoken vollen Admin-Zugriff auf die
|
||||||
|
Monitoring-InfluxDB - bewusst akzeptiert (Operator-Freigabe), unabhaengig
|
||||||
|
widerrufbar, Token nur in Appdata-Secrets (`ha_influxdb_token` + HA
|
||||||
|
`secrets.yaml`).
|
||||||
|
|
||||||
|
**Betriebsstand 2026-06-13:** HA im `monitoring_net`, Writer aktiv (Daten in
|
||||||
|
Measurements `°C`, `%`, `hPa`, `km/h`, `W/m²`, `mm`, `lx`, `°`), zweite
|
||||||
|
Grafana-Datasource `ha-weather-influx` (DB `homeassistant`) und Dashboard
|
||||||
|
provisioniert. Glance zeigt zusaetzlich eine Live-Wetterkachel direkt aus der
|
||||||
|
HA-API (`GLANCE_HA_TOKEN`).
|
||||||
|
|
||||||
|
**Review-Trigger:** InfluxDB-3-Enterprise mit Token-Scopes (dann HA-Token
|
||||||
|
einschraenken), Wegfall des Monitoring-Stacks, oder Neubewertung der
|
||||||
|
HA-Internet-Exposition (HA haengt jetzt auch im Observability-Netz).
|
||||||
|
|
||||||
|
## 2026-06-13 - SolarEdge lokal ueber Modbus TCP angebunden
|
||||||
|
|
||||||
|
**Entscheidung:** SolarEdge wird in Home Assistant lokal ueber
|
||||||
|
`solaredge_modbus_multi` angebunden, nicht ueber die SolarEdge-Cloud-API. Der
|
||||||
|
Wechselrichter ist im LAN als `192.168.178.111` erreichbar, MAC-OUI
|
||||||
|
`84:D6:C5` gehoert zu SolarEdge, Modbus TCP laeuft auf Port `1502`, Device-ID
|
||||||
|
`1`. Die Integration liefert Inverter-, Smart-Meter- und Batterie-Entitaeten.
|
||||||
|
|
||||||
|
**Kontext:** Der Operator kann im SolarEdge-Portal keinen API-Key erzeugen; das
|
||||||
|
fruehere Setup lief bereits lokal. Der alte in der Doku genannte VONETS-Adapter
|
||||||
|
`192.168.178.71` ist nicht erreichbar und bleibt kein verlaesslicher Zielpfad.
|
||||||
|
Die native HA-Core-Integration `solaredge` waere Cloud-Polling mit Site-ID/API-
|
||||||
|
Key; `solaredge_local` erwartet dagegen die lokale HTTP-SetApp-API unter
|
||||||
|
`/web/v1/status`, die am Wechselrichter nicht offen ist. Der vorhandene
|
||||||
|
HACS-/Custom-Component-Pfad `solaredge_modbus_multi` v3.2.5 passt zur realen
|
||||||
|
Schnittstelle und wurde ohne neue Downloads wiederverwendet.
|
||||||
|
|
||||||
|
**Betriebsstand 2026-06-13:** Config-Entry `SolarEdge Local` ist `loaded`,
|
||||||
|
Polling alle 60 Sekunden, Meter- und Batterie-Erkennung aktiv, Extras und
|
||||||
|
Power-Control-Schreibfunktionen deaktiviert. Relevante Energy-Dashboard-
|
||||||
|
Kandidaten:
|
||||||
|
`sensor.solaredge_local_i1_ac_energy`,
|
||||||
|
`sensor.solaredge_local_i1_m1_ac_energy_imported`,
|
||||||
|
`sensor.solaredge_local_i1_m1_ac_energy_exported`,
|
||||||
|
`sensor.solaredge_local_i1_b1_energy_import`,
|
||||||
|
`sensor.solaredge_local_i1_b1_energy_export`. Nach der Integration wurde ein
|
||||||
|
HA-native Backup erzeugt:
|
||||||
|
`/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_14.59_48645373.tar`.
|
||||||
|
Das HA Energy Dashboard wurde anschliessend mit Netz, PV und Speicher aus
|
||||||
|
SolarEdge Local konfiguriert und per `energy/validate` ohne Issues geprueft.
|
||||||
|
Kosten/Preise bleiben bis zur Tibber-Anbindung leer. Nach dieser UI-State-
|
||||||
|
Aenderung wurde ein weiteres HA-native Backup erzeugt:
|
||||||
|
`/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_15.59_25670583.tar`.
|
||||||
|
|
||||||
|
**Trade-off:** Die lokale Modbus-Integration passt zum Prinzip "lokal vor
|
||||||
|
Cloud" und liefert deutlich bessere Betriebsdaten als die Cloud-API, ist aber
|
||||||
|
eine HACS-/Custom-Integration und damit nicht durch HA-Core getestet. Bei
|
||||||
|
Problemen zuerst Integration deaktivieren oder auf HA-Core-Cloud-Polling
|
||||||
|
zurueckfallen, sobald Site-ID/API-Key verfuegbar sind.
|
||||||
|
|
||||||
|
**Review-Trigger:** HA-Core-Upgrade mit Custom-Integration-Warnungen,
|
||||||
|
Ausfaelle von `192.168.178.111:1502`, Wechselrichtertausch/IP-Aenderung,
|
||||||
|
oder wenn Energie-Automationen schreibende Power-Control-Funktionen brauchen.
|
||||||
|
|
||||||
|
## 2026-06-13 - Ecowitt-Ingress: LAN-only Host-Bind 8123 umgesetzt
|
||||||
|
|
||||||
|
**Entscheidung:** Home Assistant bekommt einen LAN-only Host-Bind
|
||||||
|
`192.168.178.58:8123:8123` (nur LAN-IP, nicht `0.0.0.0`/WAN). Das Ecowitt-GW3000
|
||||||
|
pusht per HTTP direkt an den HA-Webhook. Damit ist die offene
|
||||||
|
Phase-2-Entscheidung (Eintrag 2026-06-12) zugunsten des LAN-Bind-Fallbacks
|
||||||
|
entschieden; ein Umbau des globalen Traefik HTTP-zu-HTTPS-Redirects entfaellt,
|
||||||
|
weil Ecowitt rein im LAN pusht und Traefik gar nicht braucht.
|
||||||
|
|
||||||
|
**Kontext:** Der globale `web`->`websecure`-Redirect auf EntryPoint-Ebene laesst
|
||||||
|
sich nicht sauber selektiv aushebeln. Der LAN-Bind ist analog zur dokumentierten
|
||||||
|
InfluxDB-8181-Ausnahme, WAN-sicher (FRITZ!Box forwardet nur 443 auf Traefik) und
|
||||||
|
ohne Traefik-Umbau. Der HA-Webhook ist nicht `local_only`; Schutz ist die
|
||||||
|
128-bit-Zufalls-Webhook-ID. Restrisiko: der Pfad ist theoretisch auch ueber
|
||||||
|
Traefik/443 erreichbar, praktisch aber unratbar.
|
||||||
|
|
||||||
|
**Review-Trigger:** Wenn der Webhook haerter abgesichert werden soll
|
||||||
|
(Traefik-IPAllowList auf `/api/webhook/` oder `local_only`), oder bei Ausbau
|
||||||
|
auf Ecowitt-Langzeitspeicherung in InfluxDB.
|
||||||
|
|
||||||
## 2026-06-12 - Home Assistant als Container im GitOps-Stack
|
## 2026-06-12 - Home Assistant als Container im GitOps-Stack
|
||||||
|
|
||||||
**Entscheidung:** Home Assistant laeuft neu als `homeassistant` Container im
|
**Entscheidung:** Home Assistant laeuft neu als `homeassistant` Container im
|
||||||
@@ -30,6 +152,22 @@ kein Zielpfad mehr; HAOS bleibt die Alternative, falls Add-on-Komfort,
|
|||||||
Matter/Thread/HomeKit-Discovery oder Host-nahe HA-Funktionen wichtiger werden
|
Matter/Thread/HomeKit-Discovery oder Host-nahe HA-Funktionen wichtiger werden
|
||||||
als GitOps-Konformitaet.
|
als GitOps-Konformitaet.
|
||||||
|
|
||||||
|
**Betriebsstand 2026-06-13:** Owner-Onboarding ist abgeschlossen, die
|
||||||
|
temporaere Authelia-Onboarding-Guard-Middleware ist entfernt, `smart-home`
|
||||||
|
existiert als Komodo-Stack mit Gitea-Webhook, HA-native `backup.create` erzeugt
|
||||||
|
ein lesbares Backup-Artefakt, und der Mosquitto-Broker besteht einen
|
||||||
|
authentifizierten Publish/Subscribe-Smoke. Die Restore-Probe wurde am
|
||||||
|
2026-06-13 erfolgreich abgeschlossen: HA-native Backup + Mosquitto-Appdata +
|
||||||
|
Fachrepo-Clone wurden isoliert gestartet, HA HTTP/API/check_config waren gruen,
|
||||||
|
MQTT Publish/Subscribe und retained Topic nach Broker-Restart waren gruen.
|
||||||
|
Report: `/mnt/user/backups/restore-reports/homeassistant-2026-06-13.md`.
|
||||||
|
Die HA-MQTT-Integration wurde anschliessend am 2026-06-13 ueber den
|
||||||
|
Home-Assistant-Config-Flow verbunden; Config-Entry `smarthome-mosquitto` ist
|
||||||
|
`loaded`, Mosquitto sieht den HA-Client mit User `homeassistant`, und
|
||||||
|
`check_config` ist gruen. Damit ist die Foundation abgeschlossen. Naechster
|
||||||
|
Produktivschritt ist Tibber, danach SolarEdge mit bewusster Entscheidung
|
||||||
|
zwischen schneller Cloud-Integration und lokalem Modbus-TCP.
|
||||||
|
|
||||||
**Review-Trigger:** Viele mDNS-/SSDP-abhaengige lokale Integrationen
|
**Review-Trigger:** Viele mDNS-/SSDP-abhaengige lokale Integrationen
|
||||||
(HomeKit, Cast, Matter/Thread), Bedarf an HA-Add-ons als Betriebsstandard,
|
(HomeKit, Cast, Matter/Thread), Bedarf an HA-Add-ons als Betriebsstandard,
|
||||||
oder wiederholte Probleme durch Bridge-Netzwerkbetrieb.
|
oder wiederholte Probleme durch Bridge-Netzwerkbetrieb.
|
||||||
|
|||||||
@@ -532,8 +532,13 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
|
|||||||
### Windows-Workstation `baerchen`
|
### Windows-Workstation `baerchen`
|
||||||
|
|
||||||
`baerchen` ist die Operator-Workstation und haelt den lokalen Clone unter
|
`baerchen` ist die Operator-Workstation und haelt den lokalen Clone unter
|
||||||
`G:\Gitea_Clone\homelab-infra`. Fuer einen schnellen Windows-Bare-Metal-Restore
|
`G:\Gitea_Clone\homelab-infra`. Zusaetzlich liegt auf der externen Platte `H:`
|
||||||
existiert ein Veeam-Agent-Image-Workflow.
|
(`H:\kallilab-nearline-backups`) eine taegliche Nearline-Kopie der DB-Dumps,
|
||||||
|
Gitea-Bundles und des DR-Kits als **lokale Fallback-Restore-Quelle**, falls
|
||||||
|
Unraid/Hetzner nicht erreichbar sind. Restore-Weg und Pflicht-Frische-Pruefung:
|
||||||
|
`ops/h-drive-nearline/README.md` Abschnitt "Restore aus H:/ (DR-Fall)". Fuer
|
||||||
|
einen schnellen Windows-Bare-Metal-Restore existiert ein
|
||||||
|
Veeam-Agent-Image-Workflow.
|
||||||
|
|
||||||
Wichtige Pfade und Artefakte:
|
Wichtige Pfade und Artefakte:
|
||||||
|
|
||||||
|
|||||||
+15
-11
@@ -1,6 +1,6 @@
|
|||||||
# Master To-do - KalliLab CORE
|
# Master To-do - KalliLab CORE
|
||||||
|
|
||||||
Typ: Status/To-do · Stand: 2026-06-12 · Status: aktiv
|
Typ: Status/To-do · Stand: 2026-06-21 · Status: aktiv
|
||||||
|
|
||||||
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
|
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
|
||||||
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
|
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
|
||||||
@@ -23,17 +23,21 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie.
|
|||||||
| Family-Onboarding erster Termin | Operator | Checkliste ist fertig (`docs/FAMILY_ONBOARDING.md` Abschnitt "Erster Onboarding-Termin"). Personen/Geraete festlegen, Reihenfolge Vaultwarden -> Immich -> Mealie pro Person abarbeiten | `docs/FAMILY_ONBOARDING.md` |
|
| Family-Onboarding erster Termin | Operator | Checkliste ist fertig (`docs/FAMILY_ONBOARDING.md` Abschnitt "Erster Onboarding-Termin"). Personen/Geraete festlegen, Reihenfolge Vaultwarden -> Immich -> Mealie pro Person abarbeiten | `docs/FAMILY_ONBOARDING.md` |
|
||||||
| Restore-Test Unraid OS Flash (Stick-Boot) | Operator | Artefakt-Validierung 2026-06-05 erledigt (`ops/maintenance/check-unraid-flash-backup.sh`). **Verbleibt:** physischer Ersatzstick-Boot-Test, wenn ein Wegwerf-Stick bereitliegt | `ops/restore-tests/unraid-flash-runbook.md` |
|
| Restore-Test Unraid OS Flash (Stick-Boot) | Operator | Artefakt-Validierung 2026-06-05 erledigt (`ops/maintenance/check-unraid-flash-backup.sh`). **Verbleibt:** physischer Ersatzstick-Boot-Test, wenn ein Wegwerf-Stick bereitliegt | `ops/restore-tests/unraid-flash-runbook.md` |
|
||||||
| Restore-Test Tailscale | Operator | State-Validierung + Reconnect nur auf Wegwerf-Host/VM, danach Geraet in Tailscale-Admin entfernen | `ops/restore-tests/tailscale-runbook.md` |
|
| Restore-Test Tailscale | Operator | State-Validierung + Reconnect nur auf Wegwerf-Host/VM, danach Geraet in Tailscale-Admin entfernen | `ops/restore-tests/tailscale-runbook.md` |
|
||||||
| Authelia OIDC fuer Apps | Operator/Claude | Live: Grafana + Mealie (verifiziert), Paperless deployed (Login-Test offen). Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` |
|
| Authelia OIDC fuer Apps | Operator/Codex | Live: Grafana + Mealie login-verifiziert; Paperless Secret verdrahtet und Service-Smoke am 2026-06-17 gruen, finaler Browser-Login mit Operator-Account offen. Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` |
|
||||||
| Glance-v2-Widgets: Tokens setzen | Operator | In Komodo Stack-ENV fuer `ops-glance` setzen: `GLANCE_KOMODO_API_KEY`/`_SECRET` (Komodo read-only API-Key), `GLANCE_GITEA_TOKEN` (read-only, scope `read:repository`), `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`; bis dahin zeigen die neuen Widgets Fehler/leer. Speedtest-Widget: falls weiter 0.0, API-Response pruefen | `ops/glance/config/` |
|
| Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` |
|
||||||
| Audit-PDF aus `docs/` entfernen | Operator | `docs/KalliLab_CORE_Audit_2026-06-06.pdf` (untracked) extern ablegen (H:/ oder Documents-Share) und lokal loeschen; Binaerdateien gehoeren nicht ins GitOps-Repo | Doku-Regeln `docs/REPO_MAP.md` |
|
| Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` |
|
||||||
|
| Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | alertmanager/blackbox/loki/promtail + alertmanager-ntfy-bridge lokal auf Directory-Mounts umgestellt (grafana-provisioning war bereits Directory-Mount); `docker compose config` gruen. **Verbleibt:** Push + Komodo-Redeploy des monitoring-Stacks mit `--force-recreate` (Mount-Pfade aendern sich), danach Reload-/Alert-Smoke | `monitoring/docker-compose.yml` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Operator-Entscheidung
|
## Operator-Entscheidung
|
||||||
|
|
||||||
**Stand 2026-06-11: keine offenen Operator-Entscheidungen.**
|
|
||||||
Getroffene Entscheidungen mit Begruendung und Review-Trigger: `docs/DECISIONS.md`.
|
Getroffene Entscheidungen mit Begruendung und Review-Trigger: `docs/DECISIONS.md`.
|
||||||
|
|
||||||
|
| Thema | Entscheidung noetig | Quelle |
|
||||||
|
|---|---|---|
|
||||||
|
| `/mnt/user/projekte` Backup-Scope | Filebrowser serviert `projekte` (und ganze `documents`/`photos`), aber nur App-Unterordner sind im Borg-Scope. Entscheiden: `projekte` als read-only Borg-UI-Mount + Quelllisten-Eintrag aufnehmen, oder bewusst als "nur lokal, nicht DR-relevant" bestaetigen | `ops/borg-ui/BACKUP_SCOPE.md` Abschnitt "User-Daten-Shares ausserhalb des App-Scope" |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Geparkt
|
## Geparkt
|
||||||
@@ -49,11 +53,11 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
|
|||||||
| CrowdSec vor Traefik | breitere Attack Surface als nur `443/tcp` | `docs/DECISIONS.md` |
|
| CrowdSec vor Traefik | breitere Attack Surface als nur `443/tcp` | `docs/DECISIONS.md` |
|
||||||
| Nextcloud 2FA (Operator-TOTP) | OIDC-/SSO-Block erreicht die App-Login-Ebene | `docs/DECISIONS.md` |
|
| Nextcloud 2FA (Operator-TOTP) | OIDC-/SSO-Block erreicht die App-Login-Ebene | `docs/DECISIONS.md` |
|
||||||
| Hermes-Agent | Review-Deadline 2026-07-25; NAS-Stack bleibt deaktiviert | `docs/SERVICE_CATALOG.md` |
|
| Hermes-Agent | Review-Deadline 2026-07-25; NAS-Stack bleibt deaktiviert | `docs/SERVICE_CATALOG.md` |
|
||||||
| Tailnet-Konsole aufraeumen (Rest) | trivial, bei Gelegenheit: tote Node-Eintraege (`kallilab-core`, alter `baerchen`) in der Tailscale-Admin-Konsole entfernen; optional State-Pfad `/mnt/user/appdata/tailscale` nach `_archive/` | `docs/NETWORK_INVENTORY.md` |
|
|
||||||
| Dedizierter SMB-User `veeam-baerchen` | nur wenn Unraid-User-/Share-Rechte bewusst angefasst werden | `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
| Dedizierter SMB-User `veeam-baerchen` | nur wenn Unraid-User-/Share-Rechte bewusst angefasst werden | `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
||||||
| Filebrowser-Mount-Scope | naechster Hardening-Sprint | `docs/SERVICE_CATALOG.md` |
|
| Filebrowser-Mount-Scope | naechster Hardening-Sprint | `docs/SERVICE_CATALOG.md` |
|
||||||
| Scrutiny Privileged-Ausnahme | nur mit klarer Begruendung aendern | `docs/SERVICE_CATALOG.md` |
|
| Scrutiny Privileged-Ausnahme | nur mit klarer Begruendung aendern | `docs/SERVICE_CATALOG.md` |
|
||||||
| Immich Redis named volume | passende Wartung am Immich-Stack | `docs/SERVICE_CATALOG.md` |
|
| Immich Redis named volume | passende Wartung am Immich-Stack | `docs/SERVICE_CATALOG.md` |
|
||||||
|
| Komodo keys named volume | gemeinsames Wartungsfenster mit Operator | Live-Volume `komodo_komodo_keys` nach `/mnt/user/appdata/komodo/keys` migrieren, Compose anpassen, Periphery-Reconnect pruefen, dann in Borg-Scope aufnehmen |
|
||||||
| Storage-Wachstum (zweite NVMe, zweite Array-Disk, ZFS/BTRFS) | Trigger aus Capacity-Doku | `docs/STORAGE_LAYOUT.md`, `docs/CAPACITY_AND_LIFECYCLE.md` |
|
| Storage-Wachstum (zweite NVMe, zweite Array-Disk, ZFS/BTRFS) | Trigger aus Capacity-Doku | `docs/STORAGE_LAYOUT.md`, `docs/CAPACITY_AND_LIFECYCLE.md` |
|
||||||
| Wiederkehrende Restore-Drills | laufend nach Kadenz, inkl. quartalsweisem Frische-Negativtest (`run-restore-checks.sh freshness-negative`) | `docs/RESTORE_MATRIX.md`, `ops/restore-tests/schedule.md` |
|
| Wiederkehrende Restore-Drills | laufend nach Kadenz, inkl. quartalsweisem Frische-Negativtest (`run-restore-checks.sh freshness-negative`) | `docs/RESTORE_MATRIX.md`, `ops/restore-tests/schedule.md` |
|
||||||
| Doku-Quartals-Gaertnern (~15 min) | quartalsweise, erster Lauf mit Q3-Review ab 2026-07-01: Datiertes archivieren, Done-/Review-Logs kuerzen, tote Links pruefen | `docs/REPO_MAP.md` Doku-Regeln |
|
| Doku-Quartals-Gaertnern (~15 min) | quartalsweise, erster Lauf mit Q3-Review ab 2026-07-01: Datiertes archivieren, Done-/Review-Logs kuerzen, tote Links pruefen | `docs/REPO_MAP.md` Doku-Regeln |
|
||||||
@@ -70,11 +74,11 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
|
|||||||
|
|
||||||
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
|
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
|
||||||
|
|
||||||
- **2026-06-12** Komodo-Stack-Hygiene-Check aktiv: `services/posture-check/komodo-stack-hygiene.sh` + Unraid User Script `komodo-stack-hygiene-weekly` (Sonntag 05:00). Faengt die `immich_new`-Klasse (Stack ohne Repo, `project_missing`, Compose ohne Stack, Hash-Drift). Erster Lauf: 6 Warnings, 0 Critical.
|
- **2026-06-17** Offene TODOs gegen Live-Stand abgeglichen: Paperless-OIDC-Secret verdrahtet und Service-Smoke gruen; alter Tailscale-Docker-State nach `_archive/tailscale-removed-2026-06-06/` verschoben; Tailnet-Restpunkt geschlossen.
|
||||||
- **2026-06-12** Immich Komodo-Stack bereinigt: `immich_new` auf `immich` korrigiert, Gitea-Account `Micha` gesetzt, per Komodo aus `apps/immich/docker-compose.yml` neu deployed. Verifiziert: `deployed_hash == latest_hash`, `immich_new_count=0`, alle vier Container healthy, HTTP 200, DB-Smoke `11983` Assets, Drift-Alert resolved.
|
- **2026-06-17** Repo-Hygiene abgeschlossen: Glance-Widget-Tokens sind in Runtime gesetzt, Audit-PDF liegt extern unter `H:\kallilab-recovery\audits`, Worktree clean.
|
||||||
- **2026-06-11** Host-DNS-Fallback aktiv: `eth0` DNS2 = `192.168.178.1` (FRITZ!Box) zusaetzlich zu AdGuard. AdGuard-SPOF fuer Image-Pulls entschaerft; der dokumentierte Bulk-Deploy-Vorfall kann strukturell nicht wiederkommen.
|
- **2026-06-17** Komodo/Gitea-Webhooks normalisiert: aktive Komodo-Hooks fuer `Micha/homelab-infra` nutzen Branch-Filter `master`; DB-Backup vor Host-Hotfix erstellt. Workflow-Regel nachgezogen.
|
||||||
- **2026-06-11** Hetzner Storage Box: automatische Snapshots aktiv (taeglich 05:30 UTC, 7 Tage Retention). Schliesst das Ransomware-/Fehlbedienungs-Risiko gegen das Off-site-Backup. Siehe `docs/DECISIONS.md`.
|
- **2026-06-19** Backup-Hardening live verifiziert: Borg-Scope-Drift 0 (alle 33 Quellen konfiguriert), Dumps frisch (11/11 present), neue Dump-Alerts aktiv (25 Regeln, 0 feuern). Prometheus-`alerts.yml`-Stale-Handle (FUSE-Einzeldatei-Mount) per `--force-recreate` behoben und anschliessend dauerhaft auf Directory-Mount umgestellt (recreated, 25 Regeln aktiv).
|
||||||
- **2026-06-11** Immich Image-Tags von `release` auf `v2.7.5` gepinnt (Server + ML, Digests unveraendert): Renovate-PRs zeigen ab jetzt sichtbare Versionsspruenge statt stiller Digest-Bumps.
|
- **2026-06-18** Backup-Audit-Hardening: Dump-Frische-Metriken + Alerts `HomelabBorgDumpMissing/Stale`, Freshness-Checks + Nearline-Pull um `n8n`/`globals` ergaenzt, 4 Tier-2-Container in Critical-Watch, Scope-Doku fuer `projekte`/Hermes praezisiert. H:-Nearline (still seit 2026-06-04) nachgeholt + Task neu registriert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+17
-18
@@ -1,7 +1,7 @@
|
|||||||
# Network Inventory - KalliLab CORE
|
# Network Inventory - KalliLab CORE
|
||||||
|
|
||||||
Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; FRITZ!Box-Remote-Dienste aus; IPv6-Exposure technisch und per UI entschaerft; Tailscale-Inventar am 2026-06-05 real gemessen.
|
Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; FRITZ!Box-Remote-Dienste aus; IPv6-Exposure technisch und per UI entschaerft; Tailscale-Inventar am 2026-06-17 real gemessen.
|
||||||
Letzte Pruefung: 2026-06-05 (Tailscale-Inventar), 2026-06-01 (Router/Ports)
|
Letzte Pruefung: 2026-06-17 (Tailscale-Inventar), 2026-06-01 (Router/Ports)
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
|
|||||||
| Komponente | Rolle | Adresse | Bemerkung |
|
| Komponente | Rolle | Adresse | Bemerkung |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| AdGuard Home | LAN DNS / Filter | Host `192.168.178.58`, Docker `172.23.0.3` | DNS auf Port 53; Admin soll nur via Tailscale-IP `100.80.98.33:8082` erreichbar sein |
|
| AdGuard Home | LAN DNS / Filter | Host `192.168.178.58`, Docker `172.23.0.3` | DNS auf Port 53; Admin soll nur via Tailscale-IP `100.80.98.33:8082` erreichbar sein |
|
||||||
| Unbound | Rekursiver Resolver | Docker `dns_net` | Upstream fuer AdGuard |
|
| Unbound | DNSSEC-validierender Forwarding-Resolver | Docker `dns_net` | Upstream fuer AdGuard; forwardet per DoT zu Cloudflare, keine Root-Rekursion |
|
||||||
| Cloudflare | Authoritative DNS | extern | DNS-Challenge fuer TLS |
|
| Cloudflare | Authoritative DNS | extern | DNS-Challenge fuer TLS |
|
||||||
| Router | DHCP DNS-Verteilung | TBD | Muss auf AdGuard zeigen, falls so betrieben |
|
| Router | DHCP DNS-Verteilung | TBD | Muss auf AdGuard zeigen, falls so betrieben |
|
||||||
|
|
||||||
@@ -57,18 +57,16 @@ Gemessen am 2026-06-05 per read-only SSH auf den Host (`tailscale status`,
|
|||||||
| Subnet Router | **Ja, aktiv.** Host advertised und ist Primary fuer `192.168.178.0/24` (`Self.PrimaryRoutes: ["192.168.178.0/24"]`, ebenfalls in `AllowedIPs`). Das LAN ist also fuer das gesamte Tailnet ueber diesen Subnet-Router erreichbar — bewusst gemessener Ist-Zustand, **kein** "keine Route" wie zuvor vermutet. |
|
| Subnet Router | **Ja, aktiv.** Host advertised und ist Primary fuer `192.168.178.0/24` (`Self.PrimaryRoutes: ["192.168.178.0/24"]`, ebenfalls in `AllowedIPs`). Das LAN ist also fuer das gesamte Tailnet ueber diesen Subnet-Router erreichbar — bewusst gemessener Ist-Zustand, **kein** "keine Route" wie zuvor vermutet. |
|
||||||
| ACL-Policy extern dokumentiert | **Angewendet 2026-06-06** — restriktive Tag-basierte `grants`-Policy live (`tag:server`/`tag:operator`, `tag:family` schlafend). Default-Allow entfernt, verifiziert. Details im Block unten. |
|
| ACL-Policy extern dokumentiert | **Angewendet 2026-06-06** — restriktive Tag-basierte `grants`-Policy live (`tag:server`/`tag:operator`, `tag:family` schlafend). Default-Allow entfernt, verifiziert. Details im Block unten. |
|
||||||
|
|
||||||
### Tailnet-Geraete (Snapshot 2026-06-05)
|
### Tailnet-Geraete (Snapshot 2026-06-17)
|
||||||
|
|
||||||
| Tailscale-IP | Node | OS | Status |
|
| Tailscale-IP | Node | OS | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `100.80.98.33` | kallilabcore | linux | aktiv (Host, Subnet-Router) |
|
| `100.80.98.33` | kallilabcore | linux | aktiv (Host, Subnet-Router) |
|
||||||
| `100.78.133.37` | baerchen-1 | windows | aktiv (aktuelle Operator-Workstation, direct) |
|
| `100.78.133.37` | baerchen-1 | windows | aktiv (aktuelle Operator-Workstation, direct) |
|
||||||
| `100.105.203.21` | baerchen | windows | offline, zuletzt vor ~1 Tag gesehen (Alt-Node) |
|
| `100.73.83.55` | iphone-14 | iOS | bekannt, aktuell offline |
|
||||||
| `100.73.83.55` | iphone-14 | iOS | bekannt |
|
|
||||||
| `100.112.0.90` | kallilab-core | linux | **am 2026-06-06 entfernt.** War der redundante userspace-only `Tailscale-Docker`-Stack (`host-services/tailscale/`). Komodo-Stack gestoppt+destroyed, Repo-Pfad per `git rm` entfernt, Container weg (read-only verifiziert). Node-Eintrag in der Admin-Konsole noch zu entfernen. |
|
|
||||||
|
|
||||||
> **Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host hat **zwei**
|
> **Historischer Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host
|
||||||
> `tailscaled`-Prozesse:
|
> hatte damals **zwei** `tailscaled`-Prozesse:
|
||||||
>
|
>
|
||||||
> 1. **Native Unraid-Plugin** = `kallilabcore` (100.80.98.33). Prozess
|
> 1. **Native Unraid-Plugin** = `kallilabcore` (100.80.98.33). Prozess
|
||||||
> `/usr/local/sbin/tailscaled -statedir /boot/config/plugins/tailscale/state
|
> `/usr/local/sbin/tailscaled -statedir /boot/config/plugins/tailscale/state
|
||||||
@@ -89,9 +87,10 @@ Gemessen am 2026-06-05 per read-only SSH auf den Host (`tailscale status`,
|
|||||||
> (Operator), `git rm host-services/tailscale/`, Glance-Widget entfernt, und
|
> (Operator), `git rm host-services/tailscale/`, Glance-Widget entfernt, und
|
||||||
> Architektur-/Service-Catalog-/DR-/CLAUDE-Doku auf "natives Plugin" nachgezogen.
|
> Architektur-/Service-Catalog-/DR-/CLAUDE-Doku auf "natives Plugin" nachgezogen.
|
||||||
> Read-only verifiziert: Container weg, nur noch der native `tailscaled` mit
|
> Read-only verifiziert: Container weg, nur noch der native `tailscaled` mit
|
||||||
> `tailscale1`, Subnet-Route + Operator-Zugriff intakt. Offen: Node-Eintraege
|
> `tailscale1`, Subnet-Route + Operator-Zugriff intakt. Nachpruefung 2026-06-17:
|
||||||
> `kallilab-core` und alter `baerchen` in der Admin-Konsole entfernen; State-Pfad
|
> `tailscale status --self=false` zeigt nur noch `baerchen-1` und `iphone-14`;
|
||||||
> `/mnt/user/appdata/tailscale` bei Gelegenheit nach `_archive/` (kein Sofort-Loeschen).
|
> der alte State-Pfad `/mnt/user/appdata/tailscale` ist weg und liegt archiviert
|
||||||
|
> unter `/mnt/user/appdata/_archive/tailscale-removed-2026-06-06/`.
|
||||||
>
|
>
|
||||||
> **Doku-Korrektur erledigt:** `docs/RESTORE_MATRIX.md` zeigt jetzt auf den
|
> **Doku-Korrektur erledigt:** `docs/RESTORE_MATRIX.md` zeigt jetzt auf den
|
||||||
> funktionalen State `/boot/config/plugins/tailscale/state` (im Flash-Backup)
|
> funktionalen State `/boot/config/plugins/tailscale/state` (im Flash-Backup)
|
||||||
@@ -155,8 +154,8 @@ erhalten.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Geraete-Tags (live):** `kallilabcore` = `tag:server`; `baerchen-1` + `iphone-14`
|
**Geraete-Tags (live):** `kallilabcore` = `tag:server`; `baerchen-1` + `iphone-14`
|
||||||
= `tag:operator`; `kallilab-core` (Docker) + alter `baerchen` bewusst untagged ->
|
= `tag:operator`. Alte Nodes `kallilab-core` und `baerchen` sind nicht mehr im
|
||||||
isoliert.
|
aktuellen Tailnet-Status sichtbar.
|
||||||
|
|
||||||
**Rollout-Protokoll 2026-06-06 (lockout-sicher, je Schritt read-only verifiziert):**
|
**Rollout-Protokoll 2026-06-06 (lockout-sicher, je Schritt read-only verifiziert):**
|
||||||
|
|
||||||
@@ -193,10 +192,10 @@ ist die vollstaendige Wahrheit.
|
|||||||
- Familien-Dienste/Ports konkretisieren — erst wenn ein reales Familiengeraet dazukommt.
|
- Familien-Dienste/Ports konkretisieren — erst wenn ein reales Familiengeraet dazukommt.
|
||||||
- **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack
|
- **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack
|
||||||
abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv.
|
abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv.
|
||||||
- **Tailnet-Konsole aufraeumen: ERLEDIGT 2026-06-06** — Node-Eintraege `kallilab-core`
|
- **Tailnet-Konsole/Altstate aufraeumen: ERLEDIGT 2026-06-17** — Node-Eintraege
|
||||||
und alter Offline-`baerchen` aus der Admin-Konsole entfernt.
|
`kallilab-core` und alter Offline-`baerchen` sind im aktuellen Tailnet-Status
|
||||||
- State-Pfad `/mnt/user/appdata/tailscale` (vom entfernten Docker-Stack) bei
|
nicht mehr sichtbar; State-Pfad `/mnt/user/appdata/tailscale` vom entfernten
|
||||||
Gelegenheit nach `_archive/tailscale-removed-2026-06-06/` (kein Sofort-Loeschen).
|
Docker-Stack liegt unter `_archive/tailscale-removed-2026-06-06/`.
|
||||||
- Optionaler Off-LAN-Routentest: von einem Operator-Geraet im Mobilfunk
|
- Optionaler Off-LAN-Routentest: von einem Operator-Geraet im Mobilfunk
|
||||||
(nicht im Heim-LAN) ein LAN-Ziel ueber `192.168.178.0/24` erreichen, um die
|
(nicht im Heim-LAN) ein LAN-Ziel ueber `192.168.178.0/24` erreichen, um die
|
||||||
Subnet-Route end-to-end zu bestaetigen (im Heim-LAN nicht sauber isolierbar).
|
Subnet-Route end-to-end zu bestaetigen (im Heim-LAN nicht sauber isolierbar).
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ Grafana 13 wurde anschliessend manuell aus #7 `renovate/major-major-updates` ueb
|
|||||||
|
|
||||||
Komodo-Mongo laeuft bereits auf der erlaubten MongoDB-8.0-Schiene; ein offener Mongo-8-Renovate-PR ist aktuell nicht vorhanden.
|
Komodo-Mongo laeuft bereits auf der erlaubten MongoDB-8.0-Schiene; ein offener Mongo-8-Renovate-PR ist aktuell nicht vorhanden.
|
||||||
|
|
||||||
|
**2026-06-21 (Routine-Merge-Runde):** Sechs offene Renovate-PRs nach Sichtpruefung in einem Bulk-Deploy ueber den Komodo-Webhook gemergt: die Sammelgruppe `minor-and-patch-updates` (u. a. gitea 1.26.3, home-assistant 2026.6.4, alertmanager v0.33.0, influxdb 3.10.0-core, code-server 4.125.0, filebrowser, speedtest, super-productivity plus Digest-Refreshes fuer borg-ui/glances/scrutiny/mailarchiver/python), die reinen Digest-Refreshes fuer `unbound`, `traefik:v3.7` und `postgres:18.4` (gleiche Versionen) sowie n8n 2.26.2 -> 2.27.3 und der `nextcloud:33.0.5-apache` Digest-Refresh. Anschliessend nach Operator-Freigabe nachgezogen: Gitea 1.26.3 -> 1.26.4, cAdvisor v0.57.0 -> v0.60.1 und Nextcloud 33.0.5 -> 34.0.0.
|
||||||
|
|
||||||
## Erwartete erste PRs (historisch)
|
## Erwartete erste PRs (historisch)
|
||||||
|
|
||||||
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| Unraid OS Flash | Borg-Artefakt + optional Unraid Connect | `/boot/config` aus `unraid-flash-config.tar.gz` | `unraid-flash-config.tar.gz`, `.sha256`, Manifest | enthaelt sensible Host-Konfiguration, wie Secret-Material behandeln | Unraid USB Flash Creator / neuer Boot-Stick | Unraid bootet, Array-Zuordnung und Shares sind sichtbar |
|
| Unraid OS Flash | Borg-Artefakt + optional Unraid Connect | `/boot/config` aus `unraid-flash-config.tar.gz` | `unraid-flash-config.tar.gz`, `.sha256`, Manifest | enthaelt sensible Host-Konfiguration, wie Secret-Material behandeln | Unraid USB Flash Creator / neuer Boot-Stick | Unraid bootet, Array-Zuordnung und Shares sind sichtbar |
|
||||||
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
||||||
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert; Restore-Smoke am 2026-06-06 erfolgreich |
|
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert; Restore-Smoke am 2026-06-06 erfolgreich |
|
||||||
| Tailscale | Flash-Backup (funktional) / Share | **Funktional: `/boot/config/plugins/tailscale/state`** (native Unraid-Plugin-Instanz `kallilabcore`, Subnet-Router, im Flash-Backup gesichert). Der frueher hier genannte Pfad `/mnt/user/appdata/tailscale` gehoert zum **userspace-only Docker-Stack** `kallilab-core` (redundant, Abbau geplant — siehe `docs/NETWORK_INVENTORY.md`) | keine | Tailscale-State im jeweiligen State-Pfad | Host-Netz | Tailscale verbunden, Subnet-Route `192.168.178.0/24` aktiv |
|
| Tailscale | Flash-Backup (funktional) | **Funktional: `/boot/config/plugins/tailscale/state`** (native Unraid-Plugin-Instanz `kallilabcore`, Subnet-Router, im Flash-Backup gesichert). Der frueher genannte Pfad `/mnt/user/appdata/tailscale` gehoerte zum entfernten userspace-only Docker-Stack `kallilab-core` und ist seit 2026-06-17 nach `/mnt/user/appdata/_archive/tailscale-removed-2026-06-06/` verschoben; nicht mehr als aktive Restore-Quelle behandeln | keine | Tailscale-State im Flash-Backup; Archivpfad nur fuer Altanalyse | Host-Netz | Tailscale verbunden, Subnet-Route `192.168.178.0/24` aktiv |
|
||||||
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
||||||
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich; Restore-Smoke am 2026-06-06 erfolgreich |
|
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich; Restore-Smoke am 2026-06-06 erfolgreich |
|
||||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut; Restore-Smoke am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Config, frisches Test-Postgres, HTTP `/api/health` 200, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md` |
|
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut; Restore-Smoke am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Config, frisches Test-Postgres, HTTP `/api/health` 200, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md` |
|
||||||
@@ -52,7 +52,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|
|
||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.md` |
|
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `PAPERLESS_OIDC_SECRET`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 18, Redis, Traefik, Authelia OIDC | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.md`; OIDC-Secret am 2026-06-17 verdrahtet, lokaler Login bleibt Fallback |
|
||||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
||||||
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
||||||
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
||||||
@@ -60,8 +60,9 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
|
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
|
||||||
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
||||||
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
|
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
|
||||||
| Home Assistant | Borg + HA-native Backups + Fachrepo | `/mnt/user/appdata/homeassistant` inkl. `.storage`, `secrets.yaml`, `trusted_proxies.yaml`; Fach-YAML aus `/mnt/user/services/smart-home-kalli/home-assistant` | HA-native Backup-Artefakte unter `/mnt/user/appdata/homeassistant/backups` falls vorhanden; keine externe DB in Phase 1 | HA-Secrets in `secrets.yaml`, Integrations-Tokens in `.storage`, MQTT-Credentials, spaeter InfluxDB-Token | Traefik, `frontend_net`, `smarthome_net`, Mosquitto, Fachrepo-Clone | `https://home.kaleschke.info` zeigt Login, MQTT-Integration verbindet sich, `backup.create` funktioniert, Energy-Dashboard-Konfiguration bleibt erhalten |
|
| n8n | Borg + Dump | `/mnt/user/appdata/n8n/data` | `n8n.sqlite.dump`; Credentials sind nur mit dem passenden `N8N_ENCRYPTION_KEY` entschluesselbar | `N8N_ENCRYPTION_KEY`, GMX/OpenAI/Gitea-Credentials in n8n | Traefik, GMX IMAP, OpenAI API, Gitea API | UI startet, Owner-Login funktioniert, kritischer Mail->LLM->Gitea-Workflow ist vorhanden und deaktiviert/aktiv wie vor Restore |
|
||||||
| Smart-Home MQTT / Mosquitto | Borg / Share | `/mnt/user/appdata/mosquitto/config`, `/mnt/user/appdata/mosquitto/data`, `/mnt/user/appdata/mosquitto/log` | Mosquitto persistiert retained messages/subscriptions dateibasiert | `passwordfile`, `aclfile`, spaeter per-Device-User | `smarthome_net`, Home Assistant, spaeter ESPHome/Zigbee2MQTT | Container startet, HA kann sich authentifiziert verbinden, retained Testtopic bleibt nach Restart erhalten |
|
| Home Assistant | Borg + HA-native Backups + Fachrepo | `/mnt/user/appdata/homeassistant` inkl. `.storage`, `secrets.yaml`, `trusted_proxies.yaml`, `custom_components` (HACS, `solaredge_modbus_multi`); Fach-YAML aus `/mnt/user/services/smart-home-kalli/home-assistant` | HA-native Backup-Artefakte unter `/mnt/user/appdata/homeassistant/backups`; erstes Artefakt 2026-06-13 erzeugt und tar-lesbar (`backup.json`, `homeassistant.tar.gz`); Backup nach SolarEdge-Integration: `Custom_backup_2026.6.1_2026-06-13_14.59_48645373.tar`; Backup nach Energy-Dashboard-Konfiguration: `Custom_backup_2026.6.1_2026-06-13_15.59_25670583.tar`; keine externe DB in Phase 1 | HA-Secrets in `secrets.yaml`, Integrations-Tokens in `.storage`, MQTT-Credentials, Agent-API-Tokens als Host-Secrets `ha_token_codex`/`ha_token_claude` (nur mit erhaltenem `.storage`-Auth-State nutzbar), spaeter Tibber/InfluxDB-Tokens | Traefik, `frontend_net`, `smarthome_net`, Mosquitto, Fachrepo-Clone, SolarEdge-Wechselrichter `192.168.178.111:1502` | Restore-Test am 2026-06-13 erfolgreich: HA-native Backup + Mosquitto-Appdata + Fachrepo-Clone isoliert gestartet, HA HTTP/API/check_config gruen; produktiv danach HA-MQTT-Config-Entry `smarthome-mosquitto` geladen, SolarEdge Local `solaredge_modbus_multi` loaded mit 68 Entitaeten und Energy Dashboard fuer Netz/PV/Speicher per `energy/validate` ohne Issues; Report `/mnt/user/backups/restore-reports/homeassistant-2026-06-13.md` |
|
||||||
|
| Smart-Home MQTT / Mosquitto | Borg / Share | `/mnt/user/appdata/mosquitto/config`, `/mnt/user/appdata/mosquitto/data`, `/mnt/user/appdata/mosquitto/log` | Mosquitto persistiert retained messages/subscriptions dateibasiert | `passwordfile`, `aclfile`, spaeter per-Device-User | `smarthome_net`, Home Assistant, spaeter ESPHome/Zigbee2MQTT | Restore-Test am 2026-06-13 erfolgreich: authentifizierter Publish/Subscribe-Smoke mit `homeassistant`-User und retained Topic nach Broker-Restart gruen; produktiv verbindet sich HA als User `homeassistant` |
|
||||||
| Smart-Home Fachrepo | Gitea + Borg-Repo-Clone | `/mnt/user/services/smart-home-kalli` | keine | keine echten Secrets im Repo; `secrets-template/` nur Beispiele | Gitea, Home Assistant Mounts | `git status` sauber, HA liest `configuration.yaml` und `packages/` aus dem Clone |
|
| Smart-Home Fachrepo | Gitea + Borg-Repo-Clone | `/mnt/user/services/smart-home-kalli` | keine | keine echten Secrets im Repo; `secrets-template/` nur Beispiele | Gitea, Home Assistant Mounts | `git status` sauber, HA liest `configuration.yaml` und `packages/` aus dem Clone |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -104,6 +105,7 @@ Aktuell relevante Dump-Artefakte unter `/mnt/user/backups/borg/dumps/latest`:
|
|||||||
- `filebrowser.bolt.dump`
|
- `filebrowser.bolt.dump`
|
||||||
- `borg-ui.sqlite`
|
- `borg-ui.sqlite`
|
||||||
- `grafana.sqlite`
|
- `grafana.sqlite`
|
||||||
|
- `n8n.sqlite.dump`
|
||||||
- `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256` und Manifest
|
- `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256` und Manifest
|
||||||
- Monitoring-Stack: keine verpflichtenden Dump-Artefakte; Prometheus/Loki/Grafana named volumes sind Diagnose-/Dashboard-Zustand, keine primaere Restore-Quelle.
|
- Monitoring-Stack: keine verpflichtenden Dump-Artefakte; Prometheus/Loki/Grafana named volumes sind Diagnose-/Dashboard-Zustand, keine primaere Restore-Quelle.
|
||||||
- `komodo-mongo.archive.gz` (noch gesondert verifizieren)
|
- `komodo-mongo.archive.gz` (noch gesondert verifizieren)
|
||||||
@@ -165,6 +167,7 @@ Stand 2026-06-06. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
|
|||||||
| Borg UI | 3 | - | rebuildbar | - |
|
| Borg UI | 3 | - | rebuildbar | - |
|
||||||
| Filebrowser | 3 | - | rebuildbar | - |
|
| Filebrowser | 3 | - | rebuildbar | - |
|
||||||
| baerchen Windows Image | Workstation | 2026-06-06 | Full-Backup geschrieben; Recovery-USB-Boot, SMB-Mount und Restore-Point-Sichtpruefung erfolgreich; vor echtem Restore abgebrochen | nach Image-Aenderungen oder quartalsweise |
|
| baerchen Windows Image | Workstation | 2026-06-06 | Full-Backup geschrieben; Recovery-USB-Boot, SMB-Mount und Restore-Point-Sichtpruefung erfolgreich; vor echtem Restore abgebrochen | nach Image-Aenderungen oder quartalsweise |
|
||||||
|
| Home Assistant + Mosquitto | 2 | 2026-06-13 | HA-native Backup + Mosquitto-Appdata + Fachrepo-Clone, isolierte Testcontainer, HA HTTP/API/check_config, MQTT Publish/Subscribe + retained Topic nach Broker-Restart | vor groesseren Smart-Home-Aenderungen oder nach relevanten HA/Mosquitto-Architekturaenderungen |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -25,6 +25,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
||||||
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
||||||
|
| Paperless OIDC (Authelia) | Client Secret | Stack ENV `${PAPERLESS_OIDC_SECRET}` in `/mnt/user/services/stacks/paperless/apps/paperless/.env` (Komodo-Stack-ENV); pbkdf2-Hash im Authelia-Host-Config-Client `paperless` (kein Wert im Repo) | aktiv (2026-06-17) |
|
||||||
| Paperless-GPT | OpenAI API Key | Stack ENV `${OPENAI_API_KEY}`; nicht im Repo, nicht in Logs | aktiv |
|
| Paperless-GPT | OpenAI API Key | Stack ENV `${OPENAI_API_KEY}`; nicht im Repo, nicht in Logs | aktiv |
|
||||||
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `FILE__PASSWORD` | aktiv |
|
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `FILE__PASSWORD` | aktiv |
|
||||||
| Filebrowser | Admin Password | `/mnt/user/appdata/secrets/filebrowser_admin_password.txt` -> initialisierte SQLite-DB | aktiv |
|
| Filebrowser | Admin Password | `/mnt/user/appdata/secrets/filebrowser_admin_password.txt` -> initialisierte SQLite-DB | aktiv |
|
||||||
@@ -40,21 +41,30 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
||||||
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
||||||
| Gitea Push Mirror | GitHub fine-grained PAT fuer `michaelkaleschke-spec/homelab-infra` | Gitea Repository Mirror Settings, persistent in `/mnt/user/services/gitea/data`; kein Datei-Secret im Repo | aktiv |
|
| Gitea Push Mirror | GitHub fine-grained PAT fuer `michaelkaleschke-spec/homelab-infra` | Gitea Repository Mirror Settings, persistent in `/mnt/user/services/gitea/data`; kein Datei-Secret im Repo | aktiv |
|
||||||
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}`, `${GLANCE_KOMODO_API_KEY}`, `${GLANCE_KOMODO_API_SECRET}`, `${GLANCE_GITEA_TOKEN}`, `${GLANCE_PAPERLESS_TOKEN}`, `${GLANCE_MEALIE_TOKEN}` (alle read-only anlegen) | aktiv |
|
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}`, `${GLANCE_KOMODO_API_KEY}`, `${GLANCE_KOMODO_API_SECRET}`, `${GLANCE_GITEA_TOKEN}`, `${GLANCE_PAPERLESS_TOKEN}`, `${GLANCE_MEALIE_TOKEN}` (alle read-only anlegen), `${GLANCE_HA_TOKEN}` (HA Long-Lived Access Token; Glance nutzt nur `GET /api/states`) | aktiv |
|
||||||
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
||||||
| Nextcloud | Admin User | `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` -> `NEXTCLOUD_ADMIN_USER_FILE` | neu |
|
| Nextcloud | Admin User | `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` -> `NEXTCLOUD_ADMIN_USER_FILE` | neu |
|
||||||
| Nextcloud | Admin Password | `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` -> `NEXTCLOUD_ADMIN_PASSWORD_FILE` | neu |
|
| Nextcloud | Admin Password | `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` -> `NEXTCLOUD_ADMIN_PASSWORD_FILE` | neu |
|
||||||
| nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu |
|
| nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu |
|
||||||
| Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv |
|
| Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv |
|
||||||
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
|
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
|
||||||
|
| Healthchecks Dead-Man's-Switch (Borg Pre-Hook) | Ping-/Capability-URL | `/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600) **oder** ENV `HEALTHCHECKS_BORG_URL`/`HEALTHCHECKS_URL`, gelesen von `ops/borg-ui/scripts/pre-borg.sh`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
||||||
|
| Healthchecks Dead-Man's-Switch (Nearline-Pull) | Ping-/Capability-URL | baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` **oder** `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`, gelesen von `ops/h-drive-nearline/pull-critical-backups.ps1`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
||||||
| Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln |
|
| Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln |
|
||||||
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
|
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
|
||||||
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
|
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
|
||||||
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
||||||
|
| Home Assistant -> InfluxDB | Write Token (Wetterarchiv) | `/mnt/user/appdata/secrets/ha_influxdb_token` + HA `/config/secrets.yaml` Key `influxdb_ha_token`; InfluxDB-3-Core Named-Admin-Token (voller Zugriff, da Core keine Scopes kennt) | aktiv |
|
||||||
|
| Home Assistant | Agent API Tokens | `/mnt/user/appdata/secrets/ha_token_claude`, `ha_token_codex` (Long-Lived Access Tokens fuer read-only API-Zugriff durch KI-Agenten) | aktiv |
|
||||||
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
||||||
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
||||||
| Grafana OIDC (Authelia) | Client Secret | `/mnt/user/appdata/secrets/grafana_oidc_client_secret` (Klartext, chmod 600) -> Docker Secret `/run/secrets/grafana_oidc_client_secret` -> `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE`. Zugehoeriger pbkdf2-Hash liegt im Authelia-Host-Config-Client `grafana` (kein Wert im Repo) | aktiv (2026-06-06) |
|
| Grafana OIDC (Authelia) | Client Secret | `/mnt/user/appdata/secrets/grafana_oidc_client_secret` (Klartext, chmod 600) -> Docker Secret `/run/secrets/grafana_oidc_client_secret` -> `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE`. Zugehoeriger pbkdf2-Hash liegt im Authelia-Host-Config-Client `grafana` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
| Mealie OIDC (Authelia) | Client Secret | Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}` in `/mnt/user/services/stacks/mealie/apps/mealie/.env` (Komodo-Stack-ENV); pbkdf2-Hash im Authelia-Host-Config-Client `mealie` (kein Wert im Repo) | aktiv (2026-06-06) |
|
| Mealie OIDC (Authelia) | Client Secret | Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}` in `/mnt/user/services/stacks/mealie/apps/mealie/.env` (Komodo-Stack-ENV); pbkdf2-Hash im Authelia-Host-Config-Client `mealie` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
|
| Dawarich | DB Password | `/mnt/user/appdata/secrets/dawarich_postgres_password.txt` -> Docker Secret `/run/secrets/dawarich_postgres_password`; Postgres nutzt `POSTGRES_PASSWORD_FILE`, App/Sidekiq lesen per Entrypoint-Export | aktiv |
|
||||||
|
| Dawarich | Redis Password | `/mnt/user/appdata/secrets/dawarich_redis_password.txt` -> Docker Secret `/run/secrets/dawarich_redis_password`; Redis `--requirepass`, App/Sidekiq `REDIS_URL` | aktiv |
|
||||||
|
| Dawarich | Rails `SECRET_KEY_BASE` | `/mnt/user/appdata/secrets/dawarich_secret_key_base.txt` -> Docker Secret `/run/secrets/dawarich_secret_key_base` | geplant |
|
||||||
|
| Dawarich Metrics | Basic-Auth Password | `/mnt/user/appdata/secrets/dawarich_metrics_password.txt` -> Docker Secret `/run/secrets/dawarich_metrics_password`; Prometheus `password_file` | aktiv |
|
||||||
|
| Grafana -> Dawarich | Read-only DB Password | `/mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt` -> Docker Secret `/run/secrets/dawarich_grafana_ro_password`; Grafana-Env `DAWARICH_GRAFANA_RO_PASSWORD` | aktiv |
|
||||||
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
||||||
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
||||||
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
||||||
@@ -98,8 +108,16 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- redis_password.txt
|
|-- redis_password.txt
|
||||||
|-- borg_repo_passphrase.txt
|
|-- borg_repo_passphrase.txt
|
||||||
|-- influxdb3_admin_token.json
|
|-- influxdb3_admin_token.json
|
||||||
|
|-- ha_influxdb_token
|
||||||
|
|-- ha_token_claude
|
||||||
|
|-- ha_token_codex
|
||||||
|-- filebrowser_admin_password.txt
|
|-- filebrowser_admin_password.txt
|
||||||
|-- homelab_smtp_password.txt
|
|-- homelab_smtp_password.txt
|
||||||
|
|-- dawarich_postgres_password.txt
|
||||||
|
|-- dawarich_redis_password.txt
|
||||||
|
|-- dawarich_secret_key_base.txt
|
||||||
|
|-- dawarich_metrics_password.txt
|
||||||
|
|-- dawarich_grafana_ro_password.txt
|
||||||
`-- vaultwarden_admin_token.txt
|
`-- vaultwarden_admin_token.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,7 +129,8 @@ Weitere dokumentierte Secret-Pfade:
|
|||||||
- Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden.
|
- Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden.
|
||||||
- Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor. Der Wert ist laut Operator-Bestaetigung vom 2026-05-26 offline gesichert; Ablageort und Wert werden nicht im Repo dokumentiert.
|
- Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor. Der Wert ist laut Operator-Bestaetigung vom 2026-05-26 offline gesichert; Ablageort und Wert werden nicht im Repo dokumentiert.
|
||||||
- Gitea verwaltet den GitHub-Push-Mirror-PAT in den Repository-Mirror-Settings. Der Wert wird nicht dokumentiert und nicht in Dateien unter `docs/` oder `core/gitea/` geschrieben.
|
- Gitea verwaltet den GitHub-Push-Mirror-PAT in den Repository-Mirror-Settings. Der Wert wird nicht dokumentiert und nicht in Dateien unter `docs/` oder `core/gitea/` geschrieben.
|
||||||
- `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort und Redis-URL bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren.
|
- Die beiden Healthchecks-Ping-URLs (Borg-Pre-Hook, Nearline-Pull) sind Capability-URLs und werden wie Secrets behandelt; sie liegen nicht im Repo. Die Skript-Integration ist endpoint-agnostisch (Healthchecks.io-Cloud oder self-hosted). Ist keine URL gesetzt, sind die Pings ein No-Op und brechen keinen Lauf ab. Operator-Setup-Schritte: `ops/h-drive-nearline/README.md` Abschnitt "Externer Dead-Man's-Switch".
|
||||||
|
- `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort, Redis-URL und OIDC-Client-Secret bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren.
|
||||||
- `baerchen` nutzt fuer das Veeam-Backup aktuell den bestehenden SMB-User
|
- `baerchen` nutzt fuer das Veeam-Backup aktuell den bestehenden SMB-User
|
||||||
`micha`. Ein dedizierter SMB-User `veeam-baerchen` ist nur ein spaeteres
|
`micha`. Ein dedizierter SMB-User `veeam-baerchen` ist nur ein spaeteres
|
||||||
Hardening-Ziel, solange keine Unraid-User-/Share-Aenderungen gewuenscht sind.
|
Hardening-Ziel, solange keine Unraid-User-/Share-Aenderungen gewuenscht sind.
|
||||||
@@ -134,14 +153,14 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we
|
|||||||
|
|
||||||
| Stack | Stack-ENV-Variablen | Restore-Quelle (Reihenfolge) | Folgen bei Verlust aller Quellen |
|
| Stack | Stack-ENV-Variablen | Restore-Quelle (Reihenfolge) | Folgen bei Verlust aller Quellen |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `paperless-ngx` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | App-DB ist im Postgres-Cluster, Passwort muss in Postgres und Stack-ENV synchron neu gesetzt werden; Redis-URL ist deterministisch rekonstruierbar (Host, Port, Passwort), wenn Redis-Passwort-Datei vorliegt |
|
| `paperless-ngx` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `PAPERLESS_OIDC_SECRET` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | App-DB ist im Postgres-Cluster, Passwort muss in Postgres und Stack-ENV synchron neu gesetzt werden; Redis-URL ist deterministisch rekonstruierbar (Host, Port, Passwort), wenn Redis-Passwort-Datei vorliegt; OIDC-Client-Secret kann mit passendem Authelia-Client neu rotiert werden |
|
||||||
| `paperless-gpt` | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Paperless-Token kann in Paperless neu erzeugt werden; OpenAI-Key muss im OpenAI-Projekt rotiert/neu erstellt werden |
|
| `paperless-gpt` | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Paperless-Token kann in Paperless neu erzeugt werden; OpenAI-Key muss im OpenAI-Projekt rotiert/neu erstellt werden |
|
||||||
| `immich-server` | `IMMICH_DB_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | analog Paperless: Postgres-User-Passwort in `immich_postgres` und Stack-ENV gemeinsam zuruecksetzen |
|
| `immich-server` | `IMMICH_DB_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | analog Paperless: Postgres-User-Passwort in `immich_postgres` und Stack-ENV gemeinsam zuruecksetzen |
|
||||||
| `mail-archiver` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | DB-Connection-String enthaelt Postgres-Pass; App-Auth-Password fuer Web-UI |
|
| `mail-archiver` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | DB-Connection-String enthaelt Postgres-Pass; App-Auth-Password fuer Web-UI |
|
||||||
| `speedtest-tracker` | `APP_KEY`, `ADMIN_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | `APP_KEY` ist verschluesselungsrelevant; bei echtem Verlust App-State frisch initialisieren |
|
| `speedtest-tracker` | `APP_KEY`, `ADMIN_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | `APP_KEY` ist verschluesselungsrelevant; bei echtem Verlust App-State frisch initialisieren |
|
||||||
| `komodo-core` | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` | Vaultwarden -> externe Notiz (Henne-Ei: Komodo-Mongo-Dump ist hier **nicht** Restore-Quelle, weil Komodo dafuer schon laufen muesste) | siehe `docs/SERVICES_RECOVERY.md` Komodo-Bootstrap; ohne diese Werte ist der Self-Stack nicht reproduzierbar |
|
| `komodo-core` | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` | Vaultwarden -> externe Notiz (Henne-Ei: Komodo-Mongo-Dump ist hier **nicht** Restore-Quelle, weil Komodo dafuer schon laufen muesste) | siehe `docs/SERVICES_RECOVERY.md` Komodo-Bootstrap; ohne diese Werte ist der Self-Stack nicht reproduzierbar |
|
||||||
| `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest |
|
| `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest |
|
||||||
| `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY`, `GLANCE_KOMODO_API_KEY`, `GLANCE_KOMODO_API_SECRET`, `GLANCE_GITEA_TOKEN`, `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker, Komodo, Gitea, Paperless, Mealie) neu erzeugen | rebuildbar; alle read-only; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf |
|
| `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY`, `GLANCE_KOMODO_API_KEY`, `GLANCE_KOMODO_API_SECRET`, `GLANCE_GITEA_TOKEN`, `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`, `GLANCE_HA_TOKEN` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker, Komodo, Gitea, Paperless, Mealie, Home Assistant) neu erzeugen | rebuildbar; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf; `GLANCE_HA_TOKEN` muss zusaetzlich in `ops/glance/docker-compose.yml` durchgereicht werden |
|
||||||
| `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. |
|
| `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. |
|
||||||
|
|
||||||
### Komodo-Sonderfall
|
### Komodo-Sonderfall
|
||||||
|
|||||||
+10
-7
@@ -1,6 +1,6 @@
|
|||||||
# Service Catalog
|
# Service Catalog
|
||||||
|
|
||||||
Stand: 2026-06-02
|
Stand: 2026-06-13
|
||||||
|
|
||||||
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
||||||
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `100.80.98.33:8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkter DNS-Port 53 bleibt; Admin-Port ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP begrenzt (Operator-Entscheidung 2026-05-26) |
|
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `100.80.98.33:8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkter DNS-Port 53 bleibt; Admin-Port ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP begrenzt (Operator-Entscheidung 2026-05-26) |
|
||||||
| `unbound` | Upstream DNS Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert |
|
| `unbound` | DNSSEC-validierender Forwarding-Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert; forwardet per DoT zu Cloudflare, keine Root-Rekursion |
|
||||||
| `tailscale` | VPN/Remote-Zugang, Subnet-Router | **Natives Unraid-Plugin** `tailscale.plg` (nicht repo-/Komodo-verwaltet) | Tailscale | Host-Netz (`tailscale1`) | `/boot/config/plugins/tailscale/state` (im Flash-Backup) | Tier 1, State relevant | nein | Subnet-Router `192.168.178.0/24`; redundanter Docker-Stack `host-services/tailscale/` am 2026-06-06 entfernt |
|
| `tailscale` | VPN/Remote-Zugang, Subnet-Router | **Natives Unraid-Plugin** `tailscale.plg` (nicht repo-/Komodo-verwaltet) | Tailscale | Host-Netz (`tailscale1`) | `/boot/config/plugins/tailscale/state` (im Flash-Backup) | Tier 1, State relevant | nein | Subnet-Router `192.168.178.0/24`; redundanter Docker-Stack `host-services/tailscale/` am 2026-06-06 entfernt |
|
||||||
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net`, externe DNS-Resolver fuer GitHub-Push-Mirror | `/mnt/user/services/gitea/data` | Tier 1, `gitea.sqlite.dump` + Share; privater GitHub-Push-Mirror fuer Repo-Bootstrap | ja | SSH-Port 222 direkte Host-Port-Ausnahme; Push-Mirror nach `michaelkaleschke-spec/homelab-infra` reduziert das DR-Bootstrap-Risiko |
|
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net`, externe DNS-Resolver fuer GitHub-Push-Mirror | `/mnt/user/services/gitea/data` | Tier 1, `gitea.sqlite.dump` + Share; privater GitHub-Push-Mirror fuer Repo-Bootstrap | ja | SSH-Port 222 direkte Host-Port-Ausnahme; Push-Mirror nach `michaelkaleschke-spec/homelab-infra` reduziert das DR-Bootstrap-Risiko |
|
||||||
|
|
||||||
@@ -35,14 +35,17 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, Traefik | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja | DB/Redis Secrets bleiben bewusst Stack ENV; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, Traefik, Authelia OIDC | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja + Authelia | DB/Redis/OIDC Secrets bleiben bewusst Stack ENV; OIDC ist additiv via Authelia konfiguriert, lokaler Login bleibt Fallback; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
|
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
|
||||||
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
|
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
|
||||||
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
||||||
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
||||||
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
|
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default`, `immich_egress` | `model-cache` | rebuildbar | nein | keine Traefik-Route; `immich_egress` (nicht-internal) nur fuer Modell-Download zu huggingface, sonst scheitert Smart Search/Gesichtserkennung an DNS |
|
||||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
||||||
|
| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, Photon Reverse Geocoding, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2, Borg + `dawarich.dump` | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert. Reverse Geocoding nutzt standardmaessig `photon.komoot.io` ohne Key. App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. |
|
||||||
|
| `dawarich_db` | Dawarich PostGIS-Datenbank | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/postgres17`, `dawarich_postgres_password.txt`, `dawarich_grafana_ro_password.txt` | Dump `dawarich.dump`; raw DB nur bei gleichem PG/PostGIS und sauberem Shutdown | nein | PostGIS 17-3.5 Alpine; Grafana-Read-only-User `dawarich_grafana_ro` per Init-Script |
|
||||||
|
| `dawarich_redis` | Dawarich Cache/Queue-Backend | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/redis`, `dawarich_redis_password.txt` | Teil von Dawarich-Restore, aber aus DB/Appdaten rekonstruierbar | nein | Redis 7 Alpine, keine Host-Ports |
|
||||||
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
||||||
@@ -67,7 +70,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `filebrowser` | Datei-Browser fuer Documents/Photos/Projekte | `ops/filebrowser/docker-compose.yml` | `https://files.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/filebrowser/*`, `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte` | Tier 3, `filebrowser.bolt.dump` + Share | ja + Authelia | Breiter Appdata-Mount entfernt; Secrets und Traefik-Dynamic-Config sind nicht mehr ueber Filebrowser gemountet |
|
| `filebrowser` | Datei-Browser fuer Documents/Photos/Projekte | `ops/filebrowser/docker-compose.yml` | `https://files.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/filebrowser/*`, `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte` | Tier 3, `filebrowser.bolt.dump` + Share | ja + Authelia | Breiter Appdata-Mount entfernt; Secrets und Traefik-Dynamic-Config sind nicht mehr ueber Filebrowser gemountet |
|
||||||
| `code-server` | Web-Editor / Operations Workspace | `ops/code-server/docker-compose.yml` | `https://code.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` | Tier 3 | ja + Authelia | Passwort ueber LSIO `FILE__PASSWORD`; Workspaces beachten |
|
| `code-server` | Web-Editor / Operations Workspace | `ops/code-server/docker-compose.yml` | `https://code.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` | Tier 3 | ja + Authelia | Passwort ueber LSIO `FILE__PASSWORD`; Workspaces beachten |
|
||||||
| `monitoring-grafana` | zentrale Observability-UI fuer Metriken, Logs und InfluxDB | `monitoring/docker-compose.yml` | `https://monitoring.kaleschke.info` | Traefik + Authelia, Prometheus, Loki, InfluxDB 3 Core | named volume `grafana_data`, Provisioning unter `monitoring/grafana/provisioning`, Dashboards unter `monitoring/grafana/dashboards` | Tier 3, named volume | ja + Authelia | Admin-Passwort ueber `monitoring_grafana_admin_password.txt`; Zielbestand: `Homelab / Availability`, `Homelab / Host Overview`, `Homelab / Containers + Logs`, `Homelab / Family Status`, `Traefik Official Standalone Dashboard`; Dashboard-Importer ist optionales `bootstrap`-Profil fuer Traefik |
|
| `monitoring-grafana` | zentrale Observability-UI fuer Metriken, Logs und InfluxDB | `monitoring/docker-compose.yml` | `https://monitoring.kaleschke.info` | Traefik + Authelia, Prometheus, Loki, InfluxDB 3 Core | named volume `grafana_data`, Provisioning unter `monitoring/grafana/provisioning`, Dashboards unter `monitoring/grafana/dashboards` | Tier 3, named volume | ja + Authelia | Admin-Passwort ueber `monitoring_grafana_admin_password.txt`; Zielbestand: `Homelab / Availability`, `Homelab / Host Overview`, `Homelab / Containers + Logs`, `Homelab / Family Status`, `Traefik Official Standalone Dashboard`; Dashboard-Importer ist optionales `bootstrap`-Profil fuer Traefik |
|
||||||
| `monitoring-prometheus` | Metrik-Speicher fuer Homelab-Monitoring | `monitoring/docker-compose.yml`, `monitoring/prometheus/prometheus.yml`, `monitoring/prometheus/alerts.yml` | intern `http://prometheus:9090` | `monitoring_net`, node-exporter, cAdvisor, Traefik-Metrics, Blackbox Exporter, Alertmanager | named volume `prometheus_data` | Tier 3, transiente Metriken mit 30 Tagen Retention | nein | Scrapes: Prometheus, node-exporter, cAdvisor, Traefik `:8082`, `blackbox-http`; Prometheus-Regeln senden an Alertmanager und von dort nach ntfy |
|
| `monitoring-prometheus` | Metrik-Speicher fuer Homelab-Monitoring | `monitoring/docker-compose.yml`, `monitoring/prometheus/prometheus.yml`, `monitoring/prometheus/alerts.yml` | intern `http://prometheus:9090` | `monitoring_net`, `backend_net`, node-exporter, cAdvisor, Traefik-Metrics, Blackbox Exporter, Alertmanager | named volume `prometheus_data` | Tier 3, transiente Metriken mit 30 Tagen Retention | nein | Scrapes: Prometheus, node-exporter, cAdvisor, Traefik `:8082`, Dawarich intern `dawarich_app:3000`, `blackbox-http`; Prometheus-Regeln senden an Alertmanager und von dort nach ntfy |
|
||||||
| `monitoring-alertmanager` | Alert-Routing fuer Prometheus-Regeln | `monitoring/docker-compose.yml`, `monitoring/alertmanager/alertmanager.yml` | intern `:9093` | Prometheus, ntfy Bridge | named volume `alertmanager_data` | Tier 3 | nein | sendet firing und resolved Alerts an `monitoring-alertmanager-ntfy-bridge` |
|
| `monitoring-alertmanager` | Alert-Routing fuer Prometheus-Regeln | `monitoring/docker-compose.yml`, `monitoring/alertmanager/alertmanager.yml` | intern `:9093` | Prometheus, ntfy Bridge | named volume `alertmanager_data` | Tier 3 | nein | sendet firing und resolved Alerts an `monitoring-alertmanager-ntfy-bridge` |
|
||||||
| `monitoring-alertmanager-ntfy-bridge` | Alertmanager-Webhook nach ntfy Push | `monitoring/docker-compose.yml`, `monitoring/alertmanager-ntfy-bridge/bridge.py` | intern `:8080` | Alertmanager, `https://ntfy.kaleschke.info/homelab-alerts` | kein kritischer Zustand | rebuildbar | nein | formatiert Alertmanager JSON als ntfy Titel, Nachricht, Priority und Tags; keine Secrets |
|
| `monitoring-alertmanager-ntfy-bridge` | Alertmanager-Webhook nach ntfy Push | `monitoring/docker-compose.yml`, `monitoring/alertmanager-ntfy-bridge/bridge.py` | intern `:8080` | Alertmanager, `https://ntfy.kaleschke.info/homelab-alerts` | kein kritischer Zustand | rebuildbar | nein | formatiert Alertmanager JSON als ntfy Titel, Nachricht, Priority und Tags; keine Secrets |
|
||||||
| `monitoring-blackbox-exporter` | HTTP-Erreichbarkeitspruefungen als Uptime-Kuma-Ersatz | `monitoring/docker-compose.yml`, `monitoring/blackbox/blackbox.yml` | intern `:9115` | Prometheus, externe HTTPS-Ziele | kein kritischer Zustand | rebuildbar | nein | Uptime Kuma wurde 2026-05-25 nach erfolgreichem Blackbox-/Grafana-Smoke-Test entfernt |
|
| `monitoring-blackbox-exporter` | HTTP-Erreichbarkeitspruefungen als Uptime-Kuma-Ersatz | `monitoring/docker-compose.yml`, `monitoring/blackbox/blackbox.yml` | intern `:9115` | Prometheus, externe HTTPS-Ziele | kein kritischer Zustand | rebuildbar | nein | Uptime Kuma wurde 2026-05-25 nach erfolgreichem Blackbox-/Grafana-Smoke-Test entfernt |
|
||||||
@@ -84,8 +87,8 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `homeassistant` | Zentrale Smart-Home-Steuerung, Energy Dashboard, Integrations-Hub | Runtime: `smart-home/docker-compose.yml`; Fachkonfiguration: Repo `smart-home-kalli` | `https://home.kaleschke.info`; kein direkter Host-Port in Phase 1 | Traefik, `frontend_net`, `smarthome_net`, `smarthome-mosquitto`, Fachrepo unter `/mnt/user/services/smart-home-kalli` | `/mnt/user/appdata/homeassistant` inkl. `.storage`, `secrets.yaml`, `trusted_proxies.yaml`; YAML-Fachdateien read-only aus `/mnt/user/services/smart-home-kalli/home-assistant` | Tier 2, Borg + HA-native Backups; Restore-Probe Pflicht vor produktiven Energie-Automationen | ja, native HA-Auth | HA Container statt HAOS-VM; keine Add-ons, keine Supervised-Installation. `configuration.yaml` kommt aus dem Fachrepo, `.storage` wird nicht versioniert. `http.use_x_forwarded_for` und `trusted_proxies` muessen zur Traefik-Route passen. Ecowitt-HTTP bleibt Phase-2-Entscheidung wegen globalem Traefik-Redirect. |
|
| `homeassistant` | Zentrale Smart-Home-Steuerung, Energy Dashboard, Integrations-Hub | Runtime: `smart-home/docker-compose.yml`; Fachkonfiguration: Repo `smart-home-kalli` | `https://home.kaleschke.info`; zusaetzlich LAN-only Host-Bind `192.168.178.58:8123` nur fuer den Ecowitt-HTTP-Push | Traefik, `frontend_net`, `smarthome_net`, `smarthome-mosquitto`, SolarEdge-Wechselrichter `192.168.178.111:1502`, Fachrepo unter `/mnt/user/services/smart-home-kalli` | `/mnt/user/appdata/homeassistant` inkl. `.storage`, `secrets.yaml`, `trusted_proxies.yaml` und `custom_components` (HACS, `solaredge_modbus_multi`); YAML-Fachdateien read-only aus `/mnt/user/services/smart-home-kalli/home-assistant`; Agent-API-Tokens als Host-Secrets `ha_token_codex`/`ha_token_claude` | Tier 2, Borg + HA-native Backups; erstes HA-Backup am 2026-06-13 erzeugt/geprueft; Restore-Probe am 2026-06-13 erfolgreich, Report `/mnt/user/backups/restore-reports/homeassistant-2026-06-13.md`; Backup nach SolarEdge-Integration: `/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_14.59_48645373.tar`; Backup nach Energy-Dashboard-Konfiguration: `/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_15.59_25670583.tar` | ja, native HA-Auth | HA Container statt HAOS-VM; keine Add-ons, keine Supervised-Installation. `configuration.yaml` kommt aus dem Fachrepo, `.storage` wird nicht versioniert. `http.use_x_forwarded_for`, `trusted_proxies` und `ip_ban_enabled` sind aktiv. HA-MQTT-Integration `smarthome-mosquitto` ist seit 2026-06-13 geladen. SolarEdge ist seit 2026-06-13 lokal ueber `solaredge_modbus_multi` v3.2.5 angebunden: `SolarEdge Local`, `192.168.178.111:1502`, Device-ID `1`, Meter+Batterie-Erkennung an, Power-Control aus. Energy Dashboard ist fuer Netz, PV und Speicher konfiguriert; Kosten folgen mit Tibber. Komodo-Stack und Gitea-Webhook sind aktiv. Ecowitt-Ingress seit 2026-06-13 ueber LAN-only Host-Bind `192.168.178.58:8123` geloest; offen ist nur die GW3000-Customized-Server-Konfiguration. Naechster Produktivschritt: Tibber. |
|
||||||
| `smarthome-mosquitto` | MQTT-Broker fuer HA, spaeter ESPHome und Zigbee2MQTT | `smart-home/docker-compose.yml`, `smart-home/mosquitto/config/mosquitto.conf` | intern `smarthome_net:1883`; kein LAN-Port in Phase 1 | `smarthome_net`, Passwort-/ACL-Dateien in Appdata | `/mnt/user/appdata/mosquitto/config`, `/mnt/user/appdata/mosquitto/data`, `/mnt/user/appdata/mosquitto/log` | Tier 2, Borg; Passwortdatei, ACLs und persistente Broker-Daten relevant | nein | LAN-Port `1883` erst in ESPHome-Phase mit ACLs und per-Device-Usern. |
|
| `smarthome-mosquitto` | MQTT-Broker fuer HA, spaeter ESPHome und Zigbee2MQTT | `smart-home/docker-compose.yml`, `smart-home/mosquitto/config/mosquitto.conf` | intern `smarthome_net:1883`; kein LAN-Port in Phase 1 | `smarthome_net`, Passwort-/ACL-Dateien in Appdata | `/mnt/user/appdata/mosquitto/config`, `/mnt/user/appdata/mosquitto/data`, `/mnt/user/appdata/mosquitto/log` | Tier 2, Borg; Passwortdatei, ACLs und persistente Broker-Daten relevant; Restore-Probe am 2026-06-13 erfolgreich | nein | Authentifizierter Publish/Subscribe-Smoke und retained Topic nach Broker-Restart am 2026-06-13 erfolgreich. Home Assistant verbindet sich als User `homeassistant`. LAN-Port `1883` erst in ESPHome-Phase mit ACLs und per-Device-Usern. |
|
||||||
|
|
||||||
## Host Operations
|
## Host Operations
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -124,14 +124,20 @@ Pflichtschritte beim Anlegen:
|
|||||||
1. Stack in Komodo aus Gitea anlegen
|
1. Stack in Komodo aus Gitea anlegen
|
||||||
2. `webhook_enabled` in Komodo aktivieren
|
2. `webhook_enabled` in Komodo aktivieren
|
||||||
3. passenden Gitea-Webhook fuer die aktuelle Stack-ID anlegen
|
3. passenden Gitea-Webhook fuer die aktuelle Stack-ID anlegen
|
||||||
4. Gitea-Hook gegen `http://komodo-core:9120/listener/github/stack/<stack-id>/deploy` pruefen
|
4. Branch-Filter im Gitea-Hook auf den produktiven Branch setzen, aktuell `master`
|
||||||
5. einen Push oder Test-Delivery ausloesen und `last_status`/Komodo-Deploy pruefen
|
5. Gitea-Hook gegen `http://komodo-core:9120/listener/github/stack/<stack-id>/deploy` pruefen
|
||||||
6. Ausnahmen explizit dokumentieren
|
6. einen Push oder Test-Delivery ausloesen und `last_status`/Komodo-Deploy pruefen
|
||||||
|
7. Ausnahmen explizit dokumentieren
|
||||||
|
|
||||||
**Regel:** Kein neuer produktiver GitOps-Stack ohne funktionierenden Gitea->Komodo-Webhook. Bewusste Ausnahmen muessen im selben Aenderungsblock dokumentiert werden, inklusive Grund und Alternativ-Deploy-Weg.
|
**Regel:** Kein neuer produktiver GitOps-Stack ohne funktionierenden Gitea->Komodo-Webhook. Bewusste Ausnahmen muessen im selben Aenderungsblock dokumentiert werden, inklusive Grund und Alternativ-Deploy-Weg.
|
||||||
|
|
||||||
Der Standardfall nutzt den globalen `KOMODO_WEBHOOK_SECRET` aus der Komodo-Host-`.env`, ausser Komodo zeigt fuer den Stack explizit ein eigenes per-Stack-Secret.
|
Der Standardfall nutzt den globalen `KOMODO_WEBHOOK_SECRET` aus der Komodo-Host-`.env`, ausser Komodo zeigt fuer den Stack explizit ein eigenes per-Stack-Secret.
|
||||||
|
|
||||||
|
Der Gitea-Branch-Filter darf nicht leer oder `*` bleiben, solange der Komodo-Stack
|
||||||
|
einen konkreten Repo-Branch erwartet. Sonst triggern Feature-/Arbeitsbranches alle
|
||||||
|
Stack-Listener, Komodo verwirft sie mit `request branch does not match expected`
|
||||||
|
und der Operations-Report bekommt unnuetzes Komodo-/Traefik-Rauschen.
|
||||||
|
|
||||||
### Ausnahme: Komodo-Zugangsmodell
|
### Ausnahme: Komodo-Zugangsmodell
|
||||||
|
|
||||||
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
|
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ Komodo-Stack:
|
|||||||
- Repo: `homelab-infra`
|
- Repo: `homelab-infra`
|
||||||
- Pfad: `smart-home/docker-compose.yml`
|
- Pfad: `smart-home/docker-compose.yml`
|
||||||
- Branch: nach Review `master`
|
- Branch: nach Review `master`
|
||||||
|
- Status 2026-06-13: Stack `smart-home` existiert in Komodo, Gitea-Webhook ist
|
||||||
|
aktiv, `deployed_hash == latest_hash`.
|
||||||
|
|
||||||
Nach dem Start pruefen:
|
Nach dem Start pruefen:
|
||||||
|
|
||||||
@@ -74,12 +76,149 @@ docker logs --tail=100 smarthome-mosquitto
|
|||||||
## 5. Smoke-Test
|
## 5. Smoke-Test
|
||||||
|
|
||||||
- `https://home.kaleschke.info` zeigt die Home-Assistant-Oberflaeche.
|
- `https://home.kaleschke.info` zeigt die Home-Assistant-Oberflaeche.
|
||||||
|
- Nach Owner-Onboarding: keine Authelia-ForwardAuth mehr vor HA; HA nutzt native
|
||||||
|
Auth plus `http.ip_ban_enabled`.
|
||||||
|
- `trusted_proxies.yaml` deckt das `frontend_net` ab; damit wertet HA die echte
|
||||||
|
Client-IP aus `X-Forwarded-For` aus.
|
||||||
- Keine Trusted-Proxy-Fehler im HA-Log.
|
- Keine Trusted-Proxy-Fehler im HA-Log.
|
||||||
- MQTT-Integration verbindet sich mit Host `smarthome-mosquitto`, Port `1883`.
|
- MQTT-Broker-Smoke: `homeassistant`-User aus `secrets.yaml` kann gegen
|
||||||
- HA-native Backup-Erstellung funktioniert.
|
`smarthome-mosquitto:1883` publish/subscriben.
|
||||||
|
- HA-MQTT-Integration ist verbunden: Config-Entry `smarthome-mosquitto` ist
|
||||||
|
`loaded`, Mosquitto sieht einen HA-Client mit User `homeassistant`.
|
||||||
|
- HA-native Backup-Erstellung funktioniert; Beispielartefakt:
|
||||||
|
`/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_08.25_38034438.tar`.
|
||||||
|
- Backup-Artefakt ist lesbar (`backup.json`, `homeassistant.tar.gz`).
|
||||||
|
- Agent-API-Tokens liegen als Host-Secrets unter
|
||||||
|
`/mnt/user/appdata/secrets/ha_token_codex` und
|
||||||
|
`/mnt/user/appdata/secrets/ha_token_claude`; Werte nie ausgeben oder in Git
|
||||||
|
schreiben. Die Tokens sind nur mit erhaltenem HA-Auth-State in `.storage`
|
||||||
|
brauchbar und bei Verdacht in HA zu widerrufen.
|
||||||
|
|
||||||
## 6. Abnahmebedingung
|
## 6. Fachrepo-Update
|
||||||
|
|
||||||
|
Das Fachrepo `/mnt/user/services/smart-home-kalli` ist kein eigener
|
||||||
|
Komodo-Stack. Aenderungen wirken erst nach diesem Host-Ablauf:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /mnt/user/services/smart-home-kalli
|
||||||
|
git pull --ff-only origin main
|
||||||
|
docker compose -f /mnt/user/services/stacks/smart-home/smart-home/docker-compose.yml \
|
||||||
|
up -d --force-recreate homeassistant
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Force-Recreate ist Pflicht, weil `configuration.yaml`, `automations.yaml`,
|
||||||
|
`scripts.yaml` und `scenes.yaml` als Einzeldateien in den Container gemountet
|
||||||
|
werden. Nach einem `git pull` kann Docker sonst noch den alten Datei-Inode sehen
|
||||||
|
(`Stale file handle`).
|
||||||
|
|
||||||
|
## 7. UI-Editor-Politik
|
||||||
|
|
||||||
|
`automations.yaml`, `scripts.yaml` und `scenes.yaml` sind read-only aus Git
|
||||||
|
gemountet. Der Home-Assistant-UI-Editor fuer diese Dateien ist deshalb nicht der
|
||||||
|
primaere Schreibweg. Automationen und Scripts werden in Git gepflegt; UI-State
|
||||||
|
und Integrations-State bleiben in `.storage` und werden per Borg gesichert.
|
||||||
|
|
||||||
|
## 8. Abnahmebedingung
|
||||||
|
|
||||||
Vor produktiven Energie-Automationen muss ein Restore-Test fuer
|
Vor produktiven Energie-Automationen muss ein Restore-Test fuer
|
||||||
`/mnt/user/appdata/homeassistant`, `/mnt/user/appdata/mosquitto` und den Clone
|
`/mnt/user/appdata/homeassistant`, `/mnt/user/appdata/mosquitto` und den Clone
|
||||||
`/mnt/user/services/smart-home-kalli` dokumentiert sein.
|
`/mnt/user/services/smart-home-kalli` dokumentiert sein.
|
||||||
|
|
||||||
|
Wichtig: Ein erfolgreich erzeugtes HA-Backup ist nur die Voraussetzung. Das Gate
|
||||||
|
ist erst geschlossen, wenn eine Restore-Probe in einem isolierten Testpfad
|
||||||
|
dokumentiert ist.
|
||||||
|
|
||||||
|
Status 2026-06-13: Gate geschlossen. Die isolierte Restore-Probe war
|
||||||
|
erfolgreich:
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/homeassistant-2026-06-13.md`
|
||||||
|
- Test: HA-native Backup + Mosquitto-Appdata + Fachrepo-Clone
|
||||||
|
- Ergebnis: HA HTTP/API/check_config gruen, MQTT Publish/Subscribe und retained
|
||||||
|
Topic nach Broker-Restart gruen
|
||||||
|
|
||||||
|
Status 2026-06-13: HA-MQTT-Integration ist produktiv verbunden.
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
TOKEN=$(cat /mnt/user/appdata/secrets/ha_token_codex)
|
||||||
|
curl -ksS -H "Authorization: Bearer $TOKEN" \
|
||||||
|
https://home.kaleschke.info/api/config/config_entries/entry
|
||||||
|
docker logs --tail=120 smarthome-mosquitto
|
||||||
|
docker exec homeassistant python -m homeassistant --script check_config --config /config
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung: Ein MQTT-Config-Entry `smarthome-mosquitto` mit Status `loaded`, ein
|
||||||
|
Mosquitto-Client mit User `homeassistant`, und `check_config` ohne Fehler.
|
||||||
|
|
||||||
|
## 9. SolarEdge lokal
|
||||||
|
|
||||||
|
Status 2026-06-13: SolarEdge ist lokal per Modbus TCP angebunden.
|
||||||
|
|
||||||
|
- Integration: HACS/Custom `solaredge_modbus_multi` v3.2.5
|
||||||
|
- HA-Config-Entry: `SolarEdge Local`, Status `loaded`
|
||||||
|
- Wechselrichter: `192.168.178.111:1502`
|
||||||
|
- Modbus Device-ID: `1`
|
||||||
|
- Optionen: Polling 60 Sekunden, Meter-Erkennung aktiv, Batterie-Erkennung
|
||||||
|
aktiv, Extras aus, Power-Control aus
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
TOKEN=$(cat /mnt/user/appdata/secrets/ha_token_codex)
|
||||||
|
curl -ksS -H "Authorization: Bearer $TOKEN" \
|
||||||
|
https://home.kaleschke.info/api/config/config_entries/entry
|
||||||
|
curl -ksS -H "Authorization: Bearer $TOKEN" \
|
||||||
|
https://home.kaleschke.info/api/states
|
||||||
|
docker exec homeassistant python -m homeassistant --script check_config --config /config
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtige Energy-Dashboard-Kandidaten:
|
||||||
|
|
||||||
|
- PV-Produktion: `sensor.solaredge_local_i1_ac_energy`
|
||||||
|
- Netzbezug: `sensor.solaredge_local_i1_m1_ac_energy_imported`
|
||||||
|
- Einspeisung: `sensor.solaredge_local_i1_m1_ac_energy_exported`
|
||||||
|
- Batterie geladen: `sensor.solaredge_local_i1_b1_energy_import`
|
||||||
|
- Batterie entladen: `sensor.solaredge_local_i1_b1_energy_export`
|
||||||
|
- Batterie-SoC: `sensor.solaredge_local_i1_b1_state_of_energy`
|
||||||
|
|
||||||
|
Nach der Integration wurde ein HA-native Backup erzeugt und tar-geprueft:
|
||||||
|
`/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_14.59_48645373.tar`.
|
||||||
|
|
||||||
|
Trade-off: Dieser Pfad ist lokal und liefert Inverter, Meter und Batterie ohne
|
||||||
|
Cloud-API, nutzt aber eine Custom-Integration. Bei HA-Core-Upgrades auf Warnungen
|
||||||
|
zu `solaredge_modbus_multi` achten.
|
||||||
|
|
||||||
|
## 10. Energy Dashboard
|
||||||
|
|
||||||
|
Status 2026-06-13: Energy Dashboard ist ueber die Home-Assistant-WebSocket-API
|
||||||
|
konfiguriert und validiert.
|
||||||
|
|
||||||
|
Konfiguration:
|
||||||
|
|
||||||
|
- Netz: Bezug `sensor.solaredge_local_i1_m1_ac_energy_imported`, Einspeisung
|
||||||
|
`sensor.solaredge_local_i1_m1_ac_energy_exported`
|
||||||
|
- PV: Produktion `sensor.solaredge_local_i1_ac_energy`, Live-Leistung
|
||||||
|
`sensor.solaredge_local_i1_ac_power`
|
||||||
|
- Speicher: Entladung `sensor.solaredge_local_i1_b1_energy_export`, Ladung
|
||||||
|
`sensor.solaredge_local_i1_b1_energy_import`, SoC
|
||||||
|
`sensor.solaredge_local_i1_b1_state_of_energy`
|
||||||
|
- Kosten/Preise: noch nicht gesetzt; folgt mit Tibber
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
TOKEN=$(cat /mnt/user/appdata/secrets/ha_token_codex)
|
||||||
|
# WebSocket: energy/get_prefs und energy/validate
|
||||||
|
sed -n '1,260p' /mnt/user/appdata/homeassistant/.storage/energy
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung: `.storage/energy` enthaelt drei Quellen (`grid`, `solar`,
|
||||||
|
`battery`), und `energy/validate` meldet keine Issues.
|
||||||
|
|
||||||
|
Nach der Energy-Konfiguration wurde ein HA-native Backup erzeugt und
|
||||||
|
tar-geprueft:
|
||||||
|
`/mnt/user/appdata/homeassistant/backups/Custom_backup_2026.6.1_2026-06-13_15.59_25670583.tar`.
|
||||||
|
|
||||||
|
Naechster Schritt: Tibber per HA-UI-Config-Flow verbinden und danach Kosten im
|
||||||
|
Energy Dashboard ergaenzen.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
postgresql17:
|
postgresql17:
|
||||||
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
||||||
container_name: postgresql17
|
container_name: postgresql17
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,56 @@ INFLUXDB_BIND_IP=192.168.178.58
|
|||||||
- Uptime Kuma ist entfernt; `Homelab / Availability`, Blackbox Exporter und Prometheus-Alerts sind der Zielzustand fuer HTTP-Verfuegbarkeit.
|
- Uptime Kuma ist entfernt; `Homelab / Availability`, Blackbox Exporter und Prometheus-Alerts sind der Zielzustand fuer HTTP-Verfuegbarkeit.
|
||||||
- Dashboard-Zielbestand: `Homelab / Availability`, `Homelab / Containers + Logs`, `Homelab / Host Overview`, `Homelab / Family Status`, `Traefik Official Standalone Dashboard`.
|
- Dashboard-Zielbestand: `Homelab / Availability`, `Homelab / Containers + Logs`, `Homelab / Host Overview`, `Homelab / Family Status`, `Traefik Official Standalone Dashboard`.
|
||||||
|
|
||||||
|
## Wetter-Tagesberichte
|
||||||
|
|
||||||
|
Die Ecowitt-/HA-Wetterdaten (DB `homeassistant`, Datasource `ha-weather-influx`)
|
||||||
|
werden in drei Grafana-Dashboards sichtbar (Ordner `Homelab`, Tag `weather`):
|
||||||
|
|
||||||
|
- `Wetterarchiv KalliHome` (`ha-weather-archive`): Verlaeufe und Gauges ueber einen Zeitbereich.
|
||||||
|
- `Wetterbericht KalliHome` (`ha-weather-day-report`): visueller Tagesbericht fuer **einen** Tag (Bewertungs-Banner, Kennzahl-Karten mit Sparkline, Tagescharts Temperatur und Solar/UV).
|
||||||
|
- `Wetter-Tagesberichte KalliHome` (`ha-weather-report-history`): Index/Finder, **eine Zeile pro Tag** mit Kurzbewertung, T min/max/Mittel, Regen, UV und Boee.
|
||||||
|
|
||||||
|
### Alte Tagesberichte finden und ein Datum waehlen
|
||||||
|
|
||||||
|
1. Dashboard **`Wetter-Tagesberichte`** oeffnen (Standard: letzte 30 Tage, Europe/Berlin).
|
||||||
|
In der Tabelle **auf das Datum klicken** -> der ausfuehrliche Tagesbericht oeffnet
|
||||||
|
sich fuer genau diesen Tag. Weiter zurueck: Zeitbereich oben rechts vergroessern.
|
||||||
|
2. Alternativ im Dashboard **`Wetterbericht KalliHome`** direkt einen Tag waehlen:
|
||||||
|
Zeitbereich oben rechts -> *Absolute time range* -> z. B. From `2026-06-15 00:00:00`,
|
||||||
|
To `2026-06-16 00:00:00` -> **Apply**. Beide Dashboards haben Zeitzone
|
||||||
|
`Europe/Berlin`, die Eingaben gelten also in Berliner Lokalzeit; Standard ist
|
||||||
|
`gestern` (`now-1d/d` bis `now/d`).
|
||||||
|
|
||||||
|
Grafana hat **keine echte Datepicker-Variable**: eine Textbox-Variable kann den
|
||||||
|
Zeitbereich nicht setzen, und ein `report_date`-Ansatz braeuchte zeitzonen-genaues
|
||||||
|
Tages-Bounding in InfluxDB-3-SQL. Deshalb ist der **Timepicker bei Dashboard-Zeitzone
|
||||||
|
`Europe/Berlin`** der exakte Weg fuer einen vollstaendigen Lokaltag, und die
|
||||||
|
Index-Tabelle der bequeme Klick-Selektor fuer alte Tage. Die Tabellen-Buckets liegen
|
||||||
|
auf UTC-Mitternacht (~01:00/02:00 Berlin); der verlinkte Tagesbericht zeigt das
|
||||||
|
gewaehlte Fenster vollstaendig.
|
||||||
|
|
||||||
|
### Quelle: InfluxDB statt Markdown-Index
|
||||||
|
|
||||||
|
Die Dashboards rendern direkt per SQL aus InfluxDB 3 Core - gleiche Quelle wie der
|
||||||
|
Markdown-Generator `services/posture-check/weather-day-report.py`. Damit ist jeder
|
||||||
|
zurueckliegende Tag reproduzierbar, solange die Rohdaten vorgehalten werden; ein
|
||||||
|
separater Markdown-Index ist bewusst nicht noetig. Die erzeugten Tagesberichte als
|
||||||
|
Datei (E-Mail/Offline-Archiv) liegen weiterhin unter
|
||||||
|
`/mnt/user/services/posture-check/daily-reports/homelab-day-YYYY-MM-DD.md`.
|
||||||
|
|
||||||
|
### Deploy und Test
|
||||||
|
|
||||||
|
Dashboards werden aus `monitoring/grafana/dashboards/` provisioniert (Verzeichnis-
|
||||||
|
Mount, Reload alle 5 Minuten, Ordner `Homelab`). Nach Push nach Gitea deployt Komodo
|
||||||
|
den `monitoring`-Stack; Grafana laedt die JSON-Dateien automatisch nach. Kein
|
||||||
|
Bootstrap-Importer noetig (der gilt nur fuer grafana.com-Dashboard-ID 17346).
|
||||||
|
|
||||||
|
SQL der Index-Tabelle vor dem Verlassen einmal pruefen: Grafana -> *Explore* ->
|
||||||
|
Datasource `ha-weather-influx` -> die `rawSql` aus `weather-report-history.json`
|
||||||
|
einfuegen und einen Zeitbereich (z. B. letzte 7 Tage) waehlen. Erwartet: eine Zeile
|
||||||
|
pro Tag mit gefuellten Spalten. Falls `extract(epoch FROM ...)` auf dem InfluxDB-3-
|
||||||
|
Build nicht unterstuetzt wird, stattdessen `to_unixtime(...)` verwenden.
|
||||||
|
|
||||||
## Alerting
|
## Alerting
|
||||||
|
|
||||||
Prometheus wertet `monitoring/prometheus/alerts.yml` aus und sendet an `monitoring-alertmanager`.
|
Prometheus wertet `monitoring/prometheus/alerts.yml` aus und sendet an `monitoring-alertmanager`.
|
||||||
|
|||||||
@@ -2,20 +2,28 @@ services:
|
|||||||
prometheus:
|
prometheus:
|
||||||
image: prom/prometheus:v3.12.0@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
|
image: prom/prometheus:v3.12.0@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
|
||||||
container_name: monitoring-prometheus
|
container_name: monitoring-prometheus
|
||||||
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/prometheus/prometheus.yml
|
- --config.file=/etc/prometheus/config/prometheus.yml
|
||||||
- --storage.tsdb.path=/prometheus
|
- --storage.tsdb.path=/prometheus
|
||||||
- --storage.tsdb.retention.time=30d
|
- --storage.tsdb.retention.time=30d
|
||||||
- --web.enable-lifecycle
|
- --web.enable-lifecycle
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
# Verzeichnis-Mount statt Einzeldatei: auf dem Unraid-FUSE-Share (/mnt/user)
|
||||||
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
|
# bricht ein Einzeldatei-Bind-Mount bei git/Komodo-Updates zu
|
||||||
|
# "Stale NFS file handle" (Inode-Wechsel) -> Reload laedt 0 Regeln, nur
|
||||||
|
# --force-recreate heilt. Directory-Inode ist stabil, Reload reicht wieder.
|
||||||
|
- ./prometheus:/etc/prometheus/config:ro
|
||||||
- prometheus_data:/prometheus
|
- prometheus_data:/prometheus
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
|
- backend_net
|
||||||
expose:
|
expose:
|
||||||
- "9090"
|
- "9090"
|
||||||
|
secrets:
|
||||||
|
- source: dawarich_metrics_password
|
||||||
|
mode: 0444
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -25,14 +33,17 @@ services:
|
|||||||
- cadvisor
|
- cadvisor
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: prom/alertmanager:v0.32.2@sha256:b85533a2eb45865835315810315f6951331b2dbc8c93a6cf9a51e156a006a706
|
image: prom/alertmanager:v0.33.0@sha256:af26fbe4dd1886ac0efd7bd55cd9027da262e105b137a376522b7c14c3626e4a
|
||||||
container_name: monitoring-alertmanager
|
container_name: monitoring-alertmanager
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/alertmanager/alertmanager.yml
|
- --config.file=/etc/alertmanager/config/alertmanager.yml
|
||||||
- --storage.path=/alertmanager
|
- --storage.path=/alertmanager
|
||||||
volumes:
|
volumes:
|
||||||
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
# Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus):
|
||||||
|
# /mnt/user-FUSE bricht Einzeldatei-Bind-Mounts bei git/Komodo-Updates
|
||||||
|
# zu "Stale NFS file handle" -> Directory-Inode ist stabil.
|
||||||
|
- ./alertmanager:/etc/alertmanager/config:ro
|
||||||
- alertmanager_data:/alertmanager
|
- alertmanager_data:/alertmanager
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
@@ -42,7 +53,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
alertmanager-ntfy-bridge:
|
alertmanager-ntfy-bridge:
|
||||||
image: python:3.14-alpine@sha256:5a824eb82cc75361f98611f3cfc5091ea33f10a6ccea4d4ebdabbc523b9a1614
|
image: python:3.14-alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92
|
||||||
container_name: monitoring-alertmanager-ntfy-bridge
|
container_name: monitoring-alertmanager-ntfy-bridge
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
dns:
|
dns:
|
||||||
@@ -54,7 +65,9 @@ services:
|
|||||||
- python
|
- python
|
||||||
- /app/bridge.py
|
- /app/bridge.py
|
||||||
volumes:
|
volumes:
|
||||||
- ./alertmanager-ntfy-bridge/bridge.py:/app/bridge.py:ro
|
# Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus):
|
||||||
|
# vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates.
|
||||||
|
- ./alertmanager-ntfy-bridge:/app:ro
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
expose:
|
expose:
|
||||||
@@ -72,9 +85,11 @@ services:
|
|||||||
dns:
|
dns:
|
||||||
- 172.23.0.3
|
- 172.23.0.3
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
- --config.file=/etc/blackbox_exporter/config/blackbox.yml
|
||||||
volumes:
|
volumes:
|
||||||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
# Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus):
|
||||||
|
# vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates.
|
||||||
|
- ./blackbox:/etc/blackbox_exporter/config:ro
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
- dns_net
|
- dns_net
|
||||||
@@ -88,9 +103,11 @@ services:
|
|||||||
container_name: monitoring-loki
|
container_name: monitoring-loki
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
- -config.file=/etc/loki/loki-config.yml
|
- -config.file=/etc/loki/config/loki-config.yml
|
||||||
volumes:
|
volumes:
|
||||||
- ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro
|
# Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus):
|
||||||
|
# vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates.
|
||||||
|
- ./loki:/etc/loki/config:ro
|
||||||
- loki_data:/loki
|
- loki_data:/loki
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
@@ -104,9 +121,11 @@ services:
|
|||||||
container_name: monitoring-promtail
|
container_name: monitoring-promtail
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
- -config.file=/etc/promtail/promtail-config.yml
|
- -config.file=/etc/promtail/config/promtail-config.yml
|
||||||
volumes:
|
volumes:
|
||||||
- ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro
|
# Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus):
|
||||||
|
# vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates.
|
||||||
|
- ./promtail:/etc/promtail/config:ro
|
||||||
- promtail_positions:/positions
|
- promtail_positions:/positions
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
@@ -151,6 +170,7 @@ services:
|
|||||||
- -c
|
- -c
|
||||||
- |
|
- |
|
||||||
export GRAFANA_INFLUXDB_TOKEN="$$(cat /run/secrets/monitoring_grafana_influxdb_token)"
|
export GRAFANA_INFLUXDB_TOKEN="$$(cat /run/secrets/monitoring_grafana_influxdb_token)"
|
||||||
|
export DAWARICH_GRAFANA_RO_PASSWORD="$$(cat /run/secrets/dawarich_grafana_ro_password)"
|
||||||
exec /run.sh
|
exec /run.sh
|
||||||
volumes:
|
volumes:
|
||||||
- grafana_data:/var/lib/grafana
|
- grafana_data:/var/lib/grafana
|
||||||
@@ -159,10 +179,12 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
- frontend_net
|
- frontend_net
|
||||||
|
- backend_net
|
||||||
secrets:
|
secrets:
|
||||||
- monitoring_grafana_admin_password
|
- monitoring_grafana_admin_password
|
||||||
- monitoring_grafana_influxdb_token
|
- monitoring_grafana_influxdb_token
|
||||||
- grafana_oidc_client_secret
|
- grafana_oidc_client_secret
|
||||||
|
- dawarich_grafana_ro_password
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -316,7 +338,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
cadvisor:
|
cadvisor:
|
||||||
image: ghcr.io/google/cadvisor:v0.57.0@sha256:e75bdb03b74b0b6995f208f166fead2e6e555dde73e44200113bb26f41b1981d
|
image: ghcr.io/google/cadvisor:v0.60.1@sha256:d48aea9d9c1bcf375917279842408703293fde88982d4610aefe75134ec69759
|
||||||
container_name: monitoring-cadvisor
|
container_name: monitoring-cadvisor
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -337,7 +359,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
influxdb3-core:
|
influxdb3-core:
|
||||||
image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
|
image: influxdb:3.10.0-core@sha256:b3e577f38c19963597170d8850a3a7f77af8f0cfa866c64cd13e5de0f238e114
|
||||||
container_name: monitoring-influxdb3-core
|
container_name: monitoring-influxdb3-core
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -351,6 +373,12 @@ services:
|
|||||||
- --data-dir=/var/lib/influxdb3/data
|
- --data-dir=/var/lib/influxdb3/data
|
||||||
- --plugin-dir=/var/lib/influxdb3/plugins
|
- --plugin-dir=/var/lib/influxdb3/plugins
|
||||||
- --admin-token-file=/run/secrets/influxdb3_admin_token
|
- --admin-token-file=/run/secrets/influxdb3_admin_token
|
||||||
|
# InfluxDB 3 Core kompaktiert Parquet-Dateien nicht (nur Enterprise).
|
||||||
|
# HA schreibt viele Sensoren haeufig -> Tabellen wie "°C"/"%"/"hPa" liefen
|
||||||
|
# ins Default-Limit von 432 Dateien/Query ("No data" in Grafana).
|
||||||
|
# Stopgap: Limit anheben. Langfristig: Enterprise (Auto-Compaction, frei
|
||||||
|
# fuer Home) oder weniger/seltener nach InfluxDB schreiben.
|
||||||
|
- --query-file-limit=20000
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata/influxdb3/data:/var/lib/influxdb3/data
|
- /mnt/user/appdata/influxdb3/data:/var/lib/influxdb3/data
|
||||||
- /mnt/user/appdata/influxdb3/plugins:/var/lib/influxdb3/plugins
|
- /mnt/user/appdata/influxdb3/plugins:/var/lib/influxdb3/plugins
|
||||||
@@ -370,6 +398,8 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
frontend_net:
|
frontend_net:
|
||||||
external: true
|
external: true
|
||||||
|
backend_net:
|
||||||
|
external: true
|
||||||
dns_net:
|
dns_net:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
@@ -389,3 +419,7 @@ secrets:
|
|||||||
file: /mnt/user/appdata/secrets/grafana_oidc_client_secret
|
file: /mnt/user/appdata/secrets/grafana_oidc_client_secret
|
||||||
influxdb3_admin_token:
|
influxdb3_admin_token:
|
||||||
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
||||||
|
dawarich_metrics_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_metrics_password.txt
|
||||||
|
dawarich_grafana_ro_password:
|
||||||
|
file: /mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": false,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 16,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"basemap": {
|
||||||
|
"config": {},
|
||||||
|
"name": "Layer 0",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"mouseWheelZoom": true,
|
||||||
|
"showAttribution": true,
|
||||||
|
"showDebug": false,
|
||||||
|
"showMeasure": false,
|
||||||
|
"showScale": true,
|
||||||
|
"showZoom": true
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"showLegend": true,
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "dark-green"
|
||||||
|
},
|
||||||
|
"opacity": 0.55,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 4,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"latitude": "latitude",
|
||||||
|
"longitude": "longitude",
|
||||||
|
"mode": "coords"
|
||||||
|
},
|
||||||
|
"name": "Location points",
|
||||||
|
"tooltip": true,
|
||||||
|
"type": "markers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "details"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"allLayers": true,
|
||||||
|
"id": "fit",
|
||||||
|
"lat": 51,
|
||||||
|
"lon": 10,
|
||||||
|
"zoom": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Location Points",
|
||||||
|
"type": "geomap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 70,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "km"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Kilometers per Month",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 70,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "time_series",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Points per Day",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"refresh": "5m",
|
||||||
|
"schemaVersion": 41,
|
||||||
|
"tags": [
|
||||||
|
"dawarich",
|
||||||
|
"location"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-30d",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "Dawarich",
|
||||||
|
"uid": "dawarich",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"uid": "ha-solar-pv",
|
||||||
|
"title": "Solar PV System",
|
||||||
|
"tags": ["solar", "solaredge", "homeassistant", "energy"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": { "from": "now-24h", "to": "now" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Power",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 11, "w": 12, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "kwatt", "custom": { "drawStyle": "line", "fillOpacity": 30, "lineWidth": 1, "showPoints": "never" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Solar Produktion" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#73bf69" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Strom Verbrauch" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#fade2a" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_pv_live_power' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'kallihome_live_load_power' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Aktuelle Solar Produktion",
|
||||||
|
"type": "bargauge",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatt", "min": 0, "max": 8, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "displayMode": "lcd", "orientation": "horizontal", "showUnfilled": true },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_pv_live_power' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Strom Produziert (Heute)",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 7, "w": 6, "x": 18, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatth", "min": 0, "max": 50, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_pv_energy_today' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Produktion und Verbrauch kWh",
|
||||||
|
"type": "bargauge",
|
||||||
|
"gridPos": { "h": 7, "w": 6, "x": 12, "y": 4 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "kwatth", "min": 0, "color": { "mode": "continuous-GrYlRd" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Solar Produktion" } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Netzbezug" } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "displayMode": "lcd", "orientation": "horizontal", "showUnfilled": true },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_pv_energy_today' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_grid_import_today' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Tages Produktion 30 Tage Übersicht",
|
||||||
|
"type": "barchart",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 11 },
|
||||||
|
"timeFrom": "30d",
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatth", "color": { "mode": "continuous-GrYlRd" }, "custom": { "fillOpacity": 80, "gradientMode": "scheme", "lineWidth": 1 } }, "overrides": [] },
|
||||||
|
"options": { "orientation": "vertical", "showValue": "never", "xField": "time", "legend": { "showLegend": false }, "tooltip": { "mode": "single" } },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "table", "rawSql": "SELECT date_bin(INTERVAL '1 day', time) AS time, max(value) AS value FROM \"kWh\" WHERE entity_id = 'solaredge_pv_energy_today' AND $__timeFilter(time) GROUP BY 1 ORDER BY 1" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "Speicher-Ladestand",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 7 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "yellow", "value": 20 }, { "color": "green", "value": 50 } ] } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": true },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"%\" WHERE entity_id = 'solaredge_local_i1_b1_state_of_energy' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Erreichte TOP kWh an einem Tag",
|
||||||
|
"type": "bargauge",
|
||||||
|
"gridPos": { "h": 4, "w": 12, "x": 12, "y": 11 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "kwatth", "min": 0, "max": 50, "color": { "mode": "continuous-GrYlRd" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Bester Wert bis jetzt" } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Heute" } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "displayMode": "lcd", "orientation": "horizontal", "showUnfilled": true },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "table", "rawSql": "SELECT max(d) AS value FROM (SELECT date_bin(INTERVAL '1 day', time) AS day, max(value) AS d FROM \"kWh\" WHERE entity_id = 'solaredge_pv_energy_today' AND time > now() - INTERVAL '365 days' GROUP BY 1)" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "table", "rawSql": "SELECT value FROM \"kWh\" WHERE entity_id = 'solaredge_pv_energy_today' ORDER BY time DESC LIMIT 1" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "Gesamt Produktion kWh",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 12, "x": 12, "y": 15 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatth", "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area", "textMode": "value" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_local_i1_ac_energy' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "Netzbilanz (heute)",
|
||||||
|
"type": "bargauge",
|
||||||
|
"gridPos": { "h": 4, "w": 12, "x": 0, "y": 19 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "kwatth", "min": 0, "color": { "mode": "continuous-GrYlRd" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Netzbezug" } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Einspeisung" } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "displayMode": "lcd", "orientation": "horizontal", "showUnfilled": true },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_grid_import_today' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'solaredge_grid_export_today' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "Netz & Batterie (Verlauf)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 7, "w": 24, "x": 0, "y": 23 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "kwatt", "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Netzbezug" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#fa5252" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Einspeisung" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#4dabf7" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "C" }, "properties": [ { "id": "displayName", "value": "Speicher laden" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#37b24d" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "D" }, "properties": [ { "id": "displayName", "value": "Speicher entladen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#d6336c" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_grid_import_power' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_grid_export_power' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "C", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_battery_charge_power' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "D", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'solaredge_battery_discharge_power' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Wallbox – Ladeleistung",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 7, "w": 12, "x": 0, "y": 30 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatt", "color": { "mode": "fixed", "fixedColor": "#9775fa" }, "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2 } }, "overrides": [] },
|
||||||
|
"options": { "legend": { "showLegend": false }, "tooltip": { "mode": "single" } },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'eh7klptt_leistung' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "Ladeleistung",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 7, "w": 6, "x": 12, "y": 30 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatt", "min": 0, "max": 11, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kW\" WHERE entity_id = 'eh7klptt_leistung' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Gesamt geladen",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 30 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatth", "color": { "mode": "fixed", "fixedColor": "#9775fa" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area", "textMode": "value" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'eh7klptt_gesamtenergie' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"title": "Aktuelle Session",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 3, "w": 6, "x": 18, "y": 34 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "kwatth", "color": { "mode": "fixed", "fixedColor": "#4dabf7" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "textMode": "value" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"kWh\" WHERE entity_id = 'eh7klptt_sitzungsenergie' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
"uid": "ha-weather-archive",
|
||||||
|
"title": "Wetterarchiv KalliHome",
|
||||||
|
"tags": ["weather", "ecowitt", "homeassistant"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 2,
|
||||||
|
"refresh": "1m",
|
||||||
|
"time": { "from": "now-7d", "to": "now" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Außentemperatur",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 0, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "celsius", "min": -10, "max": 40, "color": { "mode": "continuous-BlYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Luftfeuchte",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 4, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "color": { "mode": "continuous-BlYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"%\" WHERE entity_id = 'gw3000a_humidity' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Wind",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 8, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "velocitykmh", "min": 0, "max": 60, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"km/h\" WHERE entity_id = 'gw3000a_wind_speed' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "UV-Index",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 12, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "short", "min": 0, "max": 11, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"UV index\" WHERE entity_id = 'gw3000a_uv_index' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "Solarstrahlung",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 16, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "wattm2", "min": 0, "max": 1200, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdMarkers": false },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "Luftdruck",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 5, "w": 4, "x": 20, "y": 0 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "pressurehpa", "color": { "mode": "fixed", "fixedColor": "#4dabf7" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area", "textMode": "value" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"hPa\" WHERE entity_id = 'gw3000a_relative_pressure' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Temperatur (°C)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "celsius", "custom": { "drawStyle": "line", "fillOpacity": 12, "lineWidth": 2, "showPoints": "never" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Außen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#fa5252" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Gefühlt" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#ff922b" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "C" }, "properties": [ { "id": "displayName", "value": "Taupunkt" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#4dabf7" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "D" }, "properties": [ { "id": "displayName", "value": "Innen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#82c91e" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_feels_like_temperature' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "C", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_dewpoint' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "D", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_indoor_temperature' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "Luftfeuchte (%)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "percent", "min": 0, "max": 100, "custom": { "drawStyle": "line", "fillOpacity": 12, "lineWidth": 2, "showPoints": "never" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Außen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#4dabf7" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Innen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#82c91e" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"%\" WHERE entity_id = 'gw3000a_humidity' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"%\" WHERE entity_id = 'gw3000a_indoor_humidity' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "Solarstrahlung (W/m²)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "wattm2", "color": { "mode": "fixed", "fixedColor": "#f2b705" }, "custom": { "drawStyle": "line", "fillOpacity": 35, "lineWidth": 1, "showPoints": "never", "gradientMode": "opacity" } }, "overrides": [] },
|
||||||
|
"options": { "legend": { "showLegend": false }, "tooltip": { "mode": "single" } },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "Wind (km/h)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "velocitykmh", "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2, "showPoints": "never" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Wind" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#15aabf" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Böe" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#fab005" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["lastNotNull", "max"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"km/h\" WHERE entity_id = 'gw3000a_wind_speed' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"km/h\" WHERE entity_id = 'gw3000a_wind_gust' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Regen pro Tag (mm)",
|
||||||
|
"type": "barchart",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 21 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "lengthmm", "color": { "mode": "fixed", "fixedColor": "#4dabf7" }, "custom": { "fillOpacity": 80, "lineWidth": 1 } }, "overrides": [] },
|
||||||
|
"options": { "orientation": "vertical", "showValue": "auto", "xField": "time", "legend": { "showLegend": false }, "tooltip": { "mode": "single" } },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "table", "rawSql": "SELECT date_bin(INTERVAL '1 day', time) AS time, max(value) AS value FROM \"mm\" WHERE entity_id = 'gw3000a_daily_rain' AND $__timeFilter(time) GROUP BY 1 ORDER BY 1" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "Luftdruck (hPa)",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 21 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "pressurehpa", "decimals": 0, "color": { "mode": "fixed", "fixedColor": "#9775fa" }, "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2, "showPoints": "never" } }, "overrides": [] },
|
||||||
|
"options": { "legend": { "showLegend": false }, "tooltip": { "mode": "single" } },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"hPa\" WHERE entity_id = 'gw3000a_relative_pressure' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"uid": "ha-weather-day-report",
|
||||||
|
"title": "Wetterbericht KalliHome",
|
||||||
|
"tags": ["weather", "ecowitt", "homeassistant", "report"],
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 5,
|
||||||
|
"refresh": "",
|
||||||
|
"time": { "from": "now-1d/d", "to": "now/d" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"links": [
|
||||||
|
{ "asDropdown": true, "icon": "external link", "includeVars": false, "keepTime": false, "tags": ["weather"], "targetBlank": false, "title": "Wetter-Dashboards", "tooltip": "", "type": "dashboards", "url": "" }
|
||||||
|
],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "",
|
||||||
|
"type": "text",
|
||||||
|
"gridPos": { "h": 3, "w": 24, "x": 0, "y": 0 },
|
||||||
|
"options": {
|
||||||
|
"mode": "markdown",
|
||||||
|
"content": "**Wetterbericht – Tag auswählen.** Standard: **gestern** (ganzer Tag, Europe/Berlin). Anderen Tag: Zeitbereich oben rechts → *Absolute time range* → z. B. From `2026-06-15 00:00:00`, To `2026-06-16 00:00:00`. Alle Tage als Liste: **[Wetter-Tagesberichte](/d/ha-weather-report-history)** (Datum anklicken)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Bewertung des Tages",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 3, "w": 24, "x": 0, "y": 3 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [ { "color": "#868e96", "value": null } ] },
|
||||||
|
"mappings": [
|
||||||
|
{ "type": "value", "options": {
|
||||||
|
"0": { "text": "Sonnig & warm", "color": "#ff922b", "index": 0 },
|
||||||
|
"1": { "text": "Warm", "color": "#fab005", "index": 1 },
|
||||||
|
"2": { "text": "Regnerisch", "color": "#4dabf7", "index": 2 },
|
||||||
|
"3": { "text": "Wechselhaft", "color": "#868e96", "index": 3 },
|
||||||
|
"4": { "text": "Kalt", "color": "#74c0fc", "index": 4 }
|
||||||
|
} }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"colorMode": "background",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "center",
|
||||||
|
"textMode": "value",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"rawQuery": true,
|
||||||
|
"format": "table",
|
||||||
|
"rawSql": "WITH t AS (SELECT 1 AS k, max(value) AS tmax FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time)), s AS (SELECT 1 AS k, max(value) AS smax FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time)), u AS (SELECT 1 AS k, max(value) AS uvmax FROM \"UV index\" WHERE entity_id = 'gw3000a_uv_index' AND $__timeFilter(time)), r AS (SELECT 1 AS k, max(value) AS rain FROM \"mm\" WHERE entity_id = 'gw3000a_daily_rain' AND $__timeFilter(time)) SELECT CASE WHEN s.smax >= 700 AND u.uvmax >= 6 THEN 0 WHEN coalesce(r.rain, 0) >= 5 THEN 2 WHEN t.tmax >= 25 THEN 1 WHEN t.tmax <= 5 THEN 4 ELSE 3 END AS code FROM t LEFT JOIN s ON s.k = t.k LEFT JOIN u ON u.k = t.k LEFT JOIN r ON r.k = t.k"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "Temp max",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 0, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "celsius", "min": -10, "max": 40, "decimals": 1, "color": { "mode": "continuous-BlYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "Temp min",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 4, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "celsius", "min": -10, "max": 40, "decimals": 1, "color": { "mode": "continuous-BlYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["min"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "Luftfeuchte Ø",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 8, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "decimals": 0, "color": { "mode": "continuous-BlYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["mean"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"%\" WHERE entity_id = 'gw3000a_humidity' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Böe max",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 12, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "velocitykmh", "min": 0, "max": 80, "decimals": 1, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"km/h\" WHERE entity_id = 'gw3000a_wind_gust' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"title": "UV max",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 16, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "short", "min": 0, "max": 12, "decimals": 1, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"UV index\" WHERE entity_id = 'gw3000a_uv_index' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"title": "Solar max",
|
||||||
|
"type": "gauge",
|
||||||
|
"gridPos": { "h": 6, "w": 4, "x": 20, "y": 6 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "wattm2", "min": 0, "max": 1000, "decimals": 0, "color": { "mode": "continuous-GrYlRd" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "showThresholdMarkers": false, "orientation": "auto" },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"title": "",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 0, "y": 12 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "celsius", "decimals": 1, "displayName": "Gefühlt max", "color": { "mode": "fixed", "fixedColor": "#ff922b" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "textMode": "value_and_name", "wideLayout": true },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_feels_like_temperature' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"title": "",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 12 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "lengthmm",
|
||||||
|
"decimals": 1,
|
||||||
|
"displayName": "Regen",
|
||||||
|
"noValue": "0 mm",
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"thresholds": { "mode": "absolute", "steps": [
|
||||||
|
{ "color": "#868e96", "value": null },
|
||||||
|
{ "color": "#4dabf7", "value": 0.2 },
|
||||||
|
{ "color": "#1c7ed6", "value": 5 },
|
||||||
|
{ "color": "#1864ab", "value": 15 }
|
||||||
|
] }
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["max"], "fields": "" }, "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "textMode": "value_and_name", "wideLayout": true },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"mm\" WHERE entity_id = 'gw3000a_daily_rain' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"title": "",
|
||||||
|
"type": "stat",
|
||||||
|
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 12 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "pressurehpa", "decimals": 0, "displayName": "Luftdruck Ø", "color": { "mode": "fixed", "fixedColor": "#3bc9db" } }, "overrides": [] },
|
||||||
|
"options": { "reduceOptions": { "values": false, "calcs": ["mean"], "fields": "" }, "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "textMode": "value_and_name", "wideLayout": true },
|
||||||
|
"targets": [ { "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"hPa\" WHERE entity_id = 'gw3000a_relative_pressure' AND $__timeFilter(time) ORDER BY time" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"title": "Temperatur über den Tag",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 16 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "celsius", "custom": { "drawStyle": "line", "fillOpacity": 12, "lineWidth": 2, "showPoints": "never" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Außen" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#fa5252" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "Gefühlt" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#ff922b" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "C" }, "properties": [ { "id": "displayName", "value": "Taupunkt" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#4dabf7" } } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["min", "max"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_feels_like_temperature' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "C", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"°C\" WHERE entity_id = 'gw3000a_dewpoint' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"title": "Sonne: Solarstrahlung & UV",
|
||||||
|
"type": "timeseries",
|
||||||
|
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "custom": { "drawStyle": "line", "fillOpacity": 30, "lineWidth": 1, "showPoints": "never", "gradientMode": "opacity" } },
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "A" }, "properties": [ { "id": "displayName", "value": "Solar" }, { "id": "unit", "value": "wattm2" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#f2b705" } } ] },
|
||||||
|
{ "matcher": { "id": "byFrameRefID", "options": "B" }, "properties": [ { "id": "displayName", "value": "UV-Index" }, { "id": "unit", "value": "short" }, { "id": "color", "value": { "mode": "fixed", "fixedColor": "#ff6b6b" } }, { "id": "custom.axisPlacement", "value": "right" }, { "id": "custom.fillOpacity", "value": 0 }, { "id": "custom.lineWidth", "value": 2 } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom", "calcs": ["max"] }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "refId": "A", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time) ORDER BY time" },
|
||||||
|
{ "refId": "B", "datasource": { "type": "influxdb", "uid": "ha-weather-influx" }, "rawQuery": true, "format": "time_series", "rawSql": "SELECT time, value FROM \"UV index\" WHERE entity_id = 'gw3000a_uv_index' AND $__timeFilter(time) ORDER BY time" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"uid": "ha-weather-report-history",
|
||||||
|
"title": "Wetter-Tagesberichte KalliHome",
|
||||||
|
"tags": ["weather", "ecowitt", "homeassistant", "report"],
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "",
|
||||||
|
"time": { "from": "now-30d/d", "to": "now/d" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"links": [
|
||||||
|
{ "asDropdown": true, "icon": "external link", "includeVars": false, "keepTime": false, "tags": ["weather"], "targetBlank": false, "title": "Wetter-Dashboards", "tooltip": "", "type": "dashboards", "url": "" }
|
||||||
|
],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "",
|
||||||
|
"type": "text",
|
||||||
|
"gridPos": { "h": 4, "w": 24, "x": 0, "y": 0 },
|
||||||
|
"options": {
|
||||||
|
"mode": "markdown",
|
||||||
|
"content": "## Wetter-Tagesberichte\n\nEine Zeile pro Tag im gewählten Zeitbereich (Standard: letzte 30 Tage, Europe/Berlin). **Auf das Datum klicken** öffnet den ausführlichen [Tagesbericht](/d/ha-weather-day-report) für genau diesen Tag. Zeitbereich oben rechts ändern, um weiter zurückzublättern.\n\nHinweis: Die Tagesgrenzen dieser Übersicht liegen auf UTC-Mitternacht (~01:00/02:00 Europe/Berlin); der verlinkte Tagesbericht zeigt das gewählte Tagesfenster vollständig an. Regen ist der Tages-Maximalwert von `gw3000a_daily_rain`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Tagesübersicht",
|
||||||
|
"type": "table",
|
||||||
|
"gridPos": { "h": 20, "w": 24, "x": 0, "y": 4 },
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": { "align": "left", "cellOptions": { "type": "auto" }, "filterable": true, "inspect": false }
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{ "matcher": { "id": "byName", "options": "from_ms" }, "properties": [ { "id": "custom.hidden", "value": true } ] },
|
||||||
|
{ "matcher": { "id": "byName", "options": "to_ms" }, "properties": [ { "id": "custom.hidden", "value": true } ] },
|
||||||
|
{ "matcher": { "id": "byName", "options": "Datum" }, "properties": [ { "id": "links", "value": [ { "title": "Tagesbericht öffnen", "url": "/d/ha-weather-day-report?from=${__data.fields.from_ms}&to=${__data.fields.to_ms}&timezone=Europe/Berlin", "targetBlank": true } ] } ] },
|
||||||
|
{ "matcher": { "id": "byName", "options": "Bewertung" }, "properties": [ { "id": "custom.cellOptions", "value": { "type": "color-text" } }, { "id": "mappings", "value": [ { "type": "value", "options": { "Sonnig, warm": { "color": "#ffa94d", "index": 0 }, "Warm": { "color": "#ffd43b", "index": 1 }, "Regnerisch": { "color": "#4dabf7", "index": 2 }, "Kalt": { "color": "#74c0fc", "index": 3 }, "Unauffaellig": { "color": "#adb5bd", "index": 4 } } } ] } ] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false, "reducer": ["sum"], "countRows": false }, "sortBy": [ { "displayName": "Datum", "desc": true } ] },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "influxdb", "uid": "ha-weather-influx" },
|
||||||
|
"rawQuery": true,
|
||||||
|
"format": "table",
|
||||||
|
"rawSql": "WITH t AS (SELECT date_bin(INTERVAL '1 day', time) AS d, min(value) AS tmin, max(value) AS tmax, avg(value) AS tavg FROM \"°C\" WHERE entity_id = 'gw3000a_outdoor_temperature' AND $__timeFilter(time) GROUP BY 1), r AS (SELECT date_bin(INTERVAL '1 day', time) AS d, max(value) AS rain FROM \"mm\" WHERE entity_id = 'gw3000a_daily_rain' AND $__timeFilter(time) GROUP BY 1), u AS (SELECT date_bin(INTERVAL '1 day', time) AS d, max(value) AS uvmax FROM \"UV index\" WHERE entity_id = 'gw3000a_uv_index' AND $__timeFilter(time) GROUP BY 1), s AS (SELECT date_bin(INTERVAL '1 day', time) AS d, max(value) AS smax FROM \"W/m²\" WHERE entity_id = 'gw3000a_solar_radiation' AND $__timeFilter(time) GROUP BY 1), wg AS (SELECT date_bin(INTERVAL '1 day', time) AS d, max(value) AS wmax FROM \"km/h\" WHERE entity_id = 'gw3000a_wind_gust' AND $__timeFilter(time) GROUP BY 1) SELECT cast(cast(t.d AS date) AS varchar) AS \"Datum\", CASE WHEN s.smax >= 700 AND u.uvmax >= 6 THEN 'Sonnig, warm' WHEN t.tmax >= 25 THEN 'Warm' WHEN coalesce(r.rain, 0) >= 5 THEN 'Regnerisch' WHEN t.tmax <= 5 THEN 'Kalt' ELSE 'Unauffaellig' END AS \"Bewertung\", round(t.tmin, 1) AS \"T min °C\", round(t.tmax, 1) AS \"T max °C\", round(t.tavg, 1) AS \"T Mittel °C\", round(coalesce(r.rain, 0), 1) AS \"Regen mm\", round(u.uvmax, 1) AS \"UV max\", round(wg.wmax, 1) AS \"Böe max km/h\", cast(extract(epoch FROM t.d) AS bigint) * 1000 AS from_ms, cast(extract(epoch FROM t.d + INTERVAL '1 day') AS bigint) * 1000 AS to_ms FROM t LEFT JOIN r ON r.d = t.d LEFT JOIN u ON u.d = t.d LEFT JOIN s ON s.d = t.d LEFT JOIN wg ON wg.d = t.d ORDER BY t.d DESC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -31,3 +31,19 @@ datasources:
|
|||||||
insecureGrpc: true
|
insecureGrpc: true
|
||||||
secureJsonData:
|
secureJsonData:
|
||||||
token: $GRAFANA_INFLUXDB_TOKEN
|
token: $GRAFANA_INFLUXDB_TOKEN
|
||||||
|
|
||||||
|
# Wetter-/Langzeitarchiv aus Home Assistant (Ecowitt). Gleiche InfluxDB-Instanz,
|
||||||
|
# aber Datenbank `homeassistant`; gleicher Admin-Read-Token.
|
||||||
|
- name: InfluxDB HA Weather
|
||||||
|
uid: ha-weather-influx
|
||||||
|
type: influxdb
|
||||||
|
access: proxy
|
||||||
|
url: http://influxdb3-core:8181
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
version: SQL
|
||||||
|
dbName: homeassistant
|
||||||
|
httpMode: POST
|
||||||
|
insecureGrpc: true
|
||||||
|
secureJsonData:
|
||||||
|
token: $GRAFANA_INFLUXDB_TOKEN
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Dawarich PostgreSQL
|
||||||
|
uid: dawarich-postgres
|
||||||
|
type: postgres
|
||||||
|
access: proxy
|
||||||
|
url: dawarich_db:5432
|
||||||
|
database: dawarich_production
|
||||||
|
user: dawarich_grafana_ro
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
sslmode: disable
|
||||||
|
postgresVersion: 1700
|
||||||
|
timescaledb: false
|
||||||
|
secureJsonData:
|
||||||
|
password: $DAWARICH_GRAFANA_RO_PASSWORD
|
||||||
@@ -131,6 +131,78 @@ groups:
|
|||||||
summary: "Latest Borg backup completed with warnings"
|
summary: "Latest Borg backup completed with warnings"
|
||||||
description: "The latest Borg UI job completed with warnings for archive {{ $labels.archive }}."
|
description: "The latest Borg UI job completed with warnings for archive {{ $labels.archive }}."
|
||||||
|
|
||||||
|
- alert: HomelabBorgScopeSourceListMissing
|
||||||
|
expr: homelab_borg_scope_expected_file_present != 1
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Borg expected source list is not visible"
|
||||||
|
description: "Borg UI cannot see the repo source list used for drift checks."
|
||||||
|
|
||||||
|
- alert: HomelabBorgScopeMissingSources
|
||||||
|
expr: homelab_borg_scope_missing_sources_total > 0
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Borg UI is missing expected backup sources"
|
||||||
|
description: "Borg UI is missing {{ $value }} source path(s) from ops/borg-ui/all-important-sources.txt."
|
||||||
|
|
||||||
|
- alert: HomelabBorgScopeExtraSources
|
||||||
|
expr: homelab_borg_scope_extra_sources_total > 0
|
||||||
|
for: 30m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Borg UI has sources not tracked in the repo"
|
||||||
|
description: "Borg UI has {{ $value }} source path(s) that are not listed in ops/borg-ui/all-important-sources.txt."
|
||||||
|
|
||||||
|
- alert: HomelabBorgDumpMissing
|
||||||
|
expr: homelab_borg_dump_present == 0
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Borg pre-backup dump is missing: {{ $labels.dump }}"
|
||||||
|
description: "Expected dump artifact {{ $labels.dump }} is not present in the latest dump set. The pre-backup dump job may have failed or stopped."
|
||||||
|
|
||||||
|
- alert: HomelabBorgDumpStale
|
||||||
|
expr: homelab_borg_dump_age_seconds > 30 * 60 * 60
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Borg pre-backup dump is stale: {{ $labels.dump }}"
|
||||||
|
description: "Dump artifact {{ $labels.dump }} is older than 30 hours. pre-backup-dumps.sh may have stopped; Borg would keep archiving stale database content without a job failure."
|
||||||
|
|
||||||
|
- alert: HomelabBorgRepositoryCheckStale
|
||||||
|
expr: time() - homelab_borg_repository_last_check_timestamp_seconds > 14 * 24 * 60 * 60
|
||||||
|
for: 30m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Borg repository check is stale"
|
||||||
|
description: "Borg repository {{ $labels.repository }} has not had a recorded check for more than 14 days."
|
||||||
|
|
||||||
|
- alert: HomelabBorgRetentionDisabled
|
||||||
|
expr: homelab_borg_schedule_prune_after_enabled != 1
|
||||||
|
for: 30m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Borg retention pruning is disabled"
|
||||||
|
description: "Scheduled Borg job {{ $labels.schedule }} does not run prune after backup."
|
||||||
|
|
||||||
|
- alert: HomelabBorgCompactDisabled
|
||||||
|
expr: homelab_borg_schedule_compact_after_enabled != 1
|
||||||
|
for: 30m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Borg compaction is disabled"
|
||||||
|
description: "Scheduled Borg job {{ $labels.schedule }} does not run compact after backup."
|
||||||
|
|
||||||
- alert: HomelabCriticalContainerDown
|
- alert: HomelabCriticalContainerDown
|
||||||
expr: homelab_critical_container_running == 0
|
expr: homelab_critical_container_running == 0
|
||||||
for: 5m
|
for: 5m
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ global:
|
|||||||
site: kallilabcore
|
site: kallilabcore
|
||||||
|
|
||||||
rule_files:
|
rule_files:
|
||||||
- /etc/prometheus/alerts.yml
|
- /etc/prometheus/config/alerts.yml
|
||||||
|
|
||||||
alerting:
|
alerting:
|
||||||
alertmanagers:
|
alertmanagers:
|
||||||
@@ -36,6 +36,20 @@ scrape_configs:
|
|||||||
- targets:
|
- targets:
|
||||||
- traefik:8082
|
- traefik:8082
|
||||||
|
|
||||||
|
- job_name: dawarich
|
||||||
|
metrics_path: /metrics
|
||||||
|
basic_auth:
|
||||||
|
username: prometheus
|
||||||
|
password_file: /run/secrets/dawarich_metrics_password
|
||||||
|
http_headers:
|
||||||
|
X-Forwarded-Proto:
|
||||||
|
values:
|
||||||
|
- https
|
||||||
|
static_configs:
|
||||||
|
# Dawarich >= 1.7.7 serves aggregated web + Sidekiq metrics here.
|
||||||
|
- targets:
|
||||||
|
- dawarich_app:3000
|
||||||
|
|
||||||
- job_name: blackbox-http
|
- job_name: blackbox-http
|
||||||
metrics_path: /probe
|
metrics_path: /probe
|
||||||
params:
|
params:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Borg Backup Scope for KalliLabcore
|
# Borg Backup Scope for KalliLabcore
|
||||||
|
|
||||||
Stand: 2026-05-31
|
Stand: 2026-06-17
|
||||||
|
|
||||||
This file defines the target state for replacing Backrest with Borg in this homelab.
|
This file defines the target state for replacing Backrest with Borg in this homelab.
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ The Unraid flash configuration archive is intentional as well and must be treate
|
|||||||
| Traefik | file data | `/local/appdata/traefik` |
|
| Traefik | file data | `/local/appdata/traefik` |
|
||||||
| ntfy | file data | `/local/appdata/ntfy` |
|
| ntfy | file data | `/local/appdata/ntfy` |
|
||||||
| Paperless-GPT | file data | `/local/appdata/paperless-gpt` |
|
| Paperless-GPT | file data | `/local/appdata/paperless-gpt` |
|
||||||
| Tailscale | file data | `/local/appdata/tailscale` |
|
| Tailscale | Flash config artifact | covered by `/local/borg-dumps/unraid-flash-config.tar.gz`; no active `/local/appdata/tailscale` path |
|
||||||
| AdGuard | config only | `/local/appdata/adguard/conf` |
|
| AdGuard | config only | `/local/appdata/adguard/conf` |
|
||||||
| Borg UI | SQLite dump + self-backup | `/local/borg-dumps`, `/local/appdata/borg-ui/data` |
|
| Borg UI | SQLite dump + self-backup | `/local/borg-dumps`, `/local/appdata/borg-ui/data` |
|
||||||
| Komodo | config + Mongo dump | `/local/borg-dumps`, `/local/appdata/komodo/periphery`, `/local/appdata/komodo/core` |
|
| Komodo | config + Mongo dump | `/local/borg-dumps`, `/local/appdata/komodo/periphery`, `/local/appdata/komodo/core` |
|
||||||
@@ -48,11 +48,12 @@ The Unraid flash configuration archive is intentional as well and must be treate
|
|||||||
| Grafana | SQLite dump from `monitoring_grafana_data` + provisioned config in Git | `/local/borg-dumps`, `monitoring/grafana/provisioning`, `monitoring/grafana/dashboards` |
|
| Grafana | SQLite dump from `monitoring_grafana_data` + provisioned config in Git | `/local/borg-dumps`, `monitoring/grafana/provisioning`, `monitoring/grafana/dashboards` |
|
||||||
| Filebrowser | file-backed state dump + file data | `/local/borg-dumps`, `/local/appdata/filebrowser` |
|
| Filebrowser | file-backed state dump + file data | `/local/borg-dumps`, `/local/appdata/filebrowser` |
|
||||||
| InfluxDB 3 Core | file data | `/local/appdata/influxdb3/data`, `/local/appdata/influxdb3/plugins` |
|
| InfluxDB 3 Core | file data | `/local/appdata/influxdb3/data`, `/local/appdata/influxdb3/plugins` |
|
||||||
|
| n8n | SQLite dump + encrypted workflow/credential state | `/local/borg-dumps`, `/local/appdata/n8n/data` |
|
||||||
| Home Assistant | HA-native backup + file state | `/local/appdata/homeassistant`, `/local/services/smart-home-kalli` |
|
| Home Assistant | HA-native backup + file state | `/local/appdata/homeassistant`, `/local/services/smart-home-kalli` |
|
||||||
| Smart-Home MQTT / Mosquitto | file data | `/local/appdata/mosquitto/config`, `/local/appdata/mosquitto/data`, `/local/appdata/mosquitto/log` |
|
| Smart-Home MQTT / Mosquitto | file data | `/local/appdata/mosquitto/config`, `/local/appdata/mosquitto/data` |
|
||||||
| Zigbee2MQTT (planned) | file data + coordinator state | `/local/appdata/zigbee2mqtt`, `/local/services/smart-home-kalli` |
|
| Zigbee2MQTT (planned) | file data + coordinator state | `/local/appdata/zigbee2mqtt`, `/local/services/smart-home-kalli` |
|
||||||
| ESPHome (planned) | Fachrepo + optional build/runtime cache | `/local/services/smart-home-kalli/esphome`, optional `/local/appdata/esphome` |
|
| ESPHome (planned) | Fachrepo + optional build/runtime cache | `/local/services/smart-home-kalli/esphome`, optional `/local/appdata/esphome` |
|
||||||
| Hermes Agent | file data + SSH key | `/local/appdata/hermes-agent/data`, `/local/secrets/hermes_runner_id_ed25519` |
|
| Hermes Agent | file data + SSH key | SSH-Key via `/local/secrets`; `/local/appdata/hermes-agent/data` ist bewusst NICHT in `all-important-sources.txt`, weil der Stack geparkt ist (Review 2026-07-25). Beim Aktivieren des Stacks in die Quellliste aufnehmen. |
|
||||||
| BentoPDF | rebuildable | no critical persistence in compose |
|
| BentoPDF | rebuildable | no critical persistence in compose |
|
||||||
|
|
||||||
## Open Decisions and Coverage Gaps
|
## Open Decisions and Coverage Gaps
|
||||||
@@ -71,6 +72,17 @@ Option A umgesetzt: `pre-backup-dumps.sh` writes `nextcloud.dump` from `nextclou
|
|||||||
|
|
||||||
The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homelab-infra`, while Komodo keeps stack workspaces below `/mnt/user/services/stacks`. These paths are now mounted into Borg UI as `/local/services/...` and included explicitly so host-side script hotfixes, stack workspace state, and posture-check state are recoverable.
|
The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homelab-infra`, while Komodo keeps stack workspaces below `/mnt/user/services/stacks`. These paths are now mounted into Borg UI as `/local/services/...` and included explicitly so host-side script hotfixes, stack workspace state, and posture-check state are recoverable.
|
||||||
|
|
||||||
|
### User-Daten-Shares ausserhalb des App-Scope
|
||||||
|
|
||||||
|
Filebrowser serviert `/mnt/user/projekte`, `/mnt/user/documents` und `/mnt/user/photos` komplett (`ops/filebrowser/docker-compose.yml`). Der Borg-Scope deckt aber bewusst nur die App-Unterordner ab (`documents/paperless*`, `documents/nextcloud-data`, `documents/scans_inbox`, `photos/immich`, `photos/family_archive`).
|
||||||
|
|
||||||
|
- **`/mnt/user/projekte`** ist aktuell in **keinem** Borg-Scope. Ad-hoc-Dateien, die direkt unter `documents/` oder `photos/` (ausserhalb der genannten App-Ordner) abgelegt werden, ebenfalls nicht.
|
||||||
|
- Entscheidung Operator offen (Eintrag in `docs/MASTER_TODO.md`): Entweder `projekte` als eigenen read-only Borg-UI-Mount + Quelllisten-Eintrag aufnehmen, oder bewusst als "nur lokal, nicht DR-relevant" bestaetigen. Bis zur Entscheidung gilt: dort liegende Originaldaten sind **nicht** wiederherstellbar.
|
||||||
|
|
||||||
|
### Komodo keys
|
||||||
|
|
||||||
|
Production still stores Komodo Core/Periphery keys in the Docker named volume `komodo_komodo_keys`. This is a known open migration item and is not fixed by the Borg source list alone. Target state: move the keys to a host path such as `/mnt/user/appdata/komodo/keys` and mount that path into both Komodo containers, then include it in Borg. Do not treat this as solved until the live Compose stack has been migrated and Periphery reconnect has been verified.
|
||||||
|
|
||||||
## Database Dumps Required
|
## Database Dumps Required
|
||||||
|
|
||||||
### Shared PostgreSQL (`postgresql17`, runtime PostgreSQL 18)
|
### Shared PostgreSQL (`postgresql17`, runtime PostgreSQL 18)
|
||||||
@@ -89,6 +101,7 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
|
|||||||
|
|
||||||
- Komodo MongoDB
|
- Komodo MongoDB
|
||||||
- SQLite: `gitea`, `vaultwarden`, `speedtest-tracker`, `borg-ui`, `grafana`
|
- SQLite: `gitea`, `vaultwarden`, `speedtest-tracker`, `borg-ui`, `grafana`
|
||||||
|
- SQLite: `n8n` (`n8n.sqlite.dump`, credentials require the matching `N8N_ENCRYPTION_KEY`)
|
||||||
- File-backed state: `filebrowser.bolt.dump`
|
- File-backed state: `filebrowser.bolt.dump`
|
||||||
- Unraid flash config: `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256`
|
- Unraid flash config: `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256`
|
||||||
- Home Assistant native backups: created by HA under `/mnt/user/appdata/homeassistant/backups` and captured as file state
|
- Home Assistant native backups: created by HA under `/mnt/user/appdata/homeassistant/backups` and captured as file state
|
||||||
|
|||||||
@@ -14,11 +14,16 @@
|
|||||||
/local/appdata/traefik
|
/local/appdata/traefik
|
||||||
/local/appdata/ntfy
|
/local/appdata/ntfy
|
||||||
/local/appdata/paperless-gpt
|
/local/appdata/paperless-gpt
|
||||||
/local/appdata/tailscale
|
|
||||||
/local/appdata/adguard/conf
|
/local/appdata/adguard/conf
|
||||||
/local/appdata/borg-ui/data
|
/local/appdata/borg-ui/data
|
||||||
/local/appdata/komodo/periphery
|
/local/appdata/komodo/periphery
|
||||||
/local/appdata/komodo/core
|
/local/appdata/komodo/core
|
||||||
|
/local/appdata/nextcloud/html
|
||||||
|
/local/nextcloud/data
|
||||||
|
/local/appdata/n8n/data
|
||||||
|
/local/appdata/filebrowser
|
||||||
|
/local/appdata/influxdb3/data
|
||||||
|
/local/appdata/influxdb3/plugins
|
||||||
/local/services/homelab-infra
|
/local/services/homelab-infra
|
||||||
/local/services/smart-home-kalli
|
/local/services/smart-home-kalli
|
||||||
/local/services/stacks
|
/local/services/stacks
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
borg-ui:
|
borg-ui:
|
||||||
image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f
|
image: ainullcode/borg-ui@sha256:e51b3d2e6cb38d1ba127ef60ba442c1e157965327196e6f7afb69f30c0ba99d1
|
||||||
container_name: borg-ui
|
container_name: borg-ui
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ main() {
|
|||||||
# Additional host-side SQLite dumps for admin tooling with appdata files.
|
# Additional host-side SQLite dumps for admin tooling with appdata files.
|
||||||
dump_sqlite_file "/mnt/user/appdata/borg-ui/data/borg.db" "$LATEST_DIR/borg-ui.sqlite" "borg-ui"
|
dump_sqlite_file "/mnt/user/appdata/borg-ui/data/borg.db" "$LATEST_DIR/borg-ui.sqlite" "borg-ui"
|
||||||
dump_sqlite_file "/var/lib/docker/volumes/monitoring_grafana_data/_data/grafana.db" "$LATEST_DIR/grafana.sqlite" "grafana"
|
dump_sqlite_file "/var/lib/docker/volumes/monitoring_grafana_data/_data/grafana.db" "$LATEST_DIR/grafana.sqlite" "grafana"
|
||||||
|
dump_sqlite_file "/mnt/user/appdata/n8n/data/database.sqlite" "$LATEST_DIR/n8n.sqlite.dump" "n8n"
|
||||||
|
|
||||||
# MongoDB
|
# MongoDB
|
||||||
dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz"
|
dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz"
|
||||||
|
|||||||
@@ -22,12 +22,31 @@ case "${DUMP_ROOT:-}" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Externer Dead-Man's-Switch (endpoint-agnostisch: Healthchecks.io-Cloud oder
|
||||||
|
# self-hosted). ntfy meldet nur Fehler eines tatsaechlich gestarteten Laufs;
|
||||||
|
# der externe Switch faengt zusaetzlich den Fall ab, dass der Pre-Hook gar nicht
|
||||||
|
# laeuft. Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln,
|
||||||
|
# niemals ins Repo. Ist keine URL gesetzt, ist der Switch ein No-Op.
|
||||||
|
HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-${HEALTHCHECKS_BORG_URL:-}}"
|
||||||
|
HEALTHCHECKS_URL_FILE="${HEALTHCHECKS_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_borg_url}"
|
||||||
|
if [ -z "$HEALTHCHECKS_URL" ] && [ -r "$HEALTHCHECKS_URL_FILE" ]; then
|
||||||
|
HEALTHCHECKS_URL="$(tr -d '[:space:]' < "$HEALTHCHECKS_URL_FILE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
hc_ping() {
|
||||||
|
# $1: optionaler Suffix ("/start" | "/fail"); leer = Erfolg
|
||||||
|
[ -n "$HEALTHCHECKS_URL" ] || return 0
|
||||||
|
command -v curl >/dev/null 2>&1 || return 0
|
||||||
|
curl -fsS -m 10 --retry 3 "${HEALTHCHECKS_URL}${1:-}" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
notify_failure() {
|
notify_failure() {
|
||||||
local step="$1"
|
local step="$1"
|
||||||
local message="$2"
|
local message="$2"
|
||||||
if [ -x "$NTFY_SCRIPT" ]; then
|
if [ -x "$NTFY_SCRIPT" ]; then
|
||||||
"$NTFY_SCRIPT" "$NTFY_TOPIC" "Borg pre-hook failed: $step" "$message" high || true
|
"$NTFY_SCRIPT" "$NTFY_TOPIC" "Borg pre-hook failed: $step" "$message" high || true
|
||||||
fi
|
fi
|
||||||
|
hc_ping "/fail"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_step() {
|
run_step() {
|
||||||
@@ -44,6 +63,8 @@ run_step() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
echo "[pre-borg] Running posture-check"
|
echo "[pre-borg] Running posture-check"
|
||||||
if "$POSTURE_CHECK"; then
|
if "$POSTURE_CHECK"; then
|
||||||
echo "[pre-borg] OK: posture-check"
|
echo "[pre-borg] OK: posture-check"
|
||||||
@@ -60,3 +81,4 @@ run_step "pre-backup-dumps" "$PRE_BACKUP_DUMPS"
|
|||||||
run_step "restore-freshness" env DUMP_ROOT="$FRESHNESS_DUMP_ROOT" "$FRESHNESS_CHECK"
|
run_step "restore-freshness" env DUMP_ROOT="$FRESHNESS_DUMP_ROOT" "$FRESHNESS_CHECK"
|
||||||
|
|
||||||
echo "[pre-borg] All pre-flight checks passed"
|
echo "[pre-borg] All pre-flight checks passed"
|
||||||
|
hc_ping
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
code-server:
|
code-server:
|
||||||
image: lscr.io/linuxserver/code-server:4.123.0@sha256:cb261a7f87674b445e0fd66d87d55900c1b823d276c727ab0d168a75e69e9992
|
image: lscr.io/linuxserver/code-server:4.125.0@sha256:7e9523734c003b6336781942df7b48aa6936a9df6931c12a19a1f7ad7858eeba
|
||||||
container_name: code-server
|
container_name: code-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76
|
image: filebrowser/filebrowser:v2.63.15@sha256:9805b21cf910f3ef6f4a1c8f441f1dd6cc4197136f9541fe2a1ab6d050706e4b
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -106,10 +106,91 @@
|
|||||||
- timezone: UTC
|
- timezone: UTC
|
||||||
label: UTC
|
label: UTC
|
||||||
|
|
||||||
- type: weather
|
- type: custom-api
|
||||||
location: Berlin, Germany
|
title: Wetter · KalliHome
|
||||||
units: metric
|
title-url: https://home.kaleschke.info
|
||||||
hour-format: 24h
|
cache: 30s
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_outdoor_temperature
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
Content-Type: application/json
|
||||||
|
subrequests:
|
||||||
|
feels:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_feels_like_temperature
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
humidity:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_humidity
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
wind:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_wind_speed
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
gust:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_wind_gust
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
rain:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_daily_rain
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
solar:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_solar_radiation
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
uv:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_uv_index
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
pressure:
|
||||||
|
url: http://homeassistant:8123/api/states/sensor.gw3000a_relative_pressure
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_HA_TOKEN}
|
||||||
|
template: |
|
||||||
|
{{ $temp := .JSON.String "state" }}
|
||||||
|
{{ $feels := (.Subrequest "feels").JSON.String "state" }}
|
||||||
|
{{ $hum := (.Subrequest "humidity").JSON.String "state" }}
|
||||||
|
{{ $wind := (.Subrequest "wind").JSON.String "state" }}
|
||||||
|
{{ $gust := (.Subrequest "gust").JSON.String "state" }}
|
||||||
|
{{ $rain := (.Subrequest "rain").JSON.String "state" }}
|
||||||
|
{{ $solar := (.Subrequest "solar").JSON.String "state" }}
|
||||||
|
{{ $uv := (.Subrequest "uv").JSON.String "state" }}
|
||||||
|
{{ $press := (.Subrequest "pressure").JSON.String "state" }}
|
||||||
|
{{ $gustF := (.Subrequest "gust").JSON.Float "state" }}
|
||||||
|
{{ $divider := "border-left: 1px solid hsla(220, 40%, 70%, 0.14);" }}
|
||||||
|
<div class="text-center" style="margin-bottom: 12px;">
|
||||||
|
<div class="color-highlight size-h2" style="font-weight: 700;">{{ $temp }}°C</div>
|
||||||
|
<div class="size-h6 color-subdue">gefühlt {{ $feels }}° · {{ $hum }}% feucht</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-center" style="margin-bottom: 12px;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div class="size-h4 {{ if gt $gustF 40.0 }}color-negative{{ else }}color-highlight{{ end }}">{{ $wind }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">km/h Wind</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; {{ $divider }}">
|
||||||
|
<div class="size-h4 {{ if gt $gustF 40.0 }}color-negative{{ else }}color-highlight{{ end }}">{{ $gust }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">km/h Böe</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; {{ $divider }}">
|
||||||
|
<div class="size-h4 color-highlight">{{ $rain }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">mm heute</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-center">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div class="size-h4 color-highlight">{{ $solar }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">W/m² Solar</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; {{ $divider }}">
|
||||||
|
<div class="size-h4 color-highlight">{{ $uv }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">UV-Index</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; {{ $divider }}">
|
||||||
|
<div class="size-h4 color-highlight">{{ $press }}</div>
|
||||||
|
<div class="size-h6 uppercase color-subdue">hPa Druck</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
- type: calendar
|
- type: calendar
|
||||||
first-day-of-week: monday
|
first-day-of-week: monday
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
GLANCE_GITEA_TOKEN: ${GLANCE_GITEA_TOKEN:-}
|
GLANCE_GITEA_TOKEN: ${GLANCE_GITEA_TOKEN:-}
|
||||||
GLANCE_PAPERLESS_TOKEN: ${GLANCE_PAPERLESS_TOKEN:-}
|
GLANCE_PAPERLESS_TOKEN: ${GLANCE_PAPERLESS_TOKEN:-}
|
||||||
GLANCE_MEALIE_TOKEN: ${GLANCE_MEALIE_TOKEN:-}
|
GLANCE_MEALIE_TOKEN: ${GLANCE_MEALIE_TOKEN:-}
|
||||||
|
GLANCE_HA_TOKEN: ${GLANCE_HA_TOKEN:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config:ro
|
- ./config:/app/config:ro
|
||||||
- ./assets:/app/assets:ro
|
- ./assets:/app/assets:ro
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
glances:
|
glances:
|
||||||
image: nicolargo/glances:latest-full@sha256:60872a1af0e40a3150975617c7e811ad7ad48f95bc45d033fb0c1737a037e4d2
|
image: nicolargo/glances:latest-full@sha256:58651aabedf62db8bfc1d252f8d3889675dfcdb5d0ad1c177ae5879c21626f3a
|
||||||
container_name: glances
|
container_name: glances
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
pid: host
|
pid: host
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# H:/ Nearline-Backup — Struktur und Betrieb
|
# H:/ Nearline-Backup — Struktur und Betrieb
|
||||||
|
|
||||||
Stand: 2026-06-10
|
Stand: 2026-06-21
|
||||||
|
|
||||||
## Rolle der H:/
|
## Rolle der H:/
|
||||||
|
|
||||||
@@ -25,6 +25,30 @@ Nichts weiteres gehört dauerhaft auf die H:/.
|
|||||||
Temporäre Recovery- oder Backup-Ordner aus Notfallsituationen sind nach
|
Temporäre Recovery- oder Backup-Ordner aus Notfallsituationen sind nach
|
||||||
Abschluss zu löschen.
|
Abschluss zu löschen.
|
||||||
|
|
||||||
|
## Restore aus H:/ (DR-Fall)
|
||||||
|
|
||||||
|
Wenn Unraid/Hetzner nicht erreichbar sind, ist die H:/ die schnellste **lokale**
|
||||||
|
Quelle für die aktuellsten DB-Dumps und Gitea-Bundles. Sie ersetzt **nicht** die
|
||||||
|
Hetzner-Borg-Kette (Einordnung: `docs/CAPACITY_AND_LIFECYCLE.md`), verkürzt aber
|
||||||
|
den Wiederanlauf, solange die Artefakte frisch sind.
|
||||||
|
|
||||||
|
Inhalt und Restore-Weg:
|
||||||
|
|
||||||
|
- **DB-Dumps:** `H:\kallilab-nearline-backups\borg-dumps\latest\*` — dieselben
|
||||||
|
Dateien wie `/mnt/user/backups/borg/dumps/latest`. Im DR-Fall auf den neu
|
||||||
|
aufgesetzten Host nach `/mnt/user/backups/borg/dumps/latest` zurückspielen
|
||||||
|
(SMB/Robocopy), dann pro Dienst nach `docs/RESTORE_MATRIX.md` einspielen.
|
||||||
|
- **Gitea-Bundles:** `H:\kallilab-nearline-backups\git-bundles\gitea\` — bare
|
||||||
|
Repo-Bundles für den Gitea-Bootstrap (Reihenfolge: `docs/SERVICES_RECOVERY.md`).
|
||||||
|
- **DR-Kit (Keys/Secrets):** `H:\kallilab-nearline-backups\_dr-kit\` — SSH-Keys
|
||||||
|
und Offline-Secrets, siehe Abschnitt `_dr-kit` unten.
|
||||||
|
|
||||||
|
> **Vor dem Verlassen auf H:/ immer die Frische prüfen:** Die LastWriteTime der
|
||||||
|
> Dumps muss vom selben Tag sein
|
||||||
|
> (`Get-ChildItem H:\kallilab-nearline-backups\borg-dumps\latest`). Ein still
|
||||||
|
> veralteter Spiegel (siehe S4U-Vorfall unten) ist als Restore-Quelle wertlos —
|
||||||
|
> dann stattdessen aus Hetzner-Borg restaurieren.
|
||||||
|
|
||||||
## Automatischer Pull
|
## Automatischer Pull
|
||||||
|
|
||||||
`pull-critical-backups.ps1` zieht per Robocopy vom Unraid-SMB-Share:
|
`pull-critical-backups.ps1` zieht per Robocopy vom Unraid-SMB-Share:
|
||||||
@@ -35,7 +59,25 @@ Abschluss zu löschen.
|
|||||||
Der Windows Scheduled Task `KalliLab H Drive Nearline Pull` laeuft seit
|
Der Windows Scheduled Task `KalliLab H Drive Nearline Pull` laeuft seit
|
||||||
2026-05-28 taeglich 05:30. Das Script kopiert bewusst **nicht** mit `/MIR` und
|
2026-05-28 taeglich 05:30. Das Script kopiert bewusst **nicht** mit `/MIR` und
|
||||||
loescht nichts auf H:/; alte Artefakte werden nur nach manueller Sichtpruefung
|
loescht nichts auf H:/; alte Artefakte werden nur nach manueller Sichtpruefung
|
||||||
entfernt. Aufruf zum Testen:
|
entfernt.
|
||||||
|
|
||||||
|
> **Wichtig — Task-LogonType (kein S4U!):** Der Task darf **nicht** mit `S4U`
|
||||||
|
> laufen ("Unabhaengig von der Benutzeranmeldung ausfuehren" *mit* angehaktem
|
||||||
|
> "Kennwort nicht speichern"). S4U-Laeufe haben keine Netzwerk-Anmelde-
|
||||||
|
> informationen und erreichen den authentifizierten SMB-Share
|
||||||
|
> `\\192.168.178.58\backups` nicht -> jeder geplante Lauf bricht still mit
|
||||||
|
> Exitcode 1 ab, ohne Report. Genau das passierte 2026-06-19 bis 2026-06-21
|
||||||
|
> (Spiegel still veraltet).
|
||||||
|
>
|
||||||
|
> **Behoben am 2026-06-21:** Task auf **"Nur ausfuehren, wenn der Benutzer
|
||||||
|
> angemeldet ist"** (LogonType `Interactive`) umgestellt. Das braucht **kein**
|
||||||
|
> gespeichertes Passwort und funktioniert, weil `michi` der dauerhaft
|
||||||
|
> angemeldete Konsolen-User ist (gesperrter Bildschirm zaehlt als angemeldet).
|
||||||
|
> Per Planer ausgeloest und mit Ergebnis `0x0` verifiziert. Alternative waere
|
||||||
|
> LogonType `Password` (gespeichertes Passwort), erfordert aber das
|
||||||
|
> Windows-Passwort.
|
||||||
|
|
||||||
|
Aufruf zum Testen:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1 -WhatIf
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1 -WhatIf
|
||||||
@@ -45,6 +87,47 @@ Das Script schließt bewusst aus:
|
|||||||
- `unraid-flash-config.tar.gz` (0600 root:root, nicht per SMB zugänglich → Restore aus Hetzner-Borg)
|
- `unraid-flash-config.tar.gz` (0600 root:root, nicht per SMB zugänglich → Restore aus Hetzner-Borg)
|
||||||
- Migration-/Cutover-Verzeichnisse (`immich-vectorchord-*`, `pg18-major-*`, `redis8-*` etc.)
|
- Migration-/Cutover-Verzeichnisse (`immich-vectorchord-*`, `pg18-major-*`, `redis8-*` etc.)
|
||||||
|
|
||||||
|
## Externer Dead-Man's-Switch
|
||||||
|
|
||||||
|
Der Pull lief ~2026-06-04 bis 2026-06-18 still gestoppt, ohne dass etwas Alarm
|
||||||
|
schlug (Scheduled Task fehlte; Prometheus auf Unraid sieht den baerchen-Pull
|
||||||
|
nicht). Gegenmittel: ein externer Heartbeat. `pull-critical-backups.ps1` pingt am
|
||||||
|
Lauf-Anfang `/start`, am Ende den Erfolg und im Fehlerfall `/fail`. Bleibt der
|
||||||
|
Erfolgs-Ping aus, alarmiert der externe Dienst von aussen.
|
||||||
|
|
||||||
|
Die Integration ist **endpoint-agnostisch**: jede Healthchecks-kompatible URL
|
||||||
|
funktioniert (Healthchecks.io-Cloud oder self-hosted). Ist keine URL gesetzt, ist
|
||||||
|
der Switch ein No-Op und der Pull laeuft unveraendert weiter.
|
||||||
|
|
||||||
|
URL-Quelle (in dieser Reihenfolge):
|
||||||
|
|
||||||
|
1. Parameter `-HealthchecksUrl`
|
||||||
|
2. ENV `HEALTHCHECKS_NEARLINE_URL`
|
||||||
|
3. Datei `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`
|
||||||
|
|
||||||
|
Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln, nie ins Repo
|
||||||
|
(siehe `docs/SECRETS_MAP.md`).
|
||||||
|
|
||||||
|
### Operator-Setup (einmalig)
|
||||||
|
|
||||||
|
1. Check anlegen (Healthchecks.io oder self-hosted): Period = 1 Tag, Grace z. B.
|
||||||
|
2-3 h passend zum taeglichen 05:30-Task. Ping-URL kopieren.
|
||||||
|
2. Auf baerchen die URL hinterlegen, z. B. als Datei:
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Force -Path "$HOME\.kallilab" | Out-Null
|
||||||
|
Set-Content -LiteralPath "$HOME\.kallilab\healthchecks-nearline-url.txt" -Value "https://hc-ping.com/<uuid>" -NoNewline
|
||||||
|
```
|
||||||
|
(alternativ als persistente User-Umgebungsvariable `HEALTHCHECKS_NEARLINE_URL`.)
|
||||||
|
3. Testlauf: `pull-critical-backups.ps1` ausfuehren; im Healthchecks-Dashboard
|
||||||
|
muss ein "success"-Ping ankommen.
|
||||||
|
|
||||||
|
### Borg-Pre-Hook-Pendant
|
||||||
|
|
||||||
|
Den gleichen Switch gibt es host-seitig in `ops/borg-ui/scripts/pre-borg.sh`.
|
||||||
|
URL dort via ENV `HEALTHCHECKS_BORG_URL` oder Datei
|
||||||
|
`/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600), bewusst als
|
||||||
|
**eigener** Check (getrennter Heartbeat fuer die Unraid-Backup-Vorpruefung).
|
||||||
|
|
||||||
## _dr-kit
|
## _dr-kit
|
||||||
|
|
||||||
Enthält offline hinterlegte Schlüssel und Secrets für den DR-Fall:
|
Enthält offline hinterlegte Schlüssel und Secrets für den DR-Fall:
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
param(
|
param(
|
||||||
[string]$SourceRoot = "\\192.168.178.58\backups",
|
[string]$SourceRoot = "\\192.168.178.58\backups",
|
||||||
[string]$DestinationRoot = "H:\kallilab-nearline-backups",
|
[string]$DestinationRoot = "H:\kallilab-nearline-backups",
|
||||||
|
[string]$HealthchecksUrl = $env:HEALTHCHECKS_NEARLINE_URL,
|
||||||
[switch]$WhatIf
|
[switch]$WhatIf
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Externer Dead-Man's-Switch (endpoint-agnostisch: Healthchecks.io-Cloud oder
|
||||||
|
# self-hosted). Bleibt der Erfolgs-Ping aus, alarmiert der externe Dienst von
|
||||||
|
# aussen - genau den Fall, den Prometheus auf Unraid fuer den baerchen-Pull
|
||||||
|
# nicht sieht. Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln,
|
||||||
|
# niemals ins Repo. Quelle: -HealthchecksUrl -> $env:HEALTHCHECKS_NEARLINE_URL
|
||||||
|
# -> Datei im Userprofil. Ist keine URL gesetzt, ist der Switch ein No-Op.
|
||||||
|
if (-not $HealthchecksUrl) {
|
||||||
|
$hcUrlFile = Join-Path $HOME ".kallilab\healthchecks-nearline-url.txt"
|
||||||
|
if (Test-Path -LiteralPath $hcUrlFile) {
|
||||||
|
$HealthchecksUrl = (Get-Content -LiteralPath $hcUrlFile -Raw).Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-HealthcheckPing {
|
||||||
|
param([string]$Suffix = "")
|
||||||
|
if (-not $HealthchecksUrl) { return }
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri ("{0}{1}" -f $HealthchecksUrl, $Suffix) -Method Get -TimeoutSec 15 | Out-Null
|
||||||
|
} catch {
|
||||||
|
Write-Warning "Healthchecks ping ('$Suffix') failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$Jobs = @(
|
$Jobs = @(
|
||||||
@{
|
@{
|
||||||
Name = "borg-dumps-latest"
|
Name = "borg-dumps-latest"
|
||||||
@@ -25,6 +49,7 @@ $Jobs = @(
|
|||||||
"immich.dump",
|
"immich.dump",
|
||||||
"komodo-mongo.archive.gz",
|
"komodo-mongo.archive.gz",
|
||||||
"mealie.dump",
|
"mealie.dump",
|
||||||
|
"n8n.sqlite.dump",
|
||||||
"nextcloud.dump",
|
"nextcloud.dump",
|
||||||
"postgresql17-authelia.dump",
|
"postgresql17-authelia.dump",
|
||||||
"postgresql17-globals.sql",
|
"postgresql17-globals.sql",
|
||||||
@@ -144,6 +169,10 @@ if ($WhatIf) {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Echter Lauf -> Dead-Man's-Switch aktiv. /start misst die Laufzeit, /fail
|
||||||
|
# meldet einen abgebrochenen Lauf sofort, der Erfolgs-Ping am Ende bestaetigt.
|
||||||
|
Send-HealthcheckPing "/start"
|
||||||
|
try {
|
||||||
$destinationDrive = Split-Path -Qualifier $DestinationRoot
|
$destinationDrive = Split-Path -Qualifier $DestinationRoot
|
||||||
Assert-PathExists -Path $destinationDrive -Label "Destination drive"
|
Assert-PathExists -Path $destinationDrive -Label "Destination drive"
|
||||||
|
|
||||||
@@ -185,3 +214,13 @@ $lines | Set-Content -LiteralPath $reportPath -Encoding UTF8
|
|||||||
|
|
||||||
Write-Host "H:/ nearline pull completed."
|
Write-Host "H:/ nearline pull completed."
|
||||||
Write-Host "Report: $reportPath"
|
Write-Host "Report: $reportPath"
|
||||||
|
|
||||||
|
Send-HealthcheckPing
|
||||||
|
# Explizit erfolgreich beenden: sonst leakt der letzte robocopy-Exitcode
|
||||||
|
# (1 = "Dateien kopiert") als Prozess-Exit und der Scheduled Task meldet
|
||||||
|
# 0x1, obwohl der Lauf sauber war.
|
||||||
|
exit 0
|
||||||
|
} catch {
|
||||||
|
Send-HealthcheckPing "/fail"
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nousresearch/hermes-agent:v2026.6.5
|
FROM nousresearch/hermes-agent:v2026.6.19
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,13 @@
|
|||||||
"description": "VPN / Remote-Zugang",
|
"description": "VPN / Remote-Zugang",
|
||||||
"tier": 1,
|
"tier": 1,
|
||||||
"category": "core",
|
"category": "core",
|
||||||
"container_name": "tailscale",
|
"container_name": null,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"url": null,
|
"url": null,
|
||||||
"dump_file": null,
|
"dump_file": null,
|
||||||
"data_paths": ["/mnt/user/appdata/tailscale"],
|
"data_paths": ["/boot/config/plugins/tailscale/state"],
|
||||||
"first_check": "Tailscale Status auf Host pruefen; State-Datei fuer Key-Renewal vorhanden?",
|
"first_check": "Tailscale Status auf Host pruefen; native Unraid-Plugin-Instanz und Subnet-Route aktiv?",
|
||||||
"notes": "network_mode: host; NET_ADMIN, NET_RAW, /dev/net/tun — dokumentierte VPN-Ausnahmen"
|
"notes": "Natives Unraid-Plugin, nicht Docker/Komodo-verwaltet; State liegt im Flash-Backup. Alter Docker-State ist archiviert unter /mnt/user/appdata/_archive/tailscale-removed-2026-06-06/"
|
||||||
},
|
},
|
||||||
"gitea": {
|
"gitea": {
|
||||||
"description": "Git-Server — operative Quelle der Wahrheit fuer GitOps",
|
"description": "Git-Server — operative Quelle der Wahrheit fuer GitOps",
|
||||||
|
|||||||
@@ -75,14 +75,14 @@ services:
|
|||||||
description: VPN / Remote-Zugang
|
description: VPN / Remote-Zugang
|
||||||
tier: 1
|
tier: 1
|
||||||
category: core
|
category: core
|
||||||
container_name: tailscale
|
container_name: null
|
||||||
dependencies: []
|
dependencies: []
|
||||||
url: null
|
url: null
|
||||||
dump_file: null
|
dump_file: null
|
||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/tailscale
|
- /boot/config/plugins/tailscale/state
|
||||||
first_check: "Tailscale Status auf Host pruefen; State-Datei fuer Key-Renewal vorhanden?"
|
first_check: "Tailscale Status auf Host pruefen; native Unraid-Plugin-Instanz und Subnet-Route aktiv?"
|
||||||
notes: "network_mode: host; NET_ADMIN, NET_RAW, /dev/net/tun — dokumentierte VPN-Ausnahmen"
|
notes: "Natives Unraid-Plugin, nicht Docker/Komodo-verwaltet; State liegt im Flash-Backup. Alter Docker-State ist archiviert unter /mnt/user/appdata/_archive/tailscale-removed-2026-06-06/"
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
description: Git-Server — operative Quelle der Wahrheit fuer GitOps
|
description: Git-Server — operative Quelle der Wahrheit fuer GitOps
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Stand 2026-06-11 ist der Betrieb auf V1+ (validierte Bash-Host-Jobs mit ntfy):
|
|||||||
# Frische-Check
|
# Frische-Check
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
|
|
||||||
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|komodo-bootstrap|nextcloud)
|
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|komodo-bootstrap|nextcloud)
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst>
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst>
|
||||||
|
|
||||||
# Negativtest des Alarmwegs (quartalsweise)
|
# Negativtest des Alarmwegs (quartalsweise)
|
||||||
@@ -77,6 +77,7 @@ Einziger Status-Ort ist die **Reifegrad-Tabelle** in `docs/RESTORE_MATRIX.md`
|
|||||||
- **Nextcloud:** Test am 2026-06-03 erfolgreich, aber mit Unraid-shfs-Eigenheit: Nextcloud fuehrt `chmod()` unter `/var/www/html` aus, was auf FUSE/shfs scheitert. Das Skript patcht `check_data_directory_permissions: false` und legt den `.ncdata`-Marker an.
|
- **Nextcloud:** Test am 2026-06-03 erfolgreich, aber mit Unraid-shfs-Eigenheit: Nextcloud fuehrt `chmod()` unter `/var/www/html` aus, was auf FUSE/shfs scheitert. Das Skript patcht `check_data_directory_permissions: false` und legt den `.ncdata`-Marker an.
|
||||||
- **Authelia:** bewusst Config-Smoke ohne produktiven Dump-Restore (Storage-Encryption-Key-Kopplung).
|
- **Authelia:** bewusst Config-Smoke ohne produktiven Dump-Restore (Storage-Encryption-Key-Kopplung).
|
||||||
- **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image.
|
- **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image.
|
||||||
|
- **Home Assistant:** nutzt das neueste HA-native Backup-Artefakt und eine Kopie der Mosquitto-Appdata; Testcontainer laufen nur auf localhost-Ports, ohne Traefik/Public Route.
|
||||||
- **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`.
|
- **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`.
|
||||||
|
|
||||||
## Naechste Ausbaustufen
|
## Naechste Ausbaustufen
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
restoretest-adguard:
|
restoretest-adguard:
|
||||||
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
|
image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
|
||||||
container_name: restoretest-adguard
|
container_name: restoretest-adguard
|
||||||
restart: "no"
|
restart: "no"
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$checks = @(
|
$checks = @(
|
||||||
|
@{ Name = "postgresql17-globals.sql"; Path = Join-Path $DumpRoot "postgresql17-globals.sql" },
|
||||||
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
|
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
|
||||||
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
|
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
|
||||||
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
|
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
|
||||||
@@ -13,6 +14,7 @@ $checks = @(
|
|||||||
@{ Name = "nextcloud.dump"; Path = Join-Path $DumpRoot "nextcloud.dump" },
|
@{ Name = "nextcloud.dump"; Path = Join-Path $DumpRoot "nextcloud.dump" },
|
||||||
@{ Name = "gitea.sqlite.dump"; Path = Join-Path $DumpRoot "gitea.sqlite.dump" },
|
@{ Name = "gitea.sqlite.dump"; Path = Join-Path $DumpRoot "gitea.sqlite.dump" },
|
||||||
@{ Name = "vaultwarden.sqlite.dump"; Path = Join-Path $DumpRoot "vaultwarden.sqlite.dump" },
|
@{ Name = "vaultwarden.sqlite.dump"; Path = Join-Path $DumpRoot "vaultwarden.sqlite.dump" },
|
||||||
|
@{ Name = "n8n.sqlite.dump"; Path = Join-Path $DumpRoot "n8n.sqlite.dump" },
|
||||||
@{ Name = "speedtest-tracker.sqlite.dump"; Path = Join-Path $DumpRoot "speedtest-tracker.sqlite.dump" },
|
@{ Name = "speedtest-tracker.sqlite.dump"; Path = Join-Path $DumpRoot "speedtest-tracker.sqlite.dump" },
|
||||||
@{ Name = "filebrowser.bolt.dump"; Path = Join-Path $DumpRoot "filebrowser.bolt.dump" },
|
@{ Name = "filebrowser.bolt.dump"; Path = Join-Path $DumpRoot "filebrowser.bolt.dump" },
|
||||||
@{ Name = "unraid-flash-config.tar.gz"; Path = Join-Path $DumpRoot "unraid-flash-config.tar.gz" }
|
@{ Name = "unraid-flash-config.tar.gz"; Path = Join-Path $DumpRoot "unraid-flash-config.tar.gz" }
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ check_pg_header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for dump in \
|
for dump in \
|
||||||
|
postgresql17-globals.sql \
|
||||||
postgresql17-paperless.dump \
|
postgresql17-paperless.dump \
|
||||||
postgresql17-mailarchiver.dump \
|
postgresql17-mailarchiver.dump \
|
||||||
mealie.dump \
|
mealie.dump \
|
||||||
@@ -96,6 +97,7 @@ for dump in \
|
|||||||
nextcloud.dump \
|
nextcloud.dump \
|
||||||
gitea.sqlite.dump \
|
gitea.sqlite.dump \
|
||||||
vaultwarden.sqlite.dump \
|
vaultwarden.sqlite.dump \
|
||||||
|
n8n.sqlite.dump \
|
||||||
speedtest-tracker.sqlite.dump \
|
speedtest-tracker.sqlite.dump \
|
||||||
filebrowser.bolt.dump \
|
filebrowser.bolt.dump \
|
||||||
unraid-flash-config.tar.gz; do
|
unraid-flash-config.tar.gz; do
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
restoretest-ha-mosquitto:
|
||||||
|
image: eclipse-mosquitto:2.0.22@sha256:914f529386804c8278a4e581526b9be5e1604df44b30daabc70aa97dcefe5268
|
||||||
|
container_name: restoretest-ha-mosquitto
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
||||||
|
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/config:/mosquitto/external_config:ro
|
||||||
|
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/data:/mosquitto/data
|
||||||
|
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/log:/mosquitto/log
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:11883:1883"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-homeassistant:
|
||||||
|
image: ghcr.io/home-assistant/home-assistant:2026.6.1@sha256:59aa8824955c9db491b75d2eebe42bd68494f80c2ec69ec0d66d9dae37d37514
|
||||||
|
container_name: restoretest-homeassistant
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- restoretest-ha-mosquitto
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
volumes:
|
||||||
|
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/homeassistant/config:/config
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18123:8123"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
+236
@@ -0,0 +1,236 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Home Assistant + Mosquitto Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Scope:
|
||||||
|
# - Restore aus dem neuesten HA-nativen Backup-Artefakt
|
||||||
|
# - Kopie der Mosquitto-Appdata in ein isoliertes Restore-Lab
|
||||||
|
# - Kopie des Fachrepo-Clones zur Lesbarkeits-/Git-Status-Pruefung
|
||||||
|
# - Start isolierter Testcontainer auf localhost-Ports, ohne Traefik/Public Route
|
||||||
|
# - HA HTTP/API-Smoke und MQTT Publish/Subscribe + retained Topic nach Restart
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESTORE_ROOT="/mnt/user/backups/restore-lab/homeassistant"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/homeassistant-$(date +%F).md"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/homeassistant-compose.test.yml"
|
||||||
|
HA_BACKUP_DIR="/mnt/user/appdata/homeassistant/backups"
|
||||||
|
MOSQUITTO_APPDATA="/mnt/user/appdata/mosquitto"
|
||||||
|
MOSQUITTO_REPO_CONF="/mnt/user/services/homelab-infra/smart-home/mosquitto/config/mosquitto.conf"
|
||||||
|
FACHREPO_SOURCE="/mnt/user/services/smart-home-kalli"
|
||||||
|
HA_TOKEN_FILE="/mnt/user/appdata/secrets/ha_token_codex"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Home Assistant restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
HA backup source: newest *.tar under $HA_BACKUP_DIR
|
||||||
|
Mosquitto source: $MOSQUITTO_APPDATA
|
||||||
|
Fachrepo source: $FACHREPO_SOURCE
|
||||||
|
Test endpoints: HA http://127.0.0.1:18123, MQTT 127.0.0.1:11883
|
||||||
|
Scope: HA backup extract + isolated HA boot + API token smoke + MQTT auth/retained smoke
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd tar
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
require_path "$HA_BACKUP_DIR"
|
||||||
|
require_path "$MOSQUITTO_APPDATA/config/passwordfile"
|
||||||
|
require_path "$MOSQUITTO_APPDATA/config/aclfile"
|
||||||
|
require_path "$MOSQUITTO_APPDATA/data"
|
||||||
|
require_path "$MOSQUITTO_REPO_CONF"
|
||||||
|
require_path "$FACHREPO_SOURCE"
|
||||||
|
require_path "$HA_TOKEN_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
RESTORE_ROOT="$RESTORE_ROOT" cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "homeassistant" "$RESTORE_ROOT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
latest_backup="$(find "$HA_BACKUP_DIR" -maxdepth 1 -type f -name '*.tar' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print substr($0, index($0,$2))}')"
|
||||||
|
if [ -z "$latest_backup" ] || [ ! -f "$latest_backup" ]; then
|
||||||
|
echo "No HA native backup tar found under $HA_BACKUP_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
mkdir -p \
|
||||||
|
"$RESTORE_ROOT/ha-backup" \
|
||||||
|
"$RESTORE_ROOT/homeassistant/config" \
|
||||||
|
"$RESTORE_ROOT/mosquitto/config" \
|
||||||
|
"$RESTORE_ROOT/mosquitto/appdata/config" \
|
||||||
|
"$RESTORE_ROOT/mosquitto/appdata/data" \
|
||||||
|
"$RESTORE_ROOT/mosquitto/appdata/log" \
|
||||||
|
"$RESTORE_ROOT/fachrepo"
|
||||||
|
|
||||||
|
tar -xf "$latest_backup" -C "$RESTORE_ROOT/ha-backup"
|
||||||
|
require_path "$RESTORE_ROOT/ha-backup/backup.json"
|
||||||
|
require_path "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz"
|
||||||
|
tar -xzf "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz" -C "$RESTORE_ROOT/homeassistant/config" --strip-components=1 data
|
||||||
|
|
||||||
|
cp "$MOSQUITTO_REPO_CONF" "$RESTORE_ROOT/mosquitto/config/mosquitto.conf"
|
||||||
|
cp -a "$MOSQUITTO_APPDATA/config/." "$RESTORE_ROOT/mosquitto/appdata/config/"
|
||||||
|
cp -a "$MOSQUITTO_APPDATA/data/." "$RESTORE_ROOT/mosquitto/appdata/data/"
|
||||||
|
if [ -d "$MOSQUITTO_APPDATA/log" ]; then
|
||||||
|
cp -a "$MOSQUITTO_APPDATA/log/." "$RESTORE_ROOT/mosquitto/appdata/log/" || true
|
||||||
|
fi
|
||||||
|
cp -a "$FACHREPO_SOURCE/." "$RESTORE_ROOT/fachrepo/"
|
||||||
|
|
||||||
|
ha_config="$RESTORE_ROOT/homeassistant/config"
|
||||||
|
require_path "$ha_config/configuration.yaml"
|
||||||
|
require_path "$ha_config/secrets.yaml"
|
||||||
|
require_path "$ha_config/trusted_proxies.yaml"
|
||||||
|
require_path "$ha_config/.storage/onboarding"
|
||||||
|
require_path "$ha_config/.storage/auth"
|
||||||
|
|
||||||
|
fachrepo_head="$(git -C "$RESTORE_ROOT/fachrepo" log -1 --oneline)"
|
||||||
|
fachrepo_status="$(git -C "$RESTORE_ROOT/fachrepo" status --short)"
|
||||||
|
if [ -n "$fachrepo_status" ]; then
|
||||||
|
echo "Restored fachrepo clone is not clean:" >&2
|
||||||
|
echo "$fachrepo_status" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_size="$(stat -c '%s' "$latest_backup")"
|
||||||
|
ha_file_count="$(find "$ha_config" -type f | wc -l | tr -d ' ')"
|
||||||
|
ha_bytes="$(du -sb "$ha_config" | awk '{print $1}')"
|
||||||
|
mosquitto_data_bytes="$(du -sb "$RESTORE_ROOT/mosquitto/appdata" | awk '{print $1}')"
|
||||||
|
|
||||||
|
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
||||||
|
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" up -d >/dev/null
|
||||||
|
|
||||||
|
mqtt_user="$(sed -n 's/^mqtt_username:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")"
|
||||||
|
mqtt_pass="$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")"
|
||||||
|
if [ -z "$mqtt_user" ] || [ -z "$mqtt_pass" ]; then
|
||||||
|
echo "Missing mqtt_username or mqtt_password in restored HA secrets.yaml" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mqtt_topic="restoretest/homeassistant/smoke"
|
||||||
|
mqtt_payload="ok-$(date +%s)"
|
||||||
|
mqtt_out="$RESTORE_ROOT/mqtt-sub.out"
|
||||||
|
rm -f "$mqtt_out"
|
||||||
|
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" \
|
||||||
|
restoretest-ha-mosquitto sh -lc \
|
||||||
|
'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' \
|
||||||
|
> "$mqtt_out" &
|
||||||
|
sub_pid=$!
|
||||||
|
sleep 1
|
||||||
|
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" -e MQTT_PAYLOAD="$mqtt_payload" \
|
||||||
|
restoretest-ha-mosquitto sh -lc \
|
||||||
|
'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD"'
|
||||||
|
wait "$sub_pid"
|
||||||
|
mqtt_result="$(cat "$mqtt_out")"
|
||||||
|
if [ "$mqtt_result" != "$mqtt_payload" ]; then
|
||||||
|
echo "MQTT publish/subscribe smoke failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
retained_topic="restoretest/homeassistant/retained"
|
||||||
|
retained_payload="retained-$(date +%s)"
|
||||||
|
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" -e MQTT_PAYLOAD="$retained_payload" \
|
||||||
|
restoretest-ha-mosquitto sh -lc \
|
||||||
|
'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD" -r'
|
||||||
|
docker restart restoretest-ha-mosquitto >/dev/null
|
||||||
|
sleep 3
|
||||||
|
retained_result="$(docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" \
|
||||||
|
restoretest-ha-mosquitto sh -lc \
|
||||||
|
'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' | tr -d '\r')"
|
||||||
|
if [ "$retained_result" != "$retained_payload" ]; then
|
||||||
|
echo "MQTT retained smoke failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ha_http_status=""
|
||||||
|
ha_body="$RESTORE_ROOT/ha-http-body.html"
|
||||||
|
for _ in $(seq 1 180); do
|
||||||
|
ha_http_status="$(curl -sS -o "$ha_body" -w '%{http_code}' http://127.0.0.1:18123/ || true)"
|
||||||
|
if [ "$ha_http_status" = "200" ] && grep -qi "Home Assistant" "$ha_body"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$ha_http_status" != "200" ] || ! grep -qi "Home Assistant" "$ha_body"; then
|
||||||
|
echo "HA HTTP smoke failed, status=$ha_http_status" >&2
|
||||||
|
docker logs --tail 120 restoretest-homeassistant >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ha_api_status="$(curl -sS -o "$RESTORE_ROOT/ha-api.json" -w '%{http_code}' \
|
||||||
|
-H "Authorization: Bearer $(cat "$HA_TOKEN_FILE")" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
http://127.0.0.1:18123/api/ || true)"
|
||||||
|
if [ "$ha_api_status" != "200" ]; then
|
||||||
|
echo "HA API token smoke failed, status=$ha_api_status" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" exec -T restoretest-homeassistant \
|
||||||
|
python -m homeassistant --script check_config --config /config >/tmp/restoretest-ha-check-config.out
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Home Assistant Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`homeassistant\` + \`smarthome-mosquitto\`
|
||||||
|
- HA backup source: \`$latest_backup\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers:
|
||||||
|
- \`restoretest-homeassistant\`
|
||||||
|
- \`restoretest-ha-mosquitto\`
|
||||||
|
- Test endpoints:
|
||||||
|
- HA: \`http://127.0.0.1:18123\`
|
||||||
|
- MQTT: \`127.0.0.1:11883\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- HA-native backup tar readable: \`ok\`
|
||||||
|
- HA inner archive restored: \`ok\`
|
||||||
|
- HA backup size bytes: \`$backup_size\`
|
||||||
|
- Restored HA file count: \`$ha_file_count\`
|
||||||
|
- Restored HA bytes: \`$ha_bytes\`
|
||||||
|
- Restored Mosquitto appdata bytes: \`$mosquitto_data_bytes\`
|
||||||
|
- Fachrepo clone clean: \`ok\`
|
||||||
|
- Fachrepo HEAD: \`$fachrepo_head\`
|
||||||
|
- HA HTTP status: \`$ha_http_status\`
|
||||||
|
- HA API token smoke: \`$ha_api_status\`
|
||||||
|
- HA check_config: \`ok\`
|
||||||
|
- MQTT publish/subscribe with restored credentials: \`ok\`
|
||||||
|
- MQTT retained topic after broker restart: \`ok\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Productive \`homeassistant\` and \`smarthome-mosquitto\` containers were not used.
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Test ports were bound to localhost only.
|
||||||
|
- Token and MQTT password values were used for smoke tests but not printed.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Home Assistant restore test ok -> $REPORT_FILE"
|
||||||
@@ -55,6 +55,12 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
exec "$SCRIPT_DIR/redis-restore-test.sh"
|
exec "$SCRIPT_DIR/redis-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
|
homeassistant)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/homeassistant-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/homeassistant-restore-test.sh"
|
||||||
|
;;
|
||||||
nextcloud)
|
nextcloud)
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
|
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
|
||||||
@@ -98,7 +104,7 @@ case "$MODE" in
|
|||||||
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
|
echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
scrutiny:
|
scrutiny:
|
||||||
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:228483f16a6236d2fa9b2fbfca2e76dc861e648fbc6ae6e680d23e5d00211a5d
|
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:bf55bae54eb329d96261183ce4174a4000b6350a7a71dca704d3fef215d98199
|
||||||
container_name: scrutiny
|
container_name: scrutiny
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
speedtest-tracker:
|
speedtest-tracker:
|
||||||
image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:c3750c40948a9360000ce62d694da92e85584b4ab6d3d9a9d1432d76fa5e0726
|
image: lscr.io/linuxserver/speedtest-tracker:1.14.4@sha256:f99dfd097709016dfb4387d65bfdc0419bde99cf1dce7e26e70ca616c86f1281
|
||||||
container_name: speedtest-tracker
|
container_name: speedtest-tracker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
+2
-1
@@ -39,10 +39,11 @@
|
|||||||
"labels": ["dependencies", "minor-patch"]
|
"labels": ["dependencies", "minor-patch"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Kritische Kerninfra (Traefik=Public-Entrypoint, Unbound=DNS, n8n, Nextcloud): nicht im Sammel-PR, eigene einzeln reviewbare PRs, kein Auto-Merge",
|
"description": "Kritische Kerninfra (Traefik=Public-Entrypoint, AdGuard/Unbound=DNS, n8n, Nextcloud): nicht im Sammel-PR, eigene einzeln reviewbare PRs, kein Auto-Merge",
|
||||||
"matchManagers": ["docker-compose", "dockerfile"],
|
"matchManagers": ["docker-compose", "dockerfile"],
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"traefik",
|
"traefik",
|
||||||
|
"adguard/adguardhome",
|
||||||
"shaanmajid/unbound",
|
"shaanmajid/unbound",
|
||||||
"docker.n8n.io/n8nio/n8n",
|
"docker.n8n.io/n8nio/n8n",
|
||||||
"nextcloud"
|
"nextcloud"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ parse_compose() {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
function emit() {
|
function emit() {
|
||||||
if (service && image) {
|
if (service && image && !has_profile) {
|
||||||
print clean(container) "\t" clean(image)
|
print clean(container) "\t" clean(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ parse_compose() {
|
|||||||
sub(/:$/, "", service)
|
sub(/:$/, "", service)
|
||||||
image=""
|
image=""
|
||||||
container=service
|
container=service
|
||||||
|
has_profile=0
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
service && /^ image:/ {
|
service && /^ image:/ {
|
||||||
@@ -52,6 +53,10 @@ parse_compose() {
|
|||||||
sub(/^[[:space:]]*container_name:[[:space:]]*/, "", container)
|
sub(/^[[:space:]]*container_name:[[:space:]]*/, "", container)
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
service && /^ profiles:/ {
|
||||||
|
has_profile=1
|
||||||
|
next
|
||||||
|
}
|
||||||
END { emit() }
|
END { emit() }
|
||||||
' "$compose"
|
' "$compose"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}"
|
|||||||
IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}"
|
IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}"
|
||||||
IMAGE_AGE_ALLOW_FILE="${IMAGE_AGE_ALLOW_FILE:-/mnt/user/services/homelab-infra/services/posture-check/image-age-allow.patterns}"
|
IMAGE_AGE_ALLOW_FILE="${IMAGE_AGE_ALLOW_FILE:-/mnt/user/services/homelab-infra/services/posture-check/image-age-allow.patterns}"
|
||||||
LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}"
|
LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}"
|
||||||
|
LOG_VOLUME_OBSERVE_THRESHOLD="${LOG_VOLUME_OBSERVE_THRESHOLD:-100000}"
|
||||||
DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}"
|
DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}"
|
||||||
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
|
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
|
||||||
BACKUP_DRIFT_FACTOR="${BACKUP_DRIFT_FACTOR:-2.0}"
|
BACKUP_DRIFT_FACTOR="${BACKUP_DRIFT_FACTOR:-2.0}"
|
||||||
@@ -23,6 +24,8 @@ MAIL_SCRIPT="${MAIL_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-ch
|
|||||||
SEND_NTFY="${SEND_NTFY:-0}"
|
SEND_NTFY="${SEND_NTFY:-0}"
|
||||||
NTFY_TOPIC="${NTFY_TOPIC:-homelab-info}"
|
NTFY_TOPIC="${NTFY_TOPIC:-homelab-info}"
|
||||||
NTFY_SCRIPT="${NTFY_SCRIPT:-/mnt/user/services/homelab-infra/ops/restore-tests/send-ntfy.sh}"
|
NTFY_SCRIPT="${NTFY_SCRIPT:-/mnt/user/services/homelab-infra/ops/restore-tests/send-ntfy.sh}"
|
||||||
|
INCLUDE_WEATHER_REPORT="${INCLUDE_WEATHER_REPORT:-0}"
|
||||||
|
WEATHER_REPORT_SCRIPT="${WEATHER_REPORT_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-check/weather-day-report.sh}"
|
||||||
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||||
PROMETHEUS_CONTAINER="${PROMETHEUS_CONTAINER:-monitoring-prometheus}"
|
PROMETHEUS_CONTAINER="${PROMETHEUS_CONTAINER:-monitoring-prometheus}"
|
||||||
TRAEFIK_ACME_PATH="${TRAEFIK_ACME_PATH:-/mnt/user/appdata/traefik/letsencrypt/acme.json}"
|
TRAEFIK_ACME_PATH="${TRAEFIK_ACME_PATH:-/mnt/user/appdata/traefik/letsencrypt/acme.json}"
|
||||||
@@ -217,6 +220,103 @@ derive_report_status() {
|
|||||||
set_summary "report_status" "$REPORT_STATUS"
|
set_summary "report_status" "$REPORT_STATUS"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collect_weather_report() {
|
||||||
|
[ "$INCLUDE_WEATHER_REPORT" = "1" ] || return 0
|
||||||
|
|
||||||
|
append "## Wetterbericht"
|
||||||
|
append ""
|
||||||
|
|
||||||
|
if [ ! -f "$WEATHER_REPORT_SCRIPT" ]; then
|
||||||
|
append "- Wetterbericht nicht erzeugt: Script fehlt unter \`$WEATHER_REPORT_SCRIPT\`."
|
||||||
|
append ""
|
||||||
|
record_section_error "weather" "Script $WEATHER_REPORT_SCRIPT fehlt"
|
||||||
|
set_summary "weather_report_status" "missing-script"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local weather_out
|
||||||
|
if weather_out="$(bash "$WEATHER_REPORT_SCRIPT" --heading-level 3 2>&1)"; then
|
||||||
|
printf '%s\n\n' "$weather_out" >> "$BODY_PATH"
|
||||||
|
set_summary "weather_report_status" "ok"
|
||||||
|
else
|
||||||
|
append "- Wetterbericht nicht erzeugt:"
|
||||||
|
append ""
|
||||||
|
append '```text'
|
||||||
|
printf '%s\n' "$weather_out" >> "$BODY_PATH"
|
||||||
|
append '```'
|
||||||
|
append ""
|
||||||
|
record_section_error "weather" "$(printf '%s' "$weather_out" | head -n 1 | shorten)"
|
||||||
|
set_summary "weather_report_status" "failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status_reasons() {
|
||||||
|
local count=0
|
||||||
|
|
||||||
|
add_reason() {
|
||||||
|
printf '%s\n' "- $1"
|
||||||
|
count=$((count + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "${borg_status:-unknown}" != "completed" ] && add_reason "Borg Backup ist \`${borg_status:-unknown}\` statt \`completed\`."
|
||||||
|
[ "${prometheus_alerts:-0}" = "unknown" ] && add_reason "Prometheus Alerts konnten nicht sicher gelesen werden."
|
||||||
|
[ "${cert_warnings:-0}" != "0" ] && add_reason "Zertifikatswarnungen: \`${cert_warnings:-0}\`."
|
||||||
|
[ "${disk_warnings:-0}" != "0" ] && add_reason "Storage-Warnungen: \`${disk_warnings:-0}\`."
|
||||||
|
if [ "${image_warnings:-0}" != "0" ]; then
|
||||||
|
if [ -n "${image_warning_names:-}" ]; then
|
||||||
|
add_reason "Image-Warnungen: \`${image_warnings:-0}\` (${image_warning_names})."
|
||||||
|
else
|
||||||
|
add_reason "Image-Warnungen: \`${image_warnings:-0}\`."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ "${containers_exited_nonzero:-0}" != "0" ] && add_reason "Container exited non-zero: \`${containers_exited_nonzero:-0}\`."
|
||||||
|
[ "${host_recent_boot:-0}" = "1" ] && add_reason "Host-Reboot innerhalb der letzten 24 Stunden."
|
||||||
|
[ "${backup_duration_drift:-0}" = "1" ] && add_reason "Backup-Dauer-Drift erkannt."
|
||||||
|
[ "${noise_threshold_exceeded:-0}" != "0" ] && add_reason "Noise-Pattern ueber Eskalations-Schwelle: \`${noise_threshold_exceeded:-0}\`."
|
||||||
|
|
||||||
|
if [ "${prometheus_alerts_pending:-0}" != "0" ] && [ "${prometheus_alerts_pending:-0}" != "unknown" ]; then
|
||||||
|
add_reason "Prometheus pending Alerts: \`${prometheus_alerts_pending:-0}\`."
|
||||||
|
fi
|
||||||
|
if [ "${prometheus_alerts_firing:-0}" != "0" ] && [ "${prometheus_alerts_firing:-0}" != "unknown" ]; then
|
||||||
|
add_reason "Prometheus firing Alerts: \`${prometheus_alerts_firing:-0}\`."
|
||||||
|
fi
|
||||||
|
[ "${containers_unhealthy:-0}" != "0" ] && add_reason "Unhealthy Container: \`${containers_unhealthy:-0}\`."
|
||||||
|
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
printf '%s\n' "- Keine direkten Ampel-Ausloeser im Summary-Set gefunden."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_notable_observations() {
|
||||||
|
local count=0
|
||||||
|
|
||||||
|
add_observation() {
|
||||||
|
printf '%s\n' "- $1"
|
||||||
|
count=$((count + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${traefik_5xx:-0}" != "0" ] && [ "${traefik_5xx:-0}" != "unknown" ]; then
|
||||||
|
if [ -n "${traefik_5xx_top:-}" ] && [ "${traefik_5xx_top:-none}" != "none" ]; then
|
||||||
|
add_observation "Traefik 5xx: \`${traefik_5xx:-0}\` (Top-Gruppe: \`${traefik_5xx_top}\`)."
|
||||||
|
else
|
||||||
|
add_observation "Traefik 5xx: \`${traefik_5xx:-0}\`."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "${log_highlights:-0}" != "0" ] && [ "${log_highlights:-0}" != "unknown" ]; then
|
||||||
|
add_observation "Log-Highlights: \`${log_highlights:-0}\` handlungsrelevante Treffer; Beispiele stehen in der Log-Auswertung."
|
||||||
|
fi
|
||||||
|
if printf '%s' "${log_volume_total:-0}" | grep -Eq '^[0-9]+$' && [ "${log_volume_total:-0}" -ge "$LOG_VOLUME_OBSERVE_THRESHOLD" ]; then
|
||||||
|
add_observation "Log-Volumen: \`${log_volume_total:-0}\` Zeilen im Zeitraum; Top-Verursacher stehen im Log-Volumen-Abschnitt."
|
||||||
|
fi
|
||||||
|
if [ "${docker_events:-0}" != "0" ] && [ "${docker_events:-0}" != "unknown" ]; then
|
||||||
|
add_observation "Docker Critical Events: \`${docker_events:-0}\`."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
printf '%s\n' "- Keine zusaetzlichen auffaelligen Beobachtungen im Management-Summary."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
collect_borg() {
|
collect_borg() {
|
||||||
append "## Borg Backup"
|
append "## Borg Backup"
|
||||||
append ""
|
append ""
|
||||||
@@ -584,6 +684,7 @@ collect_image_freshness() {
|
|||||||
local image_file="$TMP_DIR/images.tsv"
|
local image_file="$TMP_DIR/images.tsv"
|
||||||
local image_warnings=0
|
local image_warnings=0
|
||||||
local image_allowed=0
|
local image_allowed=0
|
||||||
|
local image_warning_names=""
|
||||||
local now_epoch
|
local now_epoch
|
||||||
: > "$image_file"
|
: > "$image_file"
|
||||||
now_epoch="$(date +%s)"
|
now_epoch="$(date +%s)"
|
||||||
@@ -630,6 +731,7 @@ collect_image_freshness() {
|
|||||||
else
|
else
|
||||||
note="ueberaltert"
|
note="ueberaltert"
|
||||||
image_warnings=$((image_warnings + 1))
|
image_warnings=$((image_warnings + 1))
|
||||||
|
image_warning_names="${image_warning_names:+$image_warning_names,}$name:${age_days}d"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
printf '%d\t%s\t%s\t%s\n' "$age_days" "$name" "$image_tag" "$note" >> "$image_file"
|
printf '%d\t%s\t%s\t%s\n' "$age_days" "$name" "$image_tag" "$note" >> "$image_file"
|
||||||
@@ -637,6 +739,7 @@ collect_image_freshness() {
|
|||||||
|
|
||||||
set_summary "image_warnings" "$image_warnings"
|
set_summary "image_warnings" "$image_warnings"
|
||||||
set_summary "image_allowed" "$image_allowed"
|
set_summary "image_allowed" "$image_allowed"
|
||||||
|
set_summary "image_warning_names" "$image_warning_names"
|
||||||
|
|
||||||
if [ ! -s "$image_file" ]; then
|
if [ ! -s "$image_file" ]; then
|
||||||
append "- Keine Image-Daten verfuegbar."
|
append "- Keine Image-Daten verfuegbar."
|
||||||
@@ -781,8 +884,16 @@ collect_traefik_5xx() {
|
|||||||
set_summary "traefik_5xx" "$count"
|
set_summary "traefik_5xx" "$count"
|
||||||
|
|
||||||
if [ "$count" -eq 0 ]; then
|
if [ "$count" -eq 0 ]; then
|
||||||
|
set_summary "traefik_5xx_top" "none"
|
||||||
append "- Keine 5xx-Antworten."
|
append "- Keine 5xx-Antworten."
|
||||||
else
|
else
|
||||||
|
local top_group
|
||||||
|
top_group="$(awk '{ code=$9; service=$12; gsub(/"/, "", service); counts[service " " code]++ } END { for (k in counts) print counts[k], k }' "$file" \
|
||||||
|
| sort -nr \
|
||||||
|
| head -n 1 \
|
||||||
|
| awk '{ print $2 ":" $3 ":" $1 }' \
|
||||||
|
| sed -E 's#[^A-Za-z0-9_.:@/-]+#_#g')"
|
||||||
|
set_summary "traefik_5xx_top" "${top_group:-none}"
|
||||||
append "- 5xx-Antworten: $count"
|
append "- 5xx-Antworten: $count"
|
||||||
append ""
|
append ""
|
||||||
append "### Gruppiert nach Service/Code"
|
append "### Gruppiert nach Service/Code"
|
||||||
@@ -1181,10 +1292,20 @@ write_report() {
|
|||||||
if [ "$REPORT_STATUS" = "OK" ]; then
|
if [ "$REPORT_STATUS" = "OK" ]; then
|
||||||
printf 'Im betrachteten Zeitraum zeigt das Homelab eine stabile Betriebslage. Das letzte Borg-Backup ist erfolgreich abgeschlossen, Prometheus meldet keine firing Alerts, keine unhealthy Container, Zertifikate und Storage im erwarteten Bereich.\n\n'
|
printf 'Im betrachteten Zeitraum zeigt das Homelab eine stabile Betriebslage. Das letzte Borg-Backup ist erfolgreich abgeschlossen, Prometheus meldet keine firing Alerts, keine unhealthy Container, Zertifikate und Storage im erwarteten Bereich.\n\n'
|
||||||
elif [ "$REPORT_STATUS" = "WARNUNG" ]; then
|
elif [ "$REPORT_STATUS" = "WARNUNG" ]; then
|
||||||
printf 'Im betrachteten Zeitraum gibt es Punkte, die Aufmerksamkeit verdienen. Der Betrieb ist nicht automatisch als kompromittiert zu bewerten, aber mindestens ein Signal (Backup, Pending Alert, Zertifikat, Storage, Image-Alter, Drift oder Reboot) weicht vom Normalzustand ab.\n\n'
|
printf 'Im betrachteten Zeitraum gibt es Punkte, die Aufmerksamkeit verdienen. Der Betrieb ist nicht automatisch als kompromittiert zu bewerten; die konkreten Ampel-Ausloeser stehen direkt darunter.\n\n'
|
||||||
else
|
else
|
||||||
printf 'Im betrachteten Zeitraum liegt ein kritisches Betriebssignal vor. Der Bericht sollte zeitnah gelesen und die betroffenen Komponenten priorisiert geprueft werden.\n\n'
|
printf 'Im betrachteten Zeitraum liegt ein kritisches Betriebssignal vor. Der Bericht sollte zeitnah gelesen und die betroffenen Komponenten priorisiert geprueft werden.\n\n'
|
||||||
fi
|
fi
|
||||||
|
printf '### Warum dieser Status?\n\n'
|
||||||
|
if [ "$REPORT_STATUS" = "OK" ]; then
|
||||||
|
printf '%s\n\n' "- Keine Ampel-Ausloeser im Summary-Set."
|
||||||
|
else
|
||||||
|
print_status_reasons
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
printf '### Weitere auffaellige Beobachtungen\n\n'
|
||||||
|
print_notable_observations
|
||||||
|
printf '\n'
|
||||||
printf '### Management-Bewertung\n\n'
|
printf '### Management-Bewertung\n\n'
|
||||||
printf '%s\n' "- Status: \`$REPORT_STATUS\`"
|
printf '%s\n' "- Status: \`$REPORT_STATUS\`"
|
||||||
printf '%s\n' "- Borg Backup: \`${borg_status:-unknown}\`"
|
printf '%s\n' "- Borg Backup: \`${borg_status:-unknown}\`"
|
||||||
@@ -1278,6 +1399,7 @@ Section errors: ${section_failures:-unknown}"
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
collect_overview
|
collect_overview
|
||||||
|
collect_weather_report
|
||||||
collect_host_health
|
collect_host_health
|
||||||
collect_borg
|
collect_borg
|
||||||
collect_prometheus
|
collect_prometheus
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ set -euo pipefail
|
|||||||
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
|
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
|
||||||
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
|
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
|
||||||
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||||
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}"
|
BORG_EXPECTED_SOURCES_FILE="${BORG_EXPECTED_SOURCES_FILE:-/local/services/homelab-infra/ops/borg-ui/all-important-sources.txt}"
|
||||||
|
# Host-Pfad der aktuellen Dump-Artefakte (pre-backup-dumps.sh schreibt hierhin).
|
||||||
|
# Wird host-seitig gestattet; der Exporter laeuft als Unraid User Script.
|
||||||
|
BORG_DUMP_DIR="${BORG_DUMP_DIR:-/mnt/user/backups/borg/dumps/latest}"
|
||||||
|
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres mail-archiver n8n homeassistant smarthome-mosquitto}"
|
||||||
# Hinweis: Tailscale laeuft als natives Unraid-Plugin (kein Docker-Container) und
|
# Hinweis: Tailscale laeuft als natives Unraid-Plugin (kein Docker-Container) und
|
||||||
# wird daher hier bewusst NICHT als kritischer Container gefuehrt (Stand 2026-06-06).
|
# wird daher hier bewusst NICHT als kritischer Container gefuehrt (Stand 2026-06-06).
|
||||||
|
|
||||||
@@ -90,11 +94,32 @@ EOF
|
|||||||
# TYPE homelab_borg_last_success gauge
|
# TYPE homelab_borg_last_success gauge
|
||||||
# HELP homelab_borg_last_job_warning Whether the most recent Borg backup job completed with warnings.
|
# HELP homelab_borg_last_job_warning Whether the most recent Borg backup job completed with warnings.
|
||||||
# TYPE homelab_borg_last_job_warning gauge
|
# TYPE homelab_borg_last_job_warning gauge
|
||||||
|
# HELP homelab_borg_repository_last_check_timestamp_seconds Unix timestamp of the latest Borg repository check known to Borg UI.
|
||||||
|
# TYPE homelab_borg_repository_last_check_timestamp_seconds gauge
|
||||||
|
# HELP homelab_borg_scope_expected_file_present Whether the expected Borg source list file is visible inside Borg UI.
|
||||||
|
# TYPE homelab_borg_scope_expected_file_present gauge
|
||||||
|
# HELP homelab_borg_scope_expected_sources_total Number of expected Borg source paths from the repo source list.
|
||||||
|
# TYPE homelab_borg_scope_expected_sources_total gauge
|
||||||
|
# HELP homelab_borg_scope_configured_sources_total Number of Borg source paths configured in Borg UI.
|
||||||
|
# TYPE homelab_borg_scope_configured_sources_total gauge
|
||||||
|
# HELP homelab_borg_scope_missing_sources_total Number of expected Borg source paths missing from Borg UI.
|
||||||
|
# TYPE homelab_borg_scope_missing_sources_total gauge
|
||||||
|
# HELP homelab_borg_scope_extra_sources_total Number of Borg UI source paths not present in the repo source list.
|
||||||
|
# TYPE homelab_borg_scope_extra_sources_total gauge
|
||||||
|
# HELP homelab_borg_scope_source_configured Whether an expected Borg source path is configured in Borg UI.
|
||||||
|
# TYPE homelab_borg_scope_source_configured gauge
|
||||||
|
# HELP homelab_borg_schedule_prune_after_enabled Whether a Borg scheduled job runs prune after backup.
|
||||||
|
# TYPE homelab_borg_schedule_prune_after_enabled gauge
|
||||||
|
# HELP homelab_borg_schedule_compact_after_enabled Whether a Borg scheduled job runs compact after backup.
|
||||||
|
# TYPE homelab_borg_schedule_compact_after_enabled gauge
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if docker inspect "$BORG_CONTAINER" >/dev/null 2>&1; then
|
if docker inspect "$BORG_CONTAINER" >/dev/null 2>&1; then
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i -e BORG_EXPECTED_SOURCES_FILE="$BORG_EXPECTED_SOURCES_FILE" "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
conn = sqlite3.connect("/data/borg.db")
|
conn = sqlite3.connect("/data/borg.db")
|
||||||
@@ -135,6 +160,9 @@ def parse_ts(value):
|
|||||||
def escape_label(value):
|
def escape_label(value):
|
||||||
return (value or "").replace("\\", "\\\\").replace('"', '\\"')
|
return (value or "").replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
|
||||||
|
def bool_metric(value):
|
||||||
|
return 1 if value else 0
|
||||||
|
|
||||||
latest_status = latest["status"] if latest else "missing"
|
latest_status = latest["status"] if latest else "missing"
|
||||||
latest_success = 1 if latest_status in ("completed", "completed_with_warnings") else 0
|
latest_success = 1 if latest_status in ("completed", "completed_with_warnings") else 0
|
||||||
latest_warning = 1 if latest_status == "completed_with_warnings" else 0
|
latest_warning = 1 if latest_status == "completed_with_warnings" else 0
|
||||||
@@ -145,12 +173,107 @@ completed_archive = escape_label(completed["archive_name"] if completed else "")
|
|||||||
print(f'homelab_borg_last_success{{status="{latest_status}",archive="{latest_archive}"}} {latest_success}')
|
print(f'homelab_borg_last_success{{status="{latest_status}",archive="{latest_archive}"}} {latest_success}')
|
||||||
print(f'homelab_borg_last_job_warning{{status="{latest_status}",archive="{latest_archive}"}} {latest_warning}')
|
print(f'homelab_borg_last_job_warning{{status="{latest_status}",archive="{latest_archive}"}} {latest_warning}')
|
||||||
print(f'homelab_borg_last_completed_timestamp_seconds{{archive="{completed_archive}"}} {completed_ts}')
|
print(f'homelab_borg_last_completed_timestamp_seconds{{archive="{completed_archive}"}} {completed_ts}')
|
||||||
|
|
||||||
|
repo = cur.execute("""
|
||||||
|
select id, name, source_directories, last_check
|
||||||
|
from repositories
|
||||||
|
order by id
|
||||||
|
limit 1
|
||||||
|
""").fetchone()
|
||||||
|
|
||||||
|
if repo:
|
||||||
|
repo_name = escape_label(repo["name"] or str(repo["id"]))
|
||||||
|
print(f'homelab_borg_repository_last_check_timestamp_seconds{{repository="{repo_name}"}} {parse_ts(repo["last_check"])}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
configured_sources = json.loads(repo["source_directories"] or "[]")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
configured_sources = []
|
||||||
|
else:
|
||||||
|
configured_sources = []
|
||||||
|
|
||||||
|
expected_path = Path(os.environ.get("BORG_EXPECTED_SOURCES_FILE", ""))
|
||||||
|
expected_file_present = expected_path.is_file()
|
||||||
|
if expected_file_present:
|
||||||
|
expected_sources = [
|
||||||
|
line.strip()
|
||||||
|
for line in expected_path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
expected_sources = []
|
||||||
|
|
||||||
|
configured_set = set(configured_sources)
|
||||||
|
expected_set = set(expected_sources)
|
||||||
|
missing_sources = [source for source in expected_sources if source not in configured_set]
|
||||||
|
extra_sources = [source for source in configured_sources if source not in expected_set]
|
||||||
|
|
||||||
|
print(f"homelab_borg_scope_expected_file_present {bool_metric(expected_file_present)}")
|
||||||
|
print(f"homelab_borg_scope_expected_sources_total {len(expected_sources)}")
|
||||||
|
print(f"homelab_borg_scope_configured_sources_total {len(configured_sources)}")
|
||||||
|
print(f"homelab_borg_scope_missing_sources_total {len(missing_sources)}")
|
||||||
|
print(f"homelab_borg_scope_extra_sources_total {len(extra_sources)}")
|
||||||
|
|
||||||
|
for source in expected_sources:
|
||||||
|
value = 1 if source in configured_set else 0
|
||||||
|
print(f'homelab_borg_scope_source_configured{{source="{escape_label(source)}"}} {value}')
|
||||||
|
|
||||||
|
for source in extra_sources:
|
||||||
|
print(f'homelab_borg_scope_source_configured{{source="{escape_label(source)}",state="extra"}} 0')
|
||||||
|
|
||||||
|
for schedule in cur.execute("""
|
||||||
|
select id, name, run_prune_after, run_compact_after
|
||||||
|
from scheduled_jobs
|
||||||
|
where enabled = 1
|
||||||
|
order by id
|
||||||
|
"""):
|
||||||
|
schedule_name = escape_label(schedule["name"] or str(schedule["id"]))
|
||||||
|
print(f'homelab_borg_schedule_prune_after_enabled{{schedule="{schedule_name}"}} {bool_metric(schedule["run_prune_after"])}')
|
||||||
|
print(f'homelab_borg_schedule_compact_after_enabled{{schedule="{schedule_name}"}} {bool_metric(schedule["run_compact_after"])}')
|
||||||
PY
|
PY
|
||||||
else
|
else
|
||||||
printf 'homelab_borg_last_success{status="container_missing",archive=""} 0\n'
|
printf 'homelab_borg_last_success{status="container_missing",archive=""} 0\n'
|
||||||
printf 'homelab_borg_last_job_warning{status="container_missing",archive=""} 0\n'
|
printf 'homelab_borg_last_job_warning{status="container_missing",archive=""} 0\n'
|
||||||
printf 'homelab_borg_last_completed_timestamp_seconds{archive=""} 0\n'
|
printf 'homelab_borg_last_completed_timestamp_seconds{archive=""} 0\n'
|
||||||
|
printf 'homelab_borg_repository_last_check_timestamp_seconds{repository=""} 0\n'
|
||||||
|
printf 'homelab_borg_scope_expected_file_present 0\n'
|
||||||
|
printf 'homelab_borg_scope_expected_sources_total 0\n'
|
||||||
|
printf 'homelab_borg_scope_configured_sources_total 0\n'
|
||||||
|
printf 'homelab_borg_scope_missing_sources_total 0\n'
|
||||||
|
printf 'homelab_borg_scope_extra_sources_total 0\n'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Dump-Frische host-seitig messen. Schliesst den Blindfleck, dass Borg
|
||||||
|
# weiterlaeuft und stale Dumps archiviert, ohne dass ein Job-Fehler entsteht
|
||||||
|
# (pre-backup-dumps.sh gestoppt). Laeuft ausserhalb des borg-ui-Containers,
|
||||||
|
# weil die Dumps host-seitig unter $BORG_DUMP_DIR liegen.
|
||||||
|
cat <<'EOF'
|
||||||
|
# HELP homelab_borg_dump_present Whether an expected Borg pre-backup dump artifact exists in the latest dump set.
|
||||||
|
# TYPE homelab_borg_dump_present gauge
|
||||||
|
# HELP homelab_borg_dump_age_seconds Age in seconds of an expected Borg pre-backup dump artifact.
|
||||||
|
# TYPE homelab_borg_dump_age_seconds gauge
|
||||||
|
EOF
|
||||||
|
for dump in \
|
||||||
|
postgresql17-globals.sql \
|
||||||
|
postgresql17-mailarchiver.dump \
|
||||||
|
postgresql17-paperless.dump \
|
||||||
|
mealie.dump \
|
||||||
|
immich.dump \
|
||||||
|
nextcloud.dump \
|
||||||
|
gitea.sqlite.dump \
|
||||||
|
vaultwarden.sqlite.dump \
|
||||||
|
n8n.sqlite.dump \
|
||||||
|
unraid-flash-config.tar.gz \
|
||||||
|
komodo-mongo.archive.gz; do
|
||||||
|
dump_path="$BORG_DUMP_DIR/$dump"
|
||||||
|
if [ -f "$dump_path" ]; then
|
||||||
|
dump_mtime="$(stat -c %Y "$dump_path" 2>/dev/null || echo 0)"
|
||||||
|
printf 'homelab_borg_dump_present{dump="%s"} 1\n' "$dump"
|
||||||
|
printf 'homelab_borg_dump_age_seconds{dump="%s"} %s\n' "$dump" "$(( now - dump_mtime ))"
|
||||||
|
else
|
||||||
|
printf 'homelab_borg_dump_present{dump="%s"} 0\n' "$dump"
|
||||||
|
fi
|
||||||
|
done
|
||||||
} > "$tmp"
|
} > "$tmp"
|
||||||
|
|
||||||
# 0644 statt mktemp-default 0600, damit der node-exporter-Textfile-Collector
|
# 0644 statt mktemp-default 0600, damit der node-exporter-Textfile-Collector
|
||||||
|
|||||||
@@ -28,3 +28,9 @@ immich_postgres 2026-09-10
|
|||||||
# (Dez 2025). Das Image-Alter ist nur Build-Alter, keine veraltete Version.
|
# (Dez 2025). Das Image-Alter ist nur Build-Alter, keine veraltete Version.
|
||||||
# Re-check: ob eine blackbox_exporter-Version > v0.28.0 erschienen ist.
|
# Re-check: ob eine blackbox_exporter-Version > v0.28.0 erschienen ist.
|
||||||
monitoring-blackbox-exporter 2026-09-10
|
monitoring-blackbox-exporter 2026-09-10
|
||||||
|
|
||||||
|
# glance-docker-socket-proxy: v0.4.2 ist am 2026-06-17 weiterhin der neueste
|
||||||
|
# stabile Tag / latest. Neuere Tags sind nur master/nightly und werden fuer den
|
||||||
|
# lesenden Glance-Socket-Proxy bewusst nicht produktiv eingesetzt.
|
||||||
|
# Re-check: ob ein stabiler Tag > v0.4.2 erschienen ist.
|
||||||
|
glance-docker-socket-proxy 2026-09-17
|
||||||
|
|||||||
@@ -87,3 +87,19 @@ adguard.*bad question section.*only 1 question allowed
|
|||||||
# this lookup is harmless and does not affect any dashboard.
|
# this lookup is harmless and does not affect any dashboard.
|
||||||
# Re-check: only if Amazon Prometheus is added as a datasource.
|
# Re-check: only if Amazon Prometheus is added as a datasource.
|
||||||
monitoring-grafana.*grafana-amazonprometheus-datasource not found
|
monitoring-grafana.*grafana-amazonprometheus-datasource not found
|
||||||
|
|
||||||
|
# cAdvisor stale container filesystem stats on Unraid.
|
||||||
|
# Why: cAdvisor can keep reporting an already removed Docker container path in
|
||||||
|
# fsHandler even though the container and path no longer exist. This is a
|
||||||
|
# collector bookkeeping issue, not a failed workload or missing data path.
|
||||||
|
# Re-check: if the message references an existing/running container, if
|
||||||
|
# Prometheus target health fails, or if broader cAdvisor errors appear.
|
||||||
|
monitoring-cadvisor.*failed to collect filesystem stats.*var/lib/docker/containers/[0-9a-f]{64}
|
||||||
|
|
||||||
|
# cAdvisor startup lines that match the generic "oom" / "failed" grep.
|
||||||
|
# Why: "oom_event" is a metric name printed during startup, and Unraid loop
|
||||||
|
# devices can disappear while cAdvisor enumerates block devices.
|
||||||
|
# Re-check: if cAdvisor target health fails or these messages appear outside
|
||||||
|
# container startup together with missing container metrics.
|
||||||
|
monitoring-cadvisor.*enabled metrics:.*oom_event
|
||||||
|
monitoring-cadvisor.*stat failed on /dev/loop[0-9]+ with error: no such file or directory
|
||||||
|
|||||||
@@ -431,24 +431,24 @@ def render_summary_grid(entries):
|
|||||||
status = classify(label, value)
|
status = classify(label, value)
|
||||||
theme = STATUS_THEMES.get(status, STATUS_THEMES["UNKNOWN"])
|
theme = STATUS_THEMES.get(status, STATUS_THEMES["UNKNOWN"])
|
||||||
cards.append(
|
cards.append(
|
||||||
'<td style="padding:6px;width:33.33%;vertical-align:top">'
|
'<td style="padding:6px;width:50%;vertical-align:top">'
|
||||||
f'<div style="background:{theme["card_bg"]};'
|
f'<div style="background:{theme["card_bg"]};'
|
||||||
f'border:1px solid {theme["card_border"]};'
|
f'border:1px solid {theme["card_border"]};'
|
||||||
'border-radius:8px;padding:12px 14px">'
|
'border-radius:8px;padding:11px 12px;min-height:74px">'
|
||||||
f'<div style="font-size:11px;color:#1e293b;'
|
f'<div style="font-size:11px;color:#1e293b;'
|
||||||
'text-transform:uppercase;letter-spacing:0.08em;font-weight:700;'
|
'text-transform:uppercase;letter-spacing:0.04em;font-weight:700;'
|
||||||
f'line-height:1.3;opacity:0.78">{html.escape(label)}</div>'
|
f'line-height:1.35;opacity:0.78;overflow-wrap:anywhere">{html.escape(label)}</div>'
|
||||||
f'<div style="font-size:17px;font-weight:700;'
|
f'<div style="font-size:16px;font-weight:700;'
|
||||||
f'color:{theme["card_text"]};margin-top:5px;line-height:1.25;'
|
f'color:{theme["card_text"]};margin-top:5px;line-height:1.25;'
|
||||||
f'word-break:break-word;font-variant-numeric:tabular-nums">'
|
f'word-break:normal;overflow-wrap:anywhere;font-variant-numeric:tabular-nums">'
|
||||||
f'{html.escape(value)}</div>'
|
f'{html.escape(value)}</div>'
|
||||||
'</div></td>'
|
'</div></td>'
|
||||||
)
|
)
|
||||||
rows_html = []
|
rows_html = []
|
||||||
for chunk_start in range(0, len(cards), 3):
|
for chunk_start in range(0, len(cards), 2):
|
||||||
chunk = cards[chunk_start:chunk_start + 3]
|
chunk = cards[chunk_start:chunk_start + 2]
|
||||||
while len(chunk) < 3:
|
while len(chunk) < 2:
|
||||||
chunk.append('<td style="padding:6px;width:33.33%"></td>')
|
chunk.append('<td style="padding:6px;width:50%"></td>')
|
||||||
rows_html.append("<tr>" + "".join(chunk) + "</tr>")
|
rows_html.append("<tr>" + "".join(chunk) + "</tr>")
|
||||||
return (
|
return (
|
||||||
'<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" '
|
'<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" '
|
||||||
|
|||||||
@@ -76,12 +76,25 @@ printf '%s' 'SMTP_PASSWORT_HIER_EINTRAGEN' > /mnt/user/appdata/secrets/homelab_s
|
|||||||
chmod 600 /mnt/user/appdata/secrets/homelab_smtp_password.txt
|
chmod 600 /mnt/user/appdata/secrets/homelab_smtp_password.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional fuer den Wetterbericht im Tagesreport: Grafana Service Account Token
|
||||||
|
mit Leserechten auf die Datasource in eine Host-Secret-Datei legen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s' 'glsa_REPLACE_WITH_ROTATED_READ_TOKEN' > /mnt/user/appdata/secrets/monitoring_grafana_weather_report_token.txt
|
||||||
|
chmod 600 /mnt/user/appdata/secrets/monitoring_grafana_weather_report_token.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Wetterbericht-Generator nutzt `python3` auf dem Host, falls vorhanden.
|
||||||
|
Ohne Host-`python3` faellt er automatisch auf Docker mit
|
||||||
|
`python:3.13-alpine` zurueck.
|
||||||
|
|
||||||
User Script:
|
User Script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
SEND_MAIL=1 \
|
SEND_MAIL=1 \
|
||||||
MAIL_MODE=always \
|
MAIL_MODE=always \
|
||||||
|
INCLUDE_WEATHER_REPORT=1 \
|
||||||
MAIL_FROM="michideheld@gmx.de" \
|
MAIL_FROM="michideheld@gmx.de" \
|
||||||
MAIL_TO="Mi.Kaleschke@gmx.de" \
|
MAIL_TO="Mi.Kaleschke@gmx.de" \
|
||||||
SMTP_HOST="smtp.gmx.net" \
|
SMTP_HOST="smtp.gmx.net" \
|
||||||
|
|||||||
@@ -0,0 +1,555 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a Markdown weather report for one KalliHome archive day.
|
||||||
|
|
||||||
|
The script queries Grafana's InfluxDB datasource through /api/ds/query. It does
|
||||||
|
not store credentials; provide a Grafana service account token through
|
||||||
|
GRAFANA_SERVICE_ACCOUNT_TOKEN or GRAFANA_TOKEN_FILE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from datetime import date, datetime, time, timedelta, timezone, tzinfo
|
||||||
|
from typing import Any
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_GRAFANA_URL = "https://monitoring.kaleschke.info"
|
||||||
|
DEFAULT_TOKEN_FILE = "/mnt/user/appdata/secrets/monitoring_grafana_weather_report_token.txt"
|
||||||
|
DATASOURCE_UID = "ha-weather-influx"
|
||||||
|
DATASOURCE_TYPE = "influxdb"
|
||||||
|
|
||||||
|
TEMP_TABLE = "\u00b0C"
|
||||||
|
SOLAR_TABLE = "W/m\u00b2"
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherReportError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def last_sunday(year: int, month: int) -> date:
|
||||||
|
current = date(year, month + 1, 1) - timedelta(days=1) if month < 12 else date(year, 12, 31)
|
||||||
|
return current - timedelta(days=(current.weekday() + 1) % 7)
|
||||||
|
|
||||||
|
|
||||||
|
class EuropeBerlinFallback(tzinfo):
|
||||||
|
"""Small Europe/Berlin fallback for systems without IANA tzdata."""
|
||||||
|
|
||||||
|
def _dst_bounds_utc(self, year: int) -> tuple[datetime, datetime]:
|
||||||
|
start = datetime.combine(last_sunday(year, 3), time(1, 0), tzinfo=timezone.utc)
|
||||||
|
end = datetime.combine(last_sunday(year, 10), time(1, 0), tzinfo=timezone.utc)
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
def _is_dst_utc(self, dt: datetime) -> bool:
|
||||||
|
utc_dt = dt.replace(tzinfo=timezone.utc) if dt.tzinfo is self else dt.astimezone(timezone.utc)
|
||||||
|
start, end = self._dst_bounds_utc(utc_dt.year)
|
||||||
|
return start <= utc_dt < end
|
||||||
|
|
||||||
|
def _is_dst_local(self, dt: datetime) -> bool:
|
||||||
|
naive = dt.replace(tzinfo=None)
|
||||||
|
start = datetime.combine(last_sunday(naive.year, 3), time(2, 0))
|
||||||
|
end = datetime.combine(last_sunday(naive.year, 10), time(3, 0))
|
||||||
|
return start <= naive < end
|
||||||
|
|
||||||
|
def utcoffset(self, dt: datetime | None) -> timedelta:
|
||||||
|
if dt is not None and self._is_dst_local(dt):
|
||||||
|
return timedelta(hours=2)
|
||||||
|
return timedelta(hours=1)
|
||||||
|
|
||||||
|
def dst(self, dt: datetime | None) -> timedelta:
|
||||||
|
if dt is not None and self._is_dst_local(dt):
|
||||||
|
return timedelta(hours=1)
|
||||||
|
return timedelta(0)
|
||||||
|
|
||||||
|
def tzname(self, dt: datetime | None) -> str:
|
||||||
|
return "CEST" if dt is not None and self._is_dst_local(dt) else "CET"
|
||||||
|
|
||||||
|
def fromutc(self, dt: datetime) -> datetime:
|
||||||
|
offset = timedelta(hours=2) if self._is_dst_utc(dt) else timedelta(hours=1)
|
||||||
|
return (dt.replace(tzinfo=timezone.utc) + offset).replace(tzinfo=self)
|
||||||
|
|
||||||
|
|
||||||
|
def load_timezone(tz_name: str) -> tzinfo:
|
||||||
|
try:
|
||||||
|
return ZoneInfo(tz_name)
|
||||||
|
except ZoneInfoNotFoundError:
|
||||||
|
if tz_name == "Europe/Berlin":
|
||||||
|
return EuropeBerlinFallback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Generate a KalliHome weather day report.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--date",
|
||||||
|
default=os.environ.get("WEATHER_REPORT_DATE"),
|
||||||
|
help="Local report date as YYYY-MM-DD. Defaults to yesterday in --timezone.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timezone",
|
||||||
|
default=os.environ.get("WEATHER_REPORT_TZ", "Europe/Berlin"),
|
||||||
|
help="IANA timezone for the local day boundary. Default: Europe/Berlin.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--grafana-url",
|
||||||
|
default=os.environ.get("GRAFANA_URL", DEFAULT_GRAFANA_URL),
|
||||||
|
help=f"Grafana base URL. Default: {DEFAULT_GRAFANA_URL}",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--token-file",
|
||||||
|
default=os.environ.get("GRAFANA_TOKEN_FILE", DEFAULT_TOKEN_FILE),
|
||||||
|
help=f"Grafana service account token file. Default: {DEFAULT_TOKEN_FILE}",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--heading-level",
|
||||||
|
type=int,
|
||||||
|
default=int(os.environ.get("WEATHER_REPORT_HEADING_LEVEL", "1")),
|
||||||
|
choices=range(1, 5),
|
||||||
|
help="Markdown heading level for the title. Default: 1.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Print raw summarized values as JSON instead of Markdown.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(token_file: str) -> str:
|
||||||
|
token = os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN", "").strip()
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
try:
|
||||||
|
with open(token_file, "r", encoding="utf-8-sig") as handle:
|
||||||
|
token = handle.read().strip()
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise WeatherReportError(
|
||||||
|
f"Grafana token missing. Set GRAFANA_SERVICE_ACCOUNT_TOKEN or create {token_file}."
|
||||||
|
) from exc
|
||||||
|
if not token:
|
||||||
|
raise WeatherReportError(f"Grafana token file is empty: {token_file}")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def day_bounds(report_date: str | None, tz_name: str) -> tuple[date, datetime, datetime]:
|
||||||
|
tz = load_timezone(tz_name)
|
||||||
|
if report_date:
|
||||||
|
local_date = date.fromisoformat(report_date)
|
||||||
|
else:
|
||||||
|
local_date = datetime.now(tz).date() - timedelta(days=1)
|
||||||
|
start_local = datetime.combine(local_date, time.min, tzinfo=tz)
|
||||||
|
end_local = start_local + timedelta(days=1)
|
||||||
|
return (
|
||||||
|
local_date,
|
||||||
|
start_local.astimezone(timezone.utc),
|
||||||
|
end_local.astimezone(timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sql_timestamp(dt: datetime) -> str:
|
||||||
|
return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def grafana_query(
|
||||||
|
grafana_url: str,
|
||||||
|
token: str,
|
||||||
|
sql: str,
|
||||||
|
start_utc: datetime,
|
||||||
|
end_utc: datetime,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
url = grafana_url.rstrip("/") + "/api/ds/query"
|
||||||
|
payload = {
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {"uid": DATASOURCE_UID, "type": DATASOURCE_TYPE},
|
||||||
|
"rawQuery": True,
|
||||||
|
"rawSql": sql,
|
||||||
|
"format": "table",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"from": sql_timestamp(start_utc),
|
||||||
|
"to": sql_timestamp(end_utc),
|
||||||
|
}
|
||||||
|
body = json.dumps(payload).encode("utf-8")
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise WeatherReportError(f"Grafana query failed with HTTP {exc.code}: {detail}") from exc
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
raise WeatherReportError(f"Grafana is not reachable: {exc}") from exc
|
||||||
|
|
||||||
|
result = data.get("results", {}).get("A", {})
|
||||||
|
if result.get("error"):
|
||||||
|
raise WeatherReportError(f"Grafana datasource error: {result['error']}")
|
||||||
|
frames = result.get("frames") or []
|
||||||
|
if not frames:
|
||||||
|
return []
|
||||||
|
frame = frames[0]
|
||||||
|
fields = [field["name"] for field in frame.get("schema", {}).get("fields", [])]
|
||||||
|
values = frame.get("data", {}).get("values", [])
|
||||||
|
if not fields or not values:
|
||||||
|
return []
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for idx in range(len(values[0])):
|
||||||
|
rows.append({field: values[pos][idx] for pos, field in enumerate(fields)})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def one(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
return rows[0] if rows else {}
|
||||||
|
|
||||||
|
|
||||||
|
def by_entity(rows: list[dict[str, Any]], entity_id: str) -> dict[str, Any]:
|
||||||
|
for row in rows:
|
||||||
|
if row.get("entity_id") == entity_id:
|
||||||
|
return row
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def as_float(value: Any) -> float | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
number = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if math.isnan(number):
|
||||||
|
return None
|
||||||
|
return number
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(value: Any, digits: int = 1, missing: str = "n/a") -> str:
|
||||||
|
number = as_float(value)
|
||||||
|
if number is None:
|
||||||
|
return missing
|
||||||
|
return f"{number:.{digits}f}".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_int(value: Any, missing: str = "n/a") -> str:
|
||||||
|
number = as_float(value)
|
||||||
|
if number is None:
|
||||||
|
return missing
|
||||||
|
return f"{round(number):d}"
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_signed(value: Any, digits: int = 1) -> str:
|
||||||
|
number = as_float(value)
|
||||||
|
if number is None:
|
||||||
|
return "n/a"
|
||||||
|
sign = "+" if number > 0 else ""
|
||||||
|
return f"{sign}{number:.{digits}f}".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
|
def local_hhmm(value: Any, tz_name: str) -> str:
|
||||||
|
if value in (None, ""):
|
||||||
|
return "n/a"
|
||||||
|
tz = load_timezone(tz_name)
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return datetime.fromtimestamp(float(value) / 1000, timezone.utc).astimezone(tz).strftime("%H:%M")
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
# Grafana may return ISO timestamps or millisecond timestamps encoded as strings.
|
||||||
|
if value.isdigit():
|
||||||
|
return datetime.fromtimestamp(float(value) / 1000, timezone.utc).astimezone(tz).strftime("%H:%M")
|
||||||
|
normalized = value.replace("Z", "+00:00")
|
||||||
|
return datetime.fromisoformat(normalized).astimezone(tz).strftime("%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(grafana_url: str, token: str, report_date: date, start_utc: datetime, end_utc: datetime) -> dict[str, Any]:
|
||||||
|
start = sql_timestamp(start_utc)
|
||||||
|
end = sql_timestamp(end_utc)
|
||||||
|
where = f"time >= timestamp '{start}' AND time < timestamp '{end}'"
|
||||||
|
|
||||||
|
temp = grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
(
|
||||||
|
f'SELECT entity_id, count(value) AS samples, min(value) AS min_value, '
|
||||||
|
f'max(value) AS max_value, avg(value) AS avg_value FROM "{TEMP_TABLE}" '
|
||||||
|
"WHERE entity_id IN ('gw3000a_outdoor_temperature',"
|
||||||
|
"'gw3000a_feels_like_temperature','gw3000a_dewpoint') "
|
||||||
|
f"AND {where} GROUP BY entity_id ORDER BY entity_id"
|
||||||
|
),
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
humidity = grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
(
|
||||||
|
'SELECT entity_id, count(value) AS samples, min(value) AS min_value, '
|
||||||
|
'max(value) AS max_value, avg(value) AS avg_value FROM "%" '
|
||||||
|
"WHERE entity_id IN ('gw3000a_humidity','gw3000a_indoor_humidity') "
|
||||||
|
f"AND {where} GROUP BY entity_id ORDER BY entity_id"
|
||||||
|
),
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
wind = grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
(
|
||||||
|
'SELECT entity_id, count(value) AS samples, min(value) AS min_value, '
|
||||||
|
'max(value) AS max_value, avg(value) AS avg_value FROM "km/h" '
|
||||||
|
"WHERE entity_id IN ('gw3000a_wind_speed','gw3000a_wind_gust') "
|
||||||
|
f"AND {where} GROUP BY entity_id ORDER BY entity_id"
|
||||||
|
),
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"date": report_date.isoformat(),
|
||||||
|
"start_utc": start,
|
||||||
|
"end_utc": end,
|
||||||
|
"outdoor_temperature": by_entity(temp, "gw3000a_outdoor_temperature"),
|
||||||
|
"feels_like": by_entity(temp, "gw3000a_feels_like_temperature"),
|
||||||
|
"dewpoint": by_entity(temp, "gw3000a_dewpoint"),
|
||||||
|
"humidity": by_entity(humidity, "gw3000a_humidity"),
|
||||||
|
"wind_speed": by_entity(wind, "gw3000a_wind_speed"),
|
||||||
|
"wind_gust": by_entity(wind, "gw3000a_wind_gust"),
|
||||||
|
"rain": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT count(value) AS samples, max(value) AS rain_mm FROM "mm" '
|
||||||
|
f"WHERE entity_id = 'gw3000a_daily_rain' AND {where}",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"solar": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT count(value) AS samples, max(value) AS max_value, avg(value) AS avg_value '
|
||||||
|
f'FROM "{SOLAR_TABLE}" WHERE entity_id = \'gw3000a_solar_radiation\' AND {where}',
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"uv": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT count(value) AS samples, max(value) AS max_value, avg(value) AS avg_value '
|
||||||
|
f'FROM "UV index" WHERE entity_id = \'gw3000a_uv_index\' AND {where}',
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"pressure": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT count(value) AS samples, min(value) AS min_value, max(value) AS max_value, '
|
||||||
|
f'avg(value) AS avg_value FROM "hPa" WHERE entity_id = \'gw3000a_relative_pressure\' AND {where}',
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"pressure_start": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "hPa" WHERE entity_id = \'gw3000a_relative_pressure\' '
|
||||||
|
f"AND {where} ORDER BY time ASC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"pressure_end": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "hPa" WHERE entity_id = \'gw3000a_relative_pressure\' '
|
||||||
|
f"AND {where} ORDER BY time DESC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"temperature_min_time": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "{TEMP_TABLE}" WHERE entity_id = \'gw3000a_outdoor_temperature\' '
|
||||||
|
f"AND {where} ORDER BY value ASC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"temperature_max_time": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "{TEMP_TABLE}" WHERE entity_id = \'gw3000a_outdoor_temperature\' '
|
||||||
|
f"AND {where} ORDER BY value DESC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"gust_max_time": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "km/h" WHERE entity_id = \'gw3000a_wind_gust\' '
|
||||||
|
f"AND {where} ORDER BY value DESC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"solar_max_time": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "{SOLAR_TABLE}" WHERE entity_id = \'gw3000a_solar_radiation\' '
|
||||||
|
f"AND {where} ORDER BY value DESC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"uv_max_time": one(
|
||||||
|
grafana_query(
|
||||||
|
grafana_url,
|
||||||
|
token,
|
||||||
|
f'SELECT time, value FROM "UV index" WHERE entity_id = \'gw3000a_uv_index\' '
|
||||||
|
f"AND {where} ORDER BY value DESC LIMIT 1",
|
||||||
|
start_utc,
|
||||||
|
end_utc,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(summary: dict[str, Any], tz_name: str, heading_level: int) -> str:
|
||||||
|
out_temp = summary["outdoor_temperature"]
|
||||||
|
feels = summary["feels_like"]
|
||||||
|
dew = summary["dewpoint"]
|
||||||
|
humidity = summary["humidity"]
|
||||||
|
wind_speed = summary["wind_speed"]
|
||||||
|
wind_gust = summary["wind_gust"]
|
||||||
|
rain = summary["rain"]
|
||||||
|
solar = summary["solar"]
|
||||||
|
uv = summary["uv"]
|
||||||
|
pressure = summary["pressure"]
|
||||||
|
|
||||||
|
pressure_start = as_float(summary["pressure_start"].get("value"))
|
||||||
|
pressure_end = as_float(summary["pressure_end"].get("value"))
|
||||||
|
pressure_trend = None if pressure_start is None or pressure_end is None else pressure_end - pressure_start
|
||||||
|
|
||||||
|
solar_max = as_float(solar.get("max_value"))
|
||||||
|
uv_max = as_float(uv.get("max_value"))
|
||||||
|
temp_max = as_float(out_temp.get("max_value"))
|
||||||
|
if solar_max is not None and uv_max is not None and solar_max >= 700 and uv_max >= 6:
|
||||||
|
narrative = (
|
||||||
|
"Der Tag war warm, hell und ueberwiegend sonnig; die hohe Solarstrahlung "
|
||||||
|
f"und der UV-Index von {fmt(uv_max)} passen klar zu einem schoenen Sommertag."
|
||||||
|
)
|
||||||
|
elif temp_max is not None and temp_max >= 25:
|
||||||
|
narrative = "Der Tag war warm; die Messwerte sprechen fuer sommerliches Wetter."
|
||||||
|
else:
|
||||||
|
narrative = "Der Tag war wettertechnisch unauffaellig; die folgenden Messwerte fassen ihn zusammen."
|
||||||
|
|
||||||
|
rain_samples = int(as_float(rain.get("samples")) or 0)
|
||||||
|
if rain_samples > 0:
|
||||||
|
rain_line = f"- Regen: {fmt(rain.get('rain_mm'))} mm Tagesmenge laut daily_rain."
|
||||||
|
else:
|
||||||
|
rain_line = (
|
||||||
|
f"- Regen: fuer {summary['date']} nicht belastbar auswertbar, "
|
||||||
|
"weil `gw3000a_daily_rain` an dem Tag keine Samples hatte."
|
||||||
|
)
|
||||||
|
|
||||||
|
heading = "#" * heading_level
|
||||||
|
lines = [
|
||||||
|
f"{heading} Wetterbericht KalliHome - {summary['date']}",
|
||||||
|
"",
|
||||||
|
f"Zeitraum: {summary['date']} 00:00 bis 24:00 Europe/Berlin.",
|
||||||
|
"",
|
||||||
|
narrative,
|
||||||
|
"",
|
||||||
|
(
|
||||||
|
f"- Temperatur aussen: {fmt(out_temp.get('min_value'))} bis {fmt(out_temp.get('max_value'))} °C, "
|
||||||
|
f"Mittel {fmt(out_temp.get('avg_value'))} °C. "
|
||||||
|
f"Minimum um {local_hhmm(summary['temperature_min_time'].get('time'), tz_name)}, "
|
||||||
|
f"Maximum um {local_hhmm(summary['temperature_max_time'].get('time'), tz_name)}."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"- Gefuehlt: Maximum {fmt(feels.get('max_value'))} °C, "
|
||||||
|
f"Mittel {fmt(feels.get('avg_value'))} °C. "
|
||||||
|
f"Taupunkt im Mittel {fmt(dew.get('avg_value'))} °C."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"- Luftfeuchte aussen: {fmt_int(humidity.get('min_value'))} bis "
|
||||||
|
f"{fmt_int(humidity.get('max_value'))} %, Mittel {fmt_int(humidity.get('avg_value'))} %."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"- Wind: Mittel {fmt(wind_speed.get('avg_value'))} km/h, "
|
||||||
|
f"Maximum Wind {fmt(wind_speed.get('max_value'))} km/h; "
|
||||||
|
f"staerkste Boe {fmt(wind_gust.get('max_value'))} km/h um "
|
||||||
|
f"{local_hhmm(summary['gust_max_time'].get('time'), tz_name)}."
|
||||||
|
),
|
||||||
|
rain_line,
|
||||||
|
(
|
||||||
|
f"- Solarstrahlung: Maximum {fmt_int(solar.get('max_value'))} W/m² um "
|
||||||
|
f"{local_hhmm(summary['solar_max_time'].get('time'), tz_name)}, "
|
||||||
|
f"Mittel {fmt_int(solar.get('avg_value'))} W/m²."
|
||||||
|
),
|
||||||
|
f"- UV-Index: Maximum {fmt(uv.get('max_value'))} um {local_hhmm(summary['uv_max_time'].get('time'), tz_name)}.",
|
||||||
|
(
|
||||||
|
f"- Luftdruck: {fmt_int(pressure.get('min_value'))} bis {fmt_int(pressure.get('max_value'))} hPa, "
|
||||||
|
f"Mittel {fmt_int(pressure.get('avg_value'))} hPa; "
|
||||||
|
f"Tendenz {fmt_signed(pressure_trend)} hPa ueber den Tag."
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"Datenabdeckung/Samples:",
|
||||||
|
(
|
||||||
|
f"- Temperatur aussen: {out_temp.get('samples', 0)}, "
|
||||||
|
f"Luftfeuchte aussen: {humidity.get('samples', 0)}, "
|
||||||
|
f"Wind: {wind_speed.get('samples', 0)}, Boeen: {wind_gust.get('samples', 0)}, "
|
||||||
|
f"Regen: {rain.get('samples', 0)}, Solar: {solar.get('samples', 0)}, "
|
||||||
|
f"UV: {uv.get('samples', 0)}, Luftdruck: {pressure.get('samples', 0)}"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
token = read_token(args.token_file)
|
||||||
|
report_date, start_utc, end_utc = day_bounds(args.date, args.timezone)
|
||||||
|
summary = summarize(args.grafana_url, token, report_date, start_utc, end_utc)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print(render_markdown(summary, args.timezone, args.heading_level))
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"weather-day-report: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PY_SCRIPT="${WEATHER_REPORT_PY_SCRIPT:-$SCRIPT_DIR/weather-day-report.py}"
|
||||||
|
TOKEN_FILE="${GRAFANA_TOKEN_FILE:-/mnt/user/appdata/secrets/monitoring_grafana_weather_report_token.txt}"
|
||||||
|
PY_IMAGE="${WEATHER_REPORT_PY_IMAGE:-python:3.13-alpine}"
|
||||||
|
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
exec python3 "$PY_SCRIPT" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "weather-day-report: neither python3 nor docker is available" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$TOKEN_FILE" ]; then
|
||||||
|
echo "weather-day-report: token file missing: $TOKEN_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec docker run --rm \
|
||||||
|
--network host \
|
||||||
|
-v "$PY_SCRIPT:/weather-day-report.py:ro" \
|
||||||
|
-v "$TOKEN_FILE:/run/secrets/grafana_weather_report_token:ro" \
|
||||||
|
-e GRAFANA_TOKEN_FILE=/run/secrets/grafana_weather_report_token \
|
||||||
|
-e GRAFANA_URL="${GRAFANA_URL:-https://monitoring.kaleschke.info}" \
|
||||||
|
-e WEATHER_REPORT_TZ="${WEATHER_REPORT_TZ:-Europe/Berlin}" \
|
||||||
|
"$PY_IMAGE" \
|
||||||
|
python /weather-day-report.py "$@"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
homeassistant:
|
homeassistant:
|
||||||
image: ghcr.io/home-assistant/home-assistant:2026.6.1@sha256:59aa8824955c9db491b75d2eebe42bd68494f80c2ec69ec0d66d9dae37d37514
|
image: ghcr.io/home-assistant/home-assistant:2026.6.4@sha256:adb3341e31e03e0048e60d8c1cf952e118a381ae258bb921d3da12a3b27bf0c2
|
||||||
container_name: homeassistant
|
container_name: homeassistant
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -15,8 +15,16 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- frontend_net
|
- frontend_net
|
||||||
- smarthome_net
|
- smarthome_net
|
||||||
expose:
|
# Zugang zum bestehenden Monitoring-Netz nur fuer den InfluxDB-3-Writer
|
||||||
- "8123"
|
# (Wetter-/Langzeitarchiv). HA schreibt intern an monitoring-influxdb3-core:8181,
|
||||||
|
# kein Host-Port, keine LAN-Exposition. Siehe docs/DECISIONS.md (2026-06-13).
|
||||||
|
- monitoring_net
|
||||||
|
# LAN-only Host-Bind nur fuer den Ecowitt-HTTP-Push: das GW3000-Gateway kann
|
||||||
|
# kein HTTPS und pusht per HTTP direkt an den HA-Webhook. Bindung ausschliesslich
|
||||||
|
# auf die LAN-IP (nicht 0.0.0.0, nicht WAN). Dokumentierte Ausnahme analog
|
||||||
|
# InfluxDB 8181, siehe docs/DECISIONS.md (2026-06-13) und Architektur-Master 10.
|
||||||
|
ports:
|
||||||
|
- "192.168.178.58:8123:8123"
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -28,6 +36,7 @@ services:
|
|||||||
- traefik.http.routers.homeassistant.entrypoints=websecure
|
- traefik.http.routers.homeassistant.entrypoints=websecure
|
||||||
- traefik.http.routers.homeassistant.tls=true
|
- traefik.http.routers.homeassistant.tls=true
|
||||||
- traefik.http.routers.homeassistant.tls.certresolver=le
|
- traefik.http.routers.homeassistant.tls.certresolver=le
|
||||||
|
- traefik.http.routers.homeassistant.middlewares=secure-headers@file
|
||||||
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
|
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
|
||||||
|
|
||||||
mosquitto:
|
mosquitto:
|
||||||
@@ -54,3 +63,9 @@ networks:
|
|||||||
name: smarthome_net
|
name: smarthome_net
|
||||||
driver: bridge
|
driver: bridge
|
||||||
internal: true
|
internal: true
|
||||||
|
|
||||||
|
# Bestehendes Observability-Netz (vom monitoring-Stack angelegt); hier nur
|
||||||
|
# extern referenziert, damit HA den InfluxDB-3-Writer erreicht.
|
||||||
|
monitoring_net:
|
||||||
|
external: true
|
||||||
|
name: monitoring_net
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.7@sha256:fcdef599e6259359833dd2e1d49f9e964f66825d69bd3dd468f51102ce013d03
|
image: traefik:v3.7@sha256:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
Reference in New Issue
Block a user