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:
1 parent
beead59907
commit
3bc2ac8ef4
9 files changed
+1866
-10
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user