Make server failures debuggable without leaking secrets:
- configurable stdout log level (config.yaml logging.level + OWNCORD_LOGGING_LEVEL)
- preserve the DB cause in ErrInternal wraps; log auth-DB failures distinctly
from bad tokens; log the previously-silent expired-session cleanup goroutine
- route HTTP handler panics through slog (was chi stderr-only, invisible to
the admin log stream)
- stackutil: argument-free panic stacks so key/token bytes never reach the
admin ring buffer / SSE; slog.LogValuer redaction on VoiceConfig/GitHubConfig/
GIFConfig/Config and db.User/db.Session
- logctx: req_id/trace_id correlation on ...Context log calls
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.
- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
Context variants; the four redundant ctx-less passthroughs removed.
db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
RequireChannelAccess) and the service.Store interface mirror the new
signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
the connection ctx via DispatchV2; hub loops and startup wiring use
context.Background(); service methods thread ctx where they have one
and Background where no ctx exists. Public service surface reached by
ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
RequireChannelAccess, message/dm/block/invite/profile methods) is now
ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
invariant, found by a 3-lens adversarial review of the diff:
* voice-leave background retries (a dead webhook/connection ctx killed
retry 2 before it ran, leaving ghost capacity-holding voice rows)
* rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
* post-2FA-change DeleteOtherSessions and logout DeleteSession (the
security tail of a committed change must not die with the request)
* all api/ws audit writes (a banned user could suppress their own
login_blocked_banned row by aborting the request mid-bcrypt)
* admin backup VACUUM INTO (an interrupt left a truncated .db that
the backup list presented as restorable)
* post-commit message/edit refetches (a committed message must still
fan out when the sender disconnects)
* hub settings-cache refresh (one dead connection could pin stale
values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
config with justification (generated source must stay world-readable)
instead of flipping genprotocol output to 0o600.
Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nhooyr.io/websocket now resolves to github.com/nhooyr/websocket-old and its
README is a one-line deprecation pointing at coder/websocket. Its last three
releases (v1.8.15-17) all shipped on 2024-08-10 as the redirect; the fork has
shipped through 2026-06-15.
The version number decreases (v1.8.17 -> v1.8.15) because both paths tagged in
the same space, but the coder release is ~2 years newer. Import path only; the
9 API symbols used are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route the four "must mirror" READ_MESSAGES filters — REST
ListVisibleChannels, ws buildReady, reconnect replay
computeAllowedChannels, and hub RefreshChannelVisibility — through a
single permissions.Checker predicate so a drift can never leak a
private channel (audit A-2026-07-07 / backlog item 3).
- add permissions.Checker.VisibleChannelIDs + ChannelRef (skips dm,
fails closed, admin bypass via HasChannelPermBatch)
- delegate the three batch sites; RefreshChannelVisibility uses
HasChannelPerm instead of its inline EffectivePerms copy
- REST/WS agreement test asserting all three sites yield the identical
non-DM set across admin / member-with-deny / denied-everywhere
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit writes stay best-effort — a LogAudit failure must never fail or abort
the request — but a failed write must no longer be silently discarded. Add
db.WriteAudit(auditor, actor, action, targetType, targetID, detail), which
logs a failed write with actor/action/target context (never the detail
string, which may be sensitive) and never propagates the error.
The Auditor interface is satisfied structurally by both *db.DB and the
service-layer Store, so api/admin/ws/service all reach the helper without an
import cycle. Converts all ~26 call sites from `_ = LogAudit(...)` (and the
two backup handlers' inline `if err` blocks) to db.WriteAudit. Pinned by
db/audit_test.go: failure logged and not propagated, success logs nothing,
detail never leaks.
Resolves the repo-wide LogAudit policy question flagged by the D8 note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the two highest-impact gaps from the client UX spec.
Optimistic send:
- messages.store gains addOptimisticMessage / markSendFailed /
removeOptimistic, and confirmSend now stamps the real id + "sent" on
the ack. addMessage reconciles the broadcast by real id (idempotent,
replay-safe) with a defensive author match, so an echo never
duplicates. Message gains status/correlationId/errorCode.
- ChannelController.performSend renders a pending row immediately and
supports retry / delete-draft (retry preserves attachments).
- MessageList renders pending (dimmed) and failed (reason + Retry /
Delete) rows; the hover action bar is limited to confirmed rows.
- Failures are precise: the server echoes the request id on error
replies (buildErrorMsgWithID), so the dispatcher maps SLOW_MODE /
FORBIDDEN / RATE_LIMITED / BAD_REQUEST to the exact row instead of
dropping the code. An offline send is shown failed, not silently lost.
Composer permission + connection gating:
- The server computes an authoritative per-channel can_send in the ready
payload (channelCanSend mirrors MessageService.checkSendPermission:
READ|SEND, MANAGE_MESSAGES for announcement, admin bypass, channel
overrides). channels.store carries it as Channel.canSend.
- MessageInput gains a disabled-with-reason mode; ChannelController
derives the reason from can_send + channel type + connection status and
disables the composer reactively (announcement read-only, no-permission,
reconnecting) rather than accepting a click and failing. Older servers
that omit can_send default permissive.
Docs: the corresponding "Current gap" callouts in docs/architecture/ux
are updated to reflect the implementation.
Verified: full server suite + client tsc + 3204 unit tests + lint + gofmt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Make 'announcement' a real channel type, resolving the contradiction where
it was documented and offered by the admin API but hard-rejected by the
migration-013 DB triggers.
Model: announcement channels are readable like text channels (same
READ_MESSAGES visibility), but posting is restricted to users with
MANAGE_MESSAGES — no new permission bit, migration, or client permission
plumbing needed.
Server:
- migrations/016: recreate the channel-type triggers to allow
text/voice/announcement/dm.
- service/message.go: checkSendPermission now takes the channel type and
rejects posts to announcement channels from users lacking MANAGE_MESSAGES
(SendMessage + CanPost paths). Added a service test.
- Unread counts: ready-payload builder (ws/serve.go) and
GetChannelUnreadCounts (db) now include announcement channels alongside
text, so they track unread/last-message like text channels.
Client:
- ChannelSidebar renders announcement channels with a megaphone icon
(added to the icon set) instead of the '#' text prefix; they otherwise
behave like text channels (already typed in ChannelType).
Specs + trackers (api.md, protocol.md, schema.md incl. migration 016,
architecture/data-model.md, audit A-2026-07-01, decisions D1) updated.
Verified: go build ./...; go test ./service ./db ./ws ./api ./admin;
sqlc-verify; client tsc + oxlint + prettier clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Protocol codegen (decision D4, audit A-2026-07-08):
- Add docs/protocol-schema.json as the real single source of truth for
WS message-type constants, making the long-standing 'generated from'
comment in both constant files true.
- Add Server/scripts/genprotocol, a generator emitting both
Server/ws/message_types.go and Client .../lib/protocolTypes.ts
(constants byte-for-byte value-identical to before; only headers,
ordering alignment, and provenance comments changed).
- Add make protocol-generate / protocol-verify and wire protocol-verify
into CI next to sqlc-verify.
Quick wins (decision D8):
- admin: log LogAudit write failures in the backup handlers instead of
discarding them (prior audit #10).
- api: fix self-contradictory upload Cache-Control to 'private,
no-cache' per remediation plan W3-4; drop the now-unused
fileCacheMaxAgeSeconds constant; update test.
- ws: route the hub settings cache through db.GetSetting instead of
inline SQL.
- ws: fix a latent data race — main.go wires SetEventPersister and
SetEventStore after NewRouter has already started the hub Run loop,
which reads those fields on the broadcast/replay paths. They (and
pluginSink, which one test sets post-Run) are now atomic pointers;
the remaining pre-Run-only setters reject late calls with an error
log instead of racing silently.
Update the audit closure table and decisions doc statuses accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Three review findings on the #93 feature:
- RefreshChannelVisibility targeted clients by their connect-time role
snapshot; a user whose role changed mid-session was evaluated against the
stale role. Resolve the current role from the DB per client (fail closed).
- Visibility updates are targeted, unsequenced messages, so a client that
disconnected before an override change and later resumed via replay never
converged (stale sidebar until a fresh connect). Track a visibility-change
sequence watermark and force resumes from at/before it onto the
full-ready path.
- The admin SPA interpolated channel/user names into single-quoted JS
strings inside onclick attributes with HTML-escaping only; a name
containing a quote broke out of the string literal (XSS in the admin
panel, reachable by any user allowed to create channels). Add a jsq()
helper (JS-escape then HTML-escape) and use it for every onclick name
interpolation.
Follow-up to #93.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
readPump teardown always ran handleVoiceLeave, trusting the joined_at
guard in LeaveVoiceChannelIfMatch to protect replacement sessions. On
reconnect the voice session TRANSFERS to the replacement client with
the same joined_at, so the guard cannot tell the two apart: whenever
teardown snapshotted voiceChID before the transfer zeroed it, the old
connection deleted the replacement's voice_state row (flaked on the
Windows CI runner as TestServeWS_Reconnect_PreservesVoiceState).
Gate voice cleanup on !replaced — the same condition the
presence-offline broadcast four lines down already uses. A genuinely
final disconnect behaves exactly as before, and a stale row from a
crashed replacement is still swept by the fresh-connect cleanup.
Also deflake TestHub_ConcurrentRegisterUnregister: poll for quiescence
with a deadline instead of a fixed 50ms sleep that loses to the -race
scheduler on slow runners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final in-sandbox completeness pass. Five focused pieces; the remaining
items in PHASE_BC_LOCAL_TODO.md after this commit are all genuinely
local-only (toolchain, network, native deps).
Test coverage (the biggest gap from prior reviews)
- Server/plugin/manifest_test.go — pluginNameRegexp accept/reject table,
validateRelativePath table, oversized version, unknown permission.
- Server/plugin/host_http_test.go — hostAllowed dot-boundary suffix,
empty-entry rejection, case insensitivity, FQDN trailing dot. ipAllowed
table over loopback, RFC1918, RFC4193 (ULA), RFC6598 (CGN), link-local,
multicast, unspecified — both v4 and v6 — plus public-IP accept cases.
- Server/plugin/loader_test.go — rejectSymlinksUnder catches direct and
nested symlinks; scanPluginDirectory rejects a plugin whose entrypoint
is a symlink. Skipped on Windows where symlink creation needs elevation.
- Server/plugin/host_ui_test.go — AssetHandler serves declared files,
rejects undeclared files (404), rejects path traversal, supports nested
asset paths.
- Server/ws/hub_seedseq_test.go — SeedSeq monotonic, never-backwards,
concurrent CAS safety, integration with nextSeq.
- Server/ws/extract_event_type_test.go — table covering happy paths,
control char rejection, escaped quote rejection, length cap (64),
empty/missing/non-JSON inputs.
Plugin install endpoint (closes a real feature gap)
- Server/plugin/registry.go — InstallFromZip extracts a plugin .zip into
a staging directory under cfg.Directory, validates it zip-slip safe
(cleaned-path Rel check), refuses non-regular entries, refuses
symlinks, caps compressed at 16 MiB and uncompressed total at 64 MiB
(each file gated by io.CopyN against the remaining budget). Manifest
is parsed at the staged root, then atomically renamed into the
canonical plugin directory and registered via the existing
installFromDisk path.
- Server/api/plugins_handler.go — POST /install accepts multipart with
one "plugin" file part, http.MaxBytesReader caps the request body,
io.LimitReader caps the in-memory buffer, calls Registry.InstallFromZip,
returns 201 with the new plugin name. The endpoint inherits the Pass 2
admin auth + IP gate (mounted under r.Use(admin.RequireAdminAuth)).
Protocol surface
- Server/ws/serve.go — buildAuthOK now takes replaySource and includes
it in the auth_ok payload as "replay_source": "none" | "buffer" | "db".
Two call sites updated: reconnect path passes the existing local,
fresh-connect path passes "none". Test export updated to pass "none".
CI build-tag matrix
- .github/workflows/ci.yml — three new steps inside server-build-test
build the server with -tags otel, -tags wazero, and -tags otel,wazero.
All three are continue-on-error: true until the upstream OTel and
wazero modules land in go.mod (tracked in PHASE_BC_LOCAL_TODO.md).
Once they do, dropping continue-on-error converts the steps into
hard CI gates against tag-boundary drift.
Documentation
- CHANGELOG.md — new root-level file with curated entries for Phase B,
Phase C, security, and behavioural changes operators must know about
(notably event_persistence.enabled = true by default).
- PHASE_BC_LOCAL_TODO.md — ticks off the install endpoint, the
replay_source field, and the existing event_persistence defaultYAML
entry. The remaining items are toolchain-bound.
After this pass, the in-sandbox completeness ceiling is reached.
Everything still pending requires Go 1.25 toolchain, npm install,
real OTel SDK + wazero modules, sqlc, postgres backend impl, or
tinygo.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Client now has three send channels:
- sendHigh (64 slots): DMs, mentions — drained first by writePump
- send (256 slots): chat messages, reactions — drained second
- sendLow (64 slots): typing, presence — drained last, dropped on overflow
writePump drains high-priority messages before checking normal/low.
PubSub gains PublishHigh/PublishLow alongside existing Publish.
EmitEvents routes events by priority:
- High: SequencedDMEvent, UserTargetedEvent
- Normal: ChannelEvent, VoiceChannelEvent
- Low: ExcludeSenderEvent (typing), PresenceEvent
Slow clients get typing/presence dropped first (sendLowMsg silently
drops), then disconnect on normal buffer overflow, ensuring DMs are
never lost to typing indicator backpressure.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
- Restrict PTT key capture to non-text keys only (function, navigation, mouse buttons) via allowlist (BUG-136)
- Gate DevTools button and F12/Ctrl+Shift+I shortcut behind import.meta.env.DEV
- Disable devtools Tauri feature in production (Cargo.toml default feature removed)
- Remove overly broad http:default capability, replace with scoped http:allow-fetch
- Set withGlobalTauri to false to avoid global __TAURI__ surface exposure
- Fix reconnect race: add abort checks after room creation, URL resolve, and connect (BUG-070)
- Fix ws.ts reconnect guard: bail out safely when config is null after disconnect
- Fix DM broadcast double-send and add monotonic seq + replay buffer support via sendSequencedToUsers
- Add seqMu mutex to serialize seq assignment across broadcastDM and deliverBroadcast paths
- Fix handleFreshConnect to unregister client and close connection on buildReady failure
- Add tests for PTT allowlist, ws reconnect config-null guard, livekit abort-after-connect, and DM sequencing
M1 — TOTP secrets are now AES-256-GCM encrypted before being stored in
the database. Key is auto-generated on first run (data/totp.key) or set
via OWNCORD_TOTP_KEY env var. Existing plaintext secrets are detected
and returned as-is for backwards compatibility.
M3 — Replay buffer events are now tagged with their channel ID. On
reconnect, the server computes the user's current accessible channels
and only replays events from those channels. Global broadcasts (presence,
voice state, member updates) are always replayed. Falls back to full
ready payload if permission computation fails.
BUG-123: Register client BEFORE writing replay/ready data so broadcasts
during the write window queue in the send buffer instead of being lost.
On handshake failure, unregister before closing.
BUG-124: sendMsg/trySendMsg now close the send channel on buffer
overflow, forcing a disconnect → reconnect with replay recovery
instead of silently dropping messages and diverging state.
Voice states were loaded with GetAllVoiceStates across the entire server,
leaking who was in hidden voice channels. Now voice states are filtered
through the visible channel set before inclusion in the ready payload.
Updated tests to use explicit roles where voice state visibility matters.
If GetRoleByID fails or returns nil during WebSocket connect, the
server now disconnects the client instead of serving a permissive
ready payload with all channels visible. In buildReady, nil role is
now treated as zero-access (no channels) instead of full-access.
Updated tests to pass explicit Owner role where channel visibility
is expected.
DM channel rows were returned by ListChannels and included in both
the REST channel list and the WebSocket ready payload. Added type="dm"
skip in handleListChannels and buildReady filter loops. DMs are already
delivered separately via dm_channels. 2 new tests verify exclusion for
both member and admin roles.
- BUG-084: Broadcast filter now delivers channel messages to unfocused
clients (channelID==0) instead of silently dropping them
- BUG-086: leaveVoiceChannelWithRetry retry goroutine respects context
cancellation and hub stop to prevent leaks on shutdown
- BUG-088: Voice channel switch verifies old state is cleared before
joining new channel, preventing capacity bypass on DB failure
- BUG-089: handleFreshConnect RemoveParticipant goroutine checks hub
stop and documents identity-based targeting safety
CRITICAL (5):
- Hub panic recovery now calls h.Stop() after 3 panics (ws/hub.go)
- Ring buffer EventsSince returns non-nil empty slice for current seq (ws/ringbuffer.go)
- PTT event listener stores unsubscribe handle to prevent leak (ptt.ts)
- verifyTotp respects config.allowSelfSigned instead of hardcoding (api.ts)
- ptt_listen_for_key uses spawn_blocking to avoid thread pool starvation (ptt.rs)
HIGH - Server (13):
- TOTP rate-limit checked after body decode; counters reset on success
- TOTP enable returns 409 if already enabled (must disable first)
- Global search pre-computes accessible channel IDs for FTS WHERE clause
- DeleteAccount queries roles by name instead of hard-coded IDs
- BackupToSafe uses absClean in VACUUM INTO
- Voice camera slot uses atomic EnableCameraIfUnderLimit DB method
- readPump snapshots voiceChID before unregister for TOCTOU safety
- Voice join sets state after token send; rollback takes broadcast flag
- Updater download uses probe pattern instead of overflow write
- Webhook checks Authorization header before reading body
- Storage.Save adds fsync and fixes double-close
- Default WS origin denies cross-origin (was: accept all)
HIGH - Client (6):
- WS reconnect uses generation counter to discard stale events
- AudioPipeline uses generation counter against stale worklet callbacks
- Screenshare mute state preserved across reconnect (not full leave)
- handleVoiceToken uses iterative loop instead of unbounded recursion
- store.ts re-entrancy guard with pending update queue
- Notification AudioContext cleaned up on logout
Reviewed by 4 parallel agents across Server Core, Server Realtime,
Client & Tauri, and Security. 55 total findings; 24 CRITICAL+HIGH
fixed here, 31 MEDIUM+LOW tracked in vault backlog (T-265–T-295).
- Video grid: sync stream type attribute on updates, add screenshare data attribute
- Dispatcher: handle voice_token messages, improve video track event handling
- LiveKit session: add video track publication support
- Hub: stale client timeout cleanup, improved voice state management
- Voice join/leave: context propagation, better error handling
- Livekit webhook: structured event handling with room/participant data
- Server DB: voice query improvements, new test coverage
- WS integration tests: expanded coverage for voice and LiveKit flows
- Gitignore: add internal dev tools directory, owncord-server.exe
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task A: REST API endpoints in api/dm_handler.go — POST /api/v1/dms
(create/get DM channel), GET /api/v1/dms (list open DMs), DELETE
/api/v1/dms/{channelId} (close DM). Routes registered in router.go.
Task B: WebSocket DM routing — handleChatSend, handleChatEdit,
handleChatDelete, and handleReaction all check channel type and use
participant-based auth for DM channels instead of role permissions.
DM messages delivered via SendToUser to each participant (bypassing
channel-subscription model). Auto-reopens DM for recipient on new
message with dm_channel_open event. New broadcastToDMParticipants
helper. New WS event types: dm_channel_open, dm_channel_close.
Task C: Ready payload includes dm_channels from GetUserDMChannels.
Also adds GetDMParticipantIDs helper to db/dm_queries.go.
Server:
- Fix YAML injection in LiveKit config generation (quote values)
- Revert token TTL to 4h (no server-side JWT revocation)
- Derive LiveKit publish permissions from user role (prevent SFU bypass)
- Add CAS guard for webhook/voice_leave race condition
- Add voice_leave broadcast to rollbackVoiceJoin (prevent ghost state)
- Limit webhook body to 64KB (prevent memory abuse)
- Add rate limit to voice_token_refresh handler (1/60s)
- Add LiveKit health check endpoint (GET /api/v1/livekit/health, 503 on degraded)
- Add voice_token_refresh WS handler for client-initiated token refresh
- Consolidate voice quality constants (single source of truth)
- Fix video limit TOCTOU race (count from DB instead of LiveKit API)
- Raise default voice_max_video from 10 to 25 (Discord parity)
- Add CountActiveCameras DB query
- Non-blocking broadcast send, circuit breaker, exponential backoff
- Close send channel before context cancel in serve.go
- Guard voice mute/deafen for active channel
- Delete orphaned message on attachment link failure
- Redact query string from proxy logs (prevent token leak)
- Use instance-level HTTP client for health checks (no redirect following)
- Set cmd.WaitDelay to prevent goroutine leak on Windows
- Log buildJSON marshal errors
Client:
- Refactor livekitSession.ts from singleton module to LiveKitSession class
- Share single AudioContext for all analysers (was 1 per participant)
- Extract createRoom() helper (DRY)
- Add token refresh timer (3.5h interval, re-arms on failure)
- Skip setSpeakers if unchanged (sort in-place, no allocations)
- Distinguish user-initiated leave from connection error in retry
- Add YouTube videoId validation (prevent iframe src injection)
- Add try/finally to disableCamera
- Wrap store subscription callbacks in try/catch
- Track and cancel initial scroll RAF on cleanup
- Add 5s timeout + encodeURIComponent to YouTube oEmbed fetch
- Clean raw mic stream on RNNoise suppressor failure
- Full voice cleanup on logout via cleanupAll()
Tests:
- Add 7 new server tests (webhook parsing, voice guards, quality fallback)
- Fix 2 pre-existing test failures (mute/deafen invalid payload)
Server now checks IsUserConnected before accepting a new WebSocket and
returns an auth_error with a clear message instead of silently replacing
the old session. Client dispatcher surfaces the error via transient UI
state so the ConnectPage can display it.
- Add cert mismatch modal for TOFU certificate pinning
- Separate voice channels from text in sidebar with user lists
- Implement scrollToMessage and jump-to-pinned-message in overlay
- Add server profiles with credential auto-fill on connect page
- Fix credential auto-fill race condition on rapid profile clicks
- Add channel_focus event for channel-scoped message delivery
- Fix member list case-insensitive role filtering
- Server normalizes role names to lowercase for protocol consistency
- Remove redundant permission-denied log in handleChannelFocus
- Voice store: bulk set states from ready payload, leave cleanup
- WebSocket reconnect and structured logging improvements
- Add tests for cert modal, overlay managers, voice sidebar,
message list scroll, quick switcher, and profile management
Server fixes:
- Move ATTACH_FILES permission check before CreateMessage to prevent
orphaned messages on permission denial
- Fix hardcoded /api/files/ URL to /api/v1/files/ per spec
- Add error logging for GetAttachmentsByMessageIDs failure
- Set 1MB WebSocket read limit to match client-side limit
- Extract requireChannelPerm helper, replacing 8 repeated patterns
Client fixes:
- Wire onUnauthorized callback to clear auth on 401 responses
- Store auth token in authStore before WS connect
- Reset WS state to disconnected when Tauri APIs unavailable
- Add connectivity guard and 200ms send debounce on message send
- Add toast container to MainPage with error feedback on 5 API failures
- Clear voice currentChannelId on server-driven voice_leave for current user
- Apply stored theme/font/compact preferences at app startup
- Fix infinite scroll throttle to use store subscription instead of fixed timer
Tests:
- Add TestChatSend_AttachmentsDeniedNoMessageCreated
- Add attachments table to handler test schema
- Critical #1: Add READ_MESSAGES permission checks to channel_focus,
GET /channels, GET /messages, and GET /search
- Critical #2: Send type "auth_error" instead of "error" with AUTH_ERROR
code, preventing infinite client reconnect loops
- Critical #3: Replace role_id (number) with role (string name) in
member_join, auth_ok, and ready payloads via JOIN on roles table
- Critical #4: Always include attachments field (empty array) in
chat_message broadcasts to prevent client crash
- High #2: Add /api/v1/health endpoint alongside /health
- Medium #1: Handle ping WS messages with pong response
- Redesign ConnectPage with modern dark theme, profile cards with delete buttons, login/register toggle
- Add DPAPI-encrypted password saving with "Remember my password" checkbox
- Fix permission bit constants to match SCHEMA.md (Member role 0x663)
- Add migration 004 to fix existing Member role permissions
- Add comprehensive audit logging across all server packages (auth, admin, ws, setup)
- Add member_join WebSocket broadcast so new users appear in members list in real-time
- Add host URL normalization (strip scheme prefix) for reverse proxy compatibility
- Add REST API client, ChatService orchestrator, WebSocket service with reconnection
- Add model types (WsEnvelope payloads, API responses), converters, tests