# Kotsch Backup-Wächter — Design

**Datum:** 2026-07-09 · **Branch:** feat/wissen-subdomain · **Status:** freigegeben

## Ziel

Perfektes, weitgehend automatisches Backup-System für den kotsch.tech-Server (Windows Server
2025, XAMPP). Ziel-Speicher = SMB-Share `\\VM-SERVER\Backup_1` (externe HDD an einem anderen
Server). Gesichert werden **alle Websites** (`C:\xampp\htdocs`, ~24 Sites), **alle Datenbanken**
(MySQL/MariaDB + SQLite), **Secrets** (.env, Lizenz-Keys — verschlüsselt) und **Server-Configs**.

### Anforderungen (aus Nutzer-Auftrag)
1. Alle Websites regelmäßig sichern, dazu Software/Configs + Datenbanken.
2. **Auto-Trigger:** Wenn die HDD „erscheint" (Share wird erreichbar) → Backup startet automatisch.
3. **Start-Mail:** E-Mail sobald ein Backup beginnt.
4. **Warten:** Backup wird komplett abgewartet, dann Ergebnis-Mail.
5. **Datierte Ablage:** Speicherung unter Name + aktuellem Datum.
6. **Überfällig-Warnung:** E-Mail wenn länger als 1 Monat kein Backup lief.
7. **Admin-Panel:** Live-Update-Cockpit inkl. freiem Plattenspeicher (Share + C:).

## Architektur — Hybrid

Schweres Kopieren macht **PowerShell/robocopy** (riesige Dateien, resumierbar, robust; C: hat nur
~6,5 GB frei → kein lokales Zwischenlagern, direkt auf Share). Die **Glue-Logik** (Datensätze,
Mails, Aufräumen, Config) liegt in **testbaren Laravel-artisan-Commands**. Das **Admin-Panel** liest
den Live-Status. Trigger = **Windows-Aufgabe** (Watcher, pollt Share-Erreichbarkeit).

```
HDD an VM-SERVER → Share erreichbar → Watcher-Task erkennt Übergang (oder Panel-Trigger-Datei)
  → Invoke-KotschBackup.ps1
      → artisan backup:config           (Einstellungen als JSON ziehen)
      → artisan backup:record-start      → BackupRun(status=running) + Start-Mail + status.json
      → robocopy Sites (ohne node_modules/vendor/.git/cache) → datierter Snapshot
      → mysqldump je DB (.sql.gz) + SQLite-Kopie
      → Secrets (.env + Lizenz-Keys) → 7z AES-256 (Passphrase aus .env; ohne → skip+Warnung)
      → Server-Configs kopieren + manifest.json + Verify
      → artisan backup:prune             (letzte N Snapshots behalten)
      → artisan backup:record-finish     → BackupRun(status=success/failed) + Ergebnis-Mail + latest.json
  → Panel /admin/backups pollt /data (alle 4s) → Live-Status + Plattenanzeige
[Laravel-Scheduler täglich] backup:check-overdue → letzter Erfolg > 30 Tage → Warn-Mail (7-Tage-Entprellung)
```

### Warum Poll statt Laufwerks-Event
Die HDD hängt an **VM-SERVER** (anderer Rechner). Dieser Server sieht sie nur als SMB-Share — ein
lokales `WM_DEVICECHANGE`/Volume-Arrival-Event feuert nur bei lokal gesteckter Platte. Also pollt
der Watcher alle 5 Min die Erreichbarkeit von `\\VM-SERVER\Backup_1` und erkennt den Übergang
*nicht erreichbar → erreichbar* als „Platte erschien".

## Speicher-Layout (Name + Datum)

```
\\VM-SERVER\Backup_1\Webseiten\
  _snapshots\2026-07-09_1430\
    kotsch.tech\ … toolaro.org\ … regenbogenhof\ …   (alle Sites, ohne Build-Müll)
    _datenbanken\mysql\  arthurkotsch_online_db.sql.gz, klassenwiki_db.sql.gz, login.sql.gz, website2_db.sql.gz
    _datenbanken\sqlite\ kotsch.tech-database.sqlite
    _secrets\ secrets-2026-07-09.7z          (AES-256, Passphrase in .env)
    _configs\ httpd.conf, vhosts\, php.ini, my.ini, cloudflared\, scheduled-tasks.xml
    manifest.json  +  backup.log
  latest.json  → Zeiger auf jüngsten Erfolg
```
**Retention:** letzte N=5 Snapshots behalten (im Panel änderbar), ältere auto-löschen.

## Komponenten (isoliert)

**Laravel (testbar):**
- `config/backup.php` — Pfade, Ausschlüsse, Tool-Pfade, MySQL-Liste, Secrets/Config-Listen.
- Migration `create_backup_tables` → `backup_runs`, `backup_settings`.
- Models `BackupRun`, `BackupSetting`.
- `App\Support\Backup\BackupManager` — settings(), diskInfo(share+C:), status lesen/schreiben,
  Trigger-Datei, Snapshot-Liste, `pruneSnapshots($dir,$keep)` (rein dateibasiert → unit-testbar).
- Commands: `backup:config`, `backup:record-start`, `backup:record-finish`, `backup:prune`,
  `backup:check-overdue`.
- Mailables `BackupStartedMail`, `BackupFinishedMail`, `BackupOverdueMail` (+ Blade-Views).
- `Admin\BackupController` — index, data(JSON, live), run(Trigger-Datei schreiben), settings, log.
- View `admin/panel/backups/index.blade.php` + Nav-Eintrag „System → Backups".
- Scheduler-Zeile `backup:check-overdue` täglich.

**PowerShell (Motor, im Repo unter `backup/`):**
- `Invoke-KotschBackup.ps1` — ein Lauf. `-DryRun` (robocopy `/L`, kein Site-Copy; DB/Secrets/Configs
  + Records + Mails + Prune real), `-Trigger`, `-NoMail`, `-Php`.
- `Watch-BackupShare.ps1` — Erreichbarkeits-Übergang + Panel-Trigger-Datei → Motor starten.
- `Install-BackupTasks.ps1` — registriert Watcher als Windows-Aufgabe (alle 5 Min).
- `README.md`.

## Sicherheit
- Kein exponierter Endpunkt: PS↔Laravel läuft über lokale `php artisan`-Aufrufe (kein HTTP-Hook).
- Secrets-Bündel **AES-256** (7z `-mhe=on`), Passphrase in `.env` (`BACKUP_SECRETS_PASSPHRASE`).
  Ohne Passphrase/7z → Secrets werden **übersprungen** (nie im Klartext abgelegt) + Warnung.
- Panel hinter `admin.auth` (+2FA). Log-Download nur innerhalb des Backup-Verzeichnisses.

## Tests
- `BackupCommandsTest` — record-start legt Run an + `BackupStartedMail`; record-finish updatet +
  `BackupFinishedMail`; `backup:config` gibt gültiges JSON.
- `BackupOverdueTest` — kein/alter Erfolg → `BackupOverdueMail`; frischer Erfolg → keine Mail;
  Entprellung verhindert Doppel-Mail.
- `BackupPruneTest` — `pruneSnapshots` behält jüngste N, löscht ältere (Temp-Verzeichnis).
- `BackupPanelTest` — index braucht Auth + rendert; `/data` liefert JSON mit Disk + letztem Run;
  `/run` schreibt Trigger-Datei; settings speichert.
- PS-Nachweis: `Invoke-KotschBackup.ps1 -DryRun` real gegen Share (DB-Dumps + Records + Mails +
  Prune echt, Site-Copy nur gelistet).

## Offene Defaults
Retention N=5 · Überfällig 30 Tage · Watcher-Poll 5 Min · Melde-Mail `arthurkotsch@kotsch.tech`.
