Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 558b0ce1a1 | |||
| e8cde1e2e0 | |||
| f236bfec00 | |||
| fd1b7001f6 | |||
| d45a49d648 | |||
| 1255863a4e | |||
| 26fc96a7af | |||
| e18720d1f8 | |||
| a1e6a03f79 | |||
| 8200697258 | |||
| 05b12c4802 | |||
| 8d01c3537a | |||
| 230e0cc9dc | |||
| c9bd4af2a8 | |||
| 5927b478fa | |||
| ee69bbf730 | |||
| d908d967d4 | |||
| 606779d342 | |||
| 0fabed4d1a | |||
| 76b9ffa140 | |||
| 170a7dcc1f | |||
| 0f5045ea8e | |||
| dfa3acc21e | |||
| 2eb8da1cd4 | |||
| 2acbc1adde | |||
| 342d0a0a27 | |||
| 4ab6dcefd2 | |||
| c24b792808 | |||
| 25a4ada891 | |||
| 6e6005aefd | |||
| ad438a07b3 | |||
| ce6f5c72dd | |||
| 630ee8dd90 | |||
| b1ca9ef19c | |||
| 1c949d3fcc | |||
| cfa6c01768 | |||
| 3474d53ce5 | |||
| ca81b959cc | |||
| 23764dff38 | |||
| 3c4a48d7e5 | |||
| c0a39f5dfc | |||
| a1d7b6e433 | |||
| 45f43da659 | |||
| 290cb8949e | |||
| d933d3cee8 | |||
| baedf9f932 | |||
| b387757e87 | |||
| 3eedbcbe16 | |||
| 9033724b15 | |||
| aae176f1b7 | |||
| c7590e6603 | |||
| 3e486b95f6 | |||
| 08b4be7a5d |
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -93,6 +93,7 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme
|
||||
| `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 +124,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 +148,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 +264,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
|
||||
|
||||
@@ -274,6 +278,7 @@ Legende Status:
|
||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
||||
| `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 +394,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. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
nextcloud:
|
||||
image: nextcloud:34.0.0-apache@sha256:e15ff2ff02d04bf272940bb63f2157599bc9440d45b688cc60f09b72be7ae717
|
||||
image: nextcloud:33.0.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
unbound:
|
||||
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b
|
||||
image: shaanmajid/unbound:1.25.1@sha256:513c3893bd3dbe7230100c998970f0ef26e899e1c5d7695813da82bf646c85ea
|
||||
container_name: unbound
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
@@ -11,6 +11,167 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt),
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
+10
-4
@@ -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-12 · Status: aktiv
|
||||
|
||||
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
|
||||
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
|
||||
@@ -24,6 +24,9 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie.
|
||||
| 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` |
|
||||
| Glance-v2-Widgets: Tokens setzen | Operator | In Komodo Stack-ENV fuer `ops-glance` setzen: `GLANCE_KOMODO_API_KEY`/`_SECRET` (Komodo read-only API-Key), `GLANCE_GITEA_TOKEN` (read-only, scope `read:repository`), `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`; bis dahin zeigen die neuen Widgets Fehler/leer. Speedtest-Widget: falls weiter 0.0, API-Response pruefen | `ops/glance/config/` |
|
||||
| Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` |
|
||||
| Audit-PDF aus `docs/` entfernen | Operator | `docs/KalliLab_CORE_Audit_2026-06-06.pdf` (untracked) extern ablegen (H:/ oder Documents-Share) und lokal loeschen; Binaerdateien gehoeren nicht ins GitOps-Repo | Doku-Regeln `docs/REPO_MAP.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -54,6 +57,7 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
|
||||
| Immich Redis named volume | passende Wartung am Immich-Stack | `docs/SERVICE_CATALOG.md` |
|
||||
| Storage-Wachstum (zweite NVMe, zweite Array-Disk, ZFS/BTRFS) | Trigger aus Capacity-Doku | `docs/STORAGE_LAYOUT.md`, `docs/CAPACITY_AND_LIFECYCLE.md` |
|
||||
| Wiederkehrende Restore-Drills | laufend nach Kadenz, inkl. quartalsweisem Frische-Negativtest (`run-restore-checks.sh freshness-negative`) | `docs/RESTORE_MATRIX.md`, `ops/restore-tests/schedule.md` |
|
||||
| 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 +71,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-13** Home Assistant MQTT-Integration produktiv verbunden: Config-Entry `smarthome-mosquitto` ist `loaded`, Mosquitto sieht den HA-Client `homeassistant`; `check_config` gruen.
|
||||
- **2026-06-13** HA Energy Dashboard konfiguriert: Netz, PV und Speicher aus SolarEdge Local gesetzt, `energy/validate` ohne Issues; HA-Backup danach erzeugt.
|
||||
- **2026-06-13** SolarEdge lokal angebunden: `solaredge_modbus_multi` v3.2.5 ueber `192.168.178.111:1502`, Device-ID `1`; 68 Entitaeten inkl. Inverter, Smart Meter und Batterie; HA-Backup danach erzeugt.
|
||||
- **2026-06-13** Home Assistant Restore-Probe erfolgreich: isolierter Test aus HA-native Backup + Mosquitto-Appdata + Fachrepo-Clone, HA HTTP/API/check_config gruen, MQTT Publish/Subscribe und retained Topic nach Broker-Restart gruen. Report: `/mnt/user/backups/restore-reports/homeassistant-2026-06-13.md`.
|
||||
- **2026-06-13** Home Assistant Foundation live: `smart-home` in Komodo angelegt, Gitea-Webhook aktiv, Authelia-Onboarding-Guard entfernt, HA-native Auth + Login-Ban aktiv, HA-Backup erzeugt/geprueft und MQTT-Broker-Smoke erfolgreich.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -60,6 +60,9 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
||||
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
|
||||
| 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` |
|
||||
| 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 +80,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 |
|
||||
|
||||
@@ -160,6 +165,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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -180,4 +186,4 @@ Verbleibende offene Restore-Pfade ohne vollstaendigen Test:
|
||||
Die Ablaeufe je Dienst liegen als Runbooks und automatisierte Skripte unter
|
||||
`ops/restore-tests/` (Einstieg: `ops/restore-tests/README.md`). Fuer die noch
|
||||
offenen Pfade: `ops/restore-tests/unraid-flash-runbook.md` und
|
||||
`ops/restore-tests/tailscale-runbook.md`.
|
||||
`ops/restore-tests/tailscale-runbook.md`.
|
||||
|
||||
+7
-2
@@ -40,7 +40,7 @@ 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 |
|
||||
@@ -51,6 +51,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
||||
| 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) |
|
||||
@@ -98,6 +100,9 @@ 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
|
||||
`-- vaultwarden_admin_token.txt
|
||||
@@ -141,7 +146,7 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we
|
||||
| `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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -75,11 +75,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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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" } ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -48,6 +48,10 @@ 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` |
|
||||
| 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 | `/local/appdata/hermes-agent/data`, `/local/secrets/hermes_runner_id_ed25519` |
|
||||
| BentoPDF | rebuildable | no critical persistence in compose |
|
||||
|
||||
@@ -87,6 +91,7 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
|
||||
- SQLite: `gitea`, `vaultwarden`, `speedtest-tracker`, `borg-ui`, `grafana`
|
||||
- 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
|
||||
|
||||
|
||||
@@ -20,5 +20,9 @@
|
||||
/local/appdata/komodo/periphery
|
||||
/local/appdata/komodo/core
|
||||
/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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
$include: home.yml
|
||||
$include: infrastructure.yml
|
||||
$include: ops.yml
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
restoretest-ha-mosquitto:
|
||||
image: eclipse-mosquitto:2.0.22@sha256:914f529386804c8278a4e581526b9be5e1604df44b30daabc70aa97dcefe5268
|
||||
container_name: restoretest-ha-mosquitto
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
||||
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/config:/mosquitto/external_config:ro
|
||||
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/data:/mosquitto/data
|
||||
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/mosquitto/appdata/log:/mosquitto/log
|
||||
ports:
|
||||
- "127.0.0.1:11883:1883"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.6.1@sha256:59aa8824955c9db491b75d2eebe42bd68494f80c2ec69ec0d66d9dae37d37514
|
||||
container_name: restoretest-homeassistant
|
||||
restart: "no"
|
||||
depends_on:
|
||||
- restoretest-ha-mosquitto
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
volumes:
|
||||
- ${RESTORE_ROOT:-/mnt/user/backups/restore-lab/homeassistant}/homeassistant/config:/config
|
||||
ports:
|
||||
- "127.0.0.1:18123:8123"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Home Assistant + Mosquitto Restore Smoke Test
|
||||
#
|
||||
# Scope:
|
||||
# - Restore aus dem neuesten HA-nativen Backup-Artefakt
|
||||
# - Kopie der Mosquitto-Appdata in ein isoliertes Restore-Lab
|
||||
# - Kopie des Fachrepo-Clones zur Lesbarkeits-/Git-Status-Pruefung
|
||||
# - Start isolierter Testcontainer auf localhost-Ports, ohne Traefik/Public Route
|
||||
# - HA HTTP/API-Smoke und MQTT Publish/Subscribe + retained Topic nach Restart
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/common.sh"
|
||||
|
||||
WHATIF=0
|
||||
KEEP_DATA=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--what-if) WHATIF=1 ;;
|
||||
--keep-data) KEEP_DATA=1 ;;
|
||||
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
RESTORE_ROOT="/mnt/user/backups/restore-lab/homeassistant"
|
||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||
REPORT_FILE="$REPORT_ROOT/homeassistant-$(date +%F).md"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/homeassistant-compose.test.yml"
|
||||
HA_BACKUP_DIR="/mnt/user/appdata/homeassistant/backups"
|
||||
MOSQUITTO_APPDATA="/mnt/user/appdata/mosquitto"
|
||||
MOSQUITTO_REPO_CONF="/mnt/user/services/homelab-infra/smart-home/mosquitto/config/mosquitto.conf"
|
||||
FACHREPO_SOURCE="/mnt/user/services/smart-home-kalli"
|
||||
HA_TOKEN_FILE="/mnt/user/appdata/secrets/ha_token_codex"
|
||||
|
||||
if [ "$WHATIF" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Home Assistant restore test
|
||||
Mode: WhatIf
|
||||
RestoreRoot: $RESTORE_ROOT
|
||||
HA backup source: newest *.tar under $HA_BACKUP_DIR
|
||||
Mosquitto source: $MOSQUITTO_APPDATA
|
||||
Fachrepo source: $FACHREPO_SOURCE
|
||||
Test endpoints: HA http://127.0.0.1:18123, MQTT 127.0.0.1:11883
|
||||
Scope: HA backup extract + isolated HA boot + API token smoke + MQTT auth/retained smoke
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_cmd docker
|
||||
require_cmd tar
|
||||
require_cmd curl
|
||||
require_path "$COMPOSE_FILE"
|
||||
require_path "$HA_BACKUP_DIR"
|
||||
require_path "$MOSQUITTO_APPDATA/config/passwordfile"
|
||||
require_path "$MOSQUITTO_APPDATA/config/aclfile"
|
||||
require_path "$MOSQUITTO_APPDATA/data"
|
||||
require_path "$MOSQUITTO_REPO_CONF"
|
||||
require_path "$FACHREPO_SOURCE"
|
||||
require_path "$HA_TOKEN_FILE"
|
||||
|
||||
RESTORE_SUCCESS=0
|
||||
cleanup() {
|
||||
RESTORE_ROOT="$RESTORE_ROOT" cleanup_compose "$COMPOSE_FILE"
|
||||
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||
preserve_on_failure "homeassistant" "$RESTORE_ROOT"
|
||||
return
|
||||
fi
|
||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||
rm -rf "$RESTORE_ROOT"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
latest_backup="$(find "$HA_BACKUP_DIR" -maxdepth 1 -type f -name '*.tar' -printf '%T@ %p\n' | sort -nr | awk 'NR==1 {print substr($0, index($0,$2))}')"
|
||||
if [ -z "$latest_backup" ] || [ ! -f "$latest_backup" ]; then
|
||||
echo "No HA native backup tar found under $HA_BACKUP_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$RESTORE_ROOT"
|
||||
mkdir -p \
|
||||
"$RESTORE_ROOT/ha-backup" \
|
||||
"$RESTORE_ROOT/homeassistant/config" \
|
||||
"$RESTORE_ROOT/mosquitto/config" \
|
||||
"$RESTORE_ROOT/mosquitto/appdata/config" \
|
||||
"$RESTORE_ROOT/mosquitto/appdata/data" \
|
||||
"$RESTORE_ROOT/mosquitto/appdata/log" \
|
||||
"$RESTORE_ROOT/fachrepo"
|
||||
|
||||
tar -xf "$latest_backup" -C "$RESTORE_ROOT/ha-backup"
|
||||
require_path "$RESTORE_ROOT/ha-backup/backup.json"
|
||||
require_path "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz"
|
||||
tar -xzf "$RESTORE_ROOT/ha-backup/homeassistant.tar.gz" -C "$RESTORE_ROOT/homeassistant/config" --strip-components=1 data
|
||||
|
||||
cp "$MOSQUITTO_REPO_CONF" "$RESTORE_ROOT/mosquitto/config/mosquitto.conf"
|
||||
cp -a "$MOSQUITTO_APPDATA/config/." "$RESTORE_ROOT/mosquitto/appdata/config/"
|
||||
cp -a "$MOSQUITTO_APPDATA/data/." "$RESTORE_ROOT/mosquitto/appdata/data/"
|
||||
if [ -d "$MOSQUITTO_APPDATA/log" ]; then
|
||||
cp -a "$MOSQUITTO_APPDATA/log/." "$RESTORE_ROOT/mosquitto/appdata/log/" || true
|
||||
fi
|
||||
cp -a "$FACHREPO_SOURCE/." "$RESTORE_ROOT/fachrepo/"
|
||||
|
||||
ha_config="$RESTORE_ROOT/homeassistant/config"
|
||||
require_path "$ha_config/configuration.yaml"
|
||||
require_path "$ha_config/secrets.yaml"
|
||||
require_path "$ha_config/trusted_proxies.yaml"
|
||||
require_path "$ha_config/.storage/onboarding"
|
||||
require_path "$ha_config/.storage/auth"
|
||||
|
||||
fachrepo_head="$(git -C "$RESTORE_ROOT/fachrepo" log -1 --oneline)"
|
||||
fachrepo_status="$(git -C "$RESTORE_ROOT/fachrepo" status --short)"
|
||||
if [ -n "$fachrepo_status" ]; then
|
||||
echo "Restored fachrepo clone is not clean:" >&2
|
||||
echo "$fachrepo_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_size="$(stat -c '%s' "$latest_backup")"
|
||||
ha_file_count="$(find "$ha_config" -type f | wc -l | tr -d ' ')"
|
||||
ha_bytes="$(du -sb "$ha_config" | awk '{print $1}')"
|
||||
mosquitto_data_bytes="$(du -sb "$RESTORE_ROOT/mosquitto/appdata" | awk '{print $1}')"
|
||||
|
||||
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
||||
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" up -d >/dev/null
|
||||
|
||||
mqtt_user="$(sed -n 's/^mqtt_username:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")"
|
||||
mqtt_pass="$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$ha_config/secrets.yaml" | sed "s/^['\"]//;s/['\"]$//")"
|
||||
if [ -z "$mqtt_user" ] || [ -z "$mqtt_pass" ]; then
|
||||
echo "Missing mqtt_username or mqtt_password in restored HA secrets.yaml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mqtt_topic="restoretest/homeassistant/smoke"
|
||||
mqtt_payload="ok-$(date +%s)"
|
||||
mqtt_out="$RESTORE_ROOT/mqtt-sub.out"
|
||||
rm -f "$mqtt_out"
|
||||
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" \
|
||||
restoretest-ha-mosquitto sh -lc \
|
||||
'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' \
|
||||
> "$mqtt_out" &
|
||||
sub_pid=$!
|
||||
sleep 1
|
||||
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$mqtt_topic" -e MQTT_PAYLOAD="$mqtt_payload" \
|
||||
restoretest-ha-mosquitto sh -lc \
|
||||
'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD"'
|
||||
wait "$sub_pid"
|
||||
mqtt_result="$(cat "$mqtt_out")"
|
||||
if [ "$mqtt_result" != "$mqtt_payload" ]; then
|
||||
echo "MQTT publish/subscribe smoke failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
retained_topic="restoretest/homeassistant/retained"
|
||||
retained_payload="retained-$(date +%s)"
|
||||
docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" -e MQTT_PAYLOAD="$retained_payload" \
|
||||
restoretest-ha-mosquitto sh -lc \
|
||||
'mosquitto_pub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -m "$MQTT_PAYLOAD" -r'
|
||||
docker restart restoretest-ha-mosquitto >/dev/null
|
||||
sleep 3
|
||||
retained_result="$(docker exec -e MQTT_USER="$mqtt_user" -e MQTT_PASS="$mqtt_pass" -e MQTT_TOPIC="$retained_topic" \
|
||||
restoretest-ha-mosquitto sh -lc \
|
||||
'mosquitto_sub -h 127.0.0.1 -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" -t "$MQTT_TOPIC" -C 1 -W 10' | tr -d '\r')"
|
||||
if [ "$retained_result" != "$retained_payload" ]; then
|
||||
echo "MQTT retained smoke failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ha_http_status=""
|
||||
ha_body="$RESTORE_ROOT/ha-http-body.html"
|
||||
for _ in $(seq 1 180); do
|
||||
ha_http_status="$(curl -sS -o "$ha_body" -w '%{http_code}' http://127.0.0.1:18123/ || true)"
|
||||
if [ "$ha_http_status" = "200" ] && grep -qi "Home Assistant" "$ha_body"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ha_http_status" != "200" ] || ! grep -qi "Home Assistant" "$ha_body"; then
|
||||
echo "HA HTTP smoke failed, status=$ha_http_status" >&2
|
||||
docker logs --tail 120 restoretest-homeassistant >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ha_api_status="$(curl -sS -o "$RESTORE_ROOT/ha-api.json" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer $(cat "$HA_TOKEN_FILE")" \
|
||||
-H 'Content-Type: application/json' \
|
||||
http://127.0.0.1:18123/api/ || true)"
|
||||
if [ "$ha_api_status" != "200" ]; then
|
||||
echo "HA API token smoke failed, status=$ha_api_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RESTORE_ROOT="$RESTORE_ROOT" docker compose -f "$COMPOSE_FILE" exec -T restoretest-homeassistant \
|
||||
python -m homeassistant --script check_config --config /config >/tmp/restoretest-ha-check-config.out
|
||||
|
||||
write_report "$REPORT_FILE" <<EOF
|
||||
# Home Assistant Restore Test Report - $(date +%F)
|
||||
|
||||
- Service: \`homeassistant\` + \`smarthome-mosquitto\`
|
||||
- HA backup source: \`$latest_backup\`
|
||||
- Restore root: \`$RESTORE_ROOT\`
|
||||
- Test containers:
|
||||
- \`restoretest-homeassistant\`
|
||||
- \`restoretest-ha-mosquitto\`
|
||||
- Test endpoints:
|
||||
- HA: \`http://127.0.0.1:18123\`
|
||||
- MQTT: \`127.0.0.1:11883\`
|
||||
- Result: \`SUCCESS\`
|
||||
|
||||
## Checks
|
||||
|
||||
- HA-native backup tar readable: \`ok\`
|
||||
- HA inner archive restored: \`ok\`
|
||||
- HA backup size bytes: \`$backup_size\`
|
||||
- Restored HA file count: \`$ha_file_count\`
|
||||
- Restored HA bytes: \`$ha_bytes\`
|
||||
- Restored Mosquitto appdata bytes: \`$mosquitto_data_bytes\`
|
||||
- Fachrepo clone clean: \`ok\`
|
||||
- Fachrepo HEAD: \`$fachrepo_head\`
|
||||
- HA HTTP status: \`$ha_http_status\`
|
||||
- HA API token smoke: \`$ha_api_status\`
|
||||
- HA check_config: \`ok\`
|
||||
- MQTT publish/subscribe with restored credentials: \`ok\`
|
||||
- MQTT retained topic after broker restart: \`ok\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Productive \`homeassistant\` and \`smarthome-mosquitto\` containers were not used.
|
||||
- Test ran without Traefik and without the productive domain.
|
||||
- Test ports were bound to localhost only.
|
||||
- Token and MQTT password values were used for smoke tests but not printed.
|
||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||
EOF
|
||||
|
||||
RESTORE_SUCCESS=1
|
||||
echo "Home Assistant restore test ok -> $REPORT_FILE"
|
||||
@@ -55,6 +55,12 @@ case "$MODE" in
|
||||
fi
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.6.1@sha256:59aa8824955c9db491b75d2eebe42bd68494f80c2ec69ec0d66d9dae37d37514
|
||||
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
|
||||
Reference in New Issue
Block a user