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
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%).
- Update settings-overlay test for 9 tabs (was 6)
- Fix keybinds-tab test for new section structure
- Fix logs-tab test for voice diagnostics header
- Fix notifications test mock hoisting
- Add empty state for ChannelSidebar ("No channels yet")
- Add empty state for MemberList ("No members online")
- Add ARIA role/aria-selected to settings tab buttons
- Add aria-pressed to voice widget mute/deafen/camera buttons
- Add aria-label and title to member status dots
- Cache refreshed LiveKit token for reconnection
- 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
The refactored LiveKit session dropped track.attach() for remote audio,
so no <audio> element was created and remote participants were silent.
Also refactors noise suppression to use LiveKit TrackProcessor API,
adds input volume gain node bypass at 100%, and exposes __lkDebug()
on window for DevTools diagnostics.
- Wire animateGifs pref to file attachments (observeMedia startFrozen)
- Fix stale user closure in AccountTab edit form
- Cache loadPref reads in hot render paths with custom event invalidation
- Extract syncOsMotion listener to lib/os-motion.ts with AbortController cleanup
- Add sideEffect to syncOsMotion toggle for in-session activation
- Use static import for invoke in AdvancedTab, add error logging
- Fix password success timer race in AccountTab
- Extract keybind section header to CSS class
- Handle clipboard.writeText rejection in Copy ID button
- Label hardware acceleration as requiring restart
The button was calling a nonexistent openDevtools() method on the
JS WebviewWindow API. Use invoke("open_devtools") to call the
registered Rust command instead.
Connect stored-but-unused settings to their actual functionality:
- Text & Images: showLinkPreviews, showEmbeds, inlineMedia now
conditionally render embeds/previews/images in messages
- Text & Images: animateGifs controls GIF auto-play via new
startFrozen parameter on observeMedia()
- Accessibility: roleColors toggles role-based username coloring
- Accessibility: syncOsMotion listens to OS prefers-reduced-motion
and auto-toggles reduced-motion class (fix default to false)
- Advanced: developerMode shows Copy ID button on messages
- Rename status-option → settings-status-option in AccountTab to avoid
CSS collision with existing status picker popup
- Replace non-existent settings-divider class with settings-separator
in KeybindsTab
Expand the Keybinds tab beyond Push-to-Talk and Quick Switcher by adding
three new read-only sections with HR separators and uppercase section headers:
- Navigation: Quick Switcher (Ctrl+K), Mark as Read (Escape), Search Messages (Ctrl+F)
- Communication: Toggle Mute (Ctrl+M), Toggle Deafen (Ctrl+D), Toggle Camera (Ctrl+Shift+V)
- Messages: Upload File (Ctrl+U), Edit Last Message (Arrow Up)
Adds a new "Advanced" settings tab as the second-to-last entry in App Settings
(before Logs). Includes toggles for developerMode and hardwareAcceleration
preferences (persisted via savePref/loadPref), plus a debug section with an
Open DevTools button that calls the Tauri webviewWindow API.
Adds a new Accessibility tab to the Settings overlay (positioned between
Text & Images and Voice & Audio) with five toggles: Reduce Motion,
High Contrast, Role Colors, Sync with OS, and Large Font. Toggles with
side effects apply/remove CSS classes (reduced-motion, high-contrast,
large-font) on documentElement immediately and are also restored at
startup via applyStoredAppearance(). Corresponding CSS rules added to
app.css; eye icon was already present in icons.ts.
Adds a new settings tab with 6 toggles for controlling media and text
display: link previews, rich embeds, inline attachment preview, GIF
animation, emoji animation, and spoiler reveal behaviour. Also registers
the "image" Lucide icon used by the tab's sidebar entry.
Adds a status selector section to the Account tab with four status options
(Online, Idle, Do Not Disturb, Invisible). Clicking a status updates the
local members store, sends a presence_update WebSocket message, and persists
the choice via savePref. Adds onStatusChange callback to SettingsOverlayOptions
so the main page handles WS dispatch, keeping the settings components
decoupled from transport.
- Added setInputVolume and setOutputVolume methods to LiveKitSession class
- setInputVolume saves mic gain preference (0-200%, persisted only for now)
- setOutputVolume saves pref and immediately applies to all remote audio elements (capped at 1.0 / 100%)
- Both methods exported as bound functions alongside existing session exports
- Added Input Volume slider (0-200%, default 100%) between input device selector and Output Device section
- Added Output Volume slider (0-200%, default 100%) between output device selector and Video Device section
- Both sliders use existing .slider-row / .settings-slider / .slider-val CSS classes
Adds a full accent color customization section below Compact Mode:
- 10 preset color swatches (Discord blurple default, plus green, yellow,
fuchsia, red, salmon, orange, dark green, cyan, grey)
- Custom hex input (#RRGGBB) with live validation and swatch sync
- Live preview via --accent CSS custom property on documentElement
- Persisted via savePref/loadPref("accentColor") in localStorage
- Restored at app startup via applyStoredAppearance() in SettingsOverlay.ts
- Accessible: role=radio, aria-checked, keyboard nav (Enter/Space)
- Remove redundant <h1> header creation and append from NotificationsTab,
VoiceAudioTab, and LogsTab — SettingsOverlay already renders the page title
- Only call startCameraPreview() when a saved video device ID is non-empty,
preventing uninvited camera access on settings open
- Fix stale .ac-name selector in tests → .account-header-name
- Remove misleading "Display Name" field (no displayName in data model)
- Extract buildProfileCard and buildPasswordSection helpers
- Use var(--green) instead of hardcoded #43b581 for status dot
- Remove conflicting position:absolute from .settings-close-btn CSS
- Remove inline style override on close button
- Replace flat account card with Discord-style layout: colored accent
banner, 80px avatar overlapping banner with status dot, username
header with "Edit User Profile" button
- Add separated Display Name / Username field rows with individual Edit
buttons; all three trigger the same inline edit form and update all
name display elements on save
- Phase 3: rename "Change Password" h3 to a .settings-section-title
"Password and Authentication" div, increase password input gap to 12px
- Update .account-card CSS to use overflow:hidden instead of padding/flex
- Add new CSS classes: .account-banner, .account-avatar-wrap,
.account-avatar-large, .account-status-dot, .account-header,
.account-header-name, .account-fields, .account-field,
.account-field-label, .account-field-value, .account-field-edit,
.settings-separator, .settings-section-title
- Minor SettingsOverlay.ts cleanup: early return guard and remove
redundant inline style on close button
Phase 1 sidebar enhancement:
- Add 5 new Lucide icons to icons.ts: user, palette, bell, keyboard, scroll-text
- Add TAB_ICONS mapping so each settings tab has its own icon (mic already existed)
- Add user profile section at top of sidebar (avatar initial + username + Edit Profile link)
- Split nav into "User Settings" (Account) and "App Settings" (Appearance–Logs) categories
- Move Log Out button from AccountTab to sidebar bottom with separator line above it
- Wrap close button in .settings-close-wrap container with "ESC" label below it
- Add h1 page title at top of content area, updates on tab switch
- Make sidebar a flex column so logout section sticks to the bottom via margin-top:auto
- Use formatMessageTimestamp for "Today at H:MM AM/PM" in headers
- Add mini avatar to reply references
- Fix virtual scroll height estimates for new spacing (61/22px)
- Include CSS margins in height measurement via getComputedStyle
- Fix 0px → 0 in --message-inline-spacing token