Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89a69fc805 | |||
| 3e9c12eb75 | |||
| 813d3bd303 | |||
| ad47979000 | |||
| 23a6975a67 | |||
| 81151d8af4 | |||
| 45ff8286cf | |||
| f318d80477 | |||
| b8d9bba5d3 | |||
| 3bebc03a8f | |||
| 0f1e78e0ca | |||
| 658750bc19 | |||
| 5afba298e9 |
@@ -8,9 +8,6 @@ POSTGRES_USER=dawarich
|
||||
POSTGRES_DB=dawarich_production
|
||||
GRAFANA_DB_USER=dawarich_grafana_ro
|
||||
|
||||
PHOTON_API_HOST=photon.komoot.io
|
||||
PHOTON_API_USE_HTTPS=true
|
||||
|
||||
METRICS_USERNAME=prometheus
|
||||
BACKGROUND_PROCESSING_CONCURRENCY=5
|
||||
RAILS_MAX_THREADS=10
|
||||
|
||||
+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`.
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Verifikation aus dem Prometheus-Container:
|
||||
|
||||
```bash
|
||||
PW="$(cat /run/secrets/dawarich_metrics_password)"
|
||||
curl -i -u "prometheus:${PW}" http://dawarich_app:3000/metrics
|
||||
```
|
||||
|
||||
Erwartung:
|
||||
|
||||
- `200`: Scrape ist direkt funktionsfaehig.
|
||||
- `301`/`308` nach HTTPS: `http_headers` mit `X-Forwarded-Proto: https` im Prometheus-Job beibehalten.
|
||||
- `403 Blocked host`: `dawarich_app` in `APPLICATION_HOSTS` aufnehmen.
|
||||
|
||||
## Grafana
|
||||
|
||||
Der Read-only-User `dawarich_grafana_ro` wird beim ersten DB-Init durch `postgres/initdb/20-grafana-readonly.sh` angelegt.
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
dawarich_redis:
|
||||
image: redis:7-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
||||
container_name: dawarich_redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -93,8 +93,6 @@ services:
|
||||
TIME_ZONE: ${TZ}
|
||||
SELF_HOSTED: "true"
|
||||
STORE_GEODATA: "true"
|
||||
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
||||
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
||||
RAILS_LOG_TO_STDOUT: "true"
|
||||
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||
@@ -134,7 +132,7 @@ services:
|
||||
- traefik.docker.network=frontend_net
|
||||
|
||||
# Public API-key endpoints for mobile apps and Home Assistant pushes.
|
||||
- traefik.http.routers.dawarich-api.rule=Host(`${DAWARICH_HOST}`) && (Path(`/api/v1/health`) || Path(`/api/v1/owntracks/points`) || Path(`/api/v1/overland/batches`) || Path(`/api/v1/traccar/points`))
|
||||
- traefik.http.routers.dawarich-api.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.tls=true
|
||||
- traefik.http.routers.dawarich-api.tls.certresolver=le
|
||||
@@ -180,13 +178,11 @@ services:
|
||||
TIME_ZONE: ${TZ}
|
||||
SELF_HOSTED: "true"
|
||||
STORE_GEODATA: "true"
|
||||
PHOTON_API_HOST: ${PHOTON_API_HOST:-photon.komoot.io}
|
||||
PHOTON_API_USE_HTTPS: "${PHOTON_API_USE_HTTPS:-true}"
|
||||
RAILS_LOG_TO_STDOUT: "true"
|
||||
PROMETHEUS_EXPORTER_ENABLED: "true"
|
||||
PROMETHEUS_EXPORTER_PORT: "9394"
|
||||
METRICS_USERNAME: ${METRICS_USERNAME}
|
||||
BACKGROUND_PROCESSING_CONCURRENCY: "5"
|
||||
BACKGROUND_PROCESSING_CONCURRENCY: ${BACKGROUND_PROCESSING_CONCURRENCY}
|
||||
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS}
|
||||
volumes:
|
||||
- dawarich_public:/var/app/public
|
||||
|
||||
@@ -20,12 +20,276 @@
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "stat",
|
||||
"title": "Points Last 30 Days",
|
||||
"datasource": {
|
||||
"type": "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": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
@@ -44,18 +308,10 @@
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 16,
|
||||
"w": 16,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
@@ -73,42 +329,29 @@
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"showLegend": false,
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.55,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
"fixed": "green"
|
||||
},
|
||||
"opacity": 0.7,
|
||||
"size": {
|
||||
"fixed": 4,
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"latitude": "lat",
|
||||
"longitude": "lon",
|
||||
"mode": "coords"
|
||||
},
|
||||
"name": "Location points",
|
||||
"name": "Points",
|
||||
"tooltip": true,
|
||||
"type": "markers"
|
||||
}
|
||||
@@ -119,14 +362,30 @@
|
||||
"view": {
|
||||
"allLayers": true,
|
||||
"id": "fit",
|
||||
"lat": 51,
|
||||
"lon": 10,
|
||||
"zoom": 5
|
||||
"lat": 52.0,
|
||||
"lon": 7.5,
|
||||
"zoom": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "table",
|
||||
"title": "Kilometers per Day",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"gridPos": {
|
||||
"x": 14,
|
||||
"y": 4,
|
||||
"w": 10,
|
||||
"h": 6
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
@@ -134,53 +393,15 @@
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
||||
"refId": "A"
|
||||
"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;"
|
||||
}
|
||||
],
|
||||
"title": "Location Points",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -192,152 +413,153 @@
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
"show": false
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
"showHeader": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Kilometers per Month",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"sum"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
"type": "table",
|
||||
"title": "Points per Day",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"gridPos": {
|
||||
"x": 14,
|
||||
"y": 10,
|
||||
"w": 10,
|
||||
"h": 6
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
||||
"refId": "A"
|
||||
"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;"
|
||||
}
|
||||
],
|
||||
"title": "Points per Day",
|
||||
"type": "timeseries"
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"schemaVersion": 41,
|
||||
"tags": [
|
||||
"dawarich",
|
||||
"location"
|
||||
"homelab",
|
||||
"dawarich"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
@@ -350,6 +572,6 @@
|
||||
"timezone": "browser",
|
||||
"title": "Dawarich",
|
||||
"uid": "dawarich",
|
||||
"version": 1,
|
||||
"version": 5,
|
||||
"weekStart": ""
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ datasources:
|
||||
user: dawarich_grafana_ro
|
||||
editable: false
|
||||
jsonData:
|
||||
database: dawarich_production
|
||||
sslmode: disable
|
||||
postgresVersion: 1700
|
||||
timescaledb: false
|
||||
|
||||
@@ -3,33 +3,22 @@ set -eu
|
||||
|
||||
GRAFANA_USER="${GRAFANA_DB_USER:-dawarich_grafana_ro}"
|
||||
GRAFANA_PASSWORD="$(cat /run/secrets/dawarich_grafana_ro_password)"
|
||||
export GRAFANA_USER GRAFANA_PASSWORD
|
||||
|
||||
sql_ident() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/"/""/g')"
|
||||
}
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'EOSQL'
|
||||
\set grafana_user `printf %s "$GRAFANA_USER"`
|
||||
\set grafana_password `printf %s "$GRAFANA_PASSWORD"`
|
||||
|
||||
sql_literal() {
|
||||
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
|
||||
}
|
||||
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'grafana_user', :'grafana_password')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'grafana_user')
|
||||
\gexec
|
||||
|
||||
DB_IDENT="$(sql_ident "$POSTGRES_DB")"
|
||||
USER_IDENT="$(sql_ident "$GRAFANA_USER")"
|
||||
USER_LITERAL="$(sql_literal "$GRAFANA_USER")"
|
||||
PASSWORD_LITERAL="$(sql_literal "$GRAFANA_PASSWORD")"
|
||||
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'grafana_user', :'grafana_password')
|
||||
WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'grafana_user')
|
||||
\gexec
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = ${USER_LITERAL}) THEN
|
||||
EXECUTE 'CREATE ROLE ${USER_IDENT} LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
||||
ELSE
|
||||
EXECUTE 'ALTER ROLE ${USER_IDENT} WITH LOGIN PASSWORD ${PASSWORD_LITERAL}';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
|
||||
GRANT CONNECT ON DATABASE ${DB_IDENT} TO ${USER_IDENT};
|
||||
GRANT USAGE ON SCHEMA public TO ${USER_IDENT};
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${USER_IDENT};
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${USER_IDENT};
|
||||
SELECT format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), :'grafana_user')\gexec
|
||||
SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'grafana_user')\gexec
|
||||
SELECT format('GRANT SELECT ON ALL TABLES IN SCHEMA public TO %I', :'grafana_user')\gexec
|
||||
SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO %I', :'grafana_user')\gexec
|
||||
EOSQL
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
super-productivity:
|
||||
image: johannesjo/super-productivity:v18.12.0@sha256:2c84668a961b090dd931f6e117dde5195b7c674d8453e0d511b777c23c242bc8
|
||||
image: johannesjo/super-productivity:v18.12.1@sha256:a108244f331a1d165f4c52ad343efe739059a078e5f5993f010daf882a53f09e
|
||||
container_name: super-productivity
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
@@ -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,71 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt),
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
**Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default`
|
||||
|
||||
@@ -40,6 +40,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln:
|
||||
|---|---|
|
||||
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
||||
| `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 |
|
||||
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART- und Power-Baseline |
|
||||
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
||||
|
||||
@@ -121,6 +121,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-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)
|
||||
|
||||
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
||||
|
||||
@@ -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 |
|
||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
||||
| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, Photon Reverse Geocoding, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2, Borg + `dawarich.dump` | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert. Reverse Geocoding nutzt standardmaessig `photon.komoot.io` ohne Key. App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. |
|
||||
| `dawarich_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_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 |
|
||||
| `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 |
|
||||
|
||||
@@ -137,7 +137,7 @@ services:
|
||||
- loki
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
|
||||
image: grafana/grafana:13.1.0@sha256:121a7a9ece6dc10b969f1f96eed64b4f07dfac0d0b8abc070f7cb83bbde86f63
|
||||
container_name: monitoring-grafana
|
||||
user: "0"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -20,12 +20,276 @@
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "stat",
|
||||
"title": "Points Last 30 Days",
|
||||
"datasource": {
|
||||
"type": "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": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
@@ -44,18 +308,10 @@
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 16,
|
||||
"w": 16,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
@@ -73,42 +329,29 @@
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"showLegend": false,
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.55,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
"fixed": "green"
|
||||
},
|
||||
"opacity": 0.7,
|
||||
"size": {
|
||||
"fixed": 4,
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"latitude": "lat",
|
||||
"longitude": "lon",
|
||||
"mode": "coords"
|
||||
},
|
||||
"name": "Location points",
|
||||
"name": "Points",
|
||||
"tooltip": true,
|
||||
"type": "markers"
|
||||
}
|
||||
@@ -119,14 +362,30 @@
|
||||
"view": {
|
||||
"allLayers": true,
|
||||
"id": "fit",
|
||||
"lat": 51,
|
||||
"lon": 10,
|
||||
"zoom": 5
|
||||
"lat": 52.0,
|
||||
"lon": 7.5,
|
||||
"zoom": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "table",
|
||||
"title": "Kilometers per Day",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"gridPos": {
|
||||
"x": 14,
|
||||
"y": 4,
|
||||
"w": 10,
|
||||
"h": 6
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
@@ -134,53 +393,15 @@
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n to_timestamp(timestamp) AS \"time\",\n ST_Y(lonlat::geometry) AS latitude,\n ST_X(lonlat::geometry) AS longitude,\n accuracy,\n tracker_id\nFROM points\nWHERE $__unixEpochFilter(timestamp)\n AND lonlat IS NOT NULL\nORDER BY timestamp DESC\nLIMIT 20000;",
|
||||
"refId": "A"
|
||||
"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;"
|
||||
}
|
||||
],
|
||||
"title": "Location Points",
|
||||
"type": "geomap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -192,152 +413,153 @@
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
"show": false
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
"showHeader": true
|
||||
}
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n make_date(year, month, 1)::timestamp AS \"time\",\n round((distance::numeric / 1000.0), 2) AS \"km\"\nFROM stats\nWHERE make_date(year, month, 1)::timestamp BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1;",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Kilometers per Month",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 70,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"sum"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
"type": "table",
|
||||
"title": "Points per Day",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"pluginVersion": "13.0.2",
|
||||
"gridPos": {
|
||||
"x": 14,
|
||||
"y": 10,
|
||||
"w": 10,
|
||||
"h": 6
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "dawarich-postgres"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "time_series",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n date_trunc('day', to_timestamp(timestamp)) AS \"time\",\n count(*) AS \"points\"\nFROM points\nWHERE $__unixEpochFilter(timestamp)\nGROUP BY 1\nORDER BY 1;",
|
||||
"refId": "A"
|
||||
"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;"
|
||||
}
|
||||
],
|
||||
"title": "Points per Day",
|
||||
"type": "timeseries"
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"schemaVersion": 41,
|
||||
"tags": [
|
||||
"dawarich",
|
||||
"location"
|
||||
"homelab",
|
||||
"dawarich"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
@@ -350,6 +572,6 @@
|
||||
"timezone": "browser",
|
||||
"title": "Dawarich",
|
||||
"uid": "dawarich",
|
||||
"version": 1,
|
||||
"version": 5,
|
||||
"weekStart": ""
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ datasources:
|
||||
user: dawarich_grafana_ro
|
||||
editable: false
|
||||
jsonData:
|
||||
database: dawarich_production
|
||||
sslmode: disable
|
||||
postgresVersion: 1700
|
||||
timescaledb: false
|
||||
|
||||
@@ -74,6 +74,13 @@ services:
|
||||
- traefik.http.routers.komodo.tls=true
|
||||
- traefik.http.routers.komodo.tls.certresolver=le
|
||||
- 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:
|
||||
- no-new-privileges:true
|
||||
|
||||
@@ -59,7 +59,7 @@ Stand 2026-06-11 ist der Betrieb auf V1+ (validierte Bash-Host-Jobs mit ntfy):
|
||||
# Frische-Check
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||
|
||||
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|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>
|
||||
|
||||
# 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.
|
||||
- **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`.
|
||||
- **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`. Stand: v1, einmalige Live-Validierung gegen die Box ausstehend.
|
||||
|
||||
## 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,47 @@
|
||||
# Hetzner Storage Box Snapshot Restore - Runbook
|
||||
|
||||
Typ: Runbook · Stand: 2026-06-23 · Status: v1 (Live-Validierung ausstehend)
|
||||
|
||||
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
|
||||
|
||||
## Ausstehend: einmalige Live-Validierung
|
||||
|
||||
Das Skript ist nach den oben belegten Werten gebaut, aber noch nicht gegen die Box
|
||||
gelaufen. Erstlauf:
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh hetzner-snapshot --what-if
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh hetzner-snapshot
|
||||
```
|
||||
|
||||
Falls die Storage-Box-Shell die SFTP-`ls`/`get`-Form oder den `.zfs/snapshot`-Pfad
|
||||
anders erwartet, ueber ENV justieren (kein Code-Change noetig):
|
||||
`SNAPSHOT_DIR`, `PROBE_FILE`, `SNAPSHOT_NAME_GLOB`, `SNAPSHOT_SSH_OPTS`,
|
||||
`MIN_SNAPSHOTS`, `MAX_SNAPSHOT_AGE_HOURS`. Bitte den tatsaechlich funktionierenden
|
||||
SFTP-Aufruf aus dem manuellen Lauf gegenpruefen (Key/Identity, Port).
|
||||
|
||||
Nach erfolgreichem Erstlauf: in `schedule.md` aufnehmen (Vorschlag: monatlich,
|
||||
analog Zufalls-Restore) und Status hier auf "aktiv" setzen.
|
||||
@@ -103,8 +103,14 @@ case "$MODE" in
|
||||
fi
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
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
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
|
||||
+2
-1
@@ -95,7 +95,8 @@
|
||||
"matchFileNames": [
|
||||
"infra/redis/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"],
|
||||
"allowedVersions": "/^8\\.\\d+\\.\\d+-alpine(?:\\d+\\.\\d+)?$/"
|
||||
|
||||
@@ -52,6 +52,17 @@ services:
|
||||
- traefik.http.routers.vaultwarden.tls=true
|
||||
- traefik.http.routers.vaultwarden.tls.certresolver=le
|
||||
- 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:
|
||||
frontend_net:
|
||||
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
- traefik.http.services.homeassistant.loadbalancer.server.port=8123
|
||||
|
||||
mosquitto:
|
||||
image: eclipse-mosquitto:2.0.22@sha256:914f529386804c8278a4e581526b9be5e1604df44b30daabc70aa97dcefe5268
|
||||
image: eclipse-mosquitto:2.0.22@sha256:212f89e1eaeb2c322d6441b64396e3346026674db8fa9c27beac293405c32b3c
|
||||
container_name: smarthome-mosquitto
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user