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
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-13 10:20:08 +02:00
1 parent beead59907
commit 3bc2ac8ef4
9 files changed
+1866 -10

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
CREATE TABLE "pending_room_deletions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"matrix_room_id" text NOT NULL,
"delete_id" text NOT NULL,
"chat_room_id" uuid,
"organization_id" uuid,
"team_id" uuid,
"room_name" text,
"actor_user_id" uuid,
"attempts" integer DEFAULT 0 NOT NULL,
"last_error" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "pending_room_deletions_matrix_room_id_unique" UNIQUE("matrix_room_id")
);
File diff suppressed because it is too large. Load diff
+7
View File
@@ -36,6 +36,13 @@
"when": 1789241972275,
"tag": "0004_loving_fabian_cortez",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1789287324574,
"tag": "0005_zippy_wiccan",
"breakpoints": true
}
]
}
+26
View File
@@ -320,3 +320,29 @@ export const auditLogs = pgTable(
index("audit_logs_created_at_idx").on(table.createdAt),
],
);
// BUG-11 (PROJ-14 QA re-test): a room purge that's still running after the
// DELETE route's ~10s poll window used to just return 202 and forget about
// it — no follow-up, no persisted state. Synapse finishes the purge anyway
// moments later, but the chat_rooms mapping (and the audit trail) was left
// stale forever, silently pointing at a room that no longer exists.
export const pendingRoomDeletions = pgTable(
"pending_room_deletions",
{
id: uuid("id").primaryKey().defaultRandom(),
matrixRoomId: text("matrix_room_id").notNull().unique(),
deleteId: text("delete_id").notNull(),
// Bookkeeping only, not FKs — this table must survive its referents being
// deleted first (that's the whole point: chat_rooms/teams/organizations
// may all be gone by the time the background worker finishes here).
chatRoomId: uuid("chat_room_id"),
organizationId: uuid("organization_id"),
teamId: uuid("team_id"),
roomName: text("room_name"),
actorUserId: uuid("actor_user_id"),
attempts: integer("attempts").notNull().default(0),
lastError: text("last_error"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
},
);
+15 -4
View File
@@ -2,7 +2,7 @@ import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { eq, inArray } from "drizzle-orm";
import { db } from "../db/index.js";
import { chatRooms, organizations, teams } from "../db/schema.js";
import { chatRooms, organizations, teams, pendingRoomDeletions } from "../db/schema.js";
import { requireSystemAdmin } from "../lib/authz.js";
import { logAudit } from "../lib/audit.js";
import { SynapseError, deleteRoom, getRoomDeleteStatus, listRooms } from "../services/synapse.js";
@@ -218,9 +218,20 @@ export async function systemAdminRoutes(app: FastifyInstance) {
}
if (finalStatus === "pending") {
// Still purging after the poll window (large room). Leave the local
// mapping in place — it's still correct — and tell the admin plainly
// instead of implying the room is gone.
// BUG-11 (QA re-test): still purging after the poll window (large
// room). The local mapping stays in place — it's still correct right
// now — but the previous version just forgot about it here. Hand it
// to the background worker so the mapping/audit get finalized the
// moment Synapse actually finishes, instead of staying stale forever.
await db.insert(pendingRoomDeletions).values({
matrixRoomId: roomId,
deleteId: deleteId!,
chatRoomId: local?.id ?? null,
organizationId: local?.organizationId ?? null,
teamId: local?.teamId ?? null,
roomName: local?.name ?? null,
actorUserId: request.user.sub,
});
return reply.code(202).send({
deleted: false,
pending: true,
+7 -1
View File
@@ -18,7 +18,11 @@ import { roleRoutes } from "./routes/roles.js";
import { auditRoutes } from "./routes/audit.js";
import { chatRoutes } from "./routes/chat.js";
import { systemAdminRoutes } from "./routes/system-admin.js";
import { startSyncWorker, startReconciliationWorker } from "./services/sync-worker.js";
import {
startSyncWorker,
startReconciliationWorker,
startRoomDeletionWorker,
} from "./services/sync-worker.js";
const app = Fastify({ logger: true });
@@ -80,8 +84,10 @@ app.listen({ port, host: "0.0.0.0" }).catch((err) => {
const syncWorkerInterval = startSyncWorker();
const reconciliationInterval = startReconciliationWorker();
const roomDeletionInterval = startRoomDeletionWorker();
app.addHook("onClose", (_instance, done) => {
clearInterval(syncWorkerInterval);
clearInterval(reconciliationInterval);
clearInterval(roomDeletionInterval);
done();
});
+80 -1
View File
@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import { and, eq, lt, or, isNull, isNotNull } from "drizzle-orm";
import { db } from "../db/index.js";
import { syncJobs, members, chatRooms, teams } from "../db/schema.js";
import { syncJobs, members, chatRooms, teams, pendingRoomDeletions } from "../db/schema.js";
import {
createUser,
createRoom,
@@ -10,6 +10,7 @@ import {
getUser,
toMatrixUserId,
toLocalpart,
getRoomDeleteStatus,
SynapseError,
} from "./synapse.js";
import { logAudit } from "../lib/audit.js";
@@ -271,3 +272,81 @@ export function startReconciliationWorker(intervalMs = 10 * 60_000): NodeJS.Time
});
}, intervalMs);
}
const ROOM_DELETION_MAX_ATTEMPTS = 30; // ~15min at the default 30s interval, generous for a big room
/**
* BUG-11 (PROJ-14 QA re-test): finishes room purges the DELETE route's
* synchronous poll window gave up on. Runs the exact same finalize step the
* route itself would have run — drop the local chat_rooms mapping (if any)
* and write the audit entry — the moment Synapse actually reports done.
*/
export async function processPendingRoomDeletions(): Promise<void> {
const pending = await db.select().from(pendingRoomDeletions);
for (const entry of pending) {
let status;
try {
status = await getRoomDeleteStatus(entry.deleteId);
} catch (err) {
console.warn(`[room-deletion] failed to poll status for ${entry.matrixRoomId}:`, err);
continue;
}
if (status.status === "purging" || status.status === "shutting_down") {
const attempts = entry.attempts + 1;
if (attempts >= ROOM_DELETION_MAX_ATTEMPTS) {
console.error(
`[room-deletion] gave up polling ${entry.matrixRoomId} after ${attempts} attempts — ` +
"local mapping left in place, needs manual follow-up.",
);
await db.delete(pendingRoomDeletions).where(eq(pendingRoomDeletions.id, entry.id));
continue;
}
await db
.update(pendingRoomDeletions)
.set({ attempts, updatedAt: new Date() })
.where(eq(pendingRoomDeletions.id, entry.id));
continue;
}
if (status.status === "failed") {
// Purge failed at Synapse — the room may still exist, so the local
// mapping (if any) is still accurate. Just stop tracking the attempt.
console.error(`[room-deletion] purge failed for ${entry.matrixRoomId}:`, status.error);
await db.delete(pendingRoomDeletions).where(eq(pendingRoomDeletions.id, entry.id));
continue;
}
// status.status === "complete"
if (entry.chatRoomId) {
await db.delete(chatRooms).where(eq(chatRooms.id, entry.chatRoomId));
await logAudit({
organizationId: entry.organizationId,
actorUserId: entry.actorUserId,
action: "system_admin.room.deleted",
targetType: "chat_room",
targetId: entry.matrixRoomId,
metadata: { name: entry.roomName, teamId: entry.teamId, finalizedByWorker: true },
});
} else {
await logAudit({
organizationId: null,
actorUserId: entry.actorUserId,
action: "system_admin.room.deleted",
targetType: "chat_room",
targetId: entry.matrixRoomId,
metadata: { unassigned: true, finalizedByWorker: true },
});
}
await db.delete(pendingRoomDeletions).where(eq(pendingRoomDeletions.id, entry.id));
}
}
export function startRoomDeletionWorker(intervalMs = 30_000): NodeJS.Timeout {
return setInterval(() => {
processPendingRoomDeletions().catch((err) => {
console.error("[room-deletion] failed to process pending deletions:", err);
});
}, intervalMs);
}
@@ -216,3 +216,97 @@ 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.
+24 -4
View File
@@ -41,6 +41,12 @@ export function SystemAdminRoomsPage() {
const [roomToDelete, setRoomToDelete] = useState<SynapseRoom | null>(null);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
// BUG-12 (QA re-test): a 202 (purge still running) used to land in
// `deleteError` and render as a destructive alert with the dialog stuck
// open — a successful background purge looked exactly like a failure, and
// the enabled delete button invited a second click that would queue a
// second purge job. This is a separate, neutral state instead.
const [pendingInfo, setPendingInfo] = useState<string | null>(null);
const { rooms, pagination, loading, error, refresh } = useSynapseRooms(token, from, search);
@@ -65,11 +71,14 @@ export function SystemAdminRoomsPage() {
// BUG-1/BUG-2 (QA): the backend now only answers 200 once the purge is
// actually confirmed complete — a 202 means it's still running in the
// background. Refreshing right away would either show the room as
// already gone (if we assumed success) or, worse, still-present with no
// explanation. Show the real state instead of guessing.
// background (a background worker finishes it, see BUG-11). Neither a
// silent success nor a destructive-looking error is accurate here.
if (res.status === 202) {
setDeleteError(body?.message ?? "Löschung läuft im Hintergrund weiter.");
setRoomToDelete(null);
setPendingInfo(
body?.message ??
`„${roomToDelete.name ?? roomToDelete.matrixRoomId}“ wird im Hintergrund weiter gelöscht.`,
);
return;
}
if (!res.ok) {
@@ -142,6 +151,17 @@ export function SystemAdminRoomsPage() {
</Alert>
)}
{pendingInfo && (
<Alert>
<AlertDescription className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<span>{pendingInfo}</span>
<Button variant="outline" size="sm" onClick={() => { setPendingInfo(null); void refresh(); }}>
Aktualisieren
</Button>
</AlertDescription>
</Alert>
)}
<div className="overflow-x-auto">
<Table>
<TableHeader>