Compare commits

...

33 Commits

Author SHA1 Message Date
Bernd Steckmeister 1ef32c0022 chore: CHANGES.md (auto-Hook) 2026-07-05 14:54:38 +02:00
Bernd Steckmeister 66bf54bdff docs: Dashboard bleibt Heimnetz-only (Bernd Nr. 3) – ROADMAP-Punkt geschlossen, Doku nachgezogen, Nr.-4-Vermerk am Secrets-Punkt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:53:27 +02:00
Bernd Steckmeister 81ec06398f docs: Fable-5-Review (v)+(w) – PC-Commits 825481e/4477ed9 geprüft, Fix korrekt, 1 Doku-Lücke (Entscheidung 3 nicht nachgezogen)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:52:09 +02:00
Bernd Steckmeister 4477ed9101 fix: Hauptkonto-Profil ohne CLAUDE_CONFIG_DIR ansprechen (Config-Pfad-Falle)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-05 14:47:35 +02:00
Bernd Steckmeister 825481e6fa docs: Bernds Richtungsfragen beantwortet (ANTWORTEN_BERND.md) - SQLCipher vor M2, Call-Pilot zuerst, Dashboard Heimnetz-only, History unangetastet 2026-07-05 07:41:36 +02:00
Bernd Steckmeister db4d001ef1 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:06:53 +02:00
Bernd Steckmeister af7c538e18 docs: Fable-5-Review (u) – (t)-Fix verifiziert, server.py-Review-Kette konvergiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:44:55 +02:00
Bernd Steckmeister 4c6efa2a7a chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:22:29 +02:00
Bernd Steckmeister d3e75c36a1 security: server.py Restbefund aus Review (t) – registration-status antwortet generisch statt str(e)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:21:19 +02:00
Bernd Steckmeister 326cc598c7 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:57:29 +02:00
Bernd Steckmeister 13b333cbec security: server.py Restbefunde aus Review (s) – ban/unban-Body vor ADMIN_LOCK, generische 500er auf oeffentlichen Routen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:56:42 +02:00
Bernd Steckmeister 4d4044b888 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:48:07 +02:00
Bernd Steckmeister 5ada0e5038 security: server.py Restbefunde aus Review (r) – GET-Leser-Lock, Content-Length-Haertung, secrets-Token
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:46:27 +02:00
Bernd Steckmeister c8ff850c98 security: server.py Admin-Routen serialisieren (ADMIN_LOCK, Review-Befund q)
Threading-Umstellung aus (p) hatte eine Nebenlaeufigkeits-Race in den
Admin-Routen erzeugt: conduit.toml Read-Modify-Write + docker restart und
der send_admin-Readback-nach-sleep waren seit multithreaded race-faehig.
ADMIN_LOCK serialisiert NUR die Admin-Mutationen; die oeffentlichen Routen
(livekit-token, e2ee-diagnostics) laufen zuerst und bleiben nebenlaeufig,
der Slow-Loris-Schutz aus (p) bleibt erhalten. Headless verifiziert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:20:42 +02:00
Bernd Steckmeister d42beaf7ab chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:52:07 +02:00
Bernd Steckmeister 343545b59e security: server.py multithreaded + Diag-Log-Lock (Review-Befund p, Threading-Haertung vollendet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:51:32 +02:00
Bernd Steckmeister 455742d1bf security: Dashboard-Admin-Routen per Interims-Gate auf Heimnetz begrenzt (Befund o)
server.py (auf dem Pi, nicht im Repo): /api/ban, /api/unban,
/api/toggle-registration, /api/create-invite, /api/run-stats nur noch fuer
Heimnetz-/localhost-Socket-IPs ohne Cloudflare-Header; sonst 403 mit
Erklaerung. Headless verifiziert (9 Pruefungen). Bernds A/B-Entscheidung
fuer den Fernzugriff bleibt offen (docs/DASHBOARD_AUTH_HARDENING.md).
Zusatzbefund: Repo privat schalten wuerde In-App-Updater brechen (ROADMAP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:17:56 +02:00
Bernd Steckmeister e97a748b35 security: KRITISCH – Dashboard-Admin-Endpoints ohne Auth (Review-Befund o)
Fable-5-Review (n)+(o) beim Gegenlesen von server.py auf dem Pi:

(o) KRITISCH: /api/ban, /api/unban, /api/toggle-registration, /api/create-invite,
/api/run-stats sind über https://dashboard.steggi-matrix.work oeffentlich
erreichbar und pruefen KEINEN Token (per curl belegt: App-eigene 400-Antwort
ueber die oeffentliche URL). Fremde koennten Uta sperren oder offene
Registrierung aktivieren; Hostname ist ueber CT-Logs auffindbar. Nicht blind
gefixt (jede Auth-Ergaenzung legt Bernds Dashboard bis zur Token-Eingabe lahm
-> kein Aussperren). Fertiger Plan mit zwei Wegen (Cloudflare Access / DASH_TOKEN)
in docs/DASHBOARD_AUTH_HARDENING.md, ROADMAP M0 dringend, wartet auf Bernds Wahl.

(n) LiveKit-Token-Route POST /api/livekit-token wurde von einer abgebrochenen
Session bereits live in server.py gebaut (unprotokolliert). Geprueft + headless
verifiziert (401/400/413/200, JWT-Signatur unabhaengig gegen livekit.yaml
gueltig, Identitaet = gepruefte user_id). Server-Seite des M0-LiveKit-Punkts
erledigt; Client-Umstellung + Rotation bleiben PC/Geraet (heiliger Call-Pfad).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 08:43:42 +02:00
Bernd Steckmeister ba97f942d6 security: Diagnose-Endpoint gegen Disk-Fill haerten (Review-Befund l)
Fable-5-Review (l)+(m): /api/e2ee-diagnostics auf dem Pi las den Body ohne
Groessenlimit und haengte ihn unbegrenzt ans Log – bei oeffentlich bekanntem
DIAG_TOKEN ein Disk-Fill-DoS-Risiko. server.py: 256-KB-Body-Limit (413) +
20-MB-Log-Kappung, headless verifiziert (200/403/413). LiveKit-Plan (m)
gegen Code + Pi-Konfig gegengeprueft, stimmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:32:38 +02:00
Bernd Steckmeister 88af6c016c chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:07:39 +02:00
Bernd Steckmeister 78e4174658 docs: LiveKit-Token-Minting-Migrationsplan (apiSecret raus aus Client)
Fertiger Umsetzungsplan fuer den offenen M0-Punkt: Token-Endpoint am
Dashboard-Server mintet LiveKit-JWTs server-seitig (Matrix-whoami-Auth,
stdlib-HMAC), Client holt nur das JWT. Braucht echten Call-Test -> nur Plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:06:53 +02:00
Bernd Steckmeister 925aafb2fe security: Admin-Token aus Client entfernen (E2EE-Diagnose entkoppeln)
Der Dashboard-Server benutzte fuer /api/e2ee-diagnostics denselben String
wie den Matrix-Admin-Token, weshalb der Client den vollen Server-Admin-Token
einbetten musste (jeder Nutzer, auch Uta, trug ihn im Geraet).

- server.py (Pi): eigener, eng begrenzter DIAG_TOKEN statt Admin-TOKEN
- settings_encryption.dart: benutzt den neuen Diagnose-Token
- grep J5lax lib/ jetzt leer; Endpoint verifiziert (neu 200, alt 403)

Admin-Token-Rotation bleibt offen (Bernds Sache, ROADMAP M0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:04:15 +02:00
Bernd Steckmeister fe6427bfba security: geleakte Secrets aus Repo entfernen (Review-Befund k)
Secret-Scan im Fable-5-Review fand vier aktive Zugangsdaten im
oeffentlich erreichbaren Gitea-Repo. Gefahrlos entfernbare Stellen:
- scripts/release.ps1: Gitea-Token -> Umgebungsvariable PYRAMID_GITEA_TOKEN
- docs/matrix-sdk/13-server-admin: 19x Admin-Token -> <ADMIN_TOKEN>

Client-eingebettete Secrets (LiveKit apiSecret, Admin-Token im
E2EE-Diagnose-Upload, Giphy-Key) brauchen Rotation + Server-Umbau
und stehen als neue M0-ROADMAP-Punkte (nicht blind auf dem Pi fixbar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 03:32:37 +02:00
Bernd Steckmeister a5a5c4baf5 docs: Fable-5-Review um Punkte (h)-(j) ergänzt, Testplan auf 24 Tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:22:15 +02:00
Bernd Steckmeister 7b90f20e36 fix: Lade-Race in BoolPref/StringSetPref (Review-Befund)
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 Test am
alten Code bewiesen). Jetzt warten add/remove auf _loaded. BoolPref bekommt
einen _userSet-Guard, damit ein spät landender persistierter Wert nie eine
schon getätigte Nutzeraktion zurücküberschreibt (Reihenfolge ist plattform-
abhängig). In der Praxis kaum treffbar (Provider laden beim App-Start),
aber die Fassade wird beim M2-Storage-Umbau Pflicht-Schnittstelle für
neue, ggf. früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:20:49 +02:00
Bernd Steckmeister 03b84e0e0e docs: PC-Testplan Schritt 0 auf aktuellen Teststand (22 Tests) gebracht
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:53:42 +02:00
Bernd Steckmeister 7dfdbf1159 test: Vertragsnetz für die Storage-Fassade settings_prefs.dart
8 Tests mit dem offiziellen SharedPreferences-In-Memory-Mock: BoolPref,
StringSetPref, Theme-Prefs (Roundtrip/Teil-Save), Server-Banner inkl.
korruptes-JSON-Fehlerpfad. Sichert die Schnittstelle ab, die beim
M2-Storage-Umbau zur Pflicht-Fassade wird. Alle 22 Tests grün auf dem Pi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:53:26 +02:00
Bernd Steckmeister e0ac5cb9fd test: AuthLog-Regressionstests (wirft nie, 500-Zeilen-Kappung)
AuthLog.write ist der einzige Aufruf im catch-Pfad des Soft-Logout-Guards;
ein Throw dort würde im SDK zu logout()+clear() eskalieren. 4 neue Tests
sichern genau das ab (kaputte Plattform, leeres Log, Zeitstempel, Kappung).
path_provider_platform_interface als dev_dependency (war schon transitiv).
Alle 14 Tests grün auf dem Pi, flutter analyze unverändert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:51:35 +02:00
Bernd Steckmeister eebf8ae7dd docs: Fable-5-Review um Nachzügler-Punkte (e-g) ergänzt
Tests 99cde9a, Doku-Commits 16561bb/566938f und Autopilot-Konto-Wechsel
bfe5876 kritisch geprüft: keine Fehler, alle Behauptungen gegen Code/SDK
verifiziert. Damit sind ALLE als erledigt markierten Punkte reviewt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:49:00 +02:00
Bernd Steckmeister 566938f004 docs: PC-Testplan um flutter-test-Vorabschritt ergänzt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:23:42 +02:00
Bernd Steckmeister 1630d0e389 chore: Commit-Hash im PROGRESS-Eintrag nachgetragen + Hook-Log
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:23:12 +02:00
Bernd Steckmeister 99cde9a1e2 test: Regressionstests für Soft-Logout-Guard und Power-Levels-Schutz
Erste Testsuite des Projekts, gezielt für die zwei heiligsten Codepfade:
guardedSoftLogoutRefresh (wirft nie -> kein SDK-logout()+clear(), Uta-Bug)
und updatePowerLevelsSafely (Selbst-Aussperr-Schutz, Server-Stand bleibt
erhalten). 10 Tests, alle gruen auf dem Pi (flutter test laeuft headless).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:22:06 +02:00
Bernd Steckmeister bfe58760ee feat: automatischer Konto-Wechsel (pyramid<->haupt) bei 5h-Limit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 19:33:13 +02:00
19 changed files with 2073 additions and 37 deletions
+42
View File
@@ -0,0 +1,42 @@
# Bernds Entscheidungen Richtungsfragen aus PROGRESS.md beantwortet
Diese Datei beantwortet die über mehrere Sessions in PROGRESS.md gesammelten
„Fragen an Bernd". Sie ist ab jetzt **maßgeblich** die betroffenen Punkte in
ROADMAP.md/PROGRESS.md gelten als entschieden.
Zuletzt aktualisiert: **2026-07-05** (Bernd + Claude am PC).
## 1. SQLCipher (lokale DB + Access-Token verschlüsseln) → VOR M2 priorisieren
- **Entscheidung:** Ja. Die Verschlüsselung der lokalen `pyramid.sqlite` hat Vorrang
vor dem M2-Refactoring. Betrifft den M1-Punkt „Sichere Speicherung von Access-Token
& Krypto-DB".
- Umsetzung nach `docs/SQLCIPHER_MIGRATION.md`. **Wichtig:** Die Migration bestehender
Klartext-DBs (Utas Gerät!) ist der riskante Teil und braucht einen echten
PC-/Gerätetest. Auf dem Pi (kein GUI) nur so weit vorbereiten, wie es OHNE GUI sicher
testbar ist; alles Verhaltensrelevante klar als UNGETESTET markieren und erst nach
`docs/PC_TESTPLAN.md` als erledigt abhaken. Grundsatz „Kein Datenverlust" im Zweifel
für den PC-Termin liegen lassen.
## 2. M2 erstes Umbau-Modul → Call-Pilot zuerst
- **Entscheidung:** Wenn M2 startet (nach SQLCipher), zuerst die Call-Schicht entwirren
(Call-Pilot / gemeinsame Fassade über `voip_manager.dart` + `livekit_call_manager.dart`),
wie in `docs/M2_MODULE_SCHNITT.md` vorgeschlagen. **Nicht** zuerst `settings_modal.dart`
aufsplitten.
- **Wichtig:** heiliger Call-Pfad → braucht zwingend echten Call-Test auf einem Gerät.
Auf dem Pi nicht blind umbauen; Vorbereitung/Analyse ok, Umsetzung + Test am PC.
## 3. Dashboard-Fernzugriff (stats.html) → Heimnetz-only lassen
- **Entscheidung:** KEIN Fernzugriff. Das Interims-Gate (nur Heimnetz/localhost) bleibt so.
Die offene A/B-Frage (Cloudflare Access vs. DASH_TOKEN) aus
`docs/DASHBOARD_AUTH_HARDENING.md` ist damit **geschlossen** nicht weiter daran
arbeiten. Unterwegs per WireGuard erreichbar.
## 4. Git-History / Admin-Token → vorerst NICHTS tun
- **Entscheidung:** Kein Git-History-Rewrite und aktuell keine Token-Rotation durch den
Autopilot. Der Admin-Token ist bereits aus dem App-Code heraus (eigener `DIAG_TOKEN`).
Eine spätere Rotation bleibt Bernds manuelle Sache nicht eigenmächtig anstoßen.
---
**Kurzfassung fürs Arbeiten:** SQLCipher vor M2, dann Call-Pilot als erstes M2-Modul.
Dashboard bleibt Heimnetz-only. History unangetastet. Riskante/GUI-relevante Teile
warten auf den PC-Termin (`docs/PC_TESTPLAN.md`); auf dem Pi nur sicher Vorbereitbares.
+106
View File
@@ -1199,3 +1199,109 @@
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md 2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md 2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md 2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
2026-07-03 18:16 [Write] /home/steggi/pyramid/test/soft_logout_guard_test.dart
2026-07-03 18:17 [Write] /home/steggi/pyramid/test/power_levels_safety_test.dart
2026-07-03 22:21 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:22 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:23 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-03 22:23 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
2026-07-03 22:48 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:50 [Write] /home/steggi/pyramid/test/auth_log_test.dart
2026-07-03 22:50 [Edit] /home/steggi/pyramid/pubspec.yaml
2026-07-03 22:51 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:52 [Write] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 22:53 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 22:53 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
2026-07-03 22:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
2026-07-03 23:19 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
2026-07-03 23:20 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
2026-07-03 23:21 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-03 23:21 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
2026-07-03 23:22 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 03:28 [Edit] /home/steggi/pyramid/scripts/release.ps1
2026-07-04 03:31 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 03:32 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 03:32 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 04:01 [Edit] /home/steggi/matrix/server.py
2026-07-04 04:02 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart
2026-07-04 04:03 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 04:03 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 04:06 [Write] /home/steggi/pyramid/docs/LIVEKIT_TOKEN_MIGRATION.md
2026-07-04 04:06 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 04:06 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 04:07 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
2026-07-04 04:31 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 04:32 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
2026-07-04 08:42 [Write] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-04 08:42 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 08:43 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 09:18 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
2026-07-04 13:50 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 13:51 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 13:51 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 13:51 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 14:17 [Write] /home/steggi/matrix/server.py
2026-07-04 14:19 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 14:20 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 14:20 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 14:43 [Edit] /home/steggi/matrix/server.py
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
2026-07-04 14:45 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 14:46 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 14:46 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 14:47 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 18:56 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 18:57 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 19:19 [Edit] /home/steggi/matrix/server.py
2026-07-04 19:20 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 19:21 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 19:21 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 19:22 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-04 19:44 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-04 19:45 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-05 14:51 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-05 14:53 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
2026-07-05 14:53 [Edit] /home/steggi/pyramid/PROGRESS.md
2026-07-05 14:54 [Write] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand-haupt.md
2026-07-05 14:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
2026-07-05 14:54 [Edit] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/MEMORY.md
+3
View File
@@ -9,6 +9,9 @@ Einsatz (u. a. Uta) Stabilität und Datensicherheit gehen vor Feature-Tempo.
(`C:\Users\nordm\pyramid` und `C:\Users\nordm\MatrixPi\pyramid` sind veraltete (`C:\Users\nordm\pyramid` und `C:\Users\nordm\MatrixPi\pyramid` sind veraltete
April-Schnappschüsse dort NIE arbeiten.) April-Schnappschüsse dort NIE arbeiten.)
> **Bernds Entscheidungen zu den offenen Richtungsfragen stehen in `ANTWORTEN_BERND.md`
> (zuletzt 2026-07-05) vor dem Abarbeiten von ROADMAP-Punkten lesen, sie sind maßgeblich.**
## Infrastruktur ## Infrastruktur
- Homeserver: Continuwuity auf dem Pi5 (`steggi-matrix.work`) - Homeserver: Continuwuity auf dem Pi5 (`steggi-matrix.work`)
+960
View File
@@ -13,6 +13,744 @@ Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:
--- ---
## 2026-07-05 Dashboard-ROADMAP-Punkt GESCHLOSSEN (Bernds Entscheidung 3 nachgezogen)
**Erledigt:** Folge aus Review-Befund (v): Bernds Entscheidungen 3 und 4 aus
`ANTWORTEN_BERND.md` sind jetzt überall nachgezogen, damit keine künftige
Session die geschlossenen Fragen wieder aufmacht:
- **ROADMAP M0 „Dashboard-Admin-Endpoints": abgehakt.** Das Heimnetz-Gate ist
laut Bernd der Endzustand (kein Fernzugriff, unterwegs WireGuard, A/B-Frage
geschlossen). Restrisiken (öffentliche Nutzerlisten-Ansicht, theoretisches
LAN-CSRF) bewusst akzeptiert im ROADMAP-Punkt und in
`docs/DASHBOARD_AUTH_HARDENING.md` vermerkt.
- **`docs/DASHBOARD_AUTH_HARDENING.md`:** Status-Kopf auf „ABGESCHLOSSEN"
umgestellt, „Interims-Gate" heißt jetzt Endzustand, Weg A/B als Archiv
markiert, „bis zur A/B-Entscheidung"-Formulierungen bereinigt.
- **ROADMAP M0 Secrets-Punkt:** Vermerk zu Entscheidung 4 ergänzt Rotation
und History-Rewrite ruhen bewusst (Bernds manuelle Sache, nicht vom
Autopilot anstoßen oder nachfragen); der Punkt bleibt nur als Merkposten
offen. KEINE Code-/Serveränderung in diesem Schritt, nur Doku
`server.py` und das Gate sind unangetastet.
**Offen/Nächster Schritt:** In M0 sind damit nur noch zwei Punkte offen, beide
nicht Pi-bearbeitbar: SQLCipher (PC-/Gerätetest, laut Entscheidung 1 als
Erstes am PC) und der Härtetest (`docs/PC_TESTPLAN.md`). Danach M2 mit
Call-Pilot (Entscheidung 2, Gerätetest nötig). Auf dem Pi bleibt ohne neuen
Anlass nichts sicher Bearbeitbares Session darf sauber enden.
**Stolperfallen:** Keine reine Doku-Angleichung. Lehre aus dem
(v)-Befund: Wenn Entscheidungen in einer eigenen Datei landen
(`ANTWORTEN_BERND.md`), müssen die referenzierten offenen Punkte in
ROADMAP/Docs im SELBEN Zug geschlossen/annotiert werden, sonst stellt die
nächste Session die beantwortete Frage erneut.
---
## 2026-07-05 Fable-5-Review (v)+(w): die zwei PC-Commits geprüft Fix korrekt, 1 Doku-Lücke gefunden
**Erledigt:** Review-Pass über die seit (u) dazugekommene Arbeit die zwei am
PC entstandenen Commits als Punkte (v) und (w) im Fable-5-Review-Abschnitt
abgehakt:
- **(v) 825481e (`ANTWORTEN_BERND.md` + CLAUDE.md-Verweis):** inhaltlich
konsistent alle referenzierten Pläne/Dateien existieren
(`docs/SQLCIPHER_MIGRATION.md`, `docs/M2_MODULE_SCHNITT.md`,
`docs/DASHBOARD_AUTH_HARDENING.md`, `docs/PC_TESTPLAN.md`;
`voip_manager.dart`/`livekit_call_manager.dart` liegen in `lib/core/`), die
vier Entscheidungen decken exakt die in PROGRESS.md gesammelten Fragen ab.
**Befund (Doku-Lücke):** Entscheidung 3 (Dashboard bleibt Heimnetz-only,
A/B-Frage GESCHLOSSEN) war noch nicht in ROADMAP.md und
`docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen beide präsentierten die
Frage weiterhin als offen, künftige Sessions hätten Bernd erneut gefragt.
Direkt im nächsten Schritt dieser Session behoben (eigener Commit,
Eintrag oben).
- **(w) 4477ed9 (`autopilot.sh`, CLAUDE_CONFIG_DIR-Falle):** Fix ist korrekt
und am lebenden System belegt: Mit gesetztem `CLAUDE_CONFIG_DIR` sucht die
Claude-CLI ihre Config unter `$CLAUDE_CONFIG_DIR/.claude.json` das
Hauptkonto-Profil liegt aber unter `~/.claude.json`, das alte
`export CLAUDE_CONFIG_DIR="$HOME/.claude"` zeigte also auf ein leeres
Zweitprofil (`~/.claude/.claude.json`). `unset` ist richtig und wirkt auch
nach einer vorherigen pyramid-Iteration derselben Shell (apply_account
läuft vor jeder Session). Lebender Beweis: DIESE Session lief als Konto
„haupt" (`~/.claude-accounts/active` = haupt, `CLAUDE_CONFIG_DIR` leer,
`~/.claude.json` vorhanden) und funktionierte. Kein weiterer
`CLAUDE_CONFIG_DIR`-Verweis im Repo. Kein Befund.
**Offen/Nächster Schritt:** Befund aus (v) nachziehen (ROADMAP-Dashboard-Punkt
schließen, Doku-Status aktualisieren, Vermerk zu Entscheidung 4 am
Secrets-Punkt) passiert direkt als nächster Schritt dieser Session.
**Stolperfallen:** Der CHANGES.md-Hook (in `.claude/settings.json`, schreibt
relativ nach `CHANGES.md`) protokolliert nur Arbeit auf der jeweiligen
Maschine die PC-Commits vom 2026-07-05 haben hier auf dem Pi KEINE
Hook-Einträge hinterlassen. Der „Hook-Log als Detektor für unprotokollierte
Arbeit"-Trick aus Review (p) funktioniert also nur pro Maschine; für
PC-Arbeit bleibt `git log` die einzige Quelle.
---
## 2026-07-05 Bernds Richtungsfragen beantwortet (am PC)
**Erledigt:** Die über mehrere Sessions gesammelten „Fragen an Bernd" sind beantwortet
und in `ANTWORTEN_BERND.md` festgehalten (maßgeblich, in CLAUDE.md verlinkt):
1. SQLCipher → **vor M2** priorisieren (M1-Punkt); Migration braucht PC-/Gerätetest.
2. M2 erstes Modul → **Call-Pilot zuerst** (nicht `settings_modal.dart`); braucht Gerätetest.
3. Dashboard-Fernzugriff → **Heimnetz-only lassen**; A/B-Frage geschlossen.
4. Git-History / Token-Rotation → **vorerst nichts tun** (Bernds manuelle Sache).
**Offen/Nächster Schritt:** Die entschiedenen Punkte bleiben PC-/gerätegebunden
(SQLCipher-Migration, Call-Pilot-Umbau). Auf dem Pi ohne GUI nur sicher Vorbereitbares
angehen und klar als UNGETESTET markieren; Verhaltensrelevantes erst nach
`docs/PC_TESTPLAN.md` als erledigt abhaken. Reihenfolge: erst SQLCipher, dann Call-Pilot.
**Stolperfallen:** Beide priorisierten Punkte sind trotz Entscheidung NICHT auf dem Pi
abschließbar (kein GUI-/Gerätetest) nicht als „jetzt endlich machbar" missverstehen.
---
## 2026-07-04 Fable-5-Review (u): (t)-Fix geprüft korrekt, KEIN Befund, server.py-Review-Kette abgeschlossen
**Erledigt:** Review-Pass über die letzte noch ungeprüfte Arbeit den
(t)-Restbefund-Fix (Commit d3e75c3) gegengelesen und als Punkt (u) im
Fable-5-Review-Abschnitt abgehakt. Wie im Arbeitsstand-Memory vorgesehen war
das ein kurzer Diff-Gegenlese-Pass, KEINE weitere Volltiefen-Runde:
- Diff gegen `server.py.bak-20260704-review-t` ist exakt der protokollierte
Mini-Fix (1 Fundstelle: `registration-status` antwortet generisch, Detail
per stderr ins Journal); `sys` ist importiert, `py_compile` sauber.
- Dienst läuft mit dem aktuellen Code (Start 19:19:40 NACH Datei-mtime
19:19:22), kein unprotokollierter Rest im CHANGES-Hook-Log (letzte Einträge
19:1919:22 gehören zur (t)-Session, danach Commit 4c6efa2).
- Die 4 verbliebenen `str(e)`-Antworten liegen alle im `with ADMIN_LOCK:`-Block
HINTER dem Heimnetz-Gate (run-stats, create-invite, toggle-registration,
ban/unban) absichtlich, LAN-only, für Bernd nützlich. Kein Handlungsbedarf.
- Kurz-Check des laufenden Dienstes: stats.html 200, registration-status 200
(`{"enabled": false}`), unbekannte Route 404.
**Damit ist die server.py-Härtungskette (o)(u) KONVERGIERT** ohne neuen
Anlass (neuer Code, neue Route) gibt es dort nichts mehr zu prüfen. ALLE als
erledigt markierten ROADMAP-/PROGRESS-Punkte sind reviewt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest, M2-Call/State) oder
Bernd (Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go). Auf dem Pi
ist nichts sicher Bearbeitbares mehr offen (Memory: nicht erneut suchen,
UNGETESTET-Stapel bewusst gedeckelt).
**Stolperfallen:** Keine der Befund aus (t) war korrekt gefixt. Eine
Review-Kette braucht ein dokumentiertes Ende-Kriterium („konvergiert wenn der
letzte Diff nur noch den Vorbefund fixt und das Muster-grep leer ist"), sonst
reviewt jede Session den Review der Vorsession bis in alle Ewigkeit.
---
## 2026-07-04 Fable-5-Review (t): (s)-Fixes geprüft korrekt, 1 kleiner Restbefund gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit die zwei
(s)-Restbefund-Fixes (Commit 13b333c) kritisch gegengelesen und als Punkt (t)
im Fable-5-Review-Abschnitt abgehakt. Der (s)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-s` verifiziert: ban/unban-Body-Lesen
sitzt vollständig VOR dem Lock inkl. Validierung, die generischen 500er auf
beiden öffentlichen POST-Routen stimmen; Dienst lief mit dem aktuellen Code,
kein unprotokollierter Rest im CHANGES-Hook-Log). Beim Ganzdatei-Gegenlesen
EIN Restbefund derselben Familie wie (s)-Befund 2:
1. **`GET /api/registration-status` leakte Exception-Texte:** Die Route ist
öffentlich erreichbar (in `do_GET` gibt es kein Heimnetz-Gate) und
antwortete bei Fehlern mit `str(e)` ein Lesefehler an `conduit.toml`
hätte den internen Dateipfad an Unauthentifizierte verraten. (s) hatte nur
die zwei öffentlichen POST-Routen gefixt und die öffentliche GET-Route
übersehen. Fix: gleiches Muster generische 500-Antwort („Interner
Fehler"), Detail per stderr ins Journal.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-t`). 8 Routen-Checks wie in (s) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, livekit-token ungültig 401, Diagnose falscher Token 403,
gespoofter Cf-Header 403, unbekannte Route 404). **500-Beweis (neu):**
`conduit.toml` für ~1 s per `chmod 000` unlesbar gemacht →
`{"error": "Interner Fehler"}` nach außen, `PermissionError` im Journal
(vorher wäre `[Errno 13] … '/home/steggi/matrix/conduit.toml'` also der
Pfad nach draußen gegangen); Rechte sofort wiederhergestellt (664), Route
danach wieder 200. Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** Bei einem Info-Leak-Fix nicht nur die Methode fixen, in
der der Befund auftauchte: (s) hatte `do_POST` gefixt, aber `do_GET` derselben
Datei hat auch öffentliche Routen mit demselben `str(e)`-Muster. Lehre: Nach
einem Musterfund (`str(e)` an Unauthentifizierte) einmal `grep str(e)` über
die GANZE Datei und jede Fundstelle als „öffentlich oder LAN-only?"
einordnen, statt nur die Nachbarroute zu fixen.
---
## 2026-07-04 Fable-5-Review (s): (r)-Fixes geprüft korrekt, 2 kleinere Restbefunde gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit die drei
(r)-Restbefund-Fixes (Commit 5ada0e5) kritisch gegengelesen und als Punkt (s)
im Fable-5-Review-Abschnitt abgehakt. Der (r)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-r` verifiziert, Dienst lief mit dem
aktuellen Code). Beim Gegenlesen der GANZEN Datei (Lehre aus (o)) zwei
kleinere Restbefunde in `server.py` gefunden und gefixt:
1. **ban/unban las den Body INNERHALB des `ADMIN_LOCK`:** Das 4-KB-Limit aus
(r) deckelt nur die Größe ein LAN-Client, der `Content-Length` ankündigt
und nichts schickt, hielt den Lock trotzdem bis zum 30-s-Socket-Timeout
fest (der (r)-Kommentar behauptete fälschlich, der Deckel verhindere das).
Seit (r) wartet auch der öffentliche GET-Leser von `registration-status`
auf diesen Lock → der hing mit. Fix: Body-Lesen/Validieren VOR den Lock
gezogen (Handler-Logik identisch, nur verschoben).
2. **Öffentliche Routen leakten Exception-Texte:** `livekit-token` und
`e2ee-diagnostics` antworteten Unauthentifizierten bei Fehlern mit
`str(e)` das kann interne Pfade (`livekit.yaml`-Pfad bei
FileNotFoundError) oder interne Adressen preisgeben. Fix: generische
500-Antwort („Interner Fehler"), Details per stderr ins Journal. Die
Admin-Routen behalten `str(e)` (LAN-only, für Bernd nützlich).
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-s`). 8 Routen-Checks wie in (r) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, `Content-Length: -5` 413, livekit-token ungültig 401,
Diagnose falscher Token 403, gespoofter Cf-Header 403). **Lock-Beweis (neu):
ban-Request mit angekündigtem, nie gesendetem Body + paralleler
`registration-status`-GET → Antwort in 15 ms statt Lock-Blockade** (vorher
hätte der GET bis zu 30 s gewartet). **500-Beweis (neu):** ungültiges JSON an
livekit-token → `{"error": "Interner Fehler"}`, Detail
(`JSONDecodeError(...)`) steht im Journal. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Ein Größen-Limit ist KEIN Zeit-Limit: `read(n)` mit
kleinem n blockiert trotzdem bis zum Socket-Timeout, wenn der Client nichts
schickt blockierende I/O gehört grundsätzlich VOR einen Lock, nicht
hinein. (2) `str(e)` in Fehlerantworten öffentlicher Endpoints ist ein
Info-Leak (Pfade, interne Hosts) Details gehören ins Journal, nach draußen
nur Generisches.
---
## 2026-07-04 Fable-5-Review (r): ADMIN_LOCK-Umbau (q) geprüft korrekt, aber 3 kleinere Restbefunde gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit den ADMIN_LOCK-Umbau
aus (q) (Commit c8ff850) kritisch gegengelesen und als Punkt (r) im
Fable-5-Review-Abschnitt abgehakt. Der Umbau selbst ist korrekt (Locking-Struktur,
Routen-Reihenfolge, Handler-Logik unverändert). Drei kleinere Restbefunde in
`server.py` gefunden und gefixt:
1. **GET-Leser-Race (gleiche Familie wie (q), dort übersehen):** (q) hatte nur
POST-gegen-POST betrachtet `GET /api/registration-status` liest
`conduit.toml` aber OHNE Lock, während `toggle-registration`/`create-invite`
die Datei per Truncate+Write in-place neu schreiben → seit multithreaded (p)
kann der Leser eine halb geschriebene/leere Datei sehen (Dashboard zeigt
transient falschen Registrierungs-Status). Fix: Lesen unter `ADMIN_LOCK`.
**Wichtige Erkenntnis dabei:** Der naheliegendere Fix (atomares `os.replace`
statt In-Place-Write) wäre FALSCH `conduit.toml` ist als **einzelne Datei**
in den Container gemountet (`./conduit.toml:/etc/conduit.toml:ro`), ein
Replace tauscht den Inode und der Container sähe für immer die alte Datei.
Im Code als Kommentar festgehalten, damit das nie jemand „verbessert".
2. **Negative `Content-Length`:** `livekit-token`, `e2ee-diagnostics` und
`ban`/`unban` prüften nur `length > MAX` ein negativer Wert wäre
durchgerutscht und `rfile.read(-n)` liest bis EOF (Thread hängt bis zum
30-s-Timeout). Fix: `length < 0` wird wie „zu groß" abgelehnt (413).
3. **`ban`/`unban` ohne Body-Limit + `random` für Einladungs-Token:**
ban/unban lasen beliebig große Bodies (und halten dabei seit (q) den
ADMIN_LOCK) → 4-KB-Deckel wie bei livekit-token. Einladungs-Token
(`create-invite`) kamen aus dem vorhersagbaren `random`-PRNG, obwohl sie
die Registrierung am Homeserver freischalten → `secrets.choice`.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-r`). 8 Routen-Checks: stats.html 200,
registration-status 200 (liest jetzt unter Lock), ban ohne Nutzer localhost →
400 „Kein Nutzer angegeben", **ban mit 8-KB-Body → 413 (neu)**, **livekit-token
mit `Content-Length: -5` → sofort 413 statt Hänger (neu)**, livekit-token
ungültiger Matrix-Token → 401, e2ee-diagnostics falscher Token → 403,
Admin-Route öffentlich über die Dashboard-Domain → 403. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Bei Nebenläufigkeits-Reviews nicht nur Schreiber gegen
Schreiber prüfen, sondern auch die LESER derselben Ressource `do_GET` war in
(q) komplett außen vor. (2) „Atomares Schreiben per `os.replace`" ist bei
Docker-**Datei**-Bind-Mounts eine Falle: der Mount hängt am Inode, nach einem
Replace sieht der Container die alte Datei weiter. In-Place-Write ist dort
absichtlich richtig. (3) `Content-Length` kann negativ sein wer nur nach oben
deckelt, lässt `read()` bis EOF blockieren.
---
## 2026-07-04 Fable-5-Review (q): Threading-Härtung (p) tiefer geprüft echte Nebenläufigkeits-Race in den Admin-Routen gefunden und mit ADMIN_LOCK behoben
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit die Threading-Umstellung
aus (p) (Commit 343545b, `ThreadingHTTPServer`) noch einmal kritisch gegengelesen und
als Punkt (q) im Fable-5-Review-Abschnitt abgehakt. Befund: Die Umstellung auf
multithreaded hat einen **echten Race erzeugt, den es vorher (single-threaded, alles
serialisiert) nicht geben konnte** (p) hat nur den öffentlich beschreibbaren
Diagnose-Pfad per `DIAG_LOCK` abgesichert, die **Admin-Routen aber übersehen**:
- `toggle-registration` und `create-invite` machen ein Read-Modify-Write auf
`conduit.toml` **plus** `docker compose restart conduit` zwei gleichzeitige Klicks
= verlorenes Update (lost update) + doppelter Container-Neustart.
- `send_admin()` schläft 4 s und liest danach die **letzten** Nachrichten aus dem
Admin-Raum zurück, um „die" Antwort zuzuordnen zwei gleichzeitige Admin-Befehle
können sich vertauschte Antworten einfangen.
**Fix (`server.py`):** Neues `ADMIN_LOCK` (threading.Lock), das **nur** die
Admin-Mutationen serialisiert. Dafür `do_POST` minimal umgebaut: die beiden
öffentlichen Routen (`/api/livekit-token`, `/api/e2ee-diagnostics`) werden jetzt
**zuerst** behandelt und returnen, danach folgt das Heimnetz-Gate und der komplette
Admin-Dispatch in einem `with ADMIN_LOCK:`. Damit bleiben die öffentlichen Pfade
**nebenläufig** der Slow-Loris-Schutz aus (p) ist unangetastet. Handler-Logik
wortwörtlich unverändert (nur verschoben/eingerückt, per `diff` gegen Backup
verifiziert). `python3 -m py_compile` sauber; Backup `server.py.bak-20260704-adminlock`.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
8 Routen-Checks identisch wie vorher (stats 200; ban localhost → 400 „Kein Nutzer",
ban „aus dem Internet"/gespoofter Cf-Header → 403; livekit-token 401/400/413;
e2ee-diagnostics falscher Token 403; unbekannte Route 404). **Race-Beweis:** ein
`run-stats` allein 1388 ms, **zwei parallel 2633 ms** (≈ 2× → serialisiert; ohne
Lock wären es ~1400 ms). **Non-Blocking-Beweis:** ein öffentliches `livekit-token`
**während** ein Admin-`run-stats` den Lock hält → in **11 ms** beantwortet (nicht
blockiert). Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** Eine Threading-Umstellung ist nie „nur" ein Server-Konstruktor-
Tausch sie macht **jeden** geteilten Zustand (Dateien, Read-Modify-Write,
Readback-nach-sleep) plötzlich race-fähig. (p) hatte richtig den einen öffentlich
beschreibbaren Pfad (`DIAG_LOCK`) gesehen, aber die Admin-Routen mit `conduit.toml`
und dem Admin-Raum-Readback sind derselbe Fall. Lehre: Nach „Server ist jetzt
multithreaded" ALLE schreibenden/zustandsändernden Handler durchgehen, nicht nur
den einen, der den Anlass gab.
---
## 2026-07-04 Fable-5-Review (p): Interims-Gate geprüft (korrekt) + halbfertige Threading-Härtung einer abgebrochenen Session vollendet
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit (Interims-Gate,
Commits e97a748/455742d) als Punkt (p) im Fable-5-Review-Abschnitt abgehakt.
Das Gate selbst ist korrekt (Socket-IP als belastbare Prüfung, Cf-Header nur
Zweitschranke, Deny-Log gedeckelt, kein Angreifer-Text im Log). Dabei über das
CHANGES.md-Hook-Log entdeckt: Eine Session hat um 09:43 (NACH dem letzten
Commit) **drei unprotokollierte server.py-Edits** gemacht und wurde mitten in
einer Threading-Härtung abgebrochen `threading`-Import + `DIAG_LOCK` waren da,
aber der Lock wurde **nirgends benutzt**, und die eigentliche Umstellung auf
`ThreadingHTTPServer` fehlte (der Kommentar behauptete sie schon). Obendrein
lief der Dienst noch mit dem Code von 09:14 nichts davon war je geladen.
Die angefangene Härtung ist fachlich richtig (der single-threaded `HTTPServer`
ließ EINEN langsamen Client den ganzen Server blockieren inkl. des öffentlich
erreichbaren `/api/livekit-token`), darum vollendet statt zurückgebaut:
`ThreadingHTTPServer` in `__main__`, Diagnose-Log-Kappen+Anhängen in den
`DIAG_LOCK` gewickelt (einziger öffentlich beschreibbarer Pfad), `timeout = 30`
der Vorsession belassen. Dienst-Neustart (kill/`Restart=always`), headless
verifiziert (11 Prüfungen): stats 200; Admin-Route localhost UND 192.168.178.71
→ 400 „Kein Nutzer angegeben"; öffentlich UND gespoofter Cf-Header → 403;
livekit-token ungültig → 401; Diagnose falscher Token 403, gültig 200 + im Log,
300 KB → 413; **Slow-Loris-Test: parallele Anfrage in 10 ms beantwortet,
während ein Client hing** (vorher unmöglich), hängender Body-Read nach exakt
~30 s serverseitig beendet, Dienst danach gesund. Test-Einträge aus beiden
Logs wieder entfernt.
**Offen/Nächster Schritt:** Unverändert alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Das CHANGES.md-Hook-Log ist der zuverlässigste Detektor
für unprotokollierte Arbeit abgebrochener Sessions Zeitstempel der letzten
Einträge mit dem letzten Commit vergleichen. (2) „Datei geändert" heißt NICHT
„läuft": Dienststart-Zeit (`systemctl status`) gegen Datei-mtime prüfen hier
lagen 30 Minuten Code unausgeführt auf der Platte. (3) Ein definierter, aber
nie benutzter Lock ist ein Halbfertig-Marker, kein Schutz.
---
## 2026-07-04 Dashboard-Befund (o): akutes Loch mit aussperr-sicherem Interims-Gate geschlossen
**Erledigt:** Der KRITISCHE Befund (o) (Dashboard-Admin-Endpoints ohne Auth,
öffentlich erreichbar) ist entschärft, OHNE Bernds A/B-Entscheidung
vorwegzunehmen und OHNE ihn auszusperren. Erkenntnis, die das möglich machte:
Öffentlicher Verkehr erreicht `server.py` (Port 8080) ausschließlich über den
Cloudflare-Tunnel-Container (Absender-IP 172.x + `Cf-*`-Header) Bernds
Heimnetz-Zugriffe kommen dagegen direkt mit `192.168.*`-Socket-IP an. Daher
„Weg C" als Interims-Gate in `server.py`: Die 5 Admin-Routen (`/api/ban`,
`/api/unban`, `/api/toggle-registration`, `/api/create-invite`, `/api/run-stats`)
akzeptieren nur noch Anfragen mit Heimnetz-/localhost-Socket-IP (nicht spoofbar)
und ohne Cloudflare-Header; sonst 403 mit deutscher Erklärung inkl.
Heimnetz-URL das Dashboard zeigt `d.error` per `alert()`, Bernd sieht also
genau, was zu tun ist. Abgelehnte Versuche landen größenbegrenzt (5-MB-Deckel,
Lehre aus Befund (l)) in `security_alerts.log` (nur Fixpfad + Socket-IP, kein
attacker-kontrollierter Text → kein HTML-Injection-Risiko in der
Dashboard-Anzeige). Dienst-Neustart per kill/`Restart=always`, headless
verifiziert (9 Prüfungen): öffentlich ban/toggle → 403, localhost UND
192.168.178.71 → 400 „Kein Nutzer angegeben" (Gate passiert, App-Validierung
erreicht), gespoofter `Cf-Connecting-Ip` aus dem LAN → 403, `GET /` öffentlich
200, `/api/livekit-token` 401 und `/api/e2ee-diagnostics` 403 wie bisher
(bleiben bewusst öffentlich, die App braucht sie). Test-Einträge aus dem
Alert-Log wieder entfernt. Rückbau jederzeit: `server.py.bak-20260704-dashgate`.
Doku/ROADMAP nachgezogen (`docs/DASHBOARD_AUTH_HARDENING.md`: Status, Weg C,
Restrisiken).
**Zusätzlicher Befund (für Bernds Secret-Rotation wichtig):** Das Gitea-Repo
einfach privat zu schalten ist KEINE schnelle Lösung für den Secret-Leak (k):
Der In-App-Updater (`lib/core/update_checker.dart`) holt Releases **anonym**
über die öffentliche Gitea-API Repo privat = Utas installierte App findet
keine Updates mehr, und ein Fix ließe sich nicht mehr per Update verteilen
(Henne-Ei). In ROADMAP M0 beim Rotations-Punkt als Warnung vermerkt.
**Offen/Nächster Schritt:** Bernds Entscheidung bleibt für den FERNZUGRIFF
relevant (Weg A empfohlen schließt auch die weiterhin öffentliche
Nutzerlisten-/Präsenz-Ansicht; Restrisiken im Doc: LAN-CSRF, offene Ansicht).
Danach ggf. Gate ergänzen/ersetzen. Sonst unverändert: übrige M0-Punkte
brauchen PC/Gerät oder Bernd (SQLCipher, Rotation, LiveKit-Client-Umstellung,
Härtetest).
**Stolperfallen:** (1) `stats.html` wird alle 5 Minuten von `stats.sh` (Cron)
NEU GENERIERT Änderungen an der HTML-Datei sind flüchtig, UI-Änderungen
müssten in `stats.sh` (deshalb bewusst NUR server-seitig gefixt). (2) Auf dem
Pi laufen ZWEI cloudflared-Instanzen: die lokal konfigurierte
(`cloudflared.yml`, nur Jellyfin) und eine remote-verwaltete
(`cloudflared-pingvin`, Ingress liegt im Cloudflare-Dashboard, lokal nicht
einsehbar) Letztere trägt `dashboard.`/`git.steggi-matrix.work`. Wer nur die
lokale YAML liest, hält die Subdomains fälschlich für tot. (3) Der
Header-Check allein würde nicht reichen (Header sind von Direktverbindern
spoofbar/wegzulassen) die Socket-IP ist die belastbare Wahrheit, der
Cf-Header-Check ist nur die zweite Schranke, falls der Tunnel je im
Host-Netz läuft.
---
## 2026-07-04 Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard
**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist
zwei neue Punkte (n)/(o) im Fable-5-Review-Abschnitt abgehakt (Details dort).
- **(n):** Beim Gegenlesen von `server.py` gefunden, dass die geplante
LiveKit-Token-Route `POST /api/livekit-token` von einer vorher abgebrochenen
(unprotokollierten) Session **bereits live gebaut** wurde. Kritisch geprüft und
headless verifiziert: Auth per whoami greift (ungültig→401), Body-Limit (413),
gültig→200; das JWT unabhängig dekodiert und die **Signatur gegen `livekit.yaml`
gegengerechnet gültig**; Identität = geprüfte Matrix-user_id (kein Spoofing).
Route ist gefahrlos additiv. Server-Seite des M0-LiveKit-Punkts damit erledigt;
Client-Umstellung + Rotation bleiben PC/Gerät (heiliger Call-Pfad).
- **(o) KRITISCH:** Derselbe Dashboard-Server hat für seine **Admin-Endpoints
(`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`,
`/api/run-stats`) KEINE Authentifizierung** und ist über
`https://dashboard.steggi-matrix.work` **öffentlich erreichbar**. Belegt per
`curl` (App-eigene 400-Antwort über die öffentliche URL, kein Login davor).
Ein Fremder könnte damit Uta (jeden Nutzer) sperren oder die offene
Registrierung am Homeserver aktivieren. Der Hostname ist über CT-Logs auffindbar.
**Offen/Nächster Schritt:** **Bernd muss entscheiden, WIE das Dashboard künftig
authentifiziert** Weg A (Cloudflare Access vor die Subdomain, kein Code, kein
Aussperr-Risiko, empfohlen) oder Weg B (eigener `DASH_TOKEN` + `stats.html`-Prompt).
Fertiger Plan mit beiden Wegen: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach sofort
umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede Auth-Ergänzung
Bernds eigenes Admin-Panel bis zur Token-Eingabe lahmlegt („kein Aussperren")
das mitten in einer autonomen Session zu deployen ist die falsche Reihenfolge.
**Stolperfallen:** Ein Review, der sich auf den *geänderten* Endpoint (Diagnose,
Disk-Fill) konzentriert, übersieht leicht die *unveränderten Nachbarn* in
derselben Datei. Lehre: bei einem Sicherheits-Review einer Datei nicht nur den
Diff prüfen, sondern die ganze Angriffsfläche der Datei (hier: alle Routen des
`do_POST`-Handlers auf Auth abklopfen, nicht nur die neu berührte).
---
## 2026-07-04 Fable-5-Review (l)+(m): Diagnose-Endpoint hatte kein Größenlimit (gefixt)
**Erledigt:** Review-Pass über die zwei seit (k) dazugekommenen Arbeiten als
Punkte (l) und (m) im Fable-5-Review-Abschnitt abgehakt (Details dort). Bei (l)
ein **echter Befund** am Server-Gegenpart des Admin-Token-Fixes: Der neue
`DIAG_TOKEN` steht bewusst im öffentlichen Repo (eng begrenzt, nur Log-Anhängen)
aber `/api/e2ee-diagnostics` in `/home/steggi/matrix/server.py` las den
Request-Body **ohne Größenlimit** und hängte ihn unbegrenzt an die Log-Datei an.
Jeder Fremde hätte damit die Pi-Platte volllaufen lassen können (Disk-Fill-DoS
auf dem Host, der auch den Homeserver trägt). Gefixt in `server.py`:
Body-Limit 256 KB pro Upload (413 bei Überschreitung) + harte Obergrenze 20 MB
für die Log-Datei (ältere Hälfte fliegt raus). Headless verifiziert nach
Dienst-Neustart (kill, `Restart=always`): stats.html 200, gültiger Upload 200
und im Log gelandet, falscher Token 403, 300-KB-Body 413; Kappungslogik isoliert
getestet (100 Zeilen → 50, neueste überleben). Test-Einträge wieder entfernt.
**Offen/Nächster Schritt:** Weiter mit dem obersten offenen ROADMAP-Punkt
(M0 LiveKit-Token): Server-Route nach `docs/LIVEKIT_TOKEN_MIGRATION.md` bauen
rein additiv, Client bleibt unangetastet (heiliger Call-Pfad wartet auf PC/Gerät).
**Stolperfallen:** Ein „eng begrenzter" Token im öffentlichen Repo ist nur dann
wirklich harmlos, wenn das Endpoint dahinter auch Missbrauch begrenzt
(Größe/Menge) „keine Admin-Rechte" allein reicht nicht. Bei künftigen
Endpoints mit öffentlich bekanntem Token immer an Body-/Speicher-Limits denken.
---
## 2026-07-04 LiveKit-Token-Minting: Migrationsplan geschrieben (Prep, kein Code)
**Erledigt:** Für den offenen M0-Punkt „LiveKit-Token server-seitig minten"
(letzter Client-eingebetteter Secret nach der Diagnose-Entkopplung) den fertigen
Umsetzungsplan `docs/LIVEKIT_TOKEN_MIGRATION.md` geschrieben gegen den echten
Client-Code (`livekit_token.dart`, `livekit_call_manager.dart:154`) und die echte
Pi-Konfig (`livekit.yaml`, `server.py`, Continuwuity `whoami`). Kern: Token-Route
`POST /api/livekit-token` am bestehenden Dashboard-Server, die den Matrix-Access-Token
per `whoami` prüft und daraus die LiveKit-Identität ableitet (kein Spoofing), JWT
per stdlib-HMAC mintet (kein neues Paket); Client holt nur noch das fertige JWT und
`apiSecret` verschwindet aus dem App-Code. Analog zum bewährten
`SQLCIPHER_MIGRATION.md`-Muster.
**Offen/Nächster Schritt:** Umsetzung braucht echten Call-Test auf einem Gerät
(Calls „heilig", auf dem Pi kein GUI) daher bewusst nur der Plan, kein Code.
Danach Secret-Rotation zusammen mit einem Release. Verbleibende M0-Secrets
(Admin-Token, Gitea-, Giphy-Token) brauchen Bernds Konten-Zugriff.
**Stolperfallen:** Keine reine Doku.
---
## 2026-07-04 Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)
**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
ihn nur zu dokumentieren und zwar sicher, weil diese Session direkt auf dem Pi
läuft, der den Dashboard-Server hostet (frühere Sessions kannten den Server-Code
nicht). Kernursache gefunden: `/home/steggi/matrix/server.py` benutzte für das
Diagnose-Endpoint `/api/e2ee-diagnostics` **denselben String** wie den
Matrix-Admin-Token (`DIAG_TOKEN = TOKEN`). Deshalb musste der Client diesen Token
mitliefern d. h. jeder App-Nutzer (auch Uta) trug den vollen Server-Admin-Token
im Gerät.
- **Server (`server.py`):** `DIAG_TOKEN` ist jetzt ein eigener, zufälliger,
eng begrenzter Token (`pyr-diag-…`), unabhängig vom Admin-`TOKEN`. Das
Diagnose-Endpoint hängt nur an den Log an (keine Admin-Rechte) der neue Token
ist also von geringer Sensibilität. Der Admin-`TOKEN` (für Ban/Invite/
Registrierung) bleibt **unverändert**, damit das Dashboard weiterläuft.
- **Client (`settings_encryption.dart`):** `_uploadDiagnostics` schickt jetzt den
neuen Diagnose-Token statt des Admin-Tokens. **`grep J5lax lib/` ist jetzt leer**
der Admin-Token ist raus aus dem App-Code.
- **Verifiziert (headless, kein GUI nötig):** Dashboard-Server nach Neustart
(Prozess gekillt, systemd `Restart=always` lud den neuen Code): `stats.html` 200,
`registration-status` 200, `/api/e2ee-diagnostics` mit neuem Token **200**, mit
altem Admin-Token **403**. `flutter analyze` unverändert (1 bekannter Hinweis),
`flutter test` 24/24 grün. Meine Test-Einträge aus dem Diagnose-Log wieder
entfernt.
**Offen/Nächster Schritt:** Die **Rotation des Admin-Tokens `J5lax…` bleibt
Pflicht und Bernds Sache** der Token ist weiterhin gültig und steht in der
Git-History sowie in Utas bereits installierter APK. Erst nach der Rotation ist
der Leak wirklich wertlos. Ebenso offen: LiveKit-`apiSecret` (server-seitiges
Minting), Gitea- und Giphy-Token (siehe ROADMAP M0). **Wichtig für den nächsten
PC-Lauf / das nächste Release:** Utas aktuell installierte App schickt beim
Diagnose-Upload noch den alten Token → dieser Button liefert bei ihr jetzt 403,
bis sie ein neues Release bekommt (nur die Diagnose-Funktion betroffen, nichts
Heiliges). Der Diagnose-Upload-Button gehört damit in den PC-Testplan (mit neuem
Build einmal drücken, Log auf dem Pi prüfen).
**Stolperfallen:** `systemctl restart matrix-stats.service` scheiterte an
fehlender polkit-Auth (kein Terminal für sudo). Lösung ohne root: den von systemd
verwalteten Prozess killen `Restart=always`/`RestartSec=5` lädt server.py neu
(neue MainPID bestätigt). Nur anwendbar, weil der Dienst als User `steggi` läuft
und der eigene Prozess killbar ist.
---
## 2026-07-04 Fable-5-Review (k): Secret-Scan ECHTER Sicherheitsbefund, teils gefixt
**Erledigt:** Beim Review-Pass zusätzlich einen systematischen Secret-Scan über
`lib/`, `scripts/`, `docs/`, `android/` gemacht (CLAUDE.md-Leitplanke „Keine
Secrets ins Repo"). **Ergebnis: mehrere echte, aktive Zugangsdaten liegen im
Repo und das Gitea-Repo ist als `private:false` unter der Internet-Domain
`git.steggi-matrix.work` erreichbar** (per `curl` bestätigt: Repo-Metadaten und
Releases anonym abrufbar). Damit sind alle folgenden Secrets als **kompromittiert
zu behandeln** (seit Commit 25ed765 in der History):
- **Matrix-Server-ADMIN-Token** `J5lax…` lag doppelt drin:
(1) als Bearer-Token in `docs/matrix-sdk/13-server-admin-continuwuity.md`
(19 Stellen) und (2) **im ausgelieferten App-Code**
`lib/widgets/settings/settings_encryption.dart:431` (E2EE-Diagnose-Upload).
Per `whoami` gegen den Homeserver geprüft: **Token ist noch gültig**
(`@todesneutron:steggi-matrix.work`, voller Server-Admin). Kritischster Fund
jeder App-Nutzer (auch Uta) trägt den Server-Admin-Token im Client.
- **Gitea-Release-Token** `bb522f96…` (Repo-Schreibrechte) hartcodiert in
`scripts/release.ps1:14`.
- **LiveKit-`apiSecret`** `rYUT2PRa…` in `lib/core/livekit_token.dart:6` die
App mintet LiveKit-JWTs **client-seitig**, das SFU-Secret muss also im Client
liegen (Architekturproblem, nicht nur ein Repo-Leak).
- **Giphy-API-Key** `QtEy…` in `lib/features/chat/gif_sticker_picker.dart:9`
(geringe Sensibilität, aber ebenfalls im Client).
**Sofort (gefahrlos, kein Laufzeit-Bezug) bereinigt:**
- `scripts/release.ps1`: Token raus, kommt jetzt aus Umgebungsvariable
`PYRAMID_GITEA_TOKEN`; fehlt sie, bricht das Skript mit klarer Anleitung ab.
- `docs/matrix-sdk/13-server-admin-continuwuity.md`: alle 19 Token-Stellen durch
`<ADMIN_TOKEN>`-Platzhalter ersetzt.
**Offen/Nächster Schritt (BRAUCHT BERND / PC nicht blind auf dem Pi fixen):**
1. **Alle vier Secrets rotieren** (sie sind bereits geleakt, egal was im Code
steht): neuen Matrix-Admin-Token erzeugen und alten widerrufen; neuen
Gitea-Token; LiveKit-`apiKey`/`apiSecret` neu setzen (Hetzner-LiveKit +
`ice.json`); Giphy-Key neu ziehen. **Das ist der wichtigste Schritt** die
Repo-Bereinigung allein nützt nichts, solange die alten Werte gültig sind.
2. **Client-seitige Secrets entschärfen (Architektur):** LiveKit-Token gehört
server-seitig gemintet (kleiner Token-Endpoint auf dem Pi, App holt sich nur
das fertige JWT) dann verschwindet `apiSecret` aus dem Client. Der
E2EE-Diagnose-Upload darf **niemals** den Server-Admin-Token benutzen: eigenen,
eng begrenzten Token (nur der Dashboard-Endpoint) oder besser einen
unauthentifizierten Upload mit Rate-Limit. Beides als ROADMAP-M0-Punkt neu
aufgenommen. Blind entfernen verbietet sich, weil Calls/Diagnose sonst brechen
(Calls sind „heilig") und auf dem Pi kein GUI-Test möglich ist.
3. **Git-History:** Die alten Werte bleiben in der Gitea-History sichtbar. Nach
der Rotation sind sie wertlos; ein History-Rewrite (force-push) ist optional
und Bernds Entscheidung (das Repo dient auch als Backup) daher NICHT
eigenmächtig gemacht.
**Stolperfallen:** Der Diagnose-„Token" in `settings_encryption.dart` ist
NICHT ein harmloser Dashboard-Key, sondern derselbe String wie der
Matrix-Server-Admin-Token beim Lesen leicht zu übersehen, weil er dort nur
`token:` heißt. Lehre: bei Secret-Funden immer prüfen, ob derselbe Wert an
mehreren Stellen (Doku UND Client) wiederkehrt und WAS er wirklich autorisiert.
---
## 2026-07-03 Fable-5-Review (h)(j): Test-/Doku-Commits geprüft, 1 echter Befund gefixt
**Erledigt:**
- **Review-Pass über die drei seit (g) dazugekommenen Commits** als Punkte
(h)(j) im Fable-5-Review-Abschnitt abgehakt (Details dort): AuthLog-Tests
e0ac5cb korrekt (h), PC-Testplan-Update 03b84e0 korrekt (j), und bei den
settings_prefs-Tests 7dfdbf1 (i) beim Gegenlesen des Produktivcodes ein
**echter Befund**: Lade-Race in `BoolPref`/`StringSetPref`
`StringSetPref.add()` vor abgeschlossenem `_load` konnte **bereits
persistierte Einträge löschen** (Read-Modify-Write auf ungeladenem State;
per Regressionstest am alten Code bewiesen, dann gefixt: 7b90f20).
Vorgehen: Test zuerst geschrieben, Fehlschlag am alten Code gesehen, dann
Fix, dann alles grün kein blinder Fix.
- Unabhängige Verifikation: `flutter test` jetzt **24/24 grün auf dem Pi**
(2 neue Lade-Race-Tests), `flutter analyze` unverändert 1 bekannter Hinweis
(`chat_provider.dart:42`). PC-Testplan Schritt 0 auf 24 Tests nachgezogen.
**Offen/Nächster Schritt:** Unverändert blockiert alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität), siehe
`docs/PC_TESTPLAN.md` und „Fragen an Bernd" weiter unten. Der Lade-Race-Fix
ist headless voll getestet, gehört aber der Vollständigkeit halber mit in den
nächsten PC-Klickdurchlauf (Einstellungs-Toggles, Space-Beitritt).
**Stolperfallen:** Der BoolPref-Teil des Race ist im Test-Mock NICHT
reproduzierbar (Microtask-Reihenfolge dort zufällig gutartig: `set()`
persistiert vor dem `_load`-Read) nur der StringSetPref-Teil schlug real
fehl. Lehre: Bei Race-Befunden nicht vom „Test wird schon rot"-Reflex leiten
lassen, sondern die konkrete Microtask-Reihenfolge durchdenken; ein Guard kann
richtig sein, auch wenn der Test ihn in dieser Umgebung nicht erzwingen kann
(dokumentiert im Review-Punkt (i)).
---
## 2026-07-03 Review-Nachzügler abgeschlossen + AuthLog-Regressionstests
**Erledigt:**
- **Fable-5-Review vervollständigt (eebf8ae):** Die drei seit Review-Abschluss
dazugekommenen erledigten Arbeiten kritisch geprüft und als Punkte (e)(g) im
Review-Abschnitt abgehakt: Unit-Tests 99cde9a (korrekt, Fake-Clients schlagen
bei jedem Fremd-Aufruf laut fehl), Doku-Commits 16561bb/566938f (alle
Behauptungen gegen Code/pubspec/matrix-6.2.0 verifiziert Factory-Stellen,
Tabellenname `box_inbound_group_session`), Autopilot-Konto-Wechsel bfe5876
(kein App-Code, Logik in Ordnung; harmloser False-Positive der
Limit-Erkennung dokumentiert). **Kein Befund, keine Code-Änderung nötig.**
Damit sind ALLE als erledigt markierten Punkte reviewt.
- **`test/auth_log_test.dart` (4 Tests):** `AuthLog` ist die einzige weitere
öffentliche Schnittstelle auf dem heiligen Pfad, die headless testbar ist
(der Soft-Logout-Guard ruft `AuthLog.write` im catch-Pfad auf würde es
werfen, eskalierte das SDK zu `logout()`+`clear()`). Getestet: write/read
werfen NIE, auch wenn die Plattform fehlt (kaputter PathProvider);
Platzhalter statt Fehler bei leerem Log; ISO-Zeitstempel pro Zeile;
500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt
offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
Pflicht-Schnittstelle ausgebaut werden soll dieses Vertragsnetz muss beim
M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
(`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
(persistierter Wert überstimmt Default, set/toggle persistieren),
`StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
leere Map statt Crash** echter Fehlerpfad). `flutter test`: **alle
22 Tests grün auf dem Pi.**
**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) siehe
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
oder braucht Widget-Tests mit gemocktem Matrix-Client lohnt erst nach dem
M2-Modulschnitt mit echten öffentlichen Schnittstellen).
**Stolperfallen:** `AuthLog` cached sein File-Handle statisch der Test für
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
laufen (im Testfile kommentiert). Bei künftigen Tests an Singletons mit
statischem Zustand auf Testreihenfolge/Isolate-Grenzen achten (flutter test
isoliert pro Datei, nicht pro Test).
---
## 2026-07-03 Erste Unit-Tests: Regressionsnetz für die zwei heiligsten Codepfade
**Erledigt:** Eine Vorsession hatte (unprotokolliert, Abbruch vor dem Testlauf
sichtbar nur am untrackten `test/`-Ordner + CHANGES.md-Hook-Log 18:16/18:17)
zwei Testdateien angelegt. Diese Session hat sie gegen den echten Code
gegengelesen, ausgeführt und committet (99cde9a) damit hat das Projekt erstmals
überhaupt eine Testsuite, und zwar genau dort, wo CLAUDE.md „heilig" sagt:
- `test/soft_logout_guard_test.dart` (4 Tests): `guardedSoftLogoutRefresh`
wirft NIE nach außen (weder Exception noch Error) ein Throw würde im SDK
zu `logout()`+`clear()` eskalieren (der Uta-Bug). Geprüft: Erfolg beim
1. Versuch = genau 1 Aufruf, transienter Fehler wird per Retry geheilt,
dauerhafter Fehler = genau 3 Versuche + normale Rückkehr. Der Fake-Client
schlägt bei JEDEM anderen SDK-Aufruf laut fehl riefe der Guard je
`logout()`/`clear()` auf, fiele der Test sofort um.
- `test/power_levels_safety_test.dart` (6 Tests): `updatePowerLevelsSafely`
(Selbst-Aussperr-Schutz im Space-Admin) Server-Stand bleibt erhalten
(fremde users-Einträge überleben; genau der Bug, der schon mal Admins
ausgesperrt hat), Selbst-Degradierung bricht ab ohne zu schreiben,
users_default-Fallback, explizites `events[m.room.power_levels]`-
Erfordernis, M_NOT_FOUND-Start mit leerem Event, fremde Serverfehler
(M_FORBIDDEN) werden durchgereicht.
- `flutter test`: **alle 10 Tests grün auf dem Pi** (läuft headless, im
Gegensatz zu `flutter run` d. h. dieser Punkt ist NICHT „UNGETESTET (Pi)").
`flutter analyze` unverändert 1 bekannter Hinweis (`chat_provider.dart:42`).
- Wert fürs Refactoring: Beide Funktionen sind Vertragsgrenzen, die bei
M2-Umbauten (Call-Fassade, Storage) nicht wackeln dürfen künftige
Sessions sollten `flutter test` fest in Schritt 3 des Arbeitszyklus
aufnehmen (geht auch auf dem Pi).
**Offen/Nächster Schritt:** Unverändert: PC-Termin (docs/PC_TESTPLAN.md) oder
Bernds Antworten (Call-Pilot, SQLCipher-Priorität). Weitere Test-Kandidaten
scheitern derzeit an der Sichtbarkeit: `_detectType`, `_formatMessageTime`,
`_compareSpaceChildren`, die Megolm-Export-Kryptohelfer usw. sind alle
bibliotheksprivat testbar erst, wenn beim M2-Modulschnitt echte öffentliche
Schnittstellen entstehen (dann Tests gleich mitschreiben).
**Stolperfallen:** Die Vorsession wurde mitten im Schritt abgebrochen, OHNE
den Zwischenstand zu protokollieren (Pflicht laut CLAUDE.md) der untrackte
`test/`-Ordner war nur per `git status` auffindbar. Lehre: `git status` am
Sessionstart ernst nehmen; untrackte Dateien können unprotokollierte,
halbfertige Arbeit einer abgebrochenen Session sein.
---
## 2026-07-03 M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan ## 2026-07-03 M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei **Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
@@ -130,6 +868,228 @@ schließt den Review ab.
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
Protokoll im Root → Archiv-Banner eingefügt. Protokoll im Root → Archiv-Banner eingefügt.
- [x] **(e) Unit-Tests (99cde9a) gegengelesen korrekt, keine Änderung nötig.**
Beide Fake-Clients schlagen bei jedem nicht implementierten SDK-Aufruf laut
fehl (NoSuchMethodError) ein versehentlicher `logout()`/`clear()`-Aufruf im
Guard würde sofort auffallen. `soft_logout_guard_test`: deckt Erfolg,
transienten Fehler (Retry), dauerhaften Fehler (genau 3 Versuche, kein Throw)
und `Error` (nicht nur `Exception`) ab passt zum `catch (e)` im Guard, das
bewusst alles fängt. Dass die Tests grün sind, beweist nebenbei, dass
`AuthLog.write` ohne path_provider-Plattform (Testumgebung) sauber schluckt
statt zu werfen genau die Eigenschaft, an der der Guard hängt.
`power_levels_safety_test`: alle 6 Fälle gegen den echten Code
(`space_admin_core.dart:10-56`) nachvollzogen, inkl. der wichtigen Kante
„explizites `events[m.room.power_levels]` schlägt `state_default`".
Einzige Anmerkung: die zwei Retry-Tests schlafen real 2s+4s (Suite ~14s)
bewusst so gelassen, statt für Testbarkeit einen Delay-Parameter in den
heiligen Produktivpfad zu bohren.
- [x] **(f) Doku-Commits 16561bb/566938f (SQLCipher-Plan, PC-Testplan) gegen
Code verifiziert Behauptungen stimmen:** Factory-Stellen exakt wie
dokumentiert (`matrix_client.dart:24` nativ Android/iOS, `:27` FFI Desktop;
`background_push.dart:188` nativ), `sqlcipher_flutter_libs ^0.6.8` +
`flutter_secure_storage ^10` in pubspec vorhanden/ungenutzt wie beschrieben,
Verifikations-Tabellenname `box_inbound_group_session` steht wirklich in
matrix-6.2.0 `matrix_sdk_database.dart:139`, beide Prozesse öffnen dieselbe
`pyramid.sqlite` mit WAL + busy_timeout 5000. PC-Testplan deckt alle in
PROGRESS verstreuten UNGETESTET-Punkte ab (stichprobenartig gegengeprüft).
- [x] **(g) Autopilot-Konto-Wechsel (bfe5876, dazu 73420fe) geprüft kein
App-Code, funktional in Ordnung:** Wechsel-Logik (pyramid↔haupt, `JUST_SWITCHED`
verhindert Endlos-Ping-Pong, 20-min-Wartepause wenn beide Konten am Limit),
`CLAUDE_CONFIG_DIR` wird vor jeder Session neu gesetzt, Matrix-Token kommt aus
`~/gatus/.env` (nicht im Repo). Einzige Anmerkung (belassen): die
Limit-Erkennung greppt die GESAMTE Session-Ausgabe auf „usage limit" u. ä.
zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
unnötigen (aber harmlosen) Konto-Wechsel.
- [x] **(h) AuthLog-Tests (e0ac5cb) geprüft korrekt, keine Änderung nötig.**
Alle 4 Tests gegen `core/auth_log.dart` nachvollzogen: Der Kaputt-Plattform-
Test läuft korrekt VOR den Tests mit funktionierender Plattform (das statische
`_file`-Handle wird beim Throw nie befüllt, daher kein vergifteter Cache);
Kappungs-Test rechnet richtig (521. Zeile triggert Kappung auf 500, „Hallo
Log" aus dem Vortest fliegt raus). pubspec-Ergänzung sauber (offizielles
`path_provider_platform_interface`, war transitiv im Lockfile). Unabhängig
verifiziert: `flutter test` 22/22 grün, `flutter analyze` 1 bekannter
Hinweis. Anmerkung (belassen): Die Kappung schreibt die Datei nicht-atomar
neu und ist nicht gegen parallele Schreiber aus dem Push-Isolate gesperrt
für ein reines Diagnose-Log akzeptabel, kein Fix nötig.
- [x] **(i) settings_prefs-Tests (7dfdbf1) geprüft Tests korrekt, aber beim
Gegenlesen des Produktivcodes 1 echter Befund gefunden und gefixt (7b90f20):
Lade-Race in `BoolPref`/`StringSetPref`.** `StringSetPref.add/remove` machten
Read-Modify-Write auf möglicherweise noch ungeladenem State ein `add()`
direkt nach Erstellung persistierte nur den neuen Eintrag und LÖSCHTE alle
bereits gespeicherten (per neuem Regressionstest am alten Code bewiesen:
`['!a:hs']` im Store, `add('!b:hs')` → Store enthielt nur noch `['!b:hs']`).
Fix: `add`/`remove` warten jetzt auf `_loaded`. `BoolPref` zusätzlich mit
`_userSet`-Guard, damit ein spät landender persistierter Wert nie eine schon
getätigte Nutzeraktion zurücküberschreibt (die Microtask-Reihenfolge ist
plattform-/versionsabhängig im Test-Mock zufällig gutartig). Einordnung:
in der heutigen App kaum treffbar (die Provider werden beim App-Start geladen,
lange bevor jemand klickt deshalb nie aufgefallen), aber die Fassade wird
laut `docs/M2_MODULE_SCHNITT.md` zur Pflicht-Schnittstelle für neue, ggf.
früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün auf dem Pi.
- [x] **(j) PC-Testplan-Update (03b84e0) geprüft stimmt.** Teststand „22
Tests" war korrekt (durch (i) jetzt 24 im Testplan nachgezogen, siehe
Eintrag oben).
- [x] **(k) Secret-Scan über das ganze Repo ECHTER Sicherheitsbefund
(2026-07-04).** Vier aktive Zugangsdaten im öffentlich erreichbaren Repo
(Matrix-Admin-Token doppelt auch im Client!, Gitea-Release-Token,
LiveKit-`apiSecret`, Giphy-Key). Gefahrlose Stellen sofort bereinigt
(release.ps1 → Env-Var, Admin-Doku → Platzhalter); Rotation aller Secrets +
server-seitiges LiveKit-Token-Minting + nicht-Admin-Diagnose-Token als neue
M0-Punkte aufgenommen (brauchen Bernd/Server-Zugriff). Details im
PROGRESS-Eintrag 2026-07-04 oben.
- [x] **(l) Admin-Token-Entkopplung (925aafb) geprüft Client-Seite korrekt,
am Server-Gegenpart 1 echter Befund gefunden und gefixt (2026-07-04).**
Client-Diff verifiziert: neuer `pyr-diag-…`-Token stimmt mit `DIAG_TOKEN` in
`server.py` überein, `grep J5lax lib/ scripts/` unabhängig nachgeprüft (leer;
einziges verbliebenes Client-Secret ist das LiveKit-`apiSecret`, eigener
M0-Punkt). Befund: `/api/e2ee-diagnostics` hatte **kein Body-/Log-Größenlimit**
bei öffentlich bekanntem Token → Disk-Fill-DoS möglich. Gefixt + headless
verifiziert (200/403/413, Kappung), Details im PROGRESS-Eintrag 2026-07-04.
- [x] **(m) LiveKit-Migrationsplan (78e4174) gegen Code + Pi-Konfig geprüft
Behauptungen stimmen.** `livekit_token.dart:6` enthält das `apiSecret`,
Aufrufstelle `livekit_call_manager.dart:154` mit `ttlSeconds: 21600`, Grants
im Plan identisch mit dem Dart-Code (roomJoin/room/canPublish/canSubscribe/
canPublishData, metadata = Anzeigename, iss = LKMatrixPi, sub = identity),
`livekit.yaml` `keys:` wie beschrieben, Continuwuity antwortet auf
`127.0.0.1:6167` (whoami-Basis). Python-Snippet (stdlib-HS256) fachlich
korrekt (base64url ohne Padding, kompaktes JSON, HMAC-SHA256). Anmerkung:
Der Plan setzt zusätzlich `nbf`/`name` LiveKit akzeptiert beides, harmlos.
- [x] **(n) LiveKit-Token-Route ist inzwischen LIVE auf dem Pi gebaut (bisher
unprotokolliert) geprüft und funktionsfähig, aber Client noch nicht
umgestellt (2026-07-04).** Beim Review von `server.py` gefunden: Eine
Vorsession hat die in `docs/LIVEKIT_TOKEN_MIGRATION.md` geplante Route
`POST /api/livekit-token` bereits in `server.py` implementiert (whoami-Auth,
stdlib-HS256-Minting aus `livekit.yaml`) der CHANGES-Hook zeigt drei
server.py-Edits um 04:33, Dienst-Neustart 04:34, aber kein PROGRESS-Eintrag
(Session vor dem Protokoll abgebrochen). Headless verifiziert (127.0.0.1:8080
UND öffentlich): ungültiger Matrix-Token → 401, fehlende Felder → 400, 5-KB-Body
→ 413 (4-KB-Limit), gültiger Admin-Token → 200; das geminete JWT unabhängig
dekodiert: iss=LKMatrixPi, sub/name/metadata = geprüfte user_id (kein Spoofing,
Client kann Identität NICHT wählen), video-Grants wie im Client, 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.
- [x] **(o) KRITISCHER Befund beim server.py-Review: die Admin-Endpoints des
Dashboards haben KEINE Authentifizierung und sind öffentlich erreichbar
(2026-07-04).** Der Review von (l) hatte nur das Diagnose-Endpoint auf
Disk-Fill gehärtet der viel größere Nachbar blieb unbemerkt:
`https://dashboard.steggi-matrix.work` ist über Cloudflare offen aus dem
Internet erreichbar, und `POST /api/ban`, `/api/unban`, `/api/toggle-registration`,
`/api/create-invite`, `/api/run-stats` prüfen **keinen** Token. Belegt:
`curl -X POST .../api/ban -d '{"user":""}'` liefert die App-eigene 400-Antwort
`{"error":"Kein Nutzer angegeben"}` identisch zu 127.0.0.1, also KEIN
Cloudflare-Login davor. Damit könnte jeder Fremde Uta (jeden Nutzer) sperren
oder die offene Registrierung am Homeserver aktivieren. Der Hostname ist über
Certificate-Transparency-Logs praktisch auffindbar „geheim" trägt nicht.
**Bewusst NICHT blind gefixt:** Jede Auth-Ergänzung macht Bernds
Browser-Dashboard funktionslos, bis er einen Token einträgt / Cloudflare Access
einrichtet das mitten in einer autonomen Session zu deployen sperrt ihn ohne
Vorwarnung aus seinem eigenen Admin-Panel („kein Aussperren"). Fertiger,
gegen den echten Code geschriebener Plan mit zwei Wegen (A: Cloudflare Access =
Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in
`docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann
sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
Punkt aufgenommen. **Update (später am 2026-07-04):** Das akute Loch ist mit
einem aussperr-sicheren Interims-Gate geschlossen (Admin-Routen nur noch
Heimnetz/localhost, siehe PROGRESS-Eintrag „Dashboard-Befund (o)" oben)
offen bleibt nur noch die Fernzugriffs-Entscheidung A/B.
- [x] **(p) Interims-Gate (e97a748/455742d) reviewt korrekt; dabei halbfertige,
unprotokollierte Threading-Härtung einer abgebrochenen Session gefunden und
vollendet (2026-07-04).** Gate-Logik bestätigt: Socket-IP ist die belastbare
Prüfung (nicht spoofbar), Cf-Header nur Zweitschranke, Deny-Log mit 5-MB-Deckel
und ohne attacker-kontrollierten Text; Restrisiken (LAN-CSRF, offene
Nutzerlisten-Ansicht) waren bereits in `docs/DASHBOARD_AUTH_HARDENING.md`
dokumentiert kein neuer Befund am Gate. ABER: CHANGES-Hook zeigte drei
server.py-Edits um 09:43 ohne Commit/Protokoll abgebrochene Session hatte
`DIAG_LOCK` definiert, aber nie benutzt, und `ThreadingHTTPServer` im Kommentar
behauptet, aber nicht umgestellt; der Dienst lief zudem noch mit altem Code.
Vollendet (Threading + Lock-Nutzung), Dienst neu gestartet, 11 Headless-
Prüfungen grün inkl. Slow-Loris (parallele Anfrage 10 ms statt blockiert;
hängender Body-Read nach 30 s gekappt). Details im PROGRESS-Eintrag „(p)" oben.
- [x] **(q) Threading-Umstellung aus (p) tiefer geprüft echte Nebenläufigkeits-Race
in den Admin-Routen gefunden und mit `ADMIN_LOCK` behoben (2026-07-04).** (p) hatte
den öffentlich beschreibbaren Diagnose-Pfad per `DIAG_LOCK` abgesichert, aber die
Admin-Routen übersehen: `toggle-registration`/`create-invite` machen ein
Read-Modify-Write auf `conduit.toml` + `docker restart`, und `send_admin()` liest
nach 4 s die letzten Admin-Raum-Nachrichten zurück beides seit multithreaded
race-fähig (lost update, doppelter Neustart, vertauschte Antworten). Fix: neues
`ADMIN_LOCK`, das **nur** die Admin-Mutationen serialisiert; die öffentlichen Routen
(`livekit-token`, `e2ee-diagnostics`) laufen zuerst und bleiben nebenläufig
(Slow-Loris-Schutz aus (p) unangetastet). Handler-Logik wortwörtlich unverändert
(per `diff` gegen Backup verifiziert). Headless verifiziert: 8 Routen-Checks wie
vorher; Race-Beweis (2 parallele `run-stats` 2633 ms ≈ 2× einzeln 1388 ms →
serialisiert); Non-Blocking-Beweis (öffentliches `livekit-token` während gehaltenem
Lock in 11 ms). Details im PROGRESS-Eintrag „(q)" oben.
- [x] **(r) ADMIN_LOCK-Umbau aus (q) (c8ff850) geprüft Umbau korrekt, 3 kleinere
Restbefunde gefixt (2026-07-04).** Locking-Struktur und Routen-Reihenfolge von (q)
bestätigt (öffentliche Routen vor dem Gate/Lock, Handler-Logik unverändert).
Befunde: (1) `GET /api/registration-status` las `conduit.toml` ohne Lock, während
die Admin-Routen sie per Truncate+Write neu schreiben Leser konnte seit
multithreaded (p) eine halb geschriebene Datei sehen; Fix: Lesen unter `ADMIN_LOCK`
(bewusst KEIN `os.replace`: die Datei ist als Einzeldatei in den Container
gemountet, Inode-Falle im Code dokumentiert). (2) Negative `Content-Length`
ließ `read()` bis EOF blockieren (Thread hing bis zum 30-s-Timeout) bei
`livekit-token`/`e2ee-diagnostics`/`ban`/`unban` wird `< 0` jetzt mit 413
abgelehnt. (3) `ban`/`unban` ohne Body-Limit (hielten dabei den ADMIN_LOCK) →
4-KB-Deckel; Einladungs-Token von `random` auf `secrets` umgestellt (Token
schaltet Registrierung frei). Headless verifiziert (8 Checks inkl. 8-KB-Body →
413, `Content-Length: -5` → sofort 413 statt Hänger). Details im
PROGRESS-Eintrag „(r)" oben.
- [x] **(s) (r)-Fixes (5ada0e5) geprüft korrekt, 2 kleinere Restbefunde gefixt
(2026-07-04).** (r)-Diff gegen Backup verifiziert (GET-Leser-Lock,
Content-Length-Härtung, secrets-Token alles wie protokolliert), Dienst lief
mit aktuellem Code, kein unprotokollierter Rest im CHANGES-Hook-Log. Befunde
beim Ganzdatei-Gegenlesen: (1) ban/unban las den Body innerhalb des
`ADMIN_LOCK` ein Client mit angekündigtem, nie gesendetem Body hielt den
Lock bis zum 30-s-Socket-Timeout (das 4-KB-Limit deckelt nur die Größe, nicht
die Wartezeit; seit (r) hing damit auch der `registration-status`-Leser mit);
Fix: Body-Lesen/Validieren vor den Lock gezogen, Handler-Logik unverändert.
(2) `livekit-token`/`e2ee-diagnostics` leakten bei Fehlern `str(e)` an
Unauthentifizierte (interne Pfade/Adressen); Fix: generisches
`{"error": "Interner Fehler"}`, Details ins Journal (stderr). Headless
verifiziert: 8 Routen-Checks unverändert, Lock-Beweis (hängender ban-Body,
paralleler registration-status-GET in 15 ms), 500-Beweis (ungültiges JSON →
generisch, Detail im Journal). Details im PROGRESS-Eintrag „(s)" oben.
- [x] **(t) (s)-Fixes (13b333c) geprüft korrekt, 1 kleiner Restbefund gefixt
(2026-07-04).** (s)-Diff gegen Backup verifiziert (ban/unban-Body vor dem
Lock inkl. Validierung, generische 500er auf den öffentlichen POST-Routen
alles wie protokolliert), Dienst lief mit aktuellem Code, kein
unprotokollierter Rest im CHANGES-Hook-Log. Befund beim
Ganzdatei-Gegenlesen: `GET /api/registration-status` (öffentlich, `do_GET`
hat kein Gate) antwortete bei Fehlern noch mit `str(e)` hätte z. B. den
`conduit.toml`-Pfad geleakt; (s) hatte nur die POST-Routen gefixt. Fix:
generisches `{"error": "Interner Fehler"}`, Detail ins Journal. Headless
verifiziert: 8 Routen-Checks unverändert, 500-Beweis per kurzzeitigem
`chmod 000` auf `conduit.toml` (generisch nach außen, `PermissionError` im
Journal, danach wieder 200). Details im PROGRESS-Eintrag „(t)" oben.
- [x] **(u) (t)-Fix (d3e75c3) geprüft korrekt, KEIN Befund; server.py-Kette
abgeschlossen (2026-07-04).** Diff gegen `server.py.bak-20260704-review-t`
= exakt der protokollierte Mini-Fix, Dienst läuft mit aktuellem Code, kein
unprotokollierter Rest. Verbliebene `str(e)`-Stellen alle LAN-gegated
(absichtlich). Ohne neuen Anlass keine weitere Review-Runde an `server.py`.
Details im PROGRESS-Eintrag „(u)" oben.
- [x] **(v) Entscheidungs-Doku 825481e (`ANTWORTEN_BERND.md`, am PC entstanden)
geprüft konsistent, 1 Doku-Lücke gefunden und behoben (2026-07-05).** Alle
referenzierten Pläne/Dateien existieren, die vier Entscheidungen decken die
gesammelten „Fragen an Bernd" ab. Befund: Entscheidung 3 (Dashboard
Heimnetz-only, A/B geschlossen) war nicht in ROADMAP.md /
`docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen beide stellten die Frage
weiter als offen dar. In derselben Session behoben (ROADMAP-Punkt
geschlossen, Doku-Status aktualisiert, Vermerk zu Entscheidung 4 am
Secrets-Punkt). Details im PROGRESS-Eintrag „(v)+(w)" oben.
- [x] **(w) autopilot.sh-Fix 4477ed9 (CLAUDE_CONFIG_DIR unset für Hauptkonto,
am PC entstanden) geprüft korrekt, kein Befund (2026-07-05).** Begründung
im Commit stimmt (CLI sucht Config unter `$CLAUDE_CONFIG_DIR/.claude.json`,
Hauptprofil liegt unter `~/.claude.json`); `unset` wirkt auch nach
pyramid-Iterationen derselben Shell. Am lebenden System verifiziert: die
Review-Session selbst lief als Konto „haupt" mit leerem `CLAUDE_CONFIG_DIR`
und korrekt geladenem Profil. Kein weiterer `CLAUDE_CONFIG_DIR`-Verweis im
Repo. Details im PROGRESS-Eintrag „(v)+(w)" oben.
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken **Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene (verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
Fable-5-Vorsession bereits gefixt beide Fixes heute unabhängig verifiziert. Fable-5-Vorsession bereits gefixt beide Fixes heute unabhängig verifiziert.
+76
View File
@@ -60,6 +60,82 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden - [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
(`_SessionsSection` in `settings_modal.dart` Liste, Umbenennen, Verifizieren (`_SessionsSection` in `settings_modal.dart` Liste, Umbenennen, Verifizieren
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth) per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
- [x] **SICHERHEIT: Dashboard-Admin-Endpoints ERLEDIGT: Loch geschlossen,
Heimnetz-only ist laut Bernd der Endzustand** (Fable-5-Review (o);
Entscheidung `ANTWORTEN_BERND.md` Nr. 3, 2026-07-05).
`server.py` nimmt `/api/ban`, `/api/unban`, `/api/toggle-registration`,
`/api/create-invite`, `/api/run-stats` jetzt NUR noch aus Heimnetz/localhost an
(Socket-IP + Cloudflare-Header-Check); Fremde aus dem Internet bekommen 403.
Headless verifiziert (öffentlich 403, Heimnetz ok, App-Endpoints unverändert),
siehe PROGRESS.md 2026-07-04. **Bernds Dashboard läuft im Heimnetz unverändert:
`http://192.168.178.71:8080/stats.html`.** Offen bleibt: Will Bernd die
Admin-Buttons auch von unterwegs? Dann Weg A (Cloudflare Access, empfohlen
schließt auch die weiterhin öffentliche Nutzerlisten-Ansicht) oder Weg B
(`DASH_TOKEN`), Plan + Restrisiken in `docs/DASHBOARD_AUTH_HARDENING.md`.
Zusatz (Fable-5-Review (p), 2026-07-04): `server.py` läuft jetzt multithreaded
mit 30-s-Socket-Timeout ein einzelner langsamer Client kann den Server
(und damit `/api/livekit-token`) nicht mehr blockieren; headless verifiziert.
Zusatz (Fable-5-Review (q), 2026-07-04): Der durch (p) eingeführte
Nebenläufigkeits-Race in den Admin-Routen (Read-Modify-Write auf `conduit.toml`,
`send_admin`-Readback) ist mit `ADMIN_LOCK` geschlossen, das NUR die
Admin-Mutationen serialisiert die öffentlichen Routen bleiben nebenläufig;
headless verifiziert (2× parallel = 2× Laufzeit, öffentlicher Pfad 11 ms).
Zusatz (Fable-5-Review (r), 2026-07-04): drei Restbefunde gefixt
`registration-status`-Leser jetzt unter ADMIN_LOCK (Truncate-Write-Race),
negative/übergroße `Content-Length` überall abgelehnt (kein Thread-Hänger,
ban/unban 4-KB-Deckel), Einladungs-Token per `secrets` statt `random`.
Zusatz (Fable-5-Review (s), 2026-07-04): ban/unban-Body wird jetzt VOR dem
ADMIN_LOCK gelesen (hängender Body-Read konnte den Lock 30 s festhalten),
und die öffentlichen Routen antworten bei internen Fehlern generisch statt
mit Exception-Text (Info-Leak); Details ins Journal. Headless verifiziert.
Zusatz (Fable-5-Review (t), 2026-07-04): auch der öffentliche GET-Leser
`registration-status` antwortet bei internen Fehlern jetzt generisch
statt mit `str(e)` (hätte den `conduit.toml`-Pfad geleakt); headless
verifiziert inkl. 500-Beweis.
Zusatz (Fable-5-Review (u), 2026-07-04): (t)-Fix gegengelesen korrekt,
kein Befund; die server.py-Härtungskette ist damit KONVERGIERT (alle
verbliebenen `str(e)`-Antworten LAN-gegated, absichtlich).
**Abschluss (2026-07-05, `ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff
gewünscht die A/B-Frage ist GESCHLOSSEN, das Heimnetz-Gate ist der
Endzustand (unterwegs nutzt Bernd WireGuard). Die Restrisiken des Gates
(öffentliche Nutzerlisten-Ansicht, theoretisches LAN-CSRF) sind bewusst
akzeptiert und in `docs/DASHBOARD_AUTH_HARDENING.md` dokumentiert.
Nicht weiter daran arbeiten.**
- [x] **LiveKit-Token-Route server-seitig gebaut + verifiziert** (Fable-5-Review (n),
PROGRESS.md 2026-07-04). `POST /api/livekit-token` läuft live in `server.py`
(whoami-Auth, stdlib-HS256-Minting aus `livekit.yaml`), headless geprüft
(401/400/413/200, JWT-Signatur unabhängig gegen `livekit.yaml` gültig, Identität =
geprüfte user_id). **Offen:** Client-Umstellung (`livekit_token.dart`/
`livekit_call_manager.dart`) + Secret-Rotation beides PC/Gerät (heiliger
Call-Pfad), siehe `docs/LIVEKIT_TOKEN_MIGRATION.md`.
- [ ] **SICHERHEIT: geleakte Secrets rotieren + aus dem Client holen** (Fable-5-Review (k),
PROGRESS.md 2026-07-04). Das Gitea-Repo ist öffentlich (`private:false`), und es lagen
vier aktive Secrets drin teils schon bereinigt (release.ps1, Admin-Doku), aber:
- **Rotieren (Pflicht, sie sind bereits geleakt):** Matrix-Server-Admin-Token
`J5lax…` (noch gültig, voller Admin!), Gitea-Release-Token, LiveKit-`apiKey`/
`apiSecret`, Giphy-Key. Alte jeweils widerrufen.
- **LiveKit-Token server-seitig minten:** kleiner Token-Endpoint auf dem Pi, damit
`apiSecret` aus `lib/core/livekit_token.dart` verschwindet (App holt nur das JWT).
**Umsetzungsplan liegt fertig in `docs/LIVEKIT_TOKEN_MIGRATION.md`** (2026-07-04,
gegen Code + Pi-Konfig geschrieben) braucht echten Call-Test auf einem Gerät.
- **E2EE-Diagnose-Upload: ERLEDIGT (2026-07-04).** Der Dashboard-Server auf dem Pi
(`/home/steggi/matrix/server.py`) benutzte für `/api/e2ee-diagnostics` denselben
String wie den Matrix-Admin-Token deshalb steckte er im Client. Jetzt hat das
Diagnose-Endpoint einen eigenen, eng begrenzten Token (`DIAG_TOKEN`, hängt nur an
den Log an, keine Admin-Rechte); `settings_encryption.dart` benutzt diesen. Der
**Admin-Token ist damit nicht mehr im App-Code** (per `grep` bestätigt). Server +
Client umgestellt und verifiziert (neuer Token 200, alter Admin-Token 403). Der
Admin-Token selbst muss trotzdem noch rotiert werden (steht oben, Bernds Sache).
- Optional: Git-History-Rewrite (Bernds Entscheidung, Repo ist auch Backup).
- **Stand 2026-07-05 (`ANTWORTEN_BERND.md` Nr. 4): Rotation und
History-Rewrite ruhen BEWUSST beides bleibt Bernds manuelle Sache,
NICHT vom Autopilot anstoßen oder nachfragen. Der Punkt bleibt nur als
Merkposten offen; Autopilot-bearbeitbar ist hier nichts mehr.**
- **ACHTUNG bei „Repo einfach privat schalten":** Der In-App-Updater
(`lib/core/update_checker.dart`) holt Releases **anonym** über die öffentliche
Gitea-API (`/api/v1/repos/steggi/pyramid/releases`). Repo privat = Utas App
findet keine Updates mehr (und ein Fix ließe sich nicht mehr per Update
verteilen Henne-Ei). Erst Updater-Strategie klären, dann Sichtbarkeit ändern.
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar - [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben; (gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
+46 -5
View File
@@ -9,9 +9,32 @@
cd "$(dirname "$0")" cd "$(dirname "$0")"
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH" export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
# Pyramid laeuft auf eigenem Claude-Konto (Profil ~/.claude-pyramid), # --- Zwei Claude-Konten mit automatischem Wechsel bei 5h-Limit ---
# das Standard-Profil ~/.claude bleibt fuer die Webseiten-Arbeit. # Konto "pyramid" = Profil ~/.claude-pyramid (Standard fuer diese Arbeit),
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid" # Konto "haupt" = Profil ~/.claude (springt ein, wenn pyramid am Limit ist).
ACCOUNT_FILE="$HOME/.claude-accounts/active"
get_account() { cat "$ACCOUNT_FILE" 2>/dev/null || echo pyramid; }
apply_account() {
if [ "$(get_account)" = "haupt" ]; then
# Hauptkonto = Standard-Profil -> Variable NICHT setzen
# (sonst sucht Claude die Config unter ~/.claude/.claude.json statt ~/.claude.json)
unset CLAUDE_CONFIG_DIR
else
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
fi
}
switch_account() {
local cur next
cur=$(get_account)
if [ "$cur" = "pyramid" ]; then next=haupt; else next=pyramid; fi
mkdir -p "$HOME/.claude-accounts"
echo "$next" > "$ACCOUNT_FILE"
notify_matrix "🔺 Autopilot: 5h-Limit auf Konto '$cur' erreicht wechsle auf Konto '$next' und arbeite direkt weiter."
}
JUST_SWITCHED=0
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) --- # --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
MATRIX_URL="http://127.0.0.1:6167" MATRIX_URL="http://127.0.0.1:6167"
@@ -36,9 +59,11 @@ while true; do
sleep 600 sleep 600
continue continue
fi fi
echo "===== $(date '+%F %T') Neue Claude-Session =====" | tee -a autopilot.log apply_account
echo "===== $(date '+%F %T') Neue Claude-Session (Konto $(get_account)) =====" | tee -a autopilot.log
BEFORE=$(git rev-parse HEAD 2>/dev/null) BEFORE=$(git rev-parse HEAD 2>/dev/null)
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log SESSION_OUT=$(mktemp)
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log "$SESSION_OUT"
AFTER=$(git rev-parse HEAD 2>/dev/null) AFTER=$(git rev-parse HEAD 2>/dev/null)
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15) COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
@@ -50,6 +75,22 @@ $COMMITS
$NEXT" $NEXT"
fi fi
# 5h-Limit erkannt? -> sofort aufs andere Konto wechseln statt zu warten
if grep -qiE "hit your.*limit|usage limit|session limit" "$SESSION_OUT"; then
rm -f "$SESSION_OUT"
if [ "$JUST_SWITCHED" != "1" ]; then
switch_account
JUST_SWITCHED=1
continue
fi
JUST_SWITCHED=0
echo "===== $(date '+%F %T') Beide Konten am Limit - naechster Versuch in 20 Minuten =====" | tee -a autopilot.log
sleep 1200
continue
fi
rm -f "$SESSION_OUT"
JUST_SWITCHED=0
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
sleep 1200 sleep 1200
done done
+177
View File
@@ -0,0 +1,177 @@
# Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
**Status (ABGESCHLOSSEN 2026-07-05): Das Loch ist mit dem Heimnetz-Gate
(„Weg C", siehe unten) geschlossen, und Bernd hat entschieden
(`ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff das Gate ist der ENDZUSTAND.**
Die fünf Admin-Endpoints akzeptieren nur noch Anfragen aus dem
Heimnetz/localhost; alles, was über den Cloudflare-Tunnel kommt, bekommt 403 mit
einer Erklärung (die das Dashboard per `alert()` anzeigt). Bernds Dashboard
funktioniert im Heimnetz unverändert unter
`http://192.168.178.71:8080/stats.html`; unterwegs kommt Bernd per WireGuard ins
Heimnetz. Die A/B-Frage (Cloudflare Access vs. `DASH_TOKEN`) ist damit
GESCHLOSSEN die Optionen unten bleiben nur als Archiv stehen, falls sich der
Bedarf je ändert. Die unten dokumentierten Restrisiken des Gates sind bewusst
akzeptiert. **Nicht weiter an diesem Thema arbeiten.**
## Heimnetz-Gate („Weg C", aktiv seit 2026-07-04, seit 2026-07-05 Endzustand)
In `server.py` (Konstante `ADMIN_PATHS` + Helfer `lan_denied_reason`): eine
Admin-POST-Anfrage wird nur angenommen, wenn (1) **kein** `Cf-Connecting-Ip`/
`Cf-Ray`-Header vorhanden ist (= sie kam nicht durch den Cloudflare-Tunnel) und
(2) die **Socket-Absender-IP** (nicht spoofbar) `127.0.0.1`/`::1`/`192.168.*`
ist. Abgelehnte Versuche landen größenbegrenzt (5 MB-Deckel) in
`security_alerts.log` und damit in der Dashboard-Anzeige. Headless verifiziert:
öffentliche URL → 403 (ban/toggle), Heimnetz-IP und localhost → App-Validierung
erreicht (400 bei leerem Nutzer), gespoofter Cf-Header aus dem LAN → 403,
`GET /` öffentlich → 200, `/api/livekit-token` (401 bei ungültigem Token) und
`/api/e2ee-diagnostics` (403 bei falschem Token) unverändert öffentlich
erreichbar (die App braucht sie, eigene Auth vorhanden).
**Bekannte Restrisiken des Gates (Bernd bekannt und bewusst akzeptiert,
Entscheidung 2026-07-05):**
- Die **öffentliche Ansicht bleibt offen** und zeigt Nutzerliste + Präsenz
(Informations-Preisgabe). Weg A würde auch das schließen.
- **CSRF aus dem Heimnetz:** Eine bösartige Webseite in Bernds Browser könnte
theoretisch POSTs an `http://192.168.178.71:8080` schicken (moderne
Chrome-Versionen blocken Public→Private-Requests, Firefox nicht vollständig).
Erst ein Token/Access-Login (Weg A/B) schließt das sauber.
- Wer **im Heimnetz** ist, kann weiterhin ohne Anmeldung administrieren.
## Problem (belegt, nicht vermutet)
Der Dashboard-Server `/home/steggi/matrix/server.py` (läuft als
`matrix-stats.service` auf `0.0.0.0:8080`) ist über Cloudflare unter
`https://dashboard.steggi-matrix.work` **öffentlich aus dem Internet erreichbar**.
Getestet: `GET /` liefert 200 (stats.html), und die **zustandsändernden POST-Endpoints
antworten mit der App-eigenen Validierung ohne jede Authentifizierung**:
```
$ curl -X POST https://dashboard.steggi-matrix.work/api/ban -d '{"user":""}'
{"error": "Kein Nutzer angegeben"} # HTTP 400 App-Antwort, KEIN Login davor
```
Damit kann **jeder Fremde im Internet**, der den Hostnamen kennt, ohne Anmeldung:
- `/api/ban` / `/api/unban` **jeden Matrix-Nutzer sperren/entsperren** (auch Uta!)
→ Aussperrung / Denial-of-Service gegen echte Nutzer.
- `/api/toggle-registration` und `/api/create-invite` **offene Registrierung am
Homeserver aktivieren** (setzt `allow_registration = true` +
`yes_i_am_very_very_sure…`) → der Homeserver wird zur Spam-/Missbrauchsschleuder.
- `/api/run-stats` Stats-Skript auslösen (harmlos, aber ebenfalls ungeschützt).
**Der Hostname ist nicht wirklich geheim:** Cloudflare stellt TLS-Zertifikate aus,
die in den öffentlichen Certificate-Transparency-Logs landen jede Subdomain von
`steggi-matrix.work` ist per CT-Log-Scan auffindbar. Die bisherige „Sicherheit"
beruht also allein auf Verborgenheit eines Namens, der praktisch entdeckbar ist.
Bereits **korrekt abgesichert** (zum Vergleich, nicht betroffen):
- `/api/e2ee-diagnostics` eigener `DIAG_TOKEN` (Bearer).
- `/api/livekit-token` Matrix-Access-Token per `whoami` gegen Continuwuity.
Nur die vier Admin-/Stats-Endpoints oben sind offen.
## Wie das Dashboard heute aufruft
`stats.html` ruft alle Aktionen als **Same-Origin-`fetch` ohne Auth-Header** auf
(`toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction`
`/api/ban` /`/api/unban`). Es gibt keine Session, kein Cookie, keinen Token
deshalb bricht **jede** hinzugefügte Authentifizierung das Dashboard so lange,
bis `stats.html` den Token mitschickt. Genau darum ist das eine Bernd-Entscheidung
und kein stiller Fix.
## Option A (empfohlen, robust): Cloudflare Access vor die Subdomain
Am saubersten wird das Dashboard **komplett** durch Cloudflare Access geschützt
(Zero-Trust-Login vor der Subdomain), statt in `server.py` Auth nachzubauen:
1. Im Cloudflare-Zero-Trust-Dashboard eine **Access-Application** für
`dashboard.steggi-matrix.work` anlegen.
2. Policy: nur Bernds E-Mail (`bernd.steckmeister@gmail.com`) per One-Time-PIN
oder Google-Login zulassen.
3. Fertig `server.py` und `stats.html` bleiben **unverändert**, das Dashboard
funktioniert für Bernd wie bisher (nach dem Access-Login), aber Fremde kommen
gar nicht erst an die Endpoints.
Vorteil: kein Secret im Code, kein Umbau der HTML-Fetches, kein Aussperr-Risiko
durch vergessene Tokens. Nachteil: Bernd muss die Access-App einmal im
Cloudflare-Panel klicken (kein Code, aber sein Konto nötig). **Das ist der
Königsweg Option B nur, falls Access nicht gewünscht ist.**
## Option B (Fallback, rein in server.py): eigener Dashboard-Token
Wenn Access nicht gewünscht ist, ein **neuer, zufälliger `DASH_TOKEN`** (getrennt
vom geleakten Admin-Token `J5lax…` und vom `DIAG_TOKEN`!), den die vier
Admin-/Stats-Endpoints als `Authorization: Bearer <DASH_TOKEN>` verlangen:
```python
# server.py, zu den anderen Token-Konstanten:
DASH_TOKEN = "<neu erzeugen: python3 -c 'import secrets;print(\"dash-\"+secrets.token_urlsafe(24))'>"
def _dash_authed(self):
return self.headers.get("Authorization", "") == "Bearer " + DASH_TOKEN
```
In `do_POST` **vor** der Behandlung von `/api/ban`, `/api/unban`,
`/api/toggle-registration`, `/api/create-invite`, `/api/run-stats` einfügen:
```python
if self.path in ("/api/ban", "/api/unban", "/api/toggle-registration",
"/api/create-invite", "/api/run-stats") and not self._dash_authed():
self.send_error(403)
return
```
`GET /` und `/api/registration-status` bleiben öffentlich (nur Lesen), damit die
Stats-Seite ohne Token sichtbar bleibt.
**`stats.html`** darf den Token **NIEMALS fest eingebettet** ausliefern (die Seite
ist öffentlich der Token läge sonst für jeden offen). Stattdessen einmal im
Browser abfragen und in `localStorage` halten:
```javascript
function dashToken() {
var t = localStorage.getItem('dashToken');
if (!t) { t = prompt('Dashboard-Token:'); if (t) localStorage.setItem('dashToken', t); }
return t || '';
}
// und bei JEDEM Aktions-fetch:
fetch('/api/ban', { method:'POST',
headers: {'Content-Type':'application/json', 'Authorization':'Bearer ' + dashToken()},
body: JSON.stringify({user:u}) })
```
(betrifft `toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction`).
Bei 403 den gecachten Token verwerfen (`localStorage.removeItem('dashToken')`) und
neu fragen. Bernd gibt den Token so genau **einmal pro Browser** ein.
## Rollout / Testplan
**Option A:** Access-App klicken → von einem fremden Netz/Inkognito prüfen, dass
`https://dashboard.steggi-matrix.work/` zum Access-Login umleitet statt die Seite
zu zeigen; dann als Bernd einloggen und alle Buttons testen. Kein Code-Deploy.
**Option B (headless auf dem Pi verifizierbar):**
1. `server.py` + `stats.html` ändern, Dienst-Neustart (Prozess killen,
`Restart=always` lädt neu siehe PROGRESS 2026-07-04 zur polkit-Falle).
2. `curl -X POST .../api/ban -d '{"user":"x"}'` **ohne** Bearer → **403** erwartet.
3. Mit `Authorization: Bearer <DASH_TOKEN>` + leerem Nutzer → 400 (App-Validierung
erreicht) → beweist: Token akzeptiert, Auth greift.
4. Im Browser: Dashboard öffnen, Token eingeben, je ein Button testen
(Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an
einem **Test-Nutzer**, nicht an Uta).
## Warum erst nur der Plan kam und dann doch das Interims-Gate
Eine Token-/Access-Absicherung (A/B) macht die Dashboard-Buttons überall
funktionslos, bis Bernd aktiv etwas tut (Token eingeben bzw. Access-App
einrichten) das mitten in einer autonomen Session zu deployen verletzt die
CLAUDE.md-Leitplanke „kein Aussperren". Das *Wie* der Anmeldung bleibt deshalb
Bernds Richtungsentscheidung. Das später ergänzte Interims-Gate umgeht dieses
Dilemma: Es braucht keinerlei Aktion von Bernd (Heimnetz-Nutzung läuft
unverändert weiter, inkl. spontanem Spam-Bann von zu Hause), sperrt aber
Fremde aus dem Internet sofort aus. Einzige Einschränkung: Admin-Buttons über
die öffentliche URL gehen nicht die 403-Meldung im Dashboard erklärt das und
nennt die Heimnetz-URL. Mit Bernds Entscheidung vom 2026-07-05 (Heimnetz-only,
unterwegs WireGuard) ist genau dieser Zustand der gewollte Endzustand. Rückbau
(falls je nötig) = die Gate-Zeilen in `server.py` entfernen
(Backup: `server.py.bak-20260704-dashgate`).
+143
View File
@@ -0,0 +1,143 @@
# LiveKit-Token server-seitig minten (apiSecret aus dem Client entfernen)
**Status:** geplant, NICHT umgesetzt (braucht echten Call-Test auf einem Gerät
Calls sind laut CLAUDE.md „heilig"). Dieser Plan ist gegen den echten Code und die
echte Pi-Konfiguration geschrieben (2026-07-04), damit ein PC-Termin direkt starten
kann. Analog zu `docs/SQLCIPHER_MIGRATION.md`.
## Problem
`lib/core/livekit_token.dart` mintet die LiveKit-JWTs **im Client**. Dafür muss das
LiveKit-`apiSecret` im App-Code liegen (`livekit_token.dart:6`) d. h. jeder
App-Nutzer (auch Uta) trägt das SFU-Secret im Gerät. Wer es hat, kann sich für
**jeden** Raum und **jede** Identität ein Beitritts-Token ausstellen. Das ist ein
Architekturproblem, kein bloßer Repo-Leak: Selbst nach einem History-Rewrite bliebe
das Secret im ausgelieferten Binary.
Aktueller Fluss (`lib/core/livekit_call_manager.dart:154`):
```dart
final token = LiveKitTokenGenerator.generate(
roomName: roomName,
identity: identity,
displayName: identity,
ttlSeconds: 21600, // 6 h
);
```
Erzeugtes JWT (HS256, `livekit_token.dart:14-33`): Grants `video` mit
`roomJoin/room/canPublish/canSubscribe/canPublishData`, `metadata` = Anzeigename,
`iss` = apiKey (`LKMatrixPi`), `sub` = identity, `exp` = jetzt+ttl.
## Ziel
Das `apiSecret` liegt **nur noch auf dem Pi**. Der Client holt sich für einen
konkreten Raum ein fertiges JWT von einem kleinen Token-Endpoint und übergibt es
unverändert an `Room.connect`.
## Server-Seite (Pi)
Es gibt bereits einen schlanken Dashboard-Server ohne Fremd-Abhängigkeiten:
`/home/steggi/matrix/server.py` (stdlib `http.server`, läuft als
`matrix-stats.service` auf `0.0.0.0:8080`, nach außen `dashboard.steggi-matrix.work`).
Der Token-Endpoint kann dort als weitere Route `POST /api/livekit-token` andocken
kein neuer Dienst nötig. Das LiveKit-`apiSecret` steht schon auf dem Pi in
`/home/steggi/matrix/livekit.yaml` (`keys: LKMatrixPi: <secret>`); der Server liest
es von dort, statt es erneut zu hinterlegen (eine Quelle der Wahrheit).
**Wichtig Authentifizierung:** Der Endpoint darf NICHT anonym Tokens ausstellen
(sonst ist nichts gewonnen). Der Client schickt seinen **Matrix-Access-Token** mit;
der Server prüft ihn gegen Continuwuity und leitet daraus die Identität ab:
1. `POST /api/livekit-token`, Body `{ "room": "<roomName>", "matrix_token": "<access_token>" }`.
2. Server ruft `GET http://127.0.0.1:6167/_matrix/client/v3/account/whoami`
mit `Authorization: Bearer <matrix_token>` auf → liefert `user_id`
(401 → Endpoint gibt 401 zurück, kein Token).
3. `identity` = `user_id` (NICHT vom Client wählbar verhindert Identitäts-Spoofing).
4. Optional, aber empfohlen: Raummitgliedschaft prüfen (`/_matrix/client/v3/rooms/
{roomId}/joined_members` oder der Matrix-Raum, zu dem der Voice-Channel gehört)
nur ausstellen, wenn der Nutzer wirklich Mitglied ist. Für Phase 1 reicht die
whoami-Authentifizierung; Mitgliedschaftsprüfung als Härtung nachziehen.
5. Server mintet das LiveKit-JWT **mit denselben Grants wie heute** und gibt
`{ "token": "<jwt>", "url": "wss://livekit.steggi-matrix.work" }` zurück.
**JWT-Minting ohne Fremd-Paket (stdlib genügt):** LiveKit-Tokens sind HS256-JWTs.
Mit `hmac`/`hashlib`/`base64`/`json` aus der stdlib:
```python
import base64, hmac, hashlib, json, time
def _b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
def mint_livekit(api_key, api_secret, identity, room, name, ttl=21600):
now = int(time.time())
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"iss": api_key, "sub": identity, "name": name,
"nbf": now, "exp": now + ttl, "metadata": name,
"video": {"roomJoin": True, "room": room,
"canPublish": True, "canSubscribe": True,
"canPublishData": True},
}
segs = [_b64url(json.dumps(header, separators=(",", ":")).encode()),
_b64url(json.dumps(payload, separators=(",", ":")).encode())]
signing_input = b".".join(segs)
sig = hmac.new(api_secret.encode(), signing_input, hashlib.sha256).digest()
return (signing_input + b"." + _b64url(sig)).decode()
```
Verifikation lokal auf dem Pi (kein Gerät nötig): geminetes Token gegen den
laufenden LiveKit prüfen, z. B. per LiveKit-CLI `lk` oder indem man das JWT auf
jwt.io-Art dekodiert und Claims/Signatur vergleicht. Zusätzlich Gegentest mit dem
**alten** Client-Pfad: dasselbe Token muss `Room.connect` akzeptieren.
## Client-Seite
- `lib/core/livekit_token.dart`: `apiKey`/`apiSecret` **entfernen**. Die Klasse wird
entweder gelöscht oder zu einem dünnen HTTP-Client
`Future<String> fetchLiveKitToken({required String room, required String matrixToken})`,
der `POST https://dashboard.steggi-matrix.work/api/livekit-token` aufruft und das
`token`-Feld zurückgibt. `http`-Paket ist schon Abhängigkeit (siehe
`update_checker.dart`).
- `lib/core/livekit_call_manager.dart:154`: `LiveKitTokenGenerator.generate(...)`
→ `await fetchLiveKitToken(room: roomName, matrixToken: matrixClient.accessToken)`.
Der Manager hat den Matrix-Client bereits (`_matrixClient`, Zeile 149) der
Access-Token ist also verfügbar. `identity`/`displayName` werden nicht mehr vom
Client bestimmt (Server leitet Identität aus whoami ab); den bisherigen
`identity`-Parameter entsprechend zurückbauen.
- Fehlerpfade: Netzfehler / 401 / 403 sauber als Call-Fehler anzeigen
(`error = ...; notifyListeners()`), NICHT still schlucken sonst „Call verbindet
nicht" ohne Hinweis. TTL bleibt 21600 s (6 h), Erneuerung wie bisher beim
Neu-Beitreten.
## Rotation (Pflicht, danach)
Das alte `apiSecret` `rYUT2PRa…` ist geleakt (Client + Git-History). Nach der
Umstellung:
1. Neues `apiSecret` in `/home/steggi/matrix/livekit.yaml` (`keys:`) setzen.
2. LiveKit-Container neu starten
(`docker compose -f /home/steggi/matrix/docker-compose.yml restart livekit`).
3. `server.py` liest das Secret aus `livekit.yaml` kein zweiter Ort zu pflegen.
4. Alte Clients (Utas installierte APK) minten mit dem alten Secret → deren
selbst-gemintete Tokens werden nach der Rotation abgelehnt. Deshalb Rotation
**zusammen mit einem neuen Release** ausrollen, das den neuen Fetch-Pfad nutzt.
## Reihenfolge / Testplan (PC + Gerät)
1. `server.py`-Route bauen, lokal auf dem Pi verifizieren (whoami-Auth greift,
geminetes Token dekodiert korrekt, `Room.connect` akzeptiert es).
2. Client umstellen, `flutter analyze` sauber, `flutter test` grün.
3. **Auf echtem Gerät** (Calls heilig, kein Headless-Test möglich): Voice-Channel
beitreten, Audio/Video/Screenshare, Verlassen/Wieder-Beitreten, zweiter
Teilnehmer. Erst wenn das steht: Secret rotieren + Release.
4. Danach ROADMAP-M0-Punkt „LiveKit-Token server-seitig minten" abhaken und den
Rotations-Teil des Secret-Punkts als erledigt markieren.
## Warum nicht jetzt (auf dem Pi) gemacht
Die Server-Route ließe sich hier bauen und headless testen aber der zugehörige
Client-Umbau tauscht den **heiligen Call-Pfad** aus und ist auf diesem Pi ohne
GUI/Gerät nicht praktisch prüfbar. Einen halben, nicht end-to-end verifizierbaren
Umbau am Call-Pfad zu stapeln verbietet die CLAUDE.md-Leitplanke („keine
Verhaltensänderung ohne Not", Calls heilig). Deshalb hier nur der fertige Plan.
+7
View File
@@ -8,6 +8,13 @@ Punkt hier eintragen (✅/❌ + Datum), Befunde als eigene PROGRESS-Einträge.
Schnellster Weg: `flutter run -d windows`. Schnellster Weg: `flutter run -d windows`.
## 0. Vorab (1 Minute)
- [ ] `flutter test` die Regressionstests unter `test/` (Soft-Logout-Guard,
Power-Levels-Schutz, AuthLog, Storage-Fassade `settings_prefs`
Stand 2026-07-03: 24 Tests) müssen grün sein, bevor irgendetwas
anderes geprüft wird.
## 1. Härtetest Login/Krypto (M1 PFLICHT, zuerst!) ## 1. Härtetest Login/Krypto (M1 PFLICHT, zuerst!)
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard). Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
+19 -19
View File
@@ -70,7 +70,7 @@ Continuwuity hat einen speziellen Admin-Raum. Als Server-Admin kannst du dort Be
# Als curl (Admin-Token erforderlich): # Als curl (Admin-Token erforderlich):
# Admin-Room-ID herausfinden: # Admin-Room-ID herausfinden:
curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3Asteggi-matrix.work" \ curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3Asteggi-matrix.work" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# → {"room_id": "!adminRoomId:steggi-matrix.work", ...} # → {"room_id": "!adminRoomId:steggi-matrix.work", ...}
``` ```
@@ -84,22 +84,22 @@ curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3
# User zu Server-Admin machen # User zu Server-Admin machen
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \ curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"user_id": "@todesneutron:steggi-matrix.work"}' -d '{"user_id": "@todesneutron:steggi-matrix.work"}'
# Server-Status abfragen # Server-Status abfragen
curl "https://steggi-matrix.work/_conduwuit/server/version" \ curl "https://steggi-matrix.work/_conduwuit/server/version" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# Alternativ — Standard Matrix-Whoami (prüft ob Token gültig): # Alternativ — Standard Matrix-Whoami (prüft ob Token gültig):
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# → {"user_id": "@todesneutron:steggi-matrix.work", "is_guest": false} # → {"user_id": "@todesneutron:steggi-matrix.work", "is_guest": false}
# Prüfen ob User Server-Admin ist (Conduwuit-spezifisch): # Prüfen ob User Server-Admin ist (Conduwuit-spezifisch):
curl "https://steggi-matrix.work/_conduwuit/admin/v1/users/@todesneutron:steggi-matrix.work" \ curl "https://steggi-matrix.work/_conduwuit/admin/v1/users/@todesneutron:steggi-matrix.work" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
``` ```
--- ---
@@ -112,7 +112,7 @@ Als Server-Admin kannst du via Matrix Client API Power Levels in jedem Raum setz
```bash ```bash
# Schritt 1: Aktuelles Power-Level-Event lesen # Schritt 1: Aktuelles Power-Level-Event lesen
ROOM_ID="!raumId:steggi-matrix.work" ROOM_ID="!raumId:steggi-matrix.work"
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" TOKEN="<ADMIN_TOKEN>"
SERVER="https://steggi-matrix.work" SERVER="https://steggi-matrix.work"
curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \ curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
@@ -172,7 +172,7 @@ await client.setRoomStateWithKey(
```bash ```bash
# Testen ob restricted join rules funktionieren: # Testen ob restricted join rules funktionieren:
ROOM_ID="!testRaum:steggi-matrix.work" ROOM_ID="!testRaum:steggi-matrix.work"
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" TOKEN="<ADMIN_TOKEN>"
SPACE_ID="!spaceId:steggi-matrix.work" SPACE_ID="!spaceId:steggi-matrix.work"
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \
@@ -230,7 +230,7 @@ try {
```bash ```bash
# 1. Prüfen: Welchen Power Level hat mein User in diesem Raum? # 1. Prüfen: Welchen Power Level hat mein User in diesem Raum?
ROOM_ID="!raumId:steggi-matrix.work" ROOM_ID="!raumId:steggi-matrix.work"
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" TOKEN="<ADMIN_TOKEN>"
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \ curl "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
-H "Authorization: Bearer ${TOKEN}" -H "Authorization: Bearer ${TOKEN}"
@@ -269,7 +269,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/capabilities" \
```bash ```bash
# Prüfen ob User Server-Admin ist: # Prüfen ob User Server-Admin ist:
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# Auf Pi (SSH): User-Datenbank direkt abfragen (Conduwuit/Continuwuity nutzt RocksDB) # Auf Pi (SSH): User-Datenbank direkt abfragen (Conduwuit/Continuwuity nutzt RocksDB)
# Alternativ: Über Admin-Room-Befehl # Alternativ: Über Admin-Room-Befehl
@@ -278,7 +278,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
# Über HTTP (Conduwuit Admin-Endpoint): # Über HTTP (Conduwuit Admin-Endpoint):
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \ curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"user_id": "@todesneutron:steggi-matrix.work"}' -d '{"user_id": "@todesneutron:steggi-matrix.work"}'
``` ```
@@ -292,19 +292,19 @@ Da kein Synapse-Admin-API existiert, nutze die Standard-Matrix-Client-API mit Ad
```bash ```bash
# Raum beitreten (als Admin, auch wenn invite-only): # Raum beitreten (als Admin, auch wenn invite-only):
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/join/!roomId%3Asteggi-matrix.work" \ curl -X POST "https://steggi-matrix.work/_matrix/client/v3/join/!roomId%3Asteggi-matrix.work" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{}' -d '{}'
# User in Raum einladen: # User in Raum einladen:
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/invite" \ curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/invite" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"user_id": "@user:steggi-matrix.work"}' -d '{"user_id": "@user:steggi-matrix.work"}'
# Space-Child-Relation setzen (Channel einem Space hinzufügen): # Space-Child-Relation setzen (Channel einem Space hinzufügen):
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Asteggi-matrix.work/state/m.space.child/!channelId%3Asteggi-matrix.work/" \ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Asteggi-matrix.work/state/m.space.child/!channelId%3Asteggi-matrix.work/" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"via": ["steggi-matrix.work"], "via": ["steggi-matrix.work"],
@@ -313,7 +313,7 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Astegg
# Space-Parent-Relation setzen (im Channel-Raum): # Space-Parent-Relation setzen (im Channel-Raum):
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Asteggi-matrix.work/state/m.space.parent/!spaceId%3Asteggi-matrix.work/" \ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Asteggi-matrix.work/state/m.space.parent/!spaceId%3Asteggi-matrix.work/" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"via": ["steggi-matrix.work"], "via": ["steggi-matrix.work"],
@@ -322,11 +322,11 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Aste
# Alle Mitglieder eines Raums auflisten: # Alle Mitglieder eines Raums auflisten:
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/members" \ curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/members" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# Raum verlassen (als User, nicht löschen): # Raum verlassen (als User, nicht löschen):
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/leave" \ curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/leave" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \ -H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{}' -d '{}'
``` ```
@@ -419,15 +419,15 @@ Wenn du M_FORBIDDEN bekommst:
```bash ```bash
# 1. Token gültig? # 1. Token gültig?
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# 2. PL im betroffenen Raum prüfen: # 2. PL im betroffenen Raum prüfen:
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.power_levels/" \ curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.power_levels/" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# 3. Mitglied des Raums? # 3. Mitglied des Raums?
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.member/%40todesneutron%3Asteggi-matrix.work/" \ curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.member/%40todesneutron%3Asteggi-matrix.work/" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" -H "Authorization: Bearer <ADMIN_TOKEN>"
# 4. Was genau schlägt fehl? (Antwort-Body lesen!) # 4. Was genau schlägt fehl? (Antwort-Body lesen!)
# M_FORBIDDEN + "Power level too low" → PL erhöhen # M_FORBIDDEN + "Power level too low" → PL erhöhen
+13 -4
View File
@@ -7,6 +7,9 @@ import 'package:shared_preferences/shared_preferences.dart';
class BoolPref extends StateNotifier<bool> { class BoolPref extends StateNotifier<bool> {
final String _key; final String _key;
// Guards against the load race: if set()/toggle() ran before _load's read
// lands, the stale persisted value must not overwrite the user's choice.
bool _userSet = false;
BoolPref(this._key, bool defaultValue) : super(defaultValue) { BoolPref(this._key, bool defaultValue) : super(defaultValue) {
_load(defaultValue); _load(defaultValue);
@@ -14,10 +17,11 @@ class BoolPref extends StateNotifier<bool> {
Future<void> _load(bool def) async { Future<void> _load(bool def) async {
final p = await SharedPreferences.getInstance(); final p = await SharedPreferences.getInstance();
if (mounted) state = p.getBool(_key) ?? def; if (mounted && !_userSet) state = p.getBool(_key) ?? def;
} }
Future<void> set(bool v) async { Future<void> set(bool v) async {
_userSet = true;
state = v; state = v;
final p = await SharedPreferences.getInstance(); final p = await SharedPreferences.getInstance();
await p.setBool(_key, v); await p.setBool(_key, v);
@@ -30,9 +34,10 @@ class BoolPref extends StateNotifier<bool> {
class StringSetPref extends StateNotifier<Set<String>> { class StringSetPref extends StateNotifier<Set<String>> {
final String _key; final String _key;
late final Future<void> _loaded;
StringSetPref(this._key) : super({}) { StringSetPref(this._key) : super({}) {
_load(); _loaded = _load();
} }
Future<void> _load() async { Future<void> _load() async {
@@ -42,14 +47,18 @@ class StringSetPref extends StateNotifier<Set<String>> {
} }
Future<void> add(String v) async { Future<void> add(String v) async {
if (state.contains(v)) return; // Read-modify-write: ohne abgeschlossenes Laden würde hier der halbleere
// Start-State zurückgeschrieben und persistierte Einträge gingen verloren.
await _loaded;
if (!mounted || state.contains(v)) return;
state = {...state, v}; state = {...state, v};
final p = await SharedPreferences.getInstance(); final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList()); await p.setStringList(_key, state.toList());
} }
Future<void> remove(String v) async { Future<void> remove(String v) async {
if (!state.contains(v)) return; await _loaded;
if (!mounted || !state.contains(v)) return;
state = {...state}..remove(v); state = {...state}..remove(v);
final p = await SharedPreferences.getInstance(); final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList()); await p.setStringList(_key, state.toList());
@@ -428,7 +428,11 @@ class _EncryptionSectionState extends ConsumerState<_EncryptionSection> {
final notifier = ref.read(e2eeDiagnosticsProvider.notifier); final notifier = ref.read(e2eeDiagnosticsProvider.notifier);
final msg = await notifier.upload( final msg = await notifier.upload(
serverUrl: 'https://dashboard.steggi-matrix.work', serverUrl: 'https://dashboard.steggi-matrix.work',
token: 'J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI', // Eng begrenzter Diagnose-Token (haengt nur an den Diagnose-Log an,
// KEINE Admin-Rechte). Bewusst NICHT der Matrix-Server-Admin-Token
// der darf niemals im Client landen. Passendes Gegenstueck in
// /home/steggi/matrix/server.py (DIAG_TOKEN) auf dem Pi.
token: 'pyr-diag-6AyELgODRv-4rF4dcau3kSa5YbgZqUcn',
); );
if (mounted) setState(() { _diagUploading = false; _diagMsgIsError = false; _diagMsg = msg; }); if (mounted) setState(() { _diagUploading = false; _diagMsgIsError = false; _diagMsg = msg; });
} catch (e) { } catch (e) {
+7 -7
View File
@@ -801,10 +801,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
mime: mime:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -918,7 +918,7 @@ packages:
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: "direct dev"
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
@@ -1294,18 +1294,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.16" version: "0.6.17"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
+4
View File
@@ -82,6 +82,10 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
# Nur für Tests: offizieller Weg, path_provider ohne echte Plattform zu
# mocken (PathProviderPlatform.instance überschreiben). War schon transitiv
# im Lockfile, kein neues externes Paket.
path_provider_platform_interface: ^2.1.2
dependency_overrides: dependency_overrides:
webcrypto: webcrypto:
+11 -1
View File
@@ -11,7 +11,17 @@ Set-Location (Split-Path $PSScriptRoot -Parent)
# ── Config ──────────────────────────────────────────────────────────────────── # ── Config ────────────────────────────────────────────────────────────────────
$GiteaBase = "http://192.168.178.71:3000" $GiteaBase = "http://192.168.178.71:3000"
$GiteaRepo = "steggi/pyramid" $GiteaRepo = "steggi/pyramid"
$GiteaToken = "bb522f9646c6856c9ab58bef0151ac36a9ce1d57" # Token NIE hier eintragen (CLAUDE.md: keine Secrets ins Repo). Er kommt aus der
# Umgebungsvariable PYRAMID_GITEA_TOKEN einmalig pro PC setzen mit:
# [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")
# (neues Terminal öffnen, damit die Variable sichtbar ist)
$GiteaToken = $env:PYRAMID_GITEA_TOKEN
if ([string]::IsNullOrWhiteSpace($GiteaToken)) {
Write-Error ("PYRAMID_GITEA_TOKEN ist nicht gesetzt. Einmalig setzen mit:`n" +
' [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")' +
"`nDen Token in Gitea unter Einstellungen > Anwendungen erzeugen (Scope: repository).")
exit 1
}
$Headers = @{ Authorization = "token $GiteaToken"; "Content-Type" = "application/json" } $Headers = @{ Authorization = "token $GiteaToken"; "Content-Type" = "application/json" }
# ── Read and Update version from pubspec.yaml ───────────────────────────────── # ── Read and Update version from pubspec.yaml ─────────────────────────────────
+82
View File
@@ -0,0 +1,82 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:pyramid/core/auth_log.dart';
/// Plattform kaputt/nicht verfügbar (z. B. sehr früher App-Start, exotischer
/// Fehler): genau der Fall, in dem AuthLog.write NIEMALS werfen darf der
/// Soft-Logout-Guard ruft es in seinem catch-Pfad auf, und ein Throw dort
/// würde im SDK zu logout()+clear() eskalieren (der Uta-Bug).
class _BrokenPathProvider extends PathProviderPlatform {
@override
Future<String?> getApplicationSupportPath() async =>
throw StateError('Plattform nicht verfügbar');
}
class _TempPathProvider extends PathProviderPlatform {
_TempPathProvider(this.path);
final String path;
@override
Future<String?> getApplicationSupportPath() async => path;
}
void main() {
// WICHTIG: Reihenfolge der Tests ist Absicht. AuthLog cached sein File-Handle
// statisch; der Kaputt-Plattform-Test muss laufen, BEVOR ein Test mit
// funktionierender Plattform das Handle in den Cache legt.
late Directory tempDir;
setUpAll(() {
tempDir = Directory.systemTemp.createTempSync('pyramid_auth_log_test');
});
tearDownAll(() {
tempDir.deleteSync(recursive: true);
});
test(
'write/read werfen NIE, auch wenn die Plattform fehlt '
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden)',
() async {
PathProviderPlatform.instance = _BrokenPathProvider();
// Darf normal zurückkehren, obwohl getApplicationSupportPath wirft.
await AuthLog.write('darf nicht werfen');
expect(await AuthLog.read(), startsWith('Log konnte nicht gelesen werden'));
});
test('read ohne bisherige Einträge liefert Platzhalter statt Fehler',
() async {
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
expect(await AuthLog.read(), '(noch keine Einträge)');
});
test('write hängt Zeile mit ISO-Zeitstempel an, read liefert sie zurück',
() async {
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
await AuthLog.write('Hallo Log');
final content = await AuthLog.read();
expect(content, contains('Hallo Log'));
expect(
RegExp(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}').hasMatch(content),
isTrue,
reason: 'jede Zeile beginnt mit einem ISO-8601-Zeitstempel',
);
});
test(
'Log wird auf 500 Zeilen gekappt (älteste fliegen raus), '
'damit es nie unbegrenzt wächst', () async {
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
for (var i = 0; i < 520; i++) {
await AuthLog.write('Zeile $i');
}
final lines = (await AuthLog.read()).trimRight().split('\n');
expect(lines.length, 500);
expect(lines.last, contains('Zeile 519'),
reason: 'neueste Einträge müssen überleben');
expect((await AuthLog.read()).contains('Hallo Log'), isFalse,
reason: 'ältester Eintrag (aus dem Test davor) muss weggekappt sein');
});
}
+170
View File
@@ -0,0 +1,170 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
/// Fake-Client für `updatePowerLevelsSafely`: liefert einen vorgegebenen
/// Server-Stand des power_levels-Events und zeichnet auf, was (wenn
/// überhaupt) zurückgeschrieben wird. Alles andere NoSuchMethodError.
class _FakeClient implements Client {
_FakeClient({required this.serverState, this.getError});
Map<String, Object?> serverState;
MatrixException? getError;
Map<String, Object?>? written;
@override
String? get userID => '@me:pyramid.example';
@override
Future<Map<String, Object?>> getRoomStateWithKey(
String roomId,
String eventType,
String stateKey, {
Format? format,
}) async {
final err = getError;
if (err != null) throw err;
return serverState;
}
@override
Future<String> setRoomStateWithKey(
String roomId,
String eventType,
String stateKey,
Map<String, Object?> body,
) async {
written = body;
return r'$neuesEvent';
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeRoom implements Room {
_FakeRoom(this.client);
@override
final Client client;
@override
String get id => '!raum:pyramid.example';
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
test('Server-Stand bleibt erhalten: fremde Einträge werden nie überschrieben '
'(genau der Bug, der schon einmal Admins ausgesperrt hat)', () async {
final client = _FakeClient(serverState: {
'users': {'@me:pyramid.example': 100, '@uta:pyramid.example': 50},
'users_default': 0,
'state_default': 50,
});
final room = _FakeRoom(client);
await updatePowerLevelsSafely(room, (content) {
(content['users'] as Map<String, dynamic>)['@neu:pyramid.example'] = 50;
});
final users = client.written!['users'] as Map;
expect(users['@uta:pyramid.example'], 50,
reason: 'bestehender Server-Eintrag muss überleben');
expect(users['@me:pyramid.example'], 100);
expect(users['@neu:pyramid.example'], 50);
});
test('Selbst-Degradierung unter das nötige Level wird abgebrochen, '
'nichts wird geschrieben', () async {
final client = _FakeClient(serverState: {
'users': {'@me:pyramid.example': 100},
'state_default': 50,
});
final room = _FakeRoom(client);
await expectLater(
updatePowerLevelsSafely(room, (content) {
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 0;
}),
throwsException,
);
expect(client.written, isNull);
});
test('eigener Eintrag entfernt → Fallback auf users_default greift und '
'schützt ebenfalls', () async {
final client = _FakeClient(serverState: {
'users': {'@me:pyramid.example': 100},
'users_default': 0,
});
final room = _FakeRoom(client);
await expectLater(
updatePowerLevelsSafely(room, (content) {
(content['users'] as Map<String, dynamic>)
.remove('@me:pyramid.example');
}),
throwsException,
);
expect(client.written, isNull);
});
test('explizites events[m.room.power_levels]-Erfordernis wird respektiert',
() async {
final client = _FakeClient(serverState: {
'users': {'@me:pyramid.example': 60},
'events': {'m.room.power_levels': 75},
'state_default': 50,
});
final room = _FakeRoom(client);
// 60 < 75 muss abbrechen, obwohl state_default (50) erfüllt wäre.
await expectLater(
updatePowerLevelsSafely(room, (_) {}),
throwsException,
);
expect(client.written, isNull);
});
test('fehlendes power_levels-Event (M_NOT_FOUND): Start mit leerem Event, '
'Schreiben nur wenn man sich selbst hoch genug setzt', () async {
final client = _FakeClient(
serverState: {},
getError: MatrixException.fromJson(
{'errcode': 'M_NOT_FOUND', 'error': 'Event not found.'}),
);
final room = _FakeRoom(client);
// Ohne eigenen Eintrag: users_default 0 < state_default-Fallback 50 Abbruch.
await expectLater(
updatePowerLevelsSafely(room, (_) {}),
throwsException,
);
expect(client.written, isNull);
// Setzt man sich selbst auf 100, darf geschrieben werden.
await updatePowerLevelsSafely(room, (content) {
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 100;
});
final users = client.written!['users'] as Map;
expect(users['@me:pyramid.example'], 100);
});
test('andere Serverfehler (z. B. M_FORBIDDEN) werden durchgereicht, '
'nichts wird geschrieben', () async {
final client = _FakeClient(
serverState: {},
getError: MatrixException.fromJson(
{'errcode': 'M_FORBIDDEN', 'error': 'Nope.'}),
);
final room = _FakeRoom(client);
await expectLater(
updatePowerLevelsSafely(room, (_) {}),
throwsA(isA<MatrixException>()),
);
expect(client.written, isNull);
});
}
+135
View File
@@ -0,0 +1,135 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pyramid/core/settings_prefs.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
// Frischer In-Memory-Store pro Test (setzt auch die gecachte
// SharedPreferences-Instanz zurück).
SharedPreferences.setMockInitialValues({});
});
group('BoolPref', () {
test('lädt persistierten Wert und überstimmt damit den Default', () async {
SharedPreferences.setMockInitialValues({'notif_sound': false});
final pref = BoolPref('notif_sound', true);
expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
await pumpEventQueue();
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
});
test(
'set() sofort nach Erstellung gewinnt gegen den später ladenden '
'persistierten Wert (Lade-Race)', () async {
SharedPreferences.setMockInitialValues({'notif_sound': false});
final pref = BoolPref('notif_sound', false);
final done = pref.set(true); // bevor _load fertig ist
await pumpEventQueue();
await done;
expect(pref.state, true,
reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis '
'zurücküberschrieben werden');
final p = await SharedPreferences.getInstance();
expect(p.getBool('notif_sound'), true);
});
test('set/toggle ändern Zustand und persistieren', () async {
final pref = BoolPref('notif_desktop', true);
await pumpEventQueue();
pref.toggle();
await pumpEventQueue();
expect(pref.state, false);
final p = await SharedPreferences.getInstance();
expect(p.getBool('notif_desktop'), false);
});
});
group('StringSetPref', () {
test('lädt persistierte Liste als Set, add/remove persistieren', () async {
SharedPreferences.setMockInitialValues({
'muted_rooms': ['!a:hs', '!b:hs'],
});
final pref = StringSetPref('muted_rooms');
await pumpEventQueue();
expect(pref.state, {'!a:hs', '!b:hs'});
await pref.add('!c:hs');
await pref.remove('!a:hs');
final p = await SharedPreferences.getInstance();
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
});
test(
'add() sofort nach Erstellung verliert keine bereits persistierten '
'Einträge (Lade-Race)', () async {
SharedPreferences.setMockInitialValues({
'muted_rooms': ['!a:hs'],
});
final pref = StringSetPref('muted_rooms');
final done = pref.add('!b:hs'); // bevor _load fertig ist
await pumpEventQueue();
await done;
expect(pref.state, {'!a:hs', '!b:hs'});
final p = await SharedPreferences.getInstance();
expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'},
reason: 'Read-Modify-Write auf ungeladenem State darf keine '
'persistierten Einträge verlieren');
});
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
final pref = StringSetPref('muted_rooms');
await pumpEventQueue();
await pref.add('!a:hs');
await pref.add('!a:hs');
await pref.remove('!gibtsnicht:hs');
expect(pref.state, {'!a:hs'});
});
});
group('Theme-Prefs', () {
test('leerer Store liefert die dokumentierten Defaults', () async {
final t = await loadThemePrefs();
expect(t.isDark, true);
expect(t.accentIdx, 0);
expect(t.radius, 12.0);
expect(t.motion, 1.0);
});
test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
'den Rest unangetastet', () async {
await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
var t = await loadThemePrefs();
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));
// Nur ein Feld ändern die anderen dürfen nicht zurückfallen.
await saveThemePrefs(accentIdx: 1);
t = await loadThemePrefs();
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
});
});
group('Server-Banner', () {
test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
await saveServerBanner('https://hs1', 'mxc://hs1/abc');
await saveServerBanner('https://hs2', 'mxc://hs2/def');
expect(await loadAllServerBanners(),
{'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});
await saveServerBanner('https://hs1', null);
await saveServerBanner('https://hs2', '');
expect(await loadAllServerBanners(), isEmpty);
});
test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
() async {
SharedPreferences.setMockInitialValues(
{'server_banners': '{kein json'});
expect(await loadAllServerBanners(), isEmpty);
});
});
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/core/soft_logout_guard.dart';
/// Fake-Client: nur `refreshAccessToken` ist implementiert. Jeder andere
/// SDK-Aufruf (insbesondere `logout()`/`clear()`!) schlägt laut mit
/// NoSuchMethodError fehl so würde der Test es sofort merken, wenn der
/// Guard je etwas anderes am Client aufriefe als den Refresh.
class _FakeClient implements Client {
_FakeClient(this._onRefresh);
final Future<void> Function(int attempt) _onRefresh;
int attempts = 0;
@override
Future<void> refreshAccessToken({Duration? customRefreshTokenLifetime}) {
attempts++;
return _onRefresh(attempts);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
test('erfolgreicher Refresh beim ersten Versuch: genau 1 Aufruf', () async {
final client = _FakeClient((_) async {});
await guardedSoftLogoutRefresh(client);
expect(client.attempts, 1);
});
test(
'transienter Fehler (z. B. Netz weg / Token-Rotations-Race mit dem '
'Push-Isolate): Retry heilt, kein Fehler nach außen',
() async {
final client = _FakeClient((attempt) async {
if (attempt == 1) throw Exception('Netzwerkfehler');
});
await guardedSoftLogoutRefresh(client);
expect(client.attempts, 2);
},
);
test(
'dauerhafter Fehler: genau 3 Versuche, und der Guard wirft NIE '
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden '
'ein Throw hier würde im SDK zu logout()+clear() eskalieren)',
() async {
final client = _FakeClient((_) async {
throw Exception('Server dauerhaft nicht erreichbar');
});
// Darf trotz 3 Fehlversuchen normal (ohne Exception) zurückkehren.
await guardedSoftLogoutRefresh(client);
expect(client.attempts, 3);
},
// Der Guard wartet real 2s+4s zwischen den Versuchen.
timeout: const Timeout(Duration(seconds: 30)),
);
test('auch ein Error (nicht nur Exception) im Refresh entkommt nie', () async {
final client = _FakeClient((_) async {
throw StateError('unerwarteter Zustand');
});
await guardedSoftLogoutRefresh(client);
expect(client.attempts, 3);
}, timeout: const Timeout(Duration(seconds: 30)));
}