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 SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-05 14:53:27 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-05 14:52:09 +02:00
Bernd SteckmeisterandClaude Fable 5 4477ed9101 fix: Hauptkonto-Profil ohne CLAUDE_CONFIG_DIR ansprechen (Config-Pfad-Falle)
Co-Authored-By: Claude Fable 5 <[email protected]>
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 SteckmeisterandClaude Fable 5 db4d001ef1 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 20:06:53 +02:00
Bernd SteckmeisterandClaude Fable 5 af7c538e18 docs: Fable-5-Review (u) – (t)-Fix verifiziert, server.py-Review-Kette konvergiert
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 19:44:55 +02:00
Bernd SteckmeisterandClaude Fable 5 4c6efa2a7a chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 19:22:29 +02:00
Bernd SteckmeisterandClaude Fable 5 d3e75c36a1 security: server.py Restbefund aus Review (t) – registration-status antwortet generisch statt str(e)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 19:21:19 +02:00
Bernd SteckmeisterandClaude Fable 5 326cc598c7 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 18:57:29 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 18:56:42 +02:00
Bernd SteckmeisterandClaude Fable 5 4d4044b888 chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 14:48:07 +02:00
Bernd SteckmeisterandClaude Fable 5 5ada0e5038 security: server.py Restbefunde aus Review (r) – GET-Leser-Lock, Content-Length-Haertung, secrets-Token
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 14:46:27 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 14:20:42 +02:00
Bernd SteckmeisterandClaude Fable 5 d42beaf7ab chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 13:52:07 +02:00
Bernd SteckmeisterandClaude Fable 5 343545b59e security: server.py multithreaded + Diag-Log-Lock (Review-Befund p, Threading-Haertung vollendet)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 13:51:32 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 09:17:56 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 08:43:42 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 04:32:38 +02:00
Bernd SteckmeisterandClaude Fable 5 88af6c016c chore: CHANGES.md (auto-Hook)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-04 04:07:39 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 04:06:53 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-04 04:04:15 +02:00
Bernd SteckmeisterandClaude Opus 4.8 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 <[email protected]>
2026-07-04 03:32:37 +02:00
Bernd SteckmeisterandClaude Fable 5 a5a5c4baf5 docs: Fable-5-Review um Punkte (h)-(j) ergänzt, Testplan auf 24 Tests
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 23:22:15 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-03 23:20:49 +02:00
Bernd SteckmeisterandClaude Fable 5 03b84e0e0e docs: PC-Testplan Schritt 0 auf aktuellen Teststand (22 Tests) gebracht
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 22:53:42 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-03 22:53:26 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-03 22:51:35 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-03 22:49:00 +02:00
Bernd SteckmeisterandClaude Fable 5 566938f004 docs: PC-Testplan um flutter-test-Vorabschritt ergänzt
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 22:23:42 +02:00
Bernd SteckmeisterandClaude Fable 5 1630d0e389 chore: Commit-Hash im PROGRESS-Eintrag nachgetragen + Hook-Log
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 22:23:12 +02:00
Bernd SteckmeisterandClaude Fable 5 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 <[email protected]>
2026-07-03 22:22:06 +02:00
Bernd SteckmeisterandClaude Fable 5 bfe58760ee feat: automatischer Konto-Wechsel (pyramid<->haupt) bei 5h-Limit
Co-Authored-By: Claude Fable 5 <[email protected]>
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: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 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
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
- 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:19–19:22 gehören zur (t)-Session, danach Commit 4c6efa2).
- Die 4 verbliebenen `str(e)`-Antworten liegen alle im `with ADMIN_LOCK:`-Block
HINTER dem Heimnetz-Gate (run-stats, create-invite, toggle-registration,
ban/unban) – absichtlich, LAN-only, für Bernd nützlich. Kein Handlungsbedarf.
- Kurz-Check des laufenden Dienstes: stats.html 200, registration-status 200
(`{"enabled": false}`), unbekannte Route 404.
**Damit ist die server.py-Härtungskette (o)–(u) KONVERGIERT** – ohne neuen
Anlass (neuer Code, neue Route) gibt es dort nichts mehr zu prüfen. ALLE als
erledigt markierten ROADMAP-/PROGRESS-Punkte sind reviewt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest, M2-Call/State) oder
Bernd (Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go). Auf dem Pi
ist nichts sicher Bearbeitbares mehr offen (Memory: nicht erneut suchen,
UNGETESTET-Stapel bewusst gedeckelt).
**Stolperfallen:** Keine – der Befund aus (t) war korrekt gefixt. Eine
Review-Kette braucht ein dokumentiertes Ende-Kriterium („konvergiert wenn der
letzte Diff nur noch den Vorbefund fixt und das Muster-grep leer ist"), sonst
reviewt jede Session den Review der Vorsession bis in alle Ewigkeit.
---
## 2026-07-04 – Fable-5-Review (t): (s)-Fixes geprüft – korrekt, 1 kleiner Restbefund gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die zwei
(s)-Restbefund-Fixes (Commit 13b333c) kritisch gegengelesen und als Punkt (t)
im Fable-5-Review-Abschnitt abgehakt. Der (s)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-s` verifiziert: ban/unban-Body-Lesen
sitzt vollständig VOR dem Lock inkl. Validierung, die generischen 500er auf
beiden öffentlichen POST-Routen stimmen; Dienst lief mit dem aktuellen Code,
kein unprotokollierter Rest im CHANGES-Hook-Log). Beim Ganzdatei-Gegenlesen
EIN Restbefund derselben Familie wie (s)-Befund 2:
1. **`GET /api/registration-status` leakte Exception-Texte:** Die Route ist
öffentlich erreichbar (in `do_GET` gibt es kein Heimnetz-Gate) und
antwortete bei Fehlern mit `str(e)` – ein Lesefehler an `conduit.toml`
hätte den internen Dateipfad an Unauthentifizierte verraten. (s) hatte nur
die zwei öffentlichen POST-Routen gefixt und die öffentliche GET-Route
übersehen. Fix: gleiches Muster – generische 500-Antwort („Interner
Fehler"), Detail per stderr ins Journal.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-t`). 8 Routen-Checks wie in (s) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, livekit-token ungültig 401, Diagnose falscher Token 403,
gespoofter Cf-Header 403, unbekannte Route 404). **500-Beweis (neu):**
`conduit.toml` für ~1 s per `chmod 000` unlesbar gemacht →
`{"error": "Interner Fehler"}` nach außen, `PermissionError` im Journal
(vorher wäre `[Errno 13] … '/home/steggi/matrix/conduit.toml'` – also der
Pfad – nach draußen gegangen); Rechte sofort wiederhergestellt (664), Route
danach wieder 200. Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** Bei einem Info-Leak-Fix nicht nur die Methode fixen, in
der der Befund auftauchte: (s) hatte `do_POST` gefixt, aber `do_GET` derselben
Datei hat auch öffentliche Routen mit demselben `str(e)`-Muster. Lehre: Nach
einem Musterfund (`str(e)` an Unauthentifizierte) einmal `grep str(e)` über
die GANZE Datei und jede Fundstelle als „öffentlich oder LAN-only?"
einordnen, statt nur die Nachbarroute zu fixen.
---
## 2026-07-04 – Fable-5-Review (s): (r)-Fixes geprüft – korrekt, 2 kleinere Restbefunde gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die drei
(r)-Restbefund-Fixes (Commit 5ada0e5) kritisch gegengelesen und als Punkt (s)
im Fable-5-Review-Abschnitt abgehakt. Der (r)-Diff selbst ist korrekt (gegen
Backup `server.py.bak-20260704-review-r` verifiziert, Dienst lief mit dem
aktuellen Code). Beim Gegenlesen der GANZEN Datei (Lehre aus (o)) zwei
kleinere Restbefunde in `server.py` gefunden und gefixt:
1. **ban/unban las den Body INNERHALB des `ADMIN_LOCK`:** Das 4-KB-Limit aus
(r) deckelt nur die Größe – ein LAN-Client, der `Content-Length` ankündigt
und nichts schickt, hielt den Lock trotzdem bis zum 30-s-Socket-Timeout
fest (der (r)-Kommentar behauptete fälschlich, der Deckel verhindere das).
Seit (r) wartet auch der öffentliche GET-Leser von `registration-status`
auf diesen Lock → der hing mit. Fix: Body-Lesen/Validieren VOR den Lock
gezogen (Handler-Logik identisch, nur verschoben).
2. **Öffentliche Routen leakten Exception-Texte:** `livekit-token` und
`e2ee-diagnostics` antworteten Unauthentifizierten bei Fehlern mit
`str(e)` – das kann interne Pfade (`livekit.yaml`-Pfad bei
FileNotFoundError) oder interne Adressen preisgeben. Fix: generische
500-Antwort („Interner Fehler"), Details per stderr ins Journal. Die
Admin-Routen behalten `str(e)` (LAN-only, für Bernd nützlich).
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-s`). 8 Routen-Checks wie in (r) alle
unverändert (stats 200, registration-status 200, ban ohne Nutzer 400,
8-KB-Body 413, `Content-Length: -5` 413, livekit-token ungültig 401,
Diagnose falscher Token 403, gespoofter Cf-Header 403). **Lock-Beweis (neu):
ban-Request mit angekündigtem, nie gesendetem Body + paralleler
`registration-status`-GET → Antwort in 15 ms statt Lock-Blockade** (vorher
hätte der GET bis zu 30 s gewartet). **500-Beweis (neu):** ungültiges JSON an
livekit-token → `{"error": "Interner Fehler"}`, Detail
(`JSONDecodeError(...)`) steht im Journal. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Ein Größen-Limit ist KEIN Zeit-Limit: `read(n)` mit
kleinem n blockiert trotzdem bis zum Socket-Timeout, wenn der Client nichts
schickt – blockierende I/O gehört grundsätzlich VOR einen Lock, nicht
hinein. (2) `str(e)` in Fehlerantworten öffentlicher Endpoints ist ein
Info-Leak (Pfade, interne Hosts) – Details gehören ins Journal, nach draußen
nur Generisches.
---
## 2026-07-04 – Fable-5-Review (r): ADMIN_LOCK-Umbau (q) geprüft – korrekt, aber 3 kleinere Restbefunde gefunden und gefixt
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – den ADMIN_LOCK-Umbau
aus (q) (Commit c8ff850) kritisch gegengelesen und als Punkt (r) im
Fable-5-Review-Abschnitt abgehakt. Der Umbau selbst ist korrekt (Locking-Struktur,
Routen-Reihenfolge, Handler-Logik unverändert). Drei kleinere Restbefunde in
`server.py` gefunden und gefixt:
1. **GET-Leser-Race (gleiche Familie wie (q), dort übersehen):** (q) hatte nur
POST-gegen-POST betrachtet – `GET /api/registration-status` liest
`conduit.toml` aber OHNE Lock, während `toggle-registration`/`create-invite`
die Datei per Truncate+Write in-place neu schreiben → seit multithreaded (p)
kann der Leser eine halb geschriebene/leere Datei sehen (Dashboard zeigt
transient falschen Registrierungs-Status). Fix: Lesen unter `ADMIN_LOCK`.
**Wichtige Erkenntnis dabei:** Der naheliegendere Fix (atomares `os.replace`
statt In-Place-Write) wäre FALSCH – `conduit.toml` ist als **einzelne Datei**
in den Container gemountet (`./conduit.toml:/etc/conduit.toml:ro`), ein
Replace tauscht den Inode und der Container sähe für immer die alte Datei.
Im Code als Kommentar festgehalten, damit das nie jemand „verbessert".
2. **Negative `Content-Length`:** `livekit-token`, `e2ee-diagnostics` und
`ban`/`unban` prüften nur `length > MAX` – ein negativer Wert wäre
durchgerutscht und `rfile.read(-n)` liest bis EOF (Thread hängt bis zum
30-s-Timeout). Fix: `length < 0` wird wie „zu groß" abgelehnt (413).
3. **`ban`/`unban` ohne Body-Limit + `random` für Einladungs-Token:**
ban/unban lasen beliebig große Bodies (und halten dabei seit (q) den
ADMIN_LOCK) → 4-KB-Deckel wie bei livekit-token. Einladungs-Token
(`create-invite`) kamen aus dem vorhersagbaren `random`-PRNG, obwohl sie
die Registrierung am Homeserver freischalten → `secrets.choice`.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
`py_compile` sauber, Diff gegen Backup minimal wie geplant
(`server.py.bak-20260704-review-r`). 8 Routen-Checks: stats.html 200,
registration-status 200 (liest jetzt unter Lock), ban ohne Nutzer localhost →
400 „Kein Nutzer angegeben", **ban mit 8-KB-Body → 413 (neu)**, **livekit-token
mit `Content-Length: -5` → sofort 413 statt Hänger (neu)**, livekit-token
ungültiger Matrix-Token → 401, e2ee-diagnostics falscher Token → 403,
Admin-Route öffentlich über die Dashboard-Domain → 403. Test-Deny-Zeile aus
`security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Bei Nebenläufigkeits-Reviews nicht nur Schreiber gegen
Schreiber prüfen, sondern auch die LESER derselben Ressource – `do_GET` war in
(q) komplett außen vor. (2) „Atomares Schreiben per `os.replace`" ist bei
Docker-**Datei**-Bind-Mounts eine Falle: der Mount hängt am Inode, nach einem
Replace sieht der Container die alte Datei weiter. In-Place-Write ist dort
absichtlich richtig. (3) `Content-Length` kann negativ sein – wer nur nach oben
deckelt, lässt `read()` bis EOF blockieren.
---
## 2026-07-04 – Fable-5-Review (q): Threading-Härtung (p) tiefer geprüft – echte Nebenläufigkeits-Race in den Admin-Routen gefunden und mit ADMIN_LOCK behoben
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit – die Threading-Umstellung
aus (p) (Commit 343545b, `ThreadingHTTPServer`) noch einmal kritisch gegengelesen und
als Punkt (q) im Fable-5-Review-Abschnitt abgehakt. Befund: Die Umstellung auf
multithreaded hat einen **echten Race erzeugt, den es vorher (single-threaded, alles
serialisiert) nicht geben konnte** – (p) hat nur den öffentlich beschreibbaren
Diagnose-Pfad per `DIAG_LOCK` abgesichert, die **Admin-Routen aber übersehen**:
- `toggle-registration` und `create-invite` machen ein Read-Modify-Write auf
`conduit.toml` **plus** `docker compose restart conduit` – zwei gleichzeitige Klicks
= verlorenes Update (lost update) + doppelter Container-Neustart.
- `send_admin()` schläft 4 s und liest danach die **letzten** Nachrichten aus dem
Admin-Raum zurück, um „die" Antwort zuzuordnen – zwei gleichzeitige Admin-Befehle
können sich vertauschte Antworten einfangen.
**Fix (`server.py`):** Neues `ADMIN_LOCK` (threading.Lock), das **nur** die
Admin-Mutationen serialisiert. Dafür `do_POST` minimal umgebaut: die beiden
öffentlichen Routen (`/api/livekit-token`, `/api/e2ee-diagnostics`) werden jetzt
**zuerst** behandelt und returnen, danach folgt das Heimnetz-Gate und der komplette
Admin-Dispatch in einem `with ADMIN_LOCK:`. Damit bleiben die öffentlichen Pfade
**nebenläufig** – der Slow-Loris-Schutz aus (p) ist unangetastet. Handler-Logik
wortwörtlich unverändert (nur verschoben/eingerückt, per `diff` gegen Backup
verifiziert). `python3 -m py_compile` sauber; Backup `server.py.bak-20260704-adminlock`.
**Verifiziert (headless, nach Dienst-Neustart per kill/`Restart=always`):**
8 Routen-Checks identisch wie vorher (stats 200; ban localhost → 400 „Kein Nutzer",
ban „aus dem Internet"/gespoofter Cf-Header → 403; livekit-token 401/400/413;
e2ee-diagnostics falscher Token 403; unbekannte Route 404). **Race-Beweis:** ein
`run-stats` allein 1388 ms, **zwei parallel 2633 ms** (≈ 2× → serialisiert; ohne
Lock wären es ~1400 ms). **Non-Blocking-Beweis:** ein öffentliches `livekit-token`
**während** ein Admin-`run-stats` den Lock hält → in **11 ms** beantwortet (nicht
blockiert). Test-Deny-Zeile aus `security_alerts.log` wieder entfernt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** Eine Threading-Umstellung ist nie „nur" ein Server-Konstruktor-
Tausch – sie macht **jeden** geteilten Zustand (Dateien, Read-Modify-Write,
Readback-nach-sleep) plötzlich race-fähig. (p) hatte richtig den einen öffentlich
beschreibbaren Pfad (`DIAG_LOCK`) gesehen, aber die Admin-Routen mit `conduit.toml`
und dem Admin-Raum-Readback sind derselbe Fall. Lehre: Nach „Server ist jetzt
multithreaded" ALLE schreibenden/zustandsändernden Handler durchgehen, nicht nur
den einen, der den Anlass gab.
---
## 2026-07-04 – Fable-5-Review (p): Interims-Gate geprüft (korrekt) + halbfertige Threading-Härtung einer abgebrochenen Session vollendet
**Erledigt:** Review-Pass über die jüngste erledigte Arbeit (Interims-Gate,
Commits e97a748/455742d) als Punkt (p) im Fable-5-Review-Abschnitt abgehakt.
Das Gate selbst ist korrekt (Socket-IP als belastbare Prüfung, Cf-Header nur
Zweitschranke, Deny-Log gedeckelt, kein Angreifer-Text im Log). Dabei über das
CHANGES.md-Hook-Log entdeckt: Eine Session hat um 09:43 (NACH dem letzten
Commit) **drei unprotokollierte server.py-Edits** gemacht und wurde mitten in
einer Threading-Härtung abgebrochen – `threading`-Import + `DIAG_LOCK` waren da,
aber der Lock wurde **nirgends benutzt**, und die eigentliche Umstellung auf
`ThreadingHTTPServer` fehlte (der Kommentar behauptete sie schon). Obendrein
lief der Dienst noch mit dem Code von 09:14 – nichts davon war je geladen.
Die angefangene Härtung ist fachlich richtig (der single-threaded `HTTPServer`
ließ EINEN langsamen Client den ganzen Server blockieren – inkl. des öffentlich
erreichbaren `/api/livekit-token`), darum vollendet statt zurückgebaut:
`ThreadingHTTPServer` in `__main__`, Diagnose-Log-Kappen+Anhängen in den
`DIAG_LOCK` gewickelt (einziger öffentlich beschreibbarer Pfad), `timeout = 30`
der Vorsession belassen. Dienst-Neustart (kill/`Restart=always`), headless
verifiziert (11 Prüfungen): stats 200; Admin-Route localhost UND 192.168.178.71
→ 400 „Kein Nutzer angegeben"; öffentlich UND gespoofter Cf-Header → 403;
livekit-token ungültig → 401; Diagnose falscher Token 403, gültig 200 + im Log,
300 KB → 413; **Slow-Loris-Test: parallele Anfrage in 10 ms beantwortet,
während ein Client hing** (vorher unmöglich), hängender Body-Read nach exakt
~30 s serverseitig beendet, Dienst danach gesund. Test-Einträge aus beiden
Logs wieder entfernt.
**Offen/Nächster Schritt:** Unverändert – alle offenen ROADMAP-Punkte brauchen
PC/Gerät (SQLCipher, LiveKit-Client-Umstellung, Härtetest) oder Bernd
(Secret-Rotation, Dashboard-Fernzugriff A/B, Call-Pilot-Go).
**Stolperfallen:** (1) Das CHANGES.md-Hook-Log ist der zuverlässigste Detektor
für unprotokollierte Arbeit abgebrochener Sessions – Zeitstempel der letzten
Einträge mit dem letzten Commit vergleichen. (2) „Datei geändert" heißt NICHT
„läuft": Dienststart-Zeit (`systemctl status`) gegen Datei-mtime prüfen – hier
lagen 30 Minuten Code unausgeführt auf der Platte. (3) Ein definierter, aber
nie benutzter Lock ist ein Halbfertig-Marker, kein Schutz.
---
## 2026-07-04 – Dashboard-Befund (o): akutes Loch mit aussperr-sicherem Interims-Gate geschlossen
**Erledigt:** Der KRITISCHE Befund (o) (Dashboard-Admin-Endpoints ohne Auth,
öffentlich erreichbar) ist entschärft, OHNE Bernds A/B-Entscheidung
vorwegzunehmen und OHNE ihn auszusperren. Erkenntnis, die das möglich machte:
Öffentlicher Verkehr erreicht `server.py` (Port 8080) ausschließlich über den
Cloudflare-Tunnel-Container (Absender-IP 172.x + `Cf-*`-Header) – Bernds
Heimnetz-Zugriffe kommen dagegen direkt mit `192.168.*`-Socket-IP an. Daher
„Weg C" als Interims-Gate in `server.py`: Die 5 Admin-Routen (`/api/ban`,
`/api/unban`, `/api/toggle-registration`, `/api/create-invite`, `/api/run-stats`)
akzeptieren nur noch Anfragen mit Heimnetz-/localhost-Socket-IP (nicht spoofbar)
und ohne Cloudflare-Header; sonst 403 mit deutscher Erklärung inkl.
Heimnetz-URL – das Dashboard zeigt `d.error` per `alert()`, Bernd sieht also
genau, was zu tun ist. Abgelehnte Versuche landen größenbegrenzt (5-MB-Deckel,
Lehre aus Befund (l)) in `security_alerts.log` (nur Fixpfad + Socket-IP, kein
attacker-kontrollierter Text → kein HTML-Injection-Risiko in der
Dashboard-Anzeige). Dienst-Neustart per kill/`Restart=always`, headless
verifiziert (9 Prüfungen): öffentlich ban/toggle → 403, localhost UND
192.168.178.71 → 400 „Kein Nutzer angegeben" (Gate passiert, App-Validierung
erreicht), gespoofter `Cf-Connecting-Ip` aus dem LAN → 403, `GET /` öffentlich
200, `/api/livekit-token` 401 und `/api/e2ee-diagnostics` 403 wie bisher
(bleiben bewusst öffentlich, die App braucht sie). Test-Einträge aus dem
Alert-Log wieder entfernt. Rückbau jederzeit: `server.py.bak-20260704-dashgate`.
Doku/ROADMAP nachgezogen (`docs/DASHBOARD_AUTH_HARDENING.md`: Status, Weg C,
Restrisiken).
**Zusätzlicher Befund (für Bernds Secret-Rotation wichtig):** Das Gitea-Repo
einfach privat zu schalten ist KEINE schnelle Lösung für den Secret-Leak (k):
Der In-App-Updater (`lib/core/update_checker.dart`) holt Releases **anonym**
über die öffentliche Gitea-API – Repo privat = Utas installierte App findet
keine Updates mehr, und ein Fix ließe sich nicht mehr per Update verteilen
(Henne-Ei). In ROADMAP M0 beim Rotations-Punkt als Warnung vermerkt.
**Offen/Nächster Schritt:** Bernds Entscheidung bleibt für den FERNZUGRIFF
relevant (Weg A empfohlen – schließt auch die weiterhin öffentliche
Nutzerlisten-/Präsenz-Ansicht; Restrisiken im Doc: LAN-CSRF, offene Ansicht).
Danach ggf. Gate ergänzen/ersetzen. Sonst unverändert: übrige M0-Punkte
brauchen PC/Gerät oder Bernd (SQLCipher, Rotation, LiveKit-Client-Umstellung,
Härtetest).
**Stolperfallen:** (1) `stats.html` wird alle 5 Minuten von `stats.sh` (Cron)
NEU GENERIERT – Änderungen an der HTML-Datei sind flüchtig, UI-Änderungen
müssten in `stats.sh` (deshalb bewusst NUR server-seitig gefixt). (2) Auf dem
Pi laufen ZWEI cloudflared-Instanzen: die lokal konfigurierte
(`cloudflared.yml`, nur Jellyfin) und eine remote-verwaltete
(`cloudflared-pingvin`, Ingress liegt im Cloudflare-Dashboard, lokal nicht
einsehbar) – Letztere trägt `dashboard.`/`git.steggi-matrix.work`. Wer nur die
lokale YAML liest, hält die Subdomains fälschlich für tot. (3) Der
Header-Check allein würde nicht reichen (Header sind von Direktverbindern
spoofbar/wegzulassen) – die Socket-IP ist die belastbare Wahrheit, der
Cf-Header-Check ist nur die zweite Schranke, falls der Tunnel je im
Host-Netz läuft.
---
## 2026-07-04 – Fable-5-Review (n)+(o): LiveKit-Route live+geprüft, KRITISCHER Auth-Befund am Dashboard
**Erledigt:** Review-Pass über das, was seit (l)/(m) am Server dazugekommen ist –
zwei neue Punkte (n)/(o) im Fable-5-Review-Abschnitt abgehakt (Details dort).
- **(n):** Beim Gegenlesen von `server.py` gefunden, dass die geplante
LiveKit-Token-Route `POST /api/livekit-token` von einer vorher abgebrochenen
(unprotokollierten) Session **bereits live gebaut** wurde. Kritisch geprüft und
headless verifiziert: Auth per whoami greift (ungültig→401), Body-Limit (413),
gültig→200; das JWT unabhängig dekodiert und die **Signatur gegen `livekit.yaml`
gegengerechnet – gültig**; Identität = geprüfte Matrix-user_id (kein Spoofing).
Route ist gefahrlos additiv. Server-Seite des M0-LiveKit-Punkts damit erledigt;
Client-Umstellung + Rotation bleiben PC/Gerät (heiliger Call-Pfad).
- **(o) KRITISCH:** Derselbe Dashboard-Server hat für seine **Admin-Endpoints
(`/api/ban`, `/api/unban`, `/api/toggle-registration`, `/api/create-invite`,
`/api/run-stats`) KEINE Authentifizierung** und ist über
`https://dashboard.steggi-matrix.work` **öffentlich erreichbar**. Belegt per
`curl` (App-eigene 400-Antwort über die öffentliche URL, kein Login davor).
Ein Fremder könnte damit Uta (jeden Nutzer) sperren oder die offene
Registrierung am Homeserver aktivieren. Der Hostname ist über CT-Logs auffindbar.
**Offen/Nächster Schritt:** **Bernd muss entscheiden, WIE das Dashboard künftig
authentifiziert** – Weg A (Cloudflare Access vor die Subdomain, kein Code, kein
Aussperr-Risiko, empfohlen) oder Weg B (eigener `DASH_TOKEN` + `stats.html`-Prompt).
Fertiger Plan mit beiden Wegen: `docs/DASHBOARD_AUTH_HARDENING.md`. Danach sofort
umsetzbar + headless verifizierbar. Nicht blind gefixt, weil jede Auth-Ergänzung
Bernds eigenes Admin-Panel bis zur Token-Eingabe lahmlegt („kein Aussperren") –
das mitten in einer autonomen Session zu deployen ist die falsche Reihenfolge.
**Stolperfallen:** Ein Review, der sich auf den *geänderten* Endpoint (Diagnose,
Disk-Fill) konzentriert, übersieht leicht die *unveränderten Nachbarn* in
derselben Datei. Lehre: bei einem Sicherheits-Review einer Datei nicht nur den
Diff prüfen, sondern die ganze Angriffsfläche der Datei (hier: alle Routen des
`do_POST`-Handlers auf Auth abklopfen, nicht nur die neu berührte).
---
## 2026-07-04 – Fable-5-Review (l)+(m): Diagnose-Endpoint hatte kein Größenlimit (gefixt)
**Erledigt:** Review-Pass über die zwei seit (k) dazugekommenen Arbeiten – als
Punkte (l) und (m) im Fable-5-Review-Abschnitt abgehakt (Details dort). Bei (l)
ein **echter Befund** am Server-Gegenpart des Admin-Token-Fixes: Der neue
`DIAG_TOKEN` steht bewusst im öffentlichen Repo (eng begrenzt, nur Log-Anhängen) –
aber `/api/e2ee-diagnostics` in `/home/steggi/matrix/server.py` las den
Request-Body **ohne Größenlimit** und hängte ihn unbegrenzt an die Log-Datei an.
Jeder Fremde hätte damit die Pi-Platte volllaufen lassen können (Disk-Fill-DoS
auf dem Host, der auch den Homeserver trägt). Gefixt in `server.py`:
Body-Limit 256 KB pro Upload (413 bei Überschreitung) + harte Obergrenze 20 MB
für die Log-Datei (ältere Hälfte fliegt raus). Headless verifiziert nach
Dienst-Neustart (kill, `Restart=always`): stats.html 200, gültiger Upload 200
und im Log gelandet, falscher Token 403, 300-KB-Body 413; Kappungslogik isoliert
getestet (100 Zeilen → 50, neueste überleben). Test-Einträge wieder entfernt.
**Offen/Nächster Schritt:** Weiter mit dem obersten offenen ROADMAP-Punkt
(M0 LiveKit-Token): Server-Route nach `docs/LIVEKIT_TOKEN_MIGRATION.md` bauen –
rein additiv, Client bleibt unangetastet (heiliger Call-Pfad wartet auf PC/Gerät).
**Stolperfallen:** Ein „eng begrenzter" Token im öffentlichen Repo ist nur dann
wirklich harmlos, wenn das Endpoint dahinter auch Missbrauch begrenzt
(Größe/Menge) – „keine Admin-Rechte" allein reicht nicht. Bei künftigen
Endpoints mit öffentlich bekanntem Token immer an Body-/Speicher-Limits denken.
---
## 2026-07-04 – LiveKit-Token-Minting: Migrationsplan geschrieben (Prep, kein Code)
**Erledigt:** Für den offenen M0-Punkt „LiveKit-Token server-seitig minten"
(letzter Client-eingebetteter Secret nach der Diagnose-Entkopplung) den fertigen
Umsetzungsplan `docs/LIVEKIT_TOKEN_MIGRATION.md` geschrieben – gegen den echten
Client-Code (`livekit_token.dart`, `livekit_call_manager.dart:154`) und die echte
Pi-Konfig (`livekit.yaml`, `server.py`, Continuwuity `whoami`). Kern: Token-Route
`POST /api/livekit-token` am bestehenden Dashboard-Server, die den Matrix-Access-Token
per `whoami` prüft und daraus die LiveKit-Identität ableitet (kein Spoofing), JWT
per stdlib-HMAC mintet (kein neues Paket); Client holt nur noch das fertige JWT und
`apiSecret` verschwindet aus dem App-Code. Analog zum bewährten
`SQLCIPHER_MIGRATION.md`-Muster.
**Offen/Nächster Schritt:** Umsetzung braucht echten Call-Test auf einem Gerät
(Calls „heilig", auf dem Pi kein GUI) – daher bewusst nur der Plan, kein Code.
Danach Secret-Rotation zusammen mit einem Release. Verbleibende M0-Secrets
(Admin-Token, Gitea-, Giphy-Token) brauchen Bernds Konten-Zugriff.
**Stolperfallen:** Keine – reine Doku.
---
## 2026-07-04 – Admin-Token aus dem Client entfernt (E2EE-Diagnose entkoppelt)
**Erledigt:** Den kritischsten Teil des Secret-Befunds (k) wirklich behoben, statt
ihn nur zu dokumentieren – und zwar sicher, weil diese Session direkt auf dem Pi
läuft, der den Dashboard-Server hostet (frühere Sessions kannten den Server-Code
nicht). Kernursache gefunden: `/home/steggi/matrix/server.py` benutzte für das
Diagnose-Endpoint `/api/e2ee-diagnostics` **denselben String** wie den
Matrix-Admin-Token (`DIAG_TOKEN = TOKEN`). Deshalb musste der Client diesen Token
mitliefern – d. h. jeder App-Nutzer (auch Uta) trug den vollen Server-Admin-Token
im Gerät.
- **Server (`server.py`):** `DIAG_TOKEN` ist jetzt ein eigener, zufälliger,
eng begrenzter Token (`pyr-diag-…`), unabhängig vom Admin-`TOKEN`. Das
Diagnose-Endpoint hängt nur an den Log an (keine Admin-Rechte) – der neue Token
ist also von geringer Sensibilität. Der Admin-`TOKEN` (für Ban/Invite/
Registrierung) bleibt **unverändert**, damit das Dashboard weiterläuft.
- **Client (`settings_encryption.dart`):** `_uploadDiagnostics` schickt jetzt den
neuen Diagnose-Token statt des Admin-Tokens. **`grep J5lax lib/` ist jetzt leer**
– der Admin-Token ist raus aus dem App-Code.
- **Verifiziert (headless, kein GUI nötig):** Dashboard-Server nach Neustart
(Prozess gekillt, systemd `Restart=always` lud den neuen Code): `stats.html` 200,
`registration-status` 200, `/api/e2ee-diagnostics` mit neuem Token **200**, mit
altem Admin-Token **403**. `flutter analyze` unverändert (1 bekannter Hinweis),
`flutter test` 24/24 grün. Meine Test-Einträge aus dem Diagnose-Log wieder
entfernt.
**Offen/Nächster Schritt:** Die **Rotation des Admin-Tokens `J5lax…` bleibt
Pflicht und Bernds Sache** – der Token ist weiterhin gültig und steht in der
Git-History sowie in Utas bereits installierter APK. Erst nach der Rotation ist
der Leak wirklich wertlos. Ebenso offen: LiveKit-`apiSecret` (server-seitiges
Minting), Gitea- und Giphy-Token (siehe ROADMAP M0). **Wichtig für den nächsten
PC-Lauf / das nächste Release:** Utas aktuell installierte App schickt beim
Diagnose-Upload noch den alten Token → dieser Button liefert bei ihr jetzt 403,
bis sie ein neues Release bekommt (nur die Diagnose-Funktion betroffen, nichts
Heiliges). Der Diagnose-Upload-Button gehört damit in den PC-Testplan (mit neuem
Build einmal drücken, Log auf dem Pi prüfen).
**Stolperfallen:** `systemctl restart matrix-stats.service` scheiterte an
fehlender polkit-Auth (kein Terminal für sudo). Lösung ohne root: den von systemd
verwalteten Prozess killen – `Restart=always`/`RestartSec=5` lädt server.py neu
(neue MainPID bestätigt). Nur anwendbar, weil der Dienst als User `steggi` läuft
und der eigene Prozess killbar ist.
---
## 2026-07-04 – Fable-5-Review (k): Secret-Scan – ECHTER Sicherheitsbefund, teils gefixt
**Erledigt:** Beim Review-Pass zusätzlich einen systematischen Secret-Scan über
`lib/`, `scripts/`, `docs/`, `android/` gemacht (CLAUDE.md-Leitplanke „Keine
Secrets ins Repo"). **Ergebnis: mehrere echte, aktive Zugangsdaten liegen im
Repo – und das Gitea-Repo ist als `private:false` unter der Internet-Domain
`git.steggi-matrix.work` erreichbar** (per `curl` bestätigt: Repo-Metadaten und
Releases anonym abrufbar). Damit sind alle folgenden Secrets als **kompromittiert
zu behandeln** (seit Commit 25ed765 in der History):
- **Matrix-Server-ADMIN-Token** `J5lax…` – lag doppelt drin:
(1) als Bearer-Token in `docs/matrix-sdk/13-server-admin-continuwuity.md`
(19 Stellen) und (2) **im ausgelieferten App-Code**
`lib/widgets/settings/settings_encryption.dart:431` (E2EE-Diagnose-Upload).
Per `whoami` gegen den Homeserver geprüft: **Token ist noch gültig**
(`@todesneutron:steggi-matrix.work`, voller Server-Admin). Kritischster Fund –
jeder App-Nutzer (auch Uta) trägt den Server-Admin-Token im Client.
- **Gitea-Release-Token** `bb522f96…` (Repo-Schreibrechte) hartcodiert in
`scripts/release.ps1:14`.
- **LiveKit-`apiSecret`** `rYUT2PRa…` in `lib/core/livekit_token.dart:6` – die
App mintet LiveKit-JWTs **client-seitig**, das SFU-Secret muss also im Client
liegen (Architekturproblem, nicht nur ein Repo-Leak).
- **Giphy-API-Key** `QtEy…` in `lib/features/chat/gif_sticker_picker.dart:9`
(geringe Sensibilität, aber ebenfalls im Client).
**Sofort (gefahrlos, kein Laufzeit-Bezug) bereinigt:**
- `scripts/release.ps1`: Token raus, kommt jetzt aus Umgebungsvariable
`PYRAMID_GITEA_TOKEN`; fehlt sie, bricht das Skript mit klarer Anleitung ab.
- `docs/matrix-sdk/13-server-admin-continuwuity.md`: alle 19 Token-Stellen durch
`<ADMIN_TOKEN>`-Platzhalter ersetzt.
**Offen/Nächster Schritt (BRAUCHT BERND / PC – nicht blind auf dem Pi fixen):**
1. **Alle vier Secrets rotieren** (sie sind bereits geleakt, egal was im Code
steht): neuen Matrix-Admin-Token erzeugen und alten widerrufen; neuen
Gitea-Token; LiveKit-`apiKey`/`apiSecret` neu setzen (Hetzner-LiveKit +
`ice.json`); Giphy-Key neu ziehen. **Das ist der wichtigste Schritt** – die
Repo-Bereinigung allein nützt nichts, solange die alten Werte gültig sind.
2. **Client-seitige Secrets entschärfen (Architektur):** LiveKit-Token gehört
server-seitig gemintet (kleiner Token-Endpoint auf dem Pi, App holt sich nur
das fertige JWT) – dann verschwindet `apiSecret` aus dem Client. Der
E2EE-Diagnose-Upload darf **niemals** den Server-Admin-Token benutzen: eigenen,
eng begrenzten Token (nur der Dashboard-Endpoint) oder besser einen
unauthentifizierten Upload mit Rate-Limit. Beides als ROADMAP-M0-Punkt neu
aufgenommen. Blind entfernen verbietet sich, weil Calls/Diagnose sonst brechen
(Calls sind „heilig") und auf dem Pi kein GUI-Test möglich ist.
3. **Git-History:** Die alten Werte bleiben in der Gitea-History sichtbar. Nach
der Rotation sind sie wertlos; ein History-Rewrite (force-push) ist optional
und Bernds Entscheidung (das Repo dient auch als Backup) – daher NICHT
eigenmächtig gemacht.
**Stolperfallen:** Der Diagnose-„Token" in `settings_encryption.dart` ist
NICHT ein harmloser Dashboard-Key, sondern derselbe String wie der
Matrix-Server-Admin-Token – beim Lesen leicht zu übersehen, weil er dort nur
`token:` heißt. Lehre: bei Secret-Funden immer prüfen, ob derselbe Wert an
mehreren Stellen (Doku UND Client) wiederkehrt und WAS er wirklich autorisiert.
---
## 2026-07-03 – Fable-5-Review (h)–(j): Test-/Doku-Commits geprüft, 1 echter Befund gefixt
**Erledigt:**
- **Review-Pass über die drei seit (g) dazugekommenen Commits** – als Punkte
(h)–(j) im Fable-5-Review-Abschnitt abgehakt (Details dort): AuthLog-Tests
e0ac5cb korrekt (h), PC-Testplan-Update 03b84e0 korrekt (j), und bei den
settings_prefs-Tests 7dfdbf1 (i) beim Gegenlesen des Produktivcodes ein
**echter Befund**: Lade-Race in `BoolPref`/`StringSetPref` –
`StringSetPref.add()` vor abgeschlossenem `_load` konnte **bereits
persistierte Einträge löschen** (Read-Modify-Write auf ungeladenem State;
per Regressionstest am alten Code bewiesen, dann gefixt: 7b90f20).
Vorgehen: Test zuerst geschrieben, Fehlschlag am alten Code gesehen, dann
Fix, dann alles grün – kein blinder Fix.
- Unabhängige Verifikation: `flutter test` jetzt **24/24 grün auf dem Pi**
(2 neue Lade-Race-Tests), `flutter analyze` unverändert 1 bekannter Hinweis
(`chat_provider.dart:42`). PC-Testplan Schritt 0 auf 24 Tests nachgezogen.
**Offen/Nächster Schritt:** Unverändert blockiert – alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität), siehe
`docs/PC_TESTPLAN.md` und „Fragen an Bernd" weiter unten. Der Lade-Race-Fix
ist headless voll getestet, gehört aber der Vollständigkeit halber mit in den
nächsten PC-Klickdurchlauf (Einstellungs-Toggles, Space-Beitritt).
**Stolperfallen:** Der BoolPref-Teil des Race ist im Test-Mock NICHT
reproduzierbar (Microtask-Reihenfolge dort zufällig gutartig: `set()`
persistiert vor dem `_load`-Read) – nur der StringSetPref-Teil schlug real
fehl. Lehre: Bei Race-Befunden nicht vom „Test wird schon rot"-Reflex leiten
lassen, sondern die konkrete Microtask-Reihenfolge durchdenken; ein Guard kann
richtig sein, auch wenn der Test ihn in dieser Umgebung nicht erzwingen kann
(dokumentiert im Review-Punkt (i)).
---
## 2026-07-03 – Review-Nachzügler abgeschlossen + AuthLog-Regressionstests
**Erledigt:**
- **Fable-5-Review vervollständigt (eebf8ae):** Die drei seit Review-Abschluss
dazugekommenen erledigten Arbeiten kritisch geprüft und als Punkte (e)–(g) im
Review-Abschnitt abgehakt: Unit-Tests 99cde9a (korrekt, Fake-Clients schlagen
bei jedem Fremd-Aufruf laut fehl), Doku-Commits 16561bb/566938f (alle
Behauptungen gegen Code/pubspec/matrix-6.2.0 verifiziert – Factory-Stellen,
Tabellenname `box_inbound_group_session`), Autopilot-Konto-Wechsel bfe5876
(kein App-Code, Logik in Ordnung; harmloser False-Positive der
Limit-Erkennung dokumentiert). **Kein Befund, keine Code-Änderung nötig.**
Damit sind ALLE als erledigt markierten Punkte reviewt.
- **`test/auth_log_test.dart` (4 Tests):** `AuthLog` ist die einzige weitere
öffentliche Schnittstelle auf dem heiligen Pfad, die headless testbar ist
(der Soft-Logout-Guard ruft `AuthLog.write` im catch-Pfad auf – würde es
werfen, eskalierte das SDK zu `logout()`+`clear()`). Getestet: write/read
werfen NIE, auch wenn die Plattform fehlt (kaputter PathProvider);
Platzhalter statt Fehler bei leerem Log; ISO-Zeitstempel pro Zeile;
500-Zeilen-Kappung (älteste Einträge fliegen raus, neueste überleben).
Dafür `path_provider_platform_interface ^2.1.2` als dev_dependency ergänzt –
offizieller Mock-Weg, war schon transitiv im Lockfile, kein neues externes
Paket. `flutter test`: alle 14 Tests grün, `flutter analyze`
unverändert (1 bekannter Hinweis `chat_provider.dart:42`).
- **`test/settings_prefs_test.dart` (8 Tests):** `core/settings_prefs.dart`
ist die Storage-Fassade, die laut `docs/M2_MODULE_SCHNITT.md` zur
Pflicht-Schnittstelle ausgebaut werden soll – dieses Vertragsnetz muss beim
M2-Umbau stehen bleiben. Getestet mit dem offiziellen In-Memory-Mock
(`SharedPreferences.setMockInitialValues`, kein neues Paket): `BoolPref`
(persistierter Wert überstimmt Default, set/toggle persistieren),
`StringSetPref` (Laden/add/remove, No-Ops bei Duplikat/Fehlendem),
Theme-Prefs (Defaults, Roundtrip, Teil-Save lässt Rest unangetastet),
Server-Banner (Roundtrip, Entfernen per null/leer, **korruptes JSON liefert
leere Map statt Crash** – echter Fehlerpfad). `flutter test`: **alle
22 Tests grün auf dem Pi.**
**Offen/Nächster Schritt:** Unverändert blockiert: Alle offenen ROADMAP-Punkte
(M0 SQLCipher, M1 Härtetest, M2 Call-Schicht/State-Management) brauchen PC/GUI
oder Bernds Antworten (Call-Pilot, SQLCipher-Priorität) – siehe
`docs/PC_TESTPLAN.md` und die Fragen an Bernd weiter unten. Weitere sichere
Pi-Testkandidaten sind jetzt wirklich erschöpft (Rest ist bibliotheksprivat
oder braucht Widget-Tests mit gemocktem Matrix-Client – lohnt erst nach dem
M2-Modulschnitt mit echten öffentlichen Schnittstellen).
**Stolperfallen:** `AuthLog` cached sein File-Handle statisch – der Test für
„Plattform kaputt" muss deshalb VOR den Tests mit funktionierender Plattform
laufen (im Testfile kommentiert). Bei künftigen Tests an Singletons mit
statischem Zustand auf Testreihenfolge/Isolate-Grenzen achten (flutter test
isoliert pro Datei, nicht pro Test).
---
## 2026-07-03 – Erste Unit-Tests: Regressionsnetz für die zwei heiligsten Codepfade
**Erledigt:** Eine Vorsession hatte (unprotokolliert, Abbruch vor dem Testlauf –
sichtbar nur am untrackten `test/`-Ordner + CHANGES.md-Hook-Log 18:16/18:17)
zwei Testdateien angelegt. Diese Session hat sie gegen den echten Code
gegengelesen, ausgeführt und committet (99cde9a) – damit hat das Projekt erstmals
überhaupt eine Testsuite, und zwar genau dort, wo CLAUDE.md „heilig" sagt:
- `test/soft_logout_guard_test.dart` (4 Tests): `guardedSoftLogoutRefresh`
wirft NIE nach außen (weder Exception noch Error) – ein Throw würde im SDK
zu `logout()`+`clear()` eskalieren (der Uta-Bug). Geprüft: Erfolg beim
1. Versuch = genau 1 Aufruf, transienter Fehler wird per Retry geheilt,
dauerhafter Fehler = genau 3 Versuche + normale Rückkehr. Der Fake-Client
schlägt bei JEDEM anderen SDK-Aufruf laut fehl – riefe der Guard je
`logout()`/`clear()` auf, fiele der Test sofort um.
- `test/power_levels_safety_test.dart` (6 Tests): `updatePowerLevelsSafely`
(Selbst-Aussperr-Schutz im Space-Admin) – Server-Stand bleibt erhalten
(fremde users-Einträge überleben; genau der Bug, der schon mal Admins
ausgesperrt hat), Selbst-Degradierung bricht ab ohne zu schreiben,
users_default-Fallback, explizites `events[m.room.power_levels]`-
Erfordernis, M_NOT_FOUND-Start mit leerem Event, fremde Serverfehler
(M_FORBIDDEN) werden durchgereicht.
- `flutter test`: **alle 10 Tests grün auf dem Pi** (läuft headless, im
Gegensatz zu `flutter run` – d. h. dieser Punkt ist NICHT „UNGETESTET (Pi)").
`flutter analyze` unverändert 1 bekannter Hinweis (`chat_provider.dart:42`).
- Wert fürs Refactoring: Beide Funktionen sind Vertragsgrenzen, die bei
M2-Umbauten (Call-Fassade, Storage) nicht wackeln dürfen – künftige
Sessions sollten `flutter test` fest in Schritt 3 des Arbeitszyklus
aufnehmen (geht auch auf dem Pi).
**Offen/Nächster Schritt:** Unverändert: PC-Termin (docs/PC_TESTPLAN.md) oder
Bernds Antworten (Call-Pilot, SQLCipher-Priorität). Weitere Test-Kandidaten
scheitern derzeit an der Sichtbarkeit: `_detectType`, `_formatMessageTime`,
`_compareSpaceChildren`, die Megolm-Export-Kryptohelfer usw. sind alle
bibliotheksprivat – testbar erst, wenn beim M2-Modulschnitt echte öffentliche
Schnittstellen entstehen (dann Tests gleich mitschreiben).
**Stolperfallen:** Die Vorsession wurde mitten im Schritt abgebrochen, OHNE
den Zwischenstand zu protokollieren (Pflicht laut CLAUDE.md) – der untrackte
`test/`-Ordner war nur per `git status` auffindbar. Lehre: `git status` am
Sessionstart ernst nehmen; untrackte Dateien können unprotokollierte,
halbfertige Arbeit einer abgebrochenen Session sein.
---
## 2026-07-03 – M1-Vorbereitung: SQLCipher-Migrationsplan + konsolidierter PC-Testplan
**Erledigt:** Nach Abschluss des Fable-5-Reviews (Abschnitt darunter) die zwei
@@ -130,6 +868,228 @@ schließt den Review ab.
eingefrorener Alt-Stand (2026-06-04) mit Verwechslungsgefahr zum echten
Protokoll im Root → Archiv-Banner eingefügt.
- [x] **(e) Unit-Tests (99cde9a) gegengelesen – korrekt, keine Änderung nötig.**
Beide Fake-Clients schlagen bei jedem nicht implementierten SDK-Aufruf laut
fehl (NoSuchMethodError) – ein versehentlicher `logout()`/`clear()`-Aufruf im
Guard würde sofort auffallen. `soft_logout_guard_test`: deckt Erfolg,
transienten Fehler (Retry), dauerhaften Fehler (genau 3 Versuche, kein Throw)
und `Error` (nicht nur `Exception`) ab – passt zum `catch (e)` im Guard, das
bewusst alles fängt. Dass die Tests grün sind, beweist nebenbei, dass
`AuthLog.write` ohne path_provider-Plattform (Testumgebung) sauber schluckt
statt zu werfen – genau die Eigenschaft, an der der Guard hängt.
`power_levels_safety_test`: alle 6 Fälle gegen den echten Code
(`space_admin_core.dart:10-56`) nachvollzogen, inkl. der wichtigen Kante
„explizites `events[m.room.power_levels]` schlägt `state_default`".
Einzige Anmerkung: die zwei Retry-Tests schlafen real 2s+4s (Suite ~14s) –
bewusst so gelassen, statt für Testbarkeit einen Delay-Parameter in den
heiligen Produktivpfad zu bohren.
- [x] **(f) Doku-Commits 16561bb/566938f (SQLCipher-Plan, PC-Testplan) gegen
Code verifiziert – Behauptungen stimmen:** Factory-Stellen exakt wie
dokumentiert (`matrix_client.dart:24` nativ Android/iOS, `:27` FFI Desktop;
`background_push.dart:188` nativ), `sqlcipher_flutter_libs ^0.6.8` +
`flutter_secure_storage ^10` in pubspec vorhanden/ungenutzt wie beschrieben,
Verifikations-Tabellenname `box_inbound_group_session` steht wirklich in
matrix-6.2.0 `matrix_sdk_database.dart:139`, beide Prozesse öffnen dieselbe
`pyramid.sqlite` mit WAL + busy_timeout 5000. PC-Testplan deckt alle in
PROGRESS verstreuten UNGETESTET-Punkte ab (stichprobenartig gegengeprüft).
- [x] **(g) Autopilot-Konto-Wechsel (bfe5876, dazu 73420fe) geprüft – kein
App-Code, funktional in Ordnung:** Wechsel-Logik (pyramid↔haupt, `JUST_SWITCHED`
verhindert Endlos-Ping-Pong, 20-min-Wartepause wenn beide Konten am Limit),
`CLAUDE_CONFIG_DIR` wird vor jeder Session neu gesetzt, Matrix-Token kommt aus
`~/gatus/.env` (nicht im Repo). Einzige Anmerkung (belassen): die
Limit-Erkennung greppt die GESAMTE Session-Ausgabe auf „usage limit" u. ä. –
zitiert eine Session diese englischen Phrasen wörtlich, gibt es einen
unnötigen (aber harmlosen) Konto-Wechsel.
- [x] **(h) AuthLog-Tests (e0ac5cb) geprüft – korrekt, keine Änderung nötig.**
Alle 4 Tests gegen `core/auth_log.dart` nachvollzogen: Der Kaputt-Plattform-
Test läuft korrekt VOR den Tests mit funktionierender Plattform (das statische
`_file`-Handle wird beim Throw nie befüllt, daher kein vergifteter Cache);
Kappungs-Test rechnet richtig (521. Zeile triggert Kappung auf 500, „Hallo
Log" aus dem Vortest fliegt raus). pubspec-Ergänzung sauber (offizielles
`path_provider_platform_interface`, war transitiv im Lockfile). Unabhängig
verifiziert: `flutter test` 22/22 grün, `flutter analyze` 1 bekannter
Hinweis. Anmerkung (belassen): Die Kappung schreibt die Datei nicht-atomar
neu und ist nicht gegen parallele Schreiber aus dem Push-Isolate gesperrt –
für ein reines Diagnose-Log akzeptabel, kein Fix nötig.
- [x] **(i) settings_prefs-Tests (7dfdbf1) geprüft – Tests korrekt, aber beim
Gegenlesen des Produktivcodes 1 echter Befund gefunden und gefixt (7b90f20):
Lade-Race in `BoolPref`/`StringSetPref`.** `StringSetPref.add/remove` machten
Read-Modify-Write auf möglicherweise noch ungeladenem State – ein `add()`
direkt nach Erstellung persistierte nur den neuen Eintrag und LÖSCHTE alle
bereits gespeicherten (per neuem Regressionstest am alten Code bewiesen:
`['!a:hs']` im Store, `add('!b:hs')` → Store enthielt nur noch `['!b:hs']`).
Fix: `add`/`remove` warten jetzt auf `_loaded`. `BoolPref` zusätzlich mit
`_userSet`-Guard, damit ein spät landender persistierter Wert nie eine schon
getätigte Nutzeraktion zurücküberschreibt (die Microtask-Reihenfolge ist
plattform-/versionsabhängig – im Test-Mock zufällig gutartig). Einordnung:
in der heutigen App kaum treffbar (die Provider werden beim App-Start geladen,
lange bevor jemand klickt – deshalb nie aufgefallen), aber die Fassade wird
laut `docs/M2_MODULE_SCHNITT.md` zur Pflicht-Schnittstelle für neue, ggf.
früh laufende Aufrufer. 2 neue Regressionstests, alle 24 grün auf dem Pi.
- [x] **(j) PC-Testplan-Update (03b84e0) geprüft – stimmt.** Teststand „22
Tests" war korrekt (durch (i) jetzt 24 – im Testplan nachgezogen, siehe
Eintrag oben).
- [x] **(k) Secret-Scan über das ganze Repo – ECHTER Sicherheitsbefund
(2026-07-04).** Vier aktive Zugangsdaten im öffentlich erreichbaren Repo
(Matrix-Admin-Token doppelt – auch im Client!, Gitea-Release-Token,
LiveKit-`apiSecret`, Giphy-Key). Gefahrlose Stellen sofort bereinigt
(release.ps1 → Env-Var, Admin-Doku → Platzhalter); Rotation aller Secrets +
server-seitiges LiveKit-Token-Minting + nicht-Admin-Diagnose-Token als neue
M0-Punkte aufgenommen (brauchen Bernd/Server-Zugriff). Details im
PROGRESS-Eintrag 2026-07-04 oben.
- [x] **(l) Admin-Token-Entkopplung (925aafb) geprüft – Client-Seite korrekt,
am Server-Gegenpart 1 echter Befund gefunden und gefixt (2026-07-04).**
Client-Diff verifiziert: neuer `pyr-diag-…`-Token stimmt mit `DIAG_TOKEN` in
`server.py` überein, `grep J5lax lib/ scripts/` unabhängig nachgeprüft (leer;
einziges verbliebenes Client-Secret ist das LiveKit-`apiSecret`, eigener
M0-Punkt). Befund: `/api/e2ee-diagnostics` hatte **kein Body-/Log-Größenlimit**
bei öffentlich bekanntem Token → Disk-Fill-DoS möglich. Gefixt + headless
verifiziert (200/403/413, Kappung), Details im PROGRESS-Eintrag 2026-07-04.
- [x] **(m) LiveKit-Migrationsplan (78e4174) gegen Code + Pi-Konfig geprüft –
Behauptungen stimmen.** `livekit_token.dart:6` enthält das `apiSecret`,
Aufrufstelle `livekit_call_manager.dart:154` mit `ttlSeconds: 21600`, Grants
im Plan identisch mit dem Dart-Code (roomJoin/room/canPublish/canSubscribe/
canPublishData, metadata = Anzeigename, iss = LKMatrixPi, sub = identity),
`livekit.yaml` `keys:` wie beschrieben, Continuwuity antwortet auf
`127.0.0.1:6167` (whoami-Basis). Python-Snippet (stdlib-HS256) fachlich
korrekt (base64url ohne Padding, kompaktes JSON, HMAC-SHA256). Anmerkung:
Der Plan setzt zusätzlich `nbf`/`name` – LiveKit akzeptiert beides, harmlos.
- [x] **(n) LiveKit-Token-Route ist inzwischen LIVE auf dem Pi gebaut (bisher
unprotokolliert) – geprüft und funktionsfähig, aber Client noch nicht
umgestellt (2026-07-04).** Beim Review von `server.py` gefunden: Eine
Vorsession hat die in `docs/LIVEKIT_TOKEN_MIGRATION.md` geplante Route
`POST /api/livekit-token` bereits in `server.py` implementiert (whoami-Auth,
stdlib-HS256-Minting aus `livekit.yaml`) – der CHANGES-Hook zeigt drei
server.py-Edits um 04:33, Dienst-Neustart 04:34, aber kein PROGRESS-Eintrag
(Session vor dem Protokoll abgebrochen). Headless verifiziert (127.0.0.1:8080
UND öffentlich): ungültiger Matrix-Token → 401, fehlende Felder → 400, 5-KB-Body
→ 413 (4-KB-Limit), gültiger Admin-Token → 200; das geminete JWT unabhängig
dekodiert: iss=LKMatrixPi, sub/name/metadata = geprüfte user_id (kein Spoofing,
Client kann Identität NICHT wählen), video-Grants wie im Client, exp−nbf =
21600 s, **Signatur unabhängig gegen `livekit.yaml` gegengerechnet: gültig**.
Die Route ist gefahrlos additiv (altes Client-Secret bleibt bis zur Rotation
gültig, nichts bricht). **Offen bleibt:** Client-Umstellung
(`livekit_token.dart`/`livekit_call_manager.dart`) + Secret-Rotation – beides
weiterhin PC/Gerät (heiliger Call-Pfad), siehe Migrationsplan. Server-Seite des
M0-LiveKit-Punkts ist damit erledigt.
- [x] **(o) KRITISCHER Befund beim server.py-Review: die Admin-Endpoints des
Dashboards haben KEINE Authentifizierung und sind öffentlich erreichbar
(2026-07-04).** Der Review von (l) hatte nur das Diagnose-Endpoint auf
Disk-Fill gehärtet – der viel größere Nachbar blieb unbemerkt:
`https://dashboard.steggi-matrix.work` ist über Cloudflare offen aus dem
Internet erreichbar, und `POST /api/ban`, `/api/unban`, `/api/toggle-registration`,
`/api/create-invite`, `/api/run-stats` prüfen **keinen** Token. Belegt:
`curl -X POST .../api/ban -d '{"user":""}'` liefert die App-eigene 400-Antwort
`{"error":"Kein Nutzer angegeben"}` – identisch zu 127.0.0.1, also KEIN
Cloudflare-Login davor. Damit könnte jeder Fremde Uta (jeden Nutzer) sperren
oder die offene Registrierung am Homeserver aktivieren. Der Hostname ist über
Certificate-Transparency-Logs praktisch auffindbar – „geheim" trägt nicht.
**Bewusst NICHT blind gefixt:** Jede Auth-Ergänzung macht Bernds
Browser-Dashboard funktionslos, bis er einen Token einträgt / Cloudflare Access
einrichtet – das mitten in einer autonomen Session zu deployen sperrt ihn ohne
Vorwarnung aus seinem eigenen Admin-Panel („kein Aussperren"). Fertiger,
gegen den echten Code geschriebener Plan mit zwei Wegen (A: Cloudflare Access =
Königsweg, kein Code; B: eigener `DASH_TOKEN` + `stats.html`-Prompt) liegt in
`docs/DASHBOARD_AUTH_HARDENING.md`. **Braucht Bernds Wahl A oder B, dann
sofortige Umsetzung + headless-Verifikation.** In ROADMAP M0 als dringender
Punkt aufgenommen. **Update (später am 2026-07-04):** Das akute Loch ist mit
einem aussperr-sicheren Interims-Gate geschlossen (Admin-Routen nur noch
Heimnetz/localhost, siehe PROGRESS-Eintrag „Dashboard-Befund (o)" oben) –
offen bleibt nur noch die Fernzugriffs-Entscheidung A/B.
- [x] **(p) Interims-Gate (e97a748/455742d) reviewt – korrekt; dabei halbfertige,
unprotokollierte Threading-Härtung einer abgebrochenen Session gefunden und
vollendet (2026-07-04).** Gate-Logik bestätigt: Socket-IP ist die belastbare
Prüfung (nicht spoofbar), Cf-Header nur Zweitschranke, Deny-Log mit 5-MB-Deckel
und ohne attacker-kontrollierten Text; Restrisiken (LAN-CSRF, offene
Nutzerlisten-Ansicht) waren bereits in `docs/DASHBOARD_AUTH_HARDENING.md`
dokumentiert – kein neuer Befund am Gate. ABER: CHANGES-Hook zeigte drei
server.py-Edits um 09:43 ohne Commit/Protokoll – abgebrochene Session hatte
`DIAG_LOCK` definiert, aber nie benutzt, und `ThreadingHTTPServer` im Kommentar
behauptet, aber nicht umgestellt; der Dienst lief zudem noch mit altem Code.
Vollendet (Threading + Lock-Nutzung), Dienst neu gestartet, 11 Headless-
Prüfungen grün inkl. Slow-Loris (parallele Anfrage 10 ms statt blockiert;
hängender Body-Read nach 30 s gekappt). Details im PROGRESS-Eintrag „(p)" oben.
- [x] **(q) Threading-Umstellung aus (p) tiefer geprüft – echte Nebenläufigkeits-Race
in den Admin-Routen gefunden und mit `ADMIN_LOCK` behoben (2026-07-04).** (p) hatte
den öffentlich beschreibbaren Diagnose-Pfad per `DIAG_LOCK` abgesichert, aber die
Admin-Routen übersehen: `toggle-registration`/`create-invite` machen ein
Read-Modify-Write auf `conduit.toml` + `docker restart`, und `send_admin()` liest
nach 4 s die letzten Admin-Raum-Nachrichten zurück – beides seit multithreaded
race-fähig (lost update, doppelter Neustart, vertauschte Antworten). Fix: neues
`ADMIN_LOCK`, das **nur** die Admin-Mutationen serialisiert; die öffentlichen Routen
(`livekit-token`, `e2ee-diagnostics`) laufen zuerst und bleiben nebenläufig
(Slow-Loris-Schutz aus (p) unangetastet). Handler-Logik wortwörtlich unverändert
(per `diff` gegen Backup verifiziert). Headless verifiziert: 8 Routen-Checks wie
vorher; Race-Beweis (2 parallele `run-stats` 2633 ms ≈ 2× einzeln 1388 ms →
serialisiert); Non-Blocking-Beweis (öffentliches `livekit-token` während gehaltenem
Lock in 11 ms). Details im PROGRESS-Eintrag „(q)" oben.
- [x] **(r) ADMIN_LOCK-Umbau aus (q) (c8ff850) geprüft – Umbau korrekt, 3 kleinere
Restbefunde gefixt (2026-07-04).** Locking-Struktur und Routen-Reihenfolge von (q)
bestätigt (öffentliche Routen vor dem Gate/Lock, Handler-Logik unverändert).
Befunde: (1) `GET /api/registration-status` las `conduit.toml` ohne Lock, während
die Admin-Routen sie per Truncate+Write neu schreiben – Leser konnte seit
multithreaded (p) eine halb geschriebene Datei sehen; Fix: Lesen unter `ADMIN_LOCK`
(bewusst KEIN `os.replace`: die Datei ist als Einzeldatei in den Container
gemountet, Inode-Falle – im Code dokumentiert). (2) Negative `Content-Length`
ließ `read()` bis EOF blockieren (Thread hing bis zum 30-s-Timeout) – bei
`livekit-token`/`e2ee-diagnostics`/`ban`/`unban` wird `< 0` jetzt mit 413
abgelehnt. (3) `ban`/`unban` ohne Body-Limit (hielten dabei den ADMIN_LOCK) →
4-KB-Deckel; Einladungs-Token von `random` auf `secrets` umgestellt (Token
schaltet Registrierung frei). Headless verifiziert (8 Checks inkl. 8-KB-Body →
413, `Content-Length: -5` → sofort 413 statt Hänger). Details im
PROGRESS-Eintrag „(r)" oben.
- [x] **(s) (r)-Fixes (5ada0e5) geprüft – korrekt, 2 kleinere Restbefunde gefixt
(2026-07-04).** (r)-Diff gegen Backup verifiziert (GET-Leser-Lock,
Content-Length-Härtung, secrets-Token – alles wie protokolliert), Dienst lief
mit aktuellem Code, kein unprotokollierter Rest im CHANGES-Hook-Log. Befunde
beim Ganzdatei-Gegenlesen: (1) ban/unban las den Body innerhalb des
`ADMIN_LOCK` – ein Client mit angekündigtem, nie gesendetem Body hielt den
Lock bis zum 30-s-Socket-Timeout (das 4-KB-Limit deckelt nur die Größe, nicht
die Wartezeit; seit (r) hing damit auch der `registration-status`-Leser mit);
Fix: Body-Lesen/Validieren vor den Lock gezogen, Handler-Logik unverändert.
(2) `livekit-token`/`e2ee-diagnostics` leakten bei Fehlern `str(e)` an
Unauthentifizierte (interne Pfade/Adressen); Fix: generisches
`{"error": "Interner Fehler"}`, Details ins Journal (stderr). Headless
verifiziert: 8 Routen-Checks unverändert, Lock-Beweis (hängender ban-Body,
paralleler registration-status-GET in 15 ms), 500-Beweis (ungültiges JSON →
generisch, Detail im Journal). Details im PROGRESS-Eintrag „(s)" oben.
- [x] **(t) (s)-Fixes (13b333c) geprüft – korrekt, 1 kleiner Restbefund gefixt
(2026-07-04).** (s)-Diff gegen Backup verifiziert (ban/unban-Body vor dem
Lock inkl. Validierung, generische 500er auf den öffentlichen POST-Routen –
alles wie protokolliert), Dienst lief mit aktuellem Code, kein
unprotokollierter Rest im CHANGES-Hook-Log. Befund beim
Ganzdatei-Gegenlesen: `GET /api/registration-status` (öffentlich, `do_GET`
hat kein Gate) antwortete bei Fehlern noch mit `str(e)` – hätte z. B. den
`conduit.toml`-Pfad geleakt; (s) hatte nur die POST-Routen gefixt. Fix:
generisches `{"error": "Interner Fehler"}`, Detail ins Journal. Headless
verifiziert: 8 Routen-Checks unverändert, 500-Beweis per kurzzeitigem
`chmod 000` auf `conduit.toml` (generisch nach außen, `PermissionError` im
Journal, danach wieder 200). Details im PROGRESS-Eintrag „(t)" oben.
- [x] **(u) (t)-Fix (d3e75c3) geprüft – korrekt, KEIN Befund; server.py-Kette
abgeschlossen (2026-07-04).** Diff gegen `server.py.bak-20260704-review-t`
= exakt der protokollierte Mini-Fix, Dienst läuft mit aktuellem Code, kein
unprotokollierter Rest. Verbliebene `str(e)`-Stellen alle LAN-gegated
(absichtlich). Ohne neuen Anlass keine weitere Review-Runde an `server.py`.
Details im PROGRESS-Eintrag „(u)" oben.
- [x] **(v) Entscheidungs-Doku 825481e (`ANTWORTEN_BERND.md`, am PC entstanden)
geprüft – konsistent, 1 Doku-Lücke gefunden und behoben (2026-07-05).** Alle
referenzierten Pläne/Dateien existieren, die vier Entscheidungen decken die
gesammelten „Fragen an Bernd" ab. Befund: Entscheidung 3 (Dashboard
Heimnetz-only, A/B geschlossen) war nicht in ROADMAP.md /
`docs/DASHBOARD_AUTH_HARDENING.md` nachgezogen – beide stellten die Frage
weiter als offen dar. In derselben Session behoben (ROADMAP-Punkt
geschlossen, Doku-Status aktualisiert, Vermerk zu Entscheidung 4 am
Secrets-Punkt). Details im PROGRESS-Eintrag „(v)+(w)" oben.
- [x] **(w) autopilot.sh-Fix 4477ed9 (CLAUDE_CONFIG_DIR unset für Hauptkonto,
am PC entstanden) geprüft – korrekt, kein Befund (2026-07-05).** Begründung
im Commit stimmt (CLI sucht Config unter `$CLAUDE_CONFIG_DIR/.claude.json`,
Hauptprofil liegt unter `~/.claude.json`); `unset` wirkt auch nach
pyramid-Iterationen derselben Shell. Am lebenden System verifiziert: die
Review-Session selbst lief als Konto „haupt" mit leerem `CLAUDE_CONFIG_DIR`
und korrekt geladenem Profil. Kein weiterer `CLAUDE_CONFIG_DIR`-Verweis im
Repo. Details im PROGRESS-Eintrag „(v)+(w)" oben.
**Fazit:** Die Sonnet-5-Arbeit war handwerklich sauber; die zwei echten Lücken
(verlorene Kommentare, fehlender Soft-Logout-Guard) hatte die abgebrochene
Fable-5-Vorsession bereits gefixt – beide Fixes heute unabhängig verifiziert.
+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
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
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
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
+46 -5
View File
@@ -9,9 +9,32 @@
cd "$(dirname "$0")"
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
# Pyramid laeuft auf eigenem Claude-Konto (Profil ~/.claude-pyramid),
# das Standard-Profil ~/.claude bleibt fuer die Webseiten-Arbeit.
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
# --- Zwei Claude-Konten mit automatischem Wechsel bei 5h-Limit ---
# Konto "pyramid" = Profil ~/.claude-pyramid (Standard fuer diese Arbeit),
# 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_URL="http://127.0.0.1:6167"
@@ -36,9 +59,11 @@ while true; do
sleep 600
continue
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)
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)
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
@@ -50,6 +75,22 @@ $COMMITS
$NEXT"
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
sleep 1200
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 (`[email protected]`) 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`.
## 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!)
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):
# Admin-Room-ID herausfinden:
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", ...}
```
@@ -84,22 +84,22 @@ curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3
# User zu Server-Admin machen
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" \
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
# Server-Status abfragen
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):
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}
# Prüfen ob User Server-Admin ist (Conduwuit-spezifisch):
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
# Schritt 1: Aktuelles Power-Level-Event lesen
ROOM_ID="!raumId:steggi-matrix.work"
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
TOKEN="<ADMIN_TOKEN>"
SERVER="https://steggi-matrix.work"
curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
@@ -172,7 +172,7 @@ await client.setRoomStateWithKey(
```bash
# Testen ob restricted join rules funktionieren:
ROOM_ID="!testRaum:steggi-matrix.work"
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
TOKEN="<ADMIN_TOKEN>"
SPACE_ID="!spaceId:steggi-matrix.work"
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \
@@ -230,7 +230,7 @@ try {
```bash
# 1. Prüfen: Welchen Power Level hat mein User in diesem Raum?
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/" \
-H "Authorization: Bearer ${TOKEN}"
@@ -269,7 +269,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/capabilities" \
```bash
# Prüfen ob User Server-Admin ist:
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)
# Alternativ: Über Admin-Room-Befehl
@@ -278,7 +278,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
# Über HTTP (Conduwuit Admin-Endpoint):
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" \
-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
# Raum beitreten (als Admin, auch wenn invite-only):
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" \
-d '{}'
# User in Raum einladen:
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" \
-d '{"user_id": "@user:steggi-matrix.work"}'
# 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/" \
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"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):
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" \
-d '{
"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:
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):
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" \
-d '{}'
```
@@ -419,15 +419,15 @@ Wenn du M_FORBIDDEN bekommst:
```bash
# 1. Token gültig?
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:
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?
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!)
# 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> {
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) {
_load(defaultValue);
@@ -14,10 +17,11 @@ class BoolPref extends StateNotifier<bool> {
Future<void> _load(bool def) async {
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 {
_userSet = true;
state = v;
final p = await SharedPreferences.getInstance();
await p.setBool(_key, v);
@@ -30,9 +34,10 @@ class BoolPref extends StateNotifier<bool> {
class StringSetPref extends StateNotifier<Set<String>> {
final String _key;
late final Future<void> _loaded;
StringSetPref(this._key) : super({}) {
_load();
_loaded = _load();
}
Future<void> _load() async {
@@ -42,14 +47,18 @@ class StringSetPref extends StateNotifier<Set<String>> {
}
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};
final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList());
}
Future<void> remove(String v) async {
if (!state.contains(v)) return;
await _loaded;
if (!mounted || !state.contains(v)) return;
state = {...state}..remove(v);
final p = await SharedPreferences.getInstance();
await p.setStringList(_key, state.toList());
@@ -428,7 +428,11 @@ class _EncryptionSectionState extends ConsumerState<_EncryptionSection> {
final notifier = ref.read(e2eeDiagnosticsProvider.notifier);
final msg = await notifier.upload(
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; });
} catch (e) {
+7 -7
View File
@@ -801,10 +801,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
mime:
dependency: "direct main"
description:
@@ -918,7 +918,7 @@ packages:
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
dependency: "direct dev"
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
@@ -1294,18 +1294,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.11"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev"
source: hosted
version: "0.6.16"
version: "0.6.17"
timezone:
dependency: transitive
description:
+4
View File
@@ -82,6 +82,10 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^6.0.0
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:
webcrypto:
+11 -1
View File
@@ -11,7 +11,17 @@ Set-Location (Split-Path $PSScriptRoot -Parent)
# ── Config ────────────────────────────────────────────────────────────────────
$GiteaBase = "http://192.168.178.71:3000"
$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" }
# ── 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)));
}