The Solid.js migration was abandoned (per CHANGELOG); the 154-LOC
beachhead and its scaffolding remained in-tree, leaving two UI paradigms
for contributors. Removed:
- src/components/solid/ (Badge, ChannelListItem, PluginContainer — none
imported by production code)
- src/lib/solidMount.ts and src/lib/solidAdapter.ts
- tests/setup-solid.ts and tests/setup-solid.test.tsx
- vite-plugin-solid from vite.config.ts and vitest.config.ts (and the
now-unneeded tsx test include + setupFiles)
- jsx/jsxImportSource from tsconfig.json
- solid-js, @solidjs/testing-library, vite-plugin-solid from package.json
docs/client-architecture.md (which described the SolidJS design) is
retired to a pointer at docs/architecture/client.md; README links
updated. Audit A-2026-07-12 and decision D6 marked closed.
Verified: tsc --noEmit clean (previous 3 test-file errors were caused by
the Solid jsx config and are gone); oxlint/eslint error counts identical
to HEAD (pre-existing); vitest runner healthy on a sample suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Forward-only versioning for the alpha reset: 1.1.0-alpha.N ascends
past the superseded v1.0.0 for every installed client, and the alpha
series stays below 1.1.0-beta.N and the final 1.1.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Client Typecheck & Test CI job was failing on the prettier format
check. Two real root causes, fixed properly:
1. prettier endOfLine was set to 'crlf' but the repo stores files with
LF (no .gitattributes forcing eol), so 'prettier --check' failed on
292 files on the Linux CI runner. Set endOfLine to 'lf' to match the
on-disk reality. Also reformat the 2 files (pluginBridge.ts,
solidAdapter.ts) that had genuine style issues.
2. 15 'eslint-disable-next-line' comments targeted oxlint-only rules
(no-await-in-loop, no-unassigned-vars) that ESLint does not enable,
so ESLint reported them as 'Unused eslint-disable directive'
warnings. Switched the directive prefix to 'oxlint-disable-next-line'
— oxlint still honors them (its native syntax), and ESLint no longer
parses them as eslint directives, so the warnings are gone without
suppressing the safety check or removing the directives that oxlint
actually relies on.
Verified locally: oxlint, tsc --noEmit, eslint, prettier --check, and
npm audit --audit-level=high all exit 0.
Install and configure mutation testing (Stryker for client, go-gremlins
for server), load testing (k6), chaos testing (toxiproxy), WAF middleware
(Coraza with OWASP rules, opt-in via waf_enabled config), and Zod for
runtime schema validation. All tools verified building cleanly.
- 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
- Fix SDP signaling race condition: add per-client negoMu to serialize
renegotiateParticipant / handleVoiceOffer / handleVoiceAnswer so
concurrent OnTrack goroutines don't race through rollback
- Fix handleVoiceLeave triple-fire: early return when clearVoice()
returns zeros so ICE callbacks don't re-enter and corrupt state
- Fix SQLite SQLITE_BUSY errors: add busy_timeout=5000 pragma and
SetMaxOpenConns(1) for file-based databases
- Fix deafen bypass: new remote audio elements now respect localDeafened
state so late-arriving streams are muted immediately
- Add debug-level logging for SDP negotiation, track fan-out, ICE
candidates, voice state changes, room lifecycle, and participant
add/remove
- Bump version to 1.1.1
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
Server:
- Add message search and pinned messages support
- Add admin hub integration and live connection stats
- Update admin test mocks for hub interface
Client:
- Add data-testid attributes to components for E2E testing
- Add window management capabilities (position, size, maximize)
- Add prod E2E test config and script
- Fix CSS imports (use vite bundling instead of HTML link tags)
- Add inline styles to InviteManager overlay for reliability
- Update CHATSERVER.md references from WPF to Tauri
Docs:
- Update quick-start guide