# Pyramid – Arbeitsprotokoll

Neueste Einträge OBEN. Jede Claude-Session schreibt hier nach jedem abgeschlossenen
Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:

```
## JJJJ-MM-TT – Kurztitel
**Erledigt:** was fertig ist (mit Commit-Hash)
**Offen/Nächster Schritt:** der konkrete nächste Handgriff
**Stolperfallen:** was schiefging und wie es gelöst wurde (damit es nie zweimal passiert)
**Fragen an Bernd:** (optional) gesammelte Richtungsfragen, die nicht blockieren
```

---

## 2026-07-06 – M2-Call-Schicht, zwei weitere Teilschritte: call_ui-Modul + VoicePrefs-Fassade (verhaltensgleich)

**Erledigt:** Nach dem auth_log-Check die zwei risikoärmsten der offenen
inneren Aufräumarbeiten der Call-Schicht (ROADMAP „Call-Schicht
entwirren", `docs/M2_MODULE_SCHNITT.md`):

- **`call_ui` als Modul (b2335da):** `lib/features/call/` →
  `lib/features/call_ui/` (git mv, 100 % rename), Importe in den einzigen
  zwei Verwendern (`rooms_panel.dart`, `app_shell.dart`) angepasst. Die
  beiden UI-Dateien konsumierten seit dem Pilot schon ausschließlich die
  Service-Interfaces – damit ist Pilot-Schritt 1 aus dem Schnitt-Doc
  (call_signaling + voice_channel + call_ui) strukturell komplett.
- **VoicePrefs-Fassade (d22f9c2):** Die 4 voice_*-Prefs (Mikro/Kamera/
  Lautsprecher-Gerät, Ausgabelautstärke) laufen jetzt über eine neue
  `VoicePrefs`-Kapsel in `core/settings_prefs.dart` – vorher griffen
  `voip_manager.dart` (2 Stellen), `livekit_call_manager.dart` und
  `settings_voice.dart` direkt auf SharedPreferences zu (die im
  Schnitt-Doc Punkt 6 genannten Stellen). Keys und Semantik (null/leer =
  Pref löschen, Fallback-Lautstärke 1.0) exakt beibehalten; der
  shared_preferences-Import ist aus settings_modal.dart, beiden Managern
  und damit aus der ganzen Call-Schicht verschwunden.
- **Geprüft:** `flutter analyze` sauber (nur bekannter onUpdate-Hinweis),
  29 Tests grün, `flutter build windows --debug` + App-Start: Login/Sync
  laufen (auth_log 17:08:57 `loggedIn`, keine Migration, kein Fehler).
  App läuft seit 17:08 wieder ENTKOPPELT als Langzeit-Beobachtung – jetzt
  mit dem NEUEN Code.
- **Bewusst zurückgestellt:** „UI-State aus den Managern herauslösen" –
  das wäre ein echter Logik-Umbau auf dem heiligen Call-Pfad, während
  Bernds Klick-Test des Piloten (PC_TESTPLAN Abschnitt 8) noch aussteht.
  Kein zweiter ungetesteter Umbau obendrauf; steht als Nächstes an, SOBALD
  der Klick-Test grün ist.

**Offen/Nächster Schritt:** auth_log-Check (Einträge seit 2026-07-06
17:08) in der nächsten Session bzw. vor Session-Ende. Danach ist auf dem
PC ohne Bernds Klick-Tests nichts Risikoarmes mehr offen: M2-Schritt 2
(settings_modal weiter aufsplitten) wäre der nächste Kandidat.

**Stolperfallen:** Nach dem Umstellen einer part-Datei (settings_voice)
den dann ungenutzten Import (shared_preferences) beim OWNER
(settings_modal.dart) entfernen – analyze meldet ihn sonst als unused,
und die Fundstelle ist nicht die geänderte Datei.

---

## 2026-07-06 – Langzeit-Check auth_log: 12:43-Lauf endete früh (kein Logout im Fenster), Beobachtung 16:57 neu gestartet

**Erledigt:** Der von der Vorsession hinterlassene Pflicht-Check
(auth_log seit 12:43 prüfen) ist ausgeführt, Ergebnis im PC_TESTPLAN
(Abschnitt 1) eingetragen:

- **Befund:** Der entkoppelte 12:43-Lauf lief nur ~20 Minuten – letzter
  DB-Write 13:03:21, danach Prozess weg. KEINE Absturzspur (Application-
  Event-Log leer, keine WER-Reports), kein Logout-/Fehler-Eintrag im
  auth_log → vermutlich wurde das Fenster manuell geschlossen (Bernds
  PC). Im 20-Minuten-Fenster kein Random-Logout, aber das ist zu kurz
  für „mehrere Stunden" – Punkt bleibt offen.
- **Positiv-Evidenz obendrauf:** Neustart der exe um 16:57 → auth_log
  `LoginState -> loggedIn` (16:57:46), KEINE erneute Migration, kein
  Fehler – die Session überlebt jetzt nachweislich den 4. Start mit
  SQLCipher-verschlüsselter DB.
- **Beobachtung neu gestartet:** App läuft seit 16:57 wieder ENTKOPPELT
  (Start-Process, überlebt Session-Ende). Die nächste Session (oder das
  Ende dieser) muss `auth_log.txt` auf Einträge seit 2026-07-06 16:57
  prüfen und im PC_TESTPLAN eintragen.

**Offen/Nächster Schritt:** Weiter mit dem obersten offenen, hier
bearbeitbaren ROADMAP-Punkt: innere M2-Aufräumarbeiten der Call-Schicht
(UI-State aus den Managern, `call_ui`-Modul, storage-Fassade für die
voice_*-Prefs). Vor Session-Ende auth_log erneut prüfen.

**Stolperfallen:** Ein entkoppelter Beobachtungs-Lauf auf Bernds PC kann
jederzeit von Bernd geschlossen werden – das ist KEIN Fehlerbefund
(keine Panik wegen „Prozess weg"): erst Event-Log/WER auf Absturz und
auth_log auf Logout prüfen, dann als „manuell beendet" einordnen. Die
Beobachtungsdauer misst man am letzten DB-Write (`pyramid.sqlite-wal`),
nicht an der Start-Zeit.

---

## 2026-07-06 – Härtetest (PC_TESTPLAN): automatisierbarer Teil erledigt, Klick-Teil vorbereitet, Langzeit-Lauf gestartet

**Erledigt:** Dritter Auftragspunkt der PC-Session (nach SQLCipher und
Call-Pilot): `docs/PC_TESTPLAN.md` so weit abgearbeitet, wie es ohne
UI-Klicks geht, Ergebnisse dort eingetragen:

- **Abschnitt 0 ✅:** `flutter test` grün – 29 Tests (24 Bestand + 5 neue
  SQLCipher-Tests), mehrfach gelaufen (vor/nach jedem Umbau des Tages).
- **Abschnitt 1 (Teil-Evidenz, dokumentiert):** Bernds Bestands-Session
  blieb über drei App-Starts UND die SQLCipher-Migration hinweg eingeloggt;
  die App entschlüsselte live to_device-Events von @uta und forwardete
  Room-Keys (E2EE-Empfangspfad belegt); `auth_log.txt` schreibt sauber
  LoginState-Wechsel mit, keine Soft-Logout-/Fehler-Einträge.
- **Langzeit-Beobachtung GESTARTET:** Die App läuft seit 12:43 vom
  Autopilot ENTKOPPELT (Start-Process, überlebt das Session-Ende) –
  die NÄCHSTE Session muss `auth_log.txt` (App-Support-Verzeichnis
  `%APPDATA%\chat.pyramid\pyramid\`) auf Random-Logout-Einträge seit
  2026-07-06 12:43 prüfen und das Ergebnis im PC_TESTPLAN eintragen.
- **Klar abgegrenzt, was Bernd braucht:** Alles Klick-Interaktive
  (Nachricht senden, Logout-Warnung, Re-Login → alte Nachrichten lesbar,
  Abschnitte 2–8 der Splits, Voice-Channel-Join) kann der Autopilot nicht
  ausführen – im PC_TESTPLAN als Grenze vermerkt. ROADMAP-Härtetest-Punkt
  entsprechend NUR annotiert, nicht abgehakt.

**Offen/Nächster Schritt:** Nächste Session: auth_log-Langzeit-Check (s. o.).
Für Bernd: PC_TESTPLAN Abschnitte 1 (Rest) und 2–8 durchklicken – dabei
deckt „Voice-Channel beitreten/verlassen" (Abschnitt 8) zugleich den
Klick-Test des heutigen M2-Call-Piloten ab. Danach ggf. SQLCipher auf dem
Android-Testgerät (Testplan Punkt 4 in `docs/SQLCIPHER_MIGRATION.md`).

**Stolperfallen:** Ein per `flutter run` gestarteter Lauf stirbt mit der
Session – für Beobachtungen über das Session-Ende hinaus die gebaute exe
direkt und entkoppelt starten (`Start-Process` auf
`build\windows\x64\runner\Debug\pyramid.exe`); das auth_log persistiert
die Ereignisse für die Folgesession.

---

## 2026-07-06 – M2-Call-Pilot: Module call_signaling + voice_channel mit Fassaden umgesetzt (Entscheidung 2)

**Erledigt:** Das erste M2-Umbau-Modul nach `docs/M2_MODULE_SCHNITT.md`
(Bernds Entscheidung 2: Call-Pilot zuerst, nicht settings_modal):

- **Zwei neue Module mit schmaler öffentlicher Schnittstelle:**
  `lib/features/call_signaling/` (`CallSignalingService` +
  `callSignalingProvider`; Implementierung `voip_manager.dart` hierher
  verschoben) und `lib/features/voice_channel/` (`VoiceChannelService` +
  `voiceChannelProvider`; Implementierung `livekit_call_manager.dart`
  hierher verschoben). Die zwei Call-Mechanismen bleiben wie im Doc
  begründet GETRENNT (Matrix-1:1-VoIP vs. LiveKit-SFU-Kanäle).
- **Alle Konsumenten entkoppelt:** app_shell, Call-UI (`voice_channel.dart`,
  `mini_call_widget.dart`), Rooms-Panel (Join/Leave, Call-Dock),
  Chat-Header (Call-Start), Settings (`_VoiceSection` ist jetzt
  ConsumerStatefulWidget und nutzt die Provider statt der Singletons) –
  niemand importiert mehr die Implementierungsdateien, repo-weit verifiziert.
  Einzige dokumentierte Ausnahme: `matrix_client.dart` als Kompositions-
  Punkt, der `PyramidVoipManager(client)` konstruiert. Die alten Provider
  `callStateProvider`/`voipStateProvider` in app_state.dart sind ersatzlos
  durch die Modul-Provider ersetzt (reine Umbenennung + Typ = Interface).
- **Bewusste Abweichungen vom Schnitt-Entwurf** (Verhaltensgleichheit vor
  Schönheit, heiliger Call-Pfad; im Doc-Status-Kopf dokumentiert):
  ChangeNotifier statt Streams, Methodennamen wie im Bestand
  (`startCall`/`hangUp` statt `join`/`leave`), `CallSession` und
  LiveKit-`Room` bleiben in den Schnittstellen sichtbar (UI rendert direkt
  Renderer/Tracks – für die WebRTC-Renderer sieht das Doc das selbst vor).
  Kein einziger Handler-/Logik-Block wurde verändert, nur Klassen-Köpfe
  (`extends` + `@override`), Imports, Provider-Namen und Typ-Annotationen.
- **Geprüft:** `flutter analyze` sauber (nur bekannter onUpdate-Hinweis),
  alle 29 Tests grün, `flutter run -d windows`: App startet, Login/Sync/E2EE
  laufen, sauberer Quit. (Commit-Hash siehe git log dieses Eintrags.)

**UNGETESTET (interaktiv/Gerät – deshalb ROADMAP-Punkt „Call-Schicht
entwirren" nur annotiert, nicht abgehakt):** echter Voice-Channel-Join per
Klick auf Windows und echter 1:1-Call zwischen zwei Geräten. Der Umbau ist
bewusst rein mechanisch (Fassade davor, Logik unangetastet), aber die
CLAUDE.md-Regel „nach Refactoring Kernflows prüfen" ist für die Call-Flows
erst nach einem Klick-/Gerätetest voll erfüllt – steht im PC_TESTPLAN
(Abschnitt 8) bzw. braucht Bernd/Uta.

**Offen/Nächster Schritt:** Härtetest nach `docs/PC_TESTPLAN.md` (so weit
ohne Klick-Interaktion möglich); die inneren M2-Aufräumarbeiten der
Call-Schicht (UI-State aus den Managern, `call_ui`-Modul, storage-Fassade
für die voice_*-Prefs) folgen als eigene Schritte.

**Stolperfallen:** (1) Bei part-Dateien (settings, rooms_panel, chat_view)
hängen die Imports an der EINEN Owner-Datei – wer eine part-Datei umstellt,
muss die Imports beim Owner ändern und findet Verwender nur per Repo-grep
auf den Provider-Namen. (2) Ein `ChangeNotifierProvider<Interface>` braucht
ein Interface, das ChangeNotifier ERWEITERT (abstract class X extends
ChangeNotifier), sonst passt der Typparameter nicht.

---

## 2026-07-06 – SQLCipher-Migration UMGESETZT + auf Windows mit echter DB praxisgetestet (M1, Bernds Prio 1)

**Erledigt:** Der M1-Punkt „Sichere Speicherung von Access-Token & Krypto-DB"
ist nach `docs/SQLCIPHER_MIGRATION.md` umgesetzt (PC-Session laut
Entscheidung 1 in `ANTWORTEN_BERND.md`):

- **Neue Storage-Fassade `lib/core/app_database.dart`** (erster Baustein im
  Sinne des M2-Modularitäts-Leitsatzes): kapselt Factory-Wahl, SQLCipher-
  Schlüssel (32 Zufallsbytes, hex im Keystore via flutter_secure_storage,
  Eintrag `db_cipher_key`, mit Read-Back-Verifikation gegen stilles
  Write-Versagen), Klartext→SQLCipher-Migration und die vorher duplizierten
  PRAGMAs. `matrix_client.dart` und `background_push.dart` rufen nur noch
  `AppDatabase.open()` / `AppDatabase.openForPush()`.
- **Migration exakt nach Plan:** Marker gegen Push-Isolate-Race →
  WAL-Checkpoint → Byte-Backup `.premigration` (nach 7 Tagen auto-gelöscht) →
  `sqlcipher_export` in NEUE Datei → Verifikation (integrity_check +
  Zeilenzahlen ALLER Tabellen) → atomarer rename-Tausch. Jeder Fehlpfad lässt
  die Klartext-DB unangetastet (App läuft dann unverschlüsselt weiter,
  Fehler im auth_log). Absturz zwischen Löschen und Umbenennen wird beim
  nächsten Start aus dem Byte-Backup repariert.
- **Wichtige Plan-Korrektur:** Die Plan-Annahme „sqlcipher_flutter_libs hat
  keine Desktop-Binaries" ist seit 0.6.x überholt – das Plugin baut SQLCipher
  auf Windows als `sqlite3.dll` MIT (im Build-Output verifiziert, nur dieses
  eine sqlite-Plugin ist im Build). Deshalb verschlüsseln Windows/Linux
  ebenfalls (kein Test-Override nötig); iOS/macOS bleiben bewusst Klartext
  (Pod-Kollision mit firebase_messaging, kein Release-Ziel). Laufzeit-Wächter
  `PRAGMA cipher_version` verhindert, dass je stillschweigend die falsche
  Bibliothek benutzt wird; verschlüsselte DB ohne Cipher-Lib/Schlüssel ist
  ein harter Fehler mit Klartext-Meldung – NIE stille Neuanlage.
- **Tests `test/app_database_test.dart`** (5 neue, gesamt 29, alle grün):
  fahren die ECHTE Migration mit der SQLCipher-DLL aus dem Build-Output –
  cipher_version-Wächter, Header-Erkennung, Migration mit Zeilenzahl-
  Verifikation, falscher Schlüssel scheitert sofort, erzwungener Fehlschlag
  lässt das Klartext-Original intakt.
- **Praxistest Windows (`flutter run -d windows`), echte Daten:** Bernds
  gewachsene 14-MB-DB (inkl. 4 MB WAL) wurde beim Start migriert
  (auth_log: „23 Tabellen verifiziert"), Session blieb eingeloggt, E2EE
  nachweislich intakt (App entschlüsselte live to_device-Events von
  @uta, forwardete Room-Keys). Zweiter Start öffnet die verschlüsselte DB
  ohne erneute Migration, sauberer Quit per `q`. DB-Header jetzt zufällig
  (verschlüsselt), `.premigration` = Klartext-Original. Zusätzliche manuelle
  Sicherung liegt als `pyramid.sqlite*.claude-vormigration-20260706` im
  App-Verzeichnis (kann nach ein paar Tagen weg).
- `sqlite3` als direkte Dependency ergänzt (war schon transitiv im Lockfile –
  kein neues externes Paket), für `open.overrideFor`/`openCipherOnAndroid`.
- `flutter analyze` sauber (nur der bekannte, bewusst zurückgestellte
  onUpdate-Hinweis).

**UNGETESTET (Android – deshalb ROADMAP-Punkt NICHT abgehakt):** Migration
auf echtem Android-Gerät, Push-Isolate liest verschlüsselte DB
(`databaseFactoryFfiNoIsolate` + `openCipherOnAndroid`), flutter_secure_storage
aus dem Background-Isolate (bekanntes Risiko, Code steigt dann kontrolliert
mit Platzhalter-Notification aus). Testplan Punkt 4 in
`docs/SQLCIPHER_MIGRATION.md`; erst danach Release + Uta (vorher ihr
Key-Backup verifizieren).

**Offen/Nächster Schritt:** M2-Call-Pilot (Entscheidung 2: Call-Fassade nach
`docs/M2_MODULE_SCHNITT.md`), danach Härtetest nach `docs/PC_TESTPLAN.md`.

**Stolperfallen:** (1) Plan-Annahmen über Paket-Fähigkeiten VOR der Umsetzung
gegen den Pub-Cache/Build-Output prüfen – die „keine Desktop-Binaries"-Annahme
hätte unnötig einen Test-Override-Sonderpfad erzeugt. (2) `sqfliteFfiInit()`
war auf Windows faktisch wirkungslos fürs DB-Isolate (Override wirkt nur im
aufrufenden Isolate; das Worker-Isolate lädt die exe-nachbarschaftliche
`sqlite3.dll` – und die IST hier SQLCipher). Für eigene Overrides deshalb
`createDatabaseFactoryFfi(ffiInit: ...)` benutzen, das läuft IM DB-Isolate.
(3) Beim Schlüssel-Anlegen im Keystore IMMER read-back-verifizieren, bevor
damit verschlüsselt wird – ein still verlorener Schlüssel wäre Totalverlust.
(4) `flutter_test` findet keine `sqlite3.dll` neben dem Tester-Binary – die
DLL aus `build/windows/x64/runner/*/` explizit laden; Tests überspringen sich
selbst, wenn kein Build-Output da ist (Pi-tauglich).

---

## 2026-07-06 – Fable-5-Review (aa): autopilot.sh fest auf nordmann-Konto (uncommittete Änderung geprüft, E-Mail-Leak vor Publikation gefixt, committet)

**Erledigt:** Session-Start nach Arbeitszyklus zeigte echten neuen Anlass (also
kein reiner Protokoll-Commit, Ende-Kriterium aus (z) greift nicht): eine
uncommittete Änderung an `autopilot.sh` von heute plus Backup
`autopilot.sh.bak-20260706-vor-nordmann-pin`. Inhalt: Die Zwei-Konten-Rotation
(pyramid↔haupt) ist komplett entfernt, der Autopilot läuft laut Kommentar auf
Bernds Vorgabe (2026-07-06) nur noch mit dem nordmann-Konto
(Standard-Profil `~/.claude`). Der CHANGES-Hook hat dafür KEINEN Eintrag
(Log endet 2026-07-05 19:03) – die Änderung kam also von außerhalb einer
gehookten Claude-Session (Bernd/manuell). Als Punkt (aa) im
Fable-5-Review-Abschnitt abgehakt:

- **Umbau korrekt:** Rotation restlos raus (repo-weit kein
  `get_account`/`switch_account`/`JUST_SWITCHED`/`ACCOUNT_FILE`-Rest),
  `apply_account` macht nur noch `unset CLAUDE_CONFIG_DIR` – exakt die Lehre
  aus Review (w) (Hauptprofil liegt unter `~/.claude.json`, die Variable darf
  nicht gesetzt sein). `bash -n` sauber. Limit-Pfad: 20 min warten statt
  Konto-Wechsel, PAUSE-/Notify-Logik unverändert.
- **Am lebenden System belegt:** tmux-Session „pyramid" wurde heute 11:54 neu
  gestartet, `autopilot.log` zeigt das neue Log-Format, und DIESE Session
  läuft nachweislich als nordmann-Konto mit leerem `CLAUDE_CONFIG_DIR`.
- **Befund (gefixt VOR dem Commit):** Die neue Fassung enthielt Bernds
  private E-Mail-Adresse im Klartext (Kommentar + `ACCOUNT_NAME`-Log-Zeile).
  Das Gitea-Repo ist ÖFFENTLICH, und bisher enthielt keine einzige getrackte
  Datei diese Adresse – ein Commit hätte sie erstmals veröffentlicht
  (PII/Spam-Harvesting, gleiche Familie wie der Secrets-Leitsatz in
  CLAUDE.md). Funktional ist die Adresse unnötig (stand nur in Kommentar und
  gitignoriertem `autopilot.log`) → durch neutrales „nordmann" ersetzt,
  Begründungs-Kommentar dagelassen, damit sie niemand „zur Klarheit" wieder
  einträgt.
- Aufräumen drumherum: `*.bak-*` in `.gitignore` aufgenommen (das heutige
  Backup bleibt als Rollback auf der Platte, hält aber den Arbeitsbaum-
  Detektor künftiger Sessions sauber). Hinweis: `~/.claude-accounts/active`
  (Inhalt „haupt") liegt verwaist außerhalb des Repos – wird von nichts mehr
  gelesen, bewusst nicht angefasst. Memory (Haupt-Profil) auf den neuen
  Ein-Konto-Stand aktualisiert.

**ROADMAP-Check danach (Schritt 2 des Auftrags):** Unverändert kein offener
Punkt auf dem Pi sicher bearbeitbar – SQLCipher und Härtetest brauchen
PC/Gerät (Entscheidung 1, `docs/PC_TESTPLAN.md`), der M2-Call-Pilot braucht
Gerätetest (Entscheidung 2, heiliger Call-Pfad), Secrets/History ruhen
bewusst (Entscheidung 4). Keine Arbeit erfunden – Session endet sauber nach
diesem Commit.

**Offen/Nächster Schritt:** Unverändert – nächster PC-/Gerätelauf startet mit
SQLCipher (`docs/SQLCIPHER_MIGRATION.md`), danach Call-Pilot; Härtetest nach
`docs/PC_TESTPLAN.md`.

**Stolperfallen:** (1) Der CHANGES-Hook-Detektor sieht nur Claude-Edits –
Bernds manuelle Änderungen tauchen NUR in `git status` auf; deshalb gehört
`git status` (nicht nur das Hook-Log) an jeden Session-Start. (2) Vor dem
Committen fremder/manueller Änderungen in ein ÖFFENTLICHES Repo den Diff auf
PII/Secrets abklopfen – hier wäre sonst eine private E-Mail-Adresse
mitpubliziert worden.

---

## 2026-07-05 – Fable-5-Review (z): (y)-Protokoll-Commit geprüft (kein Befund) + Ende-Kriterium geschärft

**Erledigt:** Session-Start nach Arbeitszyklus: Arbeitsbaum sauber, Gitea nicht
voraus, CHANGES-Hook-Log endet 18:41 = exakt die (y)-PROGRESS-Edits → keine
unprotokollierte Pi-Arbeit. Einziger seit (y) ungeprüfter Rest war der
(y)-Protokoll-Commit 725f03a selbst; als Punkt (z) im Fable-5-Review-Abschnitt
abgehakt (kurzer Gegenlese-Pass, keine Volltiefen-Runde):

- Diff von 725f03a ist reine Doku und deckt sich exakt mit dem (y)-Eintrag:
  PROGRESS.md +45 Zeilen ((y)-Eintrag + Checklisten-Häkchen), CHANGES.md
  +2 Hook-Zeilen. Kein Code, nichts Halbfertiges.
- (y)-Kernaussagen stichprobenhaft gegengeprüft, beide korrekt: 15001ae ist
  wirklich PROGRESS +46/CHANGES +2 (reine Doku), und
  `git log 925aafb..HEAD -- lib/ test/ pubspec.*` ist leer – seit dem letzten
  grünen Testlauf (24/24) ist kein Code angefasst, kein flutter-Lauf nötig.

**Ende-Kriterium geschärft (in der Review-Checkliste dokumentiert):** Die
Kette (a)–(z) reviewte zuletzt nur noch die Protokoll-Commits der jeweiligen
Vorsession – das terminiert nie von selbst. Ab jetzt gilt: Ein reiner,
befundloser Protokoll-Commit einer Review-Session ist KEIN neuer
Review-Anlass. Neue Review-Punkte gibt es nur noch für Arbeit mit Substanz
(Code- oder inhaltliche Doku-Änderungen, z. B. neue PC-Commits).

**ROADMAP-Check danach (Schritt 2 des Auftrags):** Unverändert kein offener
Punkt auf dem Pi sicher bearbeitbar – SQLCipher und Härtetest brauchen
PC/Gerät (Entscheidung 1, `docs/PC_TESTPLAN.md`), der M2-Call-Pilot braucht
Gerätetest (Entscheidung 2, heiliger Call-Pfad), Secrets/History ruhen
bewusst (Entscheidung 4, nicht anstoßen). Keine Arbeit erfunden – Session
endet sauber nach diesem Protokoll-Commit.

**Offen/Nächster Schritt:** Unverändert – nächster PC-/Gerätelauf startet mit
SQLCipher (`docs/SQLCIPHER_MIGRATION.md`), danach Call-Pilot; Härtetest nach
`docs/PC_TESTPLAN.md`.

**Stolperfallen:** Eine Review-Kette, die Protokoll-Commits reviewt, erzeugt
mit jedem Review einen neuen Protokoll-Commit – ohne explizites
„Protokoll-only zählt nicht"-Kriterium terminiert sie nie. Das Kriterium
gehört an die Checkliste selbst, nicht nur in einen Session-Eintrag.

---

## 2026-07-05 – Fable-5-Review (y): liegengebliebenen (x)-Protokoll-Commit der Vorsession geprüft und nachgeholt

**Erledigt:** Session-Start zeigte einen unsauberen Arbeitsbaum: Die
(x)-Session wurde NACH dem Schreiben ihres PROGRESS-Eintrags (15:18 laut
CHANGES-Hook-Log), aber VOR ihrem Protokoll-Commit abgebrochen – der
(x)-Eintrag samt Review-Checklisten-Häkchen lag uncommitted da. Als Punkt (y)
im Fable-5-Review-Abschnitt abgehakt:

- Diff der Hinterlassenschaft ist reine Doku (PROGRESS.md +46 Zeilen =
  (x)-Eintrag + Checklisten-Punkt, CHANGES.md +2 Hook-Zeilen) – kein Code,
  nichts Halbfertiges darüber hinaus.
- Kernaussagen des (x)-Eintrags stichprobenhaft gegengeprüft, alle korrekt:
  66bf54b-Diff-Umfang stimmt (4 Dateien, +77/−21, nur Doku); seit dem
  letzten grünen Testlauf (925aafb) keinerlei Änderung an
  `lib/`/`test/`/`pubspec.*` (git log leer); A/B-Frage wird außerhalb des
  historischen PROGRESS-Logs nirgends mehr als offen dargestellt
  (`ANTWORTEN_BERND.md` sagt „geschlossen", Hardening-Doc-Status
  ABGESCHLOSSEN/Archiv, ROADMAP-Abschluss-Block vorhanden).
- Eintrag unverändert übernommen und als eigener Commit nachgeholt
  (15001ae). Review-Stand damit (a)–(y), Kette weiterhin konvergiert.

**ROADMAP-Check danach (Schritt 2 des Auftrags):** Kein offener Punkt ist auf
dem Pi sicher bearbeitbar – SQLCipher und Härtetest brauchen PC/Gerät
(Entscheidung 1, `docs/PC_TESTPLAN.md`), der M2-Call-Pilot braucht Gerätetest
(Entscheidung 2, heiliger Call-Pfad), Secrets/History ruhen bewusst
(Entscheidung 4, nicht anstoßen). Keine Arbeit erfunden – Session endet
sauber nach diesem Protokoll-Commit.

**Offen/Nächster Schritt:** Unverändert – nächster PC-/Gerätelauf startet mit
SQLCipher (`docs/SQLCIPHER_MIGRATION.md`), danach Call-Pilot; Härtetest nach
`docs/PC_TESTPLAN.md`.

**Stolperfallen:** Ein abgebrochener „Protokoll-Commit" hinterlässt genau das
Muster „PROGRESS.md modified, sonst nichts" – das ist harmlos und einfach
nachzuholen, aber IMMER erst den Diff gegenlesen statt blind zu committen
(hier stimmte alles).

---

## 2026-07-05 – Fable-5-Review (x): (v)-Doku-Fix (66bf54b) geprüft – korrekt, KEIN Befund, Review-Stand wieder konvergiert

**Erledigt:** Review-Pass über die einzige seit (v)+(w) dazugekommene Arbeit –
den Doku-Schließungs-Commit 66bf54b (+ Hook-Commit 1ef32c0) aus derselben
Session, der den (v)-Befund fixte. Als Punkt (x) im Fable-5-Review-Abschnitt
abgehakt. Wie bei (u) ein kurzer Diff-Gegenlese-Pass, keine Volltiefen-Runde:

- Diff von 66bf54b ist exakt der protokollierte Fix: ROADMAP-Dashboard-Punkt
  abgehakt mit Abschluss-Block (Heimnetz-Gate = Endzustand, Restrisiken bewusst
  akzeptiert, „Nicht weiter daran arbeiten"), Nr.-4-Vermerk am Secrets-Punkt
  (Rotation/History ruhen, nicht anstoßen), `docs/DASHBOARD_AUTH_HARDENING.md`
  Status-Kopf auf ABGESCHLOSSEN, Weg A/B als Archiv markiert, alle „bis zur
  A/B-Entscheidung"-Formulierungen bereinigt. Alles deckungsgleich mit
  `ANTWORTEN_BERND.md` Nr. 3 und 4.
- Vollständigkeits-grep über alle .md-Dateien: Außerhalb der HISTORISCHEN
  PROGRESS-Einträge (Log wird nicht umgeschrieben – korrekt so) stellt keine
  Datei die A/B-Frage mehr als offen dar. Das „(empfohlen)" an Option A im
  Hardening-Doc steht unter dem Archiv-Vermerk des Status-Kopfs – ausreichend.
- Kein unprotokollierter Rest: CHANGES-Hook-Log endet 14:54 (= Commit-Zeit
  von 1ef32c0), Arbeitsbaum sauber. Seit dem letzten grünen Testlauf
  (24/24, Eintrag (i)/925aafb) wurde nichts in `lib/`/`test/`/`pubspec.yaml`
  angefasst (per `git log` geprüft) – kein neuer flutter-Lauf nötig, der
  Commit ist reine Doku.

**Konvergenz-Kriterium aus (u) erfüllt:** Der letzte Diff fixt nur den
Vorbefund, das Muster-grep ist leer → auch diese Kette ist geschlossen.
ALLE als erledigt markierten ROADMAP-/PROGRESS-Punkte sind reviewt (a)–(x).

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher zuerst laut Entscheidung 1, dann Call-Pilot; Härtetest
nach `docs/PC_TESTPLAN.md`) oder ruhen bewusst (Secrets, Entscheidung 4).
Auf dem Pi ohne neuen Anlass nichts sicher Bearbeitbares – Session endet
sauber nach diesem Protokoll-Commit.

**Stolperfallen:** Keine – reiner Gegenlese-Pass ohne Befund.

---

## 2026-07-05 – Dashboard-ROADMAP-Punkt GESCHLOSSEN (Bernds Entscheidung 3 nachgezogen)

**Erledigt:** Folge aus Review-Befund (v): Bernds Entscheidungen 3 und 4 aus
`ANTWORTEN_BERND.md` sind jetzt überall nachgezogen, damit keine künftige
Session die geschlossenen Fragen wieder aufmacht:

- **ROADMAP M0 „Dashboard-Admin-Endpoints": abgehakt.** Das Heimnetz-Gate ist
  laut Bernd der Endzustand (kein Fernzugriff, unterwegs WireGuard, A/B-Frage
  geschlossen). Restrisiken (öffentliche Nutzerlisten-Ansicht, theoretisches
  LAN-CSRF) bewusst akzeptiert – im ROADMAP-Punkt und in
  `docs/DASHBOARD_AUTH_HARDENING.md` vermerkt.
- **`docs/DASHBOARD_AUTH_HARDENING.md`:** Status-Kopf auf „ABGESCHLOSSEN"
  umgestellt, „Interims-Gate" heißt jetzt Endzustand, Weg A/B als Archiv
  markiert, „bis zur A/B-Entscheidung"-Formulierungen bereinigt.
- **ROADMAP M0 Secrets-Punkt:** Vermerk zu Entscheidung 4 ergänzt – Rotation
  und History-Rewrite ruhen bewusst (Bernds manuelle Sache, nicht vom
  Autopilot anstoßen oder nachfragen); der Punkt bleibt nur als Merkposten
  offen. KEINE Code-/Serveränderung in diesem Schritt, nur Doku –
  `server.py` und das Gate sind unangetastet.

**Offen/Nächster Schritt:** In M0 sind damit nur noch zwei Punkte offen, beide
nicht Pi-bearbeitbar: SQLCipher (PC-/Gerätetest, laut Entscheidung 1 als
Erstes am PC) und der Härtetest (`docs/PC_TESTPLAN.md`). Danach M2 mit
Call-Pilot (Entscheidung 2, Gerätetest nötig). Auf dem Pi bleibt ohne neuen
Anlass nichts sicher Bearbeitbares – Session darf sauber enden.

**Stolperfallen:** Keine – reine Doku-Angleichung. Lehre aus dem
(v)-Befund: Wenn Entscheidungen in einer eigenen Datei landen
(`ANTWORTEN_BERND.md`), müssen die referenzierten offenen Punkte in
ROADMAP/Docs im SELBEN Zug geschlossen/annotiert werden, sonst stellt die
nächste Session die beantwortete Frage erneut.

---

## 2026-07-05 – Fable-5-Review (v)+(w): die zwei PC-Commits geprüft – Fix korrekt, 1 Doku-Lücke gefunden

**Erledigt:** Review-Pass über die seit (u) dazugekommene Arbeit – die zwei am
PC entstandenen Commits als Punkte (v) und (w) im Fable-5-Review-Abschnitt
abgehakt:

- **(v) 825481e (`ANTWORTEN_BERND.md` + CLAUDE.md-Verweis):** inhaltlich
  konsistent – alle referenzierten Pläne/Dateien existieren
  (`docs/SQLCIPHER_MIGRATION.md`, `docs/M2_MODULE_SCHNITT.md`,
  `docs/DASHBOARD_AUTH_HARDENING.md`, `docs/PC_TESTPLAN.md`;
  `voip_manager.dart`/`livekit_call_manager.dart` liegen in `lib/core/`), die
  vier Entscheidungen decken exakt die in PROGRESS.md gesammelten Fragen ab.
  **Befund (Doku-Lücke):** Entscheidung 3 (Dashboard bleibt Heimnetz-only,
  A/B-Frage GESCHLOSSEN) war noch nicht in ROADMAP.md und
  `docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen – beide präsentierten die
  Frage weiterhin als offen, künftige Sessions hätten Bernd erneut gefragt.
  Direkt im nächsten Schritt dieser Session behoben (eigener Commit,
  Eintrag oben).
- **(w) 4477ed9 (`autopilot.sh`, CLAUDE_CONFIG_DIR-Falle):** Fix ist korrekt
  und am lebenden System belegt: Mit gesetztem `CLAUDE_CONFIG_DIR` sucht die
  Claude-CLI ihre Config unter `$CLAUDE_CONFIG_DIR/.claude.json` – das
  Hauptkonto-Profil liegt aber unter `~/.claude.json`, das alte
  `export CLAUDE_CONFIG_DIR="$HOME/.claude"` zeigte also auf ein leeres
  Zweitprofil (`~/.claude/.claude.json`). `unset` ist richtig und wirkt auch
  nach einer vorherigen pyramid-Iteration derselben Shell (apply_account
  läuft vor jeder Session). Lebender Beweis: DIESE Session lief als Konto
  „haupt" (`~/.claude-accounts/active` = haupt, `CLAUDE_CONFIG_DIR` leer,
  `~/.claude.json` vorhanden) und funktionierte. Kein weiterer
  `CLAUDE_CONFIG_DIR`-Verweis im Repo. Kein Befund.

**Offen/Nächster Schritt:** Befund aus (v) nachziehen (ROADMAP-Dashboard-Punkt
schließen, Doku-Status aktualisieren, Vermerk zu Entscheidung 4 am
Secrets-Punkt) – passiert direkt als nächster Schritt dieser Session.

**Stolperfallen:** Der CHANGES.md-Hook (in `.claude/settings.json`, schreibt
relativ nach `CHANGES.md`) protokolliert nur Arbeit auf der jeweiligen
Maschine – die PC-Commits vom 2026-07-05 haben hier auf dem Pi KEINE
Hook-Einträge hinterlassen. Der „Hook-Log als Detektor für unprotokollierte
Arbeit"-Trick aus Review (p) funktioniert also nur pro Maschine; für
PC-Arbeit bleibt `git log` die einzige Quelle.

---

## 2026-07-05 – Bernds Richtungsfragen beantwortet (am PC)

**Erledigt:** Die über mehrere Sessions gesammelten „Fragen an Bernd" sind beantwortet
und in `ANTWORTEN_BERND.md` festgehalten (maßgeblich, in CLAUDE.md verlinkt):
1. SQLCipher → **vor M2** priorisieren (M1-Punkt); Migration braucht PC-/Gerätetest.
2. M2 erstes Modul → **Call-Pilot zuerst** (nicht `settings_modal.dart`); braucht Gerätetest.
3. Dashboard-Fernzugriff → **Heimnetz-only lassen**; A/B-Frage geschlossen.
4. Git-History / Token-Rotation → **vorerst nichts tun** (Bernds manuelle Sache).

**Offen/Nächster Schritt:** Die entschiedenen Punkte bleiben PC-/gerätegebunden
(SQLCipher-Migration, Call-Pilot-Umbau). Auf dem Pi ohne GUI nur sicher Vorbereitbares
angehen und klar als UNGETESTET markieren; Verhaltensrelevantes erst nach
`docs/PC_TESTPLAN.md` als erledigt abhaken. Reihenfolge: erst SQLCipher, dann Call-Pilot.

**Stolperfallen:** Beide priorisierten Punkte sind trotz Entscheidung NICHT auf dem Pi
abschließbar (kein GUI-/Gerätetest) – nicht als „jetzt endlich machbar" missverstehen.

---


## 2026-07-04 – Fable-5-Review (u): (t)-Fix geprüft – korrekt, KEIN Befund, server.py-Review-Kette abgeschlossen

**Erledigt:** Review-Pass über die letzte noch ungeprüfte Arbeit – den
(t)-Restbefund-Fix (Commit d3e75c3) gegengelesen und als Punkt (u) im
Fable-5-Review-Abschnitt abgehakt. Wie im Arbeitsstand-Memory vorgesehen war
das ein kurzer Diff-Gegenlese-Pass, KEINE weitere Volltiefen-Runde:

- Diff gegen `server.py.bak-20260704-review-t` ist exakt der protokollierte
  Mini-Fix (1 Fundstelle: `registration-status` antwortet generisch, Detail
  per stderr ins Journal); `sys` ist importiert, `py_compile` sauber.
- Dienst läuft mit dem aktuellen Code (Start 19:19:40 NACH Datei-mtime
  19:19:22), kein unprotokollierter Rest im CHANGES-Hook-Log (letzte Einträge
  19:19–19:22 gehören zur (t)-Session, danach Commit 4c6efa2).
- Die 4 verbliebenen `str(e)`-Antworten liegen alle im `with ADMIN_LOCK:`-Block
  HINTER dem Heimnetz-Gate (run-stats, create-invite, toggle-registration,
  ban/unban) – absichtlich, LAN-only, für Bernd nützlich. Kein Handlungsbedarf.
- Kurz-Check des laufenden Dienstes: stats.html 200, registration-status 200
  (`{"enabled": false}`), unbekannte Route 404.

**Damit ist die server.py-Härtungskette (o)–(u) KONVERGIERT** – ohne neuen
Anlass (neuer Code, neue Route) gibt es dort nichts mehr zu prüfen. ALLE als
erledigt markierten ROADMAP-/PROGRESS-Punkte sind reviewt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest, M2-Call/State) oder
Bernd (Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go). Auf dem Pi
ist nichts sicher Bearbeitbares mehr offen (Memory: nicht erneut suchen,
UNGETESTET-Stapel bewusst gedeckelt).

**Stolperfallen:** Keine – der Befund aus (t) war korrekt gefixt. Eine
Review-Kette braucht ein dokumentiertes Ende-Kriterium („konvergiert wenn der
letzte Diff nur noch den Vorbefund fixt und das Muster-grep leer ist"), sonst
reviewt jede Session den Review der Vorsession bis in alle Ewigkeit.

---

## 2026-07-04 – Fable-5-Review (t): (s)-Fixes geprüft – korrekt, 1 kleiner Restbefund gefunden und gefixt

**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die zwei
(s)-Restbefund-Fixes (Commit 13b333c) kritisch gegengelesen und als Punkt (t)
im Fable-5-Review-Abschnitt abgehakt. Der (s)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-s` verifiziert: ban/unban-Body-Lesen
sitzt vollständig VOR dem Lock inkl. Validierung, die generischen 500er auf
beiden öffentlichen POST-Routen stimmen; Dienst lief mit dem aktuellen Code,
kein unprotokollierter Rest im CHANGES-Hook-Log). Beim Ganzdatei-Gegenlesen
EIN Restbefund derselben Familie wie (s)-Befund 2:

1. **`GET /api/registration-status` leakte Exception-Texte:** Die Route ist
   öffentlich erreichbar (in `do_GET` gibt es kein Heimnetz-Gate) und
   antwortete bei Fehlern mit `str(e)` – ein Lesefehler an `conduit.toml`
   hätte den internen Dateipfad an Unauthentifizierte verraten. (s) hatte nur
   die zwei öffentlichen POST-Routen gefixt und die öffentliche GET-Route
   übersehen. Fix: gleiches Muster – generische 500-Antwort („Interner
   Fehler"), Detail per stderr ins Journal.

**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-t`). 8 Routen-Checks wie in (s) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, livekit-token ungültig 401, Diagnose falscher Token 403,
gespoofter Cf-Header 403, unbekannte Route 404). **500-Beweis (neu):**
`conduit.toml` für ~1 s per `chmod 000` unlesbar gemacht →
`{"error": "Interner Fehler"}` nach außen, `PermissionError` im Journal
(vorher wäre `[Errno 13] … '/home/steggi/matrix/conduit.toml'` – also der
Pfad – nach draußen gegangen); Rechte sofort wiederhergestellt (664), Route
danach wieder 200. Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).

**Stolperfallen:** Bei einem Info-Leak-Fix nicht nur die Methode fixen, in
der der Befund auftauchte: (s) hatte `do_POST` gefixt, aber `do_GET` derselben
Datei hat auch öffentliche Routen mit demselben `str(e)`-Muster. Lehre: Nach
einem Musterfund (`str(e)` an Unauthentifizierte) einmal `grep str(e)` über
die GANZE Datei und jede Fundstelle als „öffentlich oder LAN-only?"
einordnen, statt nur die Nachbarroute zu fixen.

---

## 2026-07-04 – Fable-5-Review (s): (r)-Fixes geprüft – korrekt, 2 kleinere Restbefunde gefunden und gefixt

**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die drei
(r)-Restbefund-Fixes (Commit 5ada0e5) kritisch gegengelesen und als Punkt (s)
im Fable-5-Review-Abschnitt abgehakt. Der (r)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-r` verifiziert, Dienst lief mit dem
aktuellen Code). Beim Gegenlesen der GANZEN Datei (Lehre aus (o)) zwei
kleinere Restbefunde in `server.py` gefunden und gefixt:

1. **ban/unban las den Body INNERHALB des `ADMIN_LOCK`:** Das 4-KB-Limit aus
   (r) deckelt nur die Größe – ein LAN-Client, der `Content-Length` ankündigt
   und nichts schickt, hielt den Lock trotzdem bis zum 30-s-Socket-Timeout
   fest (der (r)-Kommentar behauptete fälschlich, der Deckel verhindere das).
   Seit (r) wartet auch der öffentliche GET-Leser von `registration-status`
   auf diesen Lock → der hing mit. Fix: Body-Lesen/Validieren VOR den Lock
   gezogen (Handler-Logik identisch, nur verschoben).
2. **Öffentliche Routen leakten Exception-Texte:** `livekit-token` und
   `e2ee-diagnostics` antworteten Unauthentifizierten bei Fehlern mit
   `str(e)` – das kann interne Pfade (`livekit.yaml`-Pfad bei
   FileNotFoundError) oder interne Adressen preisgeben. Fix: generische
   500-Antwort („Interner Fehler"), Details per stderr ins Journal. Die
   Admin-Routen behalten `str(e)` (LAN-only, für Bernd nützlich).

**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-s`). 8 Routen-Checks wie in (r) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, `Content-Length: -5` 413, livekit-token ungültig 401,
Diagnose falscher Token 403, gespoofter Cf-Header 403). **Lock-Beweis (neu):
ban-Request mit angekündigtem, nie gesendetem Body + paralleler
`registration-status`-GET → Antwort in 15 ms statt Lock-Blockade** (vorher
hätte der GET bis zu 30 s gewartet). **500-Beweis (neu):** ungültiges JSON an
livekit-token → `{"error": "Interner Fehler"}`, Detail
(`JSONDecodeError(...)`) steht im Journal. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).

**Stolperfallen:** (1) Ein Größen-Limit ist KEIN Zeit-Limit: `read(n)` mit
kleinem n blockiert trotzdem bis zum Socket-Timeout, wenn der Client nichts
schickt – blockierende I/O gehört grundsätzlich VOR einen Lock, nicht
hinein. (2) `str(e)` in Fehlerantworten öffentlicher Endpoints ist ein
Info-Leak (Pfade, interne Hosts) – Details gehören ins Journal, nach draußen
nur Generisches.

---

## 2026-07-04 – Fable-5-Review (r): ADMIN_LOCK-Umbau (q) geprüft – korrekt, aber 3 kleinere Restbefunde gefunden und gefixt

**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – den ADMIN_LOCK-Umbau
aus (q) (Commit c8ff850) kritisch gegengelesen und als Punkt (r) im
Fable-5-Review-Abschnitt abgehakt. Der Umbau selbst ist korrekt (Locking-Struktur,
Routen-Reihenfolge, Handler-Logik unverändert). Drei kleinere Restbefunde in
`server.py` gefunden und gefixt:

1. **GET-Leser-Race (gleiche Familie wie (q), dort übersehen):** (q) hatte nur
   POST-gegen-POST betrachtet – `GET /api/registration-status` liest
   `conduit.toml` aber OHNE Lock, während `toggle-registration`/`create-invite`
   die Datei per Truncate+Write in-place neu schreiben → seit multithreaded (p)
   kann der Leser eine halb geschriebene/leere Datei sehen (Dashboard zeigt
   transient falschen Registrierungs-Status). Fix: Lesen unter `ADMIN_LOCK`.
   **Wichtige Erkenntnis dabei:** Der naheliegendere Fix (atomares `os.replace`
   statt In-Place-Write) wäre FALSCH – `conduit.toml` ist als **einzelne Datei**
   in den Container gemountet (`./conduit.toml:/etc/conduit.toml:ro`), ein
   Replace tauscht den Inode und der Container sähe für immer die alte Datei.
   Im Code als Kommentar festgehalten, damit das nie jemand „verbessert".
2. **Negative `Content-Length`:** `livekit-token`, `e2ee-diagnostics` und
   `ban`/`unban` prüften nur `length > MAX` – ein negativer Wert wäre
   durchgerutscht und `rfile.read(-n)` liest bis EOF (Thread hängt bis zum
   30-s-Timeout). Fix: `length < 0` wird wie „zu groß" abgelehnt (413).
3. **`ban`/`unban` ohne Body-Limit + `random` für Einladungs-Token:**
   ban/unban lasen beliebig große Bodies (und halten dabei seit (q) den
   ADMIN_LOCK) → 4-KB-Deckel wie bei livekit-token. Einladungs-Token
   (`create-invite`) kamen aus dem vorhersagbaren `random`-PRNG, obwohl sie
   die Registrierung am Homeserver freischalten → `secrets.choice`.

**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-r`). 8 Routen-Checks: stats.html 200,
registration-status 200 (liest jetzt unter Lock), ban ohne Nutzer localhost →
400 „Kein Nutzer angegeben", **ban mit 8-KB-Body → 413 (neu)**, **livekit-token
mit `Content-Length: -5` → sofort 413 statt Hänger (neu)**, livekit-token
ungültiger Matrix-Token → 401, e2ee-diagnostics falscher Token → 403,
Admin-Route öffentlich über die Dashboard-Domain → 403. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).

**Stolperfallen:** (1) Bei Nebenläufigkeits-Reviews nicht nur Schreiber gegen
Schreiber prüfen, sondern auch die LESER derselben Ressource – `do_GET` war in
(q) komplett außen vor. (2) „Atomares Schreiben per `os.replace`" ist bei
Docker-**Datei**-Bind-Mounts eine Falle: der Mount hängt am Inode, nach einem
Replace sieht der Container die alte Datei weiter. In-Place-Write ist dort
absichtlich richtig. (3) `Content-Length` kann negativ sein – wer nur nach oben
deckelt, lässt `read()` bis EOF blockieren.

---

## 2026-07-04 – Fable-5-Review (q): Threading-Härtung (p) tiefer geprüft – echte Nebenläufigkeits-Race in den Admin-Routen gefunden und mit ADMIN_LOCK behoben

**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die Threading-Umstellung
aus (p) (Commit 343545b, `ThreadingHTTPServer`) noch einmal kritisch gegengelesen und
als Punkt (q) im Fable-5-Review-Abschnitt abgehakt. Befund: Die Umstellung auf
multithreaded hat einen **echten Race erzeugt, den es vorher (single-threaded, alles
serialisiert) nicht geben konnte** – (p) hat nur den öffentlich beschreibbaren
Diagnose-Pfad per `DIAG_LOCK` abgesichert, die **Admin-Routen aber übersehen**:
- `toggle-registration` und `create-invite` machen ein Read-Modify-Write auf
  `conduit.toml` **plus** `docker compose restart conduit` – zwei gleichzeitige Klicks
  = verlorenes Update (lost update) + doppelter Container-Neustart.
- `send_admin()` schläft 4 s und liest danach die **letzten** Nachrichten aus dem
  Admin-Raum zurück, um „die" Antwort zuzuordnen – zwei gleichzeitige Admin-Befehle
  können sich vertauschte Antworten einfangen.

**Fix (`server.py`):** Neues `ADMIN_LOCK` (threading.Lock), das **nur** die
Admin-Mutationen serialisiert. Dafür `do_POST` minimal umgebaut: die beiden
öffentlichen Routen (`/api/livekit-token`, `/api/e2ee-diagnostics`) werden jetzt
**zuerst** behandelt und returnen, danach folgt das Heimnetz-Gate und der komplette
Admin-Dispatch in einem `with ADMIN_LOCK:`. Damit bleiben die öffentlichen Pfade
**nebenläufig** – der Slow-Loris-Schutz aus (p) ist unangetastet. Handler-Logik
wortwörtlich unverändert (nur verschoben/eingerückt, per `diff` gegen Backup
verifiziert). `python3 -m py_compile` sauber; Backup `server.py.bak-20260704-adminlock`.

**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
8 Routen-Checks identisch wie vorher (stats 200; ban localhost → 400 „Kein Nutzer",
ban „aus dem Internet"/gespoofter Cf-Header → 403; livekit-token 401/400/413;
e2ee-diagnostics falscher Token 403; unbekannte Route 404). **Race-Beweis:** ein
`run-stats` allein 1388 ms, **zwei parallel 2633 ms** (≈ 2× → serialisiert; ohne
Lock wären es ~1400 ms). **Non-Blocking-Beweis:** ein öffentliches `livekit-token`
**während** ein Admin-`run-stats` den Lock hält → in **11 ms** beantwortet (nicht
blockiert). Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).

**Stolperfallen:** Eine Threading-Umstellung ist nie „nur" ein Server-Konstruktor-
Tausch – sie macht **jeden** geteilten Zustand (Dateien, Read-Modify-Write,
Readback-nach-sleep) plötzlich race-fähig. (p) hatte richtig den einen öffentlich
beschreibbaren Pfad (`DIAG_LOCK`) gesehen, aber die Admin-Routen mit `conduit.toml`
und dem Admin-Raum-Readback sind derselbe Fall. Lehre: Nach „Server ist jetzt
multithreaded" ALLE schreibenden/zustandsändernden Handler durchgehen, nicht nur
den einen, der den Anlass gab.

---

## 2026-07-04 – Fable-5-Review (p): Interims-Gate geprüft (korrekt) + halbfertige Threading-Härtung einer abgebrochenen Session vollendet

**Erledigt:** Review-Pass über die jüngste erledigte Arbeit (Interims-Gate,
Commits e97a748/455742d) als Punkt (p) im Fable-5-Review-Abschnitt abgehakt.
Das Gate selbst ist korrekt (Socket-IP als belastbare Prüfung, Cf-Header nur
Zweitschranke, Deny-Log gedeckelt, kein Angreifer-Text im Log). Dabei über das
CHANGES.md-Hook-Log entdeckt: Eine Session hat um 09:43 (NACH dem letzten
Commit) **drei unprotokollierte server.py-Edits** gemacht und wurde mitten in
einer Threading-Härtung abgebrochen – `threading`-Import + `DIAG_LOCK` waren da,
aber der Lock wurde **nirgends benutzt**, und die eigentliche Umstellung auf
`ThreadingHTTPServer` fehlte (der Kommentar behauptete sie schon). Obendrein
lief der Dienst noch mit dem Code von 09:14 – nichts davon war je geladen.
Die angefangene Härtung ist fachlich richtig (der single-threaded `HTTPServer`
ließ EINEN langsamen Client den ganzen Server blockieren – inkl. des öffentlich
erreichbaren `/api/livekit-token`), darum vollendet statt zurückgebaut:
`ThreadingHTTPServer` in `__main__`, Diagnose-Log-Kappen+Anhängen in den
`DIAG_LOCK` gewickelt (einziger öffentlich beschreibbarer Pfad), `timeout = 30`
der Vorsession belassen. Dienst-Neustart (kill/`Restart=always`), headless
verifiziert (11 Prüfungen): stats 200; Admin-Route localhost UND 192.168.178.71
→ 400 „Kein Nutzer angegeben"; öffentlich UND gespoofter Cf-Header → 403;
livekit-token ungültig → 401; Diagnose falscher Token 403, gültig 200 + im Log,
300 KB → 413; **Slow-Loris-Test: parallele Anfrage in 10 ms beantwortet,
während ein Client hing** (vorher unmöglich), hängender Body-Read nach exakt
~30 s serverseitig beendet, Dienst danach gesund. Test-Einträge aus beiden
Logs wieder entfernt.

**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).

**Stolperfallen:** (1) Das CHANGES.md-Hook-Log ist der zuverlässigste Detektor
für unprotokollierte Arbeit abgebrochener Sessions – Zeitstempel der letzten
Einträge mit dem letzten Commit vergleichen. (2) „Datei geändert" heißt NICHT
„läuft": Dienststart-Zeit (`systemctl status`) gegen Datei-mtime prüfen – hier
lagen 30 Minuten Code unausgeführt auf der Platte. (3) Ein definierter, aber
nie benutzter Lock ist ein Halbfertig-Marker, kein Schutz.

---

## 2026-07-04 – Dashboard-Befund (o): akutes Loch mit aussperr-sicherem Interims-Gate geschlossen

**Erledigt:** Der KRITISCHE Befund (o) (Dashboard-Admin-Endpoints ohne Auth,
öffentlich erreichbar) ist entschärft, OHNE Bernds A/B-Entscheidung
vorwegzunehmen und OHNE ihn auszusperren. Erkenntnis, die das möglich machte:
Öffentlicher Verkehr erreicht `server.py` (Port 8080) ausschließlich über den
Cloudflare-Tunnel-Container (Absender-IP 172.x + `Cf-*`-Header) – Bernds
Heimnetz-Zugriffe kommen dagegen direkt mit `192.168.*`-Socket-IP an. Daher
„Weg C" als Interims-Gate in `server.py`: Die 5 Admin-Routen (`/api/ban`,
`/api/unban`, `/api/toggle-registration`, `/api/create-invite`, `/api/run-stats`)
akzeptieren nur noch Anfragen mit Heimnetz-/localhost-Socket-IP (nicht spoofbar)
und ohne Cloudflare-Header; sonst 403 mit deutscher Erklärung inkl.
Heimnetz-URL – das Dashboard zeigt `d.error` per `alert()`, Bernd sieht also
genau, was zu tun ist. Abgelehnte Versuche landen größenbegrenzt (5-MB-Deckel,
Lehre aus Befund (l)) in `security_alerts.log` (nur Fixpfad + Socket-IP, kein
attacker-kontrollierter Text → kein HTML-Injection-Risiko in der
Dashboard-Anzeige). Dienst-Neustart per kill/`Restart=always`, headless
verifiziert (9 Prüfungen): öffentlich ban/toggle → 403, localhost UND
192.168.178.71 → 400 „Kein Nutzer angegeben" (Gate passiert, App-Validierung
erreicht), gespoofter `Cf-Connecting-Ip` aus dem LAN → 403, `GET /` öffentlich
200, `/api/livekit-token` 401 und `/api/e2ee-diagnostics` 403 wie bisher
(bleiben bewusst öffentlich, die App braucht sie). Test-Einträge aus dem
Alert-Log wieder entfernt. Rückbau jederzeit: `server.py.bak-20260704-dashgate`.
Doku/ROADMAP nachgezogen (`docs/DASHBOARD_AUTH_HARDENING.md`: Status, Weg C,
Restrisiken).

**Zusätzlicher Befund (für Bernds Secret-Rotation wichtig):** Das Gitea-Repo
einfach privat zu schalten ist KEINE schnelle Lösung für den Secret-Leak (k):
Der In-App-Updater (`lib/core/update_checker.dart`) holt Releases **anonym**
über die öffentliche Gitea-API – Repo privat = Utas installierte App findet
keine Updates mehr, und ein Fix ließe sich nicht mehr per Update verteilen
(Henne-Ei). In ROADMAP M0 beim Rotations-Punkt als Warnung vermerkt.

**Offen/Nächster Schritt:** Bernds Entscheidung bleibt für den FERNZUGRIFF
relevant (Weg A empfohlen – schließt auch die weiterhin öffentliche
Nutzerlisten-/Präsenz-Ansicht; Restrisiken im Doc: LAN-CSRF, offene Ansicht).
Danach ggf. Gate ergänzen/ersetzen. Sonst unverändert: übrige M0-Punkte
brauchen PC/Gerät oder Bernd (SQLCipher, Rotation, LiveKit-Client-Umstellung,
Härtetest).

**Stolperfallen:** (1) `stats.html` wird alle 5 Minuten von `stats.sh` (Cron)
NEU GENERIERT – Änderungen an der HTML-Datei sind flüchtig, UI-Änderungen
müssten in `stats.sh` (deshalb bewusst NUR server-seitig gefixt). (2) Auf dem
Pi laufen ZWEI cloudflared-Instanzen: die lokal konfigurierte
(`cloudflared.yml`, nur Jellyfin) und eine remote-verwaltete
(`cloudflared-pingvin`, Ingress liegt im Cloudflare-Dashboard, lokal nicht
einsehbar) – Letztere trägt `dashboard.`/`git.steggi-matrix.work`. Wer nur die
lokale YAML liest, hält die Subdomains fälschlich für tot. (3) Der
Header-Check allein würde nicht reichen (Header sind von Direktverbindern
spoofbar/wegzulassen) – die Socket-IP ist die belastbare Wahrheit, der
Cf-Header-Check ist nur die zweite Schranke, falls der Tunnel je im
Host-Netz läuft.

---

## 2026-07-04 – Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard

**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist –
zwei neue Punkte (n)/(o) im Fable-5-Review-Abschnitt abgehakt (Details dort).
- **(n):** Beim Gegenlesen von `server.py` gefunden, dass die geplante
  LiveKit-Token-Route `POST /api/livekit-token` von einer vorher abgebrochenen
  (unprotokollierten) Session **bereits live gebaut** wurde. Kritisch geprüft und
  headless verifiziert: Auth per whoami greift (ungültig→401), Body-Limit (413),
  gültig→200; das JWT unabhängig dekodiert und die **Signatur gegen `livekit.yaml`
  gegengerechnet – gültig**; Identität = geprüfte Matrix-user_id (kein Spoofing).
  Route ist gefahrlos additiv. Server-Seite des M0-LiveKit-Punkts damit erledigt;
  Client-Umstellung + Rotation bleiben PC/Gerät (heiliger Call-Pfad).
- **(o) KRITISCH:** Derselbe Dashboard-Server hat für seine **Admin-Endpoints
  (`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`,
  `/api/run-stats`) KEINE Authentifizierung** und ist über
  `https://dashboard.steggi-matrix.work` **öffentlich erreichbar**. Belegt per
  `curl` (App-eigene 400-Antwort über die öffentliche URL, kein Login davor).
  Ein Fremder könnte damit Uta (jeden Nutzer) sperren oder die offene
  Registrierung am Homeserver aktivieren. Der Hostname ist über CT-Logs auffindbar.

**Offen/Nächster Schritt:** **Bernd muss entscheiden, WIE das Dashboard künftig
authentifiziert** – Weg A (Cloudflare Access vor die Subdomain, kein Code, kein
Aussperr-Risiko, empfohlen) oder Weg B (eigener `DASH_TOKEN` + `stats.html`-Prompt).
Fertiger Plan mit beiden Wegen: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach sofort
umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede Auth-Ergänzung
Bernds eigenes Admin-Panel bis zur Token-Eingabe lahmlegt („kein Aussperren") –
das mitten in einer autonomen Session zu deployen ist die falsche Reihenfolge.

**Stolperfallen:** Ein Review, der sich auf den *geänderten* Endpoint (Diagnose,
Disk-Fill) konzentriert, übersieht leicht die *unveränderten Nachbarn* in
derselben Datei. Lehre: bei einem Sicherheits-Review einer Datei nicht nur den
Diff prüfen, sondern die ganze Angriffsfläche der Datei (hier: alle Routen des
`do_POST`-Handlers auf Auth abklopfen, nicht nur die neu berührte).

---

## 2026-07-04 – Fable-5-Review (l)+(m): Diagnose-Endpoint hatte kein Größenlimit (gefixt)

**Erledigt:** Review-Pass über die zwei seit (k) dazugekommenen Arbeiten – als
Punkte (l) und (m) im Fable-5-Review-Abschnitt abgehakt (Details dort). Bei (l)
ein **echter Befund** am Server-Gegenpart des Admin-Token-Fixes: Der neue
`DIAG_TOKEN` steht bewusst im öffentlichen Repo (eng begrenzt, nur Log-Anhängen) –
aber `/api/e2ee-diagnostics` in `/home/steggi/matrix/server.py` las den
Request-Body **ohne Größenlimit** und hängte ihn unbegrenzt an die Log-Datei an.
Jeder Fremde hätte damit die Pi-Platte volllaufen lassen können (Disk-Fill-DoS
auf dem Host, der auch den Homeserver trägt). Gefixt in `server.py`:
Body-Limit 256 KB pro Upload (413 bei Überschreitung) + harte Obergrenze 20 MB
für die Log-Datei (ältere Hälfte fliegt raus). Headless verifiziert nach
Dienst-Neustart (kill, `Restart=always`): stats.html 200, gültiger Upload 200
und im Log gelandet, falscher Token 403, 300-KB-Body 413; Kappungslogik isoliert
getestet (100 Zeilen → 50, neueste überleben). Test-Einträge wieder entfernt.

**Offen/Nächster Schritt:** Weiter mit dem obersten offenen ROADMAP-Punkt
(M0 LiveKit-Token): Server-Route nach `docs/LIVEKIT_TOKEN_MIGRATION.md` bauen –
rein additiv, Client bleibt unangetastet (heiliger Call-Pfad wartet auf PC/Gerät).

**Stolperfallen:** Ein „eng begrenzter" Token im öffentlichen Repo ist nur dann
wirklich harmlos, wenn das Endpoint dahinter auch Missbrauch begrenzt
(Größe/Menge) – „keine Admin-Rechte" allein reicht nicht. Bei künftigen
Endpoints mit öffentlich bekanntem Token immer an Body-/Speicher-Limits denken.

---

## 2026-07-04 – LiveKit-Token-Minting: Migrationsplan geschrieben (Prep, kein Code)

**Erledigt:** Für den offenen M0-Punkt „LiveKit-Token server-seitig minten"
(letzter Client-eingebetteter Secret nach der Diagnose-Entkopplung) den fertigen
Umsetzungsplan `docs/LIVEKIT_TOKEN_MIGRATION.md` geschrieben – gegen den echten
Client-Code (`livekit_token.dart`, `livekit_call_manager.dart:154`) und die echte
Pi-Konfig (`livekit.yaml`, `server.py`, Continuwuity `whoami`). Kern: Token-Route
`POST /api/livekit-token` am bestehenden Dashboard-Server, die den Matrix-Access-Token
per `whoami` prüft und daraus die LiveKit-Identität ableitet (kein Spoofing), JWT
per stdlib-HMAC mintet (kein neues Paket); Client holt nur noch das fertige JWT und
`apiSecret` verschwindet aus dem App-Code. Analog zum bewährten
`SQLCIPHER_MIGRATION.md`-Muster.

**Offen/Nächster Schritt:** Umsetzung braucht echten Call-Test auf einem Gerät
(Calls „heilig", auf dem Pi kein GUI) – daher bewusst nur der Plan, kein Code.
Danach Secret-Rotation zusammen mit einem Release. Verbleibende M0-Secrets
(Admin-Token, Gitea-, Giphy-Token) brauchen Bernds Konten-Zugriff.

**Stolperfallen:** Keine – reine Doku.

---

## 2026-07-04 – Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)

**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
ihn nur zu dokumentieren – und zwar sicher, weil diese Session direkt auf dem Pi
läuft, der den Dashboard-Server hostet (frühere Sessions kannten den Server-Code
nicht). Kernursache gefunden: `/home/steggi/matrix/server.py` benutzte für das
Diagnose-Endpoint `/api/e2ee-diagnostics` **denselben String** wie den
Matrix-Admin-Token (`DIAG_TOKEN = TOKEN`). Deshalb musste der Client diesen Token
mitliefern – d. h. jeder App-Nutzer (auch Uta) trug den vollen Server-Admin-Token
im Gerät.

- **Server (`server.py`):** `DIAG_TOKEN` ist jetzt ein eigener, zufälliger,
  eng begrenzter Token (`pyr-diag-…`), unabhängig vom Admin-`TOKEN`. Das
  Diagnose-Endpoint hängt nur an den Log an (keine Admin-Rechte) – der neue Token
  ist also von geringer Sensibilität. Der Admin-`TOKEN` (für Ban/Invite/
  Registrierung) bleibt **unverändert**, damit das Dashboard weiterläuft.
- **Client (`settings_encryption.dart`):** `_uploadDiagnostics` schickt jetzt den
  neuen Diagnose-Token statt des Admin-Tokens. **`grep J5lax lib/` ist jetzt leer**
  – der Admin-Token ist raus aus dem App-Code.
- **Verifiziert (headless, kein GUI nötig):** Dashboard-Server nach Neustart
  (Prozess gekillt, systemd `Restart=always` lud den neuen Code): `stats.html` 200,
  `registration-status` 200, `/api/e2ee-diagnostics` mit neuem Token **200**, mit
  altem Admin-Token **403**. `flutter analyze` unverändert (1 bekannter Hinweis),
  `flutter test` 24/24 grün. Meine Test-Einträge aus dem Diagnose-Log wieder
  entfernt.

**Offen/Nächster Schritt:** Die **Rotation des Admin-Tokens `J5lax…` bleibt
Pflicht und Bernds Sache** – der Token ist weiterhin gültig und steht in der
Git-History sowie in Utas bereits installierter APK. Erst nach der Rotation ist
der Leak wirklich wertlos. Ebenso offen: LiveKit-`apiSecret` (server-seitiges
Minting), Gitea- und Giphy-Token (siehe ROADMAP M0). **Wichtig für den nächsten
PC-Lauf / das nächste Release:** Utas aktuell installierte App schickt beim
Diagnose-Upload noch den alten Token → dieser Button liefert bei ihr jetzt 403,
bis sie ein neues Release bekommt (nur die Diagnose-Funktion betroffen, nichts
Heiliges). Der Diagnose-Upload-Button gehört damit in den PC-Testplan (mit neuem
Build einmal drücken, Log auf dem Pi prüfen).

**Stolperfallen:** `systemctl restart matrix-stats.service` scheiterte an
fehlender polkit-Auth (kein Terminal für sudo). Lösung ohne root: den von systemd
verwalteten Prozess killen – `Restart=always`/`RestartSec=5` lädt server.py neu
(neue MainPID bestätigt). Nur anwendbar, weil der Dienst als User `steggi` läuft
und der eigene Prozess killbar ist.

---

## 2026-07-04 – Fable-5-Review (k): Secret-Scan – ECHTER Sicherheitsbefund, teils gefixt

**Erledigt:** Beim Review-Pass zusätzlich einen systematischen Secret-Scan über
`lib/`, `scripts/`, `docs/`, `android/` gemacht (CLAUDE.md-Leitplanke „Keine
Secrets ins Repo"). **Ergebnis: mehrere echte, aktive Zugangsdaten liegen im
Repo – und das Gitea-Repo ist als `private:false` unter der Internet-Domain
`git.steggi-matrix.work` erreichbar** (per `curl` bestätigt: Repo-Metadaten und
Releases anonym abrufbar). Damit sind alle folgenden Secrets als **kompromittiert
zu behandeln** (seit Commit 25ed765 in der History):

- **Matrix-Server-ADMIN-Token** `J5lax…` – lag doppelt drin:
  (1) als Bearer-Token in `docs/matrix-sdk/13-server-admin-continuwuity.md`
  (19 Stellen) und (2) **im ausgelieferten App-Code**
  `lib/widgets/settings/settings_encryption.dart:431` (E2EE-Diagnose-Upload).
  Per `whoami` gegen den Homeserver geprüft: **Token ist noch gültig**
  (`@todesneutron:steggi-matrix.work`, voller Server-Admin). Kritischster Fund –
  jeder App-Nutzer (auch Uta) trägt den Server-Admin-Token im Client.
- **Gitea-Release-Token** `bb522f96…` (Repo-Schreibrechte) hartcodiert in
  `scripts/release.ps1:14`.
- **LiveKit-`apiSecret`** `rYUT2PRa…` in `lib/core/livekit_token.dart:6` – die
  App mintet LiveKit-JWTs **client-seitig**, das SFU-Secret muss also im Client
  liegen (Architekturproblem, nicht nur ein Repo-Leak).
- **Giphy-API-Key** `QtEy…` in `lib/features/chat/gif_sticker_picker.dart:9`
  (geringe Sensibilität, aber ebenfalls im Client).

**Sofort (gefahrlos, kein Laufzeit-Bezug) bereinigt:**
- `scripts/release.ps1`: Token raus, kommt jetzt aus Umgebungsvariable
  `PYRAMID_GITEA_TOKEN`; fehlt sie, bricht das Skript mit klarer Anleitung ab.
- `docs/matrix-sdk/13-server-admin-continuwuity.md`: alle 19 Token-Stellen durch
  `<ADMIN_TOKEN>`-Platzhalter ersetzt.

**Offen/Nächster Schritt (BRAUCHT BERND / PC – nicht blind auf dem Pi fixen):**
1. **Alle vier Secrets rotieren** (sie sind bereits geleakt, egal was im Code
   steht): neuen Matrix-Admin-Token erzeugen und alten widerrufen; neuen
   Gitea-Token; LiveKit-`apiKey`/`apiSecret` neu setzen (Hetzner-LiveKit +
   `ice.json`); Giphy-Key neu ziehen. **Das ist der wichtigste Schritt** – die
   Repo-Bereinigung allein nützt nichts, solange die alten Werte gültig sind.
2. **Client-seitige Secrets entschärfen (Architektur):** LiveKit-Token gehört
   server-seitig gemintet (kleiner Token-Endpoint auf dem Pi, App holt sich nur
   das fertige JWT) – dann verschwindet `apiSecret` aus dem Client. Der
   E2EE-Diagnose-Upload darf **niemals** den Server-Admin-Token benutzen: eigenen,
   eng begrenzten Token (nur der Dashboard-Endpoint) oder besser einen
   unauthentifizierten Upload mit Rate-Limit. Beides als ROADMAP-M0-Punkt neu
   aufgenommen. Blind entfernen verbietet sich, weil Calls/Diagnose sonst brechen
   (Calls sind „heilig") und auf dem Pi kein GUI-Test möglich ist.
3. **Git-History:** Die alten Werte bleiben in der Gitea-History sichtbar. Nach
   der Rotation sind sie wertlos; ein History-Rewrite (force-push) ist optional
   und Bernds Entscheidung (das Repo dient auch als Backup) – daher NICHT
   eigenmächtig gemacht.

**Stolperfallen:** Der Diagnose-„Token" in `settings_encryption.dart` ist
NICHT ein harmloser Dashboard-Key, sondern derselbe String wie der
Matrix-Server-Admin-Token – beim Lesen leicht zu übersehen, weil er dort nur
`token:` heißt. Lehre: bei Secret-Funden immer prüfen, ob derselbe Wert an
mehreren Stellen (Doku UND Client) wiederkehrt und WAS er wirklich autorisiert.

---

## 2026-07-03 – Fable-5-Review (h)–(j): Test-/Doku-Commits geprüft, 1 echter Befund gefixt

**Erledigt:**
- **Review-Pass über die drei seit (g) dazugekommenen Commits** – als Punkte
  (h)–(j) im Fable-5-Review-Abschnitt abgehakt (Details dort): AuthLog-Tests
  e0ac5cb korrekt (h), PC-Testplan-Update 03b84e0 korrekt (j), und bei den
  settings_prefs-Tests 7dfdbf1 (i) beim Gegenlesen des Produktivcodes ein
  **echter Befund**: Lade-Race in `BoolPref`/`StringSetPref` –
  `StringSetPref.add()` vor abgeschlossenem `_load` konnte **bereits
  persistierte Einträge löschen** (Read-Modify-Write auf ungeladenem State;
  per Regressionstest am alten Code bewiesen, dann gefixt: 7b90f20).
  Vorgehen: Test zuerst geschrieben, Fehlschlag am alten Code gesehen, dann
  Fix, dann alles grün – kein blinder Fix.
- Unabhängige Verifikation: `flutter test` jetzt **24/24 grün auf dem Pi**
  (2 neue Lade-Race-Tests), `flutter analyze` unverändert 1 bekannter Hinweis
  (`chat_provider.dart:42`). PC-Testplan Schritt 0 auf 24 Tests nachgezogen.

**Offen/Nächster Schritt:** Unverändert blockiert – alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität), siehe
`docs/PC_TESTPLAN.md` und „Fragen an Bernd" weiter unten. Der Lade-Race-Fix
ist headless voll getestet, gehört aber der Vollständigkeit halber mit in den
nächsten PC-Klickdurchlauf (Einstellungs-Toggles, Space-Beitritt).

**Stolperfallen:** Der BoolPref-Teil des Race ist im Test-Mock NICHT
reproduzierbar (Microtask-Reihenfolge dort zufällig gutartig: `set()`
persistiert vor dem `_load`-Read) – nur der StringSetPref-Teil schlug real
fehl. Lehre: Bei Race-Befunden nicht vom „Test wird schon rot"-Reflex leiten
lassen, sondern die konkrete Microtask-Reihenfolge durchdenken; ein Guard kann
richtig sein, auch wenn der Test ihn in dieser Umgebung nicht erzwingen kann
(dokumentiert im Review-Punkt (i)).

---

## 2026-07-03 – Review-Nachzügler abgeschlossen + AuthLog-Regressionstests

**Erledigt:**
- **Fable-5-Review vervollständigt (eebf8ae):** Die drei seit Review-Abschluss
  dazugekommenen erledigten Arbeiten kritisch geprüft und als Punkte (e)–(g) im
  Review-Abschnitt abgehakt: Unit-Tests 99cde9a (korrekt, Fake-Clients schlagen
  bei jedem Fremd-Aufruf laut fehl), Doku-Commits 16561bb/566938f (alle
  Behauptungen gegen Code/pubspec/matrix-6.2.0 verifiziert – Factory-Stellen,
  Tabellenname `box_inbound_group_session`), Autopilot-Konto-Wechsel bfe5876
  (kein App-Code, Logik in Ordnung; harmloser False-Positive der
  Limit-Erkennung dokumentiert). **Kein Befund, keine Code-Änderung nötig.**
  Damit sind ALLE als erledigt markierten Punkte reviewt.
- **`test/auth_log_test.dart` (4 Tests):** `AuthLog` ist die einzige weitere
  öffentliche Schnittstelle auf dem heiligen Pfad, die headless testbar ist
  (der Soft-Logout-Guard ruft `AuthLog.write` im catch-Pfad auf – würde es
  werfen, eskalierte das SDK zu `logout()`+`clear()`). Getestet: write/read
  werfen NIE, auch wenn die Plattform fehlt (kaputter PathProvider);
  Platzhalter statt Fehler bei leerem Log; ISO-Zeitstempel pro Zeile;
  500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
  Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt –
  offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
  Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
  unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
  ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
  Pflicht-Schnittstelle ausgebaut werden soll – dieses Vertragsnetz muss beim
  M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
  (`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
  (persistierter Wert überstimmt Default, set/toggle persistieren),
  `StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
  Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
  Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
  leere Map statt Crash** – echter Fehlerpfad). `flutter test`: **alle
  22 Tests grün auf dem Pi.**

**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) – siehe
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
oder braucht Widget-Tests mit gemocktem Matrix-Client – lohnt erst nach dem
M2-Modulschnitt mit echten öffentlichen Schnittstellen).

**Stolperfallen:** `AuthLog` cached sein File-Handle statisch – der Test für
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
laufen (im Testfile kommentiert). Bei künftigen Tests an Singletons mit
statischem Zustand auf Testreihenfolge/Isolate-Grenzen achten (flutter test
isoliert pro Datei, nicht pro Test).

---

## 2026-07-03 – Erste Unit-Tests: Regressionsnetz für die zwei heiligsten Codepfade

**Erledigt:** Eine Vorsession hatte (unprotokolliert, Abbruch vor dem Testlauf –
sichtbar nur am untrackten `test/`-Ordner + CHANGES.md-Hook-Log 18:16/18:17)
zwei Testdateien angelegt. Diese Session hat sie gegen den echten Code
gegengelesen, ausgeführt und committet (99cde9a) – damit hat das Projekt erstmals
überhaupt eine Testsuite, und zwar genau dort, wo CLAUDE.md „heilig" sagt:

- `test/soft_logout_guard_test.dart` (4 Tests): `guardedSoftLogoutRefresh`
  wirft NIE nach außen (weder Exception noch Error) – ein Throw würde im SDK
  zu `logout()`+`clear()` eskalieren (der Uta-Bug). Geprüft: Erfolg beim
  1. Versuch = genau 1 Aufruf, transienter Fehler wird per Retry geheilt,
  dauerhafter Fehler = genau 3 Versuche + normale Rückkehr. Der Fake-Client
  schlägt bei JEDEM anderen SDK-Aufruf laut fehl – riefe der Guard je
  `logout()`/`clear()` auf, fiele der Test sofort um.
- `test/power_levels_safety_test.dart` (6 Tests): `updatePowerLevelsSafely`
  (Selbst-Aussperr-Schutz im Space-Admin) – Server-Stand bleibt erhalten
  (fremde users-Einträge überleben; genau der Bug, der schon mal Admins
  ausgesperrt hat), Selbst-Degradierung bricht ab ohne zu schreiben,
  users_default-Fallback, explizites `events[m.room.power_levels]`-
  Erfordernis, M_NOT_FOUND-Start mit leerem Event, fremde Serverfehler
  (M_FORBIDDEN) werden durchgereicht.
- `flutter test`: **alle 10 Tests grün auf dem Pi** (läuft headless, im
  Gegensatz zu `flutter run` – d. h. dieser Punkt ist NICHT „UNGETESTET (Pi)").
  `flutter analyze` unverändert 1 bekannter Hinweis (`chat_provider.dart:42`).
- Wert fürs Refactoring: Beide Funktionen sind Vertragsgrenzen, die bei
  M2-Umbauten (Call-Fassade, Storage) nicht wackeln dürfen – künftige
  Sessions sollten `flutter test` fest in Schritt 3 des Arbeitszyklus
  aufnehmen (geht auch auf dem Pi).

**Offen/Nächster Schritt:** Unverändert: PC-Termin (docs/PC_TESTPLAN.md) oder
Bernds Antworten (Call-Pilot, SQLCipher-Priorität). Weitere Test-Kandidaten
scheitern derzeit an der Sichtbarkeit: `_detectType`, `_formatMessageTime`,
`_compareSpaceChildren`, die Megolm-Export-Kryptohelfer usw. sind alle
bibliotheksprivat – testbar erst, wenn beim M2-Modulschnitt echte öffentliche
Schnittstellen entstehen (dann Tests gleich mitschreiben).

**Stolperfallen:** Die Vorsession wurde mitten im Schritt abgebrochen, OHNE
den Zwischenstand zu protokollieren (Pflicht laut CLAUDE.md) – der untrackte
`test/`-Ordner war nur per `git status` auffindbar. Lehre: `git status` am
Sessionstart ernst nehmen; untrackte Dateien können unprotokollierte,
halbfertige Arbeit einer abgebrochenen Session sein.

---

## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan

**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
obersten offenen ROADMAP-Punkte (SQLCipher, Härtetest) so weit gebracht, wie es
auf dem Pi ohne GUI/Testgerät sicher geht – beide bleiben offen, aber der
nächste PC-Termin kann jetzt direkt loslegen statt zu planen:

- `docs/SQLCIPHER_MIGRATION.md`: vollständiger Umsetzungsplan, gegen den echten
  Code geschrieben (beide DB-Öffnungsstellen, Factory-Wahl pro Plattform).
  Kernpunkte: gemeinsame DB-Fassade `app_database.dart` (zahlt aufs
  M2-Storage-Modul ein), Android via `databaseFactoryFfi` +
  `openCipherOnAndroid`, im Push-Isolate zwingend `databaseFactoryFfiNoIsolate`
  (dieselbe Isolate-Falle, an der schon `NativeImplementationsIsolate`
  scheiterte), Schlüssel im Android Keystore (`flutter_secure_storage`, schon
  Abhängigkeit), Migration per `sqlcipher_export` in NEUE Datei mit
  Backup/Verifikation/atomarem Tausch – kein Fehlerpfad darf die Klartext-DB
  beschädigen. Marker-Datei gegen das Migration/Push-Race. Phase 1 nur
  Android (sqlcipher_flutter_libs liefert keine Windows-Binaries).
  Tabellenname für die Verifikation gegen matrix-6.2.0 geprüft
  (`box_inbound_group_session`).
- `docs/PC_TESTPLAN.md`: ALLE aufgelaufenen UNGETESTET(Pi)-Punkte aus den
  bisherigen PROGRESS-Einträgen an einer Stelle konsolidiert (Härtetest/
  Krypto-Kreislauf zuerst, dann die 6 Split-Bereiche, Reorder-Off-by-One-Check,
  PDF-Zoom, Push-Beobachtung, „Uta einmal neu einloggen"). Bisher waren die
  über 7 Einträge verstreut – Gefahr, dass der PC-Lauf etwas übersieht.

**Offen/Nächster Schritt:** Unverändert: PC-Termin (jetzt mit fertigem
Testplan + Migrationsplan) oder Bernds Antworten (Call-Pilot, SQLCipher-
Priorität). Auf dem Pi ist damit alles abgearbeitet, was ohne GUI/Entscheidung
sicher geht – inklusive des kompletten Qualitäts-Reviews.

**Stolperfallen:** Keine neuen – reine Dokumentation, kein Code angefasst.

---

## Fable-5-Review (Qualitäts-Review aller Autopilot-Commits 25ed765..HEAD)

Bernds Auftrag (ROADMAP M0): alle bisherigen Sonnet-5-Commits kritisch und in
der Tiefe nachprüfen. Jeder Punkt hier wird abgehakt, sobald er geprüft ist –
**bereits abgehakte Punkte in künftigen Sessions NICHT erneut prüfen.**

Hinweis zur Historie: Eine frühere Fable-5-Session hatte den Review begonnen
und zwei Fix-Commits gepusht (f9979e4, 891f348), wurde aber VOR dem
Protokollieren abgebrochen – dieser Abschnitt holt das Protokoll nach und
schließt den Review ab.

- [x] **(a) Die 6 Gott-Datei-Aufteilungen fachlich geprüft – Ergebnis: korrekt,
  1 Befund bereits gefixt (f9979e4).** Unabhängige Nachprüfung (nicht nur den
  alten Blockvergleich wiederholt): eigener Zeilen-Multiset-Vergleich
  (Code-Zeilen ohne Kommentare/Imports/part-Direktiven) Original vor dem Split
  vs. heutiger Stand für alle 6 Bibliotheken → **keine Code-Zeile verloren,
  keine hinzugekommen** (settings_modal 5055, message_group 2409, rooms_panel
  2308, space_admin_dialog 2079, chat_view 1813, document_viewer 1543 Zeilen,
  jeweils exakt identisch). Alle `part`-Direktiven == Dateien auf Platte,
  alle Part-Dateien mit korrektem `part of`. Befund der Vorsession bestätigt:
  Beim Split gingen 24 **Erklärkommentar-Blöcke** verloren (u. a. die
  Selbst-Aussperr-Warnung über `updatePowerLevelsSafely`, HTML-Subset-Doku,
  PDF-Cache-Doku) – mit f9979e4 an den richtigen Klassen wiederhergestellt
  (Stichprobe geprüft: sitzt korrekt). `flutter analyze`: unverändert 1
  bekannter Hinweis (`chat_provider.dart:42`).
- [x] **(b) Uta-Logout-/Push-Fix (63e4919, 9dc83f7) gegen das matrix-SDK 6.2.0
  verifiziert – Analyse war richtig, aber unvollständig; Lücke mit 891f348
  geschlossen (Vorsession), heute vollständig gegenverifiziert:**
  - SDK-Quelle bestätigt (client.dart:2374 ff.): Ohne eigenen `onSoftLogout`-
    Handler eskaliert JEDER fehlgeschlagene Token-Refresh (auch bloße
    Netzwerkfehler) zu `logout()` + `clear()` → Session inkl. Krypto-Schlüssel
    weg. Mit `refreshToken: true` (9dc83f7) wären Refreshes Routine geworden –
    das Restrisiko wäre also GESTIEGEN. Der Guard (`soft_logout_guard.dart`,
    891f348) ist die notwendige zweite Hälfte des Fixes.
  - Guard-Code geprüft: wirft nie (alle Pfade gefangen; `AuthLog.write` fängt
    intern selbst alles – wichtig, sonst würde ein Logging-Fehler im
    catch-Pfad doch wieder `logout()` auslösen). SDK ruft bei nicht-werfendem
    Handler nachweislich nie `logout()` auf.
  - „Retry heilt das Token-Rotations-Race mit dem Push-Isolate"-Behauptung
    gegen SDK-Quelle geprüft: stimmt – `refreshAccessToken()` liest den
    Refresh-Token bei jedem Aufruf frisch per `database.getClient()` aus der
    geteilten DB.
  - Beide `Client(`-Konstruktionen der App (matrix_client.dart,
    background_push.dart) haben den Guard – keine ungeschützte übrig.
  - `isLoggedInProvider`-Änderung (softLoggedOut ≠ ausgeloggt) korrekt:
    verhindert Login-Screen-Flackern bei Routine-Refresh; echtes `loggedOut`
    führt weiterhin zum Login-Screen.
  - **Dokumentiertes Restrisiko (kein Fix möglich/nötig):** Ein hartes
    `M_UNKNOWN_TOKEN` OHNE `soft_logout`-Flag führt im SDK weiterhin direkt zu
    `clear()`. Das ist ein echter serverseitiger Session-Widerruf (z. B.
    Admin-Logout) – lokal nicht abwendbar; Schlüssel kommen dann übers
    Key-Backup zurück. Kein stiller Fehlerpfad, sondern legitimer Server-Befehl.
- [x] **(c) analyze-/Lint-Fixes (9dc1175, 133a8ac, 99f10c5, 2395db5, 057ac1e,
  401bd6a) auf Verhaltensänderungen geprüft – alle äquivalent, 1 Kleinigkeit
  gefunden (bewusst belassen):**
  - `onReorder` → `onReorderItem` (401bd6a): gegen Flutter-SDK-Quelle
    verifiziert – der neue Callback korrigiert den Index intern, das entfernte
    manuelle `newIdx--` ist korrekt; einzige Aufrufstelle migriert. Kein
    Off-by-One beim Raum-Umsortieren.
  - `Matrix4.translate/scale` → `translateByDouble/scaleByDouble` (PDF-Zoom):
    mathematisch äquivalent (uniforme Skalierung, w=1).
  - `withOpacity`→`withValues`, `activeColor`→`activeThumbColor`,
    `cacheExtent`→`scrollCacheExtent`, `PublicRoomsChunk`→`PublishedRoomsChunk`,
    print→debugPrint, `__`→`_`-Parameter, Curly-Braces: reine Renames/Syntax.
  - Toter Code (9dc1175): entfernte Funktionen (`_showNotif`, `_fetchTitle`,
    `_get`, `_applyNoiseSuppression`, `destructive`-Param, `_saved`-Feld) per
    grep bestätigt nirgends referenziert.
  - **Kleinigkeit:** In `mini_call_widget.dart` wurde das ungenutzte
    `_hovered`-Feld nicht gelöscht, sondern verdrahtet (Hover-Farbe auf
    Call-Buttons) – streng genommen eine Mini-Verhaltensänderung in einem
    Lint-Commit. Offensichtlich die ursprünglich gemeinte Funktion des Feldes,
    rein kosmetisch, kein Risiko → belassen, hier nur dokumentiert.
- [x] **(d) Abgehakte ROADMAP-Punkte gegen Code gegengeprüft – alle stimmen:**
  `isKeyBackupMissing`-Warnung sitzt nach dem Settings-Split weiterhin in
  beiden Logout-Pfaden (settings_about.dart, settings_shared.dart);
  `refreshToken: true` in login_notifier.dart:84; Bootstrap-Flows
  (`askUnlockSsss`, `_triggerBootstrap`) vorhanden; `features/calls/`-Leiche
  existiert nicht; `docs/M2_MODULE_SCHNITT.md` vorhanden; `flutter analyze`
  sauber (1 bekannter Hinweis). Nebenbefund gefixt: `docs/PROGRESS.md` war ein
  eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
  Protokoll im Root → Archiv-Banner eingefügt.

- [x] **(e) Unit-Tests (99cde9a) gegengelesen – korrekt, keine Änderung nötig.**
  Beide Fake-Clients schlagen bei jedem nicht implementierten SDK-Aufruf laut
  fehl (NoSuchMethodError) – ein versehentlicher `logout()`/`clear()`-Aufruf im
  Guard würde sofort auffallen. `soft_logout_guard_test`: deckt Erfolg,
  transienten Fehler (Retry), dauerhaften Fehler (genau 3 Versuche, kein Throw)
  und `Error` (nicht nur `Exception`) ab – passt zum `catch (e)` im Guard, das
  bewusst alles fängt. Dass die Tests grün sind, beweist nebenbei, dass
  `AuthLog.write` ohne path_provider-Plattform (Testumgebung) sauber schluckt
  statt zu werfen – genau die Eigenschaft, an der der Guard hängt.
  `power_levels_safety_test`: alle 6 Fälle gegen den echten Code
  (`space_admin_core.dart:10-56`) nachvollzogen, inkl. der wichtigen Kante
  „explizites `events[m.room.power_levels]` schlägt `state_default`".
  Einzige Anmerkung: die zwei Retry-Tests schlafen real 2s+4s (Suite ~14s) –
  bewusst so gelassen, statt für Testbarkeit einen Delay-Parameter in den
  heiligen Produktivpfad zu bohren.
- [x] **(f) Doku-Commits 16561bb/566938f (SQLCipher-Plan, PC-Testplan) gegen
  Code verifiziert – Behauptungen stimmen:** Factory-Stellen exakt wie
  dokumentiert (`matrix_client.dart:24` nativ Android/iOS, `:27` FFI Desktop;
  `background_push.dart:188` nativ), `sqlcipher_flutter_libs ^0.6.8` +
  `flutter_secure_storage ^10` in pubspec vorhanden/ungenutzt wie beschrieben,
  Verifikations-Tabellenname `box_inbound_group_session` steht wirklich in
  matrix-6.2.0 `matrix_sdk_database.dart:139`, beide Prozesse öffnen dieselbe
  `pyramid.sqlite` mit WAL + busy_timeout 5000. PC-Testplan deckt alle in
  PROGRESS verstreuten UNGETESTET-Punkte ab (stichprobenartig gegengeprüft).
- [x] **(g) Autopilot-Konto-Wechsel (bfe5876, dazu 73420fe) geprüft – kein
  App-Code, funktional in Ordnung:** Wechsel-Logik (pyramid↔haupt, `JUST_SWITCHED`
  verhindert Endlos-Ping-Pong, 20-min-Wartepause wenn beide Konten am Limit),
  `CLAUDE_CONFIG_DIR` wird vor jeder Session neu gesetzt, Matrix-Token kommt aus
  `~/gatus/.env` (nicht im Repo). Einzige Anmerkung (belassen): die
  Limit-Erkennung greppt die GESAMTE Session-Ausgabe auf „usage limit" u. ä. –
  zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
  unnötigen (aber harmlosen) Konto-Wechsel.

- [x] **(h) AuthLog-Tests (e0ac5cb) geprüft – korrekt, keine Änderung nötig.**
  Alle 4 Tests gegen `core/auth_log.dart` nachvollzogen: Der Kaputt-Plattform-
  Test läuft korrekt VOR den Tests mit funktionierender Plattform (das statische
  `_file`-Handle wird beim Throw nie befüllt, daher kein vergifteter Cache);
  Kappungs-Test rechnet richtig (521. Zeile triggert Kappung auf 500, „Hallo
  Log" aus dem Vortest fliegt raus). pubspec-Ergänzung sauber (offizielles
  `path_provider_platform_interface`, war transitiv im Lockfile). Unabhängig
  verifiziert: `flutter test` 22/22 grün, `flutter analyze` 1 bekannter
  Hinweis. Anmerkung (belassen): Die Kappung schreibt die Datei nicht-atomar
  neu und ist nicht gegen parallele Schreiber aus dem Push-Isolate gesperrt –
  für ein reines Diagnose-Log akzeptabel, kein Fix nötig.
- [x] **(i) settings_prefs-Tests (7dfdbf1) geprüft – Tests korrekt, aber beim
  Gegenlesen des Produktivcodes 1 echter Befund gefunden und gefixt (7b90f20):
  Lade-Race in `BoolPref`/`StringSetPref`.** `StringSetPref.add/remove` machten
  Read-Modify-Write auf möglicherweise noch ungeladenem State – ein `add()`
  direkt nach Erstellung persistierte nur den neuen Eintrag und LÖSCHTE alle
  bereits gespeicherten (per neuem Regressionstest am alten Code bewiesen:
  `['!a:hs']` im Store, `add('!b:hs')` → Store enthielt nur noch `['!b:hs']`).
  Fix: `add`/`remove` warten jetzt auf `_loaded`. `BoolPref` zusätzlich mit
  `_userSet`-Guard, damit ein spät landender persistierter Wert nie eine schon
  getätigte Nutzeraktion zurücküberschreibt (die Microtask-Reihenfolge ist
  plattform-/versionsabhängig – im Test-Mock zufällig gutartig). Einordnung:
  in der heutigen App kaum treffbar (die Provider werden beim App-Start geladen,
  lange bevor jemand klickt – deshalb nie aufgefallen), aber die Fassade wird
  laut `docs/M2_MODULE_SCHNITT.md` zur Pflicht-Schnittstelle für neue, ggf.
  früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün auf dem Pi.
- [x] **(j) PC-Testplan-Update (03b84e0) geprüft – stimmt.** Teststand „22
  Tests" war korrekt (durch (i) jetzt 24 – im Testplan nachgezogen, siehe
  Eintrag oben).
- [x] **(k) Secret-Scan über das ganze Repo – ECHTER Sicherheitsbefund
  (2026-07-04).** Vier aktive Zugangsdaten im öffentlich erreichbaren Repo
  (Matrix-Admin-Token doppelt – auch im Client!, Gitea-Release-Token,
  LiveKit-`apiSecret`, Giphy-Key). Gefahrlose Stellen sofort bereinigt
  (release.ps1 → Env-Var, Admin-Doku → Platzhalter); Rotation aller Secrets +
  server-seitiges LiveKit-Token-Minting + nicht-Admin-Diagnose-Token als neue
  M0-Punkte aufgenommen (brauchen Bernd/Server-Zugriff). Details im
  PROGRESS-Eintrag 2026-07-04 oben.
- [x] **(l) Admin-Token-Entkopplung (925aafb) geprüft – Client-Seite korrekt,
  am Server-Gegenpart 1 echter Befund gefunden und gefixt (2026-07-04).**
  Client-Diff verifiziert: neuer `pyr-diag-…`-Token stimmt mit `DIAG_TOKEN` in
  `server.py` überein, `grep J5lax lib/ scripts/` unabhängig nachgeprüft (leer;
  einziges verbliebenes Client-Secret ist das LiveKit-`apiSecret`, eigener
  M0-Punkt). Befund: `/api/e2ee-diagnostics` hatte **kein Body-/Log-Größenlimit**
  bei öffentlich bekanntem Token → Disk-Fill-DoS möglich. Gefixt + headless
  verifiziert (200/403/413, Kappung), Details im PROGRESS-Eintrag 2026-07-04.
- [x] **(m) LiveKit-Migrationsplan (78e4174) gegen Code + Pi-Konfig geprüft –
  Behauptungen stimmen.** `livekit_token.dart:6` enthält das `apiSecret`,
  Aufrufstelle `livekit_call_manager.dart:154` mit `ttlSeconds: 21600`, Grants
  im Plan identisch mit dem Dart-Code (roomJoin/room/canPublish/canSubscribe/
  canPublishData, metadata = Anzeigename, iss = LKMatrixPi, sub = identity),
  `livekit.yaml` `keys:` wie beschrieben, Continuwuity antwortet auf
  `127.0.0.1:6167` (whoami-Basis). Python-Snippet (stdlib-HS256) fachlich
  korrekt (base64url ohne Padding, kompaktes JSON, HMAC-SHA256). Anmerkung:
  Der Plan setzt zusätzlich `nbf`/`name` – LiveKit akzeptiert beides, harmlos.
- [x] **(n) LiveKit-Token-Route ist inzwischen LIVE auf dem Pi gebaut (bisher
  unprotokolliert) – geprüft und funktionsfähig, aber Client noch nicht
  umgestellt (2026-07-04).** Beim Review von `server.py` gefunden: Eine
  Vorsession hat die in `docs/LIVEKIT_TOKEN_MIGRATION.md` geplante Route
  `POST /api/livekit-token` bereits in `server.py` implementiert (whoami-Auth,
  stdlib-HS256-Minting aus `livekit.yaml`) – der CHANGES-Hook zeigt drei
  server.py-Edits um 04:33, Dienst-Neustart 04:34, aber kein PROGRESS-Eintrag
  (Session vor dem Protokoll abgebrochen). Headless verifiziert (127.0.0.1:8080
  UND öffentlich): ungültiger Matrix-Token → 401, fehlende Felder → 400, 5-KB-Body
  → 413 (4-KB-Limit), gültiger Admin-Token → 200; das geminete JWT unabhängig
  dekodiert: iss=LKMatrixPi, sub/name/metadata = geprüfte user_id (kein Spoofing,
  Client kann Identität NICHT wählen), video-Grants wie im Client, exp−nbf =
  21600 s, **Signatur unabhängig gegen `livekit.yaml` gegengerechnet: gültig**.
  Die Route ist gefahrlos additiv (altes Client-Secret bleibt bis zur Rotation
  gültig, nichts bricht). **Offen bleibt:** Client-Umstellung
  (`livekit_token.dart`/`livekit_call_manager.dart`) + Secret-Rotation – beides
  weiterhin PC/Gerät (heiliger Call-Pfad), siehe Migrationsplan. Server-Seite des
  M0-LiveKit-Punkts ist damit erledigt.
- [x] **(o) KRITISCHER Befund beim server.py-Review: die Admin-Endpoints des
  Dashboards haben KEINE Authentifizierung und sind öffentlich erreichbar
  (2026-07-04).** Der Review von (l) hatte nur das Diagnose-Endpoint auf
  Disk-Fill gehärtet – der viel größere Nachbar blieb unbemerkt:
  `https://dashboard.steggi-matrix.work` ist über Cloudflare offen aus dem
  Internet erreichbar, und `POST /api/ban`, `/api/unban`, `/api/toggle-registration`,
  `/api/create-invite`, `/api/run-stats` prüfen **keinen** Token. Belegt:
  `curl -X POST .../api/ban -d '{"user":""}'` liefert die App-eigene 400-Antwort
  `{"error":"Kein Nutzer angegeben"}` – identisch zu 127.0.0.1, also KEIN
  Cloudflare-Login davor. Damit könnte jeder Fremde Uta (jeden Nutzer) sperren
  oder die offene Registrierung am Homeserver aktivieren. Der Hostname ist über
  Certificate-Transparency-Logs praktisch auffindbar – „geheim" trägt nicht.
  **Bewusst NICHT blind gefixt:** Jede Auth-Ergänzung macht Bernds
  Browser-Dashboard funktionslos, bis er einen Token einträgt / Cloudflare Access
  einrichtet – das mitten in einer autonomen Session zu deployen sperrt ihn ohne
  Vorwarnung aus seinem eigenen Admin-Panel („kein Aussperren"). Fertiger,
  gegen den echten Code geschriebener Plan mit zwei Wegen (A: Cloudflare Access =
  Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in
  `docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann
  sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
  Punkt aufgenommen. **Update (später am 2026-07-04):** Das akute Loch ist mit
  einem aussperr-sicheren Interims-Gate geschlossen (Admin-Routen nur noch
  Heimnetz/localhost, siehe PROGRESS-Eintrag „Dashboard-Befund (o)" oben) –
  offen bleibt nur noch die Fernzugriffs-Entscheidung A/B.
- [x] **(p) Interims-Gate (e97a748/455742d) reviewt – korrekt; dabei halbfertige,
  unprotokollierte Threading-Härtung einer abgebrochenen Session gefunden und
  vollendet (2026-07-04).** Gate-Logik bestätigt: Socket-IP ist die belastbare
  Prüfung (nicht spoofbar), Cf-Header nur Zweitschranke, Deny-Log mit 5-MB-Deckel
  und ohne attacker-kontrollierten Text; Restrisiken (LAN-CSRF, offene
  Nutzerlisten-Ansicht) waren bereits in `docs/DASHBOARD_AUTH_HARDENING.md`
  dokumentiert – kein neuer Befund am Gate. ABER: CHANGES-Hook zeigte drei
  server.py-Edits um 09:43 ohne Commit/Protokoll – abgebrochene Session hatte
  `DIAG_LOCK` definiert, aber nie benutzt, und `ThreadingHTTPServer` im Kommentar
  behauptet, aber nicht umgestellt; der Dienst lief zudem noch mit altem Code.
  Vollendet (Threading + Lock-Nutzung), Dienst neu gestartet, 11 Headless-
  Prüfungen grün inkl. Slow-Loris (parallele Anfrage 10 ms statt blockiert;
  hängender Body-Read nach 30 s gekappt). Details im PROGRESS-Eintrag „(p)" oben.
- [x] **(q) Threading-Umstellung aus (p) tiefer geprüft – echte Nebenläufigkeits-Race
  in den Admin-Routen gefunden und mit `ADMIN_LOCK` behoben (2026-07-04).** (p) hatte
  den öffentlich beschreibbaren Diagnose-Pfad per `DIAG_LOCK` abgesichert, aber die
  Admin-Routen übersehen: `toggle-registration`/`create-invite` machen ein
  Read-Modify-Write auf `conduit.toml` + `docker restart`, und `send_admin()` liest
  nach 4 s die letzten Admin-Raum-Nachrichten zurück – beides seit multithreaded
  race-fähig (lost update, doppelter Neustart, vertauschte Antworten). Fix: neues
  `ADMIN_LOCK`, das **nur** die Admin-Mutationen serialisiert; die öffentlichen Routen
  (`livekit-token`, `e2ee-diagnostics`) laufen zuerst und bleiben nebenläufig
  (Slow-Loris-Schutz aus (p) unangetastet). Handler-Logik wortwörtlich unverändert
  (per `diff` gegen Backup verifiziert). Headless verifiziert: 8 Routen-Checks wie
  vorher; Race-Beweis (2 parallele `run-stats` 2633 ms ≈ 2× einzeln 1388 ms →
  serialisiert); Non-Blocking-Beweis (öffentliches `livekit-token` während gehaltenem
  Lock in 11 ms). Details im PROGRESS-Eintrag „(q)" oben.
- [x] **(r) ADMIN_LOCK-Umbau aus (q) (c8ff850) geprüft – Umbau korrekt, 3 kleinere
  Restbefunde gefixt (2026-07-04).** Locking-Struktur und Routen-Reihenfolge von (q)
  bestätigt (öffentliche Routen vor dem Gate/Lock, Handler-Logik unverändert).
  Befunde: (1) `GET /api/registration-status` las `conduit.toml` ohne Lock, während
  die Admin-Routen sie per Truncate+Write neu schreiben – Leser konnte seit
  multithreaded (p) eine halb geschriebene Datei sehen; Fix: Lesen unter `ADMIN_LOCK`
  (bewusst KEIN `os.replace`: die Datei ist als Einzeldatei in den Container
  gemountet, Inode-Falle – im Code dokumentiert). (2) Negative `Content-Length`
  ließ `read()` bis EOF blockieren (Thread hing bis zum 30-s-Timeout) – bei
  `livekit-token`/`e2ee-diagnostics`/`ban`/`unban` wird `< 0` jetzt mit 413
  abgelehnt. (3) `ban`/`unban` ohne Body-Limit (hielten dabei den ADMIN_LOCK) →
  4-KB-Deckel; Einladungs-Token von `random` auf `secrets` umgestellt (Token
  schaltet Registrierung frei). Headless verifiziert (8 Checks inkl. 8-KB-Body →
  413, `Content-Length: -5` → sofort 413 statt Hänger). Details im
  PROGRESS-Eintrag „(r)" oben.
- [x] **(s) (r)-Fixes (5ada0e5) geprüft – korrekt, 2 kleinere Restbefunde gefixt
  (2026-07-04).** (r)-Diff gegen Backup verifiziert (GET-Leser-Lock,
  Content-Length-Härtung, secrets-Token – alles wie protokolliert), Dienst lief
  mit aktuellem Code, kein unprotokollierter Rest im CHANGES-Hook-Log. Befunde
  beim Ganzdatei-Gegenlesen: (1) ban/unban las den Body innerhalb des
  `ADMIN_LOCK` – ein Client mit angekündigtem, nie gesendetem Body hielt den
  Lock bis zum 30-s-Socket-Timeout (das 4-KB-Limit deckelt nur die Größe, nicht
  die Wartezeit; seit (r) hing damit auch der `registration-status`-Leser mit);
  Fix: Body-Lesen/Validieren vor den Lock gezogen, Handler-Logik unverändert.
  (2) `livekit-token`/`e2ee-diagnostics` leakten bei Fehlern `str(e)` an
  Unauthentifizierte (interne Pfade/Adressen); Fix: generisches
  `{"error": "Interner Fehler"}`, Details ins Journal (stderr). Headless
  verifiziert: 8 Routen-Checks unverändert, Lock-Beweis (hängender ban-Body,
  paralleler registration-status-GET in 15 ms), 500-Beweis (ungültiges JSON →
  generisch, Detail im Journal). Details im PROGRESS-Eintrag „(s)" oben.
- [x] **(t) (s)-Fixes (13b333c) geprüft – korrekt, 1 kleiner Restbefund gefixt
  (2026-07-04).** (s)-Diff gegen Backup verifiziert (ban/unban-Body vor dem
  Lock inkl. Validierung, generische 500er auf den öffentlichen POST-Routen –
  alles wie protokolliert), Dienst lief mit aktuellem Code, kein
  unprotokollierter Rest im CHANGES-Hook-Log. Befund beim
  Ganzdatei-Gegenlesen: `GET /api/registration-status` (öffentlich, `do_GET`
  hat kein Gate) antwortete bei Fehlern noch mit `str(e)` – hätte z. B. den
  `conduit.toml`-Pfad geleakt; (s) hatte nur die POST-Routen gefixt. Fix:
  generisches `{"error": "Interner Fehler"}`, Detail ins Journal. Headless
  verifiziert: 8 Routen-Checks unverändert, 500-Beweis per kurzzeitigem
  `chmod 000` auf `conduit.toml` (generisch nach außen, `PermissionError` im
  Journal, danach wieder 200). Details im PROGRESS-Eintrag „(t)" oben.
- [x] **(u) (t)-Fix (d3e75c3) geprüft – korrekt, KEIN Befund; server.py-Kette
  abgeschlossen (2026-07-04).** Diff gegen `server.py.bak-20260704-review-t`
  = exakt der protokollierte Mini-Fix, Dienst läuft mit aktuellem Code, kein
  unprotokollierter Rest. Verbliebene `str(e)`-Stellen alle LAN-gegated
  (absichtlich). Ohne neuen Anlass keine weitere Review-Runde an `server.py`.
  Details im PROGRESS-Eintrag „(u)" oben.
- [x] **(v) Entscheidungs-Doku 825481e (`ANTWORTEN_BERND.md`, am PC entstanden)
  geprüft – konsistent, 1 Doku-Lücke gefunden und behoben (2026-07-05).** Alle
  referenzierten Pläne/Dateien existieren, die vier Entscheidungen decken die
  gesammelten „Fragen an Bernd" ab. Befund: Entscheidung 3 (Dashboard
  Heimnetz-only, A/B geschlossen) war nicht in ROADMAP.md /
  `docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen – beide stellten die Frage
  weiter als offen dar. In derselben Session behoben (ROADMAP-Punkt
  geschlossen, Doku-Status aktualisiert, Vermerk zu Entscheidung 4 am
  Secrets-Punkt). Details im PROGRESS-Eintrag „(v)+(w)" oben.
- [x] **(w) autopilot.sh-Fix 4477ed9 (CLAUDE_CONFIG_DIR unset für Hauptkonto,
  am PC entstanden) geprüft – korrekt, kein Befund (2026-07-05).** Begründung
  im Commit stimmt (CLI sucht Config unter `$CLAUDE_CONFIG_DIR/.claude.json`,
  Hauptprofil liegt unter `~/.claude.json`); `unset` wirkt auch nach
  pyramid-Iterationen derselben Shell. Am lebenden System verifiziert: die
  Review-Session selbst lief als Konto „haupt" mit leerem `CLAUDE_CONFIG_DIR`
  und korrekt geladenem Profil. Kein weiterer `CLAUDE_CONFIG_DIR`-Verweis im
  Repo. Details im PROGRESS-Eintrag „(v)+(w)" oben.
- [x] **(x) (v)-Doku-Fix (66bf54b) geprüft – korrekt, kein Befund (2026-07-05).**
  Diff = exakt der protokollierte Fix (ROADMAP-Dashboard-Punkt geschlossen,
  Nr.-4-Vermerk, Hardening-Doc auf ABGESCHLOSSEN/Archiv), deckungsgleich mit
  `ANTWORTEN_BERND.md` Nr. 3/4; repo-weites grep: keine Datei außerhalb des
  historischen PROGRESS-Logs stellt die A/B-Frage noch als offen dar; kein
  unprotokollierter Rest, keine `lib/`-Änderung seit dem letzten grünen
  Testlauf. Konvergenz-Kriterium aus (u) erfüllt – Review-Stand (a)–(x)
  vollständig. Details im PROGRESS-Eintrag „(x)" oben.
- [x] **(y) Liegengebliebenen (x)-Protokoll-Commit geprüft und nachgeholt (2026-07-05).**
  Die (x)-Session brach vor ihrem Protokoll-Commit ab; der uncommittete Rest
  war reine Doku (der (x)-Eintrag selbst + Hook-Zeilen). Kernaussagen
  stichprobenhaft verifiziert (66bf54b-Umfang, keine `lib/`-Änderungen seit
  925aafb, A/B-Frage überall geschlossen) und unverändert committet
  (15001ae). Details im PROGRESS-Eintrag „(y)" oben.
- [x] **(z) (y)-Protokoll-Commit 725f03a gegengelesen – korrekt, KEIN Befund;
  Ende-Kriterium der Kette geschärft (2026-07-05).** Diff ist exakt der
  (y)-Eintrag + Checklisten-Häkchen + 2 Hook-Zeilen (kein Code); Kernaussagen
  stichprobenhaft verifiziert (15001ae-Umfang PROGRESS +46/CHANGES +2,
  `git log 925aafb..HEAD -- lib/ test/ pubspec.*` leer). **Ab jetzt gilt:**
  Ein reiner, befundloser Protokoll-Commit einer Review-Session ist KEIN
  neuer Review-Anlass mehr – künftige Sessions prüfen nur noch Commits mit
  Code-/Doku-Substanz (alles außer dem eigenen Review-Protokoll), sonst
  reviewt die Kette ewig ihr eigenes Protokoll. Details im
  PROGRESS-Eintrag „(z)" oben.
- [x] **(aa) Uncommittete autopilot.sh-Umstellung auf EIN Konto geprüft und
  committet – 1 Befund gefixt (2026-07-06).** Die Umstellung (Konto-Rotation
  raus, Autopilot fest auf dem nordmann-Konto = Standard-Profil `~/.claude`;
  Bernds Vorgabe laut Kommentar, Backup
  `autopilot.sh.bak-20260706-vor-nordmann-pin`) lag uncommitted im
  Arbeitsbaum. Geprüft: Rotation restlos entfernt (kein
  `get_account`/`switch_account`/`JUST_SWITCHED`/`ACCOUNT_FILE`-Rest, repo-weit
  gegrept), `apply_account` = `unset CLAUDE_CONFIG_DIR` deckt sich mit dem
  (w)-Befund, `bash -n` sauber, Limit-Pfad wartet jetzt 20 min statt zu
  wechseln. Am lebenden System belegt: tmux-Session „pyramid" am 2026-07-06
  11:54 neu gestartet, autopilot.log zeigt das neue Format, DIESE Session
  läuft als nordmann-Konto mit leerem `CLAUDE_CONFIG_DIR`. **Befund (vor dem
  Commit gefixt):** Die neue Fassung enthielt Bernds private E-Mail-Adresse
  (Kommentar + `ACCOUNT_NAME`) – das Gitea-Repo ist ÖFFENTLICH, und bisher
  enthielt keine getrackte Datei diese Adresse; ein Commit hätte sie erstmals
  veröffentlicht. Durch neutrales „nordmann" ersetzt (funktionsgleich, die
  Adresse stand nur in Kommentar und Log-Zeile).

**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
Alles weiterhin UNGETESTET (Pi) im Sinne von „nicht in echter UI gesehen";
der ausstehende PC-Praxistest (siehe Einträge unten) bleibt Pflicht.

---

## 2026-07-03 – M2: Gott-Datei `document_viewer.dart` aufgeteilt (5 Dateien)

**Erledigt:** Vor dem Weiterarbeiten wie immer Arbeitszyklus durchlaufen
(`PROGRESS.md`/`ROADMAP.md` gelesen, `flutter doctor` geprüft – GUI-Lücken
auf dem Pi unverändert). Die Ist-Analyse aus M2 hatte ursprünglich 8 große
Dateien gefunden, aber nur 5 wurden als „Gott-Dateien" tatsächlich
aufgeteilt (`settings_modal.dart`, `message_group.dart`, `rooms_panel.dart`,
`space_admin_dialog.dart`, `chat_view.dart`); `document_viewer.dart` (1773
Zeilen, 31 Klassen/Enums + 3 lose Top-Level-Helfer) war übrig, ist aber
inhaltlich genau derselbe Fall (viele unabhängige Widget-Klassen in einer
Datei) und – anders als `voice_channel.dart`/`app_shell.dart` – reines
Medien-/PDF-Anzeige-Feature ohne Berührung mit Login/Session/Verschlüsselung
oder der noch unentschiedenen Call-Fassade. Deshalb mit derselben
bewährten Technik aufgeteilt:

- `part`/`part of` wie bei den 5 vorigen Umbauten, per Skript extrahiert
  (Klammer-Zählung für Blockgrenzen inkl. der 3 losen Top-Level-Funktionen
  `_saveBytes`, `_extractPsdThumbnail`, `_detectType` – nicht nur
  `class`/`enum`, das wurde diesmal zusätzlich automatisiert gesucht, um
  keine lose Funktion zu übersehen).
- 5 neue Dateien unter `lib/features/chat/document/`:
  `document_viewer_core.dart` (`DocumentViewer`-Dispatcher, `_DocHeader`,
  `_ExtBadge`, `_DocContent`, `_DocType`-Enum + `_detectType`, `_ErrorView`,
  `_UnsupportedViewer`, `_FileFallback`), `document_viewer_pdf.dart`
  (größter Teil: `_RenderCache`, `PdfInlinePreview`+State für die
  Chat-Inline-Vorschau, `_NavArrow`, `_saveBytes`, `_PdfFullViewer`+State,
  `_PdfFullscreenViewer`+State, `_FsBtn`, `_ThumbnailStrip`+State),
  `document_viewer_text.dart` (SVG/Markdown/Text/Office-Text-Extraktion),
  `document_viewer_archive.dart` (ZIP/TAR-Archiv-Browser, PSD-Thumbnail-
  Extraktion inkl. `_extractPsdThumbnail`, Sketch/XD-ZIP-Preview),
  `document_viewer_misc.dart` (`_ProprietaryViewer`-Infokarte für nicht
  darstellbare Formate). `document_viewer.dart` selbst: nur noch
  Bibliothekskopf (Imports + 5 `part`-Direktiven, 20 Zeilen).
- Verifikation wie bei den 5 vorigen Umbauten: automatisierte
  Klammer-Zählung fand alle 34 Top-Level-Blöcke (31 Klassen/Enums + 3 lose
  Funktionen), alle wortwörtlich in den neuen Dateien wiedergefunden,
  `class`/`enum`-Anzahl (31) vorher/nachher identisch, `flutter analyze`
  unverändert bei 1 bekanntem Hinweis (`chat_provider.dart:42`). Einzige
  externe Importstelle (`message_group.dart`) bleibt unverändert gültig, da
  `document_viewer.dart` weiterhin der Bibliothekseinstieg ist.
- **Bewusst NICHT angefasst:** `voice_channel.dart` (1123 Zeilen) und
  `app_shell.dart` (1073 Zeilen) – auch große Dateien mit vielen Klassen,
  aber `voice_channel.dart` gehört zur noch unentschiedenen Call-Fassade
  (siehe Fragen an Bernd unten) und `app_shell.dart` enthält den
  Bootstrap-Trigger (`_triggerBootstrap`) für Cross-Signing/Key-Backup –
  laut CLAUDE.md heiliger Bereich. Eine reine Datei-Aufteilung wäre zwar
  vermutlich genauso risikoarm wie bei den anderen 6, aber ohne GUI-Test auf
  dem Pi wollte ich das Risiko nicht zusätzlich zum ohnehin schon
  aufgelaufenen UNGETESTET-Stapel erhöhen, bevor nicht wenigstens einmal ein
  PC-Durchlauf stattgefunden hat.

**Offen/Nächster Schritt:** UNGETESTET (Pi) – siehe ROADMAP-Eintrag oben,
gehört zum selben ausstehenden Windows-Praxistest wie die 5 vorigen
Datei-Aufteilungen (PDF/Text/Archiv/Bild im Chat öffnen, Vollbild-PDF-Viewer
inkl. Thumbnail-Leiste und Speichern-Button prüfen). Danach bleibt exakt
dieselbe Blockade wie im Eintrag direkt darunter: kein M1/M2-Punkt mehr
ohne Bernds Antwort (Call-Pilot, SQLCipher-Priorität) oder PC/GUI-Zugriff
sicher bearbeitbar.

**Stolperfallen:** Anders als bei den 5 vorigen Umbauten gab es diesmal 3
lose Top-Level-Funktionen zusätzlich zu den Klassen/Enums
(`_saveBytes`/`_extractPsdThumbnail`/`_detectType`) – eine reine
`grep -E '^(class|enum) '`-Suche hätte sie übersehen und stillschweigend aus
der neuen Datei-Struktur fallen lassen. Deshalb diesmal zusätzlich per
Diff aller durch Klammer-Zählung abgedeckten vs. nicht abgedeckten
Zeilenbereiche geprüft, dass keine „Lücke" zwischen zwei Klassenblöcken
mehr als Kommentar/Leerzeile ist. Für künftige Datei-Aufteilungen (z. B.
`voice_channel.dart`/`app_shell.dart`, falls Bernd grünes Licht gibt): immer
zuerst auf lose Top-Level-Funktionen prüfen, nicht nur auf Klassen/Enums.

---

## 2026-07-03 – Session-Check: weiterhin kein sicher bearbeitbarer Punkt auf dem Pi

**Erledigt:** Vor dem Weiterarbeiten Arbeitszyklus komplett durchlaufen: `PROGRESS.md`
+ `ROADMAP.md` gelesen, `flutter analyze` (weiterhin sauber, nur der eine bekannte
Hinweis in `chat_provider.dart:42`), `git status` (sauber, `master` = `origin/master`),
`flutter doctor` erneut geprüft (Android-SDK, Chrome, Linux-Desktop-Toolchain fehlen
weiterhin auf diesem Pi – keine GUI möglich). Damit alle offenen Punkte in M1/M2
gegengeprüft:
- M1 „SQLCipher" und „Härtetest dokumentieren": unverändert PC-only (siehe Einträge
  weiter unten), keine neue Information seit letzter Session.
- M2 „Call-Schicht entwirren": zum Einstieg `core/voip_manager.dart` (538 Zeilen)
  gelesen, um eine Fassade zu entwerfen. Ergebnis: Das ist **kein** Datei-Split wie die
  letzten 5 Gott-Datei-Umbauten (dort: reines `part`/`part of`, null Logikänderung,
  automatisiert byte-genau verifizierbar). `PyramidVoipManager` ist ein Singleton
  (`_instance`), der gleichzeitig `ChangeNotifier` UND die vom matrix-SDK verlangte
  `WebRTCDelegate`-Schnittstelle implementiert, mit Timer-basiertem Renderer-Polling,
  impliziten Reihenfolgen bei Mute/Unmute/Screenshare und einem `MediaDevicesWrapper`,
  der direkt auf private Felder des Managers zugreift. Eine Fassade davor UND die
  Umstellung der 7 Importstellen lässt sich nicht mehr rein mechanisch verifizieren –
  echtes Risiko für Verhaltensänderung an Calls, die echte Nutzer (Uta) im Einsatz
  haben. Genau das hatte die vorige Session bereits zweimal als „erste Stelle mit
  echtem Verhaltensrisiko in M2" geflaggt und Bernds Entscheidung dazu angefragt
  (siehe „Fragen an Bernd" weiter unten) – hier bestätigt und bewusst NICHT
  eigenmächtig angefasst, weil auf diesem Pi ohnehin kein GUI-Test möglich ist, um
  eine Regression aufzufangen.

**Offen/Nächster Schritt:** Es gibt aktuell **keinen** ROADMAP-Punkt in M1/M2, der auf
diesem Pi ohne (a) Bernds Antwort auf die Call-Pilot-Frage oder (b) echten PC/GUI-Zugriff
sicher bearbeitet werden kann. Nächster sinnvoller Schritt ist entweder ein PC-Termin
(Härtetest + SQLCipher-Migrationsplanung + Call-Fassade mit echtem Test) oder Bernds
kurze Antwort auf die unten stehende Frage, damit die nächste Pi-Session direkt starten
kann.

**Stolperfallen:** Keine neuen – Bestätigung, dass die Vorsicht der letzten Session
richtig war: „sieht aus wie ein weiterer Datei-Split" ist bei der Call-Schicht trügerisch,
weil Singleton-Lifecycle + SDK-Delegate-Vertrag + Timer-Polling nicht mit einem
Textvergleich absicherbar sind wie bei reinen `part`/`part of`-Umbauten.

**Fragen an Bernd:** Unverändert offen (wiederholt aus den letzten beiden Einträgen,
damit sie nicht übersehen wird):
1. Call-Pilot (Fassade für `voip_manager.dart`/`livekit_call_manager.dart`) jetzt
   angehen (nur auf einem PC mit echtem Test), oder erstmal zurückstellen?
2. SQLCipher-Verschlüsselung der lokalen DB: Priorität vor M2, oder reicht die
   App-Sandbox als Schutz vorerst?
Ohne eine dieser beiden Antworten bleibt der Pi bei jedem weiteren Durchlauf an
derselben Stelle stehen – ein kurzer PC-Termin für Härtetest + Call-Pilot würde
gleich mehrere blockierte Punkte auf einmal lösen.

---

## 2026-07-03 – M2: Gott-Datei `chat_view.dart` aufgeteilt (6 Dateien) – ROADMAP-Punkt abgehakt

**Erledigt:** Fünfter und letzter Durchgang derselben `part`/`part of`-Technik:
`features/chat/chat_view.dart` (2116 Zeilen, 22 Klassen, keine losen
Top-Level-Helfer) aufgeteilt in 6 Dateien unter `features/chat/view/`:
`chat_view_core.dart` (`ChatView`, `_ChatViewState` – der komplette
Nachrichten-Senden/Scroll/Attachment-Zustandsautomat, mit 521 Zeilen
weiterhin der größte Einzelblock, aber nicht weiter unterteilt, da es EINE
zusammenhängende State-Klasse ist, keine künstliche Aufspaltung mitten in
einer Klasse), `chat_header.dart` (`_ChatHeader`, DM-Header inkl.
`_DmExpandingHeader`+State, `_DmHeaderAvatar`, `_InitialCircle`,
`_PresenceLine`+State), `chat_connection.dart` (`_ConnectionBanner`+State,
Offline/Reconnect-Banner), `chat_message_list.dart` (`_MessageList`,
`_SelectionHeader`, `_SelBtn`, `_UnreadDivider`), `chat_typing.dart`
(`_TypingIndicator`+State, `_TypingDots`+State), `chat_misc.dart`
(`_DragOverlay`+State für Datei-Drag&Drop, `NoChatSelected` – bleibt
öffentlich, `app_shell.dart` importiert `chat_view.dart` und nutzt es).
`chat_view.dart` selbst: nur noch Bibliothekskopf (33 Zeilen).

Verifikation identisch zu den vier vorigen Umbauten: automatisierte
Klammer-Zählung, alle 22 Blöcke wortwörtlich wiedergefunden,
`class`/`enum`-Anzahl (22) und `Widget build(`-Methoden (15) vorher/nachher
identisch, `flutter analyze` unverändert bei 1 bekanntem Hinweis.

**Damit sind alle 5 in der M2-Ist-Analyse gefundenen Gott-Dateien aufgeteilt**
(`settings_modal.dart`, `message_group.dart`, `rooms_panel.dart`,
`space_admin_dialog.dart`, `chat_view.dart` – siehe die vier Einträge
darunter). ROADMAP-Punkt „Toten Code & Duplikate entfernen, Ordnerstruktur
vereinheitlichen" jetzt abgehakt.

**Offen/Nächster Schritt:** **Wichtig, bevor der nächste ROADMAP-Punkt
angefasst wird:** all diese Aufteilungen sind bisher NUR per `flutter
analyze` + automatisiertem Textvergleich geprüft, NICHT in echter UI
gesehen (kein GUI-Toolchain auf dem Pi verfügbar – kein Android-SDK, kein
Linux-Desktop-Build, siehe `flutter doctor`). Der nächste PC-/Windows-Lauf
MUSS vor dem nächsten inhaltlichen Schritt einmal durch alle 5 betroffenen
Bereiche klicken (Einstellungen komplett, ein Chat mit allen
Nachrichtentypen, die Raumliste, eine Space-Verwaltung, ein normaler
Chat-Screen mit Header/Tippanzeige/Drag&Drop) – das Risiko einer reinen
`part`/`part-of`-Verschiebung ist strukturell gering (Dart-Compiler hätte
jeden fehlenden Import/jede falsche Sichtbarkeit sofort als Analyzer-Fehler
gemeldet, nicht erst zur Laufzeit), aber „gering" ist nicht „null", und
CLAUDE.md verlangt nach jedem Refactoring-Schritt einen echten App-Start.
Danach nächster ROADMAP-Punkt in M2: „Call-Schicht entwirren" – dafür
weiterhin Bernds Entscheidung zum Call-Pilot aus „M2: Modul-Schnitt
definiert" ausstehend, UND dieser Punkt braucht ohnehin einen PC/Windows-Test
(Verhaltensänderung an State-Management, nicht nur Datei-Verschiebung).

**Stolperfallen:** Keine neuen. Generelle Lehre aus allen 5 Durchgängen:
Skript-gestützte Klammer-Zählung + automatisierter Verbatim-Abgleich
(„jeder Originalblock muss wortwörtlich in den neuen Dateien auftauchen")
war der entscheidende Sicherheitsnetz-Schritt, um trotz fehlendem GUI-Test
auf dem Pi mit vertretbarem Risiko fortzufahren – sollte bei künftigen
reinen Datei-Umbauten (z. B. `voice_channel.dart` beim Call-Pilot) als
Vorgehen wiederverwendet werden.

---

## 2026-07-03 – M2: Gott-Datei `space_admin_dialog.dart` aufgeteilt (6 Dateien)

**Erledigt:** Vierter Durchgang derselben `part`/`part of`-Technik:
`features/spaces/space_admin_dialog.dart` (2278 Zeilen, 32 Klassen/Enums +
6 lose Top-Level-Helfer) aufgeteilt in 6 Dateien unter
`features/spaces/admin/`: `space_admin_core.dart` (`SpaceAdminDialog`,
`_SpaceAdminDialogState`, `_DialogHeader` + die breit genutzten Helfer
`_kSpaceBannerEvent`, `updatePowerLevelsSafely` (öffentliche Funktion –
schützt vor Selbst-Aussperrung beim Ändern der Power-Levels, genutzt von
Members- UND Permissions-Tab), `_kPowerAdmin`/`_kPowerMod`/`_kPowerMember`,
`_powerLabel`, `_powerColor`), `space_admin_overview.dart` (`_OverviewTab`+State,
`_SpaceVisibilityToggle`+State, `_DangerZone`), `space_admin_members.dart`
(`_MembersTab`+State, `_InviteButton`+State, `_MemberTile`+State,
`_MemberActions`, `_MemberAction`), `space_admin_permissions.dart`
(`_PermissionsTab`+State, `_PermissionRow`), `space_admin_channels.dart`
(`_ChannelsTab`+State, `_ChannelItem`+State, `_CreateChannelDialog`+State,
`_AddExistingDialog`+State + die dort lokal genutzten Helfer `_kSpaceChild`/
`_kSpaceParent`/`_kVoiceChannelType`), `space_admin_shared.dart`
(`_SectionLabel`, `_Field`, `_SearchField`, `_AvatarEditor`, `_InitialAvatar`).
`space_admin_dialog.dart` selbst: nur noch Bibliothekskopf (19 Zeilen).

Auffällig beim Durchlesen (nicht angefasst, nur notiert): `updatePowerLevelsSafely`
ist als einzige Funktion in dieser Datei öffentlich (kein `_`-Präfix), wird
aber nirgends außerhalb von `space_admin_dialog.dart` importiert/aufgerufen –
vermutlich für einen mal geplanten, nie gebrauchten externen Aufrufer public
gemacht. Bewusst nicht auf privat zurückgestuft, um diesen Schritt nicht mit
einer inhaltlichen Änderung zu vermischen; wäre ein Kandidat für den nächsten
"toter Code"-Durchgang.

Verifikation wie bei den drei vorigen Umbauten: automatisierte
Klammer-Zählung für Blockgrenzen, alle 32 Blöcke + 4 Helfer-Gruppen
wortwörtlich wiedergefunden, `class`/`enum`-Anzahl (32) und
`Widget build(`-Methoden (20) vorher/nachher identisch, `flutter analyze`
unverändert bei 1 bekanntem Hinweis.

**Offen/Nächster Schritt:** UNGETESTET (Pi). Nächster Windows-Lauf: Space-
Verwaltung öffnen – Übersicht (Sichtbarkeit umschalten, Banner ändern,
Danger-Zone-Buttons NUR ansehen, nicht bestätigen), Mitglieder (einladen,
Rolle ändern, kicken/bannen – wenn möglich an einem Test-Space, nicht an
Utas echtem), Berechtigungen, Kanäle (erstellen, bestehenden Raum hinzufügen).
Danach `chat_view.dart` (2116 Zeilen) als letzte der 5 in der Ist-Analyse
gefundenen Gott-Dateien – damit wäre der komplette „Ordnerstruktur
vereinheitlichen"-Katalog aus M2 abgearbeitet (Häkchen dann setzen).

**Stolperfallen:** Keine neuen.

---

## 2026-07-03 – M2: Gott-Datei `rooms_panel.dart` aufgeteilt (8 Dateien)

**Erledigt:** Gleiche `part`/`part of`-Technik ein drittes Mal angewandt:
`features/rooms/rooms_panel.dart` (2555 Zeilen, 37 Klassen/Enums + 4 lose
Top-Level-Helfer) aufgeteilt in 8 Dateien unter `features/rooms/panel/`:
`rooms_panel_core.dart` (`RoomsPanel`, `_RoomsPanelState` + die
Top-Level-Helfer `_kVoiceRoomType`/`_kSpaceBannerType`/`_isVoiceRoom`/
`_compareSpaceChildren`, die von mehreren anderen Teilen genutzt werden –
deshalb bewusst NICHT in ein eigenes „shared"-File, sondern beim Hauptwidget
belassen, wo sie ursprünglich standen), `rooms_header.dart` (`_Header`,
`_MenuRow`, `_NotificationsButton`, `_FilterInput`, `_SpaceMenuItem`),
`rooms_space_invite.dart` (`_SpaceInviteView`+State), `rooms_space_list.dart`
(`_SpaceRoomsList`+State, `_JoinableRoom`+Item+State, `_CategoryHeader`+State),
`rooms_list.dart` (`_RoomsList`, `_SectionHeader`+State, `_RoomItem`+State,
`_DraggableRoomItem`+State), `rooms_voice.dart` (`_VoiceParticipantList`,
`_Initials`, `_VoiceBarBtn`+State), `rooms_invite_item.dart`
(`_InviteItem`+State), `rooms_shared.dart` (`_DmAvatar`+State,
`_InitialCircle`, `_RoomMenuBtn`+State, `_HoverAction`+State, `_UnreadBadge`).
`rooms_panel.dart` selbst: nur noch Bibliothekskopf (Imports + 8
`part`-Direktiven, 31 Zeilen).

Verifikation wie bei den beiden vorigen Umbauten: automatisierte
Klammer-Zählung für Blockgrenzen, alle 37 Blöcke + 4 Helfer wortwörtlich in
den neuen Dateien wiedergefunden, Anzahl `class`/`enum` (37) und
`Widget build(`-Methoden (22) vorher/nachher identisch, `flutter analyze`
unverändert bei 1 bekanntem Hinweis. `dart format` wieder bewusst nicht
gelaufen.

**Offen/Nächster Schritt:** UNGETESTET (Pi). Nächster Windows-Lauf: Raumliste
prüfen – Space wechseln, Raum beitreten/verlassen, Voice-Channel-Teilnehmerliste
in der Raumliste, Einladung annehmen/ablehnen, Drag&Drop zum Umsortieren,
Ungelesen-Badge, DM-Avatare. Danach `space_admin_dialog.dart` (2278 Zeilen)
oder `chat_view.dart` (2116 Zeilen) nach demselben Muster – damit wären alle
in der Ist-Analyse gefundenen Gott-Dateien abgearbeitet.

**Stolperfallen:** Keine neuen.

---

## 2026-07-03 – M2: Gott-Datei `message_group.dart` aufgeteilt (7 Dateien)

**Erledigt:** Gleiche Technik wie beim `settings_modal.dart`-Umbau direkt
darunter angewandt: `features/chat/message_group.dart` (2775 Zeilen, 41
Klassen/Enums + 5 lose Top-Level-Hilfsfunktionen) per Dart `part`/`part of`
in 7 thematische Dateien unter `features/chat/message/` aufgeteilt:
`message_group_core.dart` (`MessageGroup`, `_MessageGroupState`,
`_SendFailedRow`, `_MessageBody`, `_ContinuationMessage`(+State), inkl. der in
der letzten Session vereinheitlichten `_formatMessageTime`), `message_text.dart`
(`_MessageContent`, `_MatrixHtmlText`, `_PlainLinkText`),
`message_link_preview.dart` (`_LinkPreview`(+State/Data), `_ReplyPreview`),
`message_media.dart` (alle Medientypen: Bild/Audio/Video/Doc/Datei/proprietäre
Inline-Karten + die zugehörigen Top-Level-Helfer `_reservedImageSize`/
`_classifyMediaError`/`_checkedDownload`/`_mediaFallback` – mit 1000 Zeilen
weiterhin der größte Teil, aber thematisch klar abgegrenzt),
`message_reactions.dart`, `message_actions.dart` (Hover-Aktionsleiste),
`message_shared.dart` (`_Avatar`, `DateDivider` – bleibt öffentlich, da
`chat_view.dart` es importiert; über `part`/`part of` weiterhin ohne
Umbenennung nutzbar). `message_group.dart` selbst ist jetzt nur noch der
Bibliothekskopf (Imports + 7 `part`-Direktiven, 30 Zeilen).

Verifikation identisch zum Settings-Umbau: automatisierte Klammer-Zählung
für die Blockgrenzen (kein manuelles Abtippen), alle 41 Original-Blöcke +
5 Helfer wortwörtlich in den neuen Dateien wiedergefunden, Anzahl
`class`/`enum` (45) und `Widget build(`-Methoden (27) vorher/nachher
identisch, `flutter analyze` unverändert bei 1 bekanntem Hinweis. `dart format`
diesmal bewusst NICHT ausgeführt (siehe Stolperfalle im Eintrag darunter).

**Offen/Nächster Schritt:** UNGETESTET (Pi). Nächster Windows-Lauf: einen
Chat mit mehreren Nachrichtentypen öffnen (Text mit Link-Vorschau, Bild,
Video, Audio, Datei-Anhang, Reaktionen setzen, Hover-Aktionsleiste,
Datums-Trenner, gescheiterte Nachricht mit Retry) – rein mechanische
Verschiebung, aber `message_media.dart` ist der bisher am wenigsten oft
geprüfte Bereich (Download/Entschlüsselungs-Fehlerpfade), daher dort genauer
hinschauen. Danach ggf. `rooms_panel.dart` (2555 Zeilen) oder
`space_admin_dialog.dart` (2278 Zeilen) nach demselben Muster aufteilen.

**Stolperfallen:** Keine neuen – siehe `dart format`-Hinweis im
`settings_modal.dart`-Eintrag darunter, hier von vornherein vermieden.

---

## 2026-07-03 – M2: Gott-Datei `settings_modal.dart` aufgeteilt (12 Dateien)

**Erledigt:** `widgets/settings_modal.dart` (5541 Zeilen, 50+ Klassen) war laut
Ist-Analyse die größte „Gott-Datei". „Call-Schicht entwirren" (nächster
ROADMAP-Punkt) bleibt weiter blockiert, da es dabei um Verhaltens-/State-
Management-Änderungen an einem CLAUDE.md-„heiligen" Bereich (Calls) geht, die
laut Arbeitsregeln nach jedem Schritt einen echten App-Test brauchen –
auf dem Pi ohne GUI-Toolchain (kein Android-SDK, kein Linux-Desktop-Build,
siehe `flutter doctor`) unmöglich zu prüfen. Stattdessen den in
`docs/M2_MODULE_SCHNITT.md` als zweiten (risikoärmeren) Schritt vorgeschlagenen
Umbau vorgezogen: reine Datei-Aufteilung ohne Verhaltensänderung, komplett über
`flutter analyze` prüfbar.

- Technik: Dart `part`/`part of` statt eigenständiger Libraries – dadurch
  bleiben alle privaten Klassen (`_ProfileSection`, `_SettingsGroup`, …) ohne
  Umbenennung nutzbar, und alle Imports bleiben zentral in `settings_modal.dart`
  (Part-Dateien brauchen keine eigenen Imports). Kein Klassenname geändert,
  kein Zeichen Logik angefasst.
- 12 neue Dateien unter `lib/widgets/settings/`: `settings_shared.dart`
  (gemeinsame Low-Level-Widgets wie `_SettingsGroup`/`_Toggle`/`_Divider`),
  `settings_profile.dart`, `settings_notifications.dart`,
  `settings_appearance.dart`, `settings_voice.dart`, `settings_keybinds.dart`
  (Tastaturkürzel-Anzeige, NICHT zu verwechseln mit Verschlüsselungs-Keys),
  `settings_privacy.dart`, `settings_sessions.dart`, `settings_verification.dart`
  (SAS/QR-Dialog), `settings_encryption.dart` (inkl. der Megolm-Export/Import-
  Kryptofunktionen `_runPbkdf2`/`_aesCtr`/`_encryptMegolmKeys`/`_decryptMegolmKeys`,
  die bisher lose im Datei-Kopf lagen), `settings_account.dart` (Passwort ändern/
  Account löschen), `settings_about.dart`.
- `settings_modal.dart` selbst: von 5541 auf 270 Zeilen geschrumpft – enthält
  nur noch Imports, die 12 `part`-Direktiven, `SettingsModal` und
  `_SettingsModalState` (Navigation/Layout).
- Verifikation (da kein GUI-Test möglich): Skript-gestützte Extraktion per
  Klammer-Zählung (kein manuelles Copy-Paste, um Übertragungsfehler
  auszuschließen); anschließend automatisiert geprüft, dass alle 61
  ursprünglichen Klassen/Enums wortwörtlich (`in`-Substring-Check) in den neuen
  Dateien wieder auftauchen; Anzahl `class`/`enum`-Deklarationen (60) und
  `Widget build(`-Methoden (40) vorher/nachher identisch; alle Lücken zwischen
  den Klassenblöcken im Original einzeln geprüft – ausschließlich Leerzeilen
  und dekorative Trennkommentare, kein Code verloren.
- `flutter analyze`: weiterhin nur der eine bekannte, zurückgestellte Hinweis
  (`chat_provider.dart:42`).
- **Stolperfalle vermieden:** `dart format` versuchte 40 neue Lint-Hinweise
  einzuführen (`curly_braces_in_flow_control_structures`) – Ursache: die
  Originaldatei war selbst nie komplett `dart format`-clean (bestätigt per
  `dart format --set-exit-if-changed` auf dem Original-Git-Stand), und Reflow
  einzelner langer Einzeiler-`if`s ohne Klammern erzeugte neue Zeilenumbrüche,
  die der Linter anders bewertet als die Originalformatierung. Formatierung
  komplett verworfen und die Dateien unformatiert aus dem Original übernommen,
  um „keine Verhaltensänderung ohne Not" strikt einzuhalten und die
  `flutter analyze`-Baseline (1 Hinweis) nicht zu verwässern.

**Offen/Nächster Schritt:** UNGETESTET (Pi) im Sinne von „nicht in echter UI
gesehen". Nächster Windows-Lauf: Einstellungen öffnen, durch alle Reiter
klicken (Profil, Benachrichtigungen, Erscheinungsbild, Voice, Tastaturkürzel,
Privatsphäre, Sessions, Verschlüsselung inkl. SAS/QR-Verifizierung und
Recovery-Key-Anzeige/-Import, Passwort ändern, Account löschen-Dialog öffnen
(ohne zu bestätigen!), Über). Da rein mechanisch verschoben, ist das Risiko
gering, aber Verschlüsselung/Recovery-Key ist laut CLAUDE.md heilig – bitte
gezielt den Recovery-Key-Anzeige- und Passphrase-Export/Import-Flow einmal
durchklicken. Danach ROADMAP-Häkchen für „Ordnerstruktur vereinheitlichen"
erst setzen, wenn auch die übrigen Gott-Dateien (`message_group.dart`,
`rooms_panel.dart`, `space_admin_dialog.dart`, `chat_view.dart`,
`document_viewer.dart`) angegangen sind – aktuell nur `settings_modal.dart`
erledigt.

**Stolperfallen:** `dart format` NICHT blind nach einem Refactoring laufen
lassen, wenn die Baseline vorher nicht schon formatter-clean war – kann neue,
unrelated Lint-Hinweise einführen und die vermeintlich reine Verschiebung mit
Formatierungsrauschen vermischen (siehe oben).

**Fragen an Bernd:** Keine neuen – die offene Frage zum Call-Pilot aus dem
Eintrag „M2: Modul-Schnitt definiert" ist weiterhin unbeantwortet, blockiert
aber nicht (siehe dort).

---

## 2026-07-03 – M2: Toten Code & Duplikate entfernt (Chat-Timeline)

**Erledigt:** Vorgriff auf den ROADMAP-Punkt „Toten Code & Duplikate entfernen"
(technisch der 5. Punkt in M2, aber risikofrei und ohne GUI testbar – im
Gegensatz zum aktuell blockierten „Call-Schicht entwirren", siehe Frage an
Bernd im Eintrag darunter):
- `lib/features/chat/message_bubble.dart` (178 Zeilen) gelöscht – vollständig
  toter Code, wurde nirgends importiert/instanziiert (durch `message_group.dart`
  abgelöst), per `grep` doppelt bestätigt.
- Die zwei identischen `_formatTime`-Methoden in `message_group.dart`
  (`_MessageGroupState` Zeile 322, `_ContinuationMessageState` Zeile 475 –
  Vorher-Zeilennummern) zu einer gemeinsamen Top-Level-Funktion
  `_formatMessageTime()` zusammengeführt, beide Aufrufstellen umgestellt.
  Reine Extraktion, keine Verhaltensänderung (identischer Code vorher/nachher).
  `pinned_messages_panel.dart:_formatTime` bewusst NICHT angefasst – hat
  andere Logik (relatives Datum „vor Xd"), ist kein echtes Duplikat.
- `flutter analyze` weiterhin sauber: nur der eine bekannte, bewusst
  zurückgestellte Hinweis (`chat_provider.dart:42`, `Timeline.onUpdate`
  deprecated, hängt am E2EE-Redecrypt-Pfad, siehe M0-Eintrag).

**Offen/Nächster Schritt:** UNGETESTET (Pi) im Sinne von „nicht in echter UI
gesehen" – kein GUI auf diesem Pi verfügbar. Da es sich aber um reine
Code-Verschiebung ohne Logikänderung handelt (identischer Funktionskörper),
ist das Risiko gering; trotzdem beim nächsten Windows-Lauf kurz einen
Chat mit mehreren aufeinanderfolgenden Nachrichten (Zeit-Anzeige) und eine
Fortsetzungsnachricht (Hover zeigt Uhrzeit) ansehen.

**Stolperfallen:** Keine.

---

## 2026-07-03 – M2: Modul-Schnitt definiert (docs/M2_MODULE_SCHNITT.md)

**Erledigt:** Ausgehend von der Ist-Analyse (Eintrag darunter) die konkreten
Modul-Grenzen + öffentliche Schnittstellen entworfen, dokumentiert in
`docs/M2_MODULE_SCHNITT.md`. Reine Planung, kein Code geändert. Wichtigste
Erkenntnis beim genaueren Lesen von `voip_manager.dart` und
`livekit_call_manager.dart`: „Calls" ist architektonisch **kein einzelnes
Feature**, sondern zwei unabhängige Mechanismen, die getrennt bleiben sollten:
- `PyramidVoipManager` = natives Matrix-1:1-VoIP (`m.call.*`, direkte
  WebRTC-Peer-Connection, kein SFU)
- `LiveKitCallManager` = SFU-basierte Voice-Channels (LiveKit-Raum, Presence
  via Matrix-State-Event, Screensharing, Mehrpersonen) – das ist der
  Discord-artige Kanal aus M3/M4

Modul-Liste im Dokument: `call_signaling`, `voice_channel`, `call_ui`,
`settings` (aufgesplittet in Sektionsdateien), `verification` (neu, aus
Settings herausgelöst – SAS/QR/Recovery-Key), `storage` (Ausbau der
bestehenden `settings_prefs.dart`-Abstraktion zur Pflicht), `auth` (schon
relativ sauber, kein akuter Bedarf), `push` (schon getrennt, eigener
`sqflite`-Zugriff in `background_push.dart` ist bewusst so, kein
Refactoring-Kandidat), `chat_timeline`/`rooms` (niedrigste Priorität, da
strukturell am wenigsten verwoben – Hauptproblem dort ist Dateigröße/Duplikate,
nicht Kopplung). Jeweils mit Dart-Interface-Skizze bzw. Datei-Zielliste.

Empfohlene Umsetzungsreihenfolge: 1) Call-Pilot (`call_signaling` +
`voice_channel` + `call_ui`, deckt gleich zwei ROADMAP-Punkte ab), 2)
`settings_modal.dart` aufsplitten (geringes Risiko, reine Datei-Teilung),
3) `verification` herauslösen, 4) `storage`-Fassade schrittweise erzwingen,
5) `chat_timeline`/`rooms` zuletzt.

**Offen/Nächster Schritt:** Wartet auf Bernds Entscheidung, ob der Call-Pilot
wie vorgeschlagen als Nächstes umgesetzt wird (siehe Frage unten). Erst danach
den ROADMAP-Punkt „Call-Schicht entwirren" wirklich anfassen – das ist die
erste Stelle mit echtem Verhaltensrisiko in M2 (CLAUDE.md: nach Refactoring-
Schritten immer App starten + Kernflows prüfen, hier zusätzlich brisant, weil
GUI-Test auf dem Pi nicht möglich ist).

**Stolperfallen:** Der erste Ist-Analyse-Durchgang (Subagent) hatte einen
falschen Befund (`rooms_provider.dart`-Import in `app_state.dart` sei toter
Code) – beim genaueren Lesen für den Modul-Schnitt widerlegt
(`voiceParticipantsProvider` nutzt `roomListProvider` daraus). Bereits im
Ist-Analyse-Eintrag korrigiert. Lehre: Subagent-Ist-Analysen sind ein guter
Startpunkt, aber vor jedem darauf aufbauenden Schritt einzelne Befunde
gegenlesen, nicht blind übernehmen.

**Fragen an Bernd:**
- Call-Pilot wie in `docs/M2_MODULE_SCHNITT.md` vorgeschlagen (zuerst
  `call_signaling`/`voice_channel`/`call_ui` entwirren) als nächster
  Umsetzungsschritt in Ordnung, oder lieber zuerst `settings_modal.dart`
  aufsplitten (geringeres Risiko, aber deckt keinen ROADMAP-Punkt aus M2
  direkt ab)? Ohne GUI-Zugriff auf dem Pi kann der riskantere Call-Umbau
  hier ohnehin nicht praktisch getestet werden – das spricht eher für
  „auf dem PC anfangen", falls Zeitdruck besteht.

---

## 2026-07-03 – M2: Ist-Analyse der Architektur geschrieben

**Erledigt:** M1 hat aktuell keinen auf dem Pi bearbeitbaren Punkt mehr offen
(SQLCipher wartet auf Bernds Priorisierung, Härtetest braucht GUI/PC – siehe
Fragen an Bernd unten und Eintrag 2026-07-03 „3 schon vorhanden, 1 echte
Lücke"). Deshalb mit M2 weitergemacht, erster Punkt: Ist-Analyse. Per
Code-Lektüre (Zeilenzahlen, Import-Graph, grep) folgende Schwachstellen
gefunden:

- **Gott-Dateien:** `widgets/settings_modal.dart` mit 5541 Zeilen / 50+ Klassen
  bündelt mindestens 8 unabhängige Bereiche (Profil, Benachrichtigungen,
  Voice/TURN, E2EE-Verifizierung inkl. SAS/QR, Sessions, Account, Erscheinungsbild,
  Updates). Weitere große Dateien: `message_group.dart` (2780), `rooms_panel.dart`
  (2555), `space_admin_dialog.dart` (2278), `chat_view.dart` (2116),
  `document_viewer.dart` (1773), `voice_channel.dart` (1123), `app_shell.dart` (1073).
- **Call-Schicht unsauber verteilt statt hinter einer Schnittstelle:**
  `core/voip_manager.dart` (Matrix-VoIP-Signalisierung), `core/livekit_call_manager.dart`
  (LiveKit-Transport), `features/call/voice_channel.dart` + `mini_call_widget.dart`
  (UI) – aber `chat_view.dart`, `matrix_client.dart`, `settings_modal.dart` greifen
  alle DIREKT auf die Manager zu statt über eine gemeinsame Call-Fassade. Vier
  verschiedene Module importieren `voice_channel.dart` direkt.
  Genau das Gegenteil des „austauschbare Bausteine"-Ziels aus CLAUDE.md.
- **State-Management gemischt:** 2 zentrale `ChangeNotifier`-Singletons
  (`voip_manager.dart`, `livekit_call_manager.dart`) statt Riverpod-`Notifier`,
  eingebunden über `app_state.dart` (`callStateProvider`, `voipStateProvider`).
  Migration sinnvoll, aber riskant wegen breiter Call-Sites und enger
  SDK-Callback-Kopplung (LiveKit-`Room`-Events, Matrix-`WebRTCDelegate`) – sollte
  zusammen mit der Call-Fassade angegangen werden, nicht isoliert.
- **`core/` ist nicht rein generisch:** importiert Feature-Code direkt
  (`app.dart`/`router.dart` → `features/auth/*`, `matrix_client.dart` instanziiert
  `PyramidVoipManager` direkt). `app_state.dart` selbst ist ein Sammelbecken für
  Theme-, Call-, Voice-, Share-Target- und Spaces-Provider aus praktisch allen
  Features – zentraler Kopplungspunkt, der jedem Modulschnitt im Weg steht.
  (Korrektur beim genaueren Hinsehen für den Modul-Schnitt-Punkt: der
  `rooms_provider.dart`-Import dort ist entgegen der ersten Vermutung KEIN
  toter Altlast-Import – `voiceParticipantsProvider` nutzt `roomListProvider`
  daraus. Zeigt: Subagent-Befunde vor dem Weiterbauen selbst nachprüfen.)
- **Keine gemeinsame Storage-Schnittstelle:** `SharedPreferences.getInstance()`
  wird an 12 Stellen direkt aufgerufen, obwohl `core/settings_prefs.dart` bereits
  eine `StateNotifier`-Abstraktion (`BoolPref`, `StringSetPref`) anbietet – wird
  meist umgangen. `sqflite` wird zusätzlich unabhängig in `background_push.dart`
  nochmal geöffnet (eigene DB-Instanz fürs Background-Isolate). `media_cache.dart`
  ist dagegen sauber gekapselt – gutes Vorbild für die anderen Speicherzugriffe.
- **Toter Code bestätigt:** `features/chat/message_bubble.dart` (178 Zeilen,
  Klasse `MessageBubble`) wird nirgends instanziiert – von `message_group.dart`
  abgelöst, kann beim Refactoring entfernt werden.
- **Duplikate:** `_formatTime`/`_formatDate` unabhängig implementiert in
  `pinned_messages_panel.dart:206`, `message_group.dart:322` UND `:475` (zwei Mal
  im selben File!), sowie im toten `message_bubble.dart:80/165`.

**Modul-Kandidaten für den nächsten Punkt („Modul-Schnitt definieren"):**
1. `call_signaling` (Matrix-VoIP) – `core/voip_manager.dart`
2. `call_transport` (LiveKit) – `core/livekit_call_manager.dart`
3. `call_ui` (nur UI, konsumiert 1+2 über Interface) – `features/call/*`
4. `settings/*` aufsplitten: profile, encryption_verification, sessions,
   notifications, appearance, account – aus `widgets/settings_modal.dart`
5. `verification` als eigenständiges Feature (SAS/QR/Recovery-Key), nicht
   Teil von Settings – wiederverwendbar (z. B. künftig beim Login)
6. `storage` – gemeinsame Fassade, `settings_prefs.dart` als Pflichtzugang
   statt 12 Direktzugriffsstellen
7. `app_state.dart` auflösen in feature-lokale Provider-Dateien
8. `chat_timeline` – `message_group.dart` + `pinned_messages_panel.dart`,
   dabei toten Code (`message_bubble.dart`) entfernen, `_formatTime`/`_formatDate`
   in `chat/format_utils.dart` zusammenführen

**Offen/Nächster Schritt:** M2, zweiter Punkt – „Modul-Schnitt definieren":
aus der obigen Kandidatenliste konkrete Modul-Interfaces (Dart-Abstract-Classes
o. ä.) entwerfen, dann EIN Modul zuerst umbauen (Vorschlag: `call_*`, da am
klarsten abgegrenzt und am dringendsten laut CLAUDE.md-Leitprinzip) als Pilot,
bevor der Rest folgt. Kein Codeumbau in diesem Schritt, nur Schnittstellen-Design.

**Stolperfallen:** Keine – reine Lesearbeit, kein Risiko für Bestandsdaten.
Analyse wurde von einem Explore-Subagent erstellt und stichprobenartig
plausibilisiert (Zeilenzahlen/Imports selbst nachgezählt), nicht jede
Einzelbehauptung im Detail nachverifiziert.

**Fragen an Bernd:**
- SQLCipher-Priorität weiterhin offen (siehe Eintrag „3 schon vorhanden, 1 echte
  Lücke" weiter unten) – blockiert nicht, aber ohne Antwort bleibt der M1-Punkt
  bis zum nächsten PC-Praxistest liegen.
- Für M2: Soll ich die Call-Schicht als erstes Pilot-Modul umbauen (mein
  Vorschlag oben), oder hast du eine andere Präferenz (z. B. erst die
  `settings_modal.dart` aufsplitten, weil die am unübersichtlichsten ist)?

---

## 2026-07-03 – M1: Restliche Punkte geprüft – 3 schon vorhanden, 1 echte Lücke gefunden

**Erledigt:**
- **Logout-Warnung bei fehlendem Key-Backup** (Commit b5762ea): neue Funktion
  `isKeyBackupMissing(client)` in `bootstrap_dialog.dart` (prüft Cross-Signing +
  `keyManager.isCached()`). Beide Logout-Dialoge in `settings_modal.dart` zeigen
  jetzt eine rote Klartext-Warnung statt des neutralen Textes, wenn kein Backup
  eingerichtet ist.
- **Drei ROADMAP-Punkte waren bereits umgesetzt** (nur nicht abgehakt) – durch
  Code-Lektüre verifiziert, nicht neu gebaut:
  - Key-Backup einrichten/wiederherstellen: `bootstrap_dialog.dart` deckt beides
    über den `Bootstrap`-Zustandsautomaten des matrix-SDK ab (`askNewSsss` für
    Neueinrichtung, `askUnlockSsss` fragt auf einem neuen Gerät nach dem
    Recovery-Key). Wird automatisch getriggert, wenn Cross-Signing fehlt
    (`app_shell.dart:_triggerBootstrap`).
  - Geräte-/Sessionverwaltung: `_SessionsSection` in `settings_modal.dart` –
    Liste, Umbenennen, SAS/QR-Geräteverifizierung, Einzel- und
    Massen-Abmeldung (mit Passwort-Reauth bei UIA-Anforderung).
- **Echte Sicherheitslücke gefunden:** `sqlcipher_flutter_libs` ist eine
  Abhängigkeit in `pubspec.yaml`, wird aber **nirgends im Code benutzt**.
  `matrix_client.dart` (Haupt-App) und `background_push.dart` (Push-Hintergrund)
  öffnen `pyramid.sqlite` beide über den normalen, unverschlüsselten
  `sqflite`/`sqflite_common_ffi`-Factory. Die Krypto-DB (Access-Token,
  gepickelter Olm-Account, Megolm-Sessions, komplette Nachrichtenhistorie)
  liegt also **im Klartext** auf der Platte – nur durch Androids App-Sandbox
  geschützt, nicht durch eine eigene Verschlüsselung.

**Offen/Nächster Schritt:** SQLCipher-Anbindung NICHT blind auf dem Pi
umgesetzt – siehe Stolperfalle unten. Braucht einen eigenen, vorsichtig
geplanten PC-Termin: Backup der Test-DB, Migrationscode (bestehende
Klartext-DB einmalig nach SQLCipher überführen), Test auf einem Gerät mit
echten Daten, erst dann Rollout.

**Fragen an Bernd:**
- Priorität der SQLCipher-Lücke: Soll das vor M2 (Refactoring) angegangen
  werden, oder reicht die App-Sandbox als Schutz erstmal aus (Angreifer bräuchte
  Root oder physischen Zugriff + ADB, um die Datei überhaupt zu lesen)? Die
  Migration bestehender Installationen (Uta!) ist der aufwändige/riskante Teil,
  nicht die Verschlüsselung selbst.

**Stolperfallen:**
- Eine DB-Verschlüsselungs-Migration ist genau die Art Änderung, die laut
  CLAUDE.md nicht "mal eben" auf dem Pi ohne Testgerät gemacht werden darf:
  scheitert die Migration (falscher Schlüssel, falsche PRAGMA-Reihenfolge,
  Timing zwischen Haupt-App und Push-Hintergrund-Client), sind Utas
  Nachrichten und Krypto-Schlüssel unwiederbringlich weg. Deshalb hier nur
  dokumentiert, nicht implementiert.

---

## 2026-07-03 – M1: Push-Regression vermutlich dieselbe Ursache wie Random-Logout

**Erledigt:** Vor dem Blick in den Push-Code die Vermutung geprüft, ob die
Push-Entschlüsselungs-Regression und der Uta-Random-Logout-Bug (siehe Eintrag
weiter unten) zusammenhängen – Ergebnis: **sehr wahrscheinlich ja, exakt
dieselbe Ursache.**

- `client.getEventByPushNotification(...)` (aufgerufen in `_bgDecryptAndShow`,
  `background_push.dart:84`) ruft als ALLERERSTES `ensureNotSoftLoggedOut()`
  auf (matrix-Paket `client.dart:1878`). Diese Funktion prüft, ob der
  Access-Token in < 1 Minute abläuft, und löst dann `_handleSoftLogout()` aus
  — **dieselbe Funktion**, die ohne Refresh-Token (unser Zustand vor Commit
  9dc83f7) sofort einen echten `client.logout()` auslöst (siehe Eintrag unten).
- Der Hintergrund-Client aus `_buildClient()` liest **dieselbe** `pyramid.sqlite`
  wie der Haupt-Client. Ein `logout()`/`clear()` aus dem Hintergrund-Isolat
  würde also die komplette Session (inkl. Krypto-Schlüssel) für die GESAMTE
  App zerstören – nicht nur für den einen Push. Das erklärt, warum das Problem
  als dauerhafte Regression erscheint ("seit dem letzten Update nie wieder"):
  Nach dem ersten Auto-Logout-Ereignis (ausgelöst beim Sync ODER beim
  Entschlüsseln eines Pushs) sind die Schlüssel weg, jede folgende
  Entschlüsselung schlägt fehl → `BgEngine` zeigt nur noch den
  „Neue Nachricht"-Platzhalter (Fallback nach 6 s), nie mehr den echten Text.
- Der bereits committete Fix (`refreshToken: true` beim Login, 9dc83f7) behebt
  damit vermutlich BEIDE Symptome gleichzeitig: der Refresh-Token liegt in der
  gemeinsamen DB, also kann auch der Hintergrund-Client ihn lesen und einen
  drohenden Soft-Logout durch stillen Refresh abwenden, statt die Session zu
  zerstören.

**Offen/Nächster Schritt:** Gehört zum selben Praxistest wie der
Random-Logout-Punkt oben – zusätzlich beobachten, ob nach einer Weile
(mehrere Stunden/Tage) echte Push-Inhalte statt Platzhalter ankommen. Schlägt
das fehl, liegt eine zweite, unabhängige Ursache vor (dann `docs/NOTIFICATIONS.md`
+ Memory „Pyramid Push" erneut konsultieren, insbesondere die 3 dort gelisteten
Fallstricke der Hintergrund-Entschlüsselung).

**Stolperfallen:** Diese Verbindung wurde rein durch Lesen des matrix-SDK-
Quellcodes hergeleitet (`~/.pub-cache/hosted/pub.dev/matrix-6.2.0/lib/src/client.dart`),
nicht durch Beobachtung auf einem echten Gerät – bewusst als Vermutung
markiert, bis der PC-Praxistest sie bestätigt.

---

## 2026-07-03 – M1: Uta-Random-Logout – Ursache gefunden + Fix (UNGETESTET/Pi)

**Erledigt:**
- Alle Logout-Auslöser im Code kartiert:
  - 3 bewusste Nutzer-Logout-Buttons in `settings_modal.dart` (alle hinter
    Bestätigungsdialog) – kein Kandidat für "random"
  - Der eigentliche Verdächtige: `matrix`-Paket (v6.2.0) `Client._innerSync()`
    behandelt `M_UNKNOWN_TOKEN` vom `/sync`-Endpunkt so: bei `soft_logout: true`
    → `_handleSoftLogout()` → versucht `refreshAccessToken()` → **das SDK
    hatte nie einen Refresh-Token angefordert** (`client.login(...)` in
    `login_notifier.dart` rief ohne `refreshToken: true` auf) → `refreshAccessToken()`
    wirft sofort "No refresh token available" → Catch-Block ruft `await logout()`
    auf: ein **echter, destruktiver Logout**, ausgelöst rein durch einen normalen
    Soft-Logout (z. B. abgelaufenes Access-Token), ganz ohne Nutzerinteraktion.
    Quelle: `~/.pub-cache/hosted/pub.dev/matrix-6.2.0/lib/src/client.dart:2374-2384`
    und `:2496-2509`.
- **Logging (Commit 63e4919):** neue `lib/core/auth_log.dart` schreibt jeden
  Login-Status-Wechsel (`client.onLoginStateChanged`) sowie SDK-Warnungen/-Fehler
  (`Logs().onLog`) in `auth_log.txt` im App-Support-Verzeichnis – übersteht
  App-Neustart. Zusätzlich loggen alle 3 Nutzer-Logout-Buttons sich selbst, damit
  sich künftig unterscheiden lässt: Nutzer-Logout vs. SDK-Auto-Logout. Reine
  Zusatzfunktion, keine Verhaltensänderung.
- **Fix (Commit 9dc83f7):** `client.login(...)` fordert jetzt `refreshToken: true`
  an. Damit kann das SDK einen Soft-Logout künftig durch stillen Token-Refresh
  überstehen, statt die Sitzung zu zerstören. Unterstützt der Server keine
  Refresh-Tokens, bleibt das Verhalten exakt wie vorher (`response.refreshToken`
  ist dann `null`) – der Fix kann also nichts verschlimmern.

**Offen/Nächster Schritt:**
1. **Praxistest auf dem PC (zwingend vor Haken in ROADMAP!):** Login → Nachrichten
   senden/lesen → Logout → erneuter Login → alte verschlüsselte Nachrichten noch
   lesbar? Dabei prüfen, ob `auth_log.txt` (App-Support-Verzeichnis) die
   Login-Status-Wechsel sauber mitschreibt.
2. Danach: den nächsten Punkt in M1 angehen (Push-Entschlüsselungs-Regression).
3. **Wichtig für Bernd/Uta:** Der Fix wirkt nur für NEUE Logins. Utas bereits
   bestehende Sitzung bekommt erst nach einem einmaligen manuellen Neu-Login
   einen Refresh-Token und ist bis dahin weiterhin anfällig. Sobald der PC-Test
   grün ist, sollte Uta sich einmal aus- und wieder einloggen.

**Stolperfallen:**
- Konnte auf dem Pi nicht mit echtem Login/Logout getestet werden (kein GUI,
  kein Matrix-Testaccount zur Hand) – Fix beruht auf genauem Lesen des
  matrix-SDK-Quellcodes, nicht auf Beobachtung im Betrieb. Deshalb zwingend
  vor dem nächsten Haken in ROADMAP am PC nachprüfen.
- `chat_provider.dart:42` nutzt weiterhin das deprecated `Timeline.onUpdate`
  (Re-Entschlüsseln nach Schlüssel-Empfang) – hängt mit demselben Sync-Bereich
  zusammen, aber bewusst nicht angefasst, um nicht zwei riskante Änderungen im
  selben Bereich gleichzeitig ungetestet zu committen.

---

## 2026-07-03 – M0 abgeschlossen: Ordnung, flutter analyze sauber, README

**Erledigt:**
- Liegen gebliebene Deprecation-Fixes aus einer abgebrochenen Session committet
  (`withOpacity`→`withValues`, `activeColor`→`activeThumbColor`, `cacheExtent`→
  `ScrollCacheExtent`, `PublicRoomsChunk`→`PublishedRoomsChunk`) – Commit 057ac1e
- Restliche `flutter analyze`-Hinweise behoben – Commit 401bd6a:
  - `document_viewer.dart`: `Matrix4.translate/scale` → `translateByDouble`/
    `scaleByDouble` (PDF-Zoom, gleiches Verhalten)
  - `rooms_panel.dart`: `onReorder` → `onReorderItem` (Index-Korrektur jetzt intern,
    manuelles `newIdx--` entfernt); `implementation_imports`-Hinweis kommentiert
    (SpaceChild wird vom `matrix`-Paket nicht öffentlich exportiert, kein Fix möglich)
  - `voip_manager.dart`: `getSources()`-Override kommentiert (Pflicht-Override der
    abstrakten `MediaDevices`-Klasse, die die Methode selbst deprecated hat)
  - `settings_modal.dart`: zwei `BuildContext`-nach-`await`-Stellen mit
    `context.mounted` abgesichert (Geräte umbenennen, Passwortabfrage bei Massenlogout)
  - **Bewusst NICHT gefixt:** `chat_provider.dart:42` (`Timeline.onUpdate` deprecated,
    Ersatz wäre `client.onSync` + Raum-Filter). Hängt am E2EE-Redecrypt-Pfad
    (Re-Entschlüsseln nach Schlüssel-Empfang) – laut CLAUDE.md heiliger Bereich,
    Umbau nur mit dediziertem Login→Nachrichten→Logout→Login-Test, nicht nebenbei.
- `lib/features/calls/` (Leiche) geprüft: existiert nicht mehr, nur `lib/features/call/`
  mit den echten Dateien – Punkt war bereits erledigt, nur nicht abgehakt.
- README.md durch echte Projektbeschreibung ersetzt (Funktionsumfang, Build, Struktur) – Commit 74d38fd
- ROADMAP.md: M0 komplett abgehakt.

**Offen/Nächster Schritt:** M1, erster Punkt – Uta-Random-Logout-Bug untersuchen
(Logging an allen Logout-Pfaden einbauen, dann Ursache finden). Direkt danach die
Push-Entschlüsselungs-Regression (`docs/NOTIFICATIONS.md` + Memory „Pyramid Push"
vorher lesen).

**Stolperfallen:**
- Kein `test/`-Ordner vorhanden → `flutter test` läuft ins Leere, keine automatisierte
  Absicherung. `flutter analyze` lief sauber (0 Fehler/Warnungen), aber die Lint-Fixes
  in `document_viewer.dart` (PDF-Zoom) und `rooms_panel.dart` (Drag-Reorder der
  Raumliste) sind **UNGETESTET** in echter UI – nächster Windows-Lauf sollte kurz
  PDF-Zoom und Raum-Umsortieren per Drag prüfen.
- Der `ignore:`-Kommentar für einen deprecated-Aufruf muss auf der Zeile *unmittelbar*
  über der betroffenen Codezeile stehen, nicht nur "in der Nähe" – sonst wirkt er nicht
  (bei `voip_manager.dart` erst falsch über `@override` gesetzt, dann korrigiert).

---

## 2026-07-03 – Arbeitsstruktur angelegt + Vollbackup (Setup, noch kein Code)

**Erledigt:**
- Vollbackup inkl. Git-Historie: `MatrixPi\backups\pyramid-kopie_backup_2026-07-03.tar.gz` (510 MB)
- `CLAUDE.md` (Arbeitsregeln), `ROADMAP.md` (M0–M7 nach Bernds Prioritäten), dieses Protokoll
- Klargestellt: `C:\Users\nordm\pyramid - Kopie` ist das EINZIGE aktuelle Repo;
  `C:\Users\nordm\pyramid` und `MatrixPi\pyramid` sind veraltete April-Stände (Commit c9af913)

**Offen/Nächster Schritt:** M0, Punkt 2 – die ~140 uncommitteten Dateien mit `git status`
sichten und in thematische Commits aufteilen (diese drei neuen Dateien mit committen),
dann `git push origin master`.

**Stolperfallen:**
- Letzter Commit ist vom 2026-04-28 (d706ace), gearbeitet wurde aber bis mindestens
  2026-06-12 → zwischen Commits und Realität liegen 6 Wochen. Genau deshalb: ab jetzt
  kleine Commits nach jedem Schritt.
- Git-Remote (Gitea auf dem Pi) enthält Zugangsdaten in der URL – funktioniert, aber
  nur erreichbar, wenn der Pi läuft (unterwegs: WireGuard nötig).
