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).
- C-3: inject setupLimiter into NewAdminAPI instead of package-level global
- H-5: generateRandomKey returns error instead of panicking
- H-6: replace init() bcrypt with sync.Once lazy initialization
- M-2: remove unsafe-inline from admin CSP
- M-3: sanitize upload filenames (strip control chars, truncate to 255)
- M-5: truncate User-Agent to 512 bytes before storing as device
- M-10: MaxBodySizeUnless uses prefix matching instead of exact path
- M-12: wrap seedExistingDatabase in a single transaction
- M-13: use errors.Is for EOF check in upload handler
- M-14: log writeJSON encoding errors instead of discarding
- M-16: standardize error codes to INTERNAL_ERROR across all handlers
- C-3: inject setupLimiter into NewAdminAPI instead of package-level global
- H-5: generateRandomKey returns error instead of panicking
- H-6: replace init() bcrypt with sync.Once lazy initialization
- M-2: remove unsafe-inline from admin CSP script-src and style-src
- M-3: sanitize upload filenames (strip control chars, truncate to 255)
- M-5: truncate User-Agent to 512 bytes before storing as device
- M-10: MaxBodySizeUnless uses prefix matching instead of exact path
- M-12: wrap seedExistingDatabase in a single transaction
- M-13: use errors.Is for EOF check in upload handler
- M-14: log writeJSON encoding errors instead of discarding
- M-16: standardize error codes to INTERNAL_ERROR across all handlers
Move handleVerifyTOTP, handleEnableTOTP, handleConfirmTOTP, and
handleDisableTOTP along with their request/response types into a
dedicated totp_handler.go file, bringing auth_handler.go from 829
to 583 lines.
Add audit log calls for TOTP lifecycle events:
- totp_verified on successful 2FA login verification
- totp_enabled on successful TOTP enrollment confirmation
- totp_disabled on successful TOTP removal
Cover low-coverage handler functions: handleGetPins, handleSetPinned,
handleMetrics, handleDiagnosticsConnectivity, isPrivateIP, AdminIPRestrict,
handleClientUpdate, and handleLiveKitHealth. Adds 30 new test cases across
4 new test files and 2 modified test files.
Comprehensive tests for handleUpload and handleServeFile covering:
- Route mounting verification
- Successful text and PNG uploads with response validation
- Authentication enforcement (missing/invalid tokens)
- Missing file field and invalid multipart form
- Blocked file types (PE, ELF, Mach-O, shell scripts)
- DB record creation and unlinked message_id
- File serving with correct Content-Type, Cache-Control, Content-Disposition
- File not found (missing DB record and missing storage file)
- CORS headers (matching, non-matching, wildcard, no origin)
- Full upload-then-serve round-trip for text and PNG
Coverage: MountUploadRoutes 100%, handleUpload 78.6%, handleServeFile 83.9%
- Add auto-login toggle on server profiles (lightning bolt icon, single-server
enforcement). On startup, auto-connects with saved credentials; falls back
to login form on failure or 2FA.
- Add online_users field to /api/v1/health endpoint (Go server) and display
user count on server cards in the login page.
- Add periodic health check retry every 15s while on ConnectPage so offline
servers update when they come back online.
- Move DM section above text channels in sidebar, limit to 3 visible DMs
with "View all messages" button for overflow.
- DMs with new messages bubble to top of the list automatically.
- Add total unread badge on DM header (visible even when collapsed).
- Member list collapses to just header bar; state persisted across sessions.
Security fixes (from multi-reviewer code review):
- Add DM participant auth checks to channel_focus, typing, and REST
message endpoints — prevents unauthorized access to DM channels
- Fix TOCTOU race in GetOrCreateDMChannel using IMMEDIATE transaction
- Validate YAML credentials before LiveKit config interpolation
- Add CSS variable injection prevention in custom theme loader
- Validate localStorage JSON before unsafe type casts
LiveKit stability:
- Track remote mic audio elements for cleanup on abnormal disconnect
- Remove duplicate token refresh timer scheduling
- Add .catch() to all floating applyMicMuteState promises
- Clear reconnectAc after async post-connect work completes
- Fix double cmd.Wait() race in LiveKit process Stop()
- Reorder voice_join guards: validate channel before livekit==nil check
- Add startup warning for external LiveKit webhook CIDR mismatch
DM system fixes:
- Emit dm_channel_close WebSocket event from REST close handler
- Re-open DM for caller when channel already exists
- Fix unread count incrementing for own messages and active DMs
- Reset channelBeforeDm after Back navigation (stale state bug)
New feature:
- Voice call duration timer in VoiceWidget (MM:SS / HH:MM:SS elapsed)
- Accent color restored on app startup (was only applied in settings)
Test infrastructure:
- Add DM tables to all test schemas (hubTestSchema)
- Inject test LiveKit client in voice handler tests (fixes 28 failures)
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.
- Deafen now also mutes the local microphone (privacy fix)
- Mute/deafen zero the audio pipeline GainNode to guarantee silence
when the replaced sender track bypasses LiveKit's track disable
- Send voice_leave on failed auto-reconnect to prevent ghost states
- Gate voice_join on LiveKit availability (reject if h.livekit == nil)
- IP-restrict /api/v1/livekit/webhook to admin CIDRs
- Warn at startup when LiveKit API keys are auto-generated (ephemeral)
- Fix connecting guard: move pending-join dispatch outside finally block
- Add volume slider + mute button overlay on remote video tiles
- Document token refresh limitation for 4h+ sessions
- Fix TS2306 in rnnoise-worklet test (ts-expect-error for worklet import)
Server fixes:
- voice_leave: broadcast voice_leave even on DB error so peers don't see ghost users (H1)
- migrate: record migration inside transaction for atomicity (H2)
- password: use init() with panic for dummyHash to catch bcrypt init failures (M1)
- password: replace //nolint:errcheck with explicit _ = discard (M2)
- handlers: clarify edit permission comment, fix error message wording (M3)
- auth_handler: distinguish duplicate username (400) from DB error (500) (M4)
Client fixes:
- media: revert YouTube oEmbed to browser fetch — no need to disable cert verification (C1)
- messages.store: fix prependMessages cap to keep newest messages, not oldest (H5)
- ChannelSidebar: ref-count globalDragAc to prevent multi-instance teardown race (H6)
- attachments: replace console.error with project logger (M6)
- ws: clarify lastSeq reset comment to match actual behavior (M5)
CRITICAL:
- Add AuthMiddleware + rate limiting to /livekit/* proxy route (was unauthenticated)
- Remove well-known default LiveKit credentials from source; auto-generate unique
random keys on first run so voice works out of the box securely
- Reject the old "devkey"/"owncord-dev-secret-key-min-32chars" in NewLiveKitClient
HIGH:
- Add 5s timeouts to RemoveParticipant/ListParticipants SDK calls (goroutine leak)
- Fix config.yaml default file permissions from 0644 to 0600
- Fix voice store desync on unexpected LiveKit disconnect (phantom UI state)
- Add in-flight guard to handleVoiceToken (race on rapid channel switch)
- Fix lightbox listener leak on rapid reopen (orphaned mousemove/mouseup/keydown)
- Fix allTracked WeakRef set unbounded growth in media-visibility
- Replace debug console.log with createLogger in embeds.ts
- Stop persisting password in Windows credential blob (only token needed)
Server: suppress errcheck on deferred Close() calls, discard
resp.Body.Close error, remove unused voiceSpeakersPayload type
and buildVoiceSpeakers func.
Client: add unit tests for os-motion, livekitSession, and
safe-render to bring coverage from 74.16% to 76.06% (threshold 75%).
- Add LiveKit health check guard to voice_join (reject if process not running)
- Log webhook DB cleanup errors instead of swallowing
- Reject camera enable if CountActiveCameras query fails
- Add LiveKit health status to /metrics endpoint
- Add 13 unit tests for livekit.go, livekit_process.go, livekit_webhook.go
Server:
- Extract image width/height on upload via image.DecodeConfig (header-only)
- Add width/height columns to attachments table (migration 007)
- Include dimensions in AttachmentInfo JSON (optional, backward-compatible)
Client:
- Reserve exact space for images using server-provided dimensions
- Fallback min-height: 200px for external/old images, cleared on load
- Remove premeasureAll() — was caching wrong heights for unloaded images
- Smart per-type height estimates: 32px dividers, 42/72px text, +220px
per image attachment, +320px for YouTube embeds
- Batch ResizeObserver corrections to single RAF with anchor-based scroll
preservation (topmost visible item stays in place)
- Fenwick tree for O(log n) offset lookups (replaces O(n) linear scans)
- CSS contain: layout style on .msg-image to isolate layout shift
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)
Architecture:
- Add subscribeSelector to store.ts for selective state subscriptions
- Create reconcileList utility for DOM list patching without rebuild
- Create shared createContextMenu utility (dedup 3 files)
- Convert all 20 subscribe() calls to subscribeSelector across 11 files
- Split renderers.ts (1131L) into 7 focused files by concern
- Split ConnectPage.ts (838L) into ServerPanel + LoginForm + shell
- Fix ineffective (s) => s selector in ChannelSidebar
GIF visibility:
- Add media-visibility.ts with IntersectionObserver + canvas snapshots
- GIFs auto-pause after 10s, play/pause button overlay on hover
- Freeze GIFs on scroll-away, window blur, and minimize
- Wire into media.ts, attachments.ts, embeds.ts renderers
Tests: 46 new tests (1073 total), all passing
Net: -1166 lines across client codebase
- Add reverse proxy at /livekit/* that forwards to LiveKit server
- Server sends relative URL "/livekit" in voice_token; client
resolves to wss://server:port/livekit using known server host
- Fix API secret minimum length (32 chars required by LiveKit)
- Pass TLS config to LiveKit process manager for TURN certs
- livekit_webhook.go: HTTP handler for LiveKit participant_joined/left
events with JWT verification; syncs stale voice state on crash recovery
- Mount webhook at POST /api/v1/livekit/webhook in router
- Speaker detection handled client-side via LiveKit SDK events
Client:
- Wire up all 4 notification toggles (desktop, taskbar flash, sounds, @everyone)
- Add compact mode CSS with visible layout differences
- Add GIF picker with Tenor API (trending + search)
- Render inline images/GIFs instead of link previews for direct URLs
- Add push-to-talk via Rust GetAsyncKeyState polling (non-consuming)
- Add key capture UI for PTT keybinds (supports mouse buttons)
- Add error/success feedback on account settings (password, username)
- Fix mic stream cleanup on tab switch (VoiceAudioTab factory pattern)
- Fix message timestamps using UTC with proper timezone conversion
- Fix emoji reaction picker (was returning early on empty emoji)
- Fix chat scroll jumpiness with Discord-style overflow-anchor + ResizeObserver
- Pre-measure all message heights on load to prevent first-scroll jump
- Improve scrollbar visibility with semi-transparent white thumb
- Show client version on Logs tab
Server:
- Add admin_allowed_cidrs config to restrict /admin to private networks
- Fix voice config defaults lost when YAML section has omitted fields
- Add negative caching to updater (5min error cache)
Admin panel redesign:
- Rebuild frontend from mockup with Discord-style dark theme
- Stat cards, section cards, role badges, modal system, toast notifications
- All 7 sections: Dashboard, Users, Channels, Audit Log, Settings, Backups, Updates
- Modals replace confirm()/prompt() for all destructive actions
Live server logs (new):
- RingBuffer + MultiHandler tees slog to stdout AND in-memory buffer
- SSE endpoint at /admin/api/logs/stream streams logs in real-time
- Log viewer with level filters (DEBUG/INFO/WARN/ERROR), search,
auto-scroll, pause/resume, copy all, clear
- Color-coded lines by level, source categorization from file paths
Audit log improvements:
- Search filter (actor, action, target, detail)
- Action type dropdown filter
- Copy All and Export CSV buttons
- Instant client-side re-filtering
Console output:
- Switch from JSON to human-readable text format (slog.TextHandler)
- Move startup banner before init logs so it appears first
- NEW-1: Add rows.Err() check in ListMembers to catch cursor errors
- NEW-2: Add minVal parameter to queryInt so offset=0 is not rejected
- NEW-3: Fix copyFile double-close by removing defer, using explicit
close on both success and error paths
- NEW-4: Add GetAllChannelPermissionsForRole batch query, eliminating
N+1 GetChannelPermissions calls in channel list and search handlers
- NEW-5: Cap fetchBody with io.LimitReader(1 MiB) to prevent memory
exhaustion from malformed release assets
- CRIT-2: Prevent HTTP header injection in Content-Disposition via mime.FormatMediaType
- HIGH-1: Call hub.GracefulStop() on server shutdown to clean up WebSocket/voice state
- HIGH-2: Remove double send on serveErr channel that caused goroutine leak
- HIGH-3: Handle GetUserByID error after registration to prevent nil panic
- HIGH-4: Return nil,nil on sql.ErrNoRows in GetAttachmentByID (matching codebase convention)
- HIGH-5: Add voiceDone channel to bound RTP goroutine lifetime on voice teardown
- Fix handleVoiceICE to return VOICE_ERROR when no PC (consistent with other voice handlers)
- Update tests to match corrected behavior
Voice was broken over NAT due to multiple issues across the audio pipeline:
- Fix GainNode silence: WebView2 silences remote WebRTC streams routed through
Web Audio createMediaStreamSource→GainNode→createMediaStreamDestination.
Replaced with direct HTMLAudioElement playback for remote audio.
- Fix NAT traversal: Add Google public STUN server (stun.l.google.com:19302)
so remote clients can discover their public IP for ICE connectivity.
- Fix signaling race: Catch createOffer InvalidStateError when server
renegotiation offer arrives before client's initial offer is sent.
- Fix device switch: Use replaceTrack() instead of removeTrack+addTrack
to avoid SDP renegotiation. Safe rollback on failure (stop old last).
- Fix speaking flicker: setSpeakers skips local user (VAD is sole authority).
- Fix VAD sample rate: Force 48kHz AudioContext instead of system default
(192kHz) which spread FFT bins too wide for voice frequency detection.
- Fix CSP for WASM: Add wasm-unsafe-eval to script-src for RNNoise.
- Fix clearAuth leak: leaveVoice() called before resetVoiceStore().
- Fix ICE rate limit: Separate limit for ICE candidates (50/s vs 20/s).
- Fix stale ICE errors: Silently drop voice_ice with no PeerConnection.
- Fix audio play() race: Deferred to queueMicrotask after DOM attachment.
Debugging infrastructure:
- Logs tab: Copy All button, Voice Diagnostics panel with live session
state, Probe Audio Levels (measures actual signal at 3 pipeline points),
Test Direct Playback button, Copy Diagnostics button.
- Client logging: WebRTC (PeerConnection lifecycle, ICE candidates with
type/address, track events, negotiation), VAD (start/threshold/destroy),
Audio (device acquisition with settings, device changes), noise suppression
(WASM load timing, worklet vs fallback path), voiceSession (remote stream
parsing failures, deafen state, audio element playback events).
- Server logging: SFU init config, voice room mode transitions/track
lifecycle/close, RTP forwarding with packet counts and first-packet
detection, 5s no-packet warning, track fan-out counts, subscriber
transceiver state, ICE candidate details, voice credentials issued.
- Logger: Error objects now serialize .message and .stack instead of {}.
Per-user volume right-click now works on voice user rows in sidebar.
RNNoise ML noise suppression with AudioWorklet + ScriptProcessor fallback.
Tests: 7 new test cases (replaceTrack, setSpeakers skip-local, clearAuth).
- Add tauri-plugin-updater and tauri-plugin-process for in-app updates
- Rust commands (check_client_update, download_and_install_update) build
updater with dynamic endpoint at runtime for self-hosted compatibility
- Server endpoint GET /api/v1/client-update/{target}/{version} translates
GitHub Releases into Tauri updater JSON format with .sig content
- UpdateNotifier banner component with install/dismiss controls
- CI workflow produces signed .nsis.zip + .sig updater artifacts
- Self-signed TLS support via dangerousAcceptInvalidCerts config
- Add file download with native save dialog (Tauri dialog + fs plugins)
- Make attachment filename clickable as additional download trigger
- Add download button with hover styling to file attachments
- Exempt /api/v1/uploads from global 1MB body size limit (MaxBodySizeUnless)
- Add native E2E test suite (8 specs) with Playwright CDP fixture
Server:
- Add POST /api/v1/uploads and GET /api/v1/files/{id} endpoints
- Add CreateAttachment DB method for file upload records
- Allow empty message content when attachments are present
- CORS headers on file serving for WebView2 compatibility
Client — File uploads & attachments:
- Clipboard paste (Ctrl+V) and attach button (+) for file uploads
- Preview bar above input with thumbnail, spinner, and remove button
- Images fetched via Tauri HTTP plugin as base64 data URIs (bypasses
self-signed cert rejection in WebView2)
- Three-layer image cache: memory → IndexedDB → network
- In-flight deduplication prevents duplicate concurrent fetches
- Image lightbox with click-to-zoom, scroll wheel zoom, pan, keyboard shortcuts
Client — URL previews & embeds:
- URLs in messages rendered as clickable links
- YouTube embeds with thumbnail, play button, video title via oEmbed API
- Generic link previews with OG metadata (title, description, image)
- Fetched via Tauri HTTP plugin with Facebook crawler User-Agent
- YouTube title cache and OG metadata cache prevent re-fetch on re-render
- Links open in default browser via tauri-plugin-opener
Client — Voice & audio fixes:
- Mute uses replaceTrack(null) for reliable RTP-level muting in WebView2
- Deafen also mutes mic; undeafen/unmute unmutes both
- Muted users show crossed mic icon, deafened show crossed mic + headphone
- Re-apply mute state after input device switch
Client — UX improvements:
- Disable browser context menu globally (only custom menus show)
- Emoji search now matches by keyword names (smile, heart, fire, etc.)
- Emoji picker closes on click outside
- User bar status text moved below username
- Messages sorted chronologically (oldest first, newest at bottom)
- Scroll to bottom on initial load with deferred retries for layout shifts
- Image attachments constrained to 400x350px with click-to-lightbox
- 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
Fix 70+ errcheck violations by adding explicit error discards (`_ =`)
for unchecked return values across test helpers and deferred Close()
calls. Remove unused `senderID` field from broadcastMsg and unused
`defaultCleanupMaxWindow` const. Apply De Morgan's law, remove empty
branch, and simplify redundant type declaration per staticcheck.