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>
GET /api/system-admin/federation — SYSTEM_ADMIN-only, wraps Synapse's
GET /_synapse/admin/v1/federation/destinations read-only (no block/
unblock, per spec non-goals). Distinguishes federationAvailable:false
(admin API missing) from an empty destinations list (federation
disabled but the API works fine) from a 502 (Synapse unreachable) —
an admin verifying isolation must never read "Synapse is down" as
"no federation partners", which a flattened empty list would imply.
No schema change, no audit entry (pure read, same as the PROJ-14 room
list).
Verified end-to-end on talkhub/matrix: 403 without SYSTEM_ADMIN, 200
with {federationAvailable:true, destinations:[], total:0} — the
expected result with federation disabled.
Frontend page (/system-admin/federation) still outstanding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
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-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
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
GET /api/system-admin/rooms and DELETE /api/system-admin/rooms/:roomId,
both gated on requireSystemAdmin from PROJ-13. No new DB table — reads
Synapse's room list directly, cross-references the existing chat_rooms
table for org/team attribution via one inArray join.
deleteRoom() uses the v2 async purge endpoint (not v1, which blocks
until the purge finishes and would time out on a large room).
normalizeCreationTs() handles Synapse versions that return seconds
instead of documented milliseconds — moot on this deployment's Synapse
version, which omits creation_ts from the rooms list entirely; verified
live, code already treats a missing value as null rather than guessing.
Known gap: audit_logs.organization_id is NOT NULL, so deleting an
unassigned room can't get a proper audit entry (falls back to a server
log warning). Flagged rather than silently worked around.
Verified end-to-end on talkhub/matrix: 403 without the SYSTEM_ADMIN
flag, 200 with it, and used the new delete endpoint to clean up a
genuinely orphaned test room left over from earlier QA.
Frontend (/system-admin route, rooms table, delete dialog) still
outstanding — the nav button added in PROJ-13 stays a dead link until
that lands.
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
PUT /api/members/:memberId/password — admin-only, hashes and sets the
password immediately instead of going through the email reset-link
flow. Useful in this deployment where SMTP isn't configured yet, and
more generally whenever the member needs to log in right away.
Frontend: "Passwort direkt setzen" dialog in the members list dropdown,
alongside the existing "Passwort-Mail senden" (renamed for clarity now
that there are two options).
Verified end-to-end: new password logs in, old password is rejected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
BUG-27: POST /api/chat/token required no organizationId and just took
whichever provisioned membership row the DB returned first — a user in
more than one org (multi-tenant is explicitly in scope per PRD) could
get a Matrix token for the wrong org's identity. organizationId is now
required, threaded through fetchChatCredentials/useTeamChat/
useTeamUnreadCounts, and the frontend credential cache is now keyed
per-org so switching orgs doesn't reuse a stale token. Same ambiguity
fixed in GET /api/teams' BUG-13 membership lookup. Verified: token
request without organizationId now 400s.
BUG-26: the JWT lived only in useState, so every reload or deep link
threw the user back to /login. auth-context.tsx now persists it to
localStorage and restores/validates it on mount (via /api/auth/me);
ProtectedRoute waits on a new `initializing` flag instead of redirecting
before the restore attempt resolves.
BUG-28: TeamChat's fixed vh-fraction height could never guarantee the
composer stayed on screen — page header, the create-team card and the
team-tile grid all sit above it, so total content exceeds 100vh
regardless of the chat box's own height. Switched vh to dvh (mobile
keyboard/chrome resize) and added a scrollIntoView on mount, which
fires reliably since Radix Tabs only mounts the active tab's content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
First attempt checked err.code, which is undefined — drizzle-orm wraps
the raw pg error (the one that actually has .code = '23505' for a
unique violation) in .cause. Live-tested and caught before assuming
the fix worked from code review alone. Now checks err.cause?.code
first, falls back to err.code.
Verified: first add 201, duplicate add 409 "Member is already on this
team" instead of 500.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Org admins can now trigger a password reset for a member from the
members list dropdown, without ever seeing or setting the password
themselves — POST /api/members/:memberId/reset-password sends the
same reset-link email the self-service /forgot-password flow uses.
Extracted the shared token-generation + email logic into
lib/password-reset.ts so both flows use one implementation. Returns
409 member_has_no_account for members who never accepted their
invitation (no linked login account to reset).
Verified end-to-end on talkhub: admin trigger sends the mail (SMTP
not configured in this test env, confirmed via the mail service's
own log fallback), non-admin gets 403.
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
Every previously-reported bug from the three QA rounds is now addressed
(BUG-11 and BUG-15 mitigated rather than fully solved — see spec for
why). Verified end-to-end on talkhub/matrix after each change.
Security:
- BUG-21: @fastify/helmet registered, CORS origin allowlist (APP_BASE_URL
+ localhost:5173) replaces `origin: true`. Verified: own origin gets
CORS + HSTS/nosniff/X-Frame-Options headers, foreign origins get none.
- BUG-13: GET /api/teams only returns matrixRoomId for teams the
requester actually belongs to (was leaking every team's room id to
any org member). Verified with/without team membership.
- BUG-14: leftJoin now filters scope='team'; new partial unique index
chat_rooms_team_id_team_scope_unique enforces one room per team at
the DB level, not just in the query (migration 0002).
Reliability:
- BUG-5: use-team-chat.ts's setup poll now backs off exponentially
(5s->60s) instead of a fixed 15s; SyncState.Error stops the client
and reconnects with backoff instead of letting matrix-js-sdk retry
against Synapse forever.
- BUG-6: one-off backfill script (src/scripts/backfill-history-visibility.ts)
sets history_visibility=shared on rooms created before that fix;
run against talkhub/matrix.
- BUG-9: route rate limit on GET /api/chat/room, unified 404 for
nonexistent-team vs not-a-member (was a cross-org existence oracle),
LRU-correct eviction order in matrix-token-cache.ts, scroll position
preserved on "load older messages", and eslint.config.js added (lint
never ran on this feature before — flat config was simply missing).
- BUG-15 (mitigated): a small module-level credential cache in
lib/matrix.ts lets useTeamChat and useTeamUnreadCounts share one
fetched token instead of each hitting POST /api/chat/token
independently. Reduces but doesn't eliminate rate-limit risk under
rapid team switching (no in-flight dedup between the two hooks).
- BUG-16: refreshCounts() checks `cancelled` before setCounts.
- BUG-17: use-team-chat.ts uses getRoomUnreadNotificationCount()
instead of the deprecated getUnreadNotificationCount(); left
unchanged in use-team-unread-counts.ts on purpose, since importing
NotificationCountType there would undo BUG-11's bundle-splitting fix.
Cleanup:
- BUG-10: new POST /api/chat/logout invalidates the cached token and
calls Synapse's logout_all admin endpoint; frontend calls it
best-effort on logout and clears its own credential cache.
- BUG-22: trim() on displayName construction (trailing space with an
empty last name); the reported trailing-dot localpart is moot after
BUG-20 since localparts are random now, not name-derived.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
toLocalpart() built vorname.nachname from real names, so anyone could
construct another org's Matrix ID and DM them directly — completely
sidestepping the user_directory.enabled=false mitigation from BUG-2,
which only blocks *searching*, not addressing a known ID. Live-exploited
during QA round 3 (cross-club DM succeeded).
Now generates a random `u<16 hex chars>` localpart with no relation to
the member's name. Display name (passed separately to createUser) still
shows the real name in room member lists.
sync-worker.ts and invitations.ts both used to retry on collision by
appending a numeric suffix to the name-derived base; since there's no
name relation left, collision retry now just draws a fresh random id.
Verified end-to-end on talkhub/matrix: new org creator got
@u747fcd631af6db20:matrix.perlbach24.de instead of a name-derived id.
Test data cleaned up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
BUG-18 (Critical) — invitations.ts never joined the Matrix room for
members invited with a teamId (only teams.ts's add-member path did).
Standard onboarding flow left every invited team member stuck on
"Raum wird eingerichtet" forever, with no self-healing (reconciliation
only checks user provisioning, never room membership) and no repair
path (blocked by BUG-19). Now mirrors teams.ts: inline addUserToRoom
with an enqueueSyncJob("join_team_room") fallback.
BUG-19 (High) — no global Fastify error handler meant an uncaught
error (e.g. a duplicate-key insert) echoed back the raw Drizzle error,
including the full SQL statement and bound parameters, to the client.
Added app.setErrorHandler(): 5xx is logged server-side and replaced
with a generic message, 4xx keeps its already-safe original message.
BUG-12 (High) — use-team-unread-counts.ts had no SyncState.Error
handling, so an expired token meant matrix-js-sdk retried /sync
against Synapse forever with a dead token while badges silently
froze. Now stops the client and reconnects with a fresh token,
capped exponential backoff.
BUG-11 (High, partially mitigated) — deferred the app-level unread
client's start to requestIdleCallback instead of firing synchronously
on TeamsPage mount, taking it off the critical path. Does NOT fix the
underlying complaint (SDK still loads for anyone who visits the team
list, whether or not they open chat) — documented as an open
architecture question rather than declared fixed.
All four fixes verified end-to-end against the live talkhub/matrix
TLS deployment (invitation accept -> joined_rooms; duplicate insert ->
generic 500). Test data from QA round 3 and this verification pass
cleaned up (orgs, users, Matrix accounts erased, rooms purged).
Not addressed: BUG-20 (predictable Matrix IDs undermine the BUG-2
user_directory mitigation), BUG-5/6/7/9/10/13/14/15/16/17/21/22.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
Nginx Proxy Manager now terminates TLS for matrix.perlbach24.de and
talkhub.perlbach24.de with real Let's Encrypt certs. talkhub's backend
.env updated (SYNAPSE_PUBLIC_BASE_URL, APP_BASE_URL). Full chat flow
re-verified end-to-end against the public HTTPS domains: login, token
mint, room resolve, message send — all 200s, event id returned.
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
Frontend: matrix-js-sdk client per opened team, /sync against Synapse
directly (no chat traffic proxied through the backend), timeline with
day-grouping and read receipts, history backfill, lazy-loaded chat tab
in TeamsPage's team detail view (keeps matrix-js-sdk out of the initial
bundle).
Backend: POST /api/chat/token mints/caches a short-lived per-member
Matrix access token via Synapse's admin puppet-login (1h TTL, in-memory
cache with 5min pre-expiry refresh, concurrent-request dedup, audited
on real mints only); GET /api/chat/room resolves a team's Matrix room.
Cached tokens are invalidated immediately on member suspend/leave.
Fixes found while wiring this up:
- createRoom() now explicitly sets history_visibility=shared (PROJ-9)
instead of relying on the private_chat preset's implicit default.
- POST /api/organizations never provisioned a Matrix account for the
org creator (only the invitation-accept path did) — they'd have
seen "Chat wird eingerichtet" forever. Now enqueues the same
provision_matrix_user job invitations use.
Open: SYNAPSE_PUBLIC_BASE_URL needs documenting in backend/.env.example
(blocked by local permission rules on that file — added manually on
the talkhub test host instead), per-tile unread counts need an
app-level Matrix client instead of one per opened team, and Synapse
needs TLS + public reachability before this works outside a LAN.
Verified end-to-end on the talkhub/matrix test LXCs: registered a
throwaway account, created org → department → season → team, joined
the team, minted a token, sent a message directly to Synapse, read it
back from room history.
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
findMembership() and GET /api/organizations/mine ignored members.status —
a suspended or left member kept full API access to the organization (member
list, teams, roles, audit log) and still saw it in the org switcher, even
though only their Matrix account was deactivated. Both now require
status = "active".
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>
BREAKING CHANGE: Removed FEATURE_CHANGELOG.md and test-reports/
**What changed:**
- All 6 agents now use Git commands to check existing features/components
- Feature specs include test results and deployment status (no separate files)
- Git commits are the single source of truth for implementation details
- Git tags for deployment versioning (e.g., v1.0.0-PROJ-1)
**Why:**
- Prevents context bloat (no growing FEATURE_CHANGELOG.md)
- Scales better (Git is built for large projects)
- No manual changelog maintenance
- Better developer experience (native Git workflow)
**Migration:**
- Requirements Engineer: Uses `ls features/` and `git ls-files` instead of FEATURE_CHANGELOG
- Solution Architect: Uses `git ls-files src/components/` to check existing code
- Frontend/Backend Devs: Use `git log --grep="PROJ-X"` to see feature history
- QA Engineer: Adds test results directly to feature spec
- DevOps: Updates feature spec with deployment status + creates Git tags
**Files changed:**
- Updated all 6 agent instructions (.claude/agents/*.md)
- Deleted FEATURE_CHANGELOG.md
- Deleted test-reports/ folder
- Updated PROJECT_CONTEXT.md (removed references)
- Updated README.md (v1.4.0, updated workflow)
- Updated features/README.md (new format with QA + Deployment sections)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>