Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 748a477a5d | |||
| ad9bb40b95 | |||
| 036eba99a8 | |||
| 47333a26e4 | |||
| 5c5a66da0e | |||
| 0a32a36225 | |||
| c4df83ea75 | |||
| 464f953ff1 | |||
| 98f250c68e | |||
| a5c855cf47 | |||
| 7bb7dad4a7 | |||
| 0847d839e7 | |||
| f775685cd2 | |||
| a137129c75 | |||
| 5ca4922d8d | |||
| 09381d932a | |||
| 1c183df8d2 | |||
| acc92e84e1 | |||
| 2844b63b37 | |||
| 99a8d9fb6a | |||
| 8002b197af | |||
| 4613da82e2 | |||
| 4079b1cbce | |||
| 7ded74aeef | |||
| 7d4d5f901a | |||
| ad8010767d | |||
| 02389ed292 | |||
| cbfbb8ca4f | |||
| ee0d450a27 | |||
| 79657d526c | |||
| 6870ae53da | |||
| 46d6010c66 | |||
| 5a0a4c9d56 | |||
| c4ba67b55c | |||
| 275558b2db | |||
| 3e9c12eb75 | |||
| 813d3bd303 | |||
| ad47979000 | |||
| 23a6975a67 | |||
| 81151d8af4 | |||
| 45ff8286cf | |||
| f318d80477 | |||
| b8d9bba5d3 | |||
| 3bebc03a8f | |||
| 0f1e78e0ca | |||
| 658750bc19 | |||
| 5afba298e9 |
@@ -95,6 +95,7 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme
|
|||||||
| `monitoring_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | Zielzustand |
|
| `monitoring_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | Zielzustand |
|
||||||
| `glance_socket_net` | Compose-intern, `internal: true` | interner Zugriff von Glance auf den Docker-Socket-Proxy | umgesetzt |
|
| `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 |
|
| `smarthome_net` | bridge, `internal: true` | interne Smart-Home-Kommunikation zwischen Home Assistant, Mosquitto, spaeter Zigbee2MQTT/ESPHome | vorbereitet |
|
||||||
|
| `healthchecks_internal` | bridge, `internal: true` | internes Netz nur fuer `healthchecks` + `healthchecks-postgres` | vorbereitet |
|
||||||
| `host` | host | nur für echte Sonderfälle | begründet |
|
| `host` | host | nur für echte Sonderfälle | begründet |
|
||||||
|
|
||||||
### 3.2 Finales Diagramm (vereinfacht)
|
### 3.2 Finales Diagramm (vereinfacht)
|
||||||
@@ -169,6 +170,7 @@ Diese Dienste sind **keine Public Apps**:
|
|||||||
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
||||||
- `super-productivity` — sp.kaleschke.info (Middleware)
|
- `super-productivity` — sp.kaleschke.info (Middleware)
|
||||||
- `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo)
|
- `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo)
|
||||||
|
- `healthchecks` — hc.kaleschke.info (Traefik, native Healthchecks-Auth; Ping-/API-Endpunkte ohne ForwardAuth analog n8n)
|
||||||
- `Traefik-Dashboard`
|
- `Traefik-Dashboard`
|
||||||
- `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung
|
- `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung
|
||||||
|
|
||||||
@@ -309,6 +311,8 @@ Legende Status:
|
|||||||
| `monitoring-influxdb3-core` | ✅ | `monitoring_net`, `monitoring_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP` | HA-Write-Token und Sensor-Export finalisieren |
|
| `monitoring-influxdb3-core` | ✅ | `monitoring_net`, `monitoring_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP` | HA-Write-Token und Sensor-Export finalisieren |
|
||||||
| `monitoring-loki` | ✅ | `monitoring_net` | intern | interner Container-Logspeicher ohne Public Route; Monitoring-Grafana greift ueber Loki-Datasource zu | Retention/Storage beobachten |
|
| `monitoring-loki` | ✅ | `monitoring_net` | intern | interner Container-Logspeicher ohne Public Route; Monitoring-Grafana greift ueber Loki-Datasource zu | Retention/Storage beobachten |
|
||||||
| `monitoring-promtail` | ✅ | `monitoring_net` | intern | Docker-Log-Collector mit read-only Docker-Socket-Ausnahme; schreibt nach Loki | Socket-Ausnahme regelmaessig pruefen |
|
| `monitoring-promtail` | ✅ | `monitoring_net` | intern | Docker-Log-Collector mit read-only Docker-Socket-Ausnahme; schreibt nach Loki | Socket-Ausnahme regelmaessig pruefen |
|
||||||
|
| `healthchecks` | ✅ | `frontend_net`, `healthchecks_internal` | Traefik, native Auth | self-hosted Heartbeat-Monitor fuer interne Jobs; Ping-/API ohne ForwardAuth (analog n8n); externe Host-down-/Backup-Waechter bleiben auf healthchecks.io-Cloud; live seit 2026-06-23 | Gitea-Webhook noch manuell anzulegen |
|
||||||
|
| `healthchecks-postgres` | ✅ | `healthchecks_internal` | intern | dedizierte PostgreSQL 18, nie `frontend_net` | — |
|
||||||
| `grafana` / `influxdb3-core` / `loki` / `alloy` | entfernt | - | abgeloest | alte Docker-Runtime frei von Altcontainern; Compose-Pfade am 2026-05-26 aus aktivem Repo entfernt | Rollback nur ueber Git-Historie |
|
| `grafana` / `influxdb3-core` / `loki` / `alloy` | entfernt | - | abgeloest | alte Docker-Runtime frei von Altcontainern; Compose-Pfade am 2026-05-26 aus aktivem Repo entfernt | Rollback nur ueber Git-Historie |
|
||||||
|
|
||||||
### 7.7 Noch offene Sonderfälle
|
### 7.7 Noch offene Sonderfälle
|
||||||
@@ -398,6 +402,7 @@ Die Blockmigration aus der Portainer-/Dockerman-Phase ist abgeschlossen: Traefik
|
|||||||
| `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-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 |
|
| `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) |
|
| `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) |
|
||||||
|
| `healthchecks` | keine pauschale Authelia-Middleware | Ping-Endpunkte (`/ping/*`) und API muessen ohne ForwardAuth erreichbar sein, sonst koennen Cron-Jobs nicht melden; Healthchecks bringt eigene Login-Auth fuers Dashboard mit (analog n8n/Komodo). Der self-hosted Dienst deckt bewusst nur INTERNE Job-Checks ab; Host-down-/Backup-Waechter bleiben extern auf healthchecks.io-Cloud |
|
||||||
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
||||||
| `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. |
|
| `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. |
|
||||||
| `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` (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). |
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ POSTGRES_USER=dawarich
|
|||||||
POSTGRES_DB=dawarich_production
|
POSTGRES_DB=dawarich_production
|
||||||
GRAFANA_DB_USER=dawarich_grafana_ro
|
GRAFANA_DB_USER=dawarich_grafana_ro
|
||||||
|
|
||||||
PHOTON_API_HOST=photon.komoot.io
|
|
||||||
PHOTON_API_USE_HTTPS=true
|
|
||||||
|
|
||||||
METRICS_USERNAME=prometheus
|
METRICS_USERNAME=prometheus
|
||||||
BACKGROUND_PROCESSING_CONCURRENCY=5
|
BACKGROUND_PROCESSING_CONCURRENCY=5
|
||||||
RAILS_MAX_THREADS=10
|
RAILS_MAX_THREADS=10
|
||||||
|
|||||||
+1
-14
@@ -67,7 +67,7 @@ install -d -m 750 \
|
|||||||
|
|
||||||
Die UI liegt auf `https://dawarich.kaleschke.info` und nutzt `authelia@file,secure-headers@file`.
|
Die UI liegt auf `https://dawarich.kaleschke.info` und nutzt `authelia@file,secure-headers@file`.
|
||||||
|
|
||||||
Der Healthcheck und die Tracking-API-Routen fuer OwnTracks, Overland und Traccar sind separat und priorisiert ohne Authelia geroutet, weil Mobile Clients per Dawarich-API-Key authentifizieren und keine Browser-ForwardAuth-Challenge verarbeiten koennen.
|
Der Healthcheck, die Mobile-App-API-Routen (`/api/v1/settings`, `/api/v1/points`, `/api/v1/tracks`, `/api/v1/tracks/<id>/points`) und die Tracking-API-Routen fuer OwnTracks, Overland und Traccar sind separat und priorisiert ohne Authelia geroutet, weil Mobile Clients per Dawarich-API-Key authentifizieren und keine Browser-ForwardAuth-Challenge verarbeiten koennen.
|
||||||
|
|
||||||
## Prometheus
|
## Prometheus
|
||||||
|
|
||||||
@@ -79,19 +79,6 @@ Der Monitoring-Stack ist dafuer bereits vorbereitet:
|
|||||||
|
|
||||||
Nicht `dawarich_app:9394` scrapen: das ist nach aktueller Dawarich-Doku veraltet. Der Web-Service aggregiert App- und Sidekiq-Metriken unter `/metrics`. Im KalliLab scrapt Prometheus intern `http://dawarich_app:3000/metrics` ueber `backend_net` und setzt `X-Forwarded-Proto: https`, damit Dawarich mit `APPLICATION_PROTOCOL=https` keinen HTTPS-Redirect erzeugt.
|
Nicht `dawarich_app:9394` scrapen: das ist nach aktueller Dawarich-Doku veraltet. Der Web-Service aggregiert App- und Sidekiq-Metriken unter `/metrics`. Im KalliLab scrapt Prometheus intern `http://dawarich_app:3000/metrics` ueber `backend_net` und setzt `X-Forwarded-Proto: https`, damit Dawarich mit `APPLICATION_PROTOCOL=https` keinen HTTPS-Redirect erzeugt.
|
||||||
|
|
||||||
Verifikation aus dem Prometheus-Container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PW="$(cat /run/secrets/dawarich_metrics_password)"
|
|
||||||
curl -i -u "prometheus:${PW}" http://dawarich_app:3000/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
Erwartung:
|
|
||||||
|
|
||||||
- `200`: Scrape ist direkt funktionsfaehig.
|
|
||||||
- `301`/`308` nach HTTPS: `http_headers` mit `X-Forwarded-Proto: https` im Prometheus-Job beibehalten.
|
|
||||||
- `403 Blocked host`: `dawarich_app` in `APPLICATION_HOSTS` aufnehmen.
|
|
||||||
|
|
||||||
## Grafana
|
## Grafana
|
||||||
|
|
||||||
Der Read-only-User `dawarich_grafana_ro` wird beim ersten DB-Init durch `postgres/initdb/20-grafana-readonly.sh` angelegt.
|
Der Read-only-User `dawarich_grafana_ro` wird beim ersten DB-Init durch `postgres/initdb/20-grafana-readonly.sh` angelegt.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
dawarich_redis:
|
dawarich_redis:
|
||||||
image: redis:7-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
image: redis:8.8.0-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
|
||||||
container_name: dawarich_redis
|
container_name: dawarich_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -93,8 +93,6 @@ services:
|
|||||||
TIME_ZONE: ${TZ}
|
TIME_ZONE: ${TZ}
|
||||||
SELF_HOSTED: "true"
|
SELF_HOSTED: "true"
|
||||||
STORE_GEODATA: "true"
|
STORE_GEODATA: "true"
|
||||||
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
|
||||||
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
|
||||||
RAILS_LOG_TO_STDOUT: "true"
|
RAILS_LOG_TO_STDOUT: "true"
|
||||||
PROMETHEUS_EXPORTER_ENABLED: "true"
|
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||||
METRICS_USERNAME: ${METRICS_USERNAME}
|
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||||
@@ -134,7 +132,7 @@ services:
|
|||||||
- traefik.docker.network=frontend_net
|
- traefik.docker.network=frontend_net
|
||||||
|
|
||||||
# Public API-key endpoints for mobile apps and Home Assistant pushes.
|
# Public API-key endpoints for mobile apps and Home Assistant pushes.
|
||||||
- traefik.http.routers.dawarich-api.rule=Host(`${DAWARICH_HOST}`) && (Path(`/api/v1/health`) || Path(`/api/v1/owntracks/points`) || Path(`/api/v1/overland/batches`) || Path(`/api/v1/traccar/points`))
|
- traefik.http.routers.dawarich-api.rule=Host(`${DAWARICH_HOST}`) && (Path(`/api/v1/health`) || Path(`/api/v1/settings`) || Path(`/api/v1/settings/transportation_recalculation_status`) || Path(`/api/v1/points`) || Path(`/api/v1/tracks`) || PathRegexp(`/api/v1/tracks/[0-9]+/points`) || Path(`/api/v1/owntracks/points`) || Path(`/api/v1/overland/batches`) || Path(`/api/v1/traccar/points`))
|
||||||
- traefik.http.routers.dawarich-api.entrypoints=websecure
|
- traefik.http.routers.dawarich-api.entrypoints=websecure
|
||||||
- traefik.http.routers.dawarich-api.tls=true
|
- traefik.http.routers.dawarich-api.tls=true
|
||||||
- traefik.http.routers.dawarich-api.tls.certresolver=le
|
- traefik.http.routers.dawarich-api.tls.certresolver=le
|
||||||
@@ -180,13 +178,11 @@ services:
|
|||||||
TIME_ZONE: ${TZ}
|
TIME_ZONE: ${TZ}
|
||||||
SELF_HOSTED: "true"
|
SELF_HOSTED: "true"
|
||||||
STORE_GEODATA: "true"
|
STORE_GEODATA: "true"
|
||||||
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
|
||||||
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
|
||||||
RAILS_LOG_TO_STDOUT: "true"
|
RAILS_LOG_TO_STDOUT: "true"
|
||||||
PROMETHEUS_EXPORTER_ENABLED: "true"
|
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||||
PROMETHEUS_EXPORTER_PORT: "9394"
|
PROMETHEUS_EXPORTER_PORT: "9394"
|
||||||
METRICS_USERNAME: ${METRICS_USERNAME}
|
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||||
BACKGROUND_PROCESSING_CONCURRENCY: "5"
|
BACKGROUND_PROCESSING_CONCURRENCY: ${BACKGROUND_PROCESSING_CONCURRENCY}
|
||||||
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS}
|
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS}
|
||||||
volumes:
|
volumes:
|
||||||
- dawarich_public:/var/app/public
|
- dawarich_public:/var/app/public
|
||||||
|
|||||||
@@ -20,12 +20,276 @@
|
|||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"id": null,
|
"id": null,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Points Last 30 Days",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
},
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS points\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND lonlat IS NOT NULL;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "points",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Kilometers Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT round(coalesce(sum(distance),0)::numeric / 1000.0, 2)::double precision AS km\nFROM tracks\nWHERE start_at >= now() - interval '30 days';"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "km",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "km",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Tracks Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS tracks\nFROM tracks\nWHERE start_at >= now() - interval '30 days';"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "tracks",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Anomalies Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS anomalies\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND anomaly IS TRUE;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "anomalies",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "geomap",
|
||||||
|
"title": "Location Points",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 4,
|
||||||
|
"w": 14,
|
||||||
|
"h": 12
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n ST_Y(lonlat::geometry)::double precision AS lat,\n ST_X(lonlat::geometry)::double precision AS lon,\n accuracy::double precision AS accuracy,\n to_timestamp(timestamp) AS seen_at\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 5000;"
|
||||||
|
}
|
||||||
|
],
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"custom": {
|
"custom": {
|
||||||
@@ -44,18 +308,10 @@
|
|||||||
"value": null
|
"value": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"unit": "none"
|
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": {
|
|
||||||
"h": 16,
|
|
||||||
"w": 16,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
"options": {
|
||||||
"basemap": {
|
"basemap": {
|
||||||
"config": {},
|
"config": {},
|
||||||
@@ -73,42 +329,29 @@
|
|||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"showLegend": true,
|
"showLegend": false,
|
||||||
"style": {
|
"style": {
|
||||||
"color": {
|
"color": {
|
||||||
"fixed": "dark-green"
|
"fixed": "green"
|
||||||
},
|
|
||||||
"opacity": 0.55,
|
|
||||||
"rotation": {
|
|
||||||
"fixed": 0,
|
|
||||||
"max": 360,
|
|
||||||
"min": -360,
|
|
||||||
"mode": "mod"
|
|
||||||
},
|
},
|
||||||
|
"opacity": 0.7,
|
||||||
"size": {
|
"size": {
|
||||||
"fixed": 4,
|
"fixed": 5,
|
||||||
"max": 15,
|
"max": 15,
|
||||||
"min": 2
|
"min": 2
|
||||||
},
|
},
|
||||||
"symbol": {
|
"symbol": {
|
||||||
"fixed": "img/icons/marker/circle.svg",
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
"mode": "fixed"
|
"mode": "fixed"
|
||||||
},
|
|
||||||
"textConfig": {
|
|
||||||
"fontSize": 12,
|
|
||||||
"offsetX": 0,
|
|
||||||
"offsetY": 0,
|
|
||||||
"textAlign": "center",
|
|
||||||
"textBaseline": "middle"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"latitude": "latitude",
|
"latitude": "lat",
|
||||||
"longitude": "longitude",
|
"longitude": "lon",
|
||||||
"mode": "coords"
|
"mode": "coords"
|
||||||
},
|
},
|
||||||
"name": "Location points",
|
"name": "Points",
|
||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"type": "markers"
|
"type": "markers"
|
||||||
}
|
}
|
||||||
@@ -119,14 +362,30 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"allLayers": true,
|
"allLayers": true,
|
||||||
"id": "fit",
|
"id": "fit",
|
||||||
"lat": 51,
|
"lat": 52.0,
|
||||||
"lon": 10,
|
"lon": 7.5,
|
||||||
"zoom": 5
|
"zoom": 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Kilometers per Day",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
"pluginVersion": "13.0.2",
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 4,
|
||||||
|
"w": 10,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
|
"refId": "A",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
@@ -134,53 +393,15 @@
|
|||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"format": "table",
|
"format": "table",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
"rawSql": "SELECT\n date_trunc('day', start_at)::date AS day,\n round(coalesce(sum(distance),0)::numeric / 1000.0, 2)::double precision AS km\nFROM tracks\nWHERE start_at >= now() - interval '30 days'\nGROUP BY 1\nORDER BY 1 DESC;"
|
||||||
"refId": "A"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Location Points",
|
|
||||||
"type": "geomap"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
"custom": {
|
||||||
"axisBorderShow": false,
|
"align": "auto",
|
||||||
"axisCenteredZero": false,
|
"cellOptions": {
|
||||||
"axisColorMode": "text",
|
"type": "auto"
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "never",
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
@@ -192,152 +413,153 @@
|
|||||||
"value": null
|
"value": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"unit": "km"
|
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
"options": {
|
||||||
"legend": {
|
"cellHeight": "sm",
|
||||||
"calcs": [
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
"sum"
|
"sum"
|
||||||
],
|
],
|
||||||
"displayMode": "list",
|
"show": false
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"showHeader": true
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pluginVersion": "13.0.2",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"format": "time_series",
|
|
||||||
"rawQuery": true,
|
|
||||||
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Kilometers per Month",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "never",
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"options": {
|
"type": "table",
|
||||||
"legend": {
|
"title": "Points per Day",
|
||||||
"calcs": [
|
"datasource": {
|
||||||
"sum"
|
"type": "postgres",
|
||||||
],
|
"uid": "dawarich-postgres"
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"pluginVersion": "13.0.2",
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 10,
|
||||||
|
"w": 10,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
|
"refId": "A",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"format": "time_series",
|
"format": "table",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp))::date AS day,\n count(*)::double precision AS points\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\nGROUP BY 1\nORDER BY 1 DESC;"
|
||||||
"refId": "A"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Points per Day",
|
"fieldConfig": {
|
||||||
"type": "timeseries"
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Recent Tracks",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 16,
|
||||||
|
"w": 24,
|
||||||
|
"h": 7
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n start_at AS start,\n end_at AS end,\n round((distance::numeric / 1000.0), 2)::double precision AS km,\n round((duration::numeric / 60.0), 1)::double precision AS minutes\nFROM tracks\nWHERE start_at >= now() - interval '30 days'\nORDER BY start_at DESC\nLIMIT 50;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"preload": false,
|
|
||||||
"refresh": "5m",
|
"refresh": "5m",
|
||||||
"schemaVersion": 41,
|
"schemaVersion": 41,
|
||||||
"tags": [
|
"tags": [
|
||||||
"dawarich",
|
"homelab",
|
||||||
"location"
|
"dawarich"
|
||||||
],
|
],
|
||||||
"templating": {
|
"templating": {
|
||||||
"list": []
|
"list": []
|
||||||
@@ -350,6 +572,6 @@
|
|||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Dawarich",
|
"title": "Dawarich",
|
||||||
"uid": "dawarich",
|
"uid": "dawarich",
|
||||||
"version": 1,
|
"version": 5,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ datasources:
|
|||||||
user: dawarich_grafana_ro
|
user: dawarich_grafana_ro
|
||||||
editable: false
|
editable: false
|
||||||
jsonData:
|
jsonData:
|
||||||
|
database: dawarich_production
|
||||||
sslmode: disable
|
sslmode: disable
|
||||||
postgresVersion: 1700
|
postgresVersion: 1700
|
||||||
timescaledb: false
|
timescaledb: false
|
||||||
|
|||||||
@@ -3,33 +3,22 @@ set -eu
|
|||||||
|
|
||||||
GRAFANA_USER="${GRAFANA_DB_USER:-dawarich_grafana_ro}"
|
GRAFANA_USER="${GRAFANA_DB_USER:-dawarich_grafana_ro}"
|
||||||
GRAFANA_PASSWORD="$(cat /run/secrets/dawarich_grafana_ro_password)"
|
GRAFANA_PASSWORD="$(cat /run/secrets/dawarich_grafana_ro_password)"
|
||||||
|
export GRAFANA_USER GRAFANA_PASSWORD
|
||||||
|
|
||||||
sql_ident() {
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'EOSQL'
|
||||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
|
\set grafana_user `printf %s "$GRAFANA_USER"`
|
||||||
}
|
\set grafana_password `printf %s "$GRAFANA_PASSWORD"`
|
||||||
|
|
||||||
sql_literal() {
|
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'grafana_user', :'grafana_password')
|
||||||
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
|
WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'grafana_user')
|
||||||
}
|
\gexec
|
||||||
|
|
||||||
DB_IDENT="$(sql_ident "$POSTGRES_DB")"
|
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'grafana_user', :'grafana_password')
|
||||||
USER_IDENT="$(sql_ident "$GRAFANA_USER")"
|
WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'grafana_user')
|
||||||
USER_LITERAL="$(sql_literal "$GRAFANA_USER")"
|
\gexec
|
||||||
PASSWORD_LITERAL="$(sql_literal "$GRAFANA_PASSWORD")"
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
|
SELECT format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), :'grafana_user')\gexec
|
||||||
DO \$\$
|
SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'grafana_user')\gexec
|
||||||
BEGIN
|
SELECT format('GRANT SELECT ON ALL TABLES IN SCHEMA public TO %I', :'grafana_user')\gexec
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = ${USER_LITERAL}) THEN
|
SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO %I', :'grafana_user')\gexec
|
||||||
EXECUTE 'CREATE ROLE ${USER_IDENT} LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
|
||||||
ELSE
|
|
||||||
EXECUTE 'ALTER ROLE ${USER_IDENT} WITH LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
\$\$;
|
|
||||||
|
|
||||||
GRANT CONNECT ON DATABASE ${DB_IDENT} TO ${USER_IDENT};
|
|
||||||
GRANT USAGE ON SCHEMA public TO ${USER_IDENT};
|
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${USER_IDENT};
|
|
||||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${USER_IDENT};
|
|
||||||
EOSQL
|
EOSQL
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
image: redis:8.8.0-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- immich_default
|
- immich_default
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
mail-archiver:
|
mail-archiver:
|
||||||
image: s1t5/mailarchiver@sha256:9ab6f51fa036c7869f64cb052a18f7bb8b9951a120ce1c03df43a273a20d3f59
|
image: s1t5/mailarchiver@sha256:be7a56d1a97dd223408300ee9009bcb5b06be9c67bfd818181a0036f3cfdec12
|
||||||
container_name: mail-archiver
|
container_name: mail-archiver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ services:
|
|||||||
- traefik.http.services.mealie.loadbalancer.server.port=9000
|
- traefik.http.services.mealie.loadbalancer.server.port=9000
|
||||||
|
|
||||||
mealie-postgres:
|
mealie-postgres:
|
||||||
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
image: postgres:18.4@sha256:1a5b3e745bbd82d6deb146505e504da3c2f248cac15e431951b148fbe4f8613a
|
||||||
container_name: mealie-postgres
|
container_name: mealie-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
n8n:
|
n8n:
|
||||||
image: docker.n8n.io/n8nio/n8n:2.27.3@sha256:a772d24e6b4f9b3848be5a57c5e45437eed1965bbbcefa2f9a93f4835b6639fa
|
image: docker.n8n.io/n8nio/n8n:2.28.0@sha256:ddd2afb595bf4507c4147b34de9a4690bab042124e84ee3ceba16f2db2459d22
|
||||||
container_name: n8n
|
container_name: n8n
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: nextcloud:34.0.0-apache@sha256:851ca6ef9da101ce3c8a32ec7b6fc65a726b380b5f466307a54c17d32fb77c9a
|
image: nextcloud:34.0.0-apache@sha256:4d4a6b5ed15a7eb4537538c848eb78833f53bed62f93e7d5af144f360cd53ff2
|
||||||
container_name: nextcloud
|
container_name: nextcloud
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
|
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
|
||||||
|
|
||||||
nextcloud-postgres:
|
nextcloud-postgres:
|
||||||
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
image: postgres:18.4@sha256:1a5b3e745bbd82d6deb146505e504da3c2f248cac15e431951b148fbe4f8613a
|
||||||
container_name: nextcloud-postgres
|
container_name: nextcloud-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -64,7 +64,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
nextcloud-redis:
|
nextcloud-redis:
|
||||||
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
image: redis:8.8.0-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
|
||||||
container_name: nextcloud-redis
|
container_name: nextcloud-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --save 60 1 --loglevel warning
|
command: redis-server --save 60 1 --loglevel warning
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
super-productivity:
|
super-productivity:
|
||||||
image: johannesjo/super-productivity:v18.12.0@sha256:2c84668a961b090dd931f6e117dde5195b7c674d8453e0d511b777c23c242bc8
|
image: johannesjo/super-productivity:v18.12.1@sha256:a108244f331a1d165f4c52ad343efe739059a078e5f5993f010daf882a53f09e
|
||||||
container_name: super-productivity
|
container_name: super-productivity
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,6 @@
|
|||||||
# Alert Rules
|
# Alert Rules
|
||||||
|
|
||||||
Stand: 2026-06-18
|
Stand: 2026-06-23
|
||||||
|
|
||||||
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
||||||
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
||||||
@@ -18,6 +18,25 @@ Skripten unter `services/posture-check/`.
|
|||||||
| Borg Pre-Hook | `ops/borg-ui/scripts/pre-borg.sh` | ntfy `homelab-alerts` |
|
| Borg Pre-Hook | `ops/borg-ui/scripts/pre-borg.sh` | ntfy `homelab-alerts` |
|
||||||
| Restore Jobs | `ops/restore-tests/run-restore-job-with-ntfy.sh` | Fehler `homelab-alerts`, Erfolg `homelab-info` |
|
| Restore Jobs | `ops/restore-tests/run-restore-job-with-ntfy.sh` | Fehler `homelab-alerts`, Erfolg `homelab-info` |
|
||||||
|
|
||||||
|
## End-to-End-Drills
|
||||||
|
|
||||||
|
Stand 2026-06-23 sind die produktionsnahen Alarmwege bis zum Handy des
|
||||||
|
Operators synthetisch belegt:
|
||||||
|
|
||||||
|
| Pfad | Test | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| Direkter ntfy-Kanal | `ops/restore-tests/send-ntfy.sh homelab-alerts ...` | Handy-Empfang bestaetigt |
|
||||||
|
| Restore-Wrapper Fehler | `run-restore-job-with-ntfy.sh codex-negative-alert-test homelab-info` | `Restore job failed: codex-negative-alert-test` am Handy bestaetigt |
|
||||||
|
| Restore-Freshness Negativtest | `run-restore-checks.sh freshness-negative` | `TEST: Restore freshness alert path ok` am Handy bestaetigt; produktive Dumps nicht veraendert |
|
||||||
|
| Alertmanager -> Bridge -> ntfy | `amtool alert add HomelabAlertChainTest ...` im `monitoring-alertmanager`-Container | `FIRING` und `RESOLVED` am Handy bestaetigt; Bridge loggte `sent 1 ntfy notifications` |
|
||||||
|
| Docker Critical Events | `test-docker-critical-events.sh` + `docker-critical-events-supervisor.sh smoke` | Filtertest gruen; `Docker critical watcher smoke` am Handy bestaetigt |
|
||||||
|
| Borg Pre-Hook Fehler | `PRE_BACKUP_DUMPS=/bin/false POSTURE_CHECK=/bin/true FRESHNESS_CHECK=/bin/true pre-borg.sh` | `Borg pre-hook failed: pre-backup-dumps` am Handy bestaetigt; keine echten Backups/Dumps angefasst |
|
||||||
|
|
||||||
|
Nicht separat belegt wurde eine temporaere Prometheus-Testregel bis
|
||||||
|
Alertmanager. Der kritischere Pfad Alertmanager -> Bridge -> ntfy -> Handy ist
|
||||||
|
bewiesen; Prometheus-Regelladung wird im Monitoring-Reload-/Alert-Smoke des
|
||||||
|
Monitoring-Hardening-Punkts mitgeprueft.
|
||||||
|
|
||||||
## Prometheus-Regeln
|
## Prometheus-Regeln
|
||||||
|
|
||||||
| Alarm | Ausloeser | Severity | Aktion |
|
| Alarm | Ausloeser | Severity | Aktion |
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Auth-Matrix
|
||||||
|
|
||||||
|
Typ: Inventar/Referenz · Stand: 2026-06-23 · Status: aktiv
|
||||||
|
|
||||||
|
Konsolidierte Review-Sicht auf die **effektive Zugriffskontrolle je oeffentlicher
|
||||||
|
UI/Domain**. Bisher lag das verstreut: Authelia-ACL nur in der Live-Config, die
|
||||||
|
Bypass-/Ausnahmefaelle in `HOMELAB_ARCHITECTURE_MASTER_V2.md` (Prosa + Service-
|
||||||
|
und Ausnahmen-Tabellen) und die Begruendungen in `DECISIONS.md`. Diese Datei
|
||||||
|
buendelt das an **einem** Ort und **verlinkt** auf die Quellen statt zu kopieren.
|
||||||
|
|
||||||
|
## Quelle der Wahrheit
|
||||||
|
|
||||||
|
- **Authelia-ACL** (bypass / two_factor / Catch-all): `security/authelia/configuration.yml`
|
||||||
|
(`access_control`). Repo-zu-Host-Drift prueft `services/authelia-diff.sh`.
|
||||||
|
- **Dienste ohne ForwardAuth** (bewusste Ausnahmen): Ausnahmen-Tabelle in
|
||||||
|
`HOMELAB_ARCHITECTURE_MASTER_V2.md` + die jeweiligen `DECISIONS.md`-Eintraege.
|
||||||
|
- **Nicht-Authelia-Schutzschichten** (Tailscale-Bind, Traefik-IP-Allowlist): die
|
||||||
|
jeweilige `docker-compose.yml` per Label bzw. Host-Bind.
|
||||||
|
|
||||||
|
> Diese Matrix ist die kommentierte Lese-/Review-Sicht. Bei Widerspruch gewinnen
|
||||||
|
> die oben genannten Quellen; dann diese Datei nachziehen.
|
||||||
|
|
||||||
|
## Policy-Klassen
|
||||||
|
|
||||||
|
- **Bypass** — Authelia laesst durch, **kein 2FA**; Schutz liegt allein in der
|
||||||
|
App-eigenen Anmeldung. Bewusst fuer Public-Apps mit eigenen Clients.
|
||||||
|
- **two_factor** — Authelia-ForwardAuth mit 2FA (Operator-TOTP), `authelia@file`.
|
||||||
|
- **Keine Authelia (native)** — bewusste Ausnahme ohne ForwardAuth, App-Auth bleibt.
|
||||||
|
- **Tailscale-only / LAN-only** — gar nicht oeffentlich, kein Traefik bzw. an
|
||||||
|
Tailscale-/LAN-IP gebunden.
|
||||||
|
- **IP-Allowlist** — oeffentlich geroutet, aber per Traefik-Middleware auf
|
||||||
|
vertrauenswuerdige Quell-Netze begrenzt (sonst 403).
|
||||||
|
|
||||||
|
## Matrix
|
||||||
|
|
||||||
|
| UI / Domain | Effektive Policy | Mechanismus | Quelle / Begruendung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `auth.kaleschke.info` | Bypass (Authelia selbst) | — | muss immer erreichbar sein |
|
||||||
|
| `immich`, `paperless`, `mealie`, `vault`, `ntfy`, `git` `.kaleschke.info` | **Bypass** → native App-Auth, kein 2FA | Authelia laesst durch | `configuration.yml`; Public-Apps mit eigener Auth/Clients |
|
||||||
|
| `vault.kaleschke.info/admin` | **IP-Allowlist** (Tailnet `100.64.0.0/10` + LAN `192.168.178.0/24`), sonst 403 | Traefik `ipallowlist` (Label) | DECISIONS 2026-06-23 (Audit-P1) |
|
||||||
|
| `files.kaleschke.info`, `scrutiny.kaleschke.info` | **two_factor** (explizit) | `authelia@file` | `configuration.yml`; scrutiny zusaetzlich privileged |
|
||||||
|
| uebrige `*.kaleschke.info` mit Middleware (monitoring/grafana, glances, glance, speedtest, bentopdf, mail-archiver, paperless-gpt, hermes, super-productivity, borg-ui, code-server) | **two_factor** (Catch-all) | `authelia@file` Catch-all | `configuration.yml`; Haertung 2026-06-06 |
|
||||||
|
| `komodo.kaleschke.info` | **IP-Allowlist** (Tailnet + LAN), sonst 403; native (keine Authelia) | Traefik `ipallowlist` (Label) + native Komodo-Auth | DECISIONS 2026-06-23; Webhooks/Periphery laufen intern, nicht ueber Traefik |
|
||||||
|
| `nextcloud` | Keine Authelia (native) | WebDAV/CardDAV/Client-Flows | DECISIONS 2026-04 / Ausnahmen-Tabelle |
|
||||||
|
| `n8n.kaleschke.info` | Keine pauschale Authelia (native) | Webhook-Endpunkte `/webhook/*` | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check |
|
||||||
|
| `plex.kaleschke.info` | Keine Authelia (native Plex) | File-Provider-Route; WAN-Port 32400 + Remote Access aus | DECISIONS 2026-05-28 |
|
||||||
|
| `home.kaleschke.info` (homeassistant) | Keine Authelia (native HA) | Traefik + `smarthome_net`; LAN-Port 8123 | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check |
|
||||||
|
| AdGuard-Admin | **Tailscale-only**, nicht oeffentlich | Host-Bind `100.80.98.33:8082`, kein Traefik | DECISIONS 2026-05-26 |
|
||||||
|
| `influxdb3-core` :8181 | **LAN-only** Writer (HA) | Host-Port, kein Traefik, nicht in `frontend_net` | dokumentierte Ausnahme |
|
||||||
|
|
||||||
|
## Review-Gaps (Audit 2026-06-23)
|
||||||
|
|
||||||
|
- **Komodo**: beschlossen IP-Allowlist (Tailnet + LAN) statt public (DECISIONS
|
||||||
|
2026-06-23). Self-Stack ist inline in Komodo verwaltet → Labels via Komodo-UI
|
||||||
|
setzen (Task #6), dann verifizieren (#7).
|
||||||
|
- **Bypass-Liste bewusst ohne 2FA**: Bei App-CVE oder Account-Kompromiss greift
|
||||||
|
davor kein Authelia. Akzeptiert fuer Public-Apps mit eigenen Clients; Review-
|
||||||
|
Trigger = neue sensible App in der Liste oder veraendertes Risikoprofil.
|
||||||
|
- **Middleware-Abweichungen** (policy-check `TRAEFIK002`): `n8n` und
|
||||||
|
`homeassistant` sind erwartbar (native Ausnahmen). `grafana` steht **nicht** in
|
||||||
|
der Ausnahmen-Tabelle und ist als Catch-all-`two_factor` gefuehrt — die
|
||||||
|
abweichende Middleware ist live zu bestaetigen (offen).
|
||||||
|
|
||||||
|
## Pflege
|
||||||
|
|
||||||
|
Diese Matrix bei jeder Aenderung an Authelia-ACL, vorgeschalteter Middleware,
|
||||||
|
Tailscale-Bind oder IP-Allowlist mitziehen. Zugehoerige Drift-Erkennung:
|
||||||
|
`services/authelia-diff.sh` (ACL), `ops/policy-checks/check_repo.ps1` (Middleware-
|
||||||
|
Standard `TRAEFIK002`).
|
||||||
@@ -11,6 +11,111 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-23 - Healthchecks Hybrid: self-hosted fuer interne Jobs, Cloud fuer Host-down-Waechter
|
||||||
|
|
||||||
|
**Entscheidung:** Job-/Cron-Monitoring per Healthchecks (Dead-Man's-Switch) wird
|
||||||
|
hybrid betrieben. Ein **self-gehosteter** Healthchecks-Stack (`ops/healthchecks/`,
|
||||||
|
`hc.kaleschke.info`) ist der Hub fuer die vielen **internen** Checks auf einem
|
||||||
|
laufenden Host ("lief Job X heute?": posture-check, restore-tests, Dump-Erzeugung,
|
||||||
|
gitea-bundle-mirror). Die drei host-down-/backup-still-Waechter
|
||||||
|
(Borg-Pre-Hook `ops/borg-ui/scripts/pre-borg.sh`, baerchen-Nearline-Pull
|
||||||
|
`ops/h-drive-nearline/pull-critical-backups.ps1`, geplanter Monitoring-Watchdog
|
||||||
|
Empf. #8) bleiben bewusst **extern** auf healthchecks.io-Cloud (Free-Tier).
|
||||||
|
|
||||||
|
**Kontext:** Die zwei bestehenden Borg-/Nearline-Pings waren bereits
|
||||||
|
endpoint-agnostisch verdrahtet (`docs/SECRETS_MAP.md`), nur die Spielart
|
||||||
|
(Cloud vs. self-host) war offen (`docs/homelab-optimierung.md` Offene Frage #4).
|
||||||
|
Ein Waechter, der auf demselben Unraid-Host laeuft, den er ueberwacht, kann einen
|
||||||
|
Host-Ausfall nicht melden — er ist dann selbst tot, Stille ist nicht von "alles
|
||||||
|
gut" unterscheidbar. Genau dafuer existieren diese drei Checks, daher muessen sie
|
||||||
|
extern bleiben. Fuer "lief Job X auf einem lebenden Host?" ist Self-Hosting
|
||||||
|
dagegen unbedenklich (Host-down ist separat ueber die externen Pings abgedeckt)
|
||||||
|
und bringt Datenhoheit + unbegrenzte Checks (Cloud-Free = 20).
|
||||||
|
|
||||||
|
**Umsetzung / Ausnahme:** `healthchecks` haengt in `frontend_net` (Traefik, native
|
||||||
|
Healthchecks-Auth, **ohne** pauschale `authelia@file`: Ping-`/ping/*`- und
|
||||||
|
API-Endpunkte muessen ohne ForwardAuth erreichbar sein, analog n8n/Komodo — in
|
||||||
|
`ops/policy-checks/exceptions.json` als `middleware_exempt_identities` registriert).
|
||||||
|
Dedizierte PostgreSQL 18 nur in `healthchecks_internal` (`internal: true`).
|
||||||
|
SMTP bewusst aus (Login via Superuser, Alerts via ntfy-Integration nach
|
||||||
|
`homelab-alerts`). Secrets: `SECRET_KEY`/`DB_PASSWORD`/Superuser als
|
||||||
|
Komodo-Stack-ENV, Postgres zusaetzlich als Datei-Secret.
|
||||||
|
|
||||||
|
**Alternativen:** (a) Nur Cloud fuer alles — schlanker (kein Stack, kein Postgres),
|
||||||
|
aber 20-Check-Limit und Metadaten-Abfluss; (b) alles self-host inkl. Backup-Waechter
|
||||||
|
— verworfen wegen des Host-down-Blind-Spots; (c) ntfy-Heartbeat von einem zweiten
|
||||||
|
Geraet statt Cloud (Optionsnotiz in Empf. #4) — bleibt moeglich, falls die
|
||||||
|
Cloud-Abhaengigkeit spaeter unerwuenscht ist. **Review-Trigger:** mehr als ~20
|
||||||
|
externe Checks noetig, Wunsch null Cloud-Abhaengigkeit, oder zweite Hardware (dann
|
||||||
|
koennte der interne Hub dorthin und auch Host-down abdecken).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-23 - Komodo nur aus vertrauenswuerdigen Netzen (IP-Allowlist statt public)
|
||||||
|
|
||||||
|
**Entscheidung:** Der Komodo-Router (`komodo.kaleschke.info`) bekommt eine
|
||||||
|
Label-definierte `ipallowlist`-Middleware auf Tailnet `100.64.0.0/10` + LAN
|
||||||
|
`192.168.178.0/24`; public-Zugriff liefert kuenftig `403`. KEINE ForwardAuth
|
||||||
|
(die bewusste Komodo-Ausnahme bleibt), `KOMODO_HOST` bleibt
|
||||||
|
`https://komodo.kaleschke.info`. Der GANZE Router wird begrenzt, kein
|
||||||
|
pfadbasierter Public-Bypass.
|
||||||
|
|
||||||
|
**Kontext:** Audit 2026-06-23 (P1): Komodo war public mit `200` erreichbar und
|
||||||
|
koppelt ueber den RW-Docker-Socket der Periphery an Host-root-aehnliche Macht
|
||||||
|
(Core -> Periphery -> `docker.sock` -> jeder Container/Datenpfad). Read-only
|
||||||
|
gemessen: Gitea→Komodo-Webhooks (`/listener`) und Periphery (`/ws/periphery`)
|
||||||
|
laufen INTERN ueber `komodo-core:9120`, NICHT ueber Traefik. Der public Router
|
||||||
|
hat damit keine legitimen externen Consumer; eine Allowlist auf dem ganzen Router
|
||||||
|
schliesst die Public-Flaeche, ohne Automation zu brechen.
|
||||||
|
|
||||||
|
**Umsetzung / Ausnahme:** Der Komodo-Self-Stack ist inline in Komodo verwaltet
|
||||||
|
(`repo=""`, `files_on_host=false`, `webhook_enabled=false`, vgl. 2026-05-04),
|
||||||
|
KEIN GitOps-Push-Stack. Die Labels werden in der Komodo-UI am Inline-Compose
|
||||||
|
gesetzt; `ops/komodo/docker-compose.yml` ist nur Spiegel/Doku und wird zur
|
||||||
|
Paritaet nachgezogen.
|
||||||
|
|
||||||
|
**Alternativen:** Reines Tailscale-only (Route + public DNS-Record raus,
|
||||||
|
`KOMODO_HOST` auf Tailscale-Host) — strenger (kein 403-Endpunkt, keine
|
||||||
|
Hostname-Disclosure), aber mehr Aufwand und geaenderter Operator-Zugriff;
|
||||||
|
verworfen zugunsten des bewaehrten, minimalen Allowlist-Musters (analog Vault
|
||||||
|
/admin). **Review-Trigger:** Wunsch nach vollstaendiger Unsichtbarkeit von aussen
|
||||||
|
oder Aenderung am Komodo-Zugriffspfad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-23 - Vaultwarden /admin nur aus vertrauenswuerdigen Netzen (IP-Allowlist)
|
||||||
|
|
||||||
|
**Entscheidung:** Das Vaultwarden-Admin-Panel `/admin` bekommt einen zweiten,
|
||||||
|
hoeher priorisierten Traefik-Router `vaultwarden-admin` (Regel Host +
|
||||||
|
PathPrefix `/admin`, `priority=100`) mit einer Label-definierten
|
||||||
|
`ipallowlist`-Middleware auf Tailnet `100.64.0.0/10` + LAN `192.168.178.0/24`.
|
||||||
|
Der Hauptrouter bleibt unveraendert nativ (Browser-Extension, Mobile-Clients,
|
||||||
|
WebSocket `/notifications/hub`), damit normale Vault-Nutzung von ueberall
|
||||||
|
funktioniert. Public-Zugriff auf `/admin` liefert kuenftig `403`.
|
||||||
|
|
||||||
|
**Kontext:** Empirischer Audit 2026-06-23 (P1): `/admin` antwortete public mit
|
||||||
|
`200`, obwohl `SIGNUPS_ALLOWED=false`, `INVITATIONS_ALLOWED=false` und
|
||||||
|
`ADMIN_TOKEN_FILE` gesetzt sind. Der Admin-Token bleibt damit oeffentlich brute-
|
||||||
|
und CVE-exponiert. Gleiche Logik wie AdGuard-Admin (Entscheidung 2026-05-26,
|
||||||
|
Tailscale-only), hier aber pfadbasiert ueber Traefik statt Host-Port-Bind, weil
|
||||||
|
Vaultwarden nur einen Container-Port hat. Definition als Docker-Label (nicht
|
||||||
|
File-Provider), damit Komodo die Middleware mitdeployed.
|
||||||
|
|
||||||
|
**Alternativen:** (a) Authelia `two_factor` auf `/admin` — verworfen als
|
||||||
|
Primaerloesung, weil der Endpunkt dann public erreichbar bliebe; bleibt Fallback,
|
||||||
|
falls die Quelle-IP ueber den Operator-Zugriffspfad nicht zuverlaessig im
|
||||||
|
Allowlist-Bereich landet. (b) Reines Tailscale-only ohne LAN — strenger, aber
|
||||||
|
LAN bewusst als Break-glass behalten (im Bedrohungsmodell vertrauenswuerdig),
|
||||||
|
um Self-Lockout zu vermeiden.
|
||||||
|
|
||||||
|
**Abhaengigkeit / Review-Trigger:** Wirkt nur, wenn `/admin`-Zugriff mit einer
|
||||||
|
Quelle aus `100.64.0.0/10` oder `192.168.178.0/24` an Traefik ankommt — vor
|
||||||
|
finaler Abnahme per Traefik-Access-Log und `curl` aus public + Tailscale/LAN
|
||||||
|
verifizieren. Review bei Aenderung an Vault-Routing, Tailnet-CIDR oder Umstieg
|
||||||
|
auf reines Tailscale-only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-06-16 - Immich ML bekommt dediziertes Egress-Netz (Modell-Download)
|
## 2026-06-16 - Immich ML bekommt dediziertes Egress-Netz (Modell-Download)
|
||||||
|
|
||||||
**Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default`
|
**Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default`
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# Master To-do - KalliLab CORE
|
# Master To-do - KalliLab CORE
|
||||||
|
|
||||||
Typ: Status/To-do · Stand: 2026-06-21 · Status: aktiv
|
Typ: Status/To-do · Stand: 2026-06-23 · Status: aktiv
|
||||||
|
|
||||||
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
|
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
|
||||||
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
|
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
|
||||||
@@ -26,7 +26,7 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie.
|
|||||||
| Authelia OIDC fuer Apps | Operator/Codex | Live: Grafana + Mealie login-verifiziert; Paperless Secret verdrahtet und Service-Smoke am 2026-06-17 gruen, finaler Browser-Login mit Operator-Account offen. Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` |
|
| Authelia OIDC fuer Apps | Operator/Codex | Live: Grafana + Mealie login-verifiziert; Paperless Secret verdrahtet und Service-Smoke am 2026-06-17 gruen, finaler Browser-Login mit Operator-Account offen. Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` |
|
||||||
| Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` |
|
| Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` |
|
||||||
| Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` |
|
| Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` |
|
||||||
| Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | alertmanager/blackbox/loki/promtail + alertmanager-ntfy-bridge lokal auf Directory-Mounts umgestellt (grafana-provisioning war bereits Directory-Mount); `docker compose config` gruen. **Verbleibt:** Push + Komodo-Redeploy des monitoring-Stacks mit `--force-recreate` (Mount-Pfade aendern sich), danach Reload-/Alert-Smoke | `monitoring/docker-compose.yml` |
|
| Healthchecks self-hosted (interne Jobs) | Operator | **Live seit 2026-06-23** auf `https://hc.kaleschke.info` (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`, beide Container healthy, Superuser angelegt). Gitea->Komodo-Webhook seit 2026-06-23 aktiv. Projekt `KalliLab CORE` + ntfy-Integration (`homelab-alerts`). **7 interne Jobs verdrahtet + verifiziert (Status `up`):** `posture-check` (stuendlich), `cert-token-check`/`compose-runtime-drift`/`daily-status-report` (taeglich), `komodo-stack-hygiene` (woechentlich), `renovate`/`gitea-bundle-mirror` (alle 6h) - je endpoint-agnostischer Ping via EXIT-Trap, Capability-URL als Host-Secret `healthchecks_<job>_url`. **Bewusst NICHT self-hosted:** Borg-Pre-Hook + Nearline bleiben healthchecks.io-Cloud (Host-down-Erkennung); guarded Restore-Jobs (Vaultwarden/Gitea/Paperless/Authelia/Immich) sind wegen Shell-Guard nicht je Cron-Trigger monitorbar. **Verbleibt nur noch optional:** die 2 externen Cloud-Checks scharfschalten. | `ops/healthchecks/README.md` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
|
|||||||
|
|
||||||
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
|
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
|
||||||
|
|
||||||
|
- **2026-06-23** Alert-Kette end-to-end verifiziert (Codex-Drills): send-ntfy + Restore-Wrapper + Freshness-Negativtest + Alertmanager→bridge→ntfy + Docker-Critical-Watcher-Smoke + Borg-pre-hook-Fehler — alle aufs Handy bestaetigt. Beleg-Bug: `send-ntfy.sh` war nicht executable (`6870ae5`). Rest optional: Prometheus→AM-Leg via Testregel.
|
||||||
|
- **2026-06-23** Audit-Remediation abgeschlossen: Vault `/admin` + Komodo public zu (403, IP-Allowlist), Off-site-Snapshots bewiesen + monatlicher Test, Live-Drift bereinigt, `backend_net` auf `internal:true` gesetzt (Egress live BLOCKED, 12/12 Member ok). Doku: `AUTH_MATRIX.md`, DECISIONS, Snapshot-Runbook. Commits `23a6975..` ff.
|
||||||
- **2026-06-17** Offene TODOs gegen Live-Stand abgeglichen: Paperless-OIDC-Secret verdrahtet und Service-Smoke gruen; alter Tailscale-Docker-State nach `_archive/tailscale-removed-2026-06-06/` verschoben; Tailnet-Restpunkt geschlossen.
|
- **2026-06-17** Offene TODOs gegen Live-Stand abgeglichen: Paperless-OIDC-Secret verdrahtet und Service-Smoke gruen; alter Tailscale-Docker-State nach `_archive/tailscale-removed-2026-06-06/` verschoben; Tailnet-Restpunkt geschlossen.
|
||||||
- **2026-06-17** Repo-Hygiene abgeschlossen: Glance-Widget-Tokens sind in Runtime gesetzt, Audit-PDF liegt extern unter `H:\kallilab-recovery\audits`, Worktree clean.
|
|
||||||
- **2026-06-17** Komodo/Gitea-Webhooks normalisiert: aktive Komodo-Hooks fuer `Micha/homelab-infra` nutzen Branch-Filter `master`; DB-Backup vor Host-Hotfix erstellt. Workflow-Regel nachgezogen.
|
|
||||||
- **2026-06-19** Backup-Hardening live verifiziert: Borg-Scope-Drift 0 (alle 33 Quellen konfiguriert), Dumps frisch (11/11 present), neue Dump-Alerts aktiv (25 Regeln, 0 feuern). Prometheus-`alerts.yml`-Stale-Handle (FUSE-Einzeldatei-Mount) per `--force-recreate` behoben und anschliessend dauerhaft auf Directory-Mount umgestellt (recreated, 25 Regeln aktiv).
|
- **2026-06-19** Backup-Hardening live verifiziert: Borg-Scope-Drift 0 (alle 33 Quellen konfiguriert), Dumps frisch (11/11 present), neue Dump-Alerts aktiv (25 Regeln, 0 feuern). Prometheus-`alerts.yml`-Stale-Handle (FUSE-Einzeldatei-Mount) per `--force-recreate` behoben und anschliessend dauerhaft auf Directory-Mount umgestellt (recreated, 25 Regeln aktiv).
|
||||||
- **2026-06-18** Backup-Audit-Hardening: Dump-Frische-Metriken + Alerts `HomelabBorgDumpMissing/Stale`, Freshness-Checks + Nearline-Pull um `n8n`/`globals` ergaenzt, 4 Tier-2-Container in Critical-Watch, Scope-Doku fuer `projekte`/Hermes praezisiert. H:-Nearline (still seit 2026-06-04) nachgeholt + Task neu registriert.
|
- **2026-06-18** Backup-Audit-Hardening: Dump-Frische-Metriken + Alerts `HomelabBorgDumpMissing/Stale`, Freshness-Checks + Nearline-Pull um `n8n`/`globals` ergaenzt, 4 Tier-2-Container in Critical-Watch, Scope-Doku fuer `projekte`/Hermes praezisiert. H:-Nearline (still seit 2026-06-04) nachgeholt + Task neu registriert.
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,8 @@ docker network inspect frontend_net | jq '.[0].Containers | keys'
|
|||||||
docker network inspect backend_net | jq '.[0].Internal'
|
docker network inspect backend_net | jq '.[0].Internal'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Stand 2026-06-23: `backend_net` live als `internal: true` bestaetigt (Egress-Test aus `postgresql17` nach `1.1.1.1:443` = BLOCKED, 12/12 Member attached); zuvor Drift `internal:false`, per Audit-Remediation behoben.
|
||||||
|
|
||||||
## SSH-Konfiguration Host
|
## SSH-Konfiguration Host
|
||||||
|
|
||||||
Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`.
|
Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`.
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
||||||
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
||||||
|
| `AUTH_MATRIX.md` | konsolidierte Auth-Matrix: effektive Policy je Domain (bypass/2FA/native/Tailscale/IP-Allowlist) |
|
||||||
| `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC |
|
| `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC |
|
||||||
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART- und Power-Baseline |
|
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART- und Power-Baseline |
|
||||||
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
||||||
@@ -56,6 +57,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln:
|
|||||||
| `RENOVATE.md` | Self-hosted Renovate gegen Gitea |
|
| `RENOVATE.md` | Self-hosted Renovate gegen Gitea |
|
||||||
| `runbooks/komodo-bulk-deploy-dns.md` | Bulk-Deploy-Pulls scheitern an DNS bei AdGuard-Recreate |
|
| `runbooks/komodo-bulk-deploy-dns.md` | Bulk-Deploy-Pulls scheitern an DNS bei AdGuard-Recreate |
|
||||||
| `../ops/h-drive-nearline/README.md` | Windows-H:/ Nearline-Pull fuer kritische Restore-Artefakte |
|
| `../ops/h-drive-nearline/README.md` | Windows-H:/ Nearline-Pull fuer kritische Restore-Artefakte |
|
||||||
|
| `../ops/healthchecks/README.md` | Self-hosted Healthchecks (interne Job-Heartbeats); externe Host-down-/Backup-Waechter bleiben Cloud |
|
||||||
|
|
||||||
## Nutzer- und Statusdoku
|
## Nutzer- und Statusdoku
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Bewusst kein Auto-Merge: jede PR braucht eine Operator-Sichtpruefung und einen M
|
|||||||
- **Schedule:** alle 6 Stunden per Unraid User-Script `renovate-six-hourly` (`20 */6 * * *`)
|
- **Schedule:** alle 6 Stunden per Unraid User-Script `renovate-six-hourly` (`20 */6 * * *`)
|
||||||
- **Plattform:** Gitea via `https://git.kaleschke.info/api/v1`
|
- **Plattform:** Gitea via `https://git.kaleschke.info/api/v1`
|
||||||
- **Authentifizierung:** Gitea-PAT als Host-Secret-Datei
|
- **Authentifizierung:** Gitea-PAT als Host-Secret-Datei
|
||||||
|
- **GitHub.com Release Notes:** optionaler read-only GitHub.com-PAT als Host-Secret-Datei
|
||||||
- **Konfiguration:** `renovate.json` im Repo-Root
|
- **Konfiguration:** `renovate.json` im Repo-Root
|
||||||
|
|
||||||
## Operator-Setup (historisch, einmalig)
|
## Operator-Setup (historisch, einmalig)
|
||||||
@@ -55,6 +56,27 @@ chown root:root /mnt/user/appdata/secrets/renovate_token.txt
|
|||||||
|
|
||||||
Token-Wert nicht in dieses Repo, nicht in Logs, nicht in Issues.
|
Token-Wert nicht in dieses Repo, nicht in Logs, nicht in Issues.
|
||||||
|
|
||||||
|
### Schritt 3b - Optionaler GitHub.com-Token fuer Release Notes
|
||||||
|
|
||||||
|
Da Renovate gegen Gitea laeuft, hat der Bot nicht automatisch GitHub.com-Credentials. Fuer Release Notes, Changelogs und weniger GitHub-API-Rate-Limit-Rauschen kann ein separater GitHub.com-PAT hinterlegt werden.
|
||||||
|
|
||||||
|
Anforderungen:
|
||||||
|
|
||||||
|
- beliebiger GitHub.com-Account
|
||||||
|
- read-only / keine Repository-Berechtigungen noetig
|
||||||
|
- nur fuer Renovate, nicht fuer Gitea-Zugriff
|
||||||
|
|
||||||
|
Am Unraid-Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN='hier-den-github-com-token-einfuegen'
|
||||||
|
echo -n "$TOKEN" > /mnt/user/appdata/secrets/renovate_github_com_token.txt
|
||||||
|
chmod 600 /mnt/user/appdata/secrets/renovate_github_com_token.txt
|
||||||
|
chown root:root /mnt/user/appdata/secrets/renovate_github_com_token.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`ops/renovate/run-renovate.sh` liest die Datei optional und reicht sie als `RENOVATE_GITHUB_COM_TOKEN` an den Renovate-Container durch. Fehlt die Datei, laeuft Renovate weiter, aber das Dependency Dashboard meldet `No github.com token has been configured. Skipping release notes retrieval`.
|
||||||
|
|
||||||
### Schritt 4 - Erstlauf manuell
|
### Schritt 4 - Erstlauf manuell
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -92,6 +114,7 @@ Script: bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh
|
|||||||
| Lock-File-Maintenance | `lockFileMaintenance.enabled: false` | Renovate macht keine reinen Lock-File-Refreshs |
|
| Lock-File-Maintenance | `lockFileMaintenance.enabled: false` | Renovate macht keine reinen Lock-File-Refreshs |
|
||||||
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
||||||
| Dependency Dashboard | aktiv | Gitea-Issue, die alle ausstehenden Updates auflistet |
|
| Dependency Dashboard | aktiv | Gitea-Issue, die alle ausstehenden Updates auflistet |
|
||||||
|
| Rebase veralteter PRs | `rebaseWhen: "behind-base-branch"` | Offene Renovate-PRs werden neu aus `master` aufgebaut, sobald sie hinterherhaengen; verhindert Rueckwaerts-Diffs auf alte Doku-/Security-Staende |
|
||||||
| Onboarding-PR | `onboarding: false` | Keine `Configure Renovate`-Onboarding-PR; wir nutzen die Repo-`renovate.json` direkt |
|
| Onboarding-PR | `onboarding: false` | Keine `Configure Renovate`-Onboarding-PR; wir nutzen die Repo-`renovate.json` direkt |
|
||||||
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki`, `ops/komodo` | Renovate scant alte/abgeloeste Stacks nicht; `ops/komodo` ist bewusst raus (siehe unten) |
|
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki`, `ops/komodo` | Renovate scant alte/abgeloeste Stacks nicht; `ops/komodo` ist bewusst raus (siehe unten) |
|
||||||
|
|
||||||
@@ -121,6 +144,8 @@ Komodo-Mongo laeuft bereits auf der erlaubten MongoDB-8.0-Schiene; ein offener M
|
|||||||
|
|
||||||
**2026-06-21 (Routine-Merge-Runde):** Sechs offene Renovate-PRs nach Sichtpruefung in einem Bulk-Deploy ueber den Komodo-Webhook gemergt: die Sammelgruppe `minor-and-patch-updates` (u. a. gitea 1.26.3, home-assistant 2026.6.4, alertmanager v0.33.0, influxdb 3.10.0-core, code-server 4.125.0, filebrowser, speedtest, super-productivity plus Digest-Refreshes fuer borg-ui/glances/scrutiny/mailarchiver/python), die reinen Digest-Refreshes fuer `unbound`, `traefik:v3.7` und `postgres:18.4` (gleiche Versionen) sowie n8n 2.26.2 -> 2.27.3 und der `nextcloud:33.0.5-apache` Digest-Refresh. Anschliessend nach Operator-Freigabe nachgezogen: Gitea 1.26.3 -> 1.26.4, cAdvisor v0.57.0 -> v0.60.1 und Nextcloud 33.0.5 -> 34.0.0.
|
**2026-06-21 (Routine-Merge-Runde):** Sechs offene Renovate-PRs nach Sichtpruefung in einem Bulk-Deploy ueber den Komodo-Webhook gemergt: die Sammelgruppe `minor-and-patch-updates` (u. a. gitea 1.26.3, home-assistant 2026.6.4, alertmanager v0.33.0, influxdb 3.10.0-core, code-server 4.125.0, filebrowser, speedtest, super-productivity plus Digest-Refreshes fuer borg-ui/glances/scrutiny/mailarchiver/python), die reinen Digest-Refreshes fuer `unbound`, `traefik:v3.7` und `postgres:18.4` (gleiche Versionen) sowie n8n 2.26.2 -> 2.27.3 und der `nextcloud:33.0.5-apache` Digest-Refresh. Anschliessend nach Operator-Freigabe nachgezogen: Gitea 1.26.3 -> 1.26.4, cAdvisor v0.57.0 -> v0.60.1 und Nextcloud 33.0.5 -> 34.0.0.
|
||||||
|
|
||||||
|
**2026-06-22 (Dawarich-Redis nachgezogen):** `dawarich_redis` war nach den 2026-05-31-Migrationen die letzte verbliebene Redis-7-Instanz; der seinerzeit geschlossene PR #10 hielt sie als "Ignored or Blocked" im Dependency Dashboard (Issue #6). Bewusste Entscheidung, die Instanz auf die 8.x-Schiene nachzuziehen: `apps/dawarich/docker-compose.yml` von `redis:7-alpine` auf den bereits produktiven `redis:8.8.0-alpine`-Digest gehoben und in `renovate.json` zur `allowedVersions`-Redis-8.x-Liste hinzugefuegt. Damit ist die Dashboard-Blockade aufgeloest und alle Redis-Instanzen laufen auf 8.x. Datenpfad `/mnt/user/appdata/dawarich/redis` unveraendert; Redis 8 laedt die bestehenden RDB-Snapshots.
|
||||||
|
|
||||||
## Erwartete erste PRs (historisch)
|
## Erwartete erste PRs (historisch)
|
||||||
|
|
||||||
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Authelia | Postgres Password | `/mnt/user/appdata/secrets/authelia_postgres_password.txt` -> `AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE` | aktiv |
|
| Authelia | Postgres Password | `/mnt/user/appdata/secrets/authelia_postgres_password.txt` -> `AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
||||||
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
||||||
|
| Codex/Claude -> Komodo API | CLI/API Credentials | `/mnt/user/appdata/secrets/codex_komodo_api.env` (root:root, 0600) -> Host-only Shell-Env fuer `docker exec komodo-core km ...`; `KOMODO_CLI_HOST` zeigt container-intern auf `http://localhost:9120`; Werte nie ausgeben, loggen oder committen | aktiv; nur auf ausdrueckliche Operator-Anweisung nutzen |
|
||||||
|
| Codex/Claude -> Unraid API | API Key | `/mnt/user/appdata/secrets/codex_unraid_api_key.txt` (root:root, 0600) -> Host-only API-Zugriff; Wert nie ausgeben, loggen oder committen | aktiv; nur auf ausdrueckliche Operator-Anweisung nutzen |
|
||||||
| Gitea Push Mirror | GitHub fine-grained PAT fuer `michaelkaleschke-spec/homelab-infra` | Gitea Repository Mirror Settings, persistent in `/mnt/user/services/gitea/data`; kein Datei-Secret im Repo | aktiv |
|
| Gitea Push Mirror | GitHub fine-grained PAT fuer `michaelkaleschke-spec/homelab-infra` | Gitea Repository Mirror Settings, persistent in `/mnt/user/services/gitea/data`; kein Datei-Secret im Repo | aktiv |
|
||||||
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}`, `${GLANCE_KOMODO_API_KEY}`, `${GLANCE_KOMODO_API_SECRET}`, `${GLANCE_GITEA_TOKEN}`, `${GLANCE_PAPERLESS_TOKEN}`, `${GLANCE_MEALIE_TOKEN}` (alle read-only anlegen), `${GLANCE_HA_TOKEN}` (HA Long-Lived Access Token; Glance nutzt nur `GET /api/states`) | aktiv |
|
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}`, `${GLANCE_KOMODO_API_KEY}`, `${GLANCE_KOMODO_API_SECRET}`, `${GLANCE_GITEA_TOKEN}`, `${GLANCE_PAPERLESS_TOKEN}`, `${GLANCE_MEALIE_TOKEN}` (alle read-only anlegen), `${GLANCE_HA_TOKEN}` (HA Long-Lived Access Token; Glance nutzt nur `GET /api/states`) | aktiv |
|
||||||
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
||||||
@@ -50,6 +52,12 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
|
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
|
||||||
| Healthchecks Dead-Man's-Switch (Borg Pre-Hook) | Ping-/Capability-URL | `/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600) **oder** ENV `HEALTHCHECKS_BORG_URL`/`HEALTHCHECKS_URL`, gelesen von `ops/borg-ui/scripts/pre-borg.sh`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
| Healthchecks Dead-Man's-Switch (Borg Pre-Hook) | Ping-/Capability-URL | `/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600) **oder** ENV `HEALTHCHECKS_BORG_URL`/`HEALTHCHECKS_URL`, gelesen von `ops/borg-ui/scripts/pre-borg.sh`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
||||||
| Healthchecks Dead-Man's-Switch (Nearline-Pull) | Ping-/Capability-URL | baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` **oder** `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`, gelesen von `ops/h-drive-nearline/pull-critical-backups.ps1`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
| Healthchecks Dead-Man's-Switch (Nearline-Pull) | Ping-/Capability-URL | baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` **oder** `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`, gelesen von `ops/h-drive-nearline/pull-critical-backups.ps1`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup |
|
||||||
|
| Healthchecks self-hosted (`ops/healthchecks/`) | Django `SECRET_KEY` | Komodo Stack-ENV `${HEALTHCHECKS_SECRET_KEY}` (Image hat keinen `_FILE`-Support); Host-Backup `/mnt/user/appdata/secrets/healthchecks_secret_key.txt` | aktiv (2026-06-23) |
|
||||||
|
| Healthchecks self-hosted | DB Password | Komodo Stack-ENV `${HEALTHCHECKS_DB_PASSWORD}` (= Wert von `healthchecks_postgres_password.txt`) | aktiv (2026-06-23) |
|
||||||
|
| Healthchecks self-hosted | Superuser Login | Komodo Stack-ENV `${HEALTHCHECKS_SUPERUSER_EMAIL}` (Login-Mail) + `${HEALTHCHECKS_SUPERUSER_PASSWORD}`; **Login-Passwort als Host-Datei** `/mnt/user/appdata/secrets/healthchecks_superuser_password.txt` (nach erstem Login aenderbar) | aktiv (2026-06-23) |
|
||||||
|
| Healthchecks self-hosted | Gitea->Komodo Webhook Secret | `/mnt/user/appdata/secrets/healthchecks_webhook_secret.txt` (chmod 600) = per-Stack `webhook_secret` in Komodo; im Gitea-Hook identisch eintragen | aktiv (2026-06-23) |
|
||||||
|
| healthchecks-postgres | DB Password | `/mnt/user/appdata/secrets/healthchecks_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv (2026-06-23) |
|
||||||
|
| Healthchecks self-hosted (interne Job-Pings) | Ping-/Capability-URLs | je Job eine Datei `/mnt/user/appdata/secrets/healthchecks_<job>_url` (chmod 600), z. B. `healthchecks_posture_url`; gelesen vom jeweiligen Script (`HEALTHCHECKS_<JOB>_URL`/Datei, endpoint-agnostisch wie `pre-borg.sh`). Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv (2026-06-23) |
|
||||||
| Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln |
|
| Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln |
|
||||||
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
|
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
|
||||||
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
|
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
|
||||||
@@ -66,6 +74,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Dawarich Metrics | Basic-Auth Password | `/mnt/user/appdata/secrets/dawarich_metrics_password.txt` -> Docker Secret `/run/secrets/dawarich_metrics_password`; Prometheus `password_file` | aktiv |
|
| Dawarich Metrics | Basic-Auth Password | `/mnt/user/appdata/secrets/dawarich_metrics_password.txt` -> Docker Secret `/run/secrets/dawarich_metrics_password`; Prometheus `password_file` | aktiv |
|
||||||
| Grafana -> Dawarich | Read-only DB Password | `/mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt` -> Docker Secret `/run/secrets/dawarich_grafana_ro_password`; Grafana-Env `DAWARICH_GRAFANA_RO_PASSWORD` | aktiv |
|
| Grafana -> Dawarich | Read-only DB Password | `/mnt/user/appdata/secrets/dawarich_grafana_ro_password.txt` -> Docker Secret `/run/secrets/dawarich_grafana_ro_password`; Grafana-Env `DAWARICH_GRAFANA_RO_PASSWORD` | aktiv |
|
||||||
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
||||||
|
| Renovate Bot | GitHub.com Read-only PAT fuer Release Notes | `/mnt/user/appdata/secrets/renovate_github_com_token.txt` -> Host-Datei (chmod 600), optional gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_GITHUB_COM_TOKEN` weitergegeben | optional; behebt Dependency-Dashboard-Warnung zu fehlendem github.com-Token |
|
||||||
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
||||||
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
||||||
| n8n | OpenAI API Key (LLM-Extraktion Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: Bearer ...`) | aktiv |
|
| n8n | OpenAI API Key (LLM-Extraktion Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: Bearer ...`) | aktiv |
|
||||||
@@ -97,6 +106,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- authelia_smtp_password.txt
|
|-- authelia_smtp_password.txt
|
||||||
|-- authelia_storage_encryption_key.txt
|
|-- authelia_storage_encryption_key.txt
|
||||||
|-- immich_postgres_password.txt
|
|-- immich_postgres_password.txt
|
||||||
|
|-- codex_komodo_api.env
|
||||||
|
|-- codex_unraid_api_key.txt
|
||||||
|-- komodo_mongo_password.txt
|
|-- komodo_mongo_password.txt
|
||||||
|-- mealie_postgres_password.txt
|
|-- mealie_postgres_password.txt
|
||||||
|-- monitoring_grafana_admin_password.txt
|
|-- monitoring_grafana_admin_password.txt
|
||||||
@@ -106,6 +117,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- nextcloud_postgres_password.txt
|
|-- nextcloud_postgres_password.txt
|
||||||
|-- postgres_password.txt
|
|-- postgres_password.txt
|
||||||
|-- redis_password.txt
|
|-- redis_password.txt
|
||||||
|
|-- renovate_github_com_token.txt
|
||||||
|
|-- renovate_token.txt
|
||||||
|-- borg_repo_passphrase.txt
|
|-- borg_repo_passphrase.txt
|
||||||
|-- influxdb3_admin_token.json
|
|-- influxdb3_admin_token.json
|
||||||
|-- ha_influxdb_token
|
|-- ha_influxdb_token
|
||||||
@@ -118,6 +131,10 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- dawarich_secret_key_base.txt
|
|-- dawarich_secret_key_base.txt
|
||||||
|-- dawarich_metrics_password.txt
|
|-- dawarich_metrics_password.txt
|
||||||
|-- dawarich_grafana_ro_password.txt
|
|-- dawarich_grafana_ro_password.txt
|
||||||
|
|-- healthchecks_postgres_password.txt
|
||||||
|
|-- healthchecks_secret_key.txt
|
||||||
|
|-- healthchecks_superuser_password.txt
|
||||||
|
|-- healthchecks_webhook_secret.txt
|
||||||
`-- vaultwarden_admin_token.txt
|
`-- vaultwarden_admin_token.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -162,6 +179,7 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we
|
|||||||
| `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest |
|
| `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest |
|
||||||
| `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY`, `GLANCE_KOMODO_API_KEY`, `GLANCE_KOMODO_API_SECRET`, `GLANCE_GITEA_TOKEN`, `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`, `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 |
|
| `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY`, `GLANCE_KOMODO_API_KEY`, `GLANCE_KOMODO_API_SECRET`, `GLANCE_GITEA_TOKEN`, `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`, `GLANCE_HA_TOKEN` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker, Komodo, Gitea, Paperless, Mealie, Home Assistant) neu erzeugen | rebuildbar; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf; `GLANCE_HA_TOKEN` muss zusaetzlich in `ops/glance/docker-compose.yml` durchgereicht werden |
|
||||||
| `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. |
|
| `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. |
|
||||||
|
| `healthchecks` | `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL`, `HEALTHCHECKS_SUPERUSER_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | `SECRET_KEY`-Verlust invalidiert Sessions/Signaturen (neu setzen, alle Logins neu); DB-Passwort gemeinsam in Postgres + Stack-ENV zuruecksetzen; Superuser-Account ist via SUPERUSER-ENV reproduzierbar. Check-Metadaten gehen nur verloren, wenn auch das DB-Volume weg ist; die Pings selbst leben in den Jobs |
|
||||||
|
|
||||||
### Komodo-Sonderfall
|
### Komodo-Sonderfall
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default`, `immich_egress` | `model-cache` | rebuildbar | nein | keine Traefik-Route; `immich_egress` (nicht-internal) nur fuer Modell-Download zu huggingface, sonst scheitert Smart Search/Gesichtserkennung an DNS |
|
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default`, `immich_egress` | `model-cache` | rebuildbar | nein | keine Traefik-Route; `immich_egress` (nicht-internal) nur fuer Modell-Download zu huggingface, sonst scheitert Smart Search/Gesichtserkennung an DNS |
|
||||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
||||||
| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, Photon Reverse Geocoding, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2, Borg + `dawarich.dump` | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert. Reverse Geocoding nutzt standardmaessig `photon.komoot.io` ohne Key. App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. |
|
| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2, Borg + `dawarich.dump` | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert. App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. |
|
||||||
| `dawarich_db` | Dawarich PostGIS-Datenbank | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/postgres17`, `dawarich_postgres_password.txt`, `dawarich_grafana_ro_password.txt` | Dump `dawarich.dump`; raw DB nur bei gleichem PG/PostGIS und sauberem Shutdown | nein | PostGIS 17-3.5 Alpine; Grafana-Read-only-User `dawarich_grafana_ro` per Init-Script |
|
| `dawarich_db` | Dawarich PostGIS-Datenbank | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/postgres17`, `dawarich_postgres_password.txt`, `dawarich_grafana_ro_password.txt` | Dump `dawarich.dump`; raw DB nur bei gleichem PG/PostGIS und sauberem Shutdown | nein | PostGIS 17-3.5 Alpine; Grafana-Read-only-User `dawarich_grafana_ro` per Init-Script |
|
||||||
| `dawarich_redis` | Dawarich Cache/Queue-Backend | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/redis`, `dawarich_redis_password.txt` | Teil von Dawarich-Restore, aber aus DB/Appdaten rekonstruierbar | nein | Redis 7 Alpine, keine Host-Ports |
|
| `dawarich_redis` | Dawarich Cache/Queue-Backend | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/redis`, `dawarich_redis_password.txt` | Teil von Dawarich-Restore, aber aus DB/Appdaten rekonstruierbar | nein | Redis 8.8 Alpine, keine Host-Ports |
|
||||||
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
||||||
@@ -82,6 +82,8 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `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-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 |
|
| `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. |
|
| `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. |
|
||||||
|
| `healthchecks` | Self-hosted Cron-/Job-Heartbeat-Monitor (Dead-Man's-Switch) fuer interne Jobs/Scripte | `ops/healthchecks/docker-compose.yml`, `ops/healthchecks/README.md` | `https://hc.kaleschke.info` | Traefik (native Auth, ohne pauschale Authelia), `healthchecks-postgres`, ntfy | keine kritische App-Persistenz (Check-Metadaten in der DB) | Tier 3, rebuildbar | ja, native Auth | Hub fuer INTERNE Checks; die externen Host-down-/Backup-Waechter (Borg-Pre-Hook, Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst auf healthchecks.io-Cloud (ein On-Host-Waechter kann Host-Down nicht melden). Ping-/API-Endpunkte ohne ForwardAuth (analog n8n). Stack-ENV: `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL/PASSWORD`. **Live seit 2026-06-23** (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`, deployt via API; Superuser angelegt). Offen: Gitea->Komodo-Webhook noch manuell anzulegen |
|
||||||
|
| `healthchecks-postgres` | Healthchecks-Datenbank | `ops/healthchecks/docker-compose.yml` | intern | `healthchecks_internal` | `/mnt/user/appdata/healthchecks/postgres18`, `healthchecks_postgres_password.txt` | Check-Metadaten, rebuildbar | nein | interne DB; PostgreSQL 18; nie `frontend_net` |
|
||||||
|
|
||||||
## Smart Home
|
## Smart Home
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
postgresql17:
|
postgresql17:
|
||||||
image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565
|
image: postgres:18.4@sha256:1a5b3e745bbd82d6deb146505e504da3c2f248cac15e431951b148fbe4f8613a
|
||||||
container_name: postgresql17
|
container_name: postgresql17
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
image: redis:8.8.0-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
|
||||||
container_name: Redis
|
container_name: Redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ services:
|
|||||||
- loki
|
- loki
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
|
image: grafana/grafana:13.1.0@sha256:121a7a9ece6dc10b969f1f96eed64b4f07dfac0d0b8abc070f7cb83bbde86f63
|
||||||
container_name: monitoring-grafana
|
container_name: monitoring-grafana
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -20,12 +20,276 @@
|
|||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"id": null,
|
"id": null,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Points Last 30 Days",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
},
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS points\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND lonlat IS NOT NULL;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "points",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Kilometers Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT round(coalesce(sum(distance),0)::numeric / 1000.0, 2)::double precision AS km\nFROM tracks\nWHERE start_at >= now() - interval '30 days';"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "km",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "km",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Tracks Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS tracks\nFROM tracks\nWHERE start_at >= now() - interval '30 days';"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "tracks",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Anomalies Last 30 Days",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT count(*)::double precision AS anomalies\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND anomaly IS TRUE;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "anomalies",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "geomap",
|
||||||
|
"title": "Location Points",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 4,
|
||||||
|
"w": 14,
|
||||||
|
"h": 12
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n ST_Y(lonlat::geometry)::double precision AS lat,\n ST_X(lonlat::geometry)::double precision AS lon,\n accuracy::double precision AS accuracy,\n to_timestamp(timestamp) AS seen_at\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 5000;"
|
||||||
|
}
|
||||||
|
],
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"custom": {
|
"custom": {
|
||||||
@@ -44,18 +308,10 @@
|
|||||||
"value": null
|
"value": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"unit": "none"
|
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": {
|
|
||||||
"h": 16,
|
|
||||||
"w": 16,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
"options": {
|
||||||
"basemap": {
|
"basemap": {
|
||||||
"config": {},
|
"config": {},
|
||||||
@@ -73,42 +329,29 @@
|
|||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"showLegend": true,
|
"showLegend": false,
|
||||||
"style": {
|
"style": {
|
||||||
"color": {
|
"color": {
|
||||||
"fixed": "dark-green"
|
"fixed": "green"
|
||||||
},
|
|
||||||
"opacity": 0.55,
|
|
||||||
"rotation": {
|
|
||||||
"fixed": 0,
|
|
||||||
"max": 360,
|
|
||||||
"min": -360,
|
|
||||||
"mode": "mod"
|
|
||||||
},
|
},
|
||||||
|
"opacity": 0.7,
|
||||||
"size": {
|
"size": {
|
||||||
"fixed": 4,
|
"fixed": 5,
|
||||||
"max": 15,
|
"max": 15,
|
||||||
"min": 2
|
"min": 2
|
||||||
},
|
},
|
||||||
"symbol": {
|
"symbol": {
|
||||||
"fixed": "img/icons/marker/circle.svg",
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
"mode": "fixed"
|
"mode": "fixed"
|
||||||
},
|
|
||||||
"textConfig": {
|
|
||||||
"fontSize": 12,
|
|
||||||
"offsetX": 0,
|
|
||||||
"offsetY": 0,
|
|
||||||
"textAlign": "center",
|
|
||||||
"textBaseline": "middle"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"latitude": "latitude",
|
"latitude": "lat",
|
||||||
"longitude": "longitude",
|
"longitude": "lon",
|
||||||
"mode": "coords"
|
"mode": "coords"
|
||||||
},
|
},
|
||||||
"name": "Location points",
|
"name": "Points",
|
||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"type": "markers"
|
"type": "markers"
|
||||||
}
|
}
|
||||||
@@ -119,14 +362,30 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"allLayers": true,
|
"allLayers": true,
|
||||||
"id": "fit",
|
"id": "fit",
|
||||||
"lat": 51,
|
"lat": 52.0,
|
||||||
"lon": 10,
|
"lon": 7.5,
|
||||||
"zoom": 5
|
"zoom": 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Kilometers per Day",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
"pluginVersion": "13.0.2",
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 4,
|
||||||
|
"w": 10,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
|
"refId": "A",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
@@ -134,53 +393,15 @@
|
|||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"format": "table",
|
"format": "table",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
"rawSql": "SELECT\n date_trunc('day', start_at)::date AS day,\n round(coalesce(sum(distance),0)::numeric / 1000.0, 2)::double precision AS km\nFROM tracks\nWHERE start_at >= now() - interval '30 days'\nGROUP BY 1\nORDER BY 1 DESC;"
|
||||||
"refId": "A"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Location Points",
|
|
||||||
"type": "geomap"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
"custom": {
|
||||||
"axisBorderShow": false,
|
"align": "auto",
|
||||||
"axisCenteredZero": false,
|
"cellOptions": {
|
||||||
"axisColorMode": "text",
|
"type": "auto"
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "never",
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
@@ -192,152 +413,153 @@
|
|||||||
"value": null
|
"value": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"unit": "km"
|
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
"options": {
|
||||||
"legend": {
|
"cellHeight": "sm",
|
||||||
"calcs": [
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
"sum"
|
"sum"
|
||||||
],
|
],
|
||||||
"displayMode": "list",
|
"show": false
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"showHeader": true
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pluginVersion": "13.0.2",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"format": "time_series",
|
|
||||||
"rawQuery": true,
|
|
||||||
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Kilometers per Month",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"datasource": {
|
|
||||||
"type": "postgres",
|
|
||||||
"uid": "dawarich-postgres"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "never",
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"options": {
|
"type": "table",
|
||||||
"legend": {
|
"title": "Points per Day",
|
||||||
"calcs": [
|
"datasource": {
|
||||||
"sum"
|
"type": "postgres",
|
||||||
],
|
"uid": "dawarich-postgres"
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"pluginVersion": "13.0.2",
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 10,
|
||||||
|
"w": 10,
|
||||||
|
"h": 6
|
||||||
|
},
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
|
"refId": "A",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "postgres",
|
"type": "postgres",
|
||||||
"uid": "dawarich-postgres"
|
"uid": "dawarich-postgres"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"format": "time_series",
|
"format": "table",
|
||||||
"rawQuery": true,
|
"rawQuery": true,
|
||||||
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp))::date AS day,\n count(*)::double precision AS points\nFROM points\nWHERE timestamp >= extract(epoch from now() - interval '30 days')::integer\n AND timestamp <= extract(epoch from now())::integer\nGROUP BY 1\nORDER BY 1 DESC;"
|
||||||
"refId": "A"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Points per Day",
|
"fieldConfig": {
|
||||||
"type": "timeseries"
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Recent Tracks",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"pluginVersion": "13.0.2",
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 16,
|
||||||
|
"w": 24,
|
||||||
|
"h": 7
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "dawarich-postgres"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n start_at AS start,\n end_at AS end,\n round((distance::numeric / 1000.0), 2)::double precision AS km,\n round((duration::numeric / 60.0), 1)::double precision AS minutes\nFROM tracks\nWHERE start_at >= now() - interval '30 days'\nORDER BY start_at DESC\nLIMIT 50;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"preload": false,
|
|
||||||
"refresh": "5m",
|
"refresh": "5m",
|
||||||
"schemaVersion": 41,
|
"schemaVersion": 41,
|
||||||
"tags": [
|
"tags": [
|
||||||
"dawarich",
|
"homelab",
|
||||||
"location"
|
"dawarich"
|
||||||
],
|
],
|
||||||
"templating": {
|
"templating": {
|
||||||
"list": []
|
"list": []
|
||||||
@@ -350,6 +572,6 @@
|
|||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Dawarich",
|
"title": "Dawarich",
|
||||||
"uid": "dawarich",
|
"uid": "dawarich",
|
||||||
"version": 1,
|
"version": 5,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ datasources:
|
|||||||
user: dawarich_grafana_ro
|
user: dawarich_grafana_ro
|
||||||
editable: false
|
editable: false
|
||||||
jsonData:
|
jsonData:
|
||||||
|
database: dawarich_production
|
||||||
sslmode: disable
|
sslmode: disable
|
||||||
postgresVersion: 1700
|
postgresVersion: 1700
|
||||||
timescaledb: false
|
timescaledb: false
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ bundle_target_for_repo() {
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$TMP_ROOT/run.$$"
|
rm -rf "$TMP_ROOT/run.$$"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
||||||
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_gitea_bundle_url}"
|
||||||
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
||||||
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
||||||
|
trap 'hc_rc=$?; cleanup; [ "$hc_rc" -eq 0 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
need_cmd git
|
need_cmd git
|
||||||
@@ -82,13 +87,20 @@ main() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rel="${repo#$SOURCE_ROOT/}"
|
||||||
|
if ! git --git-dir="$repo" show-ref --quiet; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
printf 'SKIP\t%s\tempty repository without refs\n' "$rel" >> "$details"
|
||||||
|
printf '%s\t%s\t%s\t%s\n' "$total" "$bundled" "$skipped" "$failed" > "$run_tmp/counts"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
target="$(bundle_target_for_repo "$repo")"
|
target="$(bundle_target_for_repo "$repo")"
|
||||||
target_dir="$(dirname "$target")"
|
target_dir="$(dirname "$target")"
|
||||||
tmp="$run_tmp/$(basename "$target").tmp"
|
tmp="$run_tmp/$(basename "$target").tmp"
|
||||||
target_tmp="$target_dir/.$(basename "$target").tmp"
|
target_tmp="$target_dir/.$(basename "$target").tmp"
|
||||||
mkdir -p "$target_dir"
|
mkdir -p "$target_dir"
|
||||||
|
|
||||||
rel="${repo#$SOURCE_ROOT/}"
|
|
||||||
log "Bundling $rel"
|
log "Bundling $rel"
|
||||||
|
|
||||||
if git --git-dir="$repo" bundle create "$tmp" --all >/dev/null 2>&1 &&
|
if git --git-dir="$repo" bundle create "$tmp" --all >/dev/null 2>&1 &&
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.15@sha256:9805b21cf910f3ef6f4a1c8f441f1dd6cc4197136f9541fe2a1ab6d050706e4b
|
image: filebrowser/filebrowser:v2.63.16@sha256:a6653eb79ecf8312f4f6bf0d0adf65d81016f17a92d64d5b2562340984e39405
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
Typ: Runbook · Stand: 2026-06-23 · Status: live (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`); offen nur der Gitea->Komodo-Webhook
|
||||||
|
|
||||||
|
# Healthchecks (self-hosted) — Cron-/Job-Heartbeat-Monitor
|
||||||
|
|
||||||
|
Self-gehostete Instanz von [Healthchecks](https://github.com/healthchecks/healthchecks)
|
||||||
|
als zentraler **Dead-Man's-Switch** fuer die internen Jobs und Scripte des
|
||||||
|
Homelabs: ein Job pingt beim erfolgreichen Lauf eine URL; bleibt der Ping aus,
|
||||||
|
alarmiert Healthchecks. Damit werden stille Job-Ausfaelle sichtbar, die Docker
|
||||||
|
("Up"), Prometheus-Blackbox (nur HTTP-Routen) und der Critical-Events-Watcher
|
||||||
|
(nur `die`/`oom`) nicht sehen.
|
||||||
|
|
||||||
|
## Scope-Entscheidung (wichtig)
|
||||||
|
|
||||||
|
Dieser Stack ist bewusst der Hub fuer **interne** Checks auf einem **laufenden**
|
||||||
|
Host — Frage: "Lief Job X heute?". Beispiele:
|
||||||
|
|
||||||
|
- `services/posture-check/posture-check.sh` (stuendlich / pre-borg)
|
||||||
|
- `ops/restore-tests/run-restore-checks.sh` (Kadenz aus `schedule.md`)
|
||||||
|
- `ops/borg-ui/scripts/pre-backup-dumps.sh` (Dump-Erzeugung)
|
||||||
|
- `ops/borg-ui/scripts/gitea-bundle-mirror.sh`
|
||||||
|
|
||||||
|
**Nicht hier:** die host-down-/backup-still-Waechter bleiben **extern** auf
|
||||||
|
healthchecks.io-Cloud (Free-Tier):
|
||||||
|
|
||||||
|
| Check | Quelle | Endpoint |
|
||||||
|
|---|---|---|
|
||||||
|
| Borg-Pre-Hook | `ops/borg-ui/scripts/pre-borg.sh` | healthchecks.io-Cloud |
|
||||||
|
| baerchen Nearline-Pull | `ops/h-drive-nearline/pull-critical-backups.ps1` | healthchecks.io-Cloud |
|
||||||
|
| Monitoring-Watchdog (#8) | `monitoring/prometheus/alerts.yml` (geplant) | healthchecks.io-Cloud |
|
||||||
|
|
||||||
|
**Begruendung:** Ein Waechter, der auf demselben Unraid-Host laeuft, den er
|
||||||
|
ueberwacht, kann einen Host-Ausfall nicht melden — er ist dann selbst tot, und
|
||||||
|
Stille ist nicht von "alles gut" unterscheidbar. Genau diese drei Checks
|
||||||
|
existieren fuer den Host-/Backup-Stillstand, deshalb muessen sie extern bleiben.
|
||||||
|
Die Skripte sind endpoint-agnostisch (siehe `docs/SECRETS_MAP.md`), eine
|
||||||
|
spaetere Umstellung waere reine URL-Frage — die Architektur-Empfehlung bleibt
|
||||||
|
aber extern.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
- `healthchecks` — Web-UI + Ping-Listener, `frontend_net`, Traefik via
|
||||||
|
`https://hc.kaleschke.info`, **native Healthchecks-Auth ohne pauschale
|
||||||
|
Authelia** (analog n8n/Komodo): die Ping-Endpunkte `/ping/*` und die API
|
||||||
|
muessen ohne ForwardAuth erreichbar sein, sonst koennen Jobs nicht melden.
|
||||||
|
- `healthchecks-postgres` — dedizierte PostgreSQL 18, nur `healthchecks_internal`
|
||||||
|
(`internal: true`), nie `frontend_net`.
|
||||||
|
- SMTP ist bewusst **nicht** konfiguriert: Login laeuft ueber das
|
||||||
|
Superuser-Passwort, Benachrichtigung ueber die ntfy-Integration. SMTP (GMX)
|
||||||
|
kann spaeter additiv ergaenzt werden, falls E-Mail-Alerts gewuenscht sind.
|
||||||
|
|
||||||
|
## Secrets (siehe `docs/SECRETS_MAP.md`)
|
||||||
|
|
||||||
|
| Secret | Mechanik |
|
||||||
|
|---|---|
|
||||||
|
| `HEALTHCHECKS_SECRET_KEY` | Komodo Stack-ENV (Django Secret Key) |
|
||||||
|
| `HEALTHCHECKS_DB_PASSWORD` | Komodo Stack-ENV (gleicher Wert wie Datei-Secret) |
|
||||||
|
| `HEALTHCHECKS_SUPERUSER_EMAIL` | Komodo Stack-ENV (Login-Mail des Erst-Admins) |
|
||||||
|
| `HEALTHCHECKS_SUPERUSER_PASSWORD` | Komodo Stack-ENV (Login-Passwort des Erst-Admins) |
|
||||||
|
| `healthchecks_postgres_password.txt` | Datei-Secret `/mnt/user/appdata/secrets/` → `POSTGRES_PASSWORD_FILE` |
|
||||||
|
|
||||||
|
`SECRET_KEY` und `DB_PASSWORD` unterstuetzt das Image nicht als `_FILE` → Stack-ENV
|
||||||
|
(Regel aus `docs/SECRETS_MAP.md`). Das Postgres-Passwort liegt zusaetzlich als
|
||||||
|
Datei-Secret vor; beide Werte muessen identisch sein.
|
||||||
|
|
||||||
|
## Pre-Deploy (einmalig, Operator)
|
||||||
|
|
||||||
|
1. Appdata anlegen: `/mnt/user/appdata/healthchecks/postgres18/`.
|
||||||
|
2. Datei-Secret erzeugen: `/mnt/user/appdata/secrets/healthchecks_postgres_password.txt`
|
||||||
|
(`chmod 600`), Wert = `HEALTHCHECKS_DB_PASSWORD`.
|
||||||
|
3. In Komodo die vier Stack-ENV-Variablen setzen (`SECRET_KEY` z. B. via
|
||||||
|
`python -c "import secrets;print(secrets.token_urlsafe(64))"`).
|
||||||
|
|
||||||
|
## Deploy + Pflicht-Webhook
|
||||||
|
|
||||||
|
1. Stack in Komodo aus Gitea `Micha/homelab-infra` anlegen, `webhook_enabled` an.
|
||||||
|
2. Gitea-Webhook auf die neue Stack-ID anlegen
|
||||||
|
(`http://komodo-core:9120/listener/github/stack/<stack-id>/deploy`),
|
||||||
|
Branch-Filter `master`. **Pflicht fuer jeden neuen produktiven Stack**
|
||||||
|
(`docs/WORKFLOW.md`).
|
||||||
|
3. Test-Delivery ausloesen, `last_status`/Komodo-Deploy pruefen.
|
||||||
|
|
||||||
|
## Post-Deploy
|
||||||
|
|
||||||
|
1. Login auf `https://hc.kaleschke.info` mit Superuser-Mail/-Passwort.
|
||||||
|
2. Pro Job einen Check anlegen (Period + Grace passend zur Job-Kadenz, gern als
|
||||||
|
Cron-Ausdruck). Ping-URL kopieren.
|
||||||
|
3. **ntfy-Integration**: im Check unter Integrations ntfy hinzufuegen,
|
||||||
|
Server `https://ntfy.kaleschke.info`, Topic `homelab-alerts` (Problem-Alerts)
|
||||||
|
— konsistent mit der bestehenden Alert-Schiene.
|
||||||
|
4. Im Job am Ende des erfolgreichen Laufs pingen, z. B.:
|
||||||
|
```bash
|
||||||
|
curl -fsS -m 10 --retry 3 "https://hc.kaleschke.info/ping/<uuid>" >/dev/null || true
|
||||||
|
```
|
||||||
|
Optional `/start` am Anfang (misst Laufzeit) und `/<uuid>/fail` im Trap.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
- Letzter stabiler Git-Stand: Stack existiert noch nicht — Rollback = Stack in
|
||||||
|
Komodo stoppen/destroyen, Repo-Pfad `ops/healthchecks/` per `git rm`
|
||||||
|
zuruecknehmen, Gitea-Webhook deaktivieren.
|
||||||
|
- Datenpfad `/mnt/user/appdata/healthchecks/postgres18` bleibt unberuehrt und ist
|
||||||
|
jederzeit loeschbar (reine Check-Metadaten, kein kritischer Datentopf — die
|
||||||
|
Pings selbst sind in den Jobs definiert).
|
||||||
|
- Secrets/ENV: bei Abbau die vier Stack-ENV + die Datei-Secret entfernen.
|
||||||
|
|
||||||
|
## Image-Pinning
|
||||||
|
|
||||||
|
`healthchecks/healthchecks:v4.2@sha256:6b5f59…` ist auf den am 2026-06-23 ueber
|
||||||
|
die Docker-Hub-API ermittelten Manifest-Digest gepinnt. Beim ersten Pull den
|
||||||
|
real laufenden Digest gegenpruefen und bei Abweichung im Repo nachziehen
|
||||||
|
(`docs/WORKFLOW.md` Abschnitt Image-Versionierung).
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
name: healthchecks
|
||||||
|
|
||||||
|
# Self-gehostetes Healthchecks (Dead-Man's-Switch / Cron-Heartbeat-Monitor).
|
||||||
|
#
|
||||||
|
# SCOPE (bewusst): Hub fuer die vielen INTERNEN Jobs/Scripte, die auf einem
|
||||||
|
# laufenden Host melden sollen "lief Job X heute?" (posture-check,
|
||||||
|
# restore-tests, pre-backup-dumps, gitea-bundle-mirror, ...).
|
||||||
|
#
|
||||||
|
# NICHT hier: die host-down-/backup-still-Waechter (Borg-Pre-Hook,
|
||||||
|
# baerchen-Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst EXTERN auf
|
||||||
|
# healthchecks.io-Cloud. Ein Waechter auf demselben Host kann einen
|
||||||
|
# Host-Ausfall nicht melden (er ist dann selbst tot). Siehe ops/healthchecks/README.md.
|
||||||
|
|
||||||
|
services:
|
||||||
|
healthchecks:
|
||||||
|
image: healthchecks/healthchecks:v4.2@sha256:6b5f593d40994345053f05f86decfa9e17ab1e4422df2ae58abd032a7b14d8f6
|
||||||
|
container_name: healthchecks
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# ntfy-Integration nutzt die oeffentliche Traefik-URL; Container-DNS loest
|
||||||
|
# ntfy.kaleschke.info sonst nicht (gleiches Muster wie mealie/komodo).
|
||||||
|
extra_hosts:
|
||||||
|
- "ntfy.kaleschke.info:192.168.178.58"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
DEBUG: "False"
|
||||||
|
SITE_ROOT: https://hc.kaleschke.info
|
||||||
|
SITE_NAME: KalliLab Healthchecks
|
||||||
|
ALLOWED_HOSTS: hc.kaleschke.info,localhost
|
||||||
|
REGISTRATION_OPEN: "False"
|
||||||
|
|
||||||
|
DB: postgres
|
||||||
|
DB_HOST: healthchecks-postgres
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: healthchecks
|
||||||
|
DB_USER: healthchecks
|
||||||
|
DB_PASSWORD: ${HEALTHCHECKS_DB_PASSWORD}
|
||||||
|
|
||||||
|
SECRET_KEY: ${HEALTHCHECKS_SECRET_KEY}
|
||||||
|
|
||||||
|
# Erst-Admin wird beim Start angelegt/aktualisiert. Werte nur als
|
||||||
|
# Komodo-Stack-ENV, niemals im Repo. SMTP ist bewusst nicht konfiguriert
|
||||||
|
# (Login via Superuser-Passwort, Benachrichtigung via ntfy-Integration).
|
||||||
|
SUPERUSER_EMAIL: ${HEALTHCHECKS_SUPERUSER_EMAIL}
|
||||||
|
SUPERUSER_PASSWORD: ${HEALTHCHECKS_SUPERUSER_PASSWORD}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- frontend_net
|
||||||
|
- healthchecks_internal
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
healthchecks-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/', timeout=5).status==200 else 1)\""]
|
||||||
|
interval: 60s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Traefik mit nativer Healthchecks-Auth, bewusst OHNE pauschale
|
||||||
|
# authelia@file: die Ping-Endpunkte (/ping/*) und die API muessen ohne
|
||||||
|
# ForwardAuth erreichbar sein, sonst koennen Cron-Jobs nicht melden
|
||||||
|
# (gleiche Ausnahme-Logik wie n8n/Komodo). Dashboard ist durch den
|
||||||
|
# Healthchecks-eigenen Login geschuetzt.
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=frontend_net
|
||||||
|
- traefik.http.routers.healthchecks.rule=Host(`hc.kaleschke.info`)
|
||||||
|
- traefik.http.routers.healthchecks.entrypoints=websecure
|
||||||
|
- traefik.http.routers.healthchecks.tls=true
|
||||||
|
- traefik.http.routers.healthchecks.tls.certresolver=le
|
||||||
|
- traefik.http.routers.healthchecks.middlewares=secure-headers@file
|
||||||
|
- traefik.http.services.healthchecks.loadbalancer.server.port=8000
|
||||||
|
|
||||||
|
healthchecks-postgres:
|
||||||
|
image: postgres:18.4@sha256:1a5b3e745bbd82d6deb146505e504da3c2f248cac15e431951b148fbe4f8613a
|
||||||
|
container_name: healthchecks-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: healthchecks
|
||||||
|
POSTGRES_DB: healthchecks
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/healthchecks_postgres_password
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/appdata/healthchecks/postgres18:/var/lib/postgresql
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- healthchecks_internal
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
- healthchecks_postgres_password
|
||||||
|
|
||||||
|
expose:
|
||||||
|
- "5432"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
|
healthchecks_internal:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
healthchecks_postgres_password:
|
||||||
|
file: /mnt/user/appdata/secrets/healthchecks_postgres_password.txt
|
||||||
@@ -74,6 +74,13 @@ services:
|
|||||||
- traefik.http.routers.komodo.tls=true
|
- traefik.http.routers.komodo.tls=true
|
||||||
- traefik.http.routers.komodo.tls.certresolver=le
|
- traefik.http.routers.komodo.tls.certresolver=le
|
||||||
- traefik.http.services.komodo.loadbalancer.server.port=9120
|
- traefik.http.services.komodo.loadbalancer.server.port=9120
|
||||||
|
# Audit 2026-06-23 (P1): Komodo war public mit 200 erreichbar + RW-Docker-Socket-Kette.
|
||||||
|
# IP-Allowlist begrenzt den GANZEN Router auf Tailnet + LAN (public -> 403). KEINE ForwardAuth
|
||||||
|
# (Webhooks/Periphery laufen intern ueber komodo-core:9120, nicht ueber Traefik).
|
||||||
|
# ACHTUNG: Self-Stack ist inline in Komodo verwaltet -> diese Labels muessen in der Komodo-UI
|
||||||
|
# am Inline-Compose gesetzt werden; diese Datei ist nur Spiegel.
|
||||||
|
- traefik.http.routers.komodo.middlewares=komodo-allowlist@docker
|
||||||
|
- traefik.http.middlewares.komodo-allowlist.ipallowlist.sourcerange=100.64.0.0/10,192.168.178.0/24
|
||||||
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"middleware_exempt_identities": [
|
"middleware_exempt_identities": [
|
||||||
"authelia",
|
"authelia",
|
||||||
"gitea",
|
"gitea",
|
||||||
|
"healthchecks",
|
||||||
"immich-server",
|
"immich-server",
|
||||||
"immich_server",
|
"immich_server",
|
||||||
"komodo-core",
|
"komodo-core",
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ set -euo pipefail
|
|||||||
# 3. Im Gitea-Profil des renovate-Users ein Access-Token erzeugen:
|
# 3. Im Gitea-Profil des renovate-Users ein Access-Token erzeugen:
|
||||||
# Scope: `write:repository` + `read:user`
|
# Scope: `write:repository` + `read:user`
|
||||||
# 4. Token in `/mnt/user/appdata/secrets/renovate_token.txt` ablegen (chmod 600)
|
# 4. Token in `/mnt/user/appdata/secrets/renovate_token.txt` ablegen (chmod 600)
|
||||||
# 5. Erstlauf: `bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh`
|
# 5. Optional: GitHub.com Read-only-PAT fuer Release Notes als
|
||||||
# 6. User-Script `renovate-six-hourly` aktivieren
|
# `/mnt/user/appdata/secrets/renovate_github_com_token.txt` ablegen.
|
||||||
|
# 6. Erstlauf: `bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh`
|
||||||
|
# 7. User-Script `renovate-six-hourly` aktivieren
|
||||||
|
|
||||||
RENOVATE_IMAGE="${RENOVATE_IMAGE:-renovate/renovate:41}"
|
RENOVATE_IMAGE="${RENOVATE_IMAGE:-renovate/renovate:41}"
|
||||||
RENOVATE_TOKEN_FILE="${RENOVATE_TOKEN_FILE:-/mnt/user/appdata/secrets/renovate_token.txt}"
|
RENOVATE_TOKEN_FILE="${RENOVATE_TOKEN_FILE:-/mnt/user/appdata/secrets/renovate_token.txt}"
|
||||||
|
RENOVATE_GITHUB_COM_TOKEN_FILE="${RENOVATE_GITHUB_COM_TOKEN_FILE:-/mnt/user/appdata/secrets/renovate_github_com_token.txt}"
|
||||||
RENOVATE_LOG_DIR="${RENOVATE_LOG_DIR:-/mnt/user/services/renovate/logs}"
|
RENOVATE_LOG_DIR="${RENOVATE_LOG_DIR:-/mnt/user/services/renovate/logs}"
|
||||||
RENOVATE_STATE_DIR="${RENOVATE_STATE_DIR:-/mnt/user/services/renovate/state}"
|
RENOVATE_STATE_DIR="${RENOVATE_STATE_DIR:-/mnt/user/services/renovate/state}"
|
||||||
RENOVATE_CONFIG_FILE="${RENOVATE_CONFIG_FILE:-/mnt/user/services/homelab-infra/ops/renovate/bot-config.js}"
|
RENOVATE_CONFIG_FILE="${RENOVATE_CONFIG_FILE:-/mnt/user/services/homelab-infra/ops/renovate/bot-config.js}"
|
||||||
@@ -27,6 +30,13 @@ RENOVATE_CONFIG_FILE="${RENOVATE_CONFIG_FILE:-/mnt/user/services/homelab-infra/o
|
|||||||
# Compose). Wir mappen direkt auf die LAN-IP des Unraid-Hosts.
|
# Compose). Wir mappen direkt auf die LAN-IP des Unraid-Hosts.
|
||||||
GITEA_HOST_LAN_IP="${GITEA_HOST_LAN_IP:-192.168.178.58}"
|
GITEA_HOST_LAN_IP="${GITEA_HOST_LAN_IP:-192.168.178.58}"
|
||||||
|
|
||||||
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
||||||
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_renovate_url}"
|
||||||
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
||||||
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
||||||
|
trap 'hc_rc=$?; [ "$hc_rc" -eq 0 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
if [ ! -r "$RENOVATE_TOKEN_FILE" ]; then
|
if [ ! -r "$RENOVATE_TOKEN_FILE" ]; then
|
||||||
echo "Renovate token file missing or unreadable: $RENOVATE_TOKEN_FILE" >&2
|
echo "Renovate token file missing or unreadable: $RENOVATE_TOKEN_FILE" >&2
|
||||||
echo "See ops/renovate/run-renovate.sh header for operator setup steps." >&2
|
echo "See ops/renovate/run-renovate.sh header for operator setup steps." >&2
|
||||||
@@ -63,8 +73,16 @@ RENOVATE_TOKEN=$(cat "$RENOVATE_TOKEN_FILE")
|
|||||||
RENOVATE_CONFIG_FILE=/usr/src/app/config.js
|
RENOVATE_CONFIG_FILE=/usr/src/app/config.js
|
||||||
LOG_LEVEL=${RENOVATE_LOG_LEVEL:-info}
|
LOG_LEVEL=${RENOVATE_LOG_LEVEL:-info}
|
||||||
EFEOF
|
EFEOF
|
||||||
|
if [ -r "$RENOVATE_GITHUB_COM_TOKEN_FILE" ]; then
|
||||||
|
{
|
||||||
|
printf 'RENOVATE_GITHUB_COM_TOKEN='
|
||||||
|
cat "$RENOVATE_GITHUB_COM_TOKEN_FILE"
|
||||||
|
printf '\n'
|
||||||
|
} >> "$ENV_FILE"
|
||||||
|
fi
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
|
set +e
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--name renovate-run \
|
--name renovate-run \
|
||||||
--add-host "git.kaleschke.info:$GITEA_HOST_LAN_IP" \
|
--add-host "git.kaleschke.info:$GITEA_HOST_LAN_IP" \
|
||||||
@@ -75,6 +93,7 @@ EFEOF
|
|||||||
--env-file "$ENV_FILE" \
|
--env-file "$ENV_FILE" \
|
||||||
"$RENOVATE_IMAGE" 2>&1
|
"$RENOVATE_IMAGE" 2>&1
|
||||||
rc=$?
|
rc=$?
|
||||||
|
set -e
|
||||||
shred -u "$ENV_FILE" 2>/dev/null || rm -f "$ENV_FILE"
|
shred -u "$ENV_FILE" 2>/dev/null || rm -f "$ENV_FILE"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Stand 2026-06-11 ist der Betrieb auf V1+ (validierte Bash-Host-Jobs mit ntfy):
|
|||||||
# Frische-Check
|
# Frische-Check
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
|
|
||||||
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|komodo-bootstrap|nextcloud)
|
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|komodo-bootstrap|nextcloud|hetzner-snapshot)
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst>
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst>
|
||||||
|
|
||||||
# Negativtest des Alarmwegs (quartalsweise)
|
# Negativtest des Alarmwegs (quartalsweise)
|
||||||
@@ -79,6 +79,7 @@ Einziger Status-Ort ist die **Reifegrad-Tabelle** in `docs/RESTORE_MATRIX.md`
|
|||||||
- **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image.
|
- **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image.
|
||||||
- **Home Assistant:** nutzt das neueste HA-native Backup-Artefakt und eine Kopie der Mosquitto-Appdata; Testcontainer laufen nur auf localhost-Ports, ohne Traefik/Public Route.
|
- **Home Assistant:** nutzt das neueste HA-native Backup-Artefakt und eine Kopie der Mosquitto-Appdata; Testcontainer laufen nur auf localhost-Ports, ohne Traefik/Public Route.
|
||||||
- **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`.
|
- **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`.
|
||||||
|
- **Hetzner-Snapshot:** Infrastruktur-Test (kein Service-Restore): prueft `.zfs/snapshot` der Storage Box (Existenz, Retention, Einzeldatei-Restore) und belegt den snapshot-basierten Off-site-Schutz. Dispatcher `hetzner-snapshot`, Runbook `hetzner-snapshot-runbook.md`. Live validiert 2026-06-23 (7 Snapshots, Einzeldatei-Restore ok); monatlich im `schedule.md`.
|
||||||
|
|
||||||
## Naechste Ausbaustufen
|
## Naechste Ausbaustufen
|
||||||
|
|
||||||
|
|||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Hetzner Storage Box Snapshot Restore Test
|
||||||
|
#
|
||||||
|
# Belegt, dass der Off-site-Schutz wirklich greift. Append-only ist bewusst NICHT
|
||||||
|
# umgesetzt (DECISIONS 2026-06-01); der Schutz ist snapshot-basiert (DECISIONS
|
||||||
|
# 2026-06-11): operative Borg-Creds koennen weiter prune/compact, die ZFS-
|
||||||
|
# Snapshots der Storage Box sind hostseitig aber nicht loeschbar. Dieser Test
|
||||||
|
# macht den am 2026-06-23 manuell gefuehrten Beweis wiederholbar und ueberwachbar.
|
||||||
|
#
|
||||||
|
# Scope (READ-ONLY gegen die Storage Box, ueber den borg-ui-Container):
|
||||||
|
# 1. .zfs/snapshot/ listen -> Anzahl + neuesten Snapshot bestimmen (Retention)
|
||||||
|
# 2. Alter des neuesten Snapshots aus dem Namen pruefen (Automatic-<ISO>)
|
||||||
|
# 3. eine kleine Datei (Borg-Repo `README`) aus dem neuesten Snapshot per SFTP
|
||||||
|
# in den Container nach /tmp holen, Groesse + SHA256 pruefen, danach loeschen
|
||||||
|
# 4. Report nach /mnt/user/backups/restore-reports/
|
||||||
|
#
|
||||||
|
# KEIN Schreibzugriff auf die Box, kein borg prune/compact, keine produktiven Pfade.
|
||||||
|
#
|
||||||
|
# Verbindung wird aus der in borg-ui konfigurierten Borg-Repo-URL abgeleitet
|
||||||
|
# (kein Secret im Skript). SSH-Key + known_hosts liegen bereits im borg-ui-
|
||||||
|
# Container und werden via BORG_RSH-Konvention genutzt.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/hetzner-snapshot-$(date +%F).md"
|
||||||
|
|
||||||
|
# Erwartungswerte = Zielbild lt. DECISIONS 2026-06-11 (taeglich, Retention 7 Tage).
|
||||||
|
MIN_SNAPSHOTS="${MIN_SNAPSHOTS:-5}"
|
||||||
|
MAX_SNAPSHOT_AGE_HOURS="${MAX_SNAPSHOT_AGE_HOURS:-48}"
|
||||||
|
SNAPSHOT_DIR="${SNAPSHOT_DIR:-.zfs/snapshot}"
|
||||||
|
PROBE_FILE="${PROBE_FILE:-README}" # jede Borg-Repo-Wurzel hat README + config
|
||||||
|
SNAPSHOT_NAME_GLOB="${SNAPSHOT_NAME_GLOB:-Automatic-}"
|
||||||
|
# Zusaetzliche ssh/sftp-Optionen; Default nutzt borg-uis known_hosts wie BORG_RSH.
|
||||||
|
SNAPSHOT_SSH_OPTS="${SNAPSHOT_SSH_OPTS:--o UserKnownHostsFile=/data/known_hosts -o StrictHostKeyChecking=yes -o BatchMode=yes}"
|
||||||
|
PROBE_TMP_DIR="${PROBE_TMP_DIR:-/tmp/hetzner-snapshot-probe}"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Hetzner Storage Box snapshot restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
Container: $BORG_CONTAINER
|
||||||
|
Snapshot dir (rel. login home): $SNAPSHOT_DIR
|
||||||
|
Probe file: <repo>/$PROBE_FILE
|
||||||
|
Min snapshots: $MIN_SNAPSHOTS
|
||||||
|
Max age (h): $MAX_SNAPSHOT_AGE_HOURS
|
||||||
|
Scope: list snapshots + SFTP get one small file from newest snapshot + sha256
|
||||||
|
Note: connection derived from borg-ui repo URL; no productive write.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd date
|
||||||
|
require_borg_container
|
||||||
|
|
||||||
|
# --- Borg-Repo-URL aus borg-ui-DB; daraus user/host/port + Repo-Verzeichnis ----
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
if [ -z "$repo" ]; then
|
||||||
|
echo "Could not resolve Borg repo URL from borg-ui database" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unterstuetzte Formen: ssh://user@host:port/./reldir | user@host:reldir
|
||||||
|
proto_stripped="${repo#ssh://}"
|
||||||
|
if [ "$proto_stripped" != "$repo" ]; then
|
||||||
|
userhostport="${proto_stripped%%/*}"
|
||||||
|
repo_path="/${proto_stripped#*/}"
|
||||||
|
else
|
||||||
|
userhostport="${proto_stripped%%:*}"
|
||||||
|
repo_path="${proto_stripped#*:}"
|
||||||
|
fi
|
||||||
|
ssh_user="${userhostport%%@*}"
|
||||||
|
hostport="${userhostport#*@}"
|
||||||
|
ssh_host="${hostport%%:*}"
|
||||||
|
if [ "$hostport" = "$ssh_host" ]; then ssh_port=22; else ssh_port="${hostport##*:}"; fi
|
||||||
|
# Repo-Verzeichnis relativ zum Login-Home: fuehrende /, ./ und /./ entfernen
|
||||||
|
repo_dir="$repo_path"
|
||||||
|
repo_dir="${repo_dir#/}"; repo_dir="${repo_dir#./}"; repo_dir="${repo_dir#/}"
|
||||||
|
|
||||||
|
if [ -z "$ssh_user" ] || [ -z "$ssh_host" ] || [ -z "$repo_dir" ]; then
|
||||||
|
echo "Could not parse user/host/repo-dir from repo URL: $repo" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_sftp() { # liest Batch-Kommandos von stdin
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
docker exec -i "$BORG_CONTAINER" sftp -q -P "$ssh_port" $SNAPSHOT_SSH_OPTS -b - "$ssh_user@$ssh_host"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1) Snapshots listen --------------------------------------------------------
|
||||||
|
snap_list="$(printf 'ls -1 %s\nbye\n' "$SNAPSHOT_DIR" | run_sftp 2>/dev/null \
|
||||||
|
| tr -d '\r' | grep -F "$SNAPSHOT_NAME_GLOB" | sed 's#.*/##' | sort -u || true)"
|
||||||
|
|
||||||
|
if [ -z "$snap_list" ]; then
|
||||||
|
echo "No snapshots found in $SNAPSHOT_DIR on $ssh_host (glob: $SNAPSHOT_NAME_GLOB)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
snap_count="$(printf '%s\n' "$snap_list" | grep -c . || true)"
|
||||||
|
newest="$(printf '%s\n' "$snap_list" | sort | tail -n 1)"
|
||||||
|
|
||||||
|
# --- 2) Alter des neuesten Snapshots aus dem Namen ableiten ---------------------
|
||||||
|
# Format: Automatic-YYYY-MM-DDTHH-MM-SS
|
||||||
|
age_hours="unknown"
|
||||||
|
ts="${newest#${SNAPSHOT_NAME_GLOB}}"
|
||||||
|
date_part="${ts%%T*}"
|
||||||
|
time_part="${ts#*T}"
|
||||||
|
time_colons="$(printf '%s' "$time_part" | tr '-' ':')"
|
||||||
|
if snap_epoch="$(date -d "$date_part $time_colons" +%s 2>/dev/null)"; then
|
||||||
|
now_epoch="$(date +%s)"
|
||||||
|
age_hours="$(( (now_epoch - snap_epoch) / 3600 ))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3) Einzeldatei aus dem neuesten Snapshot holen + pruefen -------------------
|
||||||
|
remote_probe="$SNAPSHOT_DIR/$newest/$repo_dir/$PROBE_FILE"
|
||||||
|
docker exec -i "$BORG_CONTAINER" sh -c "rm -rf '$PROBE_TMP_DIR' && mkdir -p '$PROBE_TMP_DIR'"
|
||||||
|
|
||||||
|
probe_ok="no"
|
||||||
|
probe_size=0
|
||||||
|
probe_sha256="n/a"
|
||||||
|
if printf 'get %s %s/%s\nbye\n' "$remote_probe" "$PROBE_TMP_DIR" "$PROBE_FILE" | run_sftp 2>/dev/null; then
|
||||||
|
if docker exec -i "$BORG_CONTAINER" test -s "$PROBE_TMP_DIR/$PROBE_FILE"; then
|
||||||
|
probe_ok="yes"
|
||||||
|
probe_size="$(docker exec -i "$BORG_CONTAINER" stat -c '%s' "$PROBE_TMP_DIR/$PROBE_FILE" 2>/dev/null || echo 0)"
|
||||||
|
probe_sha256="$(docker exec -i "$BORG_CONTAINER" sha256sum "$PROBE_TMP_DIR/$PROBE_FILE" 2>/dev/null | awk '{print $1}' || echo n/a)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Temp im Container wieder loeschen (kein Datenrest)
|
||||||
|
docker exec -i "$BORG_CONTAINER" rm -rf "$PROBE_TMP_DIR" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# --- Bewertung ------------------------------------------------------------------
|
||||||
|
result="SUCCESS"
|
||||||
|
fail_reason=""
|
||||||
|
if [ "$probe_ok" != "yes" ]; then
|
||||||
|
result="FAILED"; fail_reason="Einzeldatei-Restore aus Snapshot fehlgeschlagen ($remote_probe)"
|
||||||
|
elif [ "$snap_count" -lt "$MIN_SNAPSHOTS" ]; then
|
||||||
|
result="FAILED"; fail_reason="Zu wenige Snapshots: $snap_count < $MIN_SNAPSHOTS"
|
||||||
|
elif [ "$age_hours" != "unknown" ] && [ "$age_hours" -gt "$MAX_SNAPSHOT_AGE_HOURS" ]; then
|
||||||
|
result="FAILED"; fail_reason="Neuester Snapshot zu alt: ${age_hours}h > ${MAX_SNAPSHOT_AGE_HOURS}h"
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Hetzner Storage Box Snapshot Restore Test - $(date +%F)
|
||||||
|
|
||||||
|
- Scope: \`Off-site Snapshot-Schutz (nicht append-only)\`
|
||||||
|
- Storage Box host: \`$ssh_host\`
|
||||||
|
- Borg repo dir: \`$repo_dir\`
|
||||||
|
- Snapshot dir: \`$SNAPSHOT_DIR\`
|
||||||
|
- Result: \`$result\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Snapshots gefunden: \`$snap_count\` (min \`$MIN_SNAPSHOTS\`)
|
||||||
|
- Neuester Snapshot: \`$newest\`
|
||||||
|
- Alter neuester Snapshot: \`${age_hours}h\` (max \`${MAX_SNAPSHOT_AGE_HOURS}h\`)
|
||||||
|
- Probe-Datei: \`$repo_dir/$PROBE_FILE\`
|
||||||
|
- Einzeldatei-Restore aus Snapshot: \`$probe_ok\`
|
||||||
|
- Probe-Groesse: \`${probe_size} B\`
|
||||||
|
- Probe-SHA256: \`$probe_sha256\`
|
||||||
|
$( [ -n "$fail_reason" ] && echo "- Fehlergrund: \`$fail_reason\`" )
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- READ-ONLY: nur \`ls\` + \`get\` einer kleinen Datei via SFTP; kein Schreibzugriff,
|
||||||
|
kein borg prune/compact, keine produktiven Pfade.
|
||||||
|
- Verbindung aus der borg-ui-Repo-URL abgeleitet; Secrets/known_hosts bleiben im Container.
|
||||||
|
- Schutzmodell ist snapshot-basiert, append-only bewusst nicht (DECISIONS 2026-06-01/-11).
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$result" != "SUCCESS" ]; then
|
||||||
|
echo "Hetzner snapshot restore test FAILED: $fail_reason -> $REPORT_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Hetzner snapshot restore test ok ($snap_count snapshots, newest $newest, probe ${probe_size}B) -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Hetzner Storage Box Snapshot Restore - Runbook
|
||||||
|
|
||||||
|
Typ: Runbook · Stand: 2026-06-23 · Status: aktiv (live validiert 2026-06-23)
|
||||||
|
|
||||||
|
Belegt den Off-site-Schutz der Hetzner Storage Box. Append-only ist bewusst NICHT
|
||||||
|
umgesetzt (DECISIONS 2026-06-01); der Schutz ist snapshot-basiert (DECISIONS
|
||||||
|
2026-06-11): operative Borg-Creds koennen weiter `prune`/`compact`, die ZFS-
|
||||||
|
Snapshots der Box sind hostseitig nicht loeschbar (taeglich 05:30 UTC, Retention 7 Tage).
|
||||||
|
|
||||||
|
## Was der Test tut
|
||||||
|
|
||||||
|
`ops/restore-tests/hetzner-snapshot-restore-test.sh` (Dispatcher: `hetzner-snapshot`):
|
||||||
|
|
||||||
|
1. listet `.zfs/snapshot/` auf der Box (READ-ONLY, via SFTP aus dem `borg-ui`-Container)
|
||||||
|
2. zaehlt Snapshots (Retention) + prueft das Alter des neuesten aus dessen Namen
|
||||||
|
3. holt eine kleine Datei (`<repo>/README`) aus dem neuesten Snapshot, prueft Groesse + SHA256
|
||||||
|
4. loescht die Temp-Datei und schreibt einen Report nach `/mnt/user/backups/restore-reports/`
|
||||||
|
|
||||||
|
Verbindung (user/host/port/Repo-Verzeichnis) wird aus der in `borg-ui` konfigurierten
|
||||||
|
Borg-Repo-URL abgeleitet; SSH-Key + `known_hosts` liegen bereits im Container. Kein
|
||||||
|
Secret im Skript, kein Schreibzugriff, kein `prune`/`compact`.
|
||||||
|
|
||||||
|
## Manuell belegter Referenzlauf (2026-06-23, Codex)
|
||||||
|
|
||||||
|
- Box `u565255.your-storagebox.de`, `.zfs/snapshot` sichtbar
|
||||||
|
- Snapshots `2026-06-17`..`2026-06-23`, je `05:30` -> 7 Tage Retention
|
||||||
|
- neuester: `Automatic-2026-06-23T05-30-24`
|
||||||
|
- Probe `hetzner_borg_appdata_critical/README`, 73 B, SHA256 erzeugt, Temp geloescht
|
||||||
|
|
||||||
|
## Live-Validierung (2026-06-23, Codex): erfolgreich
|
||||||
|
|
||||||
|
Erstlauf ohne ENV-Anpassung gruen (`SUCCESS`), Defaults passten direkt:
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/hetzner-snapshot-2026-06-23.md`
|
||||||
|
- 7 Snapshots gefunden, neuester `Automatic-2026-06-23T05-30-24`
|
||||||
|
- Einzeldatei-Restore aus `.zfs/snapshot` erfolgreich
|
||||||
|
|
||||||
|
ENV-Override (`SNAPSHOT_DIR`, `PROBE_FILE`, `SNAPSHOT_NAME_GLOB`,
|
||||||
|
`SNAPSHOT_SSH_OPTS`, `MIN_SNAPSHOTS`, `MAX_SNAPSHOT_AGE_HOURS`) bleibt fuer
|
||||||
|
kuenftige Abweichungen verfuegbar.
|
||||||
|
|
||||||
|
## Host-Schedule
|
||||||
|
|
||||||
|
Unraid-User-Script `restore-hetzner-snapshot-monthly` ist live angelegt und in
|
||||||
|
Cron aktiv:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 6 15 * * /usr/local/emhttp/plugins/user.scripts/startCustom.php /boot/config/plugins/user.scripts/scripts/restore-hetzner-snapshot-monthly/script > /dev/null 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Job ruft den ntfy-Wrapper auf:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh hetzner-snapshot homelab-info
|
||||||
|
```
|
||||||
@@ -103,8 +103,14 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
|
hetzner-snapshot)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/hetzner-snapshot-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/hetzner-snapshot-restore-test.sh"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
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
|
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|hetzner-snapshot} [--what-if]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Monatlich:
|
|||||||
|
|
||||||
- `vaultwarden` Mini-Restore
|
- `vaultwarden` Mini-Restore
|
||||||
- `gitea` Mini-Restore, versetzt zum Vaultwarden-Lauf
|
- `gitea` Mini-Restore, versetzt zum Vaultwarden-Lauf
|
||||||
|
- `hetzner-snapshot` Storage-Box Snapshot-Restore-Test (read-only `.zfs/snapshot`; Erstlauf 2026-06-23 erfolgreich)
|
||||||
|
|
||||||
Alle 2 Monate:
|
Alle 2 Monate:
|
||||||
|
|
||||||
@@ -69,6 +70,8 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
|
|||||||
- DR-/Restore-Sanity-Check
|
- DR-/Restore-Sanity-Check
|
||||||
- Quartalsweise am 2. Sonntag im zweiten Quartalsmonat, 08:30:
|
- Quartalsweise am 2. Sonntag im zweiten Quartalsmonat, 08:30:
|
||||||
- `immich`
|
- `immich`
|
||||||
|
- Jeden 15. des Monats, 06:00:
|
||||||
|
- `hetzner-snapshot`
|
||||||
|
|
||||||
## Unraid User Scripts Cron
|
## Unraid User Scripts Cron
|
||||||
|
|
||||||
@@ -83,6 +86,7 @@ Vixie-Cron (Unraid) verknuepft `day-of-month` und `day-of-week` mit **OR**, soba
|
|||||||
| `restore-authelia-bimonthly` | `30 7 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|4\|6\|8\|10\|12) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in geraden Monaten 07:30 |
|
| `restore-authelia-bimonthly` | `30 7 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|4\|6\|8\|10\|12) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in geraden Monaten 07:30 |
|
||||||
| `restore-immich-quarterly` | `30 8 * * 0` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|5\|8\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
| `restore-immich-quarterly` | `30 8 * * 0` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|5\|8\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
||||||
| `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
|
| `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
|
||||||
|
| `restore-hetzner-snapshot-monthly` | `0 6 15 * *` | - | 15. des Monats 06:00 (read-only Snapshot-Test) |
|
||||||
|
|
||||||
**Warum so**: ein frueheres Schema wie `0 7 1-7 * 6` haette in Vixie-Cron die OR-Semantik ausgeloest und an jedem Tag 1-7 zusaetzlich zu jedem Samstag gefeuert (~11 Laeufe statt 1 pro Monat). Die obige Trennung Cron-Trigger + Shell-Guard ist die einzige robuste Loesung in Standard-Cron.
|
**Warum so**: ein frueheres Schema wie `0 7 1-7 * 6` haette in Vixie-Cron die OR-Semantik ausgeloest und an jedem Tag 1-7 zusaetzlich zu jedem Samstag gefeuert (~11 Laeufe statt 1 pro Monat). Die obige Trennung Cron-Trigger + Shell-Guard ist die einzige robuste Loesung in Standard-Cron.
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
@@ -151,10 +151,32 @@ Cron:
|
|||||||
exec /mnt/user/services/homelab-infra/ops/restore-tests/monthly-random-restore.sh
|
exec /mnt/user/services/homelab-infra/ops/restore-tests/monthly-random-restore.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Script 8 - `restore-hetzner-snapshot-monthly`
|
||||||
|
|
||||||
|
Cron:
|
||||||
|
|
||||||
|
- `0 6 15 * *` (15. des Monats 06:00)
|
||||||
|
|
||||||
|
Inhalt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
exec bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
||||||
|
hetzner-snapshot homelab-info
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- listet Hetzner Storage Box Snapshots read-only via SFTP aus dem `borg-ui`-Container
|
||||||
|
- restauriert eine kleine Probe-Datei aus `.zfs/snapshot` in ein Temp-Verzeichnis
|
||||||
|
- schreibt den Report nach `/mnt/user/backups/restore-reports/`
|
||||||
|
- sendet Erfolg nach `homelab-info`, Fehler nach `homelab-alerts`
|
||||||
|
|
||||||
## Stand
|
## Stand
|
||||||
|
|
||||||
- die ersten Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
- die ersten Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
||||||
- `freshness`, `vaultwarden`, `gitea`, `paperless`, `immich` und `authelia` sind als Host-Jobs verfuegbar
|
- `freshness`, `vaultwarden`, `gitea`, `paperless`, `immich`, `authelia` und `hetzner-snapshot` sind als Host-Jobs verfuegbar
|
||||||
- ntfy-Wrapper schreibt Erfolg/Fehler-Meldungen an die definierten Topics
|
- ntfy-Wrapper schreibt Erfolg/Fehler-Meldungen an die definierten Topics
|
||||||
|
|
||||||
## Fehler-Topic
|
## Fehler-Topic
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
speedtest-tracker:
|
speedtest-tracker:
|
||||||
image: lscr.io/linuxserver/speedtest-tracker:1.14.4@sha256:f99dfd097709016dfb4387d65bfdc0419bde99cf1dce7e26e70ca616c86f1281
|
image: lscr.io/linuxserver/speedtest-tracker:1.14.5@sha256:4c698dc3a5d989c8d92512600d303f23ff2e6e789c89674adb083372ac67fe2c
|
||||||
container_name: speedtest-tracker
|
container_name: speedtest-tracker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
+3
-1
@@ -8,6 +8,7 @@
|
|||||||
"dependencyDashboardTitle": "Renovate Dependency Dashboard",
|
"dependencyDashboardTitle": "Renovate Dependency Dashboard",
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
"rangeStrategy": "pin",
|
"rangeStrategy": "pin",
|
||||||
|
"rebaseWhen": "behind-base-branch",
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
@@ -95,7 +96,8 @@
|
|||||||
"matchFileNames": [
|
"matchFileNames": [
|
||||||
"infra/redis/docker-compose.yml",
|
"infra/redis/docker-compose.yml",
|
||||||
"apps/nextcloud/docker-compose.yml",
|
"apps/nextcloud/docker-compose.yml",
|
||||||
"apps/immich/docker-compose.yml"
|
"apps/immich/docker-compose.yml",
|
||||||
|
"apps/dawarich/docker-compose.yml"
|
||||||
],
|
],
|
||||||
"matchPackageNames": ["redis"],
|
"matchPackageNames": ["redis"],
|
||||||
"allowedVersions": "/^8\\.\\d+\\.\\d+-alpine(?:\\d+\\.\\d+)?$/"
|
"allowedVersions": "/^8\\.\\d+\\.\\d+-alpine(?:\\d+\\.\\d+)?$/"
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ services:
|
|||||||
- traefik.http.routers.vaultwarden.tls=true
|
- traefik.http.routers.vaultwarden.tls=true
|
||||||
- traefik.http.routers.vaultwarden.tls.certresolver=le
|
- traefik.http.routers.vaultwarden.tls.certresolver=le
|
||||||
- traefik.http.services.vaultwarden.loadbalancer.server.port=80
|
- traefik.http.services.vaultwarden.loadbalancer.server.port=80
|
||||||
|
# Audit 2026-06-23 (P1): /admin war public mit 200 erreichbar. Zweiter, hoeher
|
||||||
|
# priorisierter Router scoped auf /admin und laesst nur Tailnet + LAN durch (sonst 403).
|
||||||
|
# Hauptrouter oben bleibt nativ, damit Browser-/Mobile-Clients von ueberall funktionieren.
|
||||||
|
- traefik.http.routers.vaultwarden-admin.rule=Host(`vault.kaleschke.info`) && PathPrefix(`/admin`)
|
||||||
|
- traefik.http.routers.vaultwarden-admin.entrypoints=websecure
|
||||||
|
- traefik.http.routers.vaultwarden-admin.tls=true
|
||||||
|
- traefik.http.routers.vaultwarden-admin.tls.certresolver=le
|
||||||
|
- traefik.http.routers.vaultwarden-admin.service=vaultwarden
|
||||||
|
- traefik.http.routers.vaultwarden-admin.priority=100
|
||||||
|
- traefik.http.routers.vaultwarden-admin.middlewares=vaultwarden-admin-allowlist@docker
|
||||||
|
- traefik.http.middlewares.vaultwarden-admin-allowlist.ipallowlist.sourcerange=100.64.0.0/10,192.168.178.0/24
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
frontend_net:
|
frontend_net:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SEND_NTFY="${SEND_NTFY:-1}"
|
|||||||
CLOUDFLARE_TOKEN_FILE="${CLOUDFLARE_TOKEN_FILE:-/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token}"
|
CLOUDFLARE_TOKEN_FILE="${CLOUDFLARE_TOKEN_FILE:-/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token}"
|
||||||
WARN_DAYS="${WARN_DAYS:-14}"
|
WARN_DAYS="${WARN_DAYS:-14}"
|
||||||
CRITICAL_DAYS="${CRITICAL_DAYS:-7}"
|
CRITICAL_DAYS="${CRITICAL_DAYS:-7}"
|
||||||
DOMAINS="${DOMAINS:-traefik.kaleschke.info auth.kaleschke.info vault.kaleschke.info git.kaleschke.info cloud.kaleschke.info glance.kaleschke.info borg.kaleschke.info monitoring.kaleschke.info ntfy.kaleschke.info}"
|
DOMAINS="${DOMAINS:-traefik.kaleschke.info auth.kaleschke.info vault.kaleschke.info git.kaleschke.info cloud.kaleschke.info glance.kaleschke.info borg.kaleschke.info monitoring.kaleschke.info ntfy.kaleschke.info hc.kaleschke.info komodo.kaleschke.info files.kaleschke.info code.kaleschke.info glances.kaleschke.info scrutiny.kaleschke.info speedtest.kaleschke.info home.kaleschke.info plex.kaleschke.info pdf.kaleschke.info immich.kaleschke.info mealie.kaleschke.info n8n.kaleschke.info mail.kaleschke.info sp.kaleschke.info paperless.kaleschke.info paperless-gpt.kaleschke.info}"
|
||||||
TMP_DIR="${TMP_DIR:-/tmp/kallilab-cert-token-check}"
|
TMP_DIR="${TMP_DIR:-/tmp/kallilab-cert-token-check}"
|
||||||
|
|
||||||
mkdir -p "$TMP_DIR"
|
mkdir -p "$TMP_DIR"
|
||||||
@@ -137,8 +137,26 @@ write_json() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
for domain in $DOMAINS; do
|
# --- Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo) ---
|
||||||
|
HEALTHCHECKS_CERT_TOKEN_URL="${HEALTHCHECKS_CERT_TOKEN_URL:-}"
|
||||||
|
HEALTHCHECKS_CERT_TOKEN_URL_FILE="${HEALTHCHECKS_CERT_TOKEN_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_cert_token_url}"
|
||||||
|
if [ -z "$HEALTHCHECKS_CERT_TOKEN_URL" ] && [ -r "$HEALTHCHECKS_CERT_TOKEN_URL_FILE" ]; then
|
||||||
|
HEALTHCHECKS_CERT_TOKEN_URL="$(tr -d '[:space:]' < "$HEALTHCHECKS_CERT_TOKEN_URL_FILE")"
|
||||||
|
fi
|
||||||
|
hc_ping() {
|
||||||
|
[ -n "$HEALTHCHECKS_CERT_TOKEN_URL" ] || return 0
|
||||||
|
curl -fsS -m 10 --retry 3 "${HEALTHCHECKS_CERT_TOKEN_URL}${1:-}" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
hc_ping "/start"
|
||||||
|
rc=0
|
||||||
|
{
|
||||||
|
for domain in $DOMAINS; do
|
||||||
check_cert "$domain"
|
check_cert "$domain"
|
||||||
done
|
done
|
||||||
check_cloudflare_token
|
check_cloudflare_token
|
||||||
write_json
|
write_json
|
||||||
|
} || rc=$?
|
||||||
|
# 0/1/2 = ok/warning/critical: der Check LIEF (Alarme laufen separat via ntfy); nur rc>2 -> /fail
|
||||||
|
if [ "$rc" -le 2 ]; then hc_ping ""; else hc_ping "/fail"; fi
|
||||||
|
exit "$rc"
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ TMP_DIR="${TMP_DIR:-/tmp/kallilab-compose-runtime-drift}"
|
|||||||
mkdir -p "$TMP_DIR"
|
mkdir -p "$TMP_DIR"
|
||||||
RESULTS_FILE="$TMP_DIR/results.$$"
|
RESULTS_FILE="$TMP_DIR/results.$$"
|
||||||
: > "$RESULTS_FILE"
|
: > "$RESULTS_FILE"
|
||||||
trap 'rm -f "$RESULTS_FILE"' EXIT
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
||||||
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_compose_drift_url}"
|
||||||
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
||||||
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
||||||
|
trap 'hc_rc=$?; rm -f "$RESULTS_FILE"; [ "$hc_rc" -le 2 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
json_escape() {
|
json_escape() {
|
||||||
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
||||||
@@ -21,7 +26,73 @@ add_result() {
|
|||||||
printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RESULTS_FILE"
|
printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$RESULTS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_compose() {
|
parse_normalized_compose() {
|
||||||
|
awk '
|
||||||
|
function clean(value) {
|
||||||
|
gsub(/\r/, "", value)
|
||||||
|
gsub(/["'\''"]/, "", value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
function emit() {
|
||||||
|
if (in_services && service && image) {
|
||||||
|
print clean(container) "\t" clean(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^services:/ {
|
||||||
|
emit()
|
||||||
|
in_services=1
|
||||||
|
service=""
|
||||||
|
image=""
|
||||||
|
container=""
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[A-Za-z0-9_.-]+:/ && $0 !~ /^services:/ {
|
||||||
|
if (in_services) {
|
||||||
|
emit()
|
||||||
|
in_services=0
|
||||||
|
service=""
|
||||||
|
image=""
|
||||||
|
container=""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in_services && /^ [A-Za-z0-9_.-]+:/ {
|
||||||
|
emit()
|
||||||
|
service=$1
|
||||||
|
sub(/:$/, "", service)
|
||||||
|
image=""
|
||||||
|
container=service
|
||||||
|
next
|
||||||
|
}
|
||||||
|
in_services && service && /^ image:/ {
|
||||||
|
image=$0
|
||||||
|
sub(/^[[:space:]]*image:[[:space:]]*/, "", image)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
in_services && service && /^ container_name:/ {
|
||||||
|
container=$0
|
||||||
|
sub(/^[[:space:]]*container_name:[[:space:]]*/, "", container)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
END { emit() }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_compose_with_docker() {
|
||||||
|
local compose="$1"
|
||||||
|
local dir
|
||||||
|
local file
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
|
dir="$(dirname "$compose")"
|
||||||
|
file="$(basename "$compose")"
|
||||||
|
(
|
||||||
|
cd "$dir"
|
||||||
|
docker compose -f "$file" config 2>/dev/null
|
||||||
|
) | parse_normalized_compose
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_compose_raw() {
|
||||||
local compose="$1"
|
local compose="$1"
|
||||||
awk '
|
awk '
|
||||||
function clean(value) {
|
function clean(value) {
|
||||||
@@ -31,9 +102,25 @@ parse_compose() {
|
|||||||
}
|
}
|
||||||
function emit() {
|
function emit() {
|
||||||
if (service && image && !has_profile) {
|
if (service && image && !has_profile) {
|
||||||
|
if (image ~ /^\*/) {
|
||||||
|
alias=image
|
||||||
|
sub(/^\*/, "", alias)
|
||||||
|
if (alias in anchors) {
|
||||||
|
image=anchors[alias]
|
||||||
|
}
|
||||||
|
}
|
||||||
print clean(container) "\t" clean(image)
|
print clean(container) "\t" clean(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/^x-[A-Za-z0-9_.-]+:[[:space:]]*&[A-Za-z0-9_.-]+[[:space:]]+/ {
|
||||||
|
alias=$0
|
||||||
|
sub(/^.*&/, "", alias)
|
||||||
|
sub(/[[:space:]].*$/, "", alias)
|
||||||
|
value=$0
|
||||||
|
sub(/^.*&[A-Za-z0-9_.-]+[[:space:]]+/, "", value)
|
||||||
|
anchors[alias]=value
|
||||||
|
next
|
||||||
|
}
|
||||||
/^ [A-Za-z0-9_.-]+:/ {
|
/^ [A-Za-z0-9_.-]+:/ {
|
||||||
emit()
|
emit()
|
||||||
service=$1
|
service=$1
|
||||||
@@ -61,6 +148,16 @@ parse_compose() {
|
|||||||
' "$compose"
|
' "$compose"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parse_compose() {
|
||||||
|
local compose="$1"
|
||||||
|
|
||||||
|
if parse_compose_with_docker "$compose"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
parse_compose_raw "$compose"
|
||||||
|
}
|
||||||
|
|
||||||
while IFS= read -r -d '' compose; do
|
while IFS= read -r -d '' compose; do
|
||||||
while IFS="$(printf '\t')" read -r container expected_image; do
|
while IFS="$(printf '\t')" read -r container expected_image; do
|
||||||
[ -n "$container" ] || continue
|
[ -n "$container" ] || continue
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ SECTION_ERRORS_FILE="$TMP_DIR/section-errors.log"
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$TMP_DIR"
|
rm -rf "$TMP_DIR"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
||||||
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_daily_report_url}"
|
||||||
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
||||||
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
||||||
|
trap 'hc_rc=$?; cleanup; [ "$hc_rc" -le 2 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
append() {
|
append() {
|
||||||
printf '%s\n' "$*" >> "$BODY_PATH"
|
printf '%s\n' "$*" >> "$BODY_PATH"
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ NTFY_TOPIC="${NTFY_TOPIC:-homelab-alerts}"
|
|||||||
SEND_NTFY="${SEND_NTFY:-1}"
|
SEND_NTFY="${SEND_NTFY:-1}"
|
||||||
KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-/mnt/user/appdata/secrets/codex_komodo_api.env}"
|
KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-/mnt/user/appdata/secrets/codex_komodo_api.env}"
|
||||||
KOMODO_CONTAINER="${KOMODO_CONTAINER:-komodo-core}"
|
KOMODO_CONTAINER="${KOMODO_CONTAINER:-komodo-core}"
|
||||||
|
KOMODO_CLI_HOST_FOR_CONTAINER="${KOMODO_CLI_HOST_FOR_CONTAINER:-http://localhost:9120}"
|
||||||
|
FETCH_BEFORE_DIFF="${FETCH_BEFORE_DIFF:-1}"
|
||||||
|
|
||||||
# Komma-separierte Allowlist fuer bewusst inline-managed Stacks.
|
# Komma-separierte Allowlist fuer bewusst inline-managed Stacks.
|
||||||
# Quelle: memory/komodo-stack-inline-managed.md, CLAUDE.md.
|
# Quelle: memory/komodo-stack-inline-managed.md, CLAUDE.md.
|
||||||
@@ -32,8 +34,16 @@ TMP_DIR="${TMP_DIR:-/tmp/kallilab-komodo-stack-hygiene}"
|
|||||||
mkdir -p "$TMP_DIR"
|
mkdir -p "$TMP_DIR"
|
||||||
RESULTS_FILE="$TMP_DIR/results.$$"
|
RESULTS_FILE="$TMP_DIR/results.$$"
|
||||||
STACKS_FILE="$TMP_DIR/stacks.$$.json"
|
STACKS_FILE="$TMP_DIR/stacks.$$.json"
|
||||||
|
API_ERROR_FILE="$TMP_DIR/komodo-api.$$.err"
|
||||||
|
API_UNREACHABLE=0
|
||||||
: > "$RESULTS_FILE"
|
: > "$RESULTS_FILE"
|
||||||
trap 'rm -f "$RESULTS_FILE" "$STACKS_FILE"' EXIT
|
: > "$API_ERROR_FILE"
|
||||||
|
# Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo)
|
||||||
|
HC_URL_FILE="${HC_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_komodo_hygiene_url}"
|
||||||
|
hc_url=""; [ -r "$HC_URL_FILE" ] && hc_url="$(tr -d '[:space:]' < "$HC_URL_FILE")"
|
||||||
|
hc_ping() { [ -n "$hc_url" ] || return 0; curl -fsS -m 10 --retry 3 "${hc_url}${1:-}" >/dev/null 2>&1 || true; }
|
||||||
|
trap 'hc_rc=$?; rm -f "$RESULTS_FILE" "$STACKS_FILE" "$API_ERROR_FILE"; [ "$hc_rc" -le 2 ] && hc_ping "" || hc_ping "/fail"' EXIT
|
||||||
|
hc_ping "/start"
|
||||||
|
|
||||||
json_escape() {
|
json_escape() {
|
||||||
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
||||||
@@ -68,11 +78,21 @@ is_expected_not_in_komodo() {
|
|||||||
stack_files_changed() {
|
stack_files_changed() {
|
||||||
local name="$1" deployed="$2" latest="$3"
|
local name="$1" deployed="$2" latest="$3"
|
||||||
local dir
|
local dir
|
||||||
|
HASH_COMPARE_REASON=""
|
||||||
# Locate the stack's compose dir (case-insensitive, same as Mode 3).
|
# 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)"
|
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
|
if [ -z "$dir" ]; then
|
||||||
( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$deployed" >/dev/null ) || return 0
|
HASH_COMPARE_REASON="no compose directory found for stack"
|
||||||
( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$latest" >/dev/null ) || return 0
|
return 1
|
||||||
|
fi
|
||||||
|
if ! ( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$deployed" >/dev/null ); then
|
||||||
|
HASH_COMPARE_REASON="deployed_hash $deployed is not available in local repo"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! ( cd "$REPO_ROOT" && git rev-parse --verify --quiet "$latest" >/dev/null ); then
|
||||||
|
HASH_COMPARE_REASON="latest_hash $latest is not available in local repo"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
local rel="${dir#$REPO_ROOT/}"
|
local rel="${dir#$REPO_ROOT/}"
|
||||||
if ( cd "$REPO_ROOT" && git diff --quiet "$deployed".."$latest" -- "$rel" ); then
|
if ( cd "$REPO_ROOT" && git diff --quiet "$deployed".."$latest" -- "$rel" ); then
|
||||||
return 1 # no change
|
return 1 # no change
|
||||||
@@ -80,20 +100,29 @@ stack_files_changed() {
|
|||||||
return 0 # real change
|
return 0 # real change
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [ "$FETCH_BEFORE_DIFF" = "1" ]; then
|
||||||
|
if ! ( cd "$REPO_ROOT" && git fetch --quiet origin >/dev/null 2>&1 ); then
|
||||||
|
add_result "warning" "repo-fetch" "Could not fetch origin before hash drift comparisons"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Komodo-API-Credentials laden und Stack-Liste holen.
|
# Komodo-API-Credentials laden und Stack-Liste holen.
|
||||||
if [ ! -r "$KOMODO_ENV_FILE" ]; then
|
if [ ! -r "$KOMODO_ENV_FILE" ]; then
|
||||||
add_result "warning" "komodo-api" "Komodo env file not readable: $KOMODO_ENV_FILE"
|
API_UNREACHABLE=1
|
||||||
|
add_result "critical" "komodo-api" "Komodo env file not readable: $KOMODO_ENV_FILE"
|
||||||
else
|
else
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$KOMODO_ENV_FILE"
|
. "$KOMODO_ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
if ! docker exec \
|
if ! docker exec \
|
||||||
-e KOMODO_CLI_HOST \
|
-e "KOMODO_CLI_HOST=$KOMODO_CLI_HOST_FOR_CONTAINER" \
|
||||||
-e KOMODO_CLI_KEY \
|
-e KOMODO_CLI_KEY \
|
||||||
-e KOMODO_CLI_SECRET \
|
-e KOMODO_CLI_SECRET \
|
||||||
"$KOMODO_CONTAINER" km list -a stacks -f json > "$STACKS_FILE" 2>/dev/null; then
|
"$KOMODO_CONTAINER" km list -a stacks -f json > "$STACKS_FILE" 2>"$API_ERROR_FILE"; then
|
||||||
add_result "warning" "komodo-api" "km list stacks failed (container=$KOMODO_CONTAINER)"
|
API_UNREACHABLE=1
|
||||||
|
api_error="$(tr '\n' ' ' < "$API_ERROR_FILE" | sed -E 's/[[:space:]]+/ /g' | cut -c 1-180)"
|
||||||
|
add_result "critical" "komodo-api" "km list stacks failed (container=$KOMODO_CONTAINER): ${api_error:-unknown error}"
|
||||||
: > "$STACKS_FILE"
|
: > "$STACKS_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -145,9 +174,12 @@ if [ -s "$STACKS_FILE" ]; then
|
|||||||
# ohne Stack-Inhalt aendert nichts und ist kein echter Drift.
|
# ohne Stack-Inhalt aendert nichts und ist kein echter Drift.
|
||||||
# "-" = unbekannt (z.B. gitea self-host edge case), nicht als Drift werten.
|
# "-" = unbekannt (z.B. gitea self-host edge case), nicht als Drift werten.
|
||||||
if [ "$deployed_hash" != "-" ] && [ "$latest_hash" != "-" ] \
|
if [ "$deployed_hash" != "-" ] && [ "$latest_hash" != "-" ] \
|
||||||
&& [ "$deployed_hash" != "$latest_hash" ] \
|
&& [ "$deployed_hash" != "$latest_hash" ]; then
|
||||||
&& stack_files_changed "$name" "$deployed_hash" "$latest_hash"; then
|
if stack_files_changed "$name" "$deployed_hash" "$latest_hash"; then
|
||||||
add_result "warning" "$name" "deployed_hash $deployed_hash != latest_hash $latest_hash (stack files changed)"
|
add_result "warning" "$name" "deployed_hash $deployed_hash != latest_hash $latest_hash (stack files changed)"
|
||||||
|
elif [ -n "${HASH_COMPARE_REASON:-}" ]; then
|
||||||
|
add_result "warning" "$name" "deployed_hash $deployed_hash != latest_hash $latest_hash (cannot compare: $HASH_COMPARE_REASON)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Failure-Mode 5: Stack ist down.
|
# Failure-Mode 5: Stack ist down.
|
||||||
@@ -232,6 +264,9 @@ if [ "$critical_count" -gt 0 ] || [ "$warning_count" -gt 0 ]; then
|
|||||||
"Komodo stack hygiene: $critical_count critical, $warning_count warning" \
|
"Komodo stack hygiene: $critical_count critical, $warning_count warning" \
|
||||||
"See $OUTPUT_PATH" "$priority" || true
|
"See $OUTPUT_PATH" "$priority" || true
|
||||||
fi
|
fi
|
||||||
|
# If Komodo could not be queried at all, the hygiene monitor itself is broken.
|
||||||
|
# Use rc=3 so the Healthchecks EXIT trap sends /fail instead of a green ping.
|
||||||
|
[ "$API_UNREACHABLE" -eq 1 ] && exit 3
|
||||||
[ "$critical_count" -gt 0 ] && exit 2
|
[ "$critical_count" -gt 0 ] && exit 2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -429,4 +429,21 @@ main() {
|
|||||||
write_json
|
write_json
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
# --- Healthchecks Heartbeat (endpoint-agnostisch; Capability-URL ist ein Secret, nie ins Repo) ---
|
||||||
|
HEALTHCHECKS_POSTURE_URL="${HEALTHCHECKS_POSTURE_URL:-}"
|
||||||
|
HEALTHCHECKS_POSTURE_URL_FILE="${HEALTHCHECKS_POSTURE_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_posture_url}"
|
||||||
|
if [ -z "$HEALTHCHECKS_POSTURE_URL" ] && [ -r "$HEALTHCHECKS_POSTURE_URL_FILE" ]; then
|
||||||
|
HEALTHCHECKS_POSTURE_URL="$(tr -d '[:space:]' < "$HEALTHCHECKS_POSTURE_URL_FILE")"
|
||||||
|
fi
|
||||||
|
hc_ping() {
|
||||||
|
[ -n "$HEALTHCHECKS_POSTURE_URL" ] || return 0
|
||||||
|
curl -fsS -m 10 --retry 3 "${HEALTHCHECKS_POSTURE_URL}${1:-}" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
hc_ping "/start"
|
||||||
|
rc=0
|
||||||
|
main "$@" || rc=$?
|
||||||
|
# Exit 0/1/2 = ok/warning/critical: der Monitor LIEF (Posture-Alarme laufen separat via ntfy).
|
||||||
|
# Nur ein echter Abbruch (rc>2) ist ein Job-Fehler -> /fail.
|
||||||
|
if [ "$rc" -le 2 ]; then hc_ping ""; else hc_ping "/fail"; fi
|
||||||
|
exit "$rc"
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ User Script:
|
|||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
SEND_MAIL=1 \
|
SEND_MAIL=1 \
|
||||||
|
SEND_NTFY=1 \
|
||||||
|
NTFY_TOPIC="homelab-info" \
|
||||||
MAIL_MODE=always \
|
MAIL_MODE=always \
|
||||||
INCLUDE_WEATHER_REPORT=1 \
|
INCLUDE_WEATHER_REPORT=1 \
|
||||||
MAIL_FROM="michideheld@gmx.de" \
|
MAIL_FROM="michideheld@gmx.de" \
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ services:
|
|||||||
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
|
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
|
||||||
|
|
||||||
mosquitto:
|
mosquitto:
|
||||||
image: eclipse-mosquitto:2.0.22@sha256:914f529386804c8278a4e581526b9be5e1604df44b30daabc70aa97dcefe5268
|
image: eclipse-mosquitto:2.0.22@sha256:212f89e1eaeb2c322d6441b64396e3346026674db8fa9c27beac293405c32b3c
|
||||||
container_name: smarthome-mosquitto
|
container_name: smarthome-mosquitto
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.7@sha256:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257
|
image: traefik:v3.7@sha256:e4d98158c01ad752fc1071d4e9573788747230d902cdde00a772516e692d07c9
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
Reference in New Issue
Block a user