Files
pyramid/PROGRESS.md
T

98 KiB
Raw Blame History

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-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.

  • (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).

  • (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.
  • (c) analyze-/Lint-Fixes (9dc1175, 133a8ac, 99f10c5, 2395db5, 057ac1e, 401bd6a) auf Verhaltensänderungen geprüft alle äquivalent, 1 Kleinigkeit gefunden (bewusst belassen):

    • onReorderonReorderItem (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/scaletranslateByDouble/scaleByDouble (PDF-Zoom): mathematisch äquivalent (uniforme Skalierung, w=1).
    • withOpacitywithValues, activeColoractiveThumbColor, cacheExtentscrollCacheExtent, PublicRoomsChunkPublishedRoomsChunk, 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.
  • (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.

  • (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.

  • (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).

  • (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.

  • (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.

  • (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.

  • (j) PC-Testplan-Update (03b84e0) geprüft stimmt. Teststand „22 Tests" war korrekt (durch (i) jetzt 24 im Testplan nachgezogen, siehe Eintrag oben).

  • (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.

  • (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.

  • (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.

  • (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, expnbf = 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.

  • (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.

  • (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.

  • (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.

  • (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.

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-ifs 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.dartfeatures/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 (withOpacitywithValues, activeColoractiveThumbColor, cacheExtentScrollCacheExtent, PublicRoomsChunkPublishedRoomsChunk) Commit 057ac1e
  • Restliche flutter analyze-Hinweise behoben Commit 401bd6a:
    • document_viewer.dart: Matrix4.translate/scaletranslateByDouble/ scaleByDouble (PDF-Zoom, gleiches Verhalten)
    • rooms_panel.dart: onReorderonReorderItem (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 (M0M7 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).