106 Commits

Author SHA1 Message Date
Micha 4cc5a1e222 Document Dawarich metrics verification 2026-06-21 23:34:40 +02:00
Micha d85972d6ab Configure Dawarich reverse geocoding 2026-06-21 23:34:40 +02:00
Micha bd0deea90d Use internal Dawarich metrics scrape 2026-06-21 23:21:21 +02:00
Micha 5a6ab2cc37 Bypass Authelia for Dawarich healthcheck 2026-06-21 23:17:38 +02:00
Micha 30c3435ddf Bypass Authelia for Dawarich metrics 2026-06-21 23:04:58 +02:00
Micha b236eaeeaa Restore Dawarich metrics basic auth config 2026-06-21 23:02:26 +02:00
Micha 4cf9e3226e Use credentials file for Dawarich metrics scrape 2026-06-21 23:00:00 +02:00
Micha 699b1f118e Scrape Dawarich metrics over HTTPS 2026-06-21 22:52:57 +02:00
Micha db886c9eb2 Send forwarded proto for Dawarich metrics 2026-06-21 22:47:51 +02:00
Micha 2a342614db Allow internal Dawarich scrape host 2026-06-21 22:44:48 +02:00
Micha 2bb6eaa267 Run Prometheus as root for file secrets 2026-06-21 22:43:28 +02:00
Micha cb80e2d2c0 Fix Dawarich Prometheus secret permissions 2026-06-21 22:41:46 +02:00
Micha 201b201657 Fix Dawarich healthcheck behind HTTPS 2026-06-21 22:39:15 +02:00
Micha 725e3b0125 Add Dawarich stack 2026-06-21 22:32:41 +02:00
Micha 1de6ffc5ac chore(deps): update nextcloud to v34 2026-06-21 21:29:37 +02:00
Micha 5559aa3f24 chore(deps): update gitea and cadvisor images 2026-06-21 21:20:11 +02:00
Micha ed61fda0ec docs(renovate): Routine-Merge-Runde 2026-06-21 im Betriebsstand festhalten
Sechs Renovate-PRs gemergt (minor-patch-Gruppe, unbound/traefik/postgres
Digest-Refreshes, n8n 2.27, nextcloud-33 Digest); Nextcloud-34-Major bewusst
gehalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:31:09 +02:00
Micha 85d8270898 Merge Renovate: renovate/docker.n8n.io-n8nio-n8n-2.x 2026-06-21 19:28:53 +02:00
Micha a904955d11 Merge Renovate: renovate/nextcloud-33.0.5-apache 2026-06-21 19:28:53 +02:00
Micha 882ea5ad01 Merge Renovate: renovate/postgres-18.4 2026-06-21 19:28:53 +02:00
Micha e3ae97bbaf Merge Renovate: renovate/traefik-v3.7 2026-06-21 19:28:53 +02:00
Micha 64976e0c0e Merge Renovate: renovate/shaanmajid-unbound-1.25.1 2026-06-21 19:28:53 +02:00
Micha 424772dcfa Merge Renovate: renovate/minor-patch-updates 2026-06-21 19:28:53 +02:00
Micha c8380b5755 docs: H:-Nearline als Restore-Quelle im DR-Fall dokumentieren
Bisher war die H:-Nearline-Kopie nur als Backup-Ziel beschrieben
(CAPACITY_AND_LIFECYCLE), nicht als Restore-Quelle. Im Ernstfall fehlte der
Hinweis, dass auf baerchen eine frische lokale Kopie aller Dumps + Bundles
liegt.

- ops/h-drive-nearline/README.md: neuer Abschnitt "Restore aus H:/ (DR-Fall)"
  mit Inhalt, Rueckspiel-Weg (-> RESTORE_MATRIX / SERVICES_RECOVERY) und
  Pflicht-Frische-Pruefung (deckt den S4U-Stale-Fall ab).
- docs/DISASTER_RECOVERY.md: baerchen-Abschnitt verweist jetzt auf die
  H:-Fallback-Restore-Quelle und das Runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:16:09 +02:00
Micha 83d7988c72 nearline: S4U-Fix angewendet und verifiziert (LogonType Interactive)
Task "KalliLab H Drive Nearline Pull" am 2026-06-21 von S4U auf LogonType
Interactive ("Nur ausfuehren, wenn der Benutzer angemeldet ist") umgestellt.
Kein gespeichertes Passwort noetig, da michi der dauerhaft angemeldete
Konsolen-User ist. Per Scheduler ausgeloest, Ergebnis 0x0 verifiziert
(SMB-Zugriff vorhanden, Spiegel frisch).

Doku korrigiert: README beschreibt jetzt Interactive als angewendete Loesung
(Password war nur die nicht genutzte Alternative). MASTER_TODO: Root-Cause
behoben, nur noch optionale Healthchecks-URL offen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:06:55 +02:00
Micha 1a4593110a nearline: S4U-Root-Cause dokumentiert + Exitcode-Leak gefixt
Diagnose 2026-06-21: Der Scheduled Task "KalliLab H Drive Nearline Pull"
lief als LogonType S4U (ohne gespeichertes Passwort) und hatte damit keine
Netzwerk-Anmeldeinformationen fuer den SMB-Share \192.168.178.58\backups.
Jeder geplante 05:30-Lauf brach still mit Exit 1 ab, ohne Report; der
Nearline-Spiegel war 2026-06-19 bis 2026-06-21 veraltet. Manuell nachgezogen,
Spiegel wieder frisch.

pull-critical-backups.ps1: explizites `exit 0` auf dem Erfolgspfad, damit der
letzte robocopy-Exitcode (1 = "Dateien kopiert") nicht als Prozess-Exit leakt
und der Scheduled Task ein wahrheitsgemaesses Ergebnis meldet.

README: Pflicht-Hinweis, dass der Task mit gespeichertem Passwort (nicht S4U)
laufen muss. MASTER_TODO: Root-Cause + verbleibender Operator-Schritt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:02:58 +02:00
Micha f296338530 monitoring + backup: Stale-Handle-Hardening und Dead-Man's-Switch
Schliesst den lokalen Code-Stand fuer zwei offene MASTER_TODO-Punkte ab.

monitoring: restliche Einzeldatei-Bind-Mounts (alertmanager, blackbox,
loki, promtail, alertmanager-ntfy-bridge) auf Directory-Mounts umgestellt,
analog zum Prometheus-Fix vom 2026-06-19. Vermeidet "Stale NFS file handle"
auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. grafana-provisioning
war bereits Directory-Mount. `docker compose config` gruen. Beim Deploy
--force-recreate noetig, da sich Mount-Zielpfade aendern.

backup: endpoint-agnostischer Dead-Man's-Switch (Healthchecks-kompatibel,
Cloud oder self-hosted) in pull-critical-backups.ps1 und pre-borg.sh.
Pings /start, Erfolg und /fail; No-Op ohne konfigurierte URL, bricht also
keinen Lauf. Ping-URLs sind Capability-URLs und bleiben als Secret
ausserhalb des Repos.

Doku: SECRETS_MAP, Nearline-README und MASTER_TODO nachgezogen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:54:53 +02:00
renovate df2e308c65 chore(deps): update shaanmajid/unbound:1.25.1 docker digest to 6fa3d52 2026-06-21 10:20:57 +00:00
renovate 3861eaa0d1 chore(deps): update minor-and-patch-updates 2026-06-20 22:20:40 +00:00
Micha 7ff6a24c9d weather day-report: No-data an trockenen Tagen abfangen
- Bewertungs-Banner: LEFT JOIN von der immer vorhandenen Temperatur-Reihe statt
  CROSS JOIN; leeres gw3000a_daily_rain killt die Zeile nicht mehr. Ergebnis als
  numerischer Code 0-4 mit Value-Mapping auf Text+Farbe (robust gegen Strings).
- Regen-Karte: noValue "0 mm" statt "No data", wenn keine Regen-Samples vorliegen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:45:28 +02:00
Micha ac1fa5b8e9 weather day-report: Layout im Stil des Wetterarchivs (Gauges + Charts)
- 6 runde Gauges wie im Wetterarchiv (Temp max/min, Luftfeuchte Ø, Boee max,
  UV max, Solar max) mit denselben continuous-Farbverlaeufen
- Strip aus 3 Stat-Karten (Gefuehlt max, Regen, Luftdruck Ø) mit Sparkline
- Bewertungs-Banner und 2 Tagescharts (Temperatur, Solar+UV)
Loest den schwer lesbaren einzeiligen Markdown-Block endgueltig ab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:38:43 +02:00
Micha f0735265eb monitoring/README: Tagesbericht-Beschreibung an visuelles Layout angepasst
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:35:17 +02:00
Micha d99082a3a7 weather day-report: visuelles Report-Layout statt Markdown-Textblock
Die markdown-html-Tabellenzelle rendert in Grafana 13 als Klartext (eine
ueberlaufende Zeile). Ersetzt durch native Panels:
- farbcodiertes Bewertungs-Banner (stat, background-color per Mapping)
- 8 Kennzahl-Karten mit Mini-Sparkline (T min/max, Regen, UV, Boee,
  Luftfeuchte, Luftdruck, Solar) inkl. Thresholds in Blau/Cyan/Amber/Gruen
- 2 Tagescharts: Temperatur (Aussen/Gefuehlt/Taupunkt) und Solar+UV
Gleiche $__timeFilter-Queries wie das Wetterarchiv-Dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:34:56 +02:00
Micha 536a6fd0cd monitoring: Wetter-Tagesberichte in Grafana auffindbar machen
- weather-report-history.json (ha-weather-report-history): Finder-Tabelle,
  eine Zeile pro Tag (Datum, Kurzbewertung, T min/max/Mittel, Regen, UV, Boee)
  mit Drilldown-Data-Link aufs Tagesbericht-Dashboard
- weather-day-report.json: Zeitzone Europe/Berlin, Info-Panel zur Tagesauswahl,
  Nav-Dropdown zu den Wetter-Dashboards
- monitoring/README: Abschnitt Wetter-Tagesberichte (finden, Datum waehlen,
  Quelle InfluxDB-SQL statt Markdown-Index, Deploy, Explore-Test)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:27:26 +02:00
Micha dee9b102bd Handle BOM in weather report token file 2026-06-20 08:11:01 +02:00
Micha 03e7f882d3 Use docker fallback for weather report script 2026-06-20 08:10:40 +02:00
Micha c7663779bb Add daily weather report automation 2026-06-20 08:08:39 +02:00
renovate 8379657446 chore(deps): update docker.n8n.io/n8nio/n8n docker tag to v2.27.3 2026-06-19 16:20:47 +00:00
Micha c39ae5cdfa monitoring: Prometheus-Directory-Mount-Hardening als erledigt vermerkt
Prometheus laeuft jetzt mit stabilem Directory-Mount (recreated, 25 Regeln
aktiv). Verbleibendes Einzeldatei-Muster bei den uebrigen Monitoring-Services
als Folge-Item praezisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:24:45 +02:00
Micha 80385c4560 monitoring: Prometheus-Config als Verzeichnis-Mount (FUSE-Stale-Handle-Fix)
Einzeldatei-Bind-Mounts von alerts.yml/prometheus.yml brechen auf dem
Unraid-FUSE-Share bei git/Komodo-Updates zu "Stale NFS file handle"
(Inode-Wechsel) -> Config-Reload laedt 0 Regeln, nur --force-recreate heilt.
Umgestellt auf stabilen Directory-Mount ./prometheus:/etc/prometheus/config:ro
plus angepasste --config.file und rule_files. Kuenftig reicht ein Reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:20:35 +02:00
Micha 7587ee4e77 Backup-Hardening: Live-Verifikation + Monitoring-Bind-Mount-Befund
Dump-Alerts live verifiziert (25 Regeln geladen, 0 feuern, Scope-Drift 0).
Stale-Handle der Prometheus-alerts.yml (FUSE-Einzeldatei-Mount) per
--force-recreate behoben; Directory-Mount-Hardening als TODO aufgenommen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:17:58 +02:00
Micha a3c5610934 Backup-Hardening: TODO-Status an verifizierten Live-Stand angleichen
Host-Clone-Pull und Borg-UI-Scope sind erledigt/verifiziert (Live-Drift 0,
alle 33 Quellen konfiguriert). Offen bleiben nur der Prometheus-Config-Reload
fuer die neuen Dump-Alerts und der Nearline-Dead-Man's-Switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:08:04 +02:00
Micha bc9ace315a Backup-Audit-Hardening: Dump-Frische-Monitoring und Scope-Konsistenz
Findings aus dem Backup-/Restore-Audit 2026-06-18 umgesetzt:

- Dump-Frische als Prometheus-Metrik (homelab_borg_dump_present /
  homelab_borg_dump_age_seconds) im Host-Exporter; schliesst den
  Blindfleck, dass Borg weiterlaeuft und stale Dumps archiviert, ohne
  Job-Fehler.
- Neue Alerts HomelabBorgDumpMissing / HomelabBorgDumpStale (critical)
  plus ALERT_RULES.md.
- Freshness-Gate (.sh + .ps1) und H:-Nearline-Pull um n8n.sqlite.dump
  und postgresql17-globals.sql ergaenzt.
- Critical-Container-Watch um mail-archiver, n8n, homeassistant,
  smarthome-mosquitto erweitert.
- BACKUP_SCOPE: /mnt/user/projekte und sonstige User-Shares ausserhalb
  App-Scope als bewusste offene Operator-Entscheidung dokumentiert;
  Hermes-data-Pfad als geparkt klargestellt.
- MASTER_TODO: Nearline-Pull-Ueberwachung, Host-Pull-Nachzug und
  projekte-Scope-Entscheidung aufgenommen.

Enthaelt ausserdem die zuvor vorbereiteten Scope-Erweiterungen
(nextcloud html+data, n8n, filebrowser, influxdb3) und Scope-Drift-/
Retention-/Compact-/Check-Alerts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:25:54 +02:00
Micha 5171059dd1 Ignore profiled services in runtime drift check 2026-06-17 22:33:15 +02:00
Micha 0ecb2aceca Refresh current homelab todo state 2026-06-17 22:30:12 +02:00
Micha 1160f50663 Clear completed Glance token todo 2026-06-17 22:04:52 +02:00
Micha 88c48faab1 Tidy AdGuard and DNS repo drift 2026-06-17 21:59:59 +02:00
Micha ec8e915a56 Classify cAdvisor startup noise 2026-06-17 21:51:55 +02:00
Micha 861f70da58 Fix operations report warnings 2026-06-17 21:49:33 +02:00
Micha fc9e4aad8e fix: raise influxdb3 query-file-limit (weather panels no data)
InfluxDB 3 Core kompaktiert nicht; haeufige HA-Writes liessen "°C"/"%"/"hPa"
ins 432-Dateien-Query-Limit laufen -> No data in Grafana. --query-file-limit
auf 20000 angehoben (Stopgap; langfristig Enterprise-Compaction oder weniger
Writes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:19:24 +02:00
renovate 410b20d74f chore(deps): update nextcloud:33.0.5-apache docker digest to fe5166b 2026-06-16 10:20:48 +00:00
Micha 15b351fa25 @
fix(immich): ML-Egress-Netz fuer Modell-Download

immich_machine_learning hing nur in immich_default (internal: true) ->
kein DNS/Egress, /cache leer, Logs "Failed to resolve huggingface.co".
Container healthy, aber Smart Search + Gesichtserkennung faktisch tot.

Fix: dediziertes nicht-internes Netz immich_egress nur an ML + explizites
dns 1.1.1.1/8.8.8.8 (DNS-Regel docs/WORKFLOW.md). DB/Redis bleiben in
immich_default isoliert (P3). Bewusst nicht frontend_net (unauth. ML-API).

Doku: Architektur-Zielbild (Netze + ML-Zeile), SERVICE_CATALOG, DECISIONS-ADR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-06-16 10:51:16 +02:00
Micha e8cde1e2e0 fix: use showLegend false for hidden timeseries legends
displayMode: hidden ist in dieser Grafana-Version fuer Timeseries ungueltig
und liess die Panels (Solarstrahlung, Luftdruck, Wallbox-Ladeleistung) leer.
Auf legend.showLegend=false umgestellt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:18:59 +02:00
Micha f236bfec00 style: upgrade grafana weather dashboard
Live-Gauges (Temp/Feuchte/Wind/UV/Solar mit Farbverlauf) + Luftdruck-Stat
oben, aufgehuebschte Verlaufs-Charts (Temp/Feuchte/Wind mit Innen-Serien,
Solar als Flaeche), Regen pro Tag als barchart. Analog zum Solar-Dashboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:14:30 +02:00
Micha fd1b7001f6 fix: solar dashboard 30-day barchart and top-day table format
- "Tages Produktion 30 Tage" auf barchart-Panel (timeseries-bars rendert
  spaerliche Tageswerte nicht); format table + xField time
- "Erreichte TOP kWh": Subquery hat keine Zeitspalte -> format table statt
  time_series (behebt Panel-Fehler); Heute als skalare Query

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:08:58 +02:00
Micha d45a49d648 style: align solar dashboard with grafana 18753 look
LCD-Segment-Bargauges mit continuous-GrYlRd, radiale Gauges im selben
Farbverlauf, Power-Chart als gruen/gelb gefuellte Flaechen, 30-Tage-
Balken mit GrYlRd, Top-Tag + Heute als LCD-Bars. SolarEdge-Entitaeten
und Batterie/Wallbox-Panels beibehalten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:00:25 +02:00
Micha 1255863a4e feat: add easee wallbox panels to solar dashboard
Wallbox-Ladeleistung (Verlauf + Gauge), Gesamt geladen und aktuelle
Session aus eh7klptt_* im Grafana Solar-Dashboard ergaenzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 22:04:52 +02:00
Micha 26fc96a7af feat: add grafana solar pv dashboard from ha/solaredge
Provisioniertes Dashboard "Solar PV System" (uid ha-solar-pv) auf der
InfluxDB-HA-Datasource: Leistung (PV/Haus/Netz/Batterie), Live-Gauges,
PV heute, Gesamt-/Lifetime-Produktion, 30-Tage-Produktion, Rekord-Tag,
Netzbilanz. Angelehnt an Grafana-Dashboard 18753, auf SolarEdge skaliert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 20:37:27 +02:00
Micha e18720d1f8 docs: record ha influxdb weather archive and glance ha token
- DECISIONS: HA -> InfluxDB 3 Core Wetterarchiv (monitoring_net-Attach,
  Admin-Token-Trade-off, Grafana-Datasource/Dashboard)
- SECRETS_MAP: ha_influxdb_token, Agent-Tokens, GLANCE_HA_TOKEN

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:51:07 +02:00
Micha a1e6a03f79 chore: remove redundant berlin weather tile from glance
Die generische Berlin-Wetterkachel ist durch die lokale Ecowitt-Kachel
(custom-api aus HA) ersetzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:51:07 +02:00
Micha 8200697258 fix: parse glance weather gust as float via gjson
toFloat erwartet eine Zahl, der HA-State kommt aber als String -> Template-
Fehler. Boee jetzt direkt per (.Subrequest "gust").JSON.Float "state" lesen,
gjson parst den numerischen String korrekt fuer den Schwellenvergleich.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:44:42 +02:00
Micha 05b12c4802 fix: pass GLANCE_HA_TOKEN into glance container
Die Compose-environment-Sektion listet die GLANCE_*-Vars einzeln; der neue
GLANCE_HA_TOKEN fehlte und kam daher nie im Container an (Glance: variable
not found). Jetzt durchgereicht analog der anderen Tokens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 19:26:46 +02:00
Micha 8d01c3537a feat: add glance weather tile from home assistant
custom-api Wetterkachel zieht die Ecowitt-Sensoren live aus HA
(intern http://homeassistant:8123, frontend_net) im Neon-Ops-Stil.
Boeen > 40 km/h werden rot markiert (analog HA-Warnautomation).
Benoetigt GLANCE_HA_TOKEN als Glance-Stack-ENV.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 18:52:43 +02:00
Micha 230e0cc9dc fix: weather dashboard entity_id without domain prefix
HA influxdb-Integration speichert entity_id ohne 'sensor.'-Praefix.
Queries entsprechend angepasst (gw3000a_* statt sensor.gw3000a_*).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 17:46:47 +02:00
Micha c9bd4af2a8 docs: record ha energy dashboard setup 2026-06-13 16:02:10 +02:00
Micha 5927b478fa docs: record local solaredge integration 2026-06-13 15:02:41 +02:00
Micha ee69bbf730 feat: add grafana weather archive dashboard
Provisioniertes Dashboard 'Wetterarchiv KalliHome' (uid ha-weather-archive)
auf der Datasource ha-weather-influx: Temperatur, Feuchte, Wind, Solar,
Regen/Tag, Luftdruck aus den Ecowitt-Langzeitdaten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:55:26 +02:00
Micha d908d967d4 feat: add grafana datasource for ha weather archive (influxdb)
Zweite InfluxDB-Datasource 'InfluxDB HA Weather' (uid ha-weather-influx)
auf DB homeassistant fuer das Ecowitt-Langzeitarchiv. Gleiche Instanz/Token
wie die bestehende Monitoring-Datasource.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:51:55 +02:00
Micha 606779d342 feat: attach home assistant to monitoring_net for influxdb writer
HA bekommt Zugang zum bestehenden monitoring_net, um Wetter-/Langzeitdaten
intern an monitoring-influxdb3-core:8181 zu schreiben (Wetterarchiv).
Kein Host-Port, keine LAN-Exposition; gewaehlte Reachability-Option aus
docs/DECISIONS.md (2026-06-13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:46:25 +02:00
Micha 0fabed4d1a docs: record ecowitt lan-only ingress decision
LAN-only Host-Bind 192.168.178.58:8123 fuer den Ecowitt-HTTP-Push
dokumentiert: DECISIONS-Eintrag (loest Phase-2-Frage), Architektur-Master
Ausnahme 10, SERVICE_CATALOG. Webhook + LAN-Endpunkt verifiziert; offen
bleibt nur die GW3000-Customized-Server-Konfiguration am Geraet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:06:29 +02:00
Micha 76b9ffa140 feat: add lan-only host bind for ecowitt http push
Ecowitt GW3000 kann kein HTTPS und pusht per HTTP an den HA-Webhook.
HA bekommt einen LAN-only Host-Bind 192.168.178.58:8123 (nicht WAN),
analog zur dokumentierten InfluxDB-8181-Ausnahme. Kein Traefik-Umbau
des globalen HTTP-Redirects noetig, da Ecowitt rein im LAN pusht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:02:46 +02:00
Micha 170a7dcc1f docs: record ha mqtt integration 2026-06-13 08:49:46 +02:00
Micha 0f5045ea8e docs: close home assistant restore gate 2026-06-13 08:40:47 +02:00
Micha dfa3acc21e ops: add home assistant restore test 2026-06-13 08:37:33 +02:00
Micha 2eb8da1cd4 docs: clarify mqtt broker smoke status 2026-06-13 08:33:01 +02:00
Micha 2acbc1adde docs: record home assistant foundation status 2026-06-13 08:30:53 +02:00
Micha 342d0a0a27 fix: use native ha auth after onboarding 2026-06-13 08:07:08 +02:00
renovate a28d3bbc33 chore(deps): update postgres:18.4 docker digest to 29ee7bb 2026-06-12 22:20:51 +00:00
Micha 4ab6dcefd2 fix: protect ha onboarding with authelia 2026-06-12 21:52:45 +02:00
Micha c24b792808 fix: allow home hairpin during ha onboarding 2026-06-12 21:51:34 +02:00
Micha 25a4ada891 fix: guard home assistant onboarding 2026-06-12 21:50:15 +02:00
Micha 6e6005aefd feat: add smart home foundation 2026-06-12 20:51:18 +02:00
Micha ad438a07b3 fix: allow mosquitto config ownership setup 2026-06-12 20:45:32 +02:00
Micha ce6f5c72dd feat: add smart home runtime foundation 2026-06-12 20:38:03 +02:00
Micha 630ee8dd90 ops: glance server-stats balken kraeftiger (13px, innenschatten)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:00:38 +02:00
Micha b1ca9ef19c ops: glance server-stats balken - rund, gradient, glow, warnfarbe ab 85%
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:57:36 +02:00
Micha 1c949d3fcc ops: glance internet-widget - bytes/s nach mbit/s umrechnen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:55:05 +02:00
Micha cfa6c01768 ops: glance komodo/immich widgets - stat-leisten mit trennlinien, pills, gradient-bars
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:24:12 +02:00
Micha 3474d53ce5 ops: glance borg-widget fix - alter via promql berechnen statt now.Unix im template
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:05:48 +02:00
Micha ca81b959cc ops: glance borg-backup-widget via prometheus + synthwave/matrix presets
- glance zusaetzlich in monitoring_net (nur lesende Prometheus-Query, kein neuer Listener)
- Borg-Widget: Backup-Alter aus homelab_borg_last_completed_timestamp_seconds, Status aus homelab_borg_last_success
- Theme-Presets synthwave und matrix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:01:32 +02:00
Micha 23764dff38 ops: glance farbschema entlilat - neutraler grund, akzente blau/cyan/amber/gruen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:44:00 +02:00
Micha 3c4a48d7e5 ops: glance neon-ops v2 - rotierende akzentfarben, gradient-zahlen, animierte header
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:30:38 +02:00
Micha c0a39f5dfc ops: glance neon-ops look - card styling, glows, sattere theme-farben
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:27:30 +02:00
Micha a1d7b6e433 ops: glance layout verdichtet - internet kombiniert, container-tabs, mealie/commit-fixes
- Home rechte Spalte: Internet+Speed in einem Widget, DNS-und-VPN-Monitor entfernt, Container-Listen als Tab-Gruppe
- Infrastructure: Container-Listen als Tab-Gruppe, Mealie-Statistik auf /api/admin/about/statistics (404-Fix)
- Commit-Widgets: toRelativeTime als span-Attribut, nur erste Commit-Zeile

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:47:25 +02:00
Micha 45f43da659 ops: glance speedtest widget - ookla raw data fallbacks (data.data.*)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:44:18 +02:00
Micha 290cb8949e ops: glance dashboard v2 - split config, stack widgets, releases page
- Config per $include aufgeteilt (glance.yml -> pages/home/infrastructure/ops, containers-map zentral)
- Neue Widgets: Komodo Stacks, Gitea GitOps, Paperless, Mealie, Scrutiny Disk Health, Wetter, To-do
- Neue Seite Ops und Releases (releases-Widget fuer gepinnte Images, RSS, Commit-Log)
- Homelab-Status in Tab-Gruppen Core/Apps/Ops, Speedtest-Widget mit ehrlichem Leerzustand
- Theme-Presets (Catppuccin, Gruvbox, Light) + custom.css via Assets-Mount
- Compose: 5 neue read-only Token-ENVs, Doku in SECRETS_MAP/MASTER_TODO nachgezogen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:06:42 +02:00
Micha d933d3cee8 ops: refine komodo stack hygiene check
- Hash drift now requires actual file changes inside the stack's
  compose-dir between deployed_hash and latest_hash. Komodo's
  deployed_hash bumps only on redeploy while latest_hash tracks master
  HEAD, which produced six false-positive "Pending Update" warnings
  for stacks whose own files never changed.
- Add EXPECTED_NOT_IN_KOMODO env (default: hermes-agent) for compose
  files intentionally not Komodo-managed (work-in-progress, build/dev
  compose).

End-to-end run on host: 0 critical, 0 warnings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 13:23:52 +02:00
Micha baedf9f932 docs: record komodo-stack-hygiene-weekly activation
Cron registered in /boot/config/plugins/user.scripts and live in
/etc/cron.d/root after update_cron. First scheduled run: Sun 05:00.
End-to-end smoke test on host: 6 warnings, 0 critical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 12:57:06 +02:00
Micha b387757e87 ops: add komodo stack hygiene posture-check
Catches the failure class that let immich_new slip through: stacks
without a configured repo, project_missing, hash drift, and repo
compose files without a matching Komodo stack. Dry-run on host found
6 honest warnings, 0 critical. Wrapper as Unraid User Script for
weekly cadence is tracked in MASTER_TODO.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 12:51:07 +02:00
Micha 3eedbcbe16 docs: record immich stack cleanup 2026-06-12 08:24:27 +02:00
Micha 9033724b15 docs: record host DNS fallback as active
eth0 DNS server 2 = 192.168.178.1 (FRITZ!Box) is set as failover behind
AdGuard. Mark the komodo-bulk-deploy-dns runbook immediate measure as
implemented. Closes the AdGuard SPOF for Docker image pulls.
Ref: docs/homelab-optimierung.md recommendation 3a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:26:22 +02:00
Micha aae176f1b7 docs: record Hetzner Storage Box automatic snapshots as active
Daily snapshots at 05:30 UTC (after the 04:30 local Borg run), 7 days
retention, snapshot directory visible for single-file restore via
.zfs/snapshot/. Closes the ransomware/misuse gap left open by the
explicit decision against Borg append-only (2026-06-01).
Ref: docs/homelab-optimierung.md recommendation 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:25:01 +02:00
Micha c7590e6603 fix(immich): pin server and ML to v2.7.5 instead of mutable release tag
Digests unchanged (verified against GHCR manifest API: release ==
v2.7.5 for both images). Renovate now produces visible version PRs
instead of silent digest bumps that hide major version jumps.
Ref: docs/homelab-optimierung.md recommendation 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:02:26 +02:00
Micha 3e486b95f6 docs: add pdf cleanup and quarterly doc gardening to MASTER_TODO
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:55:15 +02:00
Micha 08b4be7a5d docs: add AGENTS.md entry point for non-Claude AI agents
Codex CLI auto-reads AGENTS.md; file only points to AI_CONTEXT,
architecture master, workflow and the binding doc rules - no duplicated
content (one fact, one home).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:50:53 +02:00
renovate af4b7015ee chore(deps): update traefik:v3.7 docker digest to d685879 2026-06-10 22:20:18 +00:00
95 changed files with 5966 additions and 1026 deletions
+2 -1
View File
@@ -6,7 +6,8 @@
!**/stack.env.example
# Secrets and certificate material
**/secrets/
**/secrets/*
!**/secrets/*.example
**/letsencrypt/
**/acme.json
**/*.key
+20
View File
@@ -0,0 +1,20 @@
# Agent Context - Homelab Infra
Typ: Einstieg/Index · Stand: 2026-06-11 · Status: aktiv
Einstiegspunkt fuer KI-Agenten (Codex, Gemini u. a.; Claude nutzt zusaetzlich
`CLAUDE.md`). Kein eigener Inhalt - nur Pflichtpfade.
## Vor jeder Arbeit lesen
1. `docs/AI_CONTEXT.md` - Systembild, harte Regeln, Ausnahmen-Kurzliste
2. `HOMELAB_ARCHITECTURE_MASTER_V2.md` - Architektur-Zielbild
3. `docs/WORKFLOW.md` - verbindlicher GitOps-/No-Drift-Ablauf
4. die betroffene `docker-compose.yml` bzw. das betroffene Runbook (Index: `docs/README.md`)
## Nicht verhandelbar
- Keine Secret-Werte lesen, zitieren oder schreiben - nur Namen und Pfade.
- Keine Deployments, Host-Hotfixes oder Docker-Schreibbefehle ohne ausdrueckliche Anweisung.
- Doku-Regeln aus `docs/REPO_MAP.md` einhalten: ein Fakt, ein Zuhause. Status nur in `docs/MASTER_TODO.md`, Entscheidungen nur in `docs/DECISIONS.md`.
- Bei Drift oder zwei fehlgeschlagenen Reparaturversuchen: stoppen, `docs/GITOPS_DRIFT_RUNBOOK.md`.
+14 -5
View File
@@ -3,7 +3,7 @@
> **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.
**Stand:** 2026-06-11 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
**Stand:** 2026-06-13 | **Aktueller Schwerpunkt:** Home Assistant Tibber / Energie-Kosten
---
@@ -88,11 +88,13 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
| `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt |
| `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 |
| `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 |
| `glance_socket_net` | Compose-intern, `internal: true` | interner Zugriff von Glance auf den Docker-Socket-Proxy | umgesetzt |
| `smarthome_net` | bridge, `internal: true` | interne Smart-Home-Kommunikation zwischen Home Assistant, Mosquitto, spaeter Zigbee2MQTT/ESPHome | vorbereitet |
| `host` | host | nur für echte Sonderfälle | begründet |
### 3.2 Finales Diagramm (vereinfacht)
@@ -123,7 +125,8 @@ App-interne Netze
├── immich_default (internal: true) ✅
├── nextcloud_internal (internal: true) ✅
├── monitoring_net (zentraler Observability-Stack)
── monitoring_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
── monitoring_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
└── smarthome_net (HA, Mosquitto, spaeter Zigbee2MQTT/ESPHome)
Host-Sonderfälle
├── tailscale
@@ -146,6 +149,7 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar:
- `immich_server` — immich.kaleschke.info
- `nextcloud` — cloud.kaleschke.info
- `plex` — plex.kaleschke.info (Traefik, native Plex-Auth; Plex Remote Access/Port 32400 bleibt aus)
- `homeassistant` — home.kaleschke.info (Traefik, native Home-Assistant-Auth)
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
Diese Dienste sind **keine Public Apps**:
@@ -261,6 +265,7 @@ Legende Status:
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — |
| `nextcloud-redis` | ✅ | `nextcloud_internal` | intern | app-eigener Cache fuer File Locking / Sessions | — |
| `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
@@ -272,8 +277,9 @@ Legende Status:
| `ntfy` | ✅ | `frontend_net` | Traefik | aktiv via `ntfy.kaleschke.info`, Git-Stack | — |
| `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_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 |
| `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 | — |
| `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 |
@@ -389,10 +395,13 @@ Die Blockmigration aus der Portainer-/Dockerman-Phase ist abgeschlossen: Traefik
| `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang |
| `traefik/dynamic/*` | manueller Host-Sync trotz GitOps | File-Provider bleibt bewusst fuer `middlewares.yml`, `tls.yml` und `dashboards.yml`; Komodo deployed diese Dateien nicht automatisch |
| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ |
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant schreibt spaeter Langzeitdaten. Nach der HA-Container-Entscheidung muss der Writer-Pfad in der Influx-Phase explizit gewaehlt werden: entweder LAN-Bind via `INFLUXDB_BIND_IP` oder gezieltes gemeinsames internes Netz. Keine Traefik-Route, Zugriff nur ueber Token; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
| `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. |
| `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` (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. |
---
+16
View File
@@ -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
+164
View File
@@ -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.
+274
View File
@@ -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
+18 -2
View File
@@ -1,7 +1,7 @@
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:release@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
image: ghcr.io/immich-app/immich-server:v2.7.5@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
restart: unless-stopped
depends_on:
- redis
@@ -32,7 +32,7 @@ services:
immich-machine-learning:
container_name: immich_machine_learning
image: ghcr.io/immich-app/immich-machine-learning:release@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
image: ghcr.io/immich-app/immich-machine-learning:v2.7.5@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
restart: unless-stopped
environment:
# Workaround fuer gunicorn-25.1.0-Control-Socket-Bug: der Worker haengt
@@ -48,7 +48,18 @@ services:
volumes:
- model-cache:/cache
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_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:
- no-new-privileges:true
@@ -86,5 +97,10 @@ networks:
name: immich_default
internal: true
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:
external: true
+1 -1
View File
@@ -1,6 +1,6 @@
services:
mail-archiver:
image: s1t5/mailarchiver@sha256:4ea7ecc47ad1dd2c523b85c3967574b61e39def1b6fd26edf874e21733c4018c
image: s1t5/mailarchiver@sha256:9ab6f51fa036c7869f64cb052a18f7bb8b9951a120ce1c03df43a273a20d3f59
container_name: mail-archiver
restart: unless-stopped
environment:
+1 -1
View File
@@ -54,7 +54,7 @@ services:
- traefik.http.services.mealie.loadbalancer.server.port=9000
mealie-postgres:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
container_name: mealie-postgres
restart: unless-stopped
+1 -1
View File
@@ -1,6 +1,6 @@
services:
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
restart: unless-stopped
+2 -2
View File
@@ -1,6 +1,6 @@
services:
nextcloud:
image: nextcloud:33.0.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d
image: nextcloud:34.0.0-apache@sha256:851ca6ef9da101ce3c8a32ec7b6fc65a726b380b5f466307a54c17d32fb77c9a
container_name: nextcloud
restart: unless-stopped
depends_on:
@@ -46,7 +46,7 @@ services:
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
nextcloud-postgres:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
container_name: nextcloud-postgres
restart: unless-stopped
environment:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
super-productivity:
image: johannesjo/super-productivity:v18.9.1@sha256:773760107344e739f4c29409f7842db66a1b167d50eb2c40248cb5b5b328652e
image: johannesjo/super-productivity:v18.12.0@sha256:2c84668a961b090dd931f6e117dde5195b7c674d8453e0d511b777c23c242bc8
container_name: super-productivity
restart: unless-stopped
+1 -1
View File
@@ -1,6 +1,6 @@
services:
unbound:
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b
image: shaanmajid/unbound:1.25.1@sha256:6fa3d5257ff6d95ab16153c62fabfe256edc0db515f94755f5edeb1f2a2258ab
container_name: unbound
restart: unless-stopped
volumes:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
gitea:
image: docker.gitea.com/gitea:1.26.2@sha256:7d13848af12645600a5f9d93ee2560daa9c6fa6b5b859b7bff3a5e1c0b661031
image: docker.gitea.com/gitea:1.26.4@sha256:8e25c717b8f748445e15ec46e0390f577cb628101184cb0a150d1dae126c1f39
container_name: gitea
restart: unless-stopped
security_opt:
+9 -1
View File
@@ -1,6 +1,6 @@
# Alert Rules
Stand: 2026-06-05
Stand: 2026-06-18
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
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 |
| `HomelabBorgLastJobFailed` | letzter Borg-Job fehlgeschlagen | critical | Borg-UI-Job-Log pruefen |
| `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 |
| `HomelabPrometheusTargetDown` | Scrape-Ziel down | critical | node-exporter/cadvisor/blackbox/traefik pruefen |
+8 -5
View File
@@ -1,14 +1,16 @@
# 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
(`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.
> **Status:** aktives Runbook. Grafana und Mealie sind seit 2026-06-06 live
> und per Login-Smoke verifiziert. Der weitere Rollout bleibt additiv: lokale
> App-Logins bleiben als Fallback aktiv.
> und per Login-Smoke verifiziert. Paperless ist seit 2026-06-17 technisch
> 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. |
| 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) |
| 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.
@@ -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;
sonst legt die App (bei aktivem Signup) einen neuen User an.
- **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
+189
View File
@@ -11,6 +11,195 @@ 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
**Entscheidung:** Home Assistant laeuft neu als `homeassistant` Container im
Stack `smart-home/`, nicht als HAOS-VM und nicht als Supervised-Installation.
Mosquitto laeuft als eigener Container im selben Stack; Zigbee2MQTT und ESPHome
werden spaeter ebenfalls als eigenstaendige Container ergaenzt. HA haengt in
`frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome.
Das Fachrepo `smart-home-kalli` liefert versionierte HA-YAML-Dateien read-only;
`.storage`, `secrets.yaml` und Integrations-State bleiben in
`/mnt/user/appdata/homeassistant`.
**Kontext:** Das fruehere HAOS-VM-Setup ging bei einem Crash ohne brauchbares
Backup verloren. Das Homelab betreibt produktive Dienste inzwischen ueber
Gitea, Komodo, Compose, Renovate und Borg. HA Container passt in dieses
Betriebsmodell und vermeidet eine zweite Update-/Backup-Welt. Supervised ist
kein Zielpfad mehr; HAOS bleibt die Alternative, falls Add-on-Komfort,
Matter/Thread/HomeKit-Discovery oder Host-nahe HA-Funktionen wichtiger werden
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
(HomeKit, Cast, Matter/Thread), Bedarf an HA-Add-ons als Betriebsstandard,
oder wiederholte Probleme durch Bridge-Netzwerkbetrieb.
## 2026-06-12 - Ecowitt-Ingress bleibt bewusste Phase-2-Entscheidung
**Entscheidung:** In Phase 1 wird kein Host-Port `8123` fuer Home Assistant
veroeffentlicht. Ecowitt wird spaeter entweder ueber eine gezielte
Traefik-HTTP-Ausnahme fuer den Webhook-Pfad angebunden oder, falls der globale
HTTP-zu-HTTPS-EntryPoint-Redirect nicht sauber selektiv abloesbar ist, ueber
einen dokumentierten LAN-only Host-Port `8123`.
**Kontext:** Ecowitt kann nur HTTP und kein HTTPS. Traefik hat aktuell einen
globalen `web` -> `websecure` Redirect auf EntryPoint-Ebene. Ein normaler
HTTP-Router kann diese Regel voraussichtlich nicht umgehen, ohne Traefik selbst
umzubauen. Deshalb wird die Entscheidung nicht vorgezogen.
**Review-Trigger:** Start der Ecowitt-/InfluxDB-Phase oder Umbau der Traefik
HTTP-Redirect-Architektur.
## 2026-06-11 — Host-DNS-Fallback aktiv (AdGuard-SPOF entschaerft)
**Entscheidung:** Unraid-Host nutzt `eth0` DNS server 1 = `192.168.178.58` (AdGuard) und **DNS server 2 = `192.168.178.1`** (FRITZ!Box) als Failover.
**Kontext:** AdGuard war einziger LAN-Resolver; ein Recreate hat 2026-06 einen Bulk-Deploy zerlegt, weil Docker-Pulls am eigenen DNS-Container scheiterten. Der Fallback bleibt nur passiv aktiv (Go-Resolver springt erst bei Socket-Fehler weiter), der Filter wirkt im Normalbetrieb unveraendert. `options rotate` ist nicht gesetzt. Umsetzung der Empfehlung 3a aus dem Optimierungs-Assessment vom 2026-06-10. Runbook: `docs/runbooks/komodo-bulk-deploy-dns.md`.
**Review-Trigger:** Wenn AdGuard durch eine andere Filter-Loesung ersetzt wird oder ein zweiter Host-Resolver verfuegbar ist.
## 2026-06-11 — Hetzner Storage Box: automatische Snapshots aktiv
**Entscheidung:** Automatische Snapshots auf der Hetzner Storage Box (BX11, `u565255.your-storagebox.de`) sind aktiv: taeglich um 05:30 UTC (nach dem Borg-Lauf 04:30 lokal), Retention 7 Tage, Snapshot-Verzeichnis sichtbar fuer Einzeldatei-Restore via `.zfs/snapshot/`.
**Kontext:** Borg `append-only` ist bewusst nicht umgesetzt (siehe Eintrag 2026-06-01); damit war ein kompromittierter Host bisher in der Lage, auch das Off-site-Backup zu loeschen. Storage-Box-Snapshots sind host-seitig nicht loeschbar und im BX11-Tarif inklusive. Kosten: 0 EUR zusaetzlich. Umsetzung der Empfehlung 2 aus dem Optimierungs-Assessment vom 2026-06-10.
**Review-Trigger:** Hetzner-Quota-Druck (aktuell 65 GB / 1 TB - viel Luft) oder Aenderung der Backup-Strategie.
## 2026-06-11 — Doku-Konsolidierung: ein Fakt, ein Zuhause
**Entscheidung:** Die Dokumentation wird nach `docs/archive/2026/homelab-doku-optimierung-2026-06-11.md` konsolidiert: `MASTER_TODO.md` ist die einzige Statusliste, dieses Register die einzige Entscheidungssammlung, `docs/archive/` nimmt abgeschlossene Snapshots auf, Erledigtes verlaesst die Arbeitskopie. Keine Ordner-Restruktur des Bestands.
+7 -2
View File
@@ -532,8 +532,13 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
### Windows-Workstation `baerchen`
`baerchen` ist die Operator-Workstation und haelt den lokalen Clone unter
`G:\Gitea_Clone\homelab-infra`. Fuer einen schnellen Windows-Bare-Metal-Restore
existiert ein Veeam-Agent-Image-Workflow.
`G:\Gitea_Clone\homelab-infra`. Zusaetzlich liegt auf der externen Platte `H:`
(`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:
+16 -7
View File
@@ -1,6 +1,6 @@
# Master To-do - KalliLab CORE
Typ: Status/To-do · Stand: 2026-06-11 · Status: aktiv
Typ: Status/To-do · Stand: 2026-06-21 · Status: aktiv
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
@@ -23,15 +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` |
| 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` |
| 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` |
| 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` |
| 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
**Stand 2026-06-11: keine offenen Operator-Entscheidungen.**
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
@@ -47,13 +53,14 @@ 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` |
| 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` |
| 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` |
| Filebrowser-Mount-Scope | naechster Hardening-Sprint | `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` |
| 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` |
| 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 |
---
@@ -67,9 +74,11 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
- **2026-06-11** Doku-Konsolidierung umgesetzt: `docs/archive/`, `docs/DECISIONS.md`, Statuslisten auf diese Datei reduziert, Restore-Doku zusammengefuehrt. Details: `docs/DECISIONS.md` Eintrag 2026-06-11.
- **2026-06-06** Wochenend-Sprint abgeschlossen: Veeam-Recovery-Test, DR-Workstation-Kit final (DR-Smoke OK), Tailscale-ACL restriktiv + redundanter Docker-Stack entfernt, Gast-/IoT-Netz validiert, AdGuard-/Redis-Restore-Smokes, Authelia-2FA-Catch-all, Frische-Negativtest. Belege: Host-Reports, `docs/DECISIONS.md`.
- **2026-06-03** Restore-Backlog geschlossen: Nextcloud, Shared-PG18-Cluster, Komodo-Mongo, Mailarchiver, Mealie, Traefik. Reports unter `/mnt/user/backups/restore-reports/`.
- **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-17** Repo-Hygiene abgeschlossen: Glance-Widget-Tokens sind in Runtime gesetzt, Audit-PDF liegt extern unter `H:\kallilab-recovery\audits`, Worktree clean.
- **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-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-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
View File
@@ -1,7 +1,7 @@
# 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.
Letzte Pruefung: 2026-06-05 (Tailscale-Inventar), 2026-06-01 (Router/Ports)
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-17 (Tailscale-Inventar), 2026-06-01 (Router/Ports)
## Zweck
@@ -38,7 +38,7 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
| 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 |
| 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 |
| 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. |
| 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 |
|---|---|---|---|
| `100.80.98.33` | kallilabcore | linux | aktiv (Host, Subnet-Router) |
| `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 |
| `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. |
| `100.73.83.55` | iphone-14 | iOS | bekannt, aktuell offline |
> **Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host hat **zwei**
> `tailscaled`-Prozesse:
> **Historischer Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host
> hatte damals **zwei** `tailscaled`-Prozesse:
>
> 1. **Native Unraid-Plugin** = `kallilabcore` (100.80.98.33). Prozess
> `/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
> Architektur-/Service-Catalog-/DR-/CLAUDE-Doku auf "natives Plugin" nachgezogen.
> Read-only verifiziert: Container weg, nur noch der native `tailscaled` mit
> `tailscale1`, Subnet-Route + Operator-Zugriff intakt. Offen: Node-Eintraege
> `kallilab-core` und alter `baerchen` in der Admin-Konsole entfernen; State-Pfad
> `/mnt/user/appdata/tailscale` bei Gelegenheit nach `_archive/` (kein Sofort-Loeschen).
> `tailscale1`, Subnet-Route + Operator-Zugriff intakt. Nachpruefung 2026-06-17:
> `tailscale status --self=false` zeigt nur noch `baerchen-1` und `iphone-14`;
> 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
> 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`
= `tag:operator`; `kallilab-core` (Docker) + alter `baerchen` bewusst untagged ->
isoliert.
= `tag:operator`. Alte Nodes `kallilab-core` und `baerchen` sind nicht mehr im
aktuellen Tailnet-Status sichtbar.
**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.
- **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack
abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv.
- **Tailnet-Konsole aufraeumen: ERLEDIGT 2026-06-06** — Node-Eintraege `kallilab-core`
und alter Offline-`baerchen` aus der Admin-Konsole entfernt.
- State-Pfad `/mnt/user/appdata/tailscale` (vom entfernten Docker-Stack) bei
Gelegenheit nach `_archive/tailscale-removed-2026-06-06/` (kein Sofort-Loeschen).
- **Tailnet-Konsole/Altstate aufraeumen: ERLEDIGT 2026-06-17** — Node-Eintraege
`kallilab-core` und alter Offline-`baerchen` sind im aktuellen Tailnet-Status
nicht mehr sichtbar; State-Pfad `/mnt/user/appdata/tailscale` vom entfernten
Docker-Stack liegt unter `_archive/tailscale-removed-2026-06-06/`.
- 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
Subnet-Route end-to-end zu bestaetigen (im Heim-LAN nicht sauber isolierbar).
+1
View File
@@ -14,6 +14,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln:
| Datei | Zweck |
|---|---|
| `../README.md` | kurzer Repo-Einstieg |
| `../AGENTS.md` | Einstiegspunkt fuer KI-Agenten (Codex u. a.) |
| `../HOMELAB_ARCHITECTURE_MASTER_V2.md` | Architektur-Quelle fuer Netz, Zugriff und Ausnahmen |
| `WORKFLOW.md` | verbindlicher GitOps-/No-Drift-Ablauf |
| `REPO_MAP.md` | technische Landkarte des Repositories + Doku-Regeln |
+2
View File
@@ -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.
**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)
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
+10 -2
View File
@@ -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 |
| 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 |
| 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` |
| 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` |
@@ -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 |
|---|---|---|---|---|---|---|
| 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 |
| 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 |
@@ -60,6 +60,10 @@ 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` |
| 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` |
| 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 |
| 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 |
---
@@ -77,6 +81,8 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| InfluxDB 3 Core | historischer Altstand / Datenuebernahme | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | dateibasierter Object Store | `influxdb3_admin_token.json` | `monitoring-influxdb3-core` | Datenpfad wird vom Monitoring-Zielstack weitergenutzt und darf nicht blind geloescht werden |
| Loki / Alloy | historischer Altstand | `/mnt/user/appdata/loki/config`, `/mnt/user/appdata/loki/data`, `/mnt/user/appdata/alloy/config` | keine primaere DB; Loki-Dateispeicher war transient | keine zusaetzlichen Secrets | nicht aktiv | Compose-Pfad aus aktivem Repo entfernt; aktuelle Logsammlung laeuft ueber `monitoring-loki`/`monitoring-promtail` |
| Monitoring Stack | Rebuild + named volumes + InfluxDB-Appdata | `prometheus_data`, `loki_data`, `promtail_positions`, `grafana_data`; InfluxDB unter `/mnt/user/appdata/influxdb3/data` und `/mnt/user/appdata/influxdb3/plugins`; Provisioning aus `monitoring/grafana/provisioning` | Prometheus-TSDB, Loki-Dateispeicher und InfluxDB-Dateistore; Diagnose-/Langzeitdaten, keine Tier-1-Restore-Quelle | `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json` | `monitoring_net`, `monitoring_influx_lan`, `frontend_net`, Traefik, Authelia, Docker socket read-only fuer Promtail, Host-Mounts fuer node-exporter/cAdvisor | `https://monitoring.kaleschke.info` leitet zu Authelia; Prometheus Targets sind up; Grafana-Datasources `Prometheus`, `Loki` und `InfluxDB 3 Core` funktionieren |
| Zigbee2MQTT (geplant) | Borg + Fachrepo | `/mnt/user/appdata/zigbee2mqtt` inkl. `configuration.yaml`, `database.db`, `coordinator_backup.json`, `state.json`; Fach-Doku im Repo `smart-home-kalli` | keine externe DB | `network_key`, MQTT-Credentials, LAN-Koordinator-IP/Firmwarestand | Mosquitto, LAN-PoE-Koordinator, `smarthome_net` | Z2M startet, Coordinator verbindet sich, geraete bleiben gepairt, Testgeraet sendet MQTT-State |
| ESPHome (geplant) | Fachrepo + Borg fuer Build-/Runtime-State | `/mnt/user/appdata/esphome` falls Dashboard/Build-Cache genutzt wird; YAML unter `/mnt/user/services/smart-home-kalli/esphome` | keine | ESPHome-Secrets ausserhalb Git, API-/OTA-Keys | WLAN/LAN, Mosquitto falls MQTT genutzt wird | Dashboard startet, ein Testgeraet kompiliert/validiert, OTA/API-Verbindung funktioniert |
| Hermes Agent | VM-seitig offen | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh` | keine eigene DB | Host-`.env` fuer Provider-/API-/Home-Assistant-Tokens, `hermes_runner_id_ed25519`, `HERMES_DASHBOARD_HOST` | separate Hermes-VM/Runner, Traefik, Authelia, `hermes_net` | NAS-Stack nicht starten, solange Runner-VM und echte `.env` fehlen |
| ddns-updater | Rebuildbar | geringe Persistenzrelevanz | keine | Provider-Zugang ueber Stack ENV | Internetzugang | Update-Job laeuft |
@@ -99,6 +105,7 @@ Aktuell relevante Dump-Artefakte unter `/mnt/user/backups/borg/dumps/latest`:
- `filebrowser.bolt.dump`
- `borg-ui.sqlite`
- `grafana.sqlite`
- `n8n.sqlite.dump`
- `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.
- `komodo-mongo.archive.gz` (noch gesondert verifizieren)
@@ -160,6 +167,7 @@ Stand 2026-06-06. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
| Borg UI | 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 |
| 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
View File
@@ -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 |
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | 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 |
| 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 |
@@ -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 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 |
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}` | 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 |
| 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-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 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 |
| 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 |
| 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 -> 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) |
| 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`) |
| 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 |
@@ -98,8 +108,16 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|-- redis_password.txt
|-- borg_repo_passphrase.txt
|-- influxdb3_admin_token.json
|-- ha_influxdb_token
|-- ha_token_claude
|-- ha_token_codex
|-- filebrowser_admin_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
```
@@ -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.
- 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.
- `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
`micha`. Ein dedizierter SMB-User `veeam-baerchen` ist nur ein spaeteres
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 |
|---|---|---|---|
| `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 |
| `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 |
| `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 |
| `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` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker) neu erzeugen | rebuildbar; 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. |
### Komodo-Sonderfall
+16 -6
View File
@@ -1,6 +1,6 @@
# 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.
@@ -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 |
| `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 |
| `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 |
|---|---|---|---|---|---|---|---|---|
| `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. |
| `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_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-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 |
| `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 |
@@ -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 |
| `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-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-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 |
@@ -75,11 +78,18 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
| `monitoring-promtail` | Docker-Log-Collector fuer Monitoring-Loki | `monitoring/docker-compose.yml`, `monitoring/promtail/promtail-config.yml` | intern | Docker socket read-only, Docker json-file Logs, Loki | named volume `promtail_positions` | rebuildbar | nein | Dokumentierte Host-Observability-Ausnahme: `/var/run/docker.sock:/var/run/docker.sock:ro` und `/var/lib/docker/containers:ro`; keine Appdaten, nur Log-Discovery |
| `monitoring-node-exporter` | Host-Metriken fuer Prometheus | `monitoring/docker-compose.yml` | intern `:9100` | Host `/proc`, `/sys`, `/` read-only, Prometheus | kein kritischer Zustand | rebuildbar | nein | Host-Observability-Ausnahme mit read-only Rootfs/Proc/Sys-Mounts |
| `monitoring-cadvisor` | Container-Metriken fuer Prometheus | `monitoring/docker-compose.yml` | intern `:8080` | Docker/Host read-only Mounts, Prometheus | kein kritischer Zustand | rebuildbar | nein | Host-Observability-Ausnahme fuer Container-Metriken; keine direkten Ports |
| `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | Host-Port `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | 2026-05-31 effektiv auf `127.0.0.1:8181` gebunden, also nicht LAN-exponiert; `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; uebernimmt den bisherigen InfluxDB-Daten-/Token-Katalog; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test |
| `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | Host-Port `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | 2026-05-31 effektiv auf `127.0.0.1:8181` gebunden, also nicht LAN-exponiert; vor dem HA-Writer muss entschieden werden, ob `INFLUXDB_BIND_IP` auf eine LAN-IP geht oder HA gezielt ein gemeinsames internes Netz mit InfluxDB bekommt. `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test |
| `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket |
| `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts |
| `n8n` | Workflow-Automation; aktuell genutzt fuer Mail->LLM->Gitea-Issue (Inbox `Micha/mails`) | `apps/n8n/docker-compose.yml`, `apps/n8n/workflows/*.json` | `https://n8n.kaleschke.info` | Traefik (ohne pauschale Authelia, analog Komodo/Nextcloud), GMX IMAP, OpenAI API, Gitea API | `/mnt/user/appdata/n8n/data` (SQLite, Credentials, Workflows) | Tier 2, Borg + `n8n-data` (Credentials sind nur mit `N8N_ENCRYPTION_KEY` entschluesselbar) | ja, native Auth | Wegen Webhook-Endpunkten (`/webhook/*`) bewusst ohne `authelia@file`; eigene Login-/Owner-Auth bleibt Pflicht; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret, Verlust macht Credentials unbrauchbar. |
## Smart Home
| 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`; 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; 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
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
+9 -3
View File
@@ -124,14 +124,20 @@ Pflichtschritte beim Anlegen:
1. Stack in Komodo aus Gitea anlegen
2. `webhook_enabled` in Komodo aktivieren
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
5. einen Push oder Test-Delivery ausloesen und `last_status`/Komodo-Deploy pruefen
6. Ausnahmen explizit dokumentieren
4. Branch-Filter im Gitea-Hook auf den produktiven Branch setzen, aktuell `master`
5. Gitea-Hook gegen `http://komodo-core:9120/listener/github/stack/<stack-id>/deploy` pruefen
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.
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
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
+4 -4
View File
@@ -1,6 +1,6 @@
# Runbook: Komodo Bulk-Deploy schlaegt mit DNS `connection refused` fehl
Stand: 2026-06-10 · Typ: Runbook / ADR-light · Status: Sofortmassnahme empfohlen, noch nicht umgesetzt
Stand: 2026-06-11 · Typ: Runbook / ADR-light · Status: **Sofortmassnahme aktiv** (Host-DNS-Fallback gesetzt 2026-06-11 bzw. frueher)
## Symptom
@@ -19,12 +19,12 @@ Der Host nutzt **AdGuard Home als einzigen Resolver** (`/etc/resolv.conf` = nur
Es ist **kein** Webhook-, Auth- oder Docker-Hub-Rate-Limit-Problem: Webhooks authentifizieren sauber, `webhook_enabled=true`, Fehlerbild ist `connection refused` auf den eigenen DNS-Port direkt nach AdGuard-Recreate. Fuer den Pull-Pfad zaehlt der Docker-Daemon/Go-Resolver (iteriert ueber die `resolv.conf`-Server und springt bei Socket-Fehlern zum naechsten), nicht der glibc-Client.
## Sofortmassnahme (Schicht 1)
## Sofortmassnahme (Schicht 1) — umgesetzt
Unraid -> Settings -> Network Settings -> `eth0`:
- DNS server 1: `192.168.178.58` (AdGuard, bleibt)
- **DNS server 2: `192.168.178.1`** (FritzBox) -> Apply
- DNS server 1: `192.168.178.58` (AdGuard)
- **DNS server 2: `192.168.178.1`** (FritzBox) **gesetzt und aktiv** (Operator-Bestaetigung 2026-06-11; Apply-Button erfordert Docker-/VM-Stop, der gespeicherte Wert greift bereits ohne Re-Apply)
Damit ueberleben Registry-Pulls einen kurzen AdGuard-Ausfall via Resolver-Failover. Im Normalbetrieb wird weiter DNS1 (AdGuard) genutzt, der Filter bleibt aktiv.
+224
View File
@@ -0,0 +1,224 @@
# Smart-Home Bootstrap
Ziel: Den Stack `smart-home/` auf Kallilabcore initial startklar machen, ohne
Secrets oder UI-State ins Git zu schreiben.
## 1. Fachrepo auf dem Host bereitstellen
```sh
cd /mnt/user/services
git clone https://git.kaleschke.info/Micha/smart-home-kalli.git smart-home-kalli
cd smart-home-kalli
git checkout main
```
Der Home-Assistant-Container mountet daraus einzelne YAML-Dateien read-only nach
`/config`.
## 2. Home-Assistant-Appdata vorbereiten
```sh
mkdir -p /mnt/user/appdata/homeassistant
cp /mnt/user/services/smart-home-kalli/secrets-template/secrets.yaml.example \
/mnt/user/appdata/homeassistant/secrets.yaml
cp /mnt/user/services/smart-home-kalli/secrets-template/trusted_proxies.yaml.example \
/mnt/user/appdata/homeassistant/trusted_proxies.yaml
```
Danach `trusted_proxies.yaml` auf das echte Traefik-/`frontend_net`-Subnetz
anpassen:
```sh
docker network inspect frontend_net
```
## 3. Mosquitto vorbereiten
```sh
mkdir -p /mnt/user/appdata/mosquitto/config \
/mnt/user/appdata/mosquitto/data \
/mnt/user/appdata/mosquitto/log
docker run --rm -it \
-v /mnt/user/appdata/mosquitto/config:/mosquitto/external_config \
eclipse-mosquitto:2.0.22 \
mosquitto_passwd -c /mosquitto/external_config/passwordfile homeassistant
cat > /mnt/user/appdata/mosquitto/config/aclfile <<'EOF'
user homeassistant
topic readwrite #
EOF
```
Das initiale Passwort anschliessend in
`/mnt/user/appdata/homeassistant/secrets.yaml` eintragen. LAN-Port `1883` bleibt
in Phase 1 geschlossen.
## 4. Stack deployen
Komodo-Stack:
- Repo: `homelab-infra`
- Pfad: `smart-home/docker-compose.yml`
- 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:
```sh
docker ps --filter name=homeassistant
docker ps --filter name=smarthome-mosquitto
docker logs --tail=100 homeassistant
docker logs --tail=100 smarthome-mosquitto
```
## 5. Smoke-Test
- `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.
- MQTT-Broker-Smoke: `homeassistant`-User aus `secrets.yaml` kann gegen
`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. 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
`/mnt/user/appdata/homeassistant`, `/mnt/user/appdata/mosquitto` und den Clone
`/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 -1
View File
@@ -1,6 +1,6 @@
services:
postgresql17:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
container_name: postgresql17
restart: unless-stopped
+50
View File
@@ -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.
- 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
Prometheus wertet `monitoring/prometheus/alerts.yml` aus und sendet an `monitoring-alertmanager`.
+50 -16
View File
@@ -2,20 +2,28 @@ services:
prometheus:
image: prom/prometheus:v3.12.0@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
container_name: monitoring-prometheus
user: "0"
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --config.file=/etc/prometheus/config/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.enable-lifecycle
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
# Verzeichnis-Mount statt Einzeldatei: auf dem Unraid-FUSE-Share (/mnt/user)
# 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
networks:
- monitoring_net
- backend_net
expose:
- "9090"
secrets:
- source: dawarich_metrics_password
mode: 0444
security_opt:
- no-new-privileges:true
depends_on:
@@ -25,14 +33,17 @@ services:
- cadvisor
alertmanager:
image: prom/alertmanager:v0.32.2@sha256:b85533a2eb45865835315810315f6951331b2dbc8c93a6cf9a51e156a006a706
image: prom/alertmanager:v0.33.0@sha256:af26fbe4dd1886ac0efd7bd55cd9027da262e105b137a376522b7c14c3626e4a
container_name: monitoring-alertmanager
restart: unless-stopped
command:
- --config.file=/etc/alertmanager/alertmanager.yml
- --config.file=/etc/alertmanager/config/alertmanager.yml
- --storage.path=/alertmanager
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
networks:
- monitoring_net
@@ -42,7 +53,7 @@ services:
- no-new-privileges:true
alertmanager-ntfy-bridge:
image: python:3.14-alpine@sha256:5a824eb82cc75361f98611f3cfc5091ea33f10a6ccea4d4ebdabbc523b9a1614
image: python:3.14-alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92
container_name: monitoring-alertmanager-ntfy-bridge
restart: unless-stopped
dns:
@@ -54,7 +65,9 @@ services:
- python
- /app/bridge.py
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:
- monitoring_net
expose:
@@ -72,9 +85,11 @@ services:
dns:
- 172.23.0.3
command:
- --config.file=/etc/blackbox_exporter/blackbox.yml
- --config.file=/etc/blackbox_exporter/config/blackbox.yml
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:
- monitoring_net
- dns_net
@@ -88,9 +103,11 @@ services:
container_name: monitoring-loki
restart: unless-stopped
command:
- -config.file=/etc/loki/loki-config.yml
- -config.file=/etc/loki/config/loki-config.yml
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
networks:
- monitoring_net
@@ -104,9 +121,11 @@ services:
container_name: monitoring-promtail
restart: unless-stopped
command:
- -config.file=/etc/promtail/promtail-config.yml
- -config.file=/etc/promtail/config/promtail-config.yml
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
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
@@ -151,6 +170,7 @@ services:
- -c
- |
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
volumes:
- grafana_data:/var/lib/grafana
@@ -159,10 +179,12 @@ services:
networks:
- monitoring_net
- frontend_net
- backend_net
secrets:
- monitoring_grafana_admin_password
- monitoring_grafana_influxdb_token
- grafana_oidc_client_secret
- dawarich_grafana_ro_password
expose:
- "3000"
security_opt:
@@ -316,7 +338,7 @@ services:
- no-new-privileges:true
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
restart: unless-stopped
command:
@@ -337,7 +359,7 @@ services:
- no-new-privileges:true
influxdb3-core:
image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
image: influxdb:3.10.0-core@sha256:b3e577f38c19963597170d8850a3a7f77af8f0cfa866c64cd13e5de0f238e114
container_name: monitoring-influxdb3-core
user: "0"
restart: unless-stopped
@@ -351,6 +373,12 @@ services:
- --data-dir=/var/lib/influxdb3/data
- --plugin-dir=/var/lib/influxdb3/plugins
- --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:
- /mnt/user/appdata/influxdb3/data:/var/lib/influxdb3/data
- /mnt/user/appdata/influxdb3/plugins:/var/lib/influxdb3/plugins
@@ -370,6 +398,8 @@ networks:
driver: bridge
frontend_net:
external: true
backend_net:
external: true
dns_net:
external: true
@@ -389,3 +419,7 @@ secrets:
file: /mnt/user/appdata/secrets/grafana_oidc_client_secret
influxdb3_admin_token:
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
+355
View File
@@ -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": ""
}
+204
View File
@@ -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
secureJsonData:
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
+72
View File
@@ -131,6 +131,78 @@ groups:
summary: "Latest Borg backup completed with warnings"
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
expr: homelab_critical_container_running == 0
for: 5m
+15 -1
View File
@@ -5,7 +5,7 @@ global:
site: kallilabcore
rule_files:
- /etc/prometheus/alerts.yml
- /etc/prometheus/config/alerts.yml
alerting:
alertmanagers:
@@ -36,6 +36,20 @@ scrape_configs:
- targets:
- 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
metrics_path: /probe
params:
+21 -3
View File
@@ -1,6 +1,6 @@
# 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.
@@ -38,7 +38,7 @@ The Unraid flash configuration archive is intentional as well and must be treate
| Traefik | file data | `/local/appdata/traefik` |
| ntfy | file data | `/local/appdata/ntfy` |
| 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` |
| 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` |
@@ -48,7 +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` |
| 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` |
| Hermes Agent | file data + SSH key | `/local/appdata/hermes-agent/data`, `/local/secrets/hermes_runner_id_ed25519` |
| 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` |
| 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` |
| ESPHome (planned) | Fachrepo + optional build/runtime cache | `/local/services/smart-home-kalli/esphome`, optional `/local/appdata/esphome` |
| 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 |
## Open Decisions and Coverage Gaps
@@ -67,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.
### 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
### Shared PostgreSQL (`postgresql17`, runtime PostgreSQL 18)
@@ -85,8 +101,10 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
- Komodo MongoDB
- 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`
- 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
## Explicitly Not Backed Up as Raw Live DB Files
+10 -1
View File
@@ -14,11 +14,20 @@
/local/appdata/traefik
/local/appdata/ntfy
/local/appdata/paperless-gpt
/local/appdata/tailscale
/local/appdata/adguard/conf
/local/appdata/borg-ui/data
/local/appdata/komodo/periphery
/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/smart-home-kalli
/local/services/stacks
/local/services/posture-check
/local/appdata/homeassistant
/local/appdata/mosquitto/config
/local/appdata/mosquitto/data
+1 -1
View File
@@ -1,6 +1,6 @@
services:
borg-ui:
image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f
image: ainullcode/borg-ui@sha256:e51b3d2e6cb38d1ba127ef60ba442c1e157965327196e6f7afb69f30c0ba99d1
container_name: borg-ui
restart: unless-stopped
security_opt:
+1
View File
@@ -325,6 +325,7 @@ main() {
# 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 "/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
dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz"
+22
View File
@@ -22,12 +22,31 @@ case "${DUMP_ROOT:-}" in
;;
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() {
local step="$1"
local message="$2"
if [ -x "$NTFY_SCRIPT" ]; then
"$NTFY_SCRIPT" "$NTFY_TOPIC" "Borg pre-hook failed: $step" "$message" high || true
fi
hc_ping "/fail"
}
run_step() {
@@ -44,6 +63,8 @@ run_step() {
fi
}
hc_ping "/start"
echo "[pre-borg] Running posture-check"
if "$POSTURE_CHECK"; then
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"
echo "[pre-borg] All pre-flight checks passed"
hc_ping
+1 -1
View File
@@ -1,6 +1,6 @@
services:
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
restart: unless-stopped
security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
filebrowser:
image: filebrowser/filebrowser:v2.63.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76
image: filebrowser/filebrowser:v2.63.15@sha256:9805b21cf910f3ef6f4a1c8f441f1dd6cc4197136f9541fe2a1ab6d050706e4b
container_name: filebrowser
restart: unless-stopped
security_opt:
+182
View File
@@ -0,0 +1,182 @@
/* ============================================================
KalliLab "Neon Ops v2" - Glance Custom CSS
Rotierende Akzentfarben pro Widget, Gradient-Zahlen,
animierte Header-Linien, kraeftige Glows
============================================================ */
/* --- Akzentfarben rotieren ueber die Widgets --- */
.widget { --kl-accent: 205 100% 60%; }
.widget:nth-of-type(4n+2) { --kl-accent: 172 95% 48%; }
.widget:nth-of-type(4n+3) { --kl-accent: 38 100% 55%; }
.widget:nth-of-type(4n) { --kl-accent: 145 85% 50%; }
/* --- Seiten-Hintergrund: kraeftigere Farb-Glows --- */
body {
background:
radial-gradient(1300px 700px at 85% -10%, hsla(205, 100%, 55%, 0.13), transparent 60%),
radial-gradient(1000px 600px at -10% 25%, hsla(172, 95%, 45%, 0.09), transparent 55%),
radial-gradient(900px 700px at 50% 115%, hsla(38, 100%, 50%, 0.07), transparent 60%),
var(--color-background);
background-attachment: fixed;
}
/* --- Widgets als Karten mit Akzentrand --- */
.widget {
background: linear-gradient(
160deg,
hsla(220, 30%, 100%, 0.05),
hsla(220, 30%, 100%, 0.015)
);
border: 1px solid hsl(var(--kl-accent) / 0.18);
border-radius: 14px;
padding: 14px 16px;
box-shadow:
0 10px 30px hsla(220, 60%, 3%, 0.4),
0 0 24px hsl(var(--kl-accent) / 0.06),
inset 0 1px 0 hsla(220, 40%, 90%, 0.05);
transition: border-color 0.2s ease, box-shadow 0.25s ease;
}
.widget:hover {
border-color: hsl(var(--kl-accent) / 0.55);
box-shadow:
0 12px 36px hsla(220, 60%, 3%, 0.45),
0 0 36px hsl(var(--kl-accent) / 0.16),
inset 0 1px 0 hsla(220, 40%, 90%, 0.07);
}
/* Widgets in Gruppen/Tabs nicht doppelt einrahmen */
.widget .widget {
background: none;
border: none;
border-radius: 0;
padding: 0;
box-shadow: none;
}
/* --- Widget-Titel: animierte Farbverlaufs-Linie in Akzentfarbe --- */
.widget-header {
letter-spacing: 0.14em;
position: relative;
padding-bottom: 7px;
margin-bottom: 4px;
color: hsl(var(--kl-accent) / 0.85);
}
.widget-header::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 64px;
height: 2px;
border-radius: 2px;
background: linear-gradient(
90deg,
hsl(var(--kl-accent)),
hsl(var(--kl-accent) / 0.25),
hsl(var(--kl-accent))
);
background-size: 200% 100%;
animation: kl-shimmer 4s linear infinite;
}
@keyframes kl-shimmer {
0% { background-position: 0% 0; }
100% { background-position: 200% 0; }
}
/* --- Grosse Zahlen: Gradient-Text + Glow --- */
.color-highlight.size-h2,
.color-highlight.size-h3,
.color-primary.size-h2,
.color-primary.size-h3 {
background: linear-gradient(
120deg,
hsl(var(--kl-accent)),
hsl(var(--kl-accent) / 0.55) 60%,
hsl(210, 30%, 95%)
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 14px hsl(var(--kl-accent) / 0.35));
}
.color-positive {
text-shadow: 0 0 16px hsla(150, 95%, 45%, 0.45);
}
.color-negative {
text-shadow: 0 0 16px hsla(350, 95%, 58%, 0.45);
}
/* --- Status-Punkte leuchten --- */
.monitor-site-status-icon-compact,
.monitor-site-status-icon {
filter: drop-shadow(0 0 7px hsla(150, 95%, 45%, 0.55));
}
/* --- Navigation --- */
.nav-item.nav-item-current {
text-shadow: 0 0 18px hsla(212, 100%, 60%, 0.6);
}
/* --- Suchleiste --- */
.search {
border: 1px solid hsla(212, 90%, 65%, 0.2);
border-radius: 12px;
background: hsla(220, 30%, 100%, 0.04);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.search:focus-within {
border-color: hsla(212, 100%, 60%, 0.55);
box-shadow: 0 0 0 3px hsla(212, 100%, 55%, 0.15), 0 0 28px hsla(212, 100%, 55%, 0.12);
}
/* --- Server-Stats: Balken rund, gradient, glow --- */
.progress-bar {
border: none;
height: 13px;
border-radius: 999px;
background: hsla(220, 30%, 60%, 0.12);
box-shadow: inset 0 1px 3px hsla(220, 60%, 3%, 0.5);
overflow: hidden;
}
.progress-value {
border-radius: 999px;
background: linear-gradient(90deg, hsl(205, 100%, 55%), hsl(172, 95%, 48%));
box-shadow: 0 0 10px hsla(205, 100%, 55%, 0.35);
}
.progress-value-notice {
background: linear-gradient(90deg, hsl(38, 100%, 55%), hsl(355, 90%, 60%));
box-shadow: 0 0 12px hsla(355, 90%, 58%, 0.45);
}
/* --- Feinschliff --- */
::selection {
background: hsla(212, 100%, 50%, 0.35);
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: hsla(220, 30%, 50%, 0.25);
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
background: hsla(212, 80%, 55%, 0.45);
}
/* Reduzierte Bewegung respektieren */
@media (prefers-reduced-motion: reduce) {
.widget-header::after {
animation: none;
}
}
+287
View File
@@ -0,0 +1,287 @@
traefik:
name: Traefik
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
url: https://traefik.kaleschke.info
description: Reverse Proxy
category: core
hide: false
gitea:
name: Gitea
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
url: https://git.kaleschke.info
description: GitOps Origin
category: core
hide: false
authelia:
name: Authelia
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
url: https://auth.kaleschke.info
description: ForwardAuth
category: core
hide: false
vaultwarden:
name: Vaultwarden
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
url: https://vault.kaleschke.info
description: Password Vault
category: core
hide: false
postgresql17:
name: PostgreSQL 18
icon: si:postgresql
description: Shared DB
category: core
hide: false
Redis:
name: Redis
icon: si:redis
description: Shared Cache
category: core
hide: false
adguard:
name: AdGuard
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
url: http://192.168.178.58:8082
description: DNS Filter
category: network
hide: false
unbound:
name: Unbound
icon: mdi:dns
description: Upstream Resolver
category: network
hide: false
ddns-updater:
name: DDNS Updater
icon: mdi:cloud-sync
description: Cloudflare DNS
category: network
hide: false
paperless-ngx:
name: Paperless-ngx
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
url: https://paperless.kaleschke.info
description: Dokumente
category: apps
hide: false
paperless-gpt:
name: Paperless-GPT
icon: mdi:robot
url: https://paperless-gpt.kaleschke.info
description: Dokumenten-KI
category: apps
hide: false
immich_server:
name: Immich
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
url: https://immich.kaleschke.info
description: Fotos und Videos
category: apps
id: immich
hide: false
immich_postgres:
name: DB
parent: immich
category: apps
hide: false
immich_redis:
name: Redis
parent: immich
category: apps
hide: false
immich_machine_learning:
name: ML
parent: immich
category: apps
hide: false
mealie:
name: Mealie
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
url: https://mealie.kaleschke.info
description: Rezepte
category: apps
id: mealie
hide: false
mealie-postgres:
name: DB
parent: mealie
category: apps
hide: false
nextcloud:
name: Nextcloud
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
url: https://cloud.kaleschke.info
description: Dateien und Sync
category: apps
id: nextcloud
hide: false
nextcloud-postgres:
name: DB
parent: nextcloud
category: apps
hide: false
nextcloud-redis:
name: Redis
parent: nextcloud
category: apps
hide: false
mail-archiver:
name: Mail Archiver
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
url: https://mail.kaleschke.info
description: Mail-Archiv
category: apps
hide: false
ntfy:
name: ntfy
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
url: https://ntfy.kaleschke.info
description: Push Alerts
category: apps
hide: false
bentopdf:
name: BentoPDF
icon: mdi:file-pdf-box
url: https://pdf.kaleschke.info
description: PDF Tools
category: apps
hide: false
glance:
name: Glance
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
url: https://glance.kaleschke.info
description: Homelab Uebersicht
category: ops
hide: false
glance-docker-socket-proxy:
name: Glance Socket Proxy
icon: si:docker
description: Read-only Docker API
category: ops
hide: false
monitoring-grafana:
name: Monitoring Grafana
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
url: https://monitoring.kaleschke.info
description: Observability UI
category: ops
id: monitoring
hide: false
monitoring-prometheus:
name: Prometheus
parent: monitoring
category: ops
hide: false
monitoring-loki:
name: Loki
parent: monitoring
category: ops
hide: false
monitoring-promtail:
name: Promtail
parent: monitoring
category: ops
hide: false
monitoring-alertmanager:
name: Alertmanager
parent: monitoring
category: ops
hide: false
monitoring-alertmanager-ntfy-bridge:
name: ntfy Bridge
parent: monitoring
category: ops
hide: false
monitoring-blackbox-exporter:
name: Blackbox
parent: monitoring
category: ops
hide: false
monitoring-node-exporter:
name: Node Exporter
parent: monitoring
category: ops
hide: false
monitoring-cadvisor:
name: cAdvisor
parent: monitoring
category: ops
hide: false
monitoring-influxdb3-core:
name: InfluxDB 3
parent: monitoring
category: ops
hide: false
glances:
name: Glances
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
url: https://glances.kaleschke.info
description: Host-Monitoring
category: ops
hide: false
scrutiny:
name: Scrutiny
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
url: https://scrutiny.kaleschke.info
description: SMART
category: ops
hide: false
speedtest-tracker:
name: Speedtest
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
url: https://speedtest.kaleschke.info
description: WAN-Messung
category: ops
hide: false
filebrowser:
name: Filebrowser
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
url: https://files.kaleschke.info
description: Dateizugriff
category: ops
hide: false
code-server:
name: code-server
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
url: https://code.kaleschke.info
description: Web IDE
category: ops
hide: false
borg-ui:
name: Borg UI
icon: mdi:archive-sync
url: https://borg.kaleschke.info
description: Backup und Restore
category: ops
hide: false
hermes-dashboard:
name: Hermes
icon: mdi:shield-sparkles
url: https://hermes.kaleschke.info
description: Ops Agent UI
category: ops
id: hermes
hide: false
hermes-gateway:
name: Gateway
parent: hermes
category: ops
hide: false
komodo-core:
name: Komodo
icon: sh:komodo
url: https://komodo.kaleschke.info
description: Stack Manager
category: ops
id: komodo
hide: false
komodo-mongo:
name: Mongo
parent: komodo
category: ops
hide: false
komodo-periphery:
name: Periphery
parent: komodo
category: ops
hide: false
+40 -861
View File
@@ -1,5 +1,6 @@
server:
proxied: true
assets-path: /app/assets
branding:
app-name: KalliLab Dashboard
@@ -7,867 +8,45 @@ branding:
hide-footer: true
theme:
background-color: 210 20 13
primary-color: 212 100 50
positive-color: 140 70 40
negative-color: 4 78 57
contrast-multiplier: 1.25
text-saturation-multiplier: 0.9
background-color: 222 14 8
primary-color: 205 100 58
positive-color: 150 80 45
negative-color: 355 90 60
contrast-multiplier: 1.3
text-saturation-multiplier: 0.5
disable-picker: false
custom-css-file: /assets/custom.css
presets:
catppuccin-mocha:
background-color: 240 21 15
primary-color: 217 92 83
positive-color: 115 54 76
negative-color: 347 70 65
contrast-multiplier: 1.2
gruvbox-dark:
background-color: 0 0 16
primary-color: 43 59 81
positive-color: 61 66 44
negative-color: 6 96 59
kallilab-light:
light: true
background-color: 220 23 95
primary-color: 212 100 35
positive-color: 140 70 30
negative-color: 0 70 45
synthwave:
background-color: 265 35 10
primary-color: 320 100 65
positive-color: 175 100 50
negative-color: 0 100 65
contrast-multiplier: 1.3
matrix:
background-color: 130 25 6
primary-color: 130 100 55
positive-color: 130 100 45
negative-color: 35 100 55
contrast-multiplier: 1.25
text-saturation-multiplier: 1.2
pages:
- name: Home
slug: home
width: wide
head-widgets:
- type: search
search-engine: duckduckgo
new-tab: true
autofocus: true
placeholder: Suche im Web oder springe per Bang...
bangs:
- title: Gitea
shortcut: "!git"
url: https://git.kaleschke.info/explore/repos?q={QUERY}
- title: Paperless
shortcut: "!doc"
url: https://paperless.kaleschke.info/documents?query={QUERY}
- title: Nextcloud
shortcut: "!cloud"
url: https://cloud.kaleschke.info/apps/files/?dir=/{QUERY}
- title: Komodo
shortcut: "!komodo"
url: https://komodo.kaleschke.info
columns:
- size: small
widgets:
- type: group
widgets:
- type: custom-api
title: Day
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $elapsedSeconds := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $dayProgress := div (mul $elapsedSeconds 100.0) 86400.0 }}
{{ $gradient := "#70a1ff" }}
{{ if gt $dayProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $dayProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $dayProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $dayProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $dayProgress }}% des Tages sind vorbei</div>
</div>
- type: custom-api
title: Month
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $month := $localTime.Month }}
{{ $daysInMonth := 31 }}
{{ if eq $month 2 }}{{ $daysInMonth = 28 }}{{ end }}
{{ if or (eq $month 4) (eq $month 6) (eq $month 9) (eq $month 11) }}{{ $daysInMonth = 30 }}{{ end }}
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $daysElapsed := add (sub $localTime.Day 1) (div $secondsToday 86400.0) }}
{{ $monthProgress := mul (div $daysElapsed $daysInMonth) 100.0 }}
{{ $gradient := "#70a1ff" }}
{{ if gt $monthProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $monthProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $monthProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $monthProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $monthProgress }}% des Monats sind vorbei</div>
</div>
- type: custom-api
title: Year
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $secondsElapsed := add (mul (sub $localTime.YearDay 1) 86400) $secondsToday }}
{{ $yearProgress := div (mul $secondsElapsed 100.0) (mul 365 86400) }}
{{ $gradient := "#70a1ff" }}
{{ if gt $yearProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $yearProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $yearProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $yearProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $yearProgress }}% des Jahres sind vorbei</div>
</div>
- type: clock
hour-format: 24h
show-progress: true
timezones:
- timezone: Europe/Berlin
label: Berlin
- timezone: UTC
label: UTC
- type: calendar
first-day-of-week: monday
- type: bookmarks
title: Direkte Einstiege
groups:
- title: Core
color: 212 100 50
links:
- title: Komodo
url: https://komodo.kaleschke.info
icon: sh:komodo
- title: Gitea
url: https://git.kaleschke.info
icon: si:gitea
- title: Monitoring
url: https://monitoring.kaleschke.info
icon: si:grafana
- title: Ops
color: 45 70 55
links:
- title: Borg
url: https://borg.kaleschke.info
icon: mdi:archive
- title: Glances
url: https://glances.kaleschke.info
icon: sh:glances
- title: Scrutiny
url: https://scrutiny.kaleschke.info
icon: sh:scrutiny
- size: full
widgets:
- type: server-stats
title: Server Stats
servers:
- type: local
name: Kallilabcore
hide-mountpoints-by-default: false
- type: group
widgets:
- type: custom-api
title: Immich
title-url: https://immich.kaleschke.info
cache: 10m
url: http://immich_server:2283/api/server/statistics
headers:
x-api-key: ${GLANCE_IMMICH_API_KEY}
subrequests:
storage:
url: http://immich_server:2283/api/server/storage
headers:
x-api-key: ${GLANCE_IMMICH_API_KEY}
template: |
{{ $photos := .JSON.Int "photos" }}
{{ $videos := .JSON.Int "videos" }}
{{ $usageGiB := div (toFloat (.JSON.Int "usage")) 1073741824.0 }}
{{ $storage := .Subrequest "storage" }}
{{ $storageOK := and (ge $storage.Response.StatusCode 200) (le $storage.Response.StatusCode 299) }}
{{ $percentage := 0.0 }}
{{ if $storageOK }}{{ $percentage = $storage.JSON.Float "diskUsagePercentage" }}{{ end }}
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ $photos | formatNumber }}</div>
<div class="size-h6 uppercase">Fotos</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $videos | formatNumber }}</div>
<div class="size-h6 uppercase">Videos</div>
</div>
<div>
<div class="color-highlight size-h3">{{ printf "%.0f" $usageGiB }} GiB</div>
<div class="size-h6 uppercase">Medien</div>
</div>
</div>
<div style="height: 8px; margin-top: 14px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--color-text-subdue) 22%, transparent);">
<div style="height: 100%; width: {{ if $storageOK }}{{ printf "%.1f" $percentage }}%{{ else }}0%{{ end }}; border-radius: 999px; background: var(--color-primary);"></div>
</div>
<div class="size-h6 color-subdue" style="margin-top: 8px;">{{ if $storageOK }}{{ printf "%.1f" $percentage }}% Speicher belegt{{ else }}Speicher API nicht verfuegbar{{ end }}</div>
- type: monitor
title: Homelab Status
cache: 1m
sites:
- title: AdGuard Home
url: http://192.168.178.58:8082
check-url: http://adguard
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Authelia
url: https://auth.kaleschke.info
check-url: http://authelia:9091/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Gitea
url: https://git.kaleschke.info
check-url: http://gitea:3000/api/healthz
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Traefik
url: https://traefik.kaleschke.info
check-url: http://traefik:8082/metrics
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Vaultwarden
url: https://vault.kaleschke.info
check-url: http://vaultwarden/alive
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Komodo
url: https://komodo.kaleschke.info
check-url: http://komodo-core:9120
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Paperless-ngx
url: https://paperless.kaleschke.info
check-url: http://paperless-ngx:8000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Paperless-GPT
url: https://paperless-gpt.kaleschke.info
check-url: http://paperless-gpt:8080
icon: mdi:robot
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Immich
url: https://immich.kaleschke.info
check-url: http://immich_server:2283/api/server/ping
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Mealie
url: https://mealie.kaleschke.info
check-url: http://mealie:9000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Nextcloud
url: https://cloud.kaleschke.info
check-url: http://nextcloud/status.php
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: ntfy
url: https://ntfy.kaleschke.info
check-url: http://ntfy/v1/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Mail Archiver
url: https://mail.kaleschke.info
check-url: http://mail-archiver:5000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: BentoPDF
url: https://pdf.kaleschke.info
check-url: http://bentopdf:8080
icon: mdi:file-pdf-box
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glance
url: https://glance.kaleschke.info
check-url: http://glance:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Monitoring Grafana
url: https://monitoring.kaleschke.info
check-url: http://monitoring-grafana:3000/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glances
url: https://glances.kaleschke.info
check-url: http://glances:61208
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Scrutiny
url: https://scrutiny.kaleschke.info
check-url: http://scrutiny:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Speedtest Tracker
url: https://speedtest.kaleschke.info
check-url: http://speedtest-tracker
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Filebrowser
url: https://files.kaleschke.info
check-url: http://filebrowser
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: code-server
url: https://code.kaleschke.info
check-url: http://code-server:8443
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Borg UI
url: https://borg.kaleschke.info
check-url: http://borg-ui:8081
icon: mdi:archive-sync
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- size: small
widgets:
- type: custom-api
title: Internet
title-url: https://speedtest.kaleschke.info
cache: 1h
url: http://speedtest-tracker/api/v1/results/latest
headers:
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
Accept: application/json
template: |
{{ $ip := .JSON.String "external_ip" }}
{{ if eq $ip "" }}{{ $ip = .JSON.String "data.interface.externalIp" }}{{ end }}
{{ $isp := .JSON.String "isp" }}
{{ if eq $isp "" }}{{ $isp = .JSON.String "data.isp" }}{{ end }}
{{ $server := .JSON.String "server_name" }}
{{ if eq $server "" }}{{ $server = .JSON.String "data.server_name" }}{{ end }}
<div style="display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;">
<div class="color-primary size-h2" style="font-weight: 700;">{{ if ne $ip "" }}{{ $ip }}{{ else }}WAN online{{ end }}</div>
<div class="size-h5 color-highlight">Speedtest Tracker</div>
<div class="size-h6 color-subdue" style="font-style: italic;">{{ if ne $isp "" }}{{ $isp }}{{ else }}{{ $server }}{{ end }}</div>
</div>
- type: custom-api
title: Internet Speed
title-url: https://speedtest.kaleschke.info
cache: 1h
url: http://speedtest-tracker/api/v1/results/latest
headers:
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
Accept: application/json
subrequests:
stats:
url: http://speedtest-tracker/api/v1/stats
headers:
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
Accept: application/json
template: |
{{ $stats := .Subrequest "stats" }}
{{ $download := .JSON.Float "download" }}
{{ if eq $download 0.0 }}{{ $download = .JSON.Float "data.download" }}{{ end }}
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "download_bits") 1000000.0 }}{{ end }}
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "data.download_bits") 1000000.0 }}{{ end }}
{{ $upload := .JSON.Float "upload" }}
{{ if eq $upload 0.0 }}{{ $upload = .JSON.Float "data.upload" }}{{ end }}
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "upload_bits") 1000000.0 }}{{ end }}
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "data.upload_bits") 1000000.0 }}{{ end }}
{{ $ping := .JSON.Float "ping" }}
{{ if eq $ping 0.0 }}{{ $ping = .JSON.Float "data.ping" }}{{ end }}
{{ $downloadAvg := $stats.JSON.Float "avg_download" }}
{{ if eq $downloadAvg 0.0 }}{{ $downloadAvg = $stats.JSON.Float "data.download.avg" }}{{ end }}
{{ if eq $downloadAvg 0.0 }}{{ $downloadAvg = div ($stats.JSON.Float "data.download.avg_bits") 1000000.0 }}{{ end }}
{{ $uploadAvg := $stats.JSON.Float "avg_upload" }}
{{ if eq $uploadAvg 0.0 }}{{ $uploadAvg = $stats.JSON.Float "data.upload.avg" }}{{ end }}
{{ if eq $uploadAvg 0.0 }}{{ $uploadAvg = div ($stats.JSON.Float "data.upload.avg_bits") 1000000.0 }}{{ end }}
{{ $pingAvg := $stats.JSON.Float "avg_ping" }}
{{ if eq $pingAvg 0.0 }}{{ $pingAvg = $stats.JSON.Float "data.ping.avg" }}{{ end }}
{{ $downloadChange := percentChange $downloadAvg $download }}
{{ $uploadChange := percentChange $uploadAvg $upload }}
{{ $pingChange := percentChange $pingAvg $ping }}
<div class="flex justify-between text-center margin-block-3">
<div>
<div class="size-small {{ if lt $downloadChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $downloadChange }}</div>
<div class="color-highlight size-h3">{{ printf "%.1f" $download }}</div>
<div class="size-h6 color-subdue">DOWNLOAD</div>
</div>
<div>
<div class="size-small {{ if lt $uploadChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $uploadChange }}</div>
<div class="color-highlight size-h3">{{ printf "%.1f" $upload }}</div>
<div class="size-h6 color-subdue">UPLOAD</div>
</div>
<div>
<div class="size-small {{ if gt $pingChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $pingChange }}</div>
<div class="color-highlight size-h3">{{ printf "%.0f ms" $ping }}</div>
<div class="size-h6 color-subdue">PING</div>
</div>
</div>
- type: dns-stats
title: DNS Stats
service: adguard
url: http://adguard
username: ${GLANCE_ADGUARD_USERNAME}
password: ${GLANCE_ADGUARD_PASSWORD}
- type: monitor
title: DNS und VPN
cache: 1m
sites:
- title: AdGuard Home
url: http://192.168.178.58:8082
check-url: http://adguard
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Traefik
url: https://traefik.kaleschke.info
check-url: http://traefik:8082/metrics
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- type: docker-containers
title: Network Container
category: network
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: &containers
traefik:
name: Traefik
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
url: https://traefik.kaleschke.info
description: Reverse Proxy
category: core
hide: false
gitea:
name: Gitea
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
url: https://git.kaleschke.info
description: GitOps Origin
category: core
hide: false
authelia:
name: Authelia
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
url: https://auth.kaleschke.info
description: ForwardAuth
category: core
hide: false
vaultwarden:
name: Vaultwarden
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
url: https://vault.kaleschke.info
description: Password Vault
category: core
hide: false
postgresql17:
name: PostgreSQL 18
icon: si:postgresql
description: Shared DB
category: core
hide: false
Redis:
name: Redis
icon: si:redis
description: Shared Cache
category: core
hide: false
adguard:
name: AdGuard
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
url: http://192.168.178.58:8082
description: DNS Filter
category: network
hide: false
unbound:
name: Unbound
icon: mdi:dns
description: Upstream Resolver
category: network
hide: false
ddns-updater:
name: DDNS Updater
icon: mdi:cloud-sync
description: Cloudflare DNS
category: network
hide: false
paperless-ngx:
name: Paperless-ngx
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
url: https://paperless.kaleschke.info
description: Dokumente
category: apps
hide: false
paperless-gpt:
name: Paperless-GPT
icon: mdi:robot
url: https://paperless-gpt.kaleschke.info
description: Dokumenten-KI
category: apps
hide: false
immich_server:
name: Immich
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
url: https://immich.kaleschke.info
description: Fotos und Videos
category: apps
id: immich
hide: false
immich_postgres:
name: DB
parent: immich
category: apps
hide: false
immich_redis:
name: Redis
parent: immich
category: apps
hide: false
immich_machine_learning:
name: ML
parent: immich
category: apps
hide: false
mealie:
name: Mealie
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
url: https://mealie.kaleschke.info
description: Rezepte
category: apps
id: mealie
hide: false
mealie-postgres:
name: DB
parent: mealie
category: apps
hide: false
nextcloud:
name: Nextcloud
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
url: https://cloud.kaleschke.info
description: Dateien und Sync
category: apps
id: nextcloud
hide: false
nextcloud-postgres:
name: DB
parent: nextcloud
category: apps
hide: false
nextcloud-redis:
name: Redis
parent: nextcloud
category: apps
hide: false
mail-archiver:
name: Mail Archiver
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
url: https://mail.kaleschke.info
description: Mail-Archiv
category: apps
hide: false
ntfy:
name: ntfy
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
url: https://ntfy.kaleschke.info
description: Push Alerts
category: apps
hide: false
bentopdf:
name: BentoPDF
icon: mdi:file-pdf-box
url: https://pdf.kaleschke.info
description: PDF Tools
category: apps
hide: false
glance:
name: Glance
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
url: https://glance.kaleschke.info
description: Homelab Uebersicht
category: ops
hide: false
glance-docker-socket-proxy:
name: Glance Socket Proxy
icon: si:docker
description: Read-only Docker API
category: ops
hide: false
monitoring-grafana:
name: Monitoring Grafana
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
url: https://monitoring.kaleschke.info
description: Observability UI
category: ops
id: monitoring
hide: false
monitoring-prometheus:
name: Prometheus
parent: monitoring
category: ops
hide: false
monitoring-loki:
name: Loki
parent: monitoring
category: ops
hide: false
monitoring-promtail:
name: Promtail
parent: monitoring
category: ops
hide: false
monitoring-alertmanager:
name: Alertmanager
parent: monitoring
category: ops
hide: false
monitoring-alertmanager-ntfy-bridge:
name: ntfy Bridge
parent: monitoring
category: ops
hide: false
monitoring-blackbox-exporter:
name: Blackbox
parent: monitoring
category: ops
hide: false
monitoring-node-exporter:
name: Node Exporter
parent: monitoring
category: ops
hide: false
monitoring-cadvisor:
name: cAdvisor
parent: monitoring
category: ops
hide: false
monitoring-influxdb3-core:
name: InfluxDB 3
parent: monitoring
category: ops
hide: false
glances:
name: Glances
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
url: https://glances.kaleschke.info
description: Host-Monitoring
category: ops
hide: false
scrutiny:
name: Scrutiny
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
url: https://scrutiny.kaleschke.info
description: SMART
category: ops
hide: false
speedtest-tracker:
name: Speedtest
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
url: https://speedtest.kaleschke.info
description: WAN-Messung
category: ops
hide: false
filebrowser:
name: Filebrowser
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
url: https://files.kaleschke.info
description: Dateizugriff
category: ops
hide: false
code-server:
name: code-server
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
url: https://code.kaleschke.info
description: Web IDE
category: ops
hide: false
borg-ui:
name: Borg UI
icon: mdi:archive-sync
url: https://borg.kaleschke.info
description: Backup und Restore
category: ops
hide: false
hermes-dashboard:
name: Hermes
icon: mdi:shield-sparkles
url: https://hermes.kaleschke.info
description: Ops Agent UI
category: ops
id: hermes
hide: false
hermes-gateway:
name: Gateway
parent: hermes
category: ops
hide: false
komodo-core:
name: Komodo
icon: sh:komodo
url: https://komodo.kaleschke.info
description: Stack Manager
category: ops
id: komodo
hide: false
komodo-mongo:
name: Mongo
parent: komodo
category: ops
hide: false
komodo-periphery:
name: Periphery
parent: komodo
category: ops
hide: false
- type: docker-containers
title: App Container
category: apps
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: *containers
- type: docker-containers
title: Ops Container
category: ops
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: *containers
- name: Infrastructure and Media
slug: infrastructure
width: wide
columns:
- size: small
widgets:
- type: bookmarks
title: Core
groups:
- title: Control Plane
color: 212 100 50
links:
- title: Komodo
url: https://komodo.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
- title: Gitea
url: https://git.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
- title: Traefik
url: https://traefik.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
- title: Authelia
url: https://auth.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
- type: bookmarks
title: Media und Apps
groups:
- title: Apps
color: 140 70 40
links:
- title: Immich
url: https://immich.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
- title: Paperless
url: https://paperless.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
- title: Nextcloud
url: https://cloud.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
- title: Mealie
url: https://mealie.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
- size: full
widgets:
- type: monitor
title: Platform Checks
cache: 1m
sites:
- title: Gitea
url: https://git.kaleschke.info
check-url: http://gitea:3000/api/healthz
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Monitoring Grafana
url: https://monitoring.kaleschke.info
check-url: http://monitoring-grafana:3000/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glance
url: https://glance.kaleschke.info
check-url: http://glance:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Immich
url: https://immich.kaleschke.info
check-url: http://immich_server:2283/api/server/ping
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Paperless-ngx
url: https://paperless.kaleschke.info
check-url: http://paperless-ngx:8000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Nextcloud
url: https://cloud.kaleschke.info
check-url: http://nextcloud/status.php
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- type: docker-containers
title: Core Container
category: core
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: *containers
- type: docker-containers
title: App Container
category: apps
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: *containers
- type: docker-containers
title: Ops Container
category: ops
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers: *containers
- size: small
widgets:
- type: bookmarks
title: Ops
groups:
- title: Tools
color: 4 78 57
links:
- title: Glances
url: https://glances.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
- title: Scrutiny
url: https://scrutiny.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
- title: Speedtest
url: https://speedtest.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
$include: pages.yml
+596
View File
@@ -0,0 +1,596 @@
- name: Home
slug: home
width: wide
head-widgets:
- type: search
search-engine: duckduckgo
new-tab: true
autofocus: true
placeholder: Suche im Web oder springe per Bang...
bangs:
- title: Gitea
shortcut: "!git"
url: https://git.kaleschke.info/explore/repos?q={QUERY}
- title: Paperless
shortcut: "!doc"
url: https://paperless.kaleschke.info/documents?query={QUERY}
- title: Nextcloud
shortcut: "!cloud"
url: https://cloud.kaleschke.info/apps/files/?dir=/{QUERY}
- title: Komodo
shortcut: "!komodo"
url: https://komodo.kaleschke.info
- title: Immich
shortcut: "!foto"
url: https://immich.kaleschke.info/search?query={QUERY}
- title: Mealie
shortcut: "!rezept"
url: https://mealie.kaleschke.info/g/home/?search={QUERY}
columns:
- size: small
widgets:
- type: group
widgets:
- type: custom-api
title: Day
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $elapsedSeconds := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $dayProgress := div (mul $elapsedSeconds 100.0) 86400.0 }}
{{ $gradient := "#70a1ff" }}
{{ if gt $dayProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $dayProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $dayProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $dayProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $dayProgress }}% des Tages sind vorbei</div>
</div>
- type: custom-api
title: Month
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $month := $localTime.Month }}
{{ $daysInMonth := 31 }}
{{ if eq $month 2 }}{{ $daysInMonth = 28 }}{{ end }}
{{ if or (eq $month 4) (eq $month 6) (eq $month 9) (eq $month 11) }}{{ $daysInMonth = 30 }}{{ end }}
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $daysElapsed := add (sub $localTime.Day 1) (div $secondsToday 86400.0) }}
{{ $monthProgress := mul (div $daysElapsed $daysInMonth) 100.0 }}
{{ $gradient := "#70a1ff" }}
{{ if gt $monthProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $monthProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $monthProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $monthProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $monthProgress }}% des Monats sind vorbei</div>
</div>
- type: custom-api
title: Year
body-type: string
skip-json-validation: true
cache: 1s
template: |
{{ $localTime := now }}
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
{{ $secondsElapsed := add (mul (sub $localTime.YearDay 1) 86400) $secondsToday }}
{{ $yearProgress := div (mul $secondsElapsed 100.0) (mul 365 86400) }}
{{ $gradient := "#70a1ff" }}
{{ if gt $yearProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
{{ if gt $yearProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
{{ if gt $yearProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
<div style="text-align: center;">
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
<div style="height: 100%; width: {{ $yearProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
</div>
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $yearProgress }}% des Jahres sind vorbei</div>
</div>
- type: clock
hour-format: 24h
show-progress: true
timezones:
- timezone: Europe/Berlin
label: Berlin
- timezone: UTC
label: UTC
- type: custom-api
title: Wetter · KalliHome
title-url: https://home.kaleschke.info
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
first-day-of-week: monday
- type: to-do
title: Operator-Notizen
- type: bookmarks
title: Direkte Einstiege
groups:
- title: Core
color: 212 100 50
links:
- title: Komodo
url: https://komodo.kaleschke.info
icon: sh:komodo
- title: Gitea
url: https://git.kaleschke.info
icon: si:gitea
- title: Monitoring
url: https://monitoring.kaleschke.info
icon: si:grafana
- title: Ops
color: 45 70 55
links:
- title: Borg
url: https://borg.kaleschke.info
icon: mdi:archive
- title: Glances
url: https://glances.kaleschke.info
icon: sh:glances
- title: Scrutiny
url: https://scrutiny.kaleschke.info
icon: sh:scrutiny
- size: full
widgets:
- type: server-stats
title: Server Stats
servers:
- type: local
name: Kallilabcore
hide-mountpoints-by-default: false
- type: custom-api
title: Komodo Stacks
title-url: https://komodo.kaleschke.info
cache: 2m
url: http://komodo-core:9120/read
method: POST
body-type: json
body:
type: ListStacks
params: {}
headers:
X-Api-Key: ${GLANCE_KOMODO_API_KEY}
X-Api-Secret: ${GLANCE_KOMODO_API_SECRET}
Content-Type: application/json
template: |
{{ $stacks := .JSON.Array "@this" }}
{{ $total := len $stacks }}
{{ $running := 0 }}
{{ range $stacks }}{{ if eq (.String "info.state") "running" }}{{ $running = add $running 1 }}{{ end }}{{ end }}
{{ $problems := sub $total $running }}
{{ $divider := "border-left: 1px solid hsla(220, 40%, 70%, 0.14);" }}
<div style="display: flex; text-align: center;">
<div style="flex: 1;">
<div class="color-highlight size-h3">{{ $total }}</div>
<div class="size-h6 uppercase color-subdue">Stacks</div>
</div>
<div style="flex: 1; {{ $divider }}">
<div class="color-positive size-h3">{{ $running }}</div>
<div class="size-h6 uppercase color-subdue">Running</div>
</div>
<div style="flex: 1; {{ $divider }}">
<div class="{{ if gt $problems 0 }}color-negative{{ else }}color-subdue{{ end }} size-h3">{{ $problems }}</div>
<div class="size-h6 uppercase color-subdue">Auffaellig</div>
</div>
</div>
<div style="height: 5px; margin-top: 14px; border-radius: 999px; overflow: hidden; background: hsla(220, 30%, 60%, 0.12);">
<div style="height: 100%; width: {{ if gt $total 0 }}{{ div (mul $running 100.0) (toFloat $total) }}{{ else }}0{{ end }}%; border-radius: 999px; background: linear-gradient(90deg, hsl(150, 85%, 42%), hsl(172, 95%, 48%));"></div>
</div>
{{ if gt $problems 0 }}
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; margin-top: 12px;">
{{ range $stacks }}
{{ if ne (.String "info.state") "running" }}
<span class="size-h6" style="padding: 3px 12px; border-radius: 999px; border: 1px solid hsla(350, 90%, 60%, 0.45); background: hsla(350, 90%, 60%, 0.08); color: var(--color-negative); letter-spacing: 0.05em;">{{ .String "name" }} · {{ .String "info.state" }}</span>
{{ end }}
{{ end }}
</div>
{{ end }}
- type: custom-api
title: Immich
title-url: https://immich.kaleschke.info
cache: 10m
url: http://immich_server:2283/api/server/statistics
headers:
x-api-key: ${GLANCE_IMMICH_API_KEY}
subrequests:
storage:
url: http://immich_server:2283/api/server/storage
headers:
x-api-key: ${GLANCE_IMMICH_API_KEY}
template: |
{{ $photos := .JSON.Int "photos" }}
{{ $videos := .JSON.Int "videos" }}
{{ $usageGiB := div (toFloat (.JSON.Int "usage")) 1073741824.0 }}
{{ $storage := .Subrequest "storage" }}
{{ $storageOK := and (ge $storage.Response.StatusCode 200) (le $storage.Response.StatusCode 299) }}
{{ $percentage := 0.0 }}
{{ if $storageOK }}{{ $percentage = $storage.JSON.Float "diskUsagePercentage" }}{{ end }}
{{ $divider := "border-left: 1px solid hsla(220, 40%, 70%, 0.14);" }}
<div style="display: flex; text-align: center;">
<div style="flex: 1;">
<div class="color-highlight size-h3">{{ $photos | formatNumber }}</div>
<div class="size-h6 uppercase color-subdue">Fotos</div>
</div>
<div style="flex: 1; {{ $divider }}">
<div class="color-highlight size-h3">{{ $videos | formatNumber }}</div>
<div class="size-h6 uppercase color-subdue">Videos</div>
</div>
<div style="flex: 1; {{ $divider }}">
<div class="color-highlight size-h3">{{ printf "%.0f" $usageGiB }} GiB</div>
<div class="size-h6 uppercase color-subdue">Medien</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 12px; margin-top: 16px;">
<div style="flex: 1; height: 5px; border-radius: 999px; overflow: hidden; background: hsla(220, 30%, 60%, 0.12);">
<div style="height: 100%; width: {{ if $storageOK }}{{ printf "%.1f" $percentage }}%{{ else }}0%{{ end }}; border-radius: 999px; background: linear-gradient(90deg, hsl(205, 100%, 55%), hsl(172, 95%, 48%));"></div>
</div>
<div class="size-h6 color-subdue" style="white-space: nowrap;">{{ if $storageOK }}{{ printf "%.1f" $percentage }}% belegt{{ else }}Speicher API n/v{{ end }}</div>
</div>
- type: group
widgets:
- type: monitor
title: Core
cache: 1m
sites:
- title: AdGuard Home
url: http://192.168.178.58:8082
check-url: http://adguard
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Authelia
url: https://auth.kaleschke.info
check-url: http://authelia:9091/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Gitea
url: https://git.kaleschke.info
check-url: http://gitea:3000/api/healthz
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Traefik
url: https://traefik.kaleschke.info
check-url: http://traefik:8082/metrics
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Vaultwarden
url: https://vault.kaleschke.info
check-url: http://vaultwarden/alive
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Komodo
url: https://komodo.kaleschke.info
check-url: http://komodo-core:9120
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glance
url: https://glance.kaleschke.info
check-url: http://glance:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- type: monitor
title: Apps
cache: 1m
sites:
- title: Paperless-ngx
url: https://paperless.kaleschke.info
check-url: http://paperless-ngx:8000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Paperless-GPT
url: https://paperless-gpt.kaleschke.info
check-url: http://paperless-gpt:8080
icon: mdi:robot
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Immich
url: https://immich.kaleschke.info
check-url: http://immich_server:2283/api/server/ping
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Mealie
url: https://mealie.kaleschke.info
check-url: http://mealie:9000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Nextcloud
url: https://cloud.kaleschke.info
check-url: http://nextcloud/status.php
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: ntfy
url: https://ntfy.kaleschke.info
check-url: http://ntfy/v1/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Mail Archiver
url: https://mail.kaleschke.info
check-url: http://mail-archiver:5000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: BentoPDF
url: https://pdf.kaleschke.info
check-url: http://bentopdf:8080
icon: mdi:file-pdf-box
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- type: monitor
title: Ops
cache: 1m
sites:
- title: Monitoring Grafana
url: https://monitoring.kaleschke.info
check-url: http://monitoring-grafana:3000/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glances
url: https://glances.kaleschke.info
check-url: http://glances:61208
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Scrutiny
url: https://scrutiny.kaleschke.info
check-url: http://scrutiny:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Speedtest Tracker
url: https://speedtest.kaleschke.info
check-url: http://speedtest-tracker
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Filebrowser
url: https://files.kaleschke.info
check-url: http://filebrowser
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: code-server
url: https://code.kaleschke.info
check-url: http://code-server:8443
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Borg UI
url: https://borg.kaleschke.info
check-url: http://borg-ui:8081
icon: mdi:archive-sync
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- size: small
widgets:
- type: custom-api
title: Internet
title-url: https://speedtest.kaleschke.info
cache: 1h
url: http://speedtest-tracker/api/v1/results/latest
headers:
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
Accept: application/json
subrequests:
stats:
url: http://speedtest-tracker/api/v1/stats
headers:
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
Accept: application/json
template: |
{{ $ip := .JSON.String "external_ip" }}
{{ if eq $ip "" }}{{ $ip = .JSON.String "data.interface.externalIp" }}{{ end }}
{{ if eq $ip "" }}{{ $ip = .JSON.String "data.data.interface.externalIp" }}{{ end }}
{{ $isp := .JSON.String "isp" }}
{{ if eq $isp "" }}{{ $isp = .JSON.String "data.isp" }}{{ end }}
{{ if eq $isp "" }}{{ $isp = .JSON.String "data.data.isp" }}{{ end }}
{{ $download := .JSON.Float "download" }}
{{ if eq $download 0.0 }}{{ $download = .JSON.Float "data.download" }}{{ end }}
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "download_bits") 1000000.0 }}{{ end }}
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "data.download_bits") 1000000.0 }}{{ end }}
{{ if eq $download 0.0 }}{{ $download = div (mul (.JSON.Float "data.data.download.bandwidth") 8.0) 1000000.0 }}{{ end }}
{{ $upload := .JSON.Float "upload" }}
{{ if eq $upload 0.0 }}{{ $upload = .JSON.Float "data.upload" }}{{ end }}
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "upload_bits") 1000000.0 }}{{ end }}
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "data.upload_bits") 1000000.0 }}{{ end }}
{{ if eq $upload 0.0 }}{{ $upload = div (mul (.JSON.Float "data.data.upload.bandwidth") 8.0) 1000000.0 }}{{ end }}
{{ if gt $download 100000.0 }}{{ $download = div (mul $download 8.0) 1000000.0 }}{{ end }}
{{ if gt $upload 100000.0 }}{{ $upload = div (mul $upload 8.0) 1000000.0 }}{{ end }}
{{ $ping := .JSON.Float "ping" }}
{{ if eq $ping 0.0 }}{{ $ping = .JSON.Float "data.ping" }}{{ end }}
{{ if eq $ping 0.0 }}{{ $ping = .JSON.Float "data.data.ping.latency" }}{{ end }}
<div class="text-center" style="margin-bottom: 10px;">
<div class="color-primary size-h3" style="font-weight: 700;">{{ if ne $ip "" }}{{ $ip }}{{ else }}WAN online{{ end }}</div>
<div class="size-h6 color-subdue">{{ if ne $isp "" }}{{ $isp }}{{ else }}Speedtest Tracker{{ end }}</div>
</div>
{{ if and (eq $download 0.0) (eq $upload 0.0) }}
<div class="text-center color-subdue size-h6">Keine aktuellen Messdaten</div>
{{ else }}
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h4">{{ printf "%.1f" $download }}</div>
<div class="size-h6 color-subdue">MBIT DOWN</div>
</div>
<div>
<div class="color-highlight size-h4">{{ printf "%.1f" $upload }}</div>
<div class="size-h6 color-subdue">MBIT UP</div>
</div>
<div>
<div class="color-highlight size-h4">{{ printf "%.0f ms" $ping }}</div>
<div class="size-h6 color-subdue">PING</div>
</div>
</div>
{{ end }}
- type: dns-stats
title: DNS Stats
service: adguard
url: http://adguard
username: ${GLANCE_ADGUARD_USERNAME}
password: ${GLANCE_ADGUARD_PASSWORD}
- type: custom-api
title: Borg Backup
title-url: https://borg.kaleschke.info
cache: 15m
url: http://monitoring-prometheus:9090/api/v1/query?query=(time()-homelab_borg_last_completed_timestamp_seconds)/3600
subrequests:
success:
url: http://monitoring-prometheus:9090/api/v1/query?query=homelab_borg_last_success
template: |
{{ $ageHours := .JSON.Float "data.result.0.value.1" }}
{{ $archive := .JSON.String "data.result.0.metric.archive" }}
{{ $succ := .Subrequest "success" }}
{{ $ok := $succ.JSON.Float "data.result.0.value.1" }}
{{ $status := $succ.JSON.String "data.result.0.metric.status" }}
{{ if eq (len (.JSON.Array "data.result")) 0 }}
<div class="text-center color-subdue">Keine Backup-Metrik gefunden</div>
{{ else }}
<div class="text-center">
<div class="size-h2 {{ if gt $ageHours 30.0 }}color-negative{{ else }}color-positive{{ end }}">vor {{ printf "%.0f" $ageHours }} h</div>
<div class="size-h6 color-subdue" style="margin-top: 4px;">letztes abgeschlossenes Backup</div>
<div class="size-h6 {{ if eq $ok 1.0 }}color-positive{{ else }}color-negative{{ end }}" style="margin-top: 6px;">
{{ if eq $ok 1.0 }}letzter Job erfolgreich{{ else }}letzter Job: {{ $status }}{{ end }}
</div>
{{ if ne $archive "" }}<div class="size-h6 color-subdue text-truncate" style="margin-top: 2px;">{{ $archive }}</div>{{ end }}
</div>
{{ end }}
- type: group
widgets:
- type: docker-containers
title: Network
category: network
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
- type: docker-containers
title: Apps
category: apps
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
- type: docker-containers
title: Ops
category: ops
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
+244
View File
@@ -0,0 +1,244 @@
- name: Infrastructure and Media
slug: infrastructure
width: wide
columns:
- size: small
widgets:
- type: bookmarks
title: Core
groups:
- title: Control Plane
color: 212 100 50
links:
- title: Komodo
url: https://komodo.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
- title: Gitea
url: https://git.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
- title: Traefik
url: https://traefik.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
- title: Authelia
url: https://auth.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
- type: bookmarks
title: Media und Apps
groups:
- title: Apps
color: 140 70 40
links:
- title: Immich
url: https://immich.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
- title: Paperless
url: https://paperless.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
- title: Nextcloud
url: https://cloud.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
- title: Mealie
url: https://mealie.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
- type: custom-api
title: Scrutiny Disk Health
title-url: https://scrutiny.kaleschke.info
cache: 30m
url: http://scrutiny:8080/api/summary
template: |
{{ $disks := .JSON.Array "data.summary.@values" }}
{{ if eq (len $disks) 0 }}
<div class="text-center color-subdue">Keine Disks gemeldet.</div>
{{ else }}
<ul class="list list-gap-4">
{{ range $disks }}
{{ $status := .Int "device.device_status" }}
<li class="flex justify-between">
<div class="color-highlight">{{ .String "device.device_name" }}</div>
<div class="size-h6 uppercase {{ if eq $status 0 }}color-positive{{ else }}color-negative{{ end }}">
{{ if eq $status 0 }}OK{{ else }}FAILED{{ end }}
</div>
</li>
{{ end }}
</ul>
{{ end }}
- size: full
widgets:
- type: custom-api
title: GitOps - homelab-infra
title-url: https://git.kaleschke.info/Micha/homelab-infra
cache: 5m
url: http://gitea:3000/api/v1/repos/Micha/homelab-infra/commits?limit=5&stat=false
headers:
Authorization: token ${GLANCE_GITEA_TOKEN}
Accept: application/json
subrequests:
repo:
url: http://gitea:3000/api/v1/repos/Micha/homelab-infra
headers:
Authorization: token ${GLANCE_GITEA_TOKEN}
Accept: application/json
template: |
{{ $repo := .Subrequest "repo" }}
{{ $repoOK := and (ge $repo.Response.StatusCode 200) (le $repo.Response.StatusCode 299) }}
{{ if $repoOK }}
<div class="flex justify-between text-center" style="margin-bottom: 12px;">
<div>
<div class="color-highlight size-h3">{{ $repo.JSON.Int "open_issues_count" }}</div>
<div class="size-h6 uppercase">Issues</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $repo.JSON.Int "open_pr_counter" }}</div>
<div class="size-h6 uppercase">PRs</div>
</div>
<div>
<div class="color-highlight size-h3">{{ $repo.JSON.String "default_branch" }}</div>
<div class="size-h6 uppercase">Branch</div>
</div>
</div>
{{ end }}
<ul class="list list-gap-6">
{{ range .JSON.Array "@this" }}
<li>
<div class="flex justify-between">
<div class="color-highlight text-truncate" style="max-width: 75%;">{{ .String "commit.message" | replaceMatches "(?s)\n.*" "" }}</div>
<div class="size-h6 color-subdue">{{ slice (.String "sha") 0 7 }}</div>
</div>
<div class="size-h6 color-subdue">{{ .String "commit.author.name" }} · <span {{ .String "commit.author.date" | parseTime "rfc3339" | toRelativeTime }}></span></div>
</li>
{{ end }}
</ul>
- type: monitor
title: Platform Checks
cache: 1m
sites:
- title: Gitea
url: https://git.kaleschke.info
check-url: http://gitea:3000/api/healthz
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Monitoring Grafana
url: https://monitoring.kaleschke.info
check-url: http://monitoring-grafana:3000/api/health
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Glance
url: https://glance.kaleschke.info
check-url: http://glance:8080
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Immich
url: https://immich.kaleschke.info
check-url: http://immich_server:2283/api/server/ping
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Paperless-ngx
url: https://paperless.kaleschke.info
check-url: http://paperless-ngx:8000
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- title: Nextcloud
url: https://cloud.kaleschke.info
check-url: http://nextcloud/status.php
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
timeout: 5s
alt-status-codes: [200, 302, 401, 403]
- type: group
widgets:
- type: docker-containers
title: Core
category: core
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
- type: docker-containers
title: Apps
category: apps
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
- type: docker-containers
title: Ops
category: ops
hide-by-default: true
sock-path: tcp://glance-docker-socket-proxy:2375
containers:
$include: containers-map.yml
- size: small
widgets:
- type: custom-api
title: Paperless-ngx
title-url: https://paperless.kaleschke.info
cache: 15m
url: http://paperless-ngx:8000/api/statistics/
headers:
Authorization: Token ${GLANCE_PAPERLESS_TOKEN}
Accept: application/json
template: |
{{ $total := .JSON.Int "documents_total" }}
{{ $inbox := .JSON.Int "documents_inbox" }}
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ $total | formatNumber }}</div>
<div class="size-h6 uppercase">Dokumente</div>
</div>
<div>
<div class="size-h3 {{ if gt $inbox 0 }}color-negative{{ else }}color-positive{{ end }}">{{ $inbox }}</div>
<div class="size-h6 uppercase">Inbox</div>
</div>
</div>
- type: custom-api
title: Mealie
title-url: https://mealie.kaleschke.info
cache: 1h
url: http://mealie:9000/api/admin/about/statistics
headers:
Authorization: Bearer ${GLANCE_MEALIE_TOKEN}
Accept: application/json
template: |
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "totalRecipes" | formatNumber }}</div>
<div class="size-h6 uppercase">Rezepte</div>
</div>
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "totalCategories" }}</div>
<div class="size-h6 uppercase">Kategorien</div>
</div>
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "totalUsers" }}</div>
<div class="size-h6 uppercase">Nutzer</div>
</div>
</div>
- type: bookmarks
title: Ops
groups:
- title: Tools
color: 4 78 57
links:
- title: Glances
url: https://glances.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
- title: Scrutiny
url: https://scrutiny.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
- title: Speedtest
url: https://speedtest.kaleschke.info
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
+80
View File
@@ -0,0 +1,80 @@
- name: Ops und Releases
slug: ops
width: wide
columns:
- size: small
widgets:
- type: rss
title: Selfhosted News
style: vertical-list
limit: 12
collapse-after: 6
cache: 6h
feeds:
- url: https://selfh.st/rss/
title: selfh.st
- url: https://tailscale.com/blog/index.xml
title: Tailscale Blog
- size: full
widgets:
- type: releases
title: Image Releases
cache: 12h
show-source-icon: true
collapse-after: 15
repositories:
- glanceapp/glance
- traefik/traefik
- go-gitea/gitea
- moghtech/komodo
- immich-app/immich
- paperless-ngx/paperless-ngx
- AdguardTeam/AdGuardHome
- dani-garcia/vaultwarden
- authelia/authelia
- mealie-recipes/mealie
- nextcloud/server
- AnalogJ/scrutiny
- alexjustesen/speedtest-tracker
- binwiederhier/ntfy
- filebrowser/filebrowser
- coder/code-server
- qdm12/ddns-updater
- nicolargo/glances
- size: small
widgets:
- type: custom-api
title: Letzte Commits
title-url: https://git.kaleschke.info/Micha/homelab-infra/commits/branch/master
cache: 5m
url: http://gitea:3000/api/v1/repos/Micha/homelab-infra/commits?limit=8&stat=false
headers:
Authorization: token ${GLANCE_GITEA_TOKEN}
Accept: application/json
template: |
<ul class="list list-gap-6 collapsible-container" data-collapse-after="5">
{{ range .JSON.Array "@this" }}
<li>
<div class="color-highlight text-truncate">{{ .String "commit.message" | replaceMatches "(?s)\n.*" "" }}</div>
<div class="size-h6 color-subdue">{{ slice (.String "sha") 0 7 }} · <span {{ .String "commit.author.date" | parseTime "rfc3339" | toRelativeTime }}></span></div>
</li>
{{ end }}
</ul>
- type: bookmarks
title: Deploy-Kette
groups:
- title: GitOps
color: 212 100 50
links:
- title: Gitea Repo
url: https://git.kaleschke.info/Micha/homelab-infra
icon: si:gitea
- title: Komodo Stacks
url: https://komodo.kaleschke.info
icon: sh:komodo
- title: Grafana
url: https://monitoring.kaleschke.info
icon: si:grafana
+3
View File
@@ -0,0 +1,3 @@
$include: home.yml
$include: infrastructure.yml
$include: ops.yml
+11
View File
@@ -9,11 +9,20 @@ services:
GLANCE_ADGUARD_USERNAME: ${GLANCE_ADGUARD_USERNAME:-}
GLANCE_ADGUARD_PASSWORD: ${GLANCE_ADGUARD_PASSWORD:-}
GLANCE_SPEEDTEST_API_KEY: ${GLANCE_SPEEDTEST_API_KEY:-}
GLANCE_KOMODO_API_KEY: ${GLANCE_KOMODO_API_KEY:-}
GLANCE_KOMODO_API_SECRET: ${GLANCE_KOMODO_API_SECRET:-}
GLANCE_GITEA_TOKEN: ${GLANCE_GITEA_TOKEN:-}
GLANCE_PAPERLESS_TOKEN: ${GLANCE_PAPERLESS_TOKEN:-}
GLANCE_MEALIE_TOKEN: ${GLANCE_MEALIE_TOKEN:-}
GLANCE_HA_TOKEN: ${GLANCE_HA_TOKEN:-}
volumes:
- ./config:/app/config:ro
- ./assets:/app/assets:ro
networks:
- frontend_net
- glance_socket_net
# monitoring_net nur lesend fuer Prometheus-Query des Borg-Backup-Widgets
- monitoring_net
depends_on:
- glance-docker-socket-proxy
labels:
@@ -50,6 +59,8 @@ services:
networks:
frontend_net:
external: true
monitoring_net:
external: true
glance_socket_net:
name: glance_socket_net
internal: true
+1 -1
View File
@@ -1,6 +1,6 @@
services:
glances:
image: nicolargo/glances:latest-full@sha256:60872a1af0e40a3150975617c7e811ad7ad48f95bc45d033fb0c1737a037e4d2
image: nicolargo/glances:latest-full@sha256:58651aabedf62db8bfc1d252f8d3889675dfcdb5d0ad1c177ae5879c21626f3a
container_name: glances
restart: unless-stopped
pid: host
+85 -2
View File
@@ -1,6 +1,6 @@
# H:/ Nearline-Backup — Struktur und Betrieb
Stand: 2026-06-10
Stand: 2026-06-21
## 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
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
`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
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
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.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)
- 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
Enthält offline hinterlegte Schlüssel und Secrets für den DR-Fall:
+77 -38
View File
@@ -1,11 +1,35 @@
param(
[string]$SourceRoot = "\\192.168.178.58\backups",
[string]$DestinationRoot = "H:\kallilab-nearline-backups",
[string]$HealthchecksUrl = $env:HEALTHCHECKS_NEARLINE_URL,
[switch]$WhatIf
)
$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 = @(
@{
Name = "borg-dumps-latest"
@@ -25,6 +49,7 @@ $Jobs = @(
"immich.dump",
"komodo-mongo.archive.gz",
"mealie.dump",
"n8n.sqlite.dump",
"nextcloud.dump",
"postgresql17-authelia.dump",
"postgresql17-globals.sql",
@@ -144,44 +169,58 @@ if ($WhatIf) {
exit 0
}
$destinationDrive = Split-Path -Qualifier $DestinationRoot
Assert-PathExists -Path $destinationDrive -Label "Destination drive"
# 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
Assert-PathExists -Path $destinationDrive -Label "Destination drive"
$logRoot = Join-Path $DestinationRoot "_logs"
$reportRoot = Join-Path $DestinationRoot "_reports"
New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null
$logRoot = Join-Path $DestinationRoot "_logs"
$reportRoot = Join-Path $DestinationRoot "_reports"
New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null
$results = foreach ($job in $Jobs) {
Invoke-RobocopyJob -Job $job -LogRoot $logRoot
$results = foreach ($job in $Jobs) {
Invoke-RobocopyJob -Job $job -LogRoot $logRoot
}
$reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss"))
$lines = @()
$lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$lines += ""
$lines += "- Source root: ``$SourceRoot``"
$lines += "- Destination root: ``$DestinationRoot``"
$lines += "- Mode: non-destructive copy, no ``/MIR``, no purge"
$lines += ""
$lines += "| Job | Exit code | Source | Destination | Log |"
$lines += "|---|---:|---|---|---|"
foreach ($result in $results) {
$lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |"
}
$lines += ""
$lines += "Expected critical artifacts after run:"
$lines += ""
$lines += "- ``borg-dumps/latest/immich.dump``"
$lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``"
$lines += "- ``git-bundles/gitea/latest-report.md``"
$lines += "- ``git-bundles/gitea/micha/*.bundle``"
$lines += ""
$lines += "Bewusst NICHT in Nearline-Scope:"
$lines += ""
$lines += "- ``unraid-flash-config.tar.gz`` (hostseitig 0600 root:root; Restore aus Hetzner-Borg)"
$lines | Set-Content -LiteralPath $reportPath -Encoding UTF8
Write-Host "H:/ nearline pull completed."
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
}
$reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss"))
$lines = @()
$lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$lines += ""
$lines += "- Source root: ``$SourceRoot``"
$lines += "- Destination root: ``$DestinationRoot``"
$lines += "- Mode: non-destructive copy, no ``/MIR``, no purge"
$lines += ""
$lines += "| Job | Exit code | Source | Destination | Log |"
$lines += "|---|---:|---|---|---|"
foreach ($result in $results) {
$lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |"
}
$lines += ""
$lines += "Expected critical artifacts after run:"
$lines += ""
$lines += "- ``borg-dumps/latest/immich.dump``"
$lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``"
$lines += "- ``git-bundles/gitea/latest-report.md``"
$lines += "- ``git-bundles/gitea/micha/*.bundle``"
$lines += ""
$lines += "Bewusst NICHT in Nearline-Scope:"
$lines += ""
$lines += "- ``unraid-flash-config.tar.gz`` (hostseitig 0600 root:root; Restore aus Hetzner-Borg)"
$lines | Set-Content -LiteralPath $reportPath -Encoding UTF8
Write-Host "H:/ nearline pull completed."
Write-Host "Report: $reportPath"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM nousresearch/hermes-agent:v2026.6.5
FROM nousresearch/hermes-agent:v2026.6.19
USER root
+4 -4
View File
@@ -45,13 +45,13 @@
"description": "VPN / Remote-Zugang",
"tier": 1,
"category": "core",
"container_name": "tailscale",
"container_name": null,
"dependencies": [],
"url": null,
"dump_file": null,
"data_paths": ["/mnt/user/appdata/tailscale"],
"first_check": "Tailscale Status auf Host pruefen; State-Datei fuer Key-Renewal vorhanden?",
"notes": "network_mode: host; NET_ADMIN, NET_RAW, /dev/net/tun — dokumentierte VPN-Ausnahmen"
"data_paths": ["/boot/config/plugins/tailscale/state"],
"first_check": "Tailscale Status auf Host pruefen; native Unraid-Plugin-Instanz und Subnet-Route aktiv?",
"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": {
"description": "Git-Server — operative Quelle der Wahrheit fuer GitOps",
+4 -4
View File
@@ -75,14 +75,14 @@ services:
description: VPN / Remote-Zugang
tier: 1
category: core
container_name: tailscale
container_name: null
dependencies: []
url: null
dump_file: null
data_paths:
- /mnt/user/appdata/tailscale
first_check: "Tailscale Status auf Host pruefen; State-Datei fuer Key-Renewal vorhanden?"
notes: "network_mode: host; NET_ADMIN, NET_RAW, /dev/net/tun — dokumentierte VPN-Ausnahmen"
- /boot/config/plugins/tailscale/state
first_check: "Tailscale Status auf Host pruefen; native Unraid-Plugin-Instanz und Subnet-Route aktiv?"
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:
description: Git-Server — operative Quelle der Wahrheit fuer GitOps
+2 -1
View File
@@ -59,7 +59,7 @@ Stand 2026-06-11 ist der Betrieb auf V1+ (validierte Bash-Host-Jobs mit ntfy):
# Frische-Check
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>
# 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.
- **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.
- **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`.
## Naechste Ausbaustufen
+1 -1
View File
@@ -1,6 +1,6 @@
services:
restoretest-adguard:
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
container_name: restoretest-adguard
restart: "no"
ports:
@@ -6,6 +6,7 @@ param(
)
$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-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.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 = "gitea.sqlite.dump"; Path = Join-Path $DumpRoot "gitea.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 = "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" }
@@ -89,6 +89,7 @@ check_pg_header() {
}
for dump in \
postgresql17-globals.sql \
postgresql17-paperless.dump \
postgresql17-mailarchiver.dump \
mealie.dump \
@@ -96,6 +97,7 @@ for dump in \
nextcloud.dump \
gitea.sqlite.dump \
vaultwarden.sqlite.dump \
n8n.sqlite.dump \
speedtest-tracker.sqlite.dump \
filebrowser.bolt.dump \
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
View File
@@ -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"
+7 -1
View File
@@ -55,6 +55,12 @@ case "$MODE" in
fi
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)
if [ "$WHATIF" = "--what-if" ]; then
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"
;;
*)
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
;;
esac
+1 -1
View File
@@ -1,6 +1,6 @@
services:
scrutiny:
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:228483f16a6236d2fa9b2fbfca2e76dc861e648fbc6ae6e680d23e5d00211a5d
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:bf55bae54eb329d96261183ce4174a4000b6350a7a71dca704d3fef215d98199
container_name: scrutiny
restart: unless-stopped
privileged: true
+1 -1
View File
@@ -1,6 +1,6 @@
services:
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
restart: unless-stopped
security_opt:
+2 -1
View File
@@ -39,10 +39,11 @@
"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"],
"matchPackageNames": [
"traefik",
"adguard/adguardhome",
"shaanmajid/unbound",
"docker.n8n.io/n8nio/n8n",
"nextcloud"
@@ -30,7 +30,7 @@ parse_compose() {
return value
}
function emit() {
if (service && image) {
if (service && image && !has_profile) {
print clean(container) "\t" clean(image)
}
}
@@ -40,6 +40,7 @@ parse_compose() {
sub(/:$/, "", service)
image=""
container=service
has_profile=0
next
}
service && /^ image:/ {
@@ -52,6 +53,10 @@ parse_compose() {
sub(/^[[:space:]]*container_name:[[:space:]]*/, "", container)
next
}
service && /^ profiles:/ {
has_profile=1
next
}
END { emit() }
' "$compose"
}
+123 -1
View File
@@ -13,6 +13,7 @@ CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}"
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}"
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}"
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
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}"
NTFY_TOPIC="${NTFY_TOPIC:-homelab-info}"
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}"
PROMETHEUS_CONTAINER="${PROMETHEUS_CONTAINER:-monitoring-prometheus}"
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"
}
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() {
append "## Borg Backup"
append ""
@@ -584,6 +684,7 @@ collect_image_freshness() {
local image_file="$TMP_DIR/images.tsv"
local image_warnings=0
local image_allowed=0
local image_warning_names=""
local now_epoch
: > "$image_file"
now_epoch="$(date +%s)"
@@ -630,6 +731,7 @@ collect_image_freshness() {
else
note="ueberaltert"
image_warnings=$((image_warnings + 1))
image_warning_names="${image_warning_names:+$image_warning_names,}$name:${age_days}d"
fi
fi
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_allowed" "$image_allowed"
set_summary "image_warning_names" "$image_warning_names"
if [ ! -s "$image_file" ]; then
append "- Keine Image-Daten verfuegbar."
@@ -781,8 +884,16 @@ collect_traefik_5xx() {
set_summary "traefik_5xx" "$count"
if [ "$count" -eq 0 ]; then
set_summary "traefik_5xx_top" "none"
append "- Keine 5xx-Antworten."
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 ""
append "### Gruppiert nach Service/Code"
@@ -1181,10 +1292,20 @@ write_report() {
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'
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
printf 'Im betrachteten Zeitraum liegt ein kritisches Betriebssignal vor. Der Bericht sollte zeitnah gelesen und die betroffenen Komponenten priorisiert geprueft werden.\n\n'
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 '%s\n' "- Status: \`$REPORT_STATUS\`"
printf '%s\n' "- Borg Backup: \`${borg_status:-unknown}\`"
@@ -1278,6 +1399,7 @@ Section errors: ${section_failures:-unknown}"
main() {
collect_overview
collect_weather_report
collect_host_health
collect_borg
collect_prometheus
@@ -4,7 +4,11 @@ set -euo pipefail
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
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
# wird daher hier bewusst NICHT als kritischer Container gefuehrt (Stand 2026-06-06).
@@ -90,11 +94,32 @@ EOF
# TYPE homelab_borg_last_success gauge
# HELP homelab_borg_last_job_warning Whether the most recent Borg backup job completed with warnings.
# 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
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 json
import os
from pathlib import Path
import sqlite3
conn = sqlite3.connect("/data/borg.db")
@@ -135,6 +160,9 @@ def parse_ts(value):
def escape_label(value):
return (value or "").replace("\\", "\\\\").replace('"', '\\"')
def bool_metric(value):
return 1 if value else 0
latest_status = latest["status"] if latest else "missing"
latest_success = 1 if latest_status in ("completed", "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_job_warning{{status="{latest_status}",archive="{latest_archive}"}} {latest_warning}')
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
else
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_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
# 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"
# 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.
# Re-check: ob eine blackbox_exporter-Version > v0.28.0 erschienen ist.
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
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
set -euo pipefail
# Komodo-Stack-Hygiene-Check.
#
# Prueft, dass jeder Komodo-Stack sauber gegen das Git-Repo konfiguriert ist,
# und dass jeder Compose-File im Repo einen passenden Komodo-Stack hat.
# Findet die Klasse von Fehlern, die `immich_new` (2026-06-12) durchgelassen
# hat: Stack RUNNING, aber kein Repo / kein Account / project_missing.
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
OUTPUT_PATH="${OUTPUT_PATH:-/mnt/user/services/posture-check/komodo-stack-hygiene-last.json}"
NTFY_SCRIPT="${NTFY_SCRIPT:-$REPO_ROOT/ops/restore-tests/send-ntfy.sh}"
NTFY_TOPIC="${NTFY_TOPIC:-homelab-alerts}"
SEND_NTFY="${SEND_NTFY:-1}"
KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-/mnt/user/appdata/secrets/codex_komodo_api.env}"
KOMODO_CONTAINER="${KOMODO_CONTAINER:-komodo-core}"
# Komma-separierte Allowlist fuer bewusst inline-managed Stacks.
# Quelle: memory/komodo-stack-inline-managed.md, CLAUDE.md.
INLINE_ALLOWLIST="${INLINE_ALLOWLIST:-komodo,grafana}"
# Compose-Files unter diesen Pfaden zaehlen NICHT als erwartete Stacks
# (Beispiele, Archive, Submodule).
COMPOSE_EXCLUDE_PATTERN="${COMPOSE_EXCLUDE_PATTERN:-/archive/|/examples/|/.git/}"
# Compose-Dir-Namen, die bewusst NICHT als Komodo-Stack laufen sollen
# (Work-in-progress, Build-/Dev-Compose, manuell deployed). Komma-separiert.
EXPECTED_NOT_IN_KOMODO="${EXPECTED_NOT_IN_KOMODO:-hermes-agent}"
TMP_DIR="${TMP_DIR:-/tmp/kallilab-komodo-stack-hygiene}"
mkdir -p "$TMP_DIR"
RESULTS_FILE="$TMP_DIR/results.$$"
STACKS_FILE="$TMP_DIR/stacks.$$.json"
: > "$RESULTS_FILE"
trap 'rm -f "$RESULTS_FILE" "$STACKS_FILE"' EXIT
json_escape() {
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
}
add_result() {
printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RESULTS_FILE"
}
is_inline_allowed() {
local name="$1"
local IFS=,
for entry in $INLINE_ALLOWLIST; do
[ "$name" = "$entry" ] && return 0
done
return 1
}
is_expected_not_in_komodo() {
local name="$1"
local IFS=,
for entry in $EXPECTED_NOT_IN_KOMODO; do
[ "$name" = "$entry" ] && return 0
done
return 1
}
# True drift: do files inside this stack's compose-dir actually differ
# between deployed_hash and latest_hash? Komodo's deployed_hash bumps only
# on redeploy, while latest_hash tracks master HEAD - that produces a noisy
# "Pending Update" even when the stack itself wasn't touched.
stack_files_changed() {
local name="$1" deployed="$2" latest="$3"
local dir
# Locate the stack's compose dir (case-insensitive, same as Mode 3).
dir="$(find "$REPO_ROOT" -type d -iname "$name" -not -path "*/.git/*" 2>/dev/null | head -1)"
[ -n "$dir" ] || return 0 # No dir -> can't tell, treat as drift to be safe
( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$deployed" >/dev/null ) || return 0
( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$latest" >/dev/null ) || return 0
local rel="${dir#$REPO_ROOT/}"
if ( cd "$REPO_ROOT" && git diff --quiet "$deployed".."$latest" -- "$rel" ); then
return 1 # no change
fi
return 0 # real change
}
# Komodo-API-Credentials laden und Stack-Liste holen.
if [ ! -r "$KOMODO_ENV_FILE" ]; then
add_result "warning" "komodo-api" "Komodo env file not readable: $KOMODO_ENV_FILE"
else
set -a
# shellcheck disable=SC1090
. "$KOMODO_ENV_FILE"
set +a
if ! docker exec \
-e KOMODO_CLI_HOST \
-e KOMODO_CLI_KEY \
-e KOMODO_CLI_SECRET \
"$KOMODO_CONTAINER" km list -a stacks -f json > "$STACKS_FILE" 2>/dev/null; then
add_result "warning" "komodo-api" "km list stacks failed (container=$KOMODO_CONTAINER)"
: > "$STACKS_FILE"
fi
fi
# Per-Stack-Checks. Trenner: "|" statt Tab, weil IFS=Tab leere Felder kollabiert
# (Tab ist Whitespace in IFS). "|" kommt in Stack-Namen/Repos/Hashes nicht vor.
if [ -s "$STACKS_FILE" ]; then
while IFS='|' read -r name repo project_missing missing_files state deployed_hash latest_hash files_on_host file_contents; do
[ -n "$name" ] || continue
if is_inline_allowed "$name"; then
add_result "ok" "$name" "Inline-managed (allowlisted), skipping repo checks"
continue
fi
# Failure-Mode 1: Stack hat keine Git-Quelle (immich_new-Symptom).
if [ "$repo" = "-" ] && [ "$files_on_host" != "True" ] && [ "$file_contents" != "True" ]; then
add_result "critical" "$name" "Stack has no repo configured and is not inline-allowed"
continue
fi
# Failure-Mode 2: Komodo meldet Project Missing.
if [ "$project_missing" = "True" ]; then
add_result "critical" "$name" "project_missing=true (missing_files=$missing_files)"
continue
fi
# Failure-Mode 3: Stack-Name passt zu keinem Compose-File im Repo.
# Case-insensitive (Compose-Dir kann GroSs/klein abweichen, z.B. Adguard).
match_found=""
while IFS= read -r dir; do
[ -n "$dir" ] || continue
if [ -f "$dir/docker-compose.yml" ] \
|| [ -f "$dir/docker-compose.yaml" ] \
|| [ -f "$dir/compose.yml" ] \
|| [ -f "$dir/compose.yaml" ]; then
match_found=1
break
fi
done < <(find "$REPO_ROOT" -type d -iname "$name" -not -path "*/.git/*" 2>/dev/null)
if [ -z "$match_found" ]; then
# Verwaiste Stacks wie das frueher gesehene `immich_new`: Komodo kennt
# ihn, aber im Repo gibt's keinen Compose-Pfad.
add_result "warning" "$name" "Stack name does not match any compose directory in repo"
fi
# Failure-Mode 4: Deployed-Hash hinkt latest hinterher UND der Stack-Dir
# hat tatsaechlich File-Aenderungen dazwischen. Reine Komodo-Hash-Bewegung
# ohne Stack-Inhalt aendert nichts und ist kein echter Drift.
# "-" = unbekannt (z.B. gitea self-host edge case), nicht als Drift werten.
if [ "$deployed_hash" != "-" ] && [ "$latest_hash" != "-" ] \
&& [ "$deployed_hash" != "$latest_hash" ] \
&& stack_files_changed "$name" "$deployed_hash" "$latest_hash"; then
add_result "warning" "$name" "deployed_hash $deployed_hash != latest_hash $latest_hash (stack files changed)"
fi
# Failure-Mode 5: Stack ist down.
if [ "$state" = "down" ] || [ "$state" = "unknown" ]; then
add_result "warning" "$name" "Stack state is $state"
fi
add_result "ok" "$name" "Stack hygiene OK (state=$state, hash=$deployed_hash)"
done < <(jq -r '.[] | [
.name // "-",
(.info.repo // "-"),
(.info.project_missing | if . then "True" else "False" end),
(((.info.missing_files // []) | join(",")) | if . == "" then "-" else . end),
(.info.state // "-"),
(.info.deployed_hash // "-"),
(.info.latest_hash // "-"),
(.info.files_on_host | if . then "True" else "False" end),
(.info.file_contents | if . then "True" else "False" end)
] | join("|")' "$STACKS_FILE")
fi
# Failure-Mode 6: Compose-File im Repo, aber kein Komodo-Stack mit gleichem Namen.
if [ -s "$STACKS_FILE" ]; then
known_names="$(jq -r '.[].name' "$STACKS_FILE")"
while IFS= read -r -d '' compose; do
rel="${compose#$REPO_ROOT/}"
if printf '%s' "$rel" | grep -Eq "$COMPOSE_EXCLUDE_PATTERN"; then
continue
fi
dir_name="$(basename "$(dirname "$compose")")"
if is_inline_allowed "$dir_name"; then
continue
fi
if is_expected_not_in_komodo "$dir_name"; then
continue
fi
# Case-insensitive, weil z.B. host-services/Adguard <-> Komodo-Stack adguard
# legitim als gematched gilt.
if ! printf '%s\n' "$known_names" | grep -Fixq "$dir_name"; then
add_result "warning" "$dir_name" "Compose file $rel has no matching Komodo stack"
fi
done < <(find "$REPO_ROOT" -path "$REPO_ROOT/.git" -prune -o -type f \
\( -name docker-compose.yml -o -name docker-compose.yaml \
-o -name compose.yml -o -name compose.yaml \) -print0)
fi
timestamp="$(date -Iseconds)"
critical_count="$(awk -F '\t' '$1 == "critical" { c++ } END { print c + 0 }' "$RESULTS_FILE")"
warning_count="$(awk -F '\t' '$1 == "warning" { c++ } END { print c + 0 }' "$RESULTS_FILE")"
status="ok"
[ "$warning_count" -gt 0 ] && status="warning"
[ "$critical_count" -gt 0 ] && status="critical"
mkdir -p "$(dirname "$OUTPUT_PATH")"
{
printf '{\n'
printf ' "timestamp": "%s",\n' "$(printf '%s' "$timestamp" | json_escape)"
printf ' "status": "%s",\n' "$status"
printf ' "critical_count": %s,\n' "$critical_count"
printf ' "warning_count": %s,\n' "$warning_count"
printf ' "checks": [\n'
first=1
while IFS=$'\t' read -r severity name message; do
if [ "$first" -eq 0 ]; then printf ',\n'; fi
first=0
printf ' {"severity":"%s","name":"%s","message":"%s"}' \
"$(printf '%s' "$severity" | json_escape)" \
"$(printf '%s' "$name" | json_escape)" \
"$(printf '%s' "$message" | json_escape)"
done < "$RESULTS_FILE"
printf '\n ]\n}\n'
} > "$OUTPUT_PATH.tmp"
mv "$OUTPUT_PATH.tmp" "$OUTPUT_PATH"
cat "$OUTPUT_PATH"
if [ "$critical_count" -gt 0 ] || [ "$warning_count" -gt 0 ]; then
if [ "$SEND_NTFY" = "1" ] && [ -x "$NTFY_SCRIPT" ]; then
priority="default"
[ "$warning_count" -gt 0 ] && priority="high"
[ "$critical_count" -gt 0 ] && priority="urgent"
"$NTFY_SCRIPT" "$NTFY_TOPIC" \
"Komodo stack hygiene: $critical_count critical, $warning_count warning" \
"See $OUTPUT_PATH" "$priority" || true
fi
[ "$critical_count" -gt 0 ] && exit 2
exit 1
fi
+16
View File
@@ -87,3 +87,19 @@ adguard.*bad question section.*only 1 question allowed
# this lookup is harmless and does not affect any dashboard.
# Re-check: only if Amazon Prometheus is added as a datasource.
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)
theme = STATUS_THEMES.get(status, STATUS_THEMES["UNKNOWN"])
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'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;'
'text-transform:uppercase;letter-spacing:0.08em;font-weight:700;'
f'line-height:1.3;opacity:0.78">{html.escape(label)}</div>'
f'<div style="font-size:17px;font-weight:700;'
'text-transform:uppercase;letter-spacing:0.04em;font-weight:700;'
f'line-height:1.35;opacity:0.78;overflow-wrap:anywhere">{html.escape(label)}</div>'
f'<div style="font-size:16px;font-weight:700;'
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>'
'</div></td>'
)
rows_html = []
for chunk_start in range(0, len(cards), 3):
chunk = cards[chunk_start:chunk_start + 3]
while len(chunk) < 3:
chunk.append('<td style="padding:6px;width:33.33%"></td>')
for chunk_start in range(0, len(cards), 2):
chunk = cards[chunk_start:chunk_start + 2]
while len(chunk) < 2:
chunk.append('<td style="padding:6px;width:50%"></td>')
rows_html.append("<tr>" + "".join(chunk) + "</tr>")
return (
'<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
```
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:
```bash
#!/bin/bash
SEND_MAIL=1 \
MAIL_MODE=always \
INCLUDE_WEATHER_REPORT=1 \
MAIL_FROM="michideheld@gmx.de" \
MAIL_TO="Mi.Kaleschke@gmx.de" \
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 "$@"
+58
View File
@@ -0,0 +1,58 @@
# Smart Home Runtime Stack
Runtime-Zustand fuer Home Assistant auf Kallilabcore. Dieser Ordner gehoert zu
`homelab-infra`, weil Komodo den Stack deployt und Renovate die Images pflegt.
## Dienste
- `homeassistant`: Home Assistant Container, erreichbar ueber Traefik unter
`https://home.kaleschke.info`
- `smarthome-mosquitto`: interner MQTT-Broker fuer Home Assistant, spaeter
Zigbee2MQTT und ESPHome
## Abhaengigkeiten
- `frontend_net` existiert bereits und wird von Traefik genutzt.
- `smarthome_net` wird durch diesen Stack angelegt und ist `internal: true`.
- Das Fachrepo `smart-home-kalli` muss auf dem Unraid-Host unter
`/mnt/user/services/smart-home-kalli` liegen. Nur ausgewählte YAML-Dateien
werden read-only nach `/config` gemountet; `.storage` bleibt in
`/mnt/user/appdata/homeassistant`.
- Vor dem ersten Start muessen diese Dateien hostseitig angelegt werden:
- `/mnt/user/appdata/homeassistant/secrets.yaml`
- `/mnt/user/appdata/homeassistant/trusted_proxies.yaml`
- `/mnt/user/appdata/mosquitto/config/passwordfile`
- `/mnt/user/appdata/mosquitto/config/aclfile`
Das detaillierte Host-Bootstrap-Runbook liegt unter
`docs/runbooks/smart-home-bootstrap.md`.
## MQTT Bootstrap
Beispiel fuer den initialen Home-Assistant-MQTT-User auf dem Unraid-Host:
```sh
mkdir -p /mnt/user/appdata/mosquitto/config
docker run --rm -it \
-v /mnt/user/appdata/mosquitto/config:/mosquitto/external_config \
eclipse-mosquitto:2.0.22 \
mosquitto_passwd -c /mosquitto/external_config/passwordfile homeassistant
cat > /mnt/user/appdata/mosquitto/config/aclfile <<'EOF'
user homeassistant
topic readwrite #
EOF
```
LAN-Port `1883` bleibt in Phase 1 geschlossen. Eine Portfreigabe fuer externe
MQTT-Clients wird erst in der ESPHome-Phase mit ACLs und per-Device-Usern
ergaenzt.
## Ecowitt
Ecowitt wird nicht in Phase 1 exponiert. Wegen des globalen Traefik
HTTP-zu-HTTPS-Redirects bleibt die Ingress-Entscheidung offen:
1. Traefik-HTTP-Ausnahme nur fuer den Ecowitt-Webhook, falls der globale
EntryPoint-Redirect gezielt abloesbar ist.
2. Dokumentierter LAN-only Host-Port `8123` als Fallback, wenn Option 1 den
bestehenden Traefik-Standard zu stark verbiegt.
+71
View File
@@ -0,0 +1,71 @@
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:2026.6.4@sha256:adb3341e31e03e0048e60d8c1cf952e118a381ae258bb921d3da12a3b27bf0c2
container_name: homeassistant
restart: unless-stopped
environment:
TZ: Europe/Berlin
volumes:
- /mnt/user/appdata/homeassistant:/config
- /mnt/user/services/smart-home-kalli/home-assistant/configuration.yaml:/config/configuration.yaml:ro
- /mnt/user/services/smart-home-kalli/home-assistant/automations.yaml:/config/automations.yaml:ro
- /mnt/user/services/smart-home-kalli/home-assistant/scripts.yaml:/config/scripts.yaml:ro
- /mnt/user/services/smart-home-kalli/home-assistant/scenes.yaml:/config/scenes.yaml:ro
- /mnt/user/services/smart-home-kalli/home-assistant/packages:/config/packages:ro
networks:
- frontend_net
- smarthome_net
# Zugang zum bestehenden Monitoring-Netz nur fuer den InfluxDB-3-Writer
# (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:
- no-new-privileges:true
depends_on:
- mosquitto
labels:
- traefik.enable=true
- traefik.docker.network=frontend_net
- traefik.http.routers.homeassistant.rule=Host(`home.kaleschke.info`)
- traefik.http.routers.homeassistant.entrypoints=websecure
- traefik.http.routers.homeassistant.tls=true
- traefik.http.routers.homeassistant.tls.certresolver=le
- traefik.http.routers.homeassistant.middlewares=secure-headers@file
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
mosquitto:
image: eclipse-mosquitto:2.0.22@sha256:914f529386804c8278a4e581526b9be5e1604df44b30daabc70aa97dcefe5268
container_name: smarthome-mosquitto
restart: unless-stopped
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf
- /mnt/user/appdata/mosquitto/config:/mosquitto/external_config
- /mnt/user/appdata/mosquitto/data:/mosquitto/data
- /mnt/user/appdata/mosquitto/log:/mosquitto/log
networks:
- smarthome_net
expose:
- "1883"
security_opt:
- no-new-privileges:true
networks:
frontend_net:
external: true
smarthome_net:
name: smarthome_net
driver: bridge
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
@@ -0,0 +1,15 @@
per_listener_settings true
listener 1883 0.0.0.0
allow_anonymous false
password_file /mosquitto/external_config/passwordfile
acl_file /mosquitto/external_config/aclfile
persistence true
persistence_location /mosquitto/data/
log_dest stdout
log_type error
log_type warning
log_type notice
connection_messages true
+1 -1
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: traefik:v3.7@sha256:fcdef599e6259359833dd2e1d49f9e964f66825d69bd3dd468f51102ce013d03
image: traefik:v3.7@sha256:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257
container_name: traefik
restart: unless-stopped
security_opt: