From 54fd0c3347de59faa0fdb688435d94245d573331 Mon Sep 17 00:00:00 2001 From: Micha Date: Fri, 15 May 2026 16:05:34 +0200 Subject: [PATCH] =?UTF-8?q?diverse=20=C3=84nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/STORAGE_LAYOUT.draft.md | 412 +++++++++++++ docs/postinstall-erstes-ziel-codex.md | 103 ++++ docs/windows-neuaufsetzen-masterplan.md | 550 ++++++++++++++++++ ops/windows-reinstall/backup-known-data.ps1 | 105 ++++ .../clear-disk0-old-windows-ssd.ps1 | 69 +++ .../postinstall-unigetui.ps1 | 34 ++ .../prepare-backup-inventory.ps1 | 139 +++++ 7 files changed, 1412 insertions(+) create mode 100644 docs/STORAGE_LAYOUT.draft.md create mode 100644 docs/postinstall-erstes-ziel-codex.md create mode 100644 docs/windows-neuaufsetzen-masterplan.md create mode 100644 ops/windows-reinstall/backup-known-data.ps1 create mode 100644 ops/windows-reinstall/clear-disk0-old-windows-ssd.ps1 create mode 100644 ops/windows-reinstall/postinstall-unigetui.ps1 create mode 100644 ops/windows-reinstall/prepare-backup-inventory.ps1 diff --git a/docs/STORAGE_LAYOUT.draft.md b/docs/STORAGE_LAYOUT.draft.md new file mode 100644 index 0000000..6258189 --- /dev/null +++ b/docs/STORAGE_LAYOUT.draft.md @@ -0,0 +1,412 @@ +# Storage Layout — KalliLab CORE + +| Feld | Wert | +|------|------| +| Version | 1.3 (Entwurf) | +| Status | Draft, alle akuten Operator-Entscheidungen eingearbeitet, commit-reif nach Disk-Größen-Eintrag | +| Datum | 2026-05-15 | +| Geltungsbereich | Unraid-Host `Kallilabcore`, alle Pools, Array-Disks, User-Shares, Appdata-Strukturen | +| Verbindlichkeit | Bindend ab Inkraftsetzung. Abweichungen nur dokumentiert als Ausnahme (siehe Abschnitt 12) | +| Vorgänger | keiner; entstanden aus Incident 2026-05-11 (NTFS-Cache-Korruption) | +| Verwandte Docs | `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/REPO_MAP.md`, `docs/SERVICE_CATALOG.md`, `docs/RESTORE_MATRIX.md`, `docs/SECRETS_MAP.md`, `docs/DISASTER_RECOVERY.md` | + +--- + +## 1. Zweck + +Dieses Dokument definiert die verbindliche Storage-Architektur für den KalliLab-CORE-Homelab-Stack. Es beantwortet: + +- Welche physikalischen Disks gibt es, mit welchem Filesystem. +- Welche User-Shares existieren, mit welchen Cache- und Allokations-Settings. +- Wo liegen Container-Daten und in welcher Sub-Struktur. +- Welche Pfade dürfen in Compose-Dateien als Bind-Mount auftauchen, welche nie. +- Welche Backup- und Posture-Check-Pflichten daraus folgen. + +Es ist **vor** jeder Storage- oder Compose-Änderung zu lesen. Wenn ein neuer Stack, ein neuer Pfad, eine neue Disk, ein neuer Share gebraucht wird und hier nicht spezifiziert ist, wird dieses Dokument **erst** ergänzt, dann implementiert. + +## 2. Leitprinzipien + +1. **Filesystem-Semantik ist nicht verhandelbar.** Datenbanken, Container-Persistenz und Array-Daten brauchen Linux-natives Filesystem (XFS, BTRFS, ZFS). NTFS, exFAT, FAT32 sind ausschließlich für Boot-USB und externe Austauschmedien zulässig, niemals für Cache, Array oder Pool. +2. **Recovery > Convenience.** Eine Konvention, die Restore vereinfacht, schlägt jede Konvention, die nur die Erstinstallation vereinfacht. +3. **Posture-Check als First-Class-Concern.** Jede Annahme über den Host-Zustand (FS-Typ, Mount, Permissions) muss automatisiert prüfbar sein. „Wir wissen schon, dass das stimmt" ist keine zulässige Begründung. +4. **GitOps-Konformität.** Alles, was Compose-, Share-, oder Storage-Verhalten beeinflusst, ist im Repo dokumentiert oder versioniert. Was nur im Host lebt, existiert für Recovery nicht. +5. **Pfad-Stabilität.** Pfade in Compose-Bind-Mounts bleiben stabil über Filesystem-Migrationen, Disk-Wechsel und Pool-Umzüge hinweg. Konkret: Container kennen `/mnt/user/...`-Pfade, nie `/mnt/cache/...` oder `/mnt/disk1/...`. +6. **Keine Sammelordner.** Eine Datei hat genau einen logischen Eigentümer (Stack, Share, Nutzkategorie). Mischungen erzeugen Drift und brechen Backup-Excludes. +7. **Bewusste Heterogenität nur mit Begründung.** Wenn ein Stack vom Standard abweicht (z. B. Host-Network, Privileged, Direct-Cache-Mount), ist die Abweichung in Abschnitt 12 dokumentiert mit Begründung und Risiko-Akzeptanz. + +## 3. Physikalisches Layout (Soll-Zustand) + +| Slot | Device | Filesystem | Größe | Zweck | Status nach Recovery | +|------|--------|------------|-------|-------|----------------------| +| Cache (Pool) | Samsung 970 EVO Plus, NVMe | **XFS** | 2 TB | Appdata, system, domains | Reformat von NTFS auf XFS, Phase 1 | +| Disk1 (Array) | HDD (Modell TBD) | **XFS** | TBD | Nutzdaten, Backups, Services | Migration von NTFS auf XFS, Phase 2 (Folgeprojekt) | +| Parity | HDD (Modell TBD) | — (keine FS) | TBD | Redundanz für Array | Unverändert | +| Boot | USB-Stick | FAT32 | klein | Unraid-OS, Konfiguration | Unverändert, regelmäßig per Flash-Backup gesichert | +| Externe Backup-Platte | Wechselplatte (Modell TBD) | XFS oder ext4 | ~8 TB | Ausgelagertes Backup-Ziel, Recovery-Material | NEU, ersetzt WD MyBookLive | + +**TBD-Felder werden nachgetragen** sobald `lsblk -o NAME,SIZE,MODEL,SERIAL,VENDOR /dev/sd?` und `smartctl -i` für jeden Slot einmal ausgeführt wurden. Vorschlag: dieser Detection-Lauf ist Teil der Posture-Check-Erstinstallation (§11) und das Ergebnis wird automatisch in dieses Dokument eingetragen. Kein Blocker für die Inkraftsetzung — die Festsetzung „XFS auf Cache, XFS auf Disk1" steht unabhängig von Modell/Größe. + +**Begründung Filesystem-Wahl:** + +- **XFS für Cache und Disk1:** Unraid-Standard, robuste fsck, gute Performance für Datenbank-Workloads, niedrige Komplexität, einheitlicher Stack vereinfacht Operations. +- **BTRFS bewusst nicht** für Single-Disk-Cache: Mehrwerte (RAID1, Snapshots) werden ohne zweite NVMe nicht realisiert; zusätzlicher COW-Overhead und Komplexität ohne Gegenwert. +- **ZFS bewusst nicht** für aktuellen Bedarf: höherer RAM-Verbrauch, in der bestehenden Topologie kein Mehrwert; spätere Migration eines dedizierten Pools auf ZFS bleibt offen (siehe Abschnitt 13). + +**Eskalationspfad bei zweiter Cache-NVMe:** Migration zu BTRFS-Pool (RAID1) als bewusste Architektur-Entscheidung mit eigenem Migrations-Plan. Bis dahin: Single-XFS. + +## 4. Logisches Layout — User-Shares + +Verbindliche Share-Definition. Jeder Share hat genau einen primären Datenträger. + +| Share | Cache-Setting | Primary Storage | Allocation | Split Level | Zweck | +|-------|---------------|-----------------|------------|-------------|-------| +| `appdata` | **only** | Cache | High Water | 1 | Container-Persistenz, niemals auf Array | +| `system` | **only** | Cache | High Water | — | Docker-Image bzw. Folder-Mode-Verzeichnis, libvirt — bewusst `only`, weil Mover-Migration der Docker-Image-Datei oder von libvirt-State zwischen FS-Typen riskant ist | +| `domains` | **only** | Cache | High Water | 1 | VM-Disk-Images — Performance und Stabilität für produktive VMs (HAOS, hermes-runner). Wenn ältere/seltene VM-Images aufs Array dürfen, bewusst auf `prefer` herabsetzen, dokumentiert pro VM | +| `isos` | yes | Cache → Array | High Water | 2 | VM-Installations-ISOs, optional | +| `services` | no | Disk1 | High Water | 2 | Gitea, Komodo-State, secrets-Pfad — **recovery-kritisch**, siehe Hinweis unter Tabelle | +| `documents` | no | Disk1 | High Water | 2 | Persönliche Dokumente | +| `photos` | no | Disk1 | High Water | 2 | Persönliche Fotos | +| `backups` | no | Disk1 | High Water | 2 | Lokale Borg-Repos, Appdata-Backup-Archive | +| `media` | no | Disk1 | High Water | 2 | Optional, Media-Files | +| `finance` | no | Disk1 | High Water | 2 | Persönlich | +| `projekte` | no | Disk1 | High Water | 2 | Persönlich | + +**Erklärung der Cache-Settings:** + +- `only`: Daten landen ausschließlich auf Cache, Mover ignoriert sie. Schutz vor Mover-induzierter Daten-Migration zwischen Filesystemen. +- `prefer`: Neue Daten landen auf Cache, Mover schiebt sie bei Cache-Druck auf Array. +- `yes`: Neue Daten landen auf Cache, Mover schiebt sie regelmäßig auf Array (klassisches Cache-Schreibverhalten). +- `no`: Daten umgehen Cache, gehen direkt auf Array. + +**Wahl `only` für `appdata`, `system`, `domains`** ist die wichtigste Setting in diesem Dokument. Sie verhindert genau die Klasse von Vorfällen, die im Mai 2026 passiert ist: dass Container-Daten, Docker-Image-State oder VM-Disks unbemerkt zwischen Filesystemen wandern. Wenn der Cache voll wird, ist das ein Kapazitätsproblem, kein Filesystem-Problem — und wird durch zweite NVMe oder Cache-Tuning gelöst, nicht durch Mover-Migration. + +**`services` ist recovery-kritisch.** Gitea (operative Source of Truth des GitOps-Stacks) und Komodo-State (Deployment-Runtime) liegen dort. Ohne `services` ist Self-Recovery nicht mehr möglich. Daraus folgt: + +- `services` wird vor jedem strukturellen Eingriff am Array (Disk1-Migration Phase 2, Disk-Tausch, Pool-Umzug) **vollständig** auf zweites Medium gesichert +- `services` darf nicht auf einem als instabil bekannten Datenträger leben — Warnung in `posture-check`, Eskalation bei Disk1-FS-Anomalien +- Änderungen an Gitea-Repos, Komodo-Stacks oder secrets-Pfad nur nach erfolgreichem letzten Backup-Check (siehe §11) +- **Mirror-Backup für Gitea-Repo-Inhalte (Operator-Entscheidung 2026-05-15, verbindlich):** zusätzlich zum Standard-Borg-Lauf wird `services/gitea/git/repositories/` per separatem Mirror-Mechanismus auf ein zweites physisches Medium außerhalb Disk1 gesichert, mit Frequenz ≤ 6 h. Konkrete Implementierung (Optionen: `git bundle`-Bundles in separates Repo auf Cache, `rsync` auf externe Platte, separates Borg-Repo mit kürzerem Schedule) wird in `docs/SERVICES_RECOVERY.md` (zu erstellen) detailliert. Begründung: Gitea ist operative Source of Truth des GitOps; Verlust seit letztem Standard-Backup (bis zu 24 h) wäre für Recovery-Pfad unzumutbar. + +**Erklärung Allocation-Method:** „High Water" verteilt neue Daten auf der am wenigsten gefüllten Disk bis zu einer Wasserlinie, dann auf die nächste. Praktisch unauffällig bei Single-Array-Disk; wird relevant bei Erweiterung. + +**Split Level** bestimmt, wie tief Verzeichnisse über Disks aufgeteilt werden dürfen. Für `appdata` zwingend `1` — ein Stack-Verzeichnis bleibt auf einer Disk, wird nie aufgespalten. + +## 5. Appdata-Sub-Struktur + +Verbindliches Layout pro Stack: + +``` +/mnt/user/appdata// +├── config/ # Statische Konfiguration (yaml, conf, env-Dateien) +├── data/ # Live-Datenverzeichnis (DB-Datendir, Container-Persistenz) +├── dumps/ # Pre-Backup-Hooks schreiben hier (pg_dump, mongodump, sqlite .backup) +├── logs/ # Container-Logs, falls nicht via journald gelöst +└── README.md # Pflicht: Kurzbeschreibung, Restore-Zeiger, Sondereigenheiten +``` + +**Regeln:** + +- Ein Verzeichnis pro Stack, kein Mischen mehrerer Stacks +- Stack-Namen sind lowercase, Bindestriche statt Unterstriche (z. B. `mail-archiver`, nicht `Mail_Archiver`) +- `data/` enthält Live-State. **Behandlung pro Stack klassifiziert in `RESTORE_MATRIX.md`:** + - **Reines DB-Datenverzeichnis** (z. B. raw Postgres-`data/`, raw Mongo-`data/`): wird **nicht** direkt gesichert, ist durch `dumps/` abgedeckt + - **Echte Nutzdaten** (z. B. Immich-Uploads, Paperless-Dokumente, Gitea-Repo-Inhalte, Vaultwarden-Attachments, Mealie-Bilder, Grafana-Dashboards/Provisioning): **werden direkt gesichert** oder über expliziten Export-Mechanismus erfasst + - **Mischformen** (z. B. ein Stack mit DB-Files und Nutzdaten im selben `data/`): pro Stack im Detail spezifiziert, mit getrennten Sub-Pfaden wo möglich +- `dumps/` enthält von Pre-Hooks erzeugte konsistente Dumps; **wird immer von Borg gesichert** +- `config/` ist die einzige Quelle der Container-Konfiguration; idealerweise versioniert oder aus Git deploybar +- `README.md` pro Stack: drei Zeilen genügen (was, warum, Restore-Zeiger) + +**Ausnahmen, wenn Container das Layout nicht zulassen:** dokumentiert in `docs/SERVICE_CATALOG.md` pro Stack, mit Verweis hier. + +## 6. Naming-Konventionen + +| Element | Konvention | Beispiel | +|---------|------------|----------| +| Stack-Name | `kebab-case`, lowercase, keine Sonderzeichen | `vaultwarden`, `mail-archiver`, `immich-server` | +| Container-Name | identisch zu Stack-Name (oder `-`), kebab-case | `postgresql-17`, `immich-postgres`, `komodo-mongo` | +| Volume-Pfad in Compose | `/mnt/user/appdata//` | `/mnt/user/appdata/vaultwarden/data:/data` | +| Netz-Name | `frontend_net`, `backend_net`, oder `_internal` | `frontend_net`, `immich_internal` | +| Env-Datei | `.env`, im Komodo-Stack-Workspace, **nicht** im Repo | — | + +**Begründung Bindestriche:** vermeiden Verwechslung mit Shell-Variablen (Unterstriche werden in env vars verwendet), passen besser zu Docker-Compose-Konventionen, bleiben URL-safe. + +**Migration Bestand → kebab-case (Operator-Entscheidung 2026-05-15):** existierende Stacks mit Unterstrichen oder gemischten Konventionen (`postgresql17`, `immich_postgres`, `komodo-mongo` etc.) werden im Rahmen der Recovery-Phase (Cache-Restore Phase 1, Disk1-Migration Phase 2) auf kebab-case migriert. Pro Stack: alter Name → neuer Name in `docs/RESTORE_MATRIX.md` dokumentiert. Container-Renames erfolgen sauber per Compose-Down → Rename → Compose-Up mit neuer Identität, **nicht** im laufenden Betrieb. Netzwerk-Referenzen, Healthchecks und Inter-Stack-Abhängigkeiten werden im selben Schritt mit angepasst. Nicht-migrierte Stacks am Ende der Recovery-Phase werden in §20 als bewusste Ausnahme dokumentiert oder auf einen Folge-Termin verschoben. + +## 7. Permissions-Modell + +| Anwendungsfall | UID:GID | Begründung | +|----------------|---------|------------| +| Standard-Files in `appdata/` | `99:100` (`nobody:users`) | Unraid-Standard, von shfs/Mover sauber behandelt | +| Container-spezifischer User | individuell, dokumentiert | manche Images erwarten zwingend `1000:1000` o. ä. | +| `secrets/`-Verzeichnis | `0:0`, mode `0700` oder `0750` | Schutz vor versehentlichem Read | +| `logs/` | `99:100`, mode `0755` | Lesbar für Debugging, nicht weltschreibbar | +| `dumps/` | `99:100`, mode `0750` | Backup-Hook schreibt, Borg liest | + +**Regel:** Permission-Setzung beim Stack-Restore ist explizit per Skript oder Init-Container, nicht „erstmal `chown -R root:root` und gucken was kaputtgeht". Dokumentiert pro Stack. + +## 8. Backup-Architektur + +### 8.0 Aktueller Ist-Zustand vs. Ziel-Zustand + +**Operator-Entscheidung 2026-05-15:** Backrest wird abgeschaltet. **Borg ist die alleinige Backup-Technologie.** Restic-Repos werden nicht weiterentwickelt; vorhandene Restic-Daten verbleiben als historisches Material bis zur expliziten Entsorgung. + +**Ist-Zustand zum Zeitpunkt der Inkraftsetzung:** + +- Borg via Hetzner Storagebox: produktiv, im Vorfall 2026-05-11 als Restore-Quelle verifiziert +- Backrest/restic: **wird abgeschaltet** — Container nicht mehr starten, Repo-Daten archivieren oder löschen, Stack-Eintrag aus Compose entfernen +- Appdata-Backup-Plugin (CIFS auf WD MyBookLive): **abgeschaltet, Ziel komplett aus Setup entfernt** +- Lokales Borg-Repo auf `/mnt/user/backups/borg/`: einzurichten falls noch nicht vorhanden, als zweites Borg-Ziel neben Hetzner + +**Ziel-Zustand (was dieses Dokument definiert):** + +- Borg ist die einzige produktive Backup-Technologie (lokal + remote) +- Backup-Pfade, Pre-Hooks und Verifikation einheitlich über Borg +- Migrationspfad Backrest-Abschaltung pro Stack in `docs/RESTORE_MATRIX.md` + +Dieses Dokument definiert die Zielarchitektur. Abweichungen zwischen Ist und Soll sind dokumentiert in `docs/RESTORE_MATRIX.md`. + +### 8.1 Backup-Ziele + +| Ziel | Typ | Zweck | Retention | +|------|-----|-------|-----------| +| `/mnt/user/backups/borg/` (lokal) | Borg-Repo auf Disk1 | Schneller lokaler Restore | 30 Tage täglich, 12 Monate monatlich | +| Hetzner Storagebox | Borg-Repo via SSH | Off-Site, Disaster Recovery | 90 Tage täglich, 24 Monate monatlich | +| Externe Wechselplatte | Cold Storage, manuell | Air-Gap, Recovery-Material | rotiert manuell, kein festes Schema | + +**WD MyBookLive ist explizit kein Backup-Ziel mehr.** Eintrag in `docs/SECRETS_MAP.md` und allen Backup-Konfigs entfernt. + +### 8.2 Backup-Inhalt + +**Pflicht-Backup-Scope (Borg, beide Repos):** + +- `/mnt/user/appdata/*/config/` +- `/mnt/user/appdata/*/dumps/` +- `/mnt/user/appdata/*/README.md` +- `/mnt/user/appdata/*/data/` für Stacks mit Nutzdaten in `data/` (siehe §5 und Klassifikation in `RESTORE_MATRIX.md`) +- `/mnt/user/services/` +- `/boot/config/` (Unraid-Konfiguration) +- **persönliche Daten vollständig im Scope** (Operator-Entscheidung 2026-05-15): + - `/mnt/user/documents/` + - `/mnt/user/photos/` + - `/mnt/user/finance/` + - `/mnt/user/projekte/` + - `/mnt/user/media/` (falls behalten) + +Borg dedupliziert über Läufe, der erste Vollbackup ist groß, Folge-Backups inkrementell. Retention nach §8.1. + +**Pflicht-Excludes (mit Vorsicht — siehe §5):** + +- `**/data/` innerhalb `appdata/*/` **nur dann**, wenn `data/` ausschließlich rohes DB-Datenverzeichnis ist (durch `dumps/` abgedeckt). Stacks mit echten Nutzdaten in `data/` (Immich, Paperless, Gitea, Vaultwarden, Mealie, Grafana etc.) sind aus diesem Exclude **explizit ausgenommen** — pro Stack klassifiziert in `docs/RESTORE_MATRIX.md`. Unklassifizierte Stacks gelten als „nicht ausschließbar" — lieber zuviel sichern als versehentlich zu wenig. +- `**/cache/`, `**/tmp/`, `**/lost+found/` (jeweils generisch unbedenklich) +- Container-Image-Layer (`/var/lib/docker/`) +- VM-Disk-Images (`/mnt/user/domains/`) außer auf Operator-Entscheidung pro VM + +### 8.3 Pre-Backup-Hooks pro DB-Stack + +Verbindlich. Jeder Stack mit eigener DB hat einen Pre-Hook, der einen konsistenten Dump nach `appdata//dumps/` schreibt: + +| Stack-Typ | Befehl | Ziel | +|-----------|--------|------| +| Postgres | `pg_dump -Fc` oder `pg_dumpall --globals-only` | `appdata//dumps/.dump` | +| MariaDB/MySQL | `mariadb-dump --single-transaction` | `appdata//dumps/.sql` | +| MongoDB | `mongodump --archive=...` | `appdata//dumps/mongo.archive.gz` | +| SQLite (Vaultwarden, Gitea) | `sqlite3 db .backup target.db` | `appdata//dumps/.sqlite` | +| Redis | `redis-cli BGSAVE` plus Kopie der `dump.rdb` | `appdata//dumps/dump.rdb` | + +**Dumps sind nur dann wertvoll, wenn sie regelmäßig gegen Restore-Test verifiziert werden.** Siehe Abschnitt 11. + +### 8.4 Backup-Verifikation + +| Frequenz | Prüfung | Pass-Kriterium | +|----------|---------|----------------| +| Pro Backup-Lauf | Exit-Code, Dauer im erwarteten Korridor, Archivgröße im erwarteten Korridor | alle drei grün, sonst Alarm | +| Wöchentlich | `borg check --repository-only` auf beiden Repos | fehlerfrei | +| Monatlich | Probe-Restore eines zufällig ausgewählten Stacks aus Hetzner-Repo in Wegwerf-Pfad | restored Files lesbar, DB-Dump per `pg_restore --list` lesbar | +| Quartalsweise | Voll-Restore-Drill einer kompletten App-Klasse (z. B. Gitea allein) auf Test-Pfad | App startet, Login funktioniert | + +Backup-Verifikations-Ergebnisse landen in `/mnt/user/services/backup-verify/` als Log mit Zeitstempel. + +## 9. Mover-Policy + +- Mover läuft nicht auf einem Schedule, der parallel zu Backup-Jobs liegt +- Mover wird **niemals** für Shares mit `cache: only` aktiv +- Mover wird vor planmäßigen Reboots manuell ausgelöst und abgewartet +- Mover-Logs werden vom Posture-Check auf Anomalien geprüft + +## 10. Network-Bezug zu Storage + +Reine Verweis-Sektion. Authoritative Definition in `HOMELAB_ARCHITECTURE_MASTER_V2.md`. + +- Datenbanken, die Storage-intensiv sind, hängen ausschließlich an `backend_net` (`internal: true`), niemals an `frontend_net` +- Backup-Container hängen entweder an `backend_net` (für DB-Connect) oder eigenem internen Netz, nicht an `frontend_net` +- Es gibt keine Storage-Pfade, die über `frontend_net`-Container exponiert werden + +## 11. Posture-Check (Pflicht-Audit) + +Automatisierter Check, der mindestens täglich läuft (z. B. via User-Script oder Cron im posture-check-Stack), und bei jedem Fail einen Alarm produziert. + +**Zusätzlich zwingend ausgelöst:** + +- **Bei jedem Boot** des Hosts (Unraid User-Script `at start`, oder systemd-äquivalent) — fängt sofort ab, wenn nach Reboot ein Mount falsch ist +- **Vor jedem Backup-Lauf** als Pre-Hook — wenn Posture-Check fehlschlägt, **wird das Backup abgebrochen und alarmiert**, statt fragwürdige Daten zu sichern und so die Backup-Historie zu kontaminieren +- **Nach jedem Mover-Lauf** als Post-Hook — verifiziert, dass Mover keine `cache: only`-Shares angefasst hat +- **Vor jeder strukturellen Änderung** (Disk-Tausch, Pool-Umzug, Format-Aktion) manuell — Soll-Ist-Abgleich vor destruktiven Aktionen + +**Alarmziel (Operator-Entscheidung 2026-05-15): ntfy.** Selbst gehosteter ntfy-Server oder ntfy.sh, Topic-Konvention `kallilab-` (z. B. `kallilab-critical`, `kallilab-warning`). Push-Empfänger sind die Mobil-Geräte des Operators. Optional als Folge-Erweiterung: E-Mail-Fallback über mail-archiver-Stack als persistente Trail für historische Auswertung — nicht jetzt entscheiden, kann später dazukommen. + +**Pflichtprüfungen:** + +| Check | Erwartung | Bei Fail | +|-------|-----------|----------| +| `findmnt -no FSTYPE /mnt/cache` | `xfs` | Sofortalarm, kritisch | +| `findmnt -no FSTYPE /mnt/disk1` | `xfs` (nach Phase 2) | Sofortalarm, kritisch | +| `mount` enthält keinen `ntfs3`- oder `fuseblk`-Eintrag auf Cache/Array | wahr | Sofortalarm | +| `nvme smart-log /dev/nvme0n1` Critical Warning | `0x00` | Sofortalarm | +| `nvme smart-log` Media Errors | `0` oder stabil zur Vorwoche | Warnung | +| D-State-Prozesse > 60s | keine | Warnung, ab 5 Min Alarm | +| iowait über 5-Min-Mittel | < 30 % | Warnung ab 50 %, Alarm ab 80 % | +| Letztes erfolgreiches Borg-Archiv (lokal) | < 26 h alt | Warnung ab 30 h, Alarm ab 48 h | +| Letztes erfolgreiches Borg-Archiv (Hetzner) | < 26 h alt | Warnung ab 30 h, Alarm ab 48 h | +| Erwartete Mindestgröße pro Borg-Archiv | matcht Profil pro Stack | Warnung | +| CIFS/NFS-Mount-Liveness (für jeden konfigurierten Remote-Mount) | `stat` mit 10s-Timeout erfolgreich | Sofortalarm | +| Anzahl laufender Container | matcht Soll aus Repo | Warnung | + +**Implementierung:** Skript unter `services/posture-check/posture_check.sh`, ausgegeben als JSON, konsumiert von Notifier (Slack, ntfy, E-Mail — Operator-Entscheidung). + +## 12. Hard Rules — Constitution + +Diese Regeln sind nicht optional. Verstoß ist Incident, kein Feature-Request. + +1. **Kein NTFS, exFAT, FAT32 auf Cache, Pool, Array.** Nur XFS, BTRFS, ZFS. +2. **Keine Compose-Bind-Mounts auf `/mnt/cache/...` oder `/mnt/disk1/...`.** Immer `/mnt/user/...`. +3. **Keine Container schreibt nach `/mnt/disks/...` (Unassigned Devices) im Dauerbetrieb.** +4. **Keine `latest`-Image-Tags in Production-Compose.** Versionspin oder bewusster `:stable`/`:lts`-Alias. +5. **Keine Secrets im Repo.** Niemals. +6. **Kein Backup-Ziel über CIFS mit Hard-Mount ohne Liveness-Check.** +7. **Keine produktive Stack-Änderung ohne Compose-Commit im Repo.** +8. **Keine Disk- oder Pool-Änderung ohne Update dieses Dokuments im selben Commit.** +9. **Kein neuer Stack ohne Eintrag in `STORAGE_LAYOUT.md` (Share/Pfad), `SERVICE_CATALOG.md` (Funktion), `RESTORE_MATRIX.md` (Restore-Pfad).** +10. **Kein Pre-Backup-Hook, der DBs nicht konsistent dumped, für Stacks mit eigener DB.** +11. **Kein produktiver Stack ohne dokumentierten Restore-Pfad in `docs/RESTORE_MATRIX.md`.** Idealerweise mit dokumentiertem Restore-Test (≤ 90 Tage alt); bei fehlendem Test mindestens schriftliche Restore-Schritte und Backup-Quelle. Stacks ohne diesen Eintrag laufen nicht produktiv — entweder dokumentieren oder abschalten. +12. **Kein Backup-Lauf ohne vorgeschalteten Posture-Check (siehe §11).** Backup auf kompromittiertem Filesystem überschreibt unter Umständen den letzten guten Stand und kontaminiert die Backup-Historie. + +## 13. Soft Rules — Konventionen + +Erwartet, aber begründbare Abweichungen sind dokumentiert. + +- Stack-Verzeichnisse werden mit der Konvention aus Abschnitt 5 angelegt +- Ein Stack hat einen `README.md` in seinem `appdata/`-Verzeichnis +- Image-Tags werden quartalsweise auf Updates geprüft, dokumentiert in einem Update-Log +- Container ohne klare Eigentümerschaft werden quartalsweise reviewed (rauswerfen oder in Doku aufnehmen) +- Neue Shares werden grundsätzlich mit Cache-Setting `no` angelegt, Promotion auf `prefer`/`only` ist bewusste Entscheidung +- Compose-Dateien sind kommentiert, wenn die Konfiguration nicht selbsterklärend ist + +## 14. Anti-Patterns mit Begründung + +| Anti-Pattern | Warum Verboten | +|--------------|----------------| +| Direkter `/mnt/cache/X`-Bind-Mount in Compose | Pfad verschwindet, wenn Share-Setting auf Array migriert wird; bricht beim ersten Mover-Lauf | +| `chown -R 1000:1000 /mnt/user/appdata` blanket | Bricht alle Stacks, die andere UIDs erwarten; bei Mover-Lauf werden Permissions ggf. überschrieben | +| Backup direkt von Live-DB-Datendir | Inkonsistent unter Last; WAL-Divergenz nach Restore möglich | +| `latest`-Tags in Production | Reproduzierbarkeit weg; Rollback nur per Glück möglich | +| Mehrere Stacks im selben Appdata-Verzeichnis | Backup-Excludes greifen falsch; Restore betrifft Nachbar-Stack | +| Cache-Filesystem ohne automatisierten Posture-Check | Genau der Vorfall vom 2026-05-11 | +| Hard-mounted CIFS auf wackelige Geräte | D-State-Prozesse können den ganzen Host blockieren | +| Secret in Compose-Env (statt `env_file`) | Secret im Repo lesbar; bei `git log -p` für immer drin | +| Stack ohne dokumentierten Restore-Pfad | Bei Recovery muss improvisiert werden; das war der Auslöser dieses Dokuments | + +## 15. Migrations- und Wachstumspfade + +### 15.1 NTFS-zu-XFS-Migration (Cache, Phase 1) + +Behandelt in `docs/DISASTER_RECOVERY.md`, Sektion „Cache Recovery 2026-05". + +### 15.2 NTFS-zu-XFS-Migration (Disk1, Phase 2) + +Folgeprojekt nach mindestens 7 Tagen stabilem Cache-Betrieb. Eigener Plan in `docs/DISASTER_RECOVERY.md`. Grobschritte: + +1. Selektive Sicherung aller Disk1-Inhalte auf zweite Disk oder Hetzner +2. Hash-Verifikation der Sicherung +3. Disk1 entfernen aus Array (oder leer, wenn Daten temporär auf zweiter Disk) +4. Disk1 als XFS in Unraid-GUI neu formatieren +5. Daten zurückspielen +6. Parity neu bauen +7. Posture-Check grün + +### 15.3 Erweiterung um zweite Cache-NVMe + +Trigger: Cache-Auslastung > 70 %, oder Wunsch nach RAID1 für Appdata. + +Pfad: BTRFS-Pool aus zwei NVMe (RAID1) als Ersatz für Single-XFS-Cache. Migration über Pool-Wechsel mit zwischengeschalteter Image-Sicherung. Eigenes Migrations-Dokument vor Beginn. + +### 15.4 Erweiterung um zweite Array-Disk + +Trigger: Disk1 > 80 % voll, oder Performance-Bedarf. + +Pfad: zweite Daten-Disk dem Array hinzufügen, Allocation-Method überprüfen, Split-Level pro Share validieren. Parity-Disk muss ≥ größte Daten-Disk bleiben. + +### 15.5 Multi-Host (Hermes-Skalierung) + +Wenn Hermes-Worker auf weiteren Hosts skaliert: dieser Storage-Layout-Plan gilt zunächst nur für `Kallilabcore`. Weitere Hosts brauchen eigenes Storage-Layout-Dokument. Gemeinsamer Storage (NFS, iSCSI, S3-API) ist ein eigener Architektur-Schritt mit eigenem Dokument. + +## 16. Glossar + +| Begriff | Bedeutung | +|---------|-----------| +| **Cache (Pool)** | Schneller Pool (NVMe), nicht Teil des Arrays, ohne Parity-Schutz, für Performance-kritische Daten | +| **Array** | Klassische Unraid-Daten-Disks plus Parity, paritätsgeschützt, langsamer | +| **shfs** | Unraid-User-Share-FUSE-Filesystem; vereinigt Cache und Array unter `/mnt/user/` | +| **Mover** | Unraid-Service, der Daten zwischen Cache und Array gemäß Cache-Setting verschiebt | +| **Cache-Setting** | Pro Share: `only`, `prefer`, `yes`, `no` — steuert Mover-Verhalten | +| **Allocation Method** | High Water / Most Free / Fill Up — wie Daten auf Array-Disks verteilt werden | +| **Split Level** | Maximale Verzeichnistiefe, in der Unraid Inhalte über Disks aufteilen darf | +| **Pre-Backup-Hook** | Skript, das vor jedem Backup einen konsistenten DB-Dump erzeugt | +| **Posture-Check** | Automatisierter Audit, der Host-Realität gegen Soll-Zustand prüft | +| **Constitution** | Hard Rules in Abschnitt 12, nicht verhandelbar | + +## 17. Referenzen + +- `HOMELAB_ARCHITECTURE_MASTER_V2.md` — Gesamtarchitektur, Netze, Traefik, GitOps +- `docs/REPO_MAP.md` — Repo-Struktur, wo welcher Stack lebt +- `docs/SERVICE_CATALOG.md` — Pro Stack: Funktion, Abhängigkeiten, Eigenheiten +- `docs/RESTORE_MATRIX.md` — Pro Stack: Restore-Quelle und -Verfahren +- `docs/SECRETS_MAP.md` — Pro Secret: Speicherort, Rotation, Recovery +- `docs/DISASTER_RECOVERY.md` — Konkrete Recovery-Pläne, inkl. NTFS-Migration +- `docs/GITOPS_DRIFT_RUNBOOK.md` — Drift-Erkennung und -Behebung +- `docs/AI_CONTEXT.md` — Kontext für AI-Assistenten + +## 18. Changelog + +| Version | Datum | Änderung | Autor | +|---------|-------|----------|-------| +| 1.0 (Draft) | 2026-05-15 | Initialfassung nach Incident 2026-05-11. Ursprung: NTFS-Cache-Korruption, fehlende Posture-Checks, ungeplante Backup-Strategie. | Operator + AI-Assistenten | +| 1.1 (Draft) | 2026-05-15 | Operator-Review-Feedback eingearbeitet: `system`/`domains` auf `only`, `services` als recovery-kritisch markiert, `data/`-Behandlung pro Stack klassifiziert (statt blanket Exclude), Backup-Tooling Ist/Soll explizit getrennt, Posture-Check zusätzlich bei Boot/vor Backup/nach Mover, Hard Rules 11+12 ergänzt (Restore-Pfad-Pflicht, Posture-vor-Backup), Alarmziel-Optionen benannt, Review-Items in eigene Sektion §20 verschoben. | Operator + AI-Assistenten | +| 1.2 (Draft) | 2026-05-15 | Operator-Entscheidungen #3, #4, #6, #9, #11 eingearbeitet: Backrest abgeschaltet (Borg alleinige Backup-Technologie), persönliche Daten vollständig im Pflicht-Backup-Scope, ntfy als Alarmziel verbindlich, kebab-case-Migration im Rahmen der Recovery-Phase, Mirror-Backup für Gitea-Repo-Inhalte als verbindliche Spec (Implementierung in `SERVICES_RECOVERY.md` zu detaillieren). Offen: Items #1, #2, #5, #7, #8, #10. | Operator + AI-Assistenten | +| 1.3 (Draft) | 2026-05-15 | Operator-Entscheidungen #1, #7, #8 eingearbeitet: Disk-Größen/-Modelle als Deferred via Posture-Check-Detection (kein Blocker), optionale Stacks (Filebrowser, code-server, Speedtest, Scrutiny, Uptime-Kuma) bleiben im Layout und sind produktiv, Network-Verweis auf MASTER_V2 bestätigt. Damit alle akuten Items entschieden. Verbleibend: Items #2, #5, #10 (Retention, Schwellen-Kalibrierung, RESTORE_MATRIX-Klassifikation) — alle als Folge-Aufgaben über Inkraftsetzung hinaus, kein Commit-Blocker. | Operator + AI-Assistenten | + +## 19. Inkraftsetzung + +Dieses Dokument tritt in Kraft mit dem Commit der finalen Fassung in `master`-Branch des Repos `homelab-infra`. Ab Inkraftsetzung gelten alle Hard Rules; Soft Rules werden im Rahmen der laufenden Recovery-Arbeit etabliert; Posture-Check muss innerhalb von 14 Tagen nach Inkraftsetzung produktiv laufen. + +Erste Audit-Review dieses Dokuments: spätestens 90 Tage nach Inkraftsetzung. Danach jährlich oder bei jeder strukturellen Änderung des Storage-Layouts. + +## 20. Open Review Items (vor finalem Commit zu entscheiden) + +Diese Sektion dokumentiert offene Operator-Entscheidungen und Lücken. **Vor Statuswechsel von Draft auf Active ist jeder Punkt entweder eingearbeitet oder bewusst als „bleibt offen" mit Verweis auf Folge-Issue/-Doc markiert.** + +| Nr. | Item | Status | Verantwortung | +|-----|------|--------|---------------| +| 1 | Disk-Größen und Modelle in §3 (Disk1, Parity, externe Backup-Platte) | **DEFERRED 2026-05-15** — wird automatisch via Posture-Check-Detection-Lauf ergänzt; kein Blocker für Inkraftsetzung | Operator + Posture-Check | +| 2 | Retention-Werte in §8.1 (Borg-Repos lokal/remote) — abhängig von tatsächlicher Storage-Kapazität | Vorschlag steht, anzupassen | Operator | +| 3 | Backup-Tooling: Backrest abschalten, Borg alleinige Backup-Technologie | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.0) | +| 4 | Backup-Scope für persönliche Daten: `documents`, `photos`, `finance`, `projekte` (und `media` falls behalten) **vollständig** im Pflicht-Scope | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.2) | +| 5 | Posture-Check-Schwellen in §11 — Vorschlag konservativ, nach erstem Monitoring kalibrieren | Vorschlag steht | Operator nach 30 Tagen | +| 6 | Alarmziel: ntfy als primärer Push-Kanal, Mail-Fallback als Folge-Erweiterung optional | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §11) | +| 7 | Optionale Stacks (Filebrowser, code-server, Speedtest, Scrutiny, Uptime-Kuma) — bleiben im Layout, sind produktiv im Scope, folgen den Standard-Konventionen aus §5 und Backup-Pflichten aus §8.2 | **ENTSCHIEDEN 2026-05-15** | erledigt | +| 8 | Verweis auf `HOMELAB_ARCHITECTURE_MASTER_V2.md` für Network-Architektur (§10) — Net-Architektur steht dort authoritativ, Verweis ist ausreichend | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §10) | +| 9 | Naming-Konvention: kebab-case durchziehen, Migration im Rahmen der Recovery-Phase | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §6); pro Stack in RESTORE_MATRIX.md zu dokumentieren | +| 10 | Pro-Stack-Klassifizierung in `RESTORE_MATRIX.md` (DB-Typ, Nutzdaten in `data/`, Dump-Verfahren, letzter Restore-Test, kebab-case-Migrationsname) — als Folge-Aufgabe aus Hard Rule §12.11 | Folge-Aufgabe | Operator + Recovery-Phase | +| 11 | Mirror-Backup für `services/gitea/git/repositories/` auf zweites Medium, ≤ 6 h Frequenz, konkrete Implementierung in `docs/SERVICES_RECOVERY.md` zu erstellen | **ENTSCHIEDEN 2026-05-15**, Implementierungs-Doc offen | Operator + Folge-Doc | + +Wenn alle 11 Punkte bearbeitet sind und der Operator die Datei reviewed hat, wird sie als `docs/STORAGE_LAYOUT.md` (ohne `.draft`) committed und Status auf `Active` gesetzt. diff --git a/docs/postinstall-erstes-ziel-codex.md b/docs/postinstall-erstes-ziel-codex.md new file mode 100644 index 0000000..3fb2cf7 --- /dev/null +++ b/docs/postinstall-erstes-ziel-codex.md @@ -0,0 +1,103 @@ +# Post-Install: Als Erstes Codex wieder startklar machen + +Ziel: Nach der frischen Windows-Installation zuerst wieder mit Codex/ChatGPT weiterarbeiten koennen, bevor der restliche Wiederaufbau startet. + +## 1. Internet herstellen + +1. Windows starten. +2. LAN/WLAN verbinden. +3. Falls kein Internet vorhanden ist: + - externe Backup-HDD anschliessen + - Treiber aus `H:\Windows-Neuaufsetzen-Backup\13_Treiber_Windows` installieren + - besonders LAN/WLAN/Chipsatz pruefen + +## 2. Browser starten + +1. Microsoft Edge oeffnen. +2. Bei Microsoft/ChatGPT/Codex anmelden, je nachdem welche Variante genutzt wird. +3. Diese Datei auf H: oeffnen: + +```text +H:\Windows-Neuaufsetzen-Backup\POSTINSTALL_ERSTES_ZIEL_CODEX.md +``` + +## 3. Basiswerkzeuge installieren + +PowerShell als normaler Benutzer oeffnen. + +```powershell +winget install --exact --id Git.Git --source winget --accept-package-agreements --accept-source-agreements +winget install --exact --id Microsoft.VisualStudioCode --source winget --accept-package-agreements --accept-source-agreements +winget install --exact --id OpenJS.NodeJS.LTS --source winget --accept-package-agreements --accept-source-agreements +``` + +Optional danach UniGetUI: + +```powershell +winget install --exact --id Devolutions.UniGetUI --source winget --accept-package-agreements --accept-source-agreements +``` + +## 4. SSH und Git-Konfiguration zurueckholen + +Backup-HDD muss angeschlossen sein. + +```powershell +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh" | Out-Null +Copy-Item -Path "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\ssh\*" -Destination "$env:USERPROFILE\.ssh" -Force +Copy-Item -Path "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\git\.gitconfig" -Destination "$env:USERPROFILE\.gitconfig" -Force +``` + +SSH-Key-Rechte pruefen: + +```powershell +ssh -T git@github.com +``` + +Falls Gitea genutzt wird, stattdessen oder zusaetzlich den Gitea-Host testen. + +## 5. Homelab-Repo wieder verfuegbar machen + +Wenn `G:\Gitea_Clone\homelab-infra` noch existiert: + +```powershell +cd /d G:\Gitea_Clone\homelab-infra +git status +``` + +Falls das Repo neu geklont werden muss: + +```powershell +New-Item -ItemType Directory -Force -Path G:\Gitea_Clone | Out-Null +cd /d G:\Gitea_Clone +git clone homelab-infra +cd homelab-infra +git status +``` + +## 6. Codex-Kontext wieder aufnehmen + +Wichtige Dateien: + +```text +H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\installierte_programme_lesbar.md +H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\kritische_programme_lizenz_check.md +H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\keys_exporte\banking4_license_private.txt +G:\Gitea_Clone\homelab-infra\docs\windows-neuaufsetzen-masterplan.md +``` + +Dann Codex/ChatGPT sagen: + +```text +Windows ist frisch installiert. Bitte hilf mir mit dem Post-Install-Wiederaufbau anhand von H:\Windows-Neuaufsetzen-Backup und dem Masterplan. +``` + +## 7. Danach erst Programme wiederherstellen + +Reihenfolge: + +1. Banking4 installieren und Lizenz aus `banking4_license_private.txt` nutzen. +2. Banking4-Datentresor aus `H:\Windows-Neuaufsetzen-Backup\07_Banking_Finanzen\Banking4_Datentresor_explizit` oeffnen. +3. Microsoft 365 ueber Microsoft-Konto installieren. +4. WISO Steuer installieren und Steuerdateien aus `WISO_Steuer_Dokumente` pruefen. +5. Restliche Programme mit UniGetUI/WinGet/Installern wieder aufbauen. + diff --git a/docs/windows-neuaufsetzen-masterplan.md b/docs/windows-neuaufsetzen-masterplan.md new file mode 100644 index 0000000..702faac --- /dev/null +++ b/docs/windows-neuaufsetzen-masterplan.md @@ -0,0 +1,550 @@ +# Windows neu aufsetzen: Masterplan ohne Datenverlust + +Stand: 2026-05-07 + +Ziel: Windows sauber neu installieren, die Datenträgerstruktur bereinigen und wichtige Daten sicher erhalten. + +Grundregel: Vor dem Löschen, Formatieren oder Neuinstallieren müssen mindestens zwei geprüfte Kopien der wichtigen Daten existieren. + +## Aktueller Arbeitsstand + +Stand: 2026-05-07, 15:00 Uhr + +Erledigt: + +- Backup-Ziel `H:\Windows-Neuaufsetzen-Backup` auf externer 8-TB-HDD erstellt. +- Inventarlisten exportiert nach `H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen`. +- Installierte Programme inventarisiert: 161 Eintraege. +- Aktuelle Benutzerordner von `C:\Users\michi` gesichert: Desktop, Documents, Pictures, Videos, Downloads, Music. +- `C:\Users\michi\.ssh` und `C:\Users\michi\.gitconfig` gesichert. +- Alte Standardordner aus `D:\Users\Baerchen` gesichert, soweit vorhanden. +- Persoenliche/auffaellige Ordner von `F:` gesichert: `BMW Leasing`, `Marina Handy 2025`, `Marina Handy Backup`. +- Relevante Ordner von `G:` gesichert: `Gitea_Clone`, `open-webui`, `Treiber`. +- WSL-Distributionen exportiert: `Ubuntu.tar`, `docker-desktop.tar`. +- Browserprofile gesichert: Chrome und Edge. +- Kritische Programmdaten zusaetzlich gesichert: + - Banking4/Subsembly: `C:\Users\michi\AppData\Local\Subsembly` + - WISO/Buhl: `C:\Users\michi\AppData\Local\Buhl`, `C:\Users\michi\AppData\Local\Buhl Data Service GmbH`, `C:\ProgramData\Buhl Data Service GmbH` + - WISO-Steuerdateien: `C:\Users\michi\Documents\steuer` + - Banking-Exporte vom Desktop: `C:\Users\michi\Desktop\Banking` +- Registry-Exports fuer Subsembly und Microsoft Office erstellt; Buhl-Registry-Suchlisten erstellt. +- Banking4-Lizenzdaten separat gesichert: `H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\keys_exporte\banking4_license_private.txt`. +- Aktueller Banking4-Datentresor separat gesichert: `H:\Windows-Neuaufsetzen-Backup\07_Banking_Finanzen\Banking4_Datentresor_explizit\Mein Datentresor.sub`. +- Office-Aktivierungsstatus exportiert: lokal als Microsoft 365/Office16 O365 Home Premium Grace/Notifications sichtbar, daher Microsoft-Konto/Abonnement manuell pruefen. +- Lesbare Programmlisten erstellt: + - `installierte_programme_lesbar.md` + - `kritische_programme_lizenz_check.md` +- UniGetUI/Keyfinder-Empfehlungen dokumentiert: `keyfinder_tools_recommendation.md`. +- Robocopy-Summenzeilen geprueft: keine Kopierfehler in den bekannten Backup-Jobs. +- Verifikationslisten erstellt: + - `backup_verification_known_data.csv` + - `backup_verification_browser_profiles.csv` + +Noch offen: + +- Manuelle Screenshots in `H:\Windows-Neuaufsetzen-Backup\14_Screenshots` ablegen. +- BitLocker-Status mit Adminrechten pruefen. +- Passwortmanager, 2FA-Recovery-Codes und Browser-Sync manuell pruefen. +- Banking4-Speicherort explizit pruefen. +- Banking4 im Programm selbst oeffnen und aktuellen Datentresor/Backup-Export bestaetigen. Der Key und der Datentresor sind bereits lokal auf H: gesichert. +- WISO Steuer 2026 oeffnen und Lizenz/Buhl-Konto sowie Speicherorte der Steuerdateien bestaetigen. +- Microsoft-Konto fuer M365 pruefen: Office-Webkonto/Abonnement, Installationsrecht, OneDrive-Sync. +- Optional Keyfinder-Lauf durchfuehren und Ergebnisse lokal auf H: speichern. +- `G:\Ollama` bewusst entscheiden: nicht gesichert, ca. 40,9 GB lokale Modell-/Cache-Daten. +- D:, F: und G: vor dem spaeteren Loeschen noch einmal in Ruhe final bestaetigen. + +## Zielentscheidung: Neues Windows auf Datentraeger 0 + +Entscheidung vom 2026-05-07: Das neue Windows soll auf `Datentraeger 0` installiert werden. + +Aktueller Zustand laut Datentraegerverwaltung: + +| Datentraeger | Aktuelles Laufwerk | Groesse | Inhalt/Zweck | +|---|---:|---:|---| +| Datentraeger 0 | D: | ca. 167 GB | Alte Windows SSD | +| Datentraeger 1 | E: | ca. 167 GB | Blizzard Games | +| Datentraeger 2 | C: und F: | ca. 931 GB | aktuelles Windows + 980SSD-Partition | +| Datentraeger 3 | G: | ca. 931 GB | M2 SSD / Daten | +| Datentraeger 4 | H: | ca. 7,45 TB | externe Backup-HDD | + +Bewertung: + +- Machbar, wenn `Datentraeger 0` als reines Windows-/Programme-Laufwerk genutzt wird. +- Nicht ideal fuer sehr viele Programme/Games, weil nur ca. 167 GB vorhanden sind. +- Vorteil: Die aktuelle Windows-SSD auf `Datentraeger 2` bleibt waehrend der Migration zunaechst erhalten. +- Wichtig: Bei der Installation duerfen ausschliesslich Partitionen auf `Datentraeger 0` geloescht werden. + +Empfohlenes Installationsverhalten: + +1. Externe Backup-HDD `H:` vor der Windows-Installation abziehen. +2. Wenn praktisch moeglich: andere interne Datentraeger fuer die Installation abziehen oder im UEFI deaktivieren. +3. Im Windows-Setup `Benutzerdefiniert` waehlen. +4. `Datentraeger 0` anhand der Groesse ca. 167 GB identifizieren. +5. Nur auf `Datentraeger 0` alle Partitionen loeschen: + - 499 MB Wiederherstellung + - 100 MB nicht zugeordnet bleibt egal + - D: Alte Windows SSD + - 640 MB Wiederherstellung +6. Den dadurch komplett nicht zugeordneten Speicher auf `Datentraeger 0` auswaehlen. +7. Windows installieren lassen. + +Nicht loeschen: + +- `Datentraeger 1` / E: Blizzard Games +- `Datentraeger 2` / C: und F: +- `Datentraeger 3` / G: +- `Datentraeger 4` / H: + +Nach der Installation: + +- Bootreihenfolge im UEFI auf die neue Windows-Installation auf `Datentraeger 0` setzen. +- Altes Windows auf `Datentraeger 2` erst loeschen, wenn das neue System mehrere Tage stabil laeuft. + +## UniGetUI fuer den Wiederaufbau + +UniGetUI ist fuer den Wiederaufbau sinnvoll, aber nicht fuer Lizenz-Keys. + +Nutzen: + +- Programme ueber WinGet/Scoop/Chocolatey/Pip/NPM suchen und installieren. +- Updates zentral verwalten. +- Paketlisten importieren/exportieren. + +Grenzen: + +- Banking4, WISO Steuer und Microsoft 365 wurden im `winget export` nicht als sauber wiederinstallierbare Pakete abgedeckt. +- Lizenzkeys werden durch UniGetUI nicht gesichert. + +Vorhanden: + +- WinGet-Export: `H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-export.json` + +Nach Neuinstallation: + +```powershell +winget install --exact --id Devolutions.UniGetUI --source winget +``` + +Danach kann die WinGet-Liste optional importiert werden: + +```powershell +winget import --import-file "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-export.json" --accept-package-agreements --accept-source-agreements +``` + +Empfehlung: Nicht alles blind importieren. Erst Basisprogramme installieren, dann die exportierte Liste als Orientierung nutzen. + +## Laufwerksannahmen + +Diese Zuordnung muss vor dem Start geprüft werden. + +| Laufwerk | Vermutung | Behandlung | +|---|---|---| +| C: | aktuelles Windows | sichern, danach neu aufsetzen | +| D: | alte Windows-SSD oder Altbestand | erst analysieren, nicht blind löschen | +| E: | Blizzard / Games | wahrscheinlich neu ladbar, Saves prüfen | +| F: | 980SSD, fast leer | liegt auf derselben physischen Samsung 980 PRO wie C: | +| G: | M.2 SSD, stark belegt | erst analysieren, wichtige Daten sichern | +| H: | externe 8-TB-HDD | Backup-Ziel | + +Wichtige Erkenntnis aus dem Inventar vom 2026-05-07: `C:` und `F:` sind Partitionen auf derselben Samsung SSD 980 PRO 1TB. Wenn diese SSD als Ziel fuer die Neuinstallation genutzt wird, muss besonders sauber entschieden werden, welche Partitionen geloescht werden. `F:` ist kein eigener physischer Datentraeger. + +## Phase 1: Backup-Struktur auf H: anlegen + +Zielordner: + +```text +H:\Windows-Neuaufsetzen-Backup\ +|-- 01_Desktop +|-- 02_Dokumente +|-- 03_Bilder +|-- 04_Videos +|-- 05_Downloads_wichtig +|-- 06_Projekte +|-- 07_Banking_Finanzen +|-- 08_Browser_Lesezeichen_Profile +|-- 09_Programme_Settings_Lizenzen +|-- 10_Games_Savegames +|-- 11_Homelab_NAS_Doku +|-- 12_Exportierte_Listen +|-- 13_Treiber_Windows +|-- 14_Screenshots +|-- 15_Musik +`-- 99_Unsortiert_von_D_F_G +``` + +PowerShell: + +```powershell +$BackupRoot = "H:\Windows-Neuaufsetzen-Backup" +$Folders = @( + "01_Desktop", + "02_Dokumente", + "03_Bilder", + "04_Videos", + "05_Downloads_wichtig", + "06_Projekte", + "07_Banking_Finanzen", + "08_Browser_Lesezeichen_Profile", + "09_Programme_Settings_Lizenzen", + "10_Games_Savegames", + "11_Homelab_NAS_Doku", + "12_Exportierte_Listen", + "13_Treiber_Windows", + "14_Screenshots", + "15_Musik", + "99_Unsortiert_von_D_F_G" +) + +New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null +$Folders | ForEach-Object { + New-Item -ItemType Directory -Force -Path (Join-Path $BackupRoot $_) | Out-Null +} +``` + +Stop-Punkt: H: ist sichtbar, beschreibbar und hat genug freien Speicher. + +## Phase 2: Inventar exportieren + +### Installierte Programme + +```powershell +$BackupRoot = "H:\Windows-Neuaufsetzen-Backup" + +Get-ItemProperty ` + HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, ` + HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | + Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | + Where-Object { $_.DisplayName } | + Sort-Object DisplayName | + Export-Csv "$BackupRoot\12_Exportierte_Listen\installierte_programme.csv" -NoTypeInformation -Encoding UTF8 +``` + +Optional zusätzlich: + +```powershell +winget list > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-list.txt" +``` + +### Laufwerksübersicht + +```powershell +Get-Volume | + Sort-Object DriveLetter | + Select-Object DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining | + Export-Csv "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\laufwerke.csv" -NoTypeInformation -Encoding UTF8 + +Get-Disk | + Select-Object Number, FriendlyName, SerialNumber, HealthStatus, Size, PartitionStyle | + Export-Csv "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\datentraeger.csv" -NoTypeInformation -Encoding UTF8 +``` + +### Windows-Aktivierung + +```powershell +wmic path softwarelicensingservice get OA3xOriginalProductKey > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\windows_oem_key.txt" +``` + +Zusätzlich Screenshot speichern: + +- Windows-Aktivierung +- Datenträgerverwaltung +- Apps & Features +- Gerätemanager +- Netzwerkadapter + +Stop-Punkt: Programmliste, Laufwerkslisten und wichtige Screenshots liegen auf H:. + +## Phase 3: Muss-Daten sichern + +Die folgenden Daten haben Priorität. + +### Benutzerordner + +Passe `` an. + +```powershell +$User = "C:\Users\" +$BackupRoot = "H:\Windows-Neuaufsetzen-Backup" + +robocopy "$User\Desktop" "$BackupRoot\01_Desktop" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_desktop.log" +robocopy "$User\Documents" "$BackupRoot\02_Dokumente" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_dokumente.log" +robocopy "$User\Pictures" "$BackupRoot\03_Bilder" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_bilder.log" +robocopy "$User\Videos" "$BackupRoot\04_Videos" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_videos.log" +robocopy "$User\Music" "$BackupRoot\15_Musik" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_music.log" +``` + +Downloads nicht blind komplett übernehmen. Erst wichtige Installer, PDFs, ZIPs, Rechnungen, Exporte und persönliche Dateien aussortieren. + +### Kritische versteckte Daten + +Prüfen und bei Bedarf sichern: + +| Pfad | Warum | +|---|---| +| `C:\Users\\.ssh` | SSH Keys | +| `C:\Users\\.gitconfig` | Git-Konfiguration | +| `C:\Users\\AppData\Roaming` | wichtige App-Einstellungen | +| `C:\Users\\AppData\Local` | Browserprofile, App-Daten | +| `C:\ProgramData` | gemeinsame App-Daten, Lizenzen | +| `C:\Users\\Documents\Outlook-Dateien` | PST/Outlook-Archive | + +Empfehlung: AppData nur sichern, später aber nicht komplett zurückkopieren. + +## Phase 4: Spezialdaten prüfen + +Diese Daten sind leicht zu übersehen. + +- Banking4-Daten, Exporte, Tresore +- Passwortmanager-Backups oder lokale Datenbanken +- 2FA-Recovery-Codes +- Browser-Lesezeichen-Export +- lokale Spielstände ohne Cloud-Sync +- Steuerunterlagen +- Verträge und Rechnungen +- GPG/PGP Keys +- Zertifikate +- VPN-Profile +- API-Keys und `.env` Dateien +- Homelab-, NAS- und Router-Dokumentation +- Docker Desktop Daten +- WSL-Distributionen +- virtuelle Maschinen + +### WSL exportieren, falls genutzt + +```powershell +wsl --list --verbose > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\wsl-distros.txt" +wsl --export "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\.tar" +``` + +## Phase 5: D:, F: und G: analysieren + +Nicht löschen. Erst suchen. + +PowerShell: + +```powershell +$BackupRoot = "H:\Windows-Neuaufsetzen-Backup" +$SearchRoots = @("D:\", "F:\", "G:\") +$Patterns = @( + "Users", + "Windows.old", + "Dokumente", + "Documents", + "Bilder", + "Pictures", + "Desktop", + "Downloads", + "Projekte", + "Projects", + "Backup", + "NAS", + "Git", + "Python", + "Banking", + "Steuern", + "Vertraege", + "Verträge" +) + +foreach ($Root in $SearchRoots) { + if (Test-Path $Root) { + Get-ChildItem -Path $Root -Directory -ErrorAction SilentlyContinue | + Select-Object FullName, LastWriteTime | + Export-Csv "$BackupRoot\12_Exportierte_Listen\top_level_$($Root[0]).csv" -NoTypeInformation -Encoding UTF8 + + foreach ($Pattern in $Patterns) { + Get-ChildItem -Path $Root -Recurse -Directory -ErrorAction SilentlyContinue -Filter "*$Pattern*" | + Select-Object FullName, LastWriteTime | + Export-Csv "$BackupRoot\12_Exportierte_Listen\fundstellen_$($Root[0])_$Pattern.csv" -NoTypeInformation -Encoding UTF8 + } + } +} +``` + +Alles Unklare zuerst nach `H:\Windows-Neuaufsetzen-Backup\99_Unsortiert_von_D_F_G\` sichern. + +Stop-Punkt: Für D:, F: und G: ist klar, was wichtig ist, was neu installiert werden kann und was später gelöscht werden darf. + +## Phase 6: Treiber und Installationsmedien vorbereiten + +Vorher herunterladen und auf H: speichern: + +- Windows 11 Media Creation Tool +- Mainboard-Chipsatztreiber +- LAN-Treiber +- WLAN-Treiber +- GPU-Treiber +- Audio-Treiber +- Bluetooth-Treiber, falls relevant +- Drucker-/Scanner-Treiber, falls relevant + +Minimum: LAN/WLAN-Treiber müssen offline verfügbar sein. + +Stop-Punkt: Windows-USB-Stick funktioniert und Netzwerk-Treiber liegen auf H:. + +## Phase 7: Backup prüfen + +Pflichtprüfung: + +- H: abziehen und wieder anschließen +- mehrere Bilder öffnen +- mehrere PDFs öffnen +- Office-Dateien öffnen +- Projektordner prüfen +- Banking-/Finanzdaten prüfen +- SSH-Key-Ordner prüfen +- Programmliste öffnen +- Robocopy-Logs auf Fehler prüfen + +Optional Ordnergrößen vergleichen: + +```powershell +Get-ChildItem "C:\Users\\Documents" -Recurse -Force -ErrorAction SilentlyContinue | + Measure-Object -Property Length -Sum + +Get-ChildItem "H:\Windows-Neuaufsetzen-Backup\02_Dokumente" -Recurse -Force -ErrorAction SilentlyContinue | + Measure-Object -Property Length -Sum +``` + +Go/No-Go: + +- Go: wichtige Daten sind auf H: lesbar und mindestens die kritischsten Daten existieren zusätzlich auf NAS, Cloud oder zweiter Platte. +- No-Go: unbekannte Daten auf D:, F: oder G:, fehlende Browser-/Passwort-/2FA-Sicherung, unklarer Banking4-Speicherort, kein funktionierender Netzwerk-Treiber. + +## Phase 8: Ziel-SSD für Windows festlegen + +Empfehlung: + +- Eine schnelle, zuverlässige SSD als neues C: +- Games, Daten und Projekte getrennt halten +- Alte Windows-SSD erst später löschen + +Vermutlicher Kandidat: + +- Samsung SSD 980 PRO 1TB, falls sie bewusst komplett als neue System-SSD neu partitioniert werden soll +- WDC WDS100T2B0C 1TB, falls die aktuelle M.2-Datenplatte nach vollstaendiger Sicherung als neues Systemlaufwerk dienen soll +- nicht einfach `F:` auswaehlen, ohne die physische SSD-Struktur zu beachten, da `F:` und `C:` auf derselben SSD liegen + +Vor der Installation ideal: + +- Nur Ziel-SSD angeschlossen lassen +- Backup-HDD H: abziehen +- andere interne Laufwerke abziehen, falls praktisch möglich + +Das verhindert, dass Windows Bootpartitionen auf dem falschen Datenträger ablegt. + +## Phase 9: Windows neu installieren + +Installation: + +1. Vom Windows-USB-Stick booten. +2. Benutzerdefinierte Installation wählen. +3. Ziel-SSD eindeutig identifizieren. +4. Nur auf der Ziel-SSD alte Partitionen löschen. +5. Nicht zugeordneten Speicher auf der Ziel-SSD auswählen. +6. Windows installieren. + +Nicht anfassen: + +- externe Backup-HDD +- Datenlaufwerke +- alte Windows-SSD, solange sie nicht final geprüft wurde + +## Phase 10: Ersteinrichtung + +Direkt nach der Installation: + +- Windows Update vollständig laufen lassen +- Chipsatztreiber installieren +- GPU-Treiber installieren +- LAN/WLAN prüfen +- Windows-Aktivierung prüfen +- Laufwerksbuchstaben sauber vergeben +- Windows Defender und Firewall prüfen +- BitLocker bewusst aktivieren oder deaktiviert lassen +- Wiederherstellungspunkt erstellen + +Basisprogramme: + +- Browser +- Passwortmanager +- 7-Zip +- Office oder LibreOffice +- Banking4 +- Git +- VS Code / Codex / Dev-Tools +- Docker Desktop / WSL, falls benötigt +- Trading-/Finanztools +- Drucker/Scanner +- Steam / Battle.net + +## Phase 11: Daten kontrolliert zurückholen + +Zuerst: + +- Dokumente +- Bilder +- Projekte +- Finanzen +- Desktop +- wichtige Downloads +- SSH Keys +- Browser-Lesezeichen + +Danach gezielt: + +- einzelne App-Konfigurationen +- Spielstände +- WSL-Distributionen +- Docker-Daten +- Outlook/PST + +Nicht tun: + +- `AppData` komplett zurückkopieren +- alte Windows-Ordner zurückmischen +- Programme aus alten Ordnern starten statt neu installieren + +## Phase 12: Alte Datenträger bereinigen + +Erst nach mehreren Tagen stabiler Nutzung: + +- D: alte Windows-SSD final prüfen +- alte Benutzerordner gezielt archivieren oder löschen +- alte Windows-/Recovery-/EFI-Partitionen nur löschen, wenn sicher nicht davon gebootet wird +- Games-Laufwerke neu strukturieren +- Datenlaufwerke sinnvoll benennen + +Zielstruktur: + +| Laufwerk | Zweck | +|---|---| +| C: | Windows + Programme | +| D: | Daten / Projekte | +| E: | Games | +| F: | Arbeits-SSD / schnelle Daten | +| H: | Backup extern | + +## Finale Checkliste vor dem Löschen + +- [ ] Backup-Struktur auf H: erstellt +- [ ] Programmliste exportiert +- [ ] Laufwerksliste exportiert +- [ ] Windows-Aktivierung dokumentiert +- [ ] Benutzerordner gesichert +- [ ] Browser-Lesezeichen exportiert oder Sync geprüft +- [ ] Passwortmanager geprüft +- [ ] 2FA-Recovery-Codes gesichert +- [ ] SSH/API/GPG/Zertifikate gesichert +- [ ] Banking4-Speicherort geprüft und gesichert +- [ ] Homelab-/NAS-Doku gesichert +- [ ] D:, F: und G: analysiert +- [ ] Unklare Daten nach `99_Unsortiert_von_D_F_G` kopiert +- [ ] LAN/WLAN-Treiber auf H: gespeichert +- [ ] Windows-USB-Stick erstellt +- [ ] Backup-Dateien stichprobenartig geöffnet +- [ ] Kritische Daten zusätzlich auf NAS, Cloud oder zweiter Platte gesichert +- [ ] Ziel-SSD eindeutig festgelegt + +Erst wenn alle Punkte erledigt sind, ist die Neuinstallation freigegeben. diff --git a/ops/windows-reinstall/backup-known-data.ps1 b/ops/windows-reinstall/backup-known-data.ps1 new file mode 100644 index 0000000..aa77eaa --- /dev/null +++ b/ops/windows-reinstall/backup-known-data.ps1 @@ -0,0 +1,105 @@ +param( + [string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup", + [string]$UserProfilePath = $env:USERPROFILE, + [switch]$IncludeOllama +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Invoke-RobocopyBackup { + param( + [string]$Source, + [string]$Destination, + [string]$LogName + ) + + if (-not (Test-Path $Source)) { + Write-Host "SKIP missing source: $Source" + return + } + + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + + $LogDir = Join-Path $BackupRoot "12_Exportierte_Listen" + New-Item -ItemType Directory -Force -Path $LogDir | Out-Null + $LogPath = Join-Path $LogDir $LogName + + Write-Host "COPY $Source" + Write-Host " -> $Destination" + + robocopy $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:$LogPath + $ExitCode = $LASTEXITCODE + + if ($ExitCode -gt 7) { + throw "Robocopy failed with exit code $ExitCode for source: $Source" + } +} + +if (-not (Test-Path $BackupRoot)) { + throw "BackupRoot does not exist: $BackupRoot" +} + +if (-not (Test-Path $UserProfilePath)) { + throw "UserProfilePath does not exist: $UserProfilePath" +} + +$CurrentUserJobs = @( + @{ Source = Join-Path $UserProfilePath "Desktop"; Destination = Join-Path $BackupRoot "01_Desktop"; Log = "backup_current_desktop.log" }, + @{ Source = Join-Path $UserProfilePath "Documents"; Destination = Join-Path $BackupRoot "02_Dokumente"; Log = "backup_current_documents.log" }, + @{ Source = Join-Path $UserProfilePath "Pictures"; Destination = Join-Path $BackupRoot "03_Bilder"; Log = "backup_current_pictures.log" }, + @{ Source = Join-Path $UserProfilePath "Videos"; Destination = Join-Path $BackupRoot "04_Videos"; Log = "backup_current_videos.log" }, + @{ Source = Join-Path $UserProfilePath "Downloads"; Destination = Join-Path $BackupRoot "05_Downloads_wichtig\_current_downloads_all"; Log = "backup_current_downloads.log" }, + @{ Source = Join-Path $UserProfilePath "Music"; Destination = Join-Path $BackupRoot "15_Musik"; Log = "backup_current_music.log" }, + @{ Source = Join-Path $UserProfilePath ".ssh"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\ssh"; Log = "backup_current_ssh.log" } +) + +foreach ($Job in $CurrentUserJobs) { + Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log +} + +$GitConfig = Join-Path $UserProfilePath ".gitconfig" +if (Test-Path $GitConfig) { + $GitDest = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\git" + New-Item -ItemType Directory -Force -Path $GitDest | Out-Null + Copy-Item -LiteralPath $GitConfig -Destination (Join-Path $GitDest ".gitconfig") -Force +} + +$OldUserRoot = "D:\Users\Baerchen" +$OldUserJobs = @( + @{ Source = Join-Path $OldUserRoot "Desktop"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Desktop"; Log = "backup_old_baerchen_desktop.log" }, + @{ Source = Join-Path $OldUserRoot "Documents"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Documents"; Log = "backup_old_baerchen_documents.log" }, + @{ Source = Join-Path $OldUserRoot "Pictures"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Pictures"; Log = "backup_old_baerchen_pictures.log" }, + @{ Source = Join-Path $OldUserRoot "Videos"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Videos"; Log = "backup_old_baerchen_videos.log" }, + @{ Source = Join-Path $OldUserRoot "Downloads"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Downloads"; Log = "backup_old_baerchen_downloads.log" }, + @{ Source = Join-Path $OldUserRoot "Music"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Music"; Log = "backup_old_baerchen_music.log" } +) + +foreach ($Job in $OldUserJobs) { + Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log +} + +$ExtraJobs = @( + @{ Source = "F:\BMW Leasing"; Destination = Join-Path $BackupRoot "07_Banking_Finanzen\BMW Leasing"; Log = "backup_f_bmw_leasing.log" }, + @{ Source = "F:\Marina Handy 2025"; Destination = Join-Path $BackupRoot "03_Bilder\Marina Handy 2025"; Log = "backup_f_marina_handy_2025.log" }, + @{ Source = "F:\Marina Handy Backup"; Destination = Join-Path $BackupRoot "03_Bilder\Marina Handy Backup"; Log = "backup_f_marina_handy_backup.log" }, + @{ Source = "G:\Gitea_Clone"; Destination = Join-Path $BackupRoot "06_Projekte\Gitea_Clone"; Log = "backup_g_gitea_clone.log" }, + @{ Source = "G:\open-webui"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\open-webui"; Log = "backup_g_open_webui.log" }, + @{ Source = "G:\Treiber"; Destination = Join-Path $BackupRoot "13_Treiber_Windows\G_Treiber"; Log = "backup_g_treiber.log" } +) + +if ($IncludeOllama) { + $ExtraJobs += @{ Source = "G:\Ollama"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\Ollama"; Log = "backup_g_ollama.log" } +} + +foreach ($Job in $ExtraJobs) { + Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log +} + +Write-Host "" +Write-Host "Known-data backup finished." +Write-Host "Backup root: $BackupRoot" +Write-Host "Logs: $BackupRoot\12_Exportierte_Listen" +if (-not $IncludeOllama) { + Write-Host "Note: G:\Ollama was not copied. Re-run with -IncludeOllama if you want to keep local models too." +} diff --git a/ops/windows-reinstall/clear-disk0-old-windows-ssd.ps1 b/ops/windows-reinstall/clear-disk0-old-windows-ssd.ps1 new file mode 100644 index 0000000..3d42117 --- /dev/null +++ b/ops/windows-reinstall/clear-disk0-old-windows-ssd.ps1 @@ -0,0 +1,69 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$ExpectedDiskNumber = 0 +$ExpectedFriendlyName = "INTEL SSDSC2BW180A3L" +$ExpectedSerialNumber = "CVCV3105053K180EGN" +$MinSizeGB = 160 +$MaxSizeGB = 180 + +function Assert-Admin { + $Identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $Principal = [Security.Principal.WindowsPrincipal]::new($Identity) + if (-not $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "Dieses Skript muss in PowerShell als Administrator ausgefuehrt werden." + } +} + +Assert-Admin + +$Disk = Get-Disk -Number $ExpectedDiskNumber +$SizeGB = [math]::Round($Disk.Size / 1GB, 2) + +Write-Host "Zieldatentraeger:" +$Disk | Select-Object Number, FriendlyName, SerialNumber, HealthStatus, OperationalStatus, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}}, PartitionStyle | Format-Table -AutoSize + +Write-Host "Aktuelle Partitionen auf Datentraeger ${ExpectedDiskNumber}:" +Get-Partition -DiskNumber $ExpectedDiskNumber | Sort-Object PartitionNumber | Select-Object DiskNumber, PartitionNumber, DriveLetter, Type, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,3)}} | Format-Table -AutoSize + +if ($Disk.FriendlyName -ne $ExpectedFriendlyName) { + throw "Abbruch: FriendlyName passt nicht. Erwartet '$ExpectedFriendlyName', gefunden '$($Disk.FriendlyName)'." +} + +if ($Disk.SerialNumber -ne $ExpectedSerialNumber) { + throw "Abbruch: SerialNumber passt nicht. Erwartet '$ExpectedSerialNumber', gefunden '$($Disk.SerialNumber)'." +} + +if ($SizeGB -lt $MinSizeGB -or $SizeGB -gt $MaxSizeGB) { + throw "Abbruch: Groesse passt nicht. Erwartet ca. 167 GB, gefunden $SizeGB GB." +} + +$SystemPartitions = Get-Partition -DiskNumber $ExpectedDiskNumber | Where-Object { + $_.IsBoot -or $_.IsSystem -or $_.IsActive +} + +if ($SystemPartitions) { + throw "Abbruch: Datentraeger $ExpectedDiskNumber enthaelt Boot/System/Aktiv-Partitionen." +} + +Write-Host "" +Write-Host "WARNUNG: Datentraeger 0 wird jetzt komplett geleert." +Write-Host "Das betrifft die alte Windows-SSD mit aktuell D: und Recovery-Partitionen." +Write-Host "Andere Datentraeger werden nicht angefasst." +Write-Host "" + +$Confirmation = Read-Host "Tippe exakt LEEREN um fortzufahren" +if ($Confirmation -ne "LEEREN") { + throw "Abbruch: Bestaetigung wurde nicht eingegeben." +} + +Clear-Disk -Number $ExpectedDiskNumber -RemoveData -RemoveOEM -Confirm:$false + +Write-Host "" +Write-Host "Datentraeger $ExpectedDiskNumber wurde geleert." +Write-Host "Ergebnis:" +Get-Disk -Number $ExpectedDiskNumber | Select-Object Number, FriendlyName, HealthStatus, OperationalStatus, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}}, PartitionStyle | Format-Table -AutoSize +Get-Partition -DiskNumber $ExpectedDiskNumber -ErrorAction SilentlyContinue | Format-Table -AutoSize + +Write-Host "" +Write-Host "Naechster Schritt: Vom Windows-USB-Stick booten und Windows auf den nicht zugeordneten Speicher von Datentraeger 0 installieren." diff --git a/ops/windows-reinstall/postinstall-unigetui.ps1 b/ops/windows-reinstall/postinstall-unigetui.ps1 new file mode 100644 index 0000000..dbd5ead --- /dev/null +++ b/ops/windows-reinstall/postinstall-unigetui.ps1 @@ -0,0 +1,34 @@ +param( + [string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup", + [switch]$ImportWingetList +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + throw "winget is not available. Install or update App Installer from Microsoft Store first." +} + +Write-Host "Installing UniGetUI..." +winget install --exact --id Devolutions.UniGetUI --source winget --accept-package-agreements --accept-source-agreements + +$WingetExport = Join-Path $BackupRoot "12_Exportierte_Listen\winget-export.json" + +if ($ImportWingetList) { + if (-not (Test-Path $WingetExport)) { + throw "WinGet export file not found: $WingetExport" + } + + Write-Host "Importing WinGet package list..." + Write-Host "Source: $WingetExport" + winget import --import-file $WingetExport --accept-package-agreements --accept-source-agreements +} else { + Write-Host "" + Write-Host "UniGetUI installed." + Write-Host "WinGet export is available here:" + Write-Host " $WingetExport" + Write-Host "" + Write-Host "Review the list before importing. To import later, run:" + Write-Host " .\postinstall-unigetui.ps1 -ImportWingetList" +} diff --git a/ops/windows-reinstall/prepare-backup-inventory.ps1 b/ops/windows-reinstall/prepare-backup-inventory.ps1 new file mode 100644 index 0000000..1ef273b --- /dev/null +++ b/ops/windows-reinstall/prepare-backup-inventory.ps1 @@ -0,0 +1,139 @@ +param( + [string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup", + [string]$UserProfilePath = $env:USERPROFILE +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function New-BackupFolder { + param([string]$Name) + + $Path = Join-Path $BackupRoot $Name + New-Item -ItemType Directory -Force -Path $Path | Out-Null + return $Path +} + +function Export-CsvSafe { + param( + [Parameter(ValueFromPipeline = $true)]$InputObject, + [string]$Path + ) + + begin { + $Items = @() + } + + process { + $Items += $InputObject + } + + end { + $Items | Export-Csv -Path $Path -NoTypeInformation -Encoding UTF8 + } +} + +if (-not (Test-Path $UserProfilePath)) { + throw "UserProfilePath does not exist: $UserProfilePath" +} + +New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null + +$Folders = @( + "01_Desktop", + "02_Dokumente", + "03_Bilder", + "04_Videos", + "05_Downloads_wichtig", + "06_Projekte", + "07_Banking_Finanzen", + "08_Browser_Lesezeichen_Profile", + "09_Programme_Settings_Lizenzen", + "10_Games_Savegames", + "11_Homelab_NAS_Doku", + "12_Exportierte_Listen", + "13_Treiber_Windows", + "14_Screenshots", + "15_Musik", + "99_Unsortiert_von_D_F_G" +) + +$Folders | ForEach-Object { New-BackupFolder $_ | Out-Null } +$ExportDir = Join-Path $BackupRoot "12_Exportierte_Listen" + +Get-ItemProperty ` + HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, ` + HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | + Where-Object { + $_.PSObject.Properties.Name -contains "DisplayName" -and + -not [string]::IsNullOrWhiteSpace($_.DisplayName) + } | + Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation | + Sort-Object DisplayName | + Export-CsvSafe -Path (Join-Path $ExportDir "installierte_programme.csv") + +if (Get-Command winget -ErrorAction SilentlyContinue) { + winget list | Out-File -FilePath (Join-Path $ExportDir "winget-list.txt") -Encoding UTF8 +} + +Get-Volume | + Sort-Object DriveLetter | + Select-Object DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining, HealthStatus | + Export-CsvSafe -Path (Join-Path $ExportDir "laufwerke.csv") + +Get-Disk | + Select-Object Number, FriendlyName, SerialNumber, HealthStatus, OperationalStatus, Size, PartitionStyle | + Export-CsvSafe -Path (Join-Path $ExportDir "datentraeger.csv") + +Get-Partition | + Select-Object DiskNumber, PartitionNumber, DriveLetter, Type, Size, GptType | + Sort-Object DiskNumber, PartitionNumber | + Export-CsvSafe -Path (Join-Path $ExportDir "partitionen.csv") + +try { + wmic path softwarelicensingservice get OA3xOriginalProductKey | + Out-File -FilePath (Join-Path $ExportDir "windows_oem_key.txt") -Encoding UTF8 +} catch { + "Could not read OEM key: $($_.Exception.Message)" | + Out-File -FilePath (Join-Path $ExportDir "windows_oem_key.txt") -Encoding UTF8 +} + +if (Get-Command manage-bde -ErrorAction SilentlyContinue) { + manage-bde -status | Out-File -FilePath (Join-Path $ExportDir "bitlocker_status.txt") -Encoding UTF8 +} + +if (Get-Command wsl -ErrorAction SilentlyContinue) { + wsl --list --verbose | Out-File -FilePath (Join-Path $ExportDir "wsl-distros.txt") -Encoding UTF8 +} + +$KnownUserPaths = @( + "Desktop", + "Documents", + "Pictures", + "Videos", + "Downloads", + "Music", + ".ssh", + ".gitconfig" +) + +$KnownUserPaths | + ForEach-Object { + $Path = Join-Path $UserProfilePath $_ + [pscustomobject]@{ + Name = $_ + Path = $Path + Exists = Test-Path $Path + LastWriteTime = if (Test-Path $Path) { (Get-Item $Path -Force).LastWriteTime } else { $null } + } + } | + Export-CsvSafe -Path (Join-Path $ExportDir "benutzerpfade.csv") + +Write-Host "" +Write-Host "Backup preparation inventory created:" +Write-Host " $BackupRoot" +Write-Host "" +Write-Host "Next manual steps:" +Write-Host " 1. Save screenshots into: $BackupRoot\14_Screenshots" +Write-Host " 2. Review CSV files in: $ExportDir" +Write-Host " 3. Start data backup only after confirming H: is the correct external drive."