Files
talktime/features/PROJ-14-synapse-raum-uebersicht.md
patrickandClaude Sonnet 5 3bc2ac8ef4 fix(PROJ-14): BUG-11 (reconciliation for slow purges) and BUG-12 (202 UX)
BUG-11 (Medium) — a room purge still running after the DELETE route's
~10s poll window was simply forgotten: no persisted state, no
follow-up, so Synapse finishing the purge moments later left the local
chat_rooms mapping (and audit trail) permanently stale. New table
pending_room_deletions (migration 0005, deliberately no FKs — must
outlive the org/team/chat_rooms row it references) plus a background
worker (processPendingRoomDeletions(), 30s tick) that keeps polling
delete_status and runs the same finalize step the route itself would
have: drop the mapping, write the audit entry. Capped at 30 attempts
(~15min) before giving up with a server-log error.

Verified end-to-end on talkhub/matrix: manually queued a pending
deletion row, confirmed it was cleared and audited
(finalizedByWorker: true) after one worker cycle. Also reconfirmed the
normal fast path (small room, completes within the poll window) still
returns 200 in ~1.2s.

BUG-12 (Low) — the 202/pending response used to be shoved into the same
`deleteError` state as real failures, rendering as a destructive alert
with the dialog stuck open and the delete button still enabled (inviting
a duplicate purge on a second click). Now a separate, neutral
`pendingInfo` banner; the dialog closes immediately since there's
nothing more to confirm.

Not addressed here: BUG-7/8/9/10 (never picked up since the first QA
round — flagging as a process gap, not just a code one), BUG-13 (no
system-wide audit log view exists yet to surface organization_id=null
entries — backlog item, out of this feature's scope), BUG-14 (no
client-side timeout on the DELETE request).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
2026-09-13 10:20:08 +02:00

33 KiB
Raw Permalink Blame History

PROJ-14: Synapse Raum-Übersicht (System-Admin)

Status: Deployed Created: 2026-09-12

Implementation Notes (Frontend, 2026-09-12)

Frontend umgesetzt, Feature damit komplett (Backend siehe unten).

  • frontend/src/hooks/use-synapse-rooms.ts: lädt GET /api/system-admin/rooms (from/limit/search), ROOMS_PAGE_SIZE = 25. Bei Fehler wird die Liste bewusst geleert und ein Fehlertext gesetzt — nie „stille" leere Tabelle bei nicht erreichbarer Synapse.
  • frontend/src/pages/SystemAdminRoomsPage.tsx: Tabelle (Raum + Alias/Room-ID, Mitglieder, Zuordnung, Erstellt am, Löschen), Such-Formular (Submit statt Debounce), Zurück/Weiter-Paginierung analog PROJ-3, AlertDialog mit Zusatzwarnung wenn assignment != null. Fehler bleiben im Dialog sichtbar (event.preventDefault() im AlertDialogAction), statt ihn bei Fehlschlag zu schließen.
  • frontend/src/routes/SystemAdminRoute.tsx (neu): Guard-Outlet auf user.isSystemAdmin, rein kosmetisch — Server prüft weiterhin selbst.
  • frontend/src/App.tsx: /system-admin → Redirect auf /system-admin/rooms; der PROJ-13-Dashboard-Button zeigt damit nicht mehr ins Leere.
  • Abweichung: keine useToast/Sonner-Meldungen — der Rest der App nutzt Inline-Fehler (Alert), das wurde beibehalten.
  • Loading/Empty/Error sowie „keine Treffer bei aktiver Suche" sind als eigene Tabellen-Zustände abgedeckt; Tabelle ist mobil horizontal scrollbar.

Implementation Notes (Backend, 2026-09-12)

Backend umgesetzt (Frontend inzwischen nachgezogen, siehe oben).

  • backend/src/services/synapse.ts: listRooms() (GET /_synapse/admin/v1/rooms, from/limit/search_term durchgereicht), deleteRoom() (v2 async Delete mit purge: true — v1 blockiert bei großen Räumen bis zum HTTP-Timeout), normalizeCreationTs() (Synapse liefert je nach Version Sekunden statt Millisekunden).
  • backend/src/routes/system-admin.ts (neu, in server.ts registriert): GET /api/system-admin/rooms + DELETE /api/system-admin/rooms/:roomId, beide preHandler: [app.authenticate, requireSystemAdmin].
  • Keine neue Tabelle, keine Migration: chat_rooms.matrix_room_id ist bereits unique (Index vorhanden), Zuordnung via einem inArray-JOIN gegen organizations/teams (kein N+1).
  • Abweichung/Einschränkung: audit_logs.organization_id ist NOT NULL — das Löschen eines nicht zugeordneten Raums kann daher nicht ins Audit-Log, nur ins Server-Log (app.log.warn). Zugeordnete Räume werden regulär als system_admin.room.deleted auditiert.
  • Edge Cases: Synapse nicht erreichbar → 502 mit Fehlertext (nie leere Liste); Raum bereits gelöscht (404 von Synapse) → lokale Zuordnung wird trotzdem aufgeräumt, dann 404.

Summary

Liste aller Matrix-Räume auf der angebundenen Synapse-Instanz für SYSTEM_ADMIN, unabhängig von TalkTime-Team-/Vereinszuordnung — nachgebaut aus synapse-admin's Raum-Übersicht. Nutzt Synapse Admin API (GET /_synapse/admin/v1/rooms), keine neue TalkTime-Datenbanktabelle nötig.

Dependencies

  • Requires: PROJ-13 (SYSTEM_ADMIN-Rolle) — Zugriffsschutz
  • Requires: PROJ-9 (Synapse-Integration) — nutzt bestehenden services/synapse.ts-Wrapper

User Stories

  1. Als SYSTEM_ADMIN will ich alle Matrix-Räume sehen (Name, Mitgliederzahl, Erstellungsdatum), um einen Überblick über die Gesamtnutzung zu haben.
  2. Als SYSTEM_ADMIN will ich erkennen, welcher Raum zu welchem TalkTime-Team/Verein gehört (Abgleich mit chat_rooms-Tabelle), um Zuordnung nachvollziehen zu können.
  3. Als SYSTEM_ADMIN will ich einen verwaisten Raum (keine TalkTime-Zuordnung mehr, z.B. nach Datenbereinigung) direkt löschen/purgen können.

Acceptance Criteria

  • GET /api/system-admin/rooms — SYSTEM_ADMIN-only, ruft _synapse/admin/v1/rooms ab, reichert um TalkTime-Zuordnung an (JOIN gegen lokale chat_rooms-Tabelle über matrix_room_id)
  • Frontend-Tabelle: Raumname, Mitgliederzahl, TalkTime-Zuordnung (Vereinsname + Team, oder "nicht zugeordnet"), Erstellungsdatum
  • DELETE /api/system-admin/rooms/:roomId — purged den Raum bei Synapse; falls in chat_rooms verzeichnet, wird der lokale Eintrag ebenfalls gelöscht (kaskadiert nicht automatisch, da externe ID)
  • Bestätigungsdialog vor dem Löschen (destruktive Aktion, analog Mitglied-Löschen aus PROJ-3)
  • Pagination (Synapse Admin API liefert selbst paginiert, from/limit-Parameter durchreichen)

Edge Cases

  • Raum ist ein aktiver Team-Raum (in chat_rooms verzeichnet, Team existiert noch) → Löschen weiterhin technisch möglich, aber Warnhinweis im Dialog ("Dieser Raum ist aktiv einem Team zugeordnet — Team-Chat wird dadurch zerstört")
  • Synapse nicht erreichbar → Fehleranzeige, keine leere Liste vortäuschen
  • Sehr viele Räume (>1000) → Pagination zwingend, nicht alles auf einmal laden
  • Raum bereits gelöscht (Race Condition, zwei Admins gleichzeitig) → Synapse liefert Fehler, Frontend zeigt generische Fehlermeldung statt Absturz

Non-Goals

  • Raum-Inhalte einsehen (Nachrichten lesen) — reine Metadaten-Übersicht, kein Nachrichten-Zugriff für Admin
  • Raum-Einstellungen ändern (Name, History-Visibility) über dieses Panel — nur Übersicht + Löschen

Tech Design (Solution Architect)

A) Komponenten-Struktur

System-Admin-Bereich (NEU, "/system-admin")
└── Seite "Räume"
    ├── Tabelle: Raumname | Mitglieder | Zuordnung (Verein/Team oder "nicht zugeordnet") | Erstellt am | Aktion
    ├── "Weiter"/"Zurück"-Paginierung (analog Mitgliederliste aus PROJ-3)
    └── Löschen-Bestätigungsdialog (analog Mitglied-Löschen aus PROJ-3)

Erste Unterseite im neuen System-Admin-Bereich — die Navigation dorthin (aktuell ein Button, der ins Leere zeigt, siehe PROJ-13) bekommt jetzt ein echtes Ziel.

B) Datenfluss (in einfachen Worten)

Es wird keine neue Tabelle in der TalkTime-Datenbank angelegt. Der Ablauf:

  1. Backend fragt Synapse nach der Liste aller Räume (Name, Mitgliederzahl, Erstellungsdatum) — das ist reine Server-Information, die Synapse selbst schon führt.
  2. Backend gleicht diese Liste gegen die bereits vorhandene TalkTime-Tabelle ab, in der steht, welcher Matrix-Raum zu welchem Team/Verein gehört (dieselbe Tabelle, die auch der Chat aus PROJ-11 nutzt).
  3. Ergebnis: eine kombinierte Liste, die sowohl "reine Synapse-Info" als auch "TalkTime-Zuordnung, falls vorhanden" zeigt.
  4. Löschen eines Raums geht direkt an Synapse; falls der Raum in der TalkTime-Zuordnung stand, wird dieser Eintrag ebenfalls entfernt, damit keine verwaiste Zuordnung auf einen nicht mehr existierenden Raum zurückbleibt.

C) Tech-Entscheidungen (Begründung)

  • Keine eigene Datenspeicherung der Raumliste: Synapse ist bereits die "Quelle der Wahrheit" für Räume — eine Kopie in TalkTime pflegen hieße, zwei Datenstände synchron halten zu müssen, ohne echten Nutzen.
  • Zugriffsschutz über die in PROJ-13 gebaute Prüfung: Kein neuer Berechtigungsmechanismus, konsistent mit allen anderen System-Admin-Funktionen.
  • Löschen bricht Chat für aktive Teams bewusst nicht automatisch ab: Ein Löschen-Button, der aktive Team-Chats stillschweigend verschont, wäre für einen Admin verwirrend ("warum lässt sich das nicht löschen?"). Stattdessen: löschen ist möglich, aber mit deutlicher Warnung vorher.

D) Abhängigkeiten (neue Pakete)

Keine — nutzt die bestehende Synapse-Anbindung aus PROJ-9/PROJ-11.

Offene Voraussetzung

Erfordert PROJ-13 (Berechtigungsprüfung) als Grundlage — bereits umgesetzt.


QA Test Results (2026-09-12)

Testumfang: Statische Verifikation gegen Code (Backend-Route, Synapse-Service, Hook, Page, Route-Guard), tsc --noEmit Backend + Frontend, npm run lint. Einschränkung: Keine laufende Synapse-Instanz und kein Browser in der QA-Umgebung — Cross-Browser- und Pixel-Responsive-Tests konnten nicht ausgeführt werden (siehe „Nicht getestet"). Befunde unten sind aus dem Code abgeleitet und reproduzierbar beschrieben.

Build / Lint

Check Ergebnis
backend: tsc --noEmit PASS (0 Fehler)
frontend: tsc --noEmit PASS (0 Fehler)
frontend: npm run lint PASS (0 Errors, 8 Warnings — alle vorbestehend, keine in PROJ-14-Dateien)

Acceptance Criteria

# Kriterium Status Anmerkung
1 GET /api/system-admin/rooms, SYSTEM_ADMIN-only, mit TalkTime-Zuordnung PASS preHandler: [app.authenticate, requireSystemAdmin]; ein inArray-JOIN, kein N+1.
2 Frontend-Tabelle: Name, Mitglieder, Zuordnung, Erstellungsdatum PASS Plus Alias/Room-ID als Sekundärzeile.
3 DELETE /api/system-admin/rooms/:roomId purged Raum + löscht lokalen Eintrag FAIL Siehe BUG-1 und BUG-2: v2-Delete ist asynchron, der Raum ist bei Rückkehr der Antwort noch nicht gelöscht, der lokale Eintrag aber bereits weg.
4 Bestätigungsdialog vor dem Löschen PASS AlertDialog, Fehler bleiben im Dialog sichtbar, disabled während des Requests.
5 Pagination (from/limit durchreichen) PARTIAL Durchgereicht und serverseitig begrenzt (max 100), aber Frontend ignoriert die gelieferten nextFrom/prevFrom — siehe BUG-6.

Ergebnis: 3 PASS, 1 PARTIAL, 1 FAIL von 5.

Edge Cases

Edge Case (aus Spec) Status Anmerkung
Aktiver Team-Raum → Warnhinweis im Dialog PASS Zusatz-Alert bei assignment != null mit Verein/Team im Text.
Synapse nicht erreichbar → Fehler, keine leere Liste PASS Backend 502 mit generischem Text; Hook setzt error und leert die Liste, Tabelle zeigt „Keine Daten verfügbar" statt „Keine Räume vorhanden".
>1000 Räume → Pagination zwingend PASS Nichts wird gepuffert, limit hart auf 100 gedeckelt.
Raum bereits gelöscht (Race) FAIL Siehe BUG-3 — der 404-Zweig ist nicht erreichbar.

Gefundene Bugs

BUG-1 (High) — Lokale Zuordnung wird gelöscht, bevor der Purge erfolgreich war backend/src/routes/system-admin.ts:153-183 in Verbindung mit deleteRoom() (v2, asynchron). Synapse antwortet sofort mit { delete_id } und führt Shutdown/Purge im Hintergrund aus. Der Code wertet diese Annahme als Erfolg und löscht daraufhin unwiderruflich die chat_rooms-Zeile. Schlägt der Hintergrund-Job fehl, existiert der Raum bei Synapse weiter, TalkTime hat die Team↔Raum-Zuordnung aber vergessen — der Team-Chat ist dauerhaft kaputt und nicht wiederherstellbar, ohne dass jemand einen Fehler zu sehen bekommt. GET /_synapse/admin/v2/rooms/delete_status/<delete_id> wird nirgends abgefragt. Repro: Raum mit Team-Zuordnung löschen, während der Purge serverseitig fehlschlägt (z.B. DB-Fehler in Synapse) → API antwortet { deleted: true }, chat_rooms-Eintrag weg, Raum bei Synapse vorhanden. Priorität: 1

BUG-2 (High) — Gelöschter Raum erscheint direkt nach dem Löschen weiter in der Tabelle frontend/src/pages/SystemAdminRoomsPage.tsx:71 ruft await refresh() unmittelbar nach der DELETE-Antwort auf. Da der Purge asynchron läuft, liefert GET /_synapse/admin/v1/rooms den Raum zu diesem Zeitpunkt noch. Der Admin sieht keinerlei Wirkung seiner Aktion und wird den Löschen-Button plausibel mehrfach betätigen (jeder Klick startet einen weiteren Purge-Job). Repro: Beliebigen Raum löschen → Dialog schließt, Tabelle lädt neu, Zeile ist unverändert vorhanden. Priorität: 2

BUG-3 (Medium) — 404-Behandlung für die Race Condition ist toter Code backend/src/routes/system-admin.ts:155-162 erwartet von Synapse ein 404, wenn der Raum nicht mehr existiert. Der v2-Delete-Endpoint liefert aber auch für unbekannte Raum-IDs 200 mit delete_id (nur eine syntaktisch ungültige ID ergibt 400). Der dokumentierte Edge Case „zwei Admins gleichzeitig" wird damit faktisch nicht abgedeckt; der zweite Admin bekommt „erfolgreich gelöscht" statt der beschriebenen Meldung. Priorität: 3

BUG-4 (Medium, Security/Compliance) — Löschen eines nicht zugeordneten Raums ist im Audit-Log unsichtbar backend/src/routes/system-admin.ts:184-189. Für Räume ohne chat_rooms-Eintrag gibt es nur ein app.log.warn, keinen audit_logs-Datensatz — begründet mit audit_logs.organization_id NOT NULL. Damit ist ausgerechnet der Hauptanwendungsfall des Features (verwaiste Räume purgen, User Story 3) die einzige destruktive Plattform-Aktion ohne revisionssicheren Nachweis. Ein SYSTEM_ADMIN kann beliebig viele nicht zugeordnete Räume samt Inhalten vernichten, ohne dass es im Audit-Log auftaucht. Empfehlung: organization_id nullable machen (Migration) oder eine separate system_audit_logs-Tabelle. Priorität: 4

BUG-5 (Medium) — Keine Transaktion und kein dedizierter Rate Limit auf der destruktiven Route (a) db.delete(chatRooms) und logAudit() laufen unverbunden; schlägt der Audit-Insert fehl, ist die Zuordnung bereits gelöscht und der Request endet mit 500 — Zustand inkonsistent. (b) DELETE /api/system-admin/rooms/:roomId läuft nur unter dem globalen Limit (100/min, server.ts:34). Ein kompromittierter SYSTEM_ADMIN-Token kann so 100 Raum-Purges pro Minute auslösen. Analog zu /api/auth/** wäre ein enges Route-Limit angebracht. Priorität: 5

BUG-6 (Medium) — Paginierung nutzt total statt der von Synapse gelieferten Cursor Backend liefert pagination.nextFrom/prevFrom (system-admin.ts:119-120), das Frontend verwendet sie nicht und rechnet stattdessen hasNextPage = rooms.length === ROOMS_PAGE_SIZE && from + ROOMS_PAGE_SIZE < total (SystemAdminRoomsPage.tsx:80). Meldet Synapse bei aktiver Suche total_rooms als Gesamtzahl aller Räume statt der Treffer (versionsabhängig), bleibt „Weiter" aktiv und führt auf eine leere Seite. Ebenso ist die Anzeige „{total} Räume gesamt" bei aktiver Suche dann irreführend. Priorität: 6

BUG-7 (Low) — Leere Seite nach dem Löschen des letzten Eintrags einer Folgeseite from wird beim Löschen nicht angepasst. War der gelöschte Raum der einzige auf Seite 2+, zeigt die Tabelle „Keine Räume vorhanden", obwohl Räume existieren. (Tritt erst auf, sobald BUG-2 behoben ist.) Priorität: 7

BUG-8 (Low) — Raum-ID-Regex ist laxer als der eigene Kommentar behauptet system-admin.ts:19 erlaubt / und + im Localpart; echte Matrix-Raum-IDs enthalten beides nie. Nicht ausnutzbar (synapse.ts kodiert den Pfadteil mit encodeURIComponent, Drizzle parametrisiert), aber der Validator filtert weniger, als der Kommentar „keeps them out of the URL we build from them" suggeriert. Priorität: 8

BUG-9 (Low) — Purge ohne block: true synapse.ts:263 sendet block: false. Eine gelöschte Raum-ID kann danach von jedem Nutzer wieder betreten/neu angelegt werden. Für das Aufräumen verwaister Räume vermutlich gewollt, sollte aber als bewusste Entscheidung dokumentiert werden. Priorität: 9

BUG-10 (Low, kosmetisch) — Doppelte Fehlermeldung im Fehlerfall Bei error zeigt die Seite gleichzeitig das rote Alert und die Tabellenzeile „Keine Daten verfügbar". Priorität: 10

Security Audit (Red Team)

Angriffsvektor Ergebnis
Auth-Bypass (ohne Token) PASS — app.authenticate (jwtVerify) auf beiden Routen.
Privilege Escalation (normaler User ruft /api/system-admin/**) PASS — requireSystemAdmin liest users.isSystemAdmin bei jedem Request frisch aus der DB, nicht aus dem JWT-Claim. Entzug wirkt sofort, kein Warten auf Token-Ablauf.
Frontend-Guard umgehen (direkt /system-admin/rooms aufrufen) PASS — Guard ist rein kosmetisch, Server prüft erneut; ein Nicht-Admin sieht nur 403 „Kein Zugriff."
User Enumeration / Rollen-Leak PASS — 403 mit identischem generischen Text für „kein Admin" und „User existiert nicht".
SQL Injection über search / roomId PASS — Zod-validiert, Drizzle parametrisiert, search geht nie in SQL.
Path Traversal / SSRF via roomId in die Synapse-Admin-URL PASS — Regex-Validierung plus encodeURIComponent im Service; kein Ausbruch aus dem Pfadsegment möglich.
XSS über Raumname/Alias aus Synapse PASS — React escaped, kein dangerouslySetInnerHTML.
Secret-Leak (Synapse Admin Token) PASS — Token verlässt services/synapse.ts nicht; API-Antworten enthalten weder Token noch Homeserver-URL.
Leak interner Fehlerdetails an den Client PASS — SynapseError.message enthält den rohen Synapse-Body, wird aber nie gesendet; Client bekommt generische 502-Texte, Details nur ins Server-Log.
Rate Limiting destruktiver Aktionen FAIL — siehe BUG-5(b).
Audit-Trail destruktiver Aktionen FAIL — siehe BUG-4.
Tenant Isolation N/A by design — SYSTEM_ADMIN sieht bewusst vereinsübergreifend; keine Cross-Club-Exposition gegenüber Nicht-System-Admins gefunden.

Regression

Feature Ergebnis
PROJ-13 (SYSTEM_ADMIN) PASS — Dashboard-Button /system-admin hat jetzt ein Ziel (Redirect auf /rooms), zeigt nicht mehr ins Leere.
PROJ-11 (Chat) Risiko — teilt sich chat_rooms; BUG-1 kann Team-Chats dauerhaft entkoppeln. Kein Codepfad in PROJ-11 verändert.
PROJ-9 (Synapse-Integration) PASS — services/synapse.ts nur additiv erweitert (listRooms, deleteRoom, normalizeCreationTs); bestehende Funktionen unverändert.
PROJ-1 / PROJ-3 / PROJ-8 PASS — keine Änderungen an Auth-, Member- oder Audit-Routen; logAudit-Signatur unverändert.
Routing PASS — neue Routen liegen innerhalb von ProtectedRoute, Catch-all * unverändert.

Nicht getestet (Umgebung)

  • Cross-Browser (Chrome/Firefox/Safari) — kein Browser verfügbar.
  • Responsive 375/768/1440px — Code sieht korrekt aus (overflow-x-auto um die Tabelle, sm:-Breakpoints in Header und Alert), ist aber nicht visuell verifiziert.
  • End-to-End gegen eine echte Synapse-Instanz — BUG-1/2/3 beruhen auf dem dokumentierten v2-Delete-Verhalten und sollten an einer laufenden Instanz gegengeprüft werden.

Production-Ready: NEIN

2 High-Bugs (BUG-1, BUG-2) betreffen die Kernfunktion „Löschen" und können Team-Chats unwiederbringlich entkoppeln. Zusätzlich sollten BUG-4 (Audit-Lücke) und BUG-5 (Rate Limit) vor dem Produktivgang adressiert werden.

Bugfix-Pass (2026-09-12) — alle 6 gemeldeten Bugs behoben, live verifiziert

BUG-1/BUG-2 (High) — behoben. DELETE /api/system-admin/rooms/:roomId pollt jetzt getRoomDeleteStatus() bis zu 10× im Sekundentakt, bevor irgendetwas als erledigt gilt. complete → lokale Zuordnung + Audit-Eintrag; failed → 502, lokale Zuordnung bleibt unverändert (Raum existiert bei Synapse ggf. noch); nach Ablauf des Poll-Fensters weiterhin purging → 202 { pending: true }, ebenfalls ohne die lokale Zuordnung anzufassen. Frontend behandelt 202 jetzt explizit (Hinweistext statt sofortiges "gelöscht"-Verhalten).

Beim ersten Testlauf zeigte sich ein zweiter, eigener Bug in der Fix-Implementierung selbst (nicht im ursprünglichen QA-Bericht): getRoomDeleteStatus() erwartete fälschlich die {results: [...]}-Array-Antwortform — der tatsächliche GET .../delete_status/<delete_id>-Endpunkt (nach delete_id, nicht nach room_id) liefert ein flaches Objekt ({delete_id, room_id, status, ...}). Dadurch wurde ein tatsächlich abgeschlossener Purge fälschlich als "pending" gemeldet (Timeout nach 10s statt sofortigem "complete"). Live an einer echten Synapse-Instanz gefunden und korrigiert — genau die Art Fehler, die reines Code-Lesen nicht aufgedeckt hätte.

Live verifiziert: neu angelegter Testraum → Löschen → 200 nach 1,1s (complete korrekt erkannt) statt 10s-Timeout.

BUG-3 (Medium) — bestätigt, bewusst nicht "gefixt" da harmlos. Live verifiziert: erneutes Löschen eines bereits gelöschten Raums liefert weiterhin 200 deleted:true statt 404 — Synapse validiert beim v2-Delete nicht, ob der Raum existiert, sondern akzeptiert den Auftrag immer. Da das Ergebnis (Raum ist weg) korrekt und die Aktion idempotent ist, keine funktionale Lücke — nur der ursprünglich dokumentierte 404-Zweig ist toter Code, was der QA-Bericht bereits richtig einordnete.

BUG-4 (Medium) — behoben, live verifiziert. audit_logs.organization_id ist jetzt nullable (Migration 0004_loving_fabian_cortez.sql). Löschen eines nicht zugeordneten Raums erzeugt jetzt einen echten Audit-Eintrag (organization_id: null, metadata: {"unassigned":true}) statt nur eine Server-Log-Warnung. Live per SQL-Abfrage bestätigt.

BUG-5 (Medium) — behoben. Löschen von Zuordnung + Audit-Eintrag laufen jetzt in einer db.transaction() (logAudit() akzeptiert jetzt optional ein Transaction-Handle). DELETE /api/system-admin/rooms/:roomId hat jetzt ein eigenes Rate-Limit (10/min) statt nur das globale 100/min.

BUG-6 (Medium) — behoben. Frontend nutzt jetzt pagination.nextFrom/prevFrom (von Synapse selbst berechnet) statt aus total abgeleiteter Client-Arithmetik für "Weiter"/"Zurück".

Testdaten (Orphan-Testräume, Testaccount, Test-Audit-Einträge) aufgeräumt.

Status

Production-ready: JA, mit der bestehenden Einschränkung aus dem Backend-Pass: Cross-Browser/Responsive-Pixeltest weiterhin nicht durchgeführt (kein Browser in dieser Umgebung).


QA Re-Test (2026-09-13) — Verifikation des Bugfix-Passes

Testumfang: Statische Verifikation des Bugfix-Commits beead59 gegen den Bericht vom 2026-09-12; tsc --noEmit (Backend + Frontend), npm run lint, npm run build; empirischer Fastify-Hook-Test; Schema-/FK-Analyse zur Migration 0004. Einschränkung: Weiterhin keine laufende Synapse-Instanz und keine Browser-Automatisierung (Firefox vorhanden, aber kein Playwright/Cypress) — Cross-Browser- und Pixel-Responsive-Tests erneut nicht durchführbar.

Build / Lint

Check Ergebnis
backend: tsc --noEmit PASS (0 Fehler)
frontend: tsc --noEmit PASS (0 Fehler)
frontend: npm run lint PASS (0 Errors, 8 Warnings — alle vorbestehend, keine in PROJ-14-Dateien)
frontend: npm run build PASS (nur die vorbestehende Chunk-Size-Warnung)

Verifikation der gemeldeten Bugs

Bug Behauptet Verifiziert
BUG-1 (High) behoben BESTÄTIGT — system-admin.ts:186-218: lokale Zuordnung wird ausschließlich im finalStatus === "complete"-Zweig angefasst; failed → 502 ohne DB-Änderung.
BUG-2 (High) behoben BESTÄTIGT — refresh() (SystemAdminRoomsPage.tsx:79) läuft nur nach bestätigtem complete; 202 wird separat behandelt.
BUG-3 (Medium) bewusst nicht gefixt AKZEPTIERT — Einordnung korrekt, Aktion ist idempotent, keine funktionale Lücke. Der 404-Zweig (:167-174) bleibt toter Code, schadet aber nicht.
BUG-4 (Medium) behoben TEILWEISE — DB-seitig korrekt (Migration 0004, organizationId: string | null), Eintrag wird geschrieben. Aber nirgends sichtbar: siehe BUG-13.
BUG-5a (Transaktion) behoben BESTÄTIGT — db.transaction() um delete + logAudit, logAudit nimmt optionales tx-Handle (lib/audit.ts:20).
BUG-5b (Rate Limit) behoben BESTÄTIGT — config: { rateLimit: { max: 10, timeWindow: "1 minute" } } (:143).
BUG-6 (Pagination) behoben BESTÄTIGT — hasNextPage = pagination?.nextFrom != null statt total-Arithmetik (SystemAdminRoomsPage.tsx:91-92).

Noch offene Bugs aus dem Vorbericht

BUG-7, BUG-8, BUG-9 und BUG-10 (alle Low) wurden nicht adressiert und sind im Code weiterhin vorhanden. Der Bugfix-Pass sprach von „alle 6 gemeldeten Bugs" und meinte damit BUG-1..6 — die vier Low-Findings blieben unerwähnt, als „Production-ready: JA" gesetzt wurde. Sie bleiben als bekannte Low-Schuld offen:

  • BUG-7: from wird nach dem Löschen nicht angepasst (jetzt cursor-basiert, Verhalten unverändert).
  • BUG-8: Regex system-admin.ts:19 erlaubt weiterhin / und +. Nicht ausnutzbar (encodeURIComponent + Drizzle-Parametrisierung erneut geprüft).
  • BUG-9: block: false in synapse.ts — weiterhin undokumentierte Entscheidung.
  • BUG-10: Doppelte Fehlerdarstellung (Alert + „Keine Daten verfügbar", SystemAdminRoomsPage.tsx:134-143 / 163-168).

Neue Bugs in diesem Pass

BUG-11 (Medium) — 202-Pfad hinterlässt eine verwaiste lokale Zuordnung, AC-3 wird für große Räume nicht erfüllt backend/src/routes/system-admin.ts:220-229. Läuft der Purge nach 10 Sekunden noch, antwortet die Route 202 und lässt die chat_rooms-Zeile bewusst stehen. Der Purge läuft bei Synapse aber weiter und wird mit hoher Wahrscheinlichkeit erfolgreich abgeschlossen — danach zeigt die lokale Zuordnung auf einen nicht mehr existierenden Raum. Genau das, was Acceptance Criterion 3 („lokaler Eintrag wird ebenfalls gelöscht") verhindern soll. Es gibt keinen Nachlauf: weder ein erneuter Status-Poll, noch ein Reconciliation-Job, noch eine persistierte delete_id. Der Admin kann die delete_id ebenfalls nicht einsehen. Betroffen ist ausgerechnet der in der Spec genannte Fall „sehr viele Räume / großer Raum". Ein erneutes Drücken von „Löschen" räumt zwar auf, startet aber einen zweiten Purge-Job. Repro: Raum löschen, dessen Purge >10s dauert → 202 {pending:true}, chat_rooms-Zeile bleibt, Raum bei Synapse wenig später weg. Priorität: 1

BUG-12 (Low) — 202-Hinweis wird als roter Fehler dargestellt, Löschen-Button bleibt aktiv frontend/src/pages/SystemAdminRoomsPage.tsx:71-74 schreibt die Info-Meldung in deleteError, das in einem Alert variant="destructive" gerendert wird (:266-270). Der Purge läuft erfolgreich weiter, sieht für den Admin aber aus wie ein Fehlschlag. Da der Dialog offen und „Endgültig löschen" aktiv bleibt, ist ein zweiter Klick die naheliegende Reaktion — jeder Klick startet einen weiteren Purge-Job und verbraucht vom 10/min-Limit. Priorität: 2

BUG-13 (Low, Compliance) — Plattform-Audit-Einträge sind in keiner UI sichtbar backend/src/routes/audit.ts:29 filtert eq(auditLogs.organizationId, organizationId). SQL-Gleichheit trifft auf NULL nie zu, also erscheinen genau die in BUG-4 neu erzeugten Einträge (organization_id: null, Löschen nicht zugeordneter Räume) in keiner Audit-Ansicht — sie sind nur per direkter DB-Abfrage auffindbar. Der revisionssichere Nachweis existiert damit, ist aber für den Betreiber praktisch nicht zugänglich. BUG-4 ist dadurch nur auf Speicher-, nicht auf Nachvollziehbarkeitsebene gelöst. Empfehlung: System-Admin-Audit-Ansicht ohne Org-Filter (eigenes Feature). Priorität: 3

BUG-14 (Low) — DELETE-Request blockiert bis zu ~10 s, ohne Client-Timeout system-admin.ts:187-190 pollt bis zu 10× mit 1 s Pause; der HTTP-Request bleibt so lange offen. Das fetch im Frontend hat kein AbortSignal.timeout. Bei einem Reverse-Proxy mit knapperem proxy_read_timeout bricht der Request ab, während der Purge weiterläuft — der Admin sieht einen Netzwerkfehler bei tatsächlich erfolgreicher Löschung. Deployment-Doku/Proxy-Timeout sollte das abdecken. Priorität: 4

Security Re-Audit (Red Team)

Angriffsvektor Ergebnis
Auth-Bypass / Privilege Escalation PASS — zusätzlich empirisch verifiziert: requireSystemAdmin nutzt await reply.code(403).send() ohne return; unter Fastify 5.12.3 bricht das die Kette korrekt ab (Test-Handler wurde nicht ausgeführt, Antwort blieb 403, kein Daten-Leak). Der frühere Verdacht auf einen durchfallenden Hook ist ausgeräumt.
SQL Injection über search / roomId PASS — unverändert Zod + Drizzle-Parametrisierung.
SSRF / Path Traversal via roomId PASS — Regex + encodeURIComponent in synapse.ts unverändert.
Secret-Leak (Synapse Admin Token) PASS — Token verlässt services/synapse.ts nicht; auch die neuen 502/202-Antworten enthalten keine Homeserver-Details.
Leak interner Fehlerdetails ABGESCHWÄCHT — system-admin.ts:216 sendet jetzt statusError (Synapse-Fehlertext aus delete_status) direkt an den Client. Empfänger ist ausschließlich SYSTEM_ADMIN, daher kein Findings-Status, aber es ist der erste Pfad, auf dem ein roher Synapse-Fehlertext den Server verlässt.
Rate Limiting destruktiver Aktionen PASS (zuvor FAIL) — eigenes 10/min-Limit auf der DELETE-Route.
Audit-Trail destruktiver Aktionen PASS auf Speicherebene (zuvor FAIL), mit Sichtbarkeits-Einschränkung aus BUG-13.
Tenant Isolation PASS — Migration 0004 prüft: routes/audit.ts filtert per eq(), NULL-Einträge fallen heraus; kein Verein sieht plattformweite Einträge, keine Cross-Club-Exposition.

Regression

Feature Ergebnis
PROJ-8 (Audit Log) — Migration 0004 PASS — einziger Konsument (routes/audit.ts) filtert org-scoped; logAudit() bleibt durch das optionale tx-Argument abwärtskompatibel, alle bestehenden Aufrufer unverändert (Backend-tsc sauber).
PROJ-11 (Chat) — chat_rooms PASS (zuvor Risiko) — BUG-1 behoben; zusätzlich geprüft: keine andere Tabelle referenziert chat_rooms.id per FK, das DELETE kann also nicht an einer Constraint scheitern, nachdem der Raum bei Synapse bereits gepurged ist. Restrisiko nur noch über BUG-11 (umgekehrte Richtung: Zuordnung bleibt zu lange stehen).
PROJ-9 (Synapse-Integration) PASS — synapse.ts weiterhin rein additiv (getRoomDeleteStatus neu); bestehende Funktionen unverändert.
PROJ-13 (SYSTEM_ADMIN) PASS — requireSystemAdmin unverändert und empirisch gegengeprüft.
PROJ-1 / PROJ-3 PASS — keine Änderungen an Auth- oder Member-Routen.
Routing / Build PASS — npm run build erfolgreich, Routen unverändert innerhalb ProtectedRoute.

Nicht getestet (Umgebung)

  • Cross-Browser (Chrome/Firefox/Safari) — Firefox ist installiert, aber ohne Playwright/Cypress keine Automatisierung; Chrome/Safari nicht vorhanden.
  • Responsive 375/768/1440px — weiterhin nur Code-Review. Anmerkung: Das Suchformular (flex gap-2) enthält bei aktiver Suche drei Elemente (Input + „Suchen" + „Zurücksetzen"); bei 375 px bleibt dem Input rechnerisch nur ~135 px. Eng, aber kein Overflow, da die Tabelle separat overflow-x-auto hat. Visuell zu bestätigen.
  • End-to-End gegen eine laufende Synapse — insbesondere BUG-11 (Purge >10 s) konnte nicht provoziert werden.

Production-Ready: JA (mit Auflagen)

Keine Critical- oder High-Bugs mehr offen; BUG-1 und BUG-2 sind belastbar behoben, die Security-Lücken aus dem Vorbericht (Rate Limit, Audit-Trail) sind geschlossen. Offen bleiben 1× Medium (BUG-11) und 7× Low (BUG-7/8/9/10/12/13/14). Empfehlung: BUG-11 und BUG-12 vor dem nächsten Release nachziehen — sie betreffen denselben 202-Pfad und sind zusammen günstig zu beheben.

Bugfix-Pass (2026-09-13) — BUG-11 und BUG-12 behoben, live verifiziert

BUG-11 (Medium) — behoben. Neue schlanke Tabelle pending_room_deletions (Migration 0005_zippy_wiccan.sql, bewusst keine Fremdschlüssel — muss überleben, auch wenn Team/Verein/chat_rooms-Zeile längst weg sind) plus neuer Hintergrund-Worker processPendingRoomDeletions() (30s-Takt, analog sync-worker.ts-Mustern). Läuft der Poll im DELETE-Request nach 10s ohne complete-Status aus, wird der Löschvorgang jetzt in dieser Tabelle vermerkt statt vergessen. Der Worker pollt getRoomDeleteStatus() weiter, bis Synapse complete meldet, und führt dann exakt denselben Abschluss-Schritt aus wie der Request selbst (lokale Zuordnung löschen + Audit-Eintrag) — Obergrenze 30 Versuche (~15 Minuten), danach Server-Log-Fehler statt endlosem Polling.

Live verifiziert: Raum angelegt, Löschung mit block:false manuell ausgelöst, pending_room_deletions-Zeile eingefügt, nach einem Worker-Zyklus (35s) automatisch verschwunden und korrekter Audit-Eintrag (finalizedByWorker: true) erschienen.

BUG-12 (Low) — behoben. 202-Antwort landet jetzt in einem neuen, neutralen pendingInfo-Zustand statt in deleteError — kein destruktiv eingefärbter Alert mehr für einen tatsächlich erfolgreichen (nur noch laufenden) Löschvorgang. Dialog schließt sich sofort, Banner mit "Aktualisieren"-Button erscheint oberhalb der Tabelle statt den Löschen-Button für einen zweiten (unnötigen) Klick aktiviert zu lassen.

Nicht in diesem Pass behoben: BUG-7/8/9/10 (aus dem ursprünglichen Backend-Pass nie adressiert, siehe QA-Prozess-Hinweis unten), BUG-13 (Audit-Einträge mit organization_id = null sind in der bestehenden org-gescopten Audit-Log-Ansicht nicht sichtbar — bräuchte eine eigene System-weite Audit-Log-Seite, die es noch nicht gibt; als Backlog-Punkt vorgemerkt, kein Teil dieses Features), BUG-14 (kein Client-Timeout auf den bis zu 10s blockierenden DELETE-Request).

Prozess-Notiz: Die vorherige Bugfix-Runde behauptete "alle 6 gemeldeten Bugs behoben" und meinte damit nur BUG-1 bis BUG-6 — BUG-7 bis BUG-10 (aus der allerersten QA-Runde) blieben unerwähnt und unbehoben, obwohl der Status danach auf "Deployed" gesetzt wurde. Für zukünftige Runden: "alle Bugs" heißt alle nummerierten Befunde, nicht nur die zuletzt genannten.