Code-read + upstream-issue triage this session:
- 41.7.0 / crypto-wasm 18.3.1 does NOT fix KE-1 (no OTK/upload change; #5200
still open) — the SDK-pin remediation lever is closed.
- Confirmed root cause = rust-crypto store <-> Synapse OTK divergence; the
leading web trigger is that cinny never requests persistent storage, so the
IndexedDB crypto store is evictable while the localStorage session survives.
- New buildable preventive mitigation: navigator.storage.persist() on login
(+ multi-tab guard, 400-loop recovery prompt). Added as §6 with a secondary
KE-2 to-device-validation hypothesis and capture discriminators.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add HSTS + Permissions-Policy + the standard X-Frame/X-Content/Referrer set to
the contrib nginx (443 block) and caddy examples; fix the caddy SPA try_files
fallback (stray space). Generic (no homeserver-specific CSP). The real prod
config lives in the matrix repo. P6-4 trimmed to headers only — patch-package /
types-drift / build-config skipped (see LOTUS_TODO).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forward: checkbox multi-select room picker + "Send to N rooms" batch send
(Promise.allSettled). Full success auto-closes; partial failure keeps the dialog
open with a "Forwarded to X/N — failed: …" summary and prunes the selection to
only the failures (retry won't duplicate to already-sent rooms). Content builder
extracted to a unit-tested forwardContent.ts (edit-forwarding, reply-strip,
undecryptable-refused; 4 tests).
Bookmarks: BookmarksPanel resolves each saved message's live event (useRoomEvent)
so previews reflect edits and show a deleted indicator for redactions; the stored
snapshot stays as the fallback while loading, on fetch failure, or after leaving
the room. Stored bookmark shape unchanged.
Gates: tsc/eslint/prettier clean, build OK, 665 tests. Reviewed (dup-resend on
retry + Checkbox readOnly fixed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CallControl now sends the new io.lotus.set_deafen action (join-gated via
forceState) on every deafen / screenshare-audio-mute toggle + on join, ALONGSIDE
the retained iframe-DOM .muted hack (transitional). Against the current pinned
bundle the action is immediately error-replied + swallowed by .catch — inert, no
timeout. Reordered toggleSound() to commit state before setSound() so the sent
deafen value isn't inverted.
Phase 2 (after the fork is published): bump the pin lotus.1 -> lotus.2 and delete
the DOM hack. Docs: HANDOFF §12.4, LOTUS_TODO P6-2, LOTUS_BUGS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dep triage recorded (zero shipped exposure; SDK now 41.7.0 stable; dompurify
removed); Needs Verification rows for the audit-wave fixes (scheduled-cancel,
emoji lazy-load, SW precache, desktop CSP smoke).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TODO: P4-7 already-implemented [x]; P4-6 mozilla test enablement verified live;
Audit-3 researched → deferred tracking MSC4427 (banner_url proposal, unmerged);
P3-8 Thread Panel now carries the complete SDK-evidence-backed build plan
(threadSupport side effects, local-echo gap, receipt fix, 4-agent partition) —
ready for its own session. BUGS: N127 removed, Big #5 (backgrounds/seasonal)
done, CDN env-var closed (VITE_DECORATION_CDN exists), test count updated, KE
section points at the new investigation kit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.
Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.
Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.
- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
manual-test steps.
Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- avatarDecorations: resolve the decoration CDN base from VITE_DECORATION_CDN at
runtime, falling back to the DECORATION_CDN literal (kept intact so the sync
script + tests still parse it). Lets a deploy repoint the CDN without a code
edit. Guarded for the tsx test runner (import.meta.env undefined there).
- LOTUS_BUGS: close N127 — the denoise dev-injection gap dissolved with the A7
cutover (no getUserMedia shim is injected anymore; denoise is in-source in the
EC fork), so there is nothing to inject in dev.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- scheduledMessages.test.ts (9): pins the MSC4140 request shape (PUT to the room
send endpoint with the org.matrix.msc4140.delay query, POST cancel/restart to
/delayed_events with the unstable prefix), the delay-floor math (Math.max(1000,
round(sendAt-now)) — "now"/past targets still yield a valid >=1000ms delay),
rounding, and url-encoding.
- lotusDenoiseUtils.test.ts (9): model-catalog data integrity + isMLDenoiseSupported
feature detection across AudioContext/webkit/getUserMedia.
- Bug found + fixed: isMLDenoiseSupported used `!!AudioWorkletNode`, a bare global
reference that throws ReferenceError (not returns false) on a browser with
AudioContext but no AudioWorkletNode binding. Switched to `typeof` so the
detection helper reports unsupported instead of throwing. Regression test proven
to fail on the old code.
Suite now 545 tests (4th real bug caught by the prevention work).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
callSounds.ts had no tests despite 106 lines of user-facing audio logic. Adds 13
tests (mocking AudioContext) pinning: the chime/soft/retro join+leave melodies
(frequencies, oscillator types, stagger), the click-avoidance gain envelope and
osc->gain->destination wiring, and the defensive contracts — unknown style is a
no-op that never creates a context, a throwing AudioContext constructor is
swallowed, and the shared context is reused / recreated-when-closed / resumed-
when-suspended. Suite is now 527 tests; refreshed the stale count in LOTUS_BUGS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LOTUS_BUGS.md: new Encryption/E2EE section tagged EXTREME complexity +
planning-session-required for a senior-engineer deep dive — OTK upload
conflict storm (KE-1), Element Call media-key distribution failures causing
audio/video dropouts (KE-2), a timeline decryption error (KE-3), and
MatrixRTC delayed-event timeouts (KE-4). All observed live 2026-06-30; not
caused by the EC fork work. Plus a non-developer ELEMENT_CALL_TEST_CHECKLIST.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch to @lotusguild/element-call-embedded@0.20.1-lotus.1 (our self-built
fork) and turn on the source-level features it adds:
- #1 denoise CUTOVER: in-source ML denoise (lotusDenoiseSource=1) replaces
the build-time getUserMedia shim — removed the shim injection from
vite.config.js (denoise/ assets still shipped; the processor loads them).
Survives reconnects (fixes A7).
- #2 call-state: CallEmbed consumes io.lotus.call_state; useCallSpeakers /
useRemoteAllMuted prefer it over scraping EC's DOM (DOM fallback kept;
empty payloads ignored).
- #4 focus: CallControl.focusCameraParticipant sends io.lotus.focus_participant
(works during screenshare), replacing the DOM tile-click hack.
- #5 theming: lotusTransparent=1 (native transparent background).
- #6 decorations: LotusDecorationPusher sends each member's decoration URL
via io.lotus.decorations -> rendered on in-call tiles.
#3 soundboard / #7 quality ship dormant (EC-ready; no host UI sends them yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the plan to fork element-hq/element-call and build it from source for
true ownership of the in-call experience (decorations, focus/screenshare,
reconnect mic, native theming, call-audio injection) — none of which are fixable
against the prebuilt @element-hq/element-call-embedded bundle we ship today.
- New HANDOFF_ELEMENT_CALL_FORK.md: self-contained plan for a fresh session
(current architecture, full file inventory, phases, new-repo decision, the
denoise-shim interaction, doc corrections).
- Tagged every related note with [EC-FORK] + links: README (For Developers),
LOTUS_BUGS (EC limitations), LOTUS_TODO (soundboard, denoise, soundboard
cross-origin correction), LOTUS_FEATURES (call section).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified passing: A2, B1-B4, C1, C3, D. Re-fixed and awaiting re-test: A1
(ringtone loudness), A3/A4 (caller decline notice), G1 (All-muted badge).
Documented A5/A6/A7 as known Element Call iframe-boundary limitations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two stale facts in README.md: it said "Forked from Cinny v4.12.1" (we've since
synced through v4.12.3) and referenced the logo as lotus_chat.png (the file is
public/res/Lotus.png). CONTRIBUTING.md is intentionally left as upstream
Cinny's and is not modified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per request, removed completed/resolved items (full history is in git) and
reorganized both into actionable form.
LOTUS_BUGS.md (864 -> 77 lines): dropped ~120 fixed-and-verified entries plus
all false-positive / won't-fix records. Now two clear sections: "Needs
Verification" (fixed in code, awaiting live test, cross-referenced to
LOTUS_TESTING.md) and "Open — Actionable" (grouped by theme).
LOTUS_TODO.md (771 -> 694 lines): removed completed [x] blocks (they live in
LOTUS_FEATURES.md) and consolidated the done-but-untested ones into a single
"Done — Awaiting Verification" index pointing at LOTUS_TESTING.md. Pending
[ ] items and nested roadmaps (e.g. DeepFilterNet/FRCRN under P5-30) were
preserved exactly (verified 42 -> 42); empty sections removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add grow="Yes" to ChatBgGrid and SeasonalBgGrid containers so they
expand to fill their flex parent — without it the Box shrank to one
column (~76px wide) because folds Box defaults to display:flex and
the wrapper is a flex-row with no explicit width.
- Mark N4 (PollContent) FIXED ✅ VERIFIED in LOTUS_BUGS.md after
confirmed pass on default Cinny themes and Lotus TDS.
- Mark B1 and B4 PASS in LOTUS_TESTING.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security & data persistence (N97–N100): plaintext access token storage
detail, normal logout wiping user prefs via localStorage.clear(), sync
ERROR freezing the loading screen, unrestricted CSS classes on <pre>.
PWA/SW/notifications (N105–N109): missing SW notificationclick + push
handlers, decrypted E2EE message body leaked to OS notification center,
missing maskable PWA icon, auth media URLs producing 401 in notification
icon/badge fetches.
Lotus feature internals (N113–N120, N128): reminder read-modify-write
race, fire-and-forget removeReminder silently drops on network failure,
setInterval restart on every reminder state change, useCallSpeakers
rebuilds speaker set from mutation batch only (drops current speakers),
static NodeList misses mid-call tile additions, CDN outage silently wipes
decoration catalog, CDN URL drift between two source files, patch-folds
silent exit-0 when patch target not found.
Call system & noise suppression (N122–N127): setMediaState Promise hangs
forever if EC omits DeviceMute echo, focusCameraParticipant drops tile
click if spotlight isn't ready in 2 rAFs, denoise cleanup() leaks
AudioWorklet gateNode, postMessage wildcard '*' origin, PiP position
NaN on corrupt localStorage, denoise shim inactive in vite dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both Retry and Leave called the same dismiss function; Retry implied a
reconnect attempt that never happened. Collapsed to a single Back button
that honestly describes returning to the prescreen.
docs: correct Gemini audit entries — sanitize-html not DOMPurify (Claim A),
retract inaccurate LiveKit replaceTrack soundboard approach (Claim B,
contradicts confirmed cross-origin iframe constraint), expand N95 fix note
to clarify track-stop vs AudioContext-suspend distinction.
docs(testing): add L1 N95 reproduction guide; update A7 to reflect single Back button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#1 documented as implemented (focusCameraParticipant + MemberGlance
"Focus camera" menu); #4 ringtone selection landed, with the remaining
active-call non-intrusive-notification work scoped and deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark items resolved by commits b7e1f89c / d2946c00 / 0394fce9 / 203568c9
as FIXED, and record the false-positives surfaced during the audit
(useMatrixEventRenderer null contract, Lobby getRoom already memoized,
RoomTimeline/RoomInput already wrapped by RoomView's ErrorBoundary).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>