Platform SYSTEM_ADMIN is deliberately independent of org membership
status (PROJ-13 non-goal: revoke is DB-only, no suspend concept on
users). requireSystemAdmin already reads the flag fresh per request,
so revocation is immediate. QA's finding conflated org-level
suspension with platform-admin revocation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deployed: 2026-09-18. Server checkout had drifted (36 commits behind,
uncommitted local edits to auth-context.tsx/LoginForm.tsx from prior
direct server edits) — stashed before pull, not discarded. Noted
drizzle-kit --omit=dev migration issue and workaround for future
deploys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QA round 1 found: HTTP 400 from Synapse was misread as "federation
disabled", falsely confirming isolation on a rejected request instead
of a real disabled state. Only 404 (no such admin endpoint) now maps
to that state; 400 falls through to the 502 error path.
Also fixes: nextToken type mismatch (Synapse sends a string, hook
declared number), empty-state panel wrongly shown on pages beyond the
last result, and a NaN guard when advancing pagination.
AC amended: "letzter erfolgreicher Kontakt" removed — Synapse's
destinations list endpoint has no such timestamp, only retry_last_ts
for failed attempts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verified the actual admin API against matrix.perlbach24.de before
finalizing the design (lesson from PROJ-14/15, where per-version
response-shape surprises cost a real bug and a wasted defensive code
path): GET /_synapse/admin/v1/federation/destinations returns a clean
{destinations: [], total: 0} instantly with federation disabled — no
surprises, no open questions left for the backend pass. Status set to
In Progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
POST /api/system-admin/media/purge-cache (SYSTEM_ADMIN-only, 10/min
rate limit) plus /system-admin/media page — a date picker (default
30 days back), confirmation dialog, and honest UI copy: this only
clears Synapse's *federated*-media cache, which is nearly empty with
federation disabled. No summary/size-preview endpoint, matching the
tech design's explicit call to not show a number that might be wrong
across Synapse versions.
Backend agent couldn't reach the live Synapse to confirm sync/async
behavior or whether a count comes back (no access to backend/.env in
that sandbox), so it built defensively (number | null). Verified
directly against matrix.perlbach24.de: the call is synchronous (~1.1s)
and does return a real count ({"deleted": 0}). The defensive handling
was unnecessary but harmless (correctly treats 0 as a real count, not
"no data") and stays in as a safety net for other Synapse versions.
Verified end-to-end on talkhub/matrix: purge call returns
{"deleted":0}, audit log gets a proper system_admin.media_cache.purged
entry (organization_id: null), non-admin gets 403, a future cutoff
date is rejected with 400. Test accounts/audit rows cleaned up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Scoped honestly: Synapse's purge only clears the *remote* (federated)
media cache, which is nearly empty with federation disabled. Local
media now exists in a small way since the Element Web deployment lets
members set profile pictures directly — noted, but out of this
feature's scope (button copy says "Föderations-Medien-Cache" rather
than implying general disk cleanup). No pre-purge size estimate,
since Synapse doesn't reliably support that across versions — flagged
for the backend pass to verify against the actual deployed version
(lesson from PROJ-14's delete_status response-shape surprise). Status
set to In Progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
BUG-1/BUG-2 (High) — DELETE now polls getRoomDeleteStatus() (up to 10x
1s) before treating the purge as done. "complete" -> drop local mapping
+ audit; "failed" -> 502, mapping left untouched; still "purging" after
the poll window -> 202 {pending:true}, mapping also left untouched.
Frontend handles 202 explicitly instead of assuming success.
Found and fixed a second bug while verifying the first fix live:
getRoomDeleteStatus() assumed a {results:[...]} array response, but
GET .../delete_status/<delete_id> (by delete_id, as used here) returns
a flat object. This silently misread every "complete" as "pending" —
caught only by testing against a real Synapse instance, not by
reading the code.
BUG-3 (Medium) — confirmed live: re-deleting an already-gone room still
returns 200 instead of 404, because Synapse's v2 delete never validates
room existence. Left as-is: the outcome is correct and the operation is
idempotent, so the originally-documented 404 branch is dead code but
not a functional gap.
BUG-4 (Medium) — audit_logs.organization_id is nullable now (migration
0004). Deleting an unassigned room gets a real audit row
(organization_id: null) instead of only a server log warning.
BUG-5 (Medium) — the mapping delete and its audit write now run inside
one db.transaction() (logAudit() takes an optional tx handle); the
delete route has its own 10/min rate limit instead of only the global
100/min.
BUG-6 (Medium) — frontend pagination uses pagination.nextFrom/prevFrom
(Synapse's own cursors) instead of client-side arithmetic against
`total`, which could offer a "Weiter" onto an empty page during a
search.
All verified end-to-end against the live talkhub/matrix Synapse:
fresh orphan room deletes in ~1.1s as "complete" (was a 10s timeout
misreported as pending before the results/flat-object fix); re-deleting
an already-gone room still 200s; a null-org audit row appears after
deleting an unassigned room. Test rooms/accounts/audit rows cleaned up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
/system-admin/rooms — table (name/alias, members, TalkTime assignment,
created date, delete), search, pagination, delete confirmation dialog
that warns extra hard when the room is still bound to a live team.
SystemAdminRoute gates on user.isSystemAdmin (nav-level only, the
backend routes are the real enforcement).
Also corrected PROJ-13's status to Deployed — it was stuck at
"In Review" despite being live-verified end-to-end already.
Verified: /system-admin/rooms serves the SPA shell (200).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
No new TalkTime DB table — reads Synapse's room list directly and
cross-references the existing chat_rooms table for team/org
attribution. Delete goes straight to Synapse. Status set to In Progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
users.isSystemAdmin (boolean, default false), DB-only to grant (no UI
in MVP scope, per spec). requesterIsSystemAdmin()/requireSystemAdmin()
in authz.ts — checked fresh from the DB on every request, not cached
in the JWT, so revoking takes effect immediately. GET /api/auth/me
returns the flag; DashboardPage shows a "System-Admin" nav button when
set (convenience only, not the access control — PROJ-14/15/16/18's
own routes enforce it).
/system-admin route doesn't exist yet (comes with PROJ-14) — the nav
button currently bounces back to / via the catch-all route, no stub
page added.
Verified end-to-end on talkhub: /api/auth/me flips true immediately
after a direct DB update, no re-login required.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
New boolean flag on the login account (not tied to any organization
membership), checked on every request like the existing org-role
pattern. No UI to grant it — DB-only for MVP. Status set to In Progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Six single-responsibility specs covering the synapse-admin feature set
the user wants rebuilt into TalkTime: a new SYSTEM_ADMIN role (platform-
wide, DB-only for MVP — no UI to grant it), room overview, media cache
cleanup (manual trigger only), federation status (mostly a no-op display
since federation is disabled), message reporting in chat, and the admin
moderation view for those reports.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
BUG-25: DashboardPage header ran ~200px past the viewport at 375px,
"Abmelden" unreachable without horizontal scroll. flex-col/flex-wrap
so it stacks on narrow screens instead of overflowing.
BUG-23: the previous fix (logout_all admin endpoint) never worked —
that route doesn't exist on Synapse (404, swallowed by try/catch).
A second attempt (list + bulk-delete devices) also did nothing,
because loginAsUser()'s admin puppet-login issues a deviceless token
that never appears in the devices list. Live-tested both failures via
account/whoami with the "revoked" token still returning 200.
Actual fix: revokeAccessToken() calls the plain Client-Server /logout
endpoint authenticated as the token being revoked, not as the backend's
admin token — the only mechanism that invalidates a deviceless token.
invalidateMatrixAccessToken() now returns the token it removed so the
route can pass it along. Verified: whoami returns 200 before logout,
401 M_UNKNOWN_TOKEN with the same token right after.
QA round 4 (first with real cross-browser access — Chromium/Firefox/
WebKit at 375/768/1440px) found no remaining Critical or functional
High findings once these two land: production-ready.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
BUG-4 (High, failed acceptance criterion) — per-tile unread badges:
new use-team-unread-counts.ts runs one app-level Matrix client (separate
from useTeamChat's detail-view client) that syncs every room GET /api/teams
now resolves (matrixRoomId added via leftJoin on chat_rooms) and reports
unread counts per team. TeamsPage tiles show the badge without the chat
tab being open. Side fix: createMatrixClient() now dynamic-imports
matrix-js-sdk instead of a static import, so the new always-mounted hook
doesn't drag the SDK (and its 7.8MB crypto WASM) back into TeamsPage's
main bundle — verified via build output (TeamDetail chunk 853KB -> 27KB,
SDK now in its own on-demand chunk).
BUG-3 (High) — kick-while-open: use-team-chat.ts now listens for
RoomEvent.MyMembership; leaving/removal flips status to a new "removed"
state instead of leaving history and the composer usable.
BUG-2 (High, partially mitigated) — minted tokens are unscoped Matrix
credentials by nature (Matrix has no token scoping). Disabled
user_directory search on Synapse (config + live matrix.perlbach24.de
instance), verified empty results instead of cross-club user listing.
Direct addressing by known Matrix ID is still possible — full tenant
isolation needs an explicit decision (one Synapse per club vs. an
isolation module on a shared homeserver), documented as an open item
in the spec rather than silently declared fixed.
BUG-1 (Critical, not fixed) — backend/.env.example is outside my
permission sandbox (can't read or write .env* files). Documented the
required block in the spec for manual addition; talkhub's real .env
already has the values.
Verified end-to-end on talkhub/matrix test LXCs. Not addressed in this
pass (lower priority per QA): BUG-5/6/7/8/9/10.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Direct browser-to-Synapse connection via matrix-js-sdk, backend only
mints a short-lived per-member access token (no chat proxy, no new
WebSocket server, no new DB table). Status set to In Progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
- CLAUDE.md header/description now match actual project (TalkTime), not starter-kit template
- Retro-documented already-built MVP features as PROJ-1..PROJ-10 in features/INDEX.md
- Corrected PROJ-9: Matrix User Provisioning, room sync and reconciliation were already implemented, not just health-check
- Added PROJ-11 requirements spec for basic chat (Matrix Client-Server API, team scope, text-only MVP)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Replace Next.js/Supabase/Vercel stack with a self-hostable setup: Vite+React
frontend, Fastify+Drizzle+PostgreSQL backend with own JWT auth, deploy via
git pull + rsync + systemd (checkout/service separated, updates pulled from
Gitea). Adds a reference auth route (register/login/me) as the canonical
pattern for new routes. Updates all skills/rules/agents/docs to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BMdBJfD9TT6XUU2NqGG1PN
Replace the manual "read .claude/agents/*.md" workflow with native
Claude Code features for a more efficient, scalable development experience:
- **Skills** (.claude/skills/): 7 auto-discovered slash commands
(/requirements, /architecture, /frontend, /backend, /qa, /deploy, /help)
with forked sub-agents for heavy tasks and inline execution for interactive ones
- **Rules** (.claude/rules/): 4 modular rule files (general, frontend, backend,
security) auto-applied based on file context
- **Sub-Agents** (.claude/agents/): Lightweight configs for frontend-dev,
backend-dev, and qa-engineer with model, tool, and turn limit settings
- **Context Engineering**: Layered context loading, context isolation via
forked skills, built-in context recovery after compaction, and
"always read, never guess" rules to prevent hallucinated code references
- **CLAUDE.md**: Auto-loaded project context replacing PROJECT_CONTEXT.md
- **Feature tracking**: features/INDEX.md as persistent state across sessions
- **Production guides**: docs/production/ for error tracking, security,
performance, database optimization, and rate limiting
- **Init Mode**: /requirements detects empty PRD and bootstraps full project
setup (PRD + all feature specs) from a single project description
Removed: 6 monolithic agent files, PROJECT_CONTEXT.md, HOW_TO_USE_AGENTS.md,
TEMPLATE_CHANGELOG.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>