Replace CSS grid-template-columns with a JS layout calculator that
tries every column count and picks the arrangement maximising tile
area while preserving exact 16:9 ratio. ResizeObserver triggers
recalculation on container resize. Tests updated to exercise the
pure computeGridLayout function directly.
- Add hover/focus preview for remote voice users' camera/screenshare
streams in the voice channel sidebar. Preview reuses already-subscribed
LiveKit tracks (zero bandwidth cost). Full-width 16:9 preview with
neon border-glow treatment matching DESIGN.md.
- Fix screenshare focus bug: clicking a screensharing user now correctly
focuses the screenshare tile (userId + SCREENSHARE_TILE_ID_OFFSET)
instead of the camera tile.
- Extract SCREENSHARE_TILE_ID_OFFSET to shared lib/constants.ts (was
duplicated in VideoModeController.ts and MainPage.ts).
- New lib/streamPreview.ts module: attachStreamPreview() with 300ms
debounce, track renegotiation detection, autoplay failure handling,
scroll collapse, keyboard accessibility (focusin/focusout), ARIA
labels, and full AbortSignal cleanup.
- Placeholder shows "Join to preview" with click-to-join behavior.
Live video preview is also clickable to watch the stream.
- 22 new tests (16 stream-preview + 6 channel-sidebar).
Fix multiple bugs across settings tabs (AppearanceTab theme restoration,
AdvancedTab testability, LogsTab refresh, VoiceAudioTab device listing),
harden embeds/media/attachments with cache validation, add logPersistence
rotation logic, and add 8 new test files with expanded test cases for
existing tests. Brings client test coverage from ~68% to ~72%.
- Fix race condition: handleDisconnected no longer nulls the room during
initial connect, allowing the retry loop to complete all 3 attempts
- Fix TLS proxy port: default to 443 instead of 8443 when server host
has no explicit port (servers behind nginx/reverse proxy)
- Fix cert store key: strip :443 suffix so LiveKit proxy fingerprint
lookup matches ws_proxy's stored key format
- Add node_ip config option for LiveKit WebRTC ICE candidates (required
for remote users behind NAT)
- Add resolved URL to all connection error/retry/reconnect logs for
easier debugging
- Add diagnostic logging to resolveLiveKitUrl showing which path was
taken (direct/proxy/passthrough)
Fix accent color not applying on startup due to localStorage key mismatch
in restoreTheme() (was reading "owncord:pref:" instead of "owncord:settings:").
Redesign settings overlay as a centered floating panel with blurred backdrop,
rounded corners, scale animation, and click-outside-to-close.
- Add auto-login feature description (lightning bolt toggle, startup flow)
- Add server health with online_users and 15s periodic health checks
- Update sidebar layout: DMs above channels (3-item preview, bubble to
top, unread badge), collapsible member list with persisted state
- 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.
- Add voice call duration timer to Key Features
- Note accent color is restored on startup (not just settings)
- Add DM authorization critical rule (IsDMParticipant checks)
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)
- Document full 1-on-1 DM implementation with REST endpoints, WebSocket events
- Add unified sidebar architecture replacing 4-column Discord layout
- Document theme system (neon-glow default, custom theme support)
- Add quick-switch server overlay feature (door button)
- Update PROTOCOL.md with dm_create, dm_channel_open/close events
- Add ready payload dm_channels field for initial DM state
- Expand CLAUDE.md key features section with all new implementations
Changes reflect completed session work on sidebar redesign and DM system.
Three DM display fixes:
- Chat header now shows @ prefix and username for DM channels instead of #
- Empty message state shows DM-appropriate welcome text instead of channel welcome
- New DMs are added to dmStore on creation so they appear in the sidebar immediately
Implements the complete client-side DM system:
- DM store with channel list, unread tracking, and CRUD actions
- REST API methods for GET/POST/DELETE /api/v1/dms
- WebSocket dispatcher handlers for ready dm_channels, dm_channel_open,
dm_channel_close, and chat_message DM updates
- Sidebar DM section wired to real dmStore data with unread badges
- DM mode sidebar renders actual conversations from store
- Member picker modal for creating new DMs via API
- DM channel selection adds to channelsStore for ChannelController loading
- Extended ChannelType to include "dm" variant
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.
Add migration 008 with dm_participants and dm_open_state tables.
Add dm_queries.go with GetOrCreateDMChannel, GetUserDMChannels,
OpenDM, CloseDM, IsDMParticipant, and GetDMRecipient functions.
- Add "MEMBERS" category header and drag-to-resize handle on the member
list section with height persisted to localStorage (min 80px, max 40vh)
- Add collapsible "DIRECT MESSAGES" section between channels and members
showing up to 5 online users as DM targets
- Remove members toggle button from ChatHeader (members are always in sidebar)
- Clean up onToggleMembers from ChatHeaderOptions and ChatArea
- Update unit and E2E tests to reflect the removed toggle
Add loadCollapsedCategories/saveCollapsedCategories persistence layer
keyed by server name. toggleCategory now auto-saves to localStorage.
On sidebar mount, collapsed state is loaded for the current server so
each server remembers its own collapsed/expanded sections across sessions.
Build DM conversation list from online members instead of empty array.
Each online member (excluding current user) appears as a DM target with
status indicator and "No messages yet" placeholder. Selection sets the
active DM user and keeps sidebar in DM mode. Sidebar re-renders on
member presence changes and active DM user switches.
Also loads per-server collapsed category state on sidebar mount.
The onSwitch callback now stores the target host in sessionStorage and
calls clearAuth(), which triggers the existing logout flow (voice leave,
WS disconnect, navigate to ConnectPage). On the ConnectPage side, a new
selectServer() method auto-fills the host and loads saved credentials.
The onAddServer callback similarly disconnects and lands on ConnectPage
so the user can add a new server profile.
Mount the MemberList component below the channel list in channel mode,
with admin callbacks (kick, ban, role change) and visibility toggled
via the existing ChatHeader member list button / uiStore subscription.
Tracked as channelModeExtras for proper cleanup on mode switch.
- Fix test regression: update theme option count 3→4 and midnight index [1]→[2] for neon-glow insertion
- Fix stale Voice & Audio tests: update select count 3→4 (video quality + device added), replace sensitivity slider test with meter bar render check
- Consolidate theme systems: applyTheme() in helpers.ts now delegates body class + persistence to applyThemeByName() so both paths write to owncord:theme:active
- Add input validation to loadCustomTheme(): reject non-object payloads and entries missing name/colors fields
- Remove dead CSS tokens --strip-width and --members-width from tokens.css
- Extend 800px responsive breakpoint to cover .unified-sidebar alongside .channel-sidebar
- Import restoreTheme from @lib/themes in main.ts
- Call restoreTheme() after applyStoredAppearance() so the body
class (theme-<name>) is applied before first render
- Add "neon-glow" entry to THEMES in settings/helpers.ts
- Update applyTheme() to also toggle theme-<name> class on document.body
- Update UiState and setTheme() in ui.store.ts to include "neon-glow"
- Delete ServerStrip.ts and its unit test
- Remove all .server-strip / .server-icon / .server-separator CSS rules
- Remove @media (max-width: 600px) responsive rule for .server-strip
Remove serverStripSlot and memberListSlot from the app layout in
MainPage.ts. Remove standalone MemberList creation, mounting, and
visibility subscription from ChatArea.ts. The member list is now
part of the unified sidebar (Task 8).
- Remove ServerStrip import and server strip slot from sidebar layout
- Add unified sidebar header with OC icon, server name, and online count
- Subscribe to uiStore.sidebarMode to swap between ChannelSidebar and
DmSidebar content views within the same DOM slot
- Wire UserBar onDisconnect to open QuickSwitchOverlay with saved profiles
- Add openQuickSwitch to SidebarAreaResult for external disconnect flow
- Add unified sidebar CSS classes to app.css
- Voice widget and user bar remain visible in both sidebar modes
Adds dmMode option (hides member-list toggle), exposes hashEl in refs,
and adds updateChatHeaderForDm helper that swaps # for @, sets username
and status when a DM recipient is active.
Adds optional onBack/serverName fields to DmSidebarOptions. When onBack
is provided, a clickable back header is rendered above the search bar
with arrow, title and subtitle. Supporting CSS added to app.css.
- Change UserBarOptions from Record<string,never> to interface with optional onDisconnect callback
- Render a "Switch server" button in the controls area when onDisconnect is provided
- Add log-out icon (Lucide path data) to icons.ts for the button
Implements lib/themes.ts with listThemeNames, applyThemeByName,
getActiveThemeName, saveCustomTheme, loadCustomTheme, deleteCustomTheme,
exportTheme, and restoreTheme. Built-in themes applied via body CSS
classes; custom themes applied via inline CSS variable overrides.
Active theme persisted to localStorage. All 7 unit tests pass (TDD).
Extends UiState with sidebarMode ("channels" | "dms") and
activeDmUserId (number | null). Adds setSidebarMode (which auto-clears
activeDmUserId when switching back to channels) and setActiveDmUser
actions. All three new tests pass (TDD: RED → GREEN).
Split layout redesign with deep indigo gradient left panel,
OC neon glow logo (orange→pink→purple→cyan), accent stripe
server cards, and full motion animations.
- Left panel: gradient background with ambient glow spots
- OC logo: SVG lettermark with Gaussian blur glow pulse
- Server cards: compact rows with purple accent stripe
- Inputs: 12px border-radius with focus glow ring
- Button: gradient with box-shadow and shimmer on hover
- Animations: panel slide-in, logo pulse, card stagger,
button shimmer, all respecting prefers-reduced-motion
The Rust save_credential command was accepting but ignoring the
password parameter (_password). Now stores it in the Windows
Credential Manager JSON blob alongside username and token.
DPAPI encrypts the blob at rest, tied to the Windows user —
plaintext is never written to disk.
Also wire the checkbox state to the profile's rememberPassword
field so it persists across sessions. Previously it was
hardcoded to false on every login.
- Collect stats from both publisher AND subscriber PeerConnections
(RTT is typically on the subscriber PC in LiveKit's SFU model)
- Accept candidate-pair with valid RTT regardless of state
(not just "succeeded" — subscriber may report "in-progress")
- Round raw byte values in formatBytes to avoid 15-digit floats
- Use max instead of sum for totalUp/totalDown across candidate
pairs to avoid double-counting across PCs
Add a signal-bars icon + ping text to the voice widget header
that shows real-time connection quality. Clicking it expands a
transport statistics pane with outgoing/incoming rates, packet
counts, RTT, and session totals.
- New lib/connectionStats.ts: polls WebRTC RTCPeerConnection
stats every 2s, computes quality level from RTT thresholds
- Signal icon with 4-tier coloring: green (<100ms), yellow
(100-200ms), red (>200ms), with 1-4 bars lit
- Expandable stats pane between header and controls
- Auto-starts/stops poller on voice connect/disconnect
- New createSignalIcon() in icons.ts for per-bar coloring
- getRoomForStats() export from livekitSession.ts
- Add stream quality selector (Low/Medium/High/Source) in Voice & Audio
settings with per-preset bitrate and resolution for camera + screenshare
- Use createLocalVideoTrack/createLocalScreenTracks + publishTrack for
explicit encoding control (bypasses LiveKit conservative defaults)
- Source quality: 8Mbps camera, 10Mbps screenshare, no adaptive/dynacast
- Nuclear mute: fully unpublish mic track when muting, re-publish on
unmute — guarantees SFU has no audio to forward
- Listen for LocalTrackPublished to re-enforce mute on renegotiation
- Store manually published tracks for explicit unpublish on disable
- VAD updatePipelineGain respects mute/deafen state
- Widen channel sidebar from 240px to 260px, add overflow handling
so voice user icons don't clip and cause horizontal scrollbar
- Remove invalid LiveKit server-side room config fields
- 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)
Two specs for the video/screenshare experience:
1. SCREENSHARE-AUDIO.md — system audio capture + per-tile mute
2. VIDEO-FOCUS-MODE.md — Discord-style opt-in viewing with focus layout
- Add enableScreenshare/disableScreenshare to LiveKitSession with
proper LiveKit track publishing, error handling, and WS notification
- VoiceWidget screenshare button now shows active state (red highlight,
icon swap, aria-pressed) matching mute/deafen/camera pattern
- VideoModeController activates video grid for screenshare (not just
camera), with local self-view tile using ID offset to avoid collision
- MainPage voice store subscription now watches screenshare state
changes to trigger checkVideoMode automatically
- Reset localScreenshare on leaveVoice to prevent stale button state
- Add auto-reconnect on unexpected LiveKit disconnect (2 attempts with
3s delay, fresh token request on success)
- Fix pre-existing missing reapplyAudioProcessing mock in settings test
- 8 new tests covering screenshare button, video grid activation,
tile lifecycle, and state cleanup
Drag LEFT = easier for mic to pass (high sensitivity),
drag RIGHT = harder (low sensitivity). Inverts both the
threshold indicator position and pointer-to-value mapping.
Co-Authored-By: claude-flow <ruv@ruv.net>
- Add --autoplay-policy=no-user-gesture-required to WebView2 config
so remote participants' audio plays immediately on join (desktop app
doesn't need browser autoplay restrictions)
- Add AudioPlaybackStatusChanged handler with click-to-unlock fallback
for browsers that still block autoplay
- Replace broken VAD implementation that used setMicrophoneEnabled/
mediaStreamTrack.enabled (both fought LiveKit's track lifecycle) with
a unified GainNode audio pipeline:
rawMic → AnalyserNode (VAD) → GainNode (volume × gate) → sender
- VAD now gates by setting gain=0 instead of touching the track —
analyser always sees real audio, no stale track references
- Merge input volume and VAD into single pipeline (always active)
- Add voice settings UI: draggable sensitivity threshold on mic meter,
input/output volume sliders (0-200%), audio processing toggles
Co-Authored-By: claude-flow <ruv@ruv.net>
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)
- Detach existing audio elements before attaching to prevent double playback on reconnects
- Remove webAudioMix to eliminate Web Audio overhead compounding with multiple participants
- Use participant.setVolume() for full 0-200% per-user volume range
- Add pli_throttle and active_loopback_prevention to LiveKit server config
- Bump version to 1.3.0