From f2963385304d7a71a6aba6b513a323ee78a67301 Mon Sep 17 00:00:00 2001 From: Micha Date: Sun, 21 Jun 2026 17:54:53 +0200 Subject: [PATCH] monitoring + backup: Stale-Handle-Hardening und Dead-Man's-Switch Schliesst den lokalen Code-Stand fuer zwei offene MASTER_TODO-Punkte ab. monitoring: restliche Einzeldatei-Bind-Mounts (alertmanager, blackbox, loki, promtail, alertmanager-ntfy-bridge) auf Directory-Mounts umgestellt, analog zum Prometheus-Fix vom 2026-06-19. Vermeidet "Stale NFS file handle" auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. grafana-provisioning war bereits Directory-Mount. `docker compose config` gruen. Beim Deploy --force-recreate noetig, da sich Mount-Zielpfade aendern. backup: endpoint-agnostischer Dead-Man's-Switch (Healthchecks-kompatibel, Cloud oder self-hosted) in pull-critical-backups.ps1 und pre-borg.sh. Pings /start, Erfolg und /fail; No-Op ohne konfigurierte URL, bricht also keinen Lauf. Ping-URLs sind Capability-URLs und bleiben als Secret ausserhalb des Repos. Doku: SECRETS_MAP, Nearline-README und MASTER_TODO nachgezogen. Co-Authored-By: Claude Opus 4.8 --- docs/MASTER_TODO.md | 6 +- docs/SECRETS_MAP.md | 3 + monitoring/docker-compose.yml | 29 +++-- ops/borg-ui/scripts/pre-borg.sh | 22 ++++ ops/h-drive-nearline/README.md | 43 ++++++- .../pull-critical-backups.ps1 | 110 ++++++++++++------ 6 files changed, 162 insertions(+), 51 deletions(-) diff --git a/docs/MASTER_TODO.md b/docs/MASTER_TODO.md index 89212f3..ca3bca6 100644 --- a/docs/MASTER_TODO.md +++ b/docs/MASTER_TODO.md @@ -1,6 +1,6 @@ # Master To-do - KalliLab CORE -Typ: Status/To-do · Stand: 2026-06-18 · Status: aktiv +Typ: Status/To-do · Stand: 2026-06-21 · Status: aktiv Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit @@ -25,8 +25,8 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie. | Restore-Test Tailscale | Operator | State-Validierung + Reconnect nur auf Wegwerf-Host/VM, danach Geraet in Tailscale-Admin entfernen | `ops/restore-tests/tailscale-runbook.md` | | Authelia OIDC fuer Apps | Operator/Codex | Live: Grafana + Mealie login-verifiziert; Paperless Secret verdrahtet und Service-Smoke am 2026-06-17 gruen, finaler Browser-Login mit Operator-Account offen. Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` | | Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` | -| Nearline-Pull Dead-Man's-Switch | Operator | H:-Pull war ~2026-06-04 bis 2026-06-18 still gestoppt (Task fehlte, kein Alarm). Lauf nachgeholt + Scheduled Task `KalliLab H Drive Nearline Pull` neu registriert (State Ready). **Verbleibt:** externer Dead-Man's-Switch (Healthchecks.io-Ping am Ende von `pull-critical-backups.ps1` und `ops/borg-ui/scripts/pre-borg.sh`), da Prometheus auf Unraid den baerchen-Pull nicht sieht | `ops/h-drive-nearline/README.md` | -| Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | Prometheus am 2026-06-19 auf stabilen Directory-Mount (`./prometheus:/etc/prometheus/config:ro`) umgestellt + recreated -> Stale-Handle-Footgun dort beseitigt, Reload reicht wieder. **Verbleibt:** gleiches Einzeldatei-Muster bei alertmanager/blackbox/loki/promtail/grafana-provisioning praeventiv auf Directory-Mount umstellen | `monitoring/docker-compose.yml` | +| Nearline-Pull Dead-Man's-Switch | Operator | Heartbeat-Pings (`/start`, `/fail`, Erfolg) sind lokal in `pull-critical-backups.ps1` und `ops/borg-ui/scripts/pre-borg.sh` verdrahtet (endpoint-agnostisch, No-Op ohne URL). **Verbleibt:** Operator legt je einen Healthchecks-Check an, hinterlegt die Capability-URL (baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` bzw. `%USERPROFILE%\.kallilab\…`; Unraid: `/mnt/user/appdata/secrets/healthchecks_borg_url`) und macht je einen Testlauf | `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` | --- diff --git a/docs/SECRETS_MAP.md b/docs/SECRETS_MAP.md index bf50891..5482820 100644 --- a/docs/SECRETS_MAP.md +++ b/docs/SECRETS_MAP.md @@ -48,6 +48,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb | nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu | | Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv | | Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv | +| Healthchecks Dead-Man's-Switch (Borg Pre-Hook) | Ping-/Capability-URL | `/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600) **oder** ENV `HEALTHCHECKS_BORG_URL`/`HEALTHCHECKS_URL`, gelesen von `ops/borg-ui/scripts/pre-borg.sh`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup | +| Healthchecks Dead-Man's-Switch (Nearline-Pull) | Ping-/Capability-URL | baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` **oder** `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`, gelesen von `ops/h-drive-nearline/pull-critical-backups.ps1`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup | | Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln | | Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen | | Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen | @@ -117,6 +119,7 @@ Weitere dokumentierte Secret-Pfade: - Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden. - Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor. Der Wert ist laut Operator-Bestaetigung vom 2026-05-26 offline gesichert; Ablageort und Wert werden nicht im Repo dokumentiert. - Gitea verwaltet den GitHub-Push-Mirror-PAT in den Repository-Mirror-Settings. Der Wert wird nicht dokumentiert und nicht in Dateien unter `docs/` oder `core/gitea/` geschrieben. +- Die beiden Healthchecks-Ping-URLs (Borg-Pre-Hook, Nearline-Pull) sind Capability-URLs und werden wie Secrets behandelt; sie liegen nicht im Repo. Die Skript-Integration ist endpoint-agnostisch (Healthchecks.io-Cloud oder self-hosted). Ist keine URL gesetzt, sind die Pings ein No-Op und brechen keinen Lauf ab. Operator-Setup-Schritte: `ops/h-drive-nearline/README.md` Abschnitt "Externer Dead-Man's-Switch". - `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort, Redis-URL und OIDC-Client-Secret bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren. - `baerchen` nutzt fuer das Veeam-Backup aktuell den bestehenden SMB-User `micha`. Ein dedizierter SMB-User `veeam-baerchen` ist nur ein spaeteres diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index f0ed391..fe12cbd 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -32,10 +32,13 @@ services: container_name: monitoring-alertmanager restart: unless-stopped command: - - --config.file=/etc/alertmanager/alertmanager.yml + - --config.file=/etc/alertmanager/config/alertmanager.yml - --storage.path=/alertmanager volumes: - - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + # Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus): + # /mnt/user-FUSE bricht Einzeldatei-Bind-Mounts bei git/Komodo-Updates + # zu "Stale NFS file handle" -> Directory-Inode ist stabil. + - ./alertmanager:/etc/alertmanager/config:ro - alertmanager_data:/alertmanager networks: - monitoring_net @@ -57,7 +60,9 @@ services: - python - /app/bridge.py volumes: - - ./alertmanager-ntfy-bridge/bridge.py:/app/bridge.py:ro + # Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus): + # vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. + - ./alertmanager-ntfy-bridge:/app:ro networks: - monitoring_net expose: @@ -75,9 +80,11 @@ services: dns: - 172.23.0.3 command: - - --config.file=/etc/blackbox_exporter/blackbox.yml + - --config.file=/etc/blackbox_exporter/config/blackbox.yml volumes: - - ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro + # Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus): + # vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. + - ./blackbox:/etc/blackbox_exporter/config:ro networks: - monitoring_net - dns_net @@ -91,9 +98,11 @@ services: container_name: monitoring-loki restart: unless-stopped command: - - -config.file=/etc/loki/loki-config.yml + - -config.file=/etc/loki/config/loki-config.yml volumes: - - ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro + # Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus): + # vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. + - ./loki:/etc/loki/config:ro - loki_data:/loki networks: - monitoring_net @@ -107,9 +116,11 @@ services: container_name: monitoring-promtail restart: unless-stopped command: - - -config.file=/etc/promtail/promtail-config.yml + - -config.file=/etc/promtail/config/promtail-config.yml volumes: - - ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro + # Verzeichnis-Mount statt Einzeldatei (Begruendung siehe prometheus): + # vermeidet Stale-Handle auf dem /mnt/user-FUSE-Share bei git/Komodo-Updates. + - ./promtail:/etc/promtail/config:ro - promtail_positions:/positions - /var/run/docker.sock:/var/run/docker.sock:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro diff --git a/ops/borg-ui/scripts/pre-borg.sh b/ops/borg-ui/scripts/pre-borg.sh index 70ddfb9..a8d9f8d 100755 --- a/ops/borg-ui/scripts/pre-borg.sh +++ b/ops/borg-ui/scripts/pre-borg.sh @@ -22,12 +22,31 @@ case "${DUMP_ROOT:-}" in ;; esac +# Externer Dead-Man's-Switch (endpoint-agnostisch: Healthchecks.io-Cloud oder +# self-hosted). ntfy meldet nur Fehler eines tatsaechlich gestarteten Laufs; +# der externe Switch faengt zusaetzlich den Fall ab, dass der Pre-Hook gar nicht +# laeuft. Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln, +# niemals ins Repo. Ist keine URL gesetzt, ist der Switch ein No-Op. +HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-${HEALTHCHECKS_BORG_URL:-}}" +HEALTHCHECKS_URL_FILE="${HEALTHCHECKS_URL_FILE:-/mnt/user/appdata/secrets/healthchecks_borg_url}" +if [ -z "$HEALTHCHECKS_URL" ] && [ -r "$HEALTHCHECKS_URL_FILE" ]; then + HEALTHCHECKS_URL="$(tr -d '[:space:]' < "$HEALTHCHECKS_URL_FILE")" +fi + +hc_ping() { + # $1: optionaler Suffix ("/start" | "/fail"); leer = Erfolg + [ -n "$HEALTHCHECKS_URL" ] || return 0 + command -v curl >/dev/null 2>&1 || return 0 + curl -fsS -m 10 --retry 3 "${HEALTHCHECKS_URL}${1:-}" >/dev/null 2>&1 || true +} + notify_failure() { local step="$1" local message="$2" if [ -x "$NTFY_SCRIPT" ]; then "$NTFY_SCRIPT" "$NTFY_TOPIC" "Borg pre-hook failed: $step" "$message" high || true fi + hc_ping "/fail" } run_step() { @@ -44,6 +63,8 @@ run_step() { fi } +hc_ping "/start" + echo "[pre-borg] Running posture-check" if "$POSTURE_CHECK"; then echo "[pre-borg] OK: posture-check" @@ -60,3 +81,4 @@ run_step "pre-backup-dumps" "$PRE_BACKUP_DUMPS" run_step "restore-freshness" env DUMP_ROOT="$FRESHNESS_DUMP_ROOT" "$FRESHNESS_CHECK" echo "[pre-borg] All pre-flight checks passed" +hc_ping diff --git a/ops/h-drive-nearline/README.md b/ops/h-drive-nearline/README.md index 4f3ebb3..6d4f654 100644 --- a/ops/h-drive-nearline/README.md +++ b/ops/h-drive-nearline/README.md @@ -1,6 +1,6 @@ # H:/ Nearline-Backup — Struktur und Betrieb -Stand: 2026-06-10 +Stand: 2026-06-21 ## Rolle der H:/ @@ -45,6 +45,47 @@ Das Script schließt bewusst aus: - `unraid-flash-config.tar.gz` (0600 root:root, nicht per SMB zugänglich → Restore aus Hetzner-Borg) - Migration-/Cutover-Verzeichnisse (`immich-vectorchord-*`, `pg18-major-*`, `redis8-*` etc.) +## Externer Dead-Man's-Switch + +Der Pull lief ~2026-06-04 bis 2026-06-18 still gestoppt, ohne dass etwas Alarm +schlug (Scheduled Task fehlte; Prometheus auf Unraid sieht den baerchen-Pull +nicht). Gegenmittel: ein externer Heartbeat. `pull-critical-backups.ps1` pingt am +Lauf-Anfang `/start`, am Ende den Erfolg und im Fehlerfall `/fail`. Bleibt der +Erfolgs-Ping aus, alarmiert der externe Dienst von aussen. + +Die Integration ist **endpoint-agnostisch**: jede Healthchecks-kompatible URL +funktioniert (Healthchecks.io-Cloud oder self-hosted). Ist keine URL gesetzt, ist +der Switch ein No-Op und der Pull laeuft unveraendert weiter. + +URL-Quelle (in dieser Reihenfolge): + +1. Parameter `-HealthchecksUrl` +2. ENV `HEALTHCHECKS_NEARLINE_URL` +3. Datei `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt` + +Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln, nie ins Repo +(siehe `docs/SECRETS_MAP.md`). + +### Operator-Setup (einmalig) + +1. Check anlegen (Healthchecks.io oder self-hosted): Period = 1 Tag, Grace z. B. + 2-3 h passend zum taeglichen 05:30-Task. Ping-URL kopieren. +2. Auf baerchen die URL hinterlegen, z. B. als Datei: + ```powershell + New-Item -ItemType Directory -Force -Path "$HOME\.kallilab" | Out-Null + Set-Content -LiteralPath "$HOME\.kallilab\healthchecks-nearline-url.txt" -Value "https://hc-ping.com/" -NoNewline + ``` + (alternativ als persistente User-Umgebungsvariable `HEALTHCHECKS_NEARLINE_URL`.) +3. Testlauf: `pull-critical-backups.ps1` ausfuehren; im Healthchecks-Dashboard + muss ein "success"-Ping ankommen. + +### Borg-Pre-Hook-Pendant + +Den gleichen Switch gibt es host-seitig in `ops/borg-ui/scripts/pre-borg.sh`. +URL dort via ENV `HEALTHCHECKS_BORG_URL` oder Datei +`/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600), bewusst als +**eigener** Check (getrennter Heartbeat fuer die Unraid-Backup-Vorpruefung). + ## _dr-kit Enthält offline hinterlegte Schlüssel und Secrets für den DR-Fall: diff --git a/ops/h-drive-nearline/pull-critical-backups.ps1 b/ops/h-drive-nearline/pull-critical-backups.ps1 index dedb07f..7255186 100644 --- a/ops/h-drive-nearline/pull-critical-backups.ps1 +++ b/ops/h-drive-nearline/pull-critical-backups.ps1 @@ -1,11 +1,35 @@ param( [string]$SourceRoot = "\\192.168.178.58\backups", [string]$DestinationRoot = "H:\kallilab-nearline-backups", + [string]$HealthchecksUrl = $env:HEALTHCHECKS_NEARLINE_URL, [switch]$WhatIf ) $ErrorActionPreference = "Stop" +# Externer Dead-Man's-Switch (endpoint-agnostisch: Healthchecks.io-Cloud oder +# self-hosted). Bleibt der Erfolgs-Ping aus, alarmiert der externe Dienst von +# aussen - genau den Fall, den Prometheus auf Unraid fuer den baerchen-Pull +# nicht sieht. Die Ping-URL ist eine Capability-URL -> wie ein Secret behandeln, +# niemals ins Repo. Quelle: -HealthchecksUrl -> $env:HEALTHCHECKS_NEARLINE_URL +# -> Datei im Userprofil. Ist keine URL gesetzt, ist der Switch ein No-Op. +if (-not $HealthchecksUrl) { + $hcUrlFile = Join-Path $HOME ".kallilab\healthchecks-nearline-url.txt" + if (Test-Path -LiteralPath $hcUrlFile) { + $HealthchecksUrl = (Get-Content -LiteralPath $hcUrlFile -Raw).Trim() + } +} + +function Send-HealthcheckPing { + param([string]$Suffix = "") + if (-not $HealthchecksUrl) { return } + try { + Invoke-RestMethod -Uri ("{0}{1}" -f $HealthchecksUrl, $Suffix) -Method Get -TimeoutSec 15 | Out-Null + } catch { + Write-Warning "Healthchecks ping ('$Suffix') failed: $($_.Exception.Message)" + } +} + $Jobs = @( @{ Name = "borg-dumps-latest" @@ -145,44 +169,54 @@ if ($WhatIf) { exit 0 } -$destinationDrive = Split-Path -Qualifier $DestinationRoot -Assert-PathExists -Path $destinationDrive -Label "Destination drive" +# Echter Lauf -> Dead-Man's-Switch aktiv. /start misst die Laufzeit, /fail +# meldet einen abgebrochenen Lauf sofort, der Erfolgs-Ping am Ende bestaetigt. +Send-HealthcheckPing "/start" +try { + $destinationDrive = Split-Path -Qualifier $DestinationRoot + Assert-PathExists -Path $destinationDrive -Label "Destination drive" -$logRoot = Join-Path $DestinationRoot "_logs" -$reportRoot = Join-Path $DestinationRoot "_reports" -New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null + $logRoot = Join-Path $DestinationRoot "_logs" + $reportRoot = Join-Path $DestinationRoot "_reports" + New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null -$results = foreach ($job in $Jobs) { - Invoke-RobocopyJob -Job $job -LogRoot $logRoot + $results = foreach ($job in $Jobs) { + Invoke-RobocopyJob -Job $job -LogRoot $logRoot + } + + $reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss")) + + $lines = @() + $lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $lines += "" + $lines += "- Source root: ``$SourceRoot``" + $lines += "- Destination root: ``$DestinationRoot``" + $lines += "- Mode: non-destructive copy, no ``/MIR``, no purge" + $lines += "" + $lines += "| Job | Exit code | Source | Destination | Log |" + $lines += "|---|---:|---|---|---|" + foreach ($result in $results) { + $lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |" + } + $lines += "" + $lines += "Expected critical artifacts after run:" + $lines += "" + $lines += "- ``borg-dumps/latest/immich.dump``" + $lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``" + $lines += "- ``git-bundles/gitea/latest-report.md``" + $lines += "- ``git-bundles/gitea/micha/*.bundle``" + $lines += "" + $lines += "Bewusst NICHT in Nearline-Scope:" + $lines += "" + $lines += "- ``unraid-flash-config.tar.gz`` (hostseitig 0600 root:root; Restore aus Hetzner-Borg)" + + $lines | Set-Content -LiteralPath $reportPath -Encoding UTF8 + + Write-Host "H:/ nearline pull completed." + Write-Host "Report: $reportPath" + + Send-HealthcheckPing +} catch { + Send-HealthcheckPing "/fail" + throw } - -$reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss")) - -$lines = @() -$lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -$lines += "" -$lines += "- Source root: ``$SourceRoot``" -$lines += "- Destination root: ``$DestinationRoot``" -$lines += "- Mode: non-destructive copy, no ``/MIR``, no purge" -$lines += "" -$lines += "| Job | Exit code | Source | Destination | Log |" -$lines += "|---|---:|---|---|---|" -foreach ($result in $results) { - $lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |" -} -$lines += "" -$lines += "Expected critical artifacts after run:" -$lines += "" -$lines += "- ``borg-dumps/latest/immich.dump``" -$lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``" -$lines += "- ``git-bundles/gitea/latest-report.md``" -$lines += "- ``git-bundles/gitea/micha/*.bundle``" -$lines += "" -$lines += "Bewusst NICHT in Nearline-Scope:" -$lines += "" -$lines += "- ``unraid-flash-config.tar.gz`` (hostseitig 0600 root:root; Restore aus Hetzner-Borg)" - -$lines | Set-Content -LiteralPath $reportPath -Encoding UTF8 - -Write-Host "H:/ nearline pull completed." -Write-Host "Report: $reportPath"