Files
cinny/LOTUS_TODO.md
T
jared d7d261a019
CI / Build & Quality Checks (push) Successful in 10m56s
CI / Trigger Desktop Build (push) Successful in 9s
docs(todo): add Discovery pass 2 — PERF/SEC/COR findings (agent-surveyed)
Three agents swept fresh lenses (performance, security/privacy, correctness in
under-covered subsystems). 16 verified items filed: PERF-1..6, SEC-1..5 (no
exploitable XSS found — sanitization surface is hardened), COR-1..6. Single-pass,
not yet TPVR'd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 01:11:42 -04:00

38 KiB
Raw Blame History

Lotus Chat — Work Backlog

Repo: lotus branch at https://code.lotusguild.org/LotusGuild/cinny Deploy: push to lotus → CI → auto-deploy to chat.lotusguild.org (~11 min)

Completed features are documented in LOTUS_FEATURES.md. Manual test steps live in LOTUS_TESTING.md. This file is open work only — resolved audit findings and shipped-feature write-ups were removed 2026-07 (full history in git).

Status legend: [ ] pending · [~] in progress / shipped-awaiting-QA · [x] done · [BLOCKED] server/upstream-gated · [DEFERRED]/[DROPPED]/[WON'T FIX] decided.


⚠️ TDS DESIGN LAW — READ BEFORE TOUCHING ANY UI

ALL Lotus Terminal Design System (TDS) styling — colors, animations, glows, borders, fonts, spacing — MUST come exclusively from /root/code/web_template/base.css CSS variables. Do NOT hardcode hex values. Do NOT invent new variable names. Canonical tokens: --lt-accent-orange, --lt-accent-cyan, --lt-accent-green, --lt-glow-*, --lt-box-glow-*, --lt-border-color, --lt-font-mono. Syntax-highlight token classes: .tok-kw .tok-str .tok-num .tok-cmt .tok-fn. Reference patterns: /root/code/tinker_tickets/ (markdown.js, base.js, ticket.css). Applies to every task without exception. New components must respect both TDS dark (LotusTerminalTheme) and TDS light (LotusTerminalLightTheme); non-TDS theme work uses vanilla-extract (match src/lotus-terminal.css.ts).

🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY

Every feature must feel native to upstream Cinny — indistinguishable from what the Cinny team would ship. Reference: https://github.com/cinnyapp/cinny.

  • Use the folds design system, not bespoke UI (Button, Chip, IconButton, Menu, MenuItem, Dialog, Modal, Input, Switch, Badge, SettingTile, SequenceCard, …) and folds tokens (color.*, config.space.*, config.radii.*). Use folds Icon/Icons, never literal emoji, in UI chrome. No hardcoded hex/rgba(), no invented CSS variables.
  • Match Cinny's existing patterns — find the closest existing component/flow and mirror it before adding UI.
  • The ONE exception: explicit TDS features, which follow the TDS Design Law above (opt-in, only in Lotus Terminal mode).

Audit (2026-07) — closed out

A three-wave feature bug-hunt (~15 parallel agents, each batch independently reviewed) plus a low-tail cleanup. All confirmed 🔴/🟠 and the clean 🟡 tail are fixed, reviewed, and gate-green; details in git history + LOTUS_FEATURES. Only the minor items below remain open.

Still open (low tail — all 🟡 minor):

  • Calls host: C-M1 deafen DOM-fallback leaks late-added <audio> tracks; C-M2 .click()-by-testid toggles no-op if EC renames — both retire via EC-fork P6-2. C-L1 AFK mic not released if EC elides the echo; C-L2 ringtone-preview global cross-cancel; C-L3 first ring after cold load can be silent (ctx not unlocked); C-L5 speaker-observer churn on membership change; C-L7 all-muted DOM miscount if EC label format differs; C-L8 PiP sw/nw resize anchor jitter at min size.
  • Threads: T5 participating detection is server-bundle-only (thread.hasCurrentUserParticipated) → can under-notify a thread you just replied to; T6 room "Mentions & Keywords" not honored for participated/Default thread replies (over-notify); T7 account-data thread-mute write is a lost-update race.
  • Crypto/session: F5 OIDC refresh drops expiresAt on persist (persistTokens can't reach the expiry without SDK-internal plumbing; refresh is reactive on 401).
  • Native/desktop: D7 Unity badge application://cinny.desktop id may not match the installed .desktop basename — runtime-verify on the .deb/AppImage. H10 room-name setter fire-and-forget/silent length reject (trivial). N6 per-message read-receipt avatars may not refresh on membership change (emitter uncertain, low impact).
  • EC fork (EC1EC6 fixed on element-call:lotus, needs a republish): re-apply setTimeout cleanup, remote-gated subscription → allConnections$, per-call decoration state leak, re-subscribe-every-render, focus-clear on missing userId. Rides with P6-2 phase 2.

Shipped — Awaiting Live Verification

Built and gate-green; verify per LOTUS_TESTING.md, then graduate to LOTUS_FEATURES.md. Includes the desktop/native Tier A/B stack (P5-35/36/41/42/43/44/46/47/48/49/55/56/57, P6-1 Linux parity) — all CI-compile-verified, runtime-verify on Windows/Linux — plus:

Area Test guide
Full-Screen Camera Broadcasts (per-participant focus) A5 / G2
Advanced search filters + virtualized infinite scroll K2 / M1 / M2 / M4
Custom Accent Color Picker (non-TDS) · 5 Color Theme Presets M3 / M5
Intersection lazy media loading · context-aware thumbnails H1 / H2
Thread Panel (side drawer) + per-thread notification modes (P4-1) (thread QA)
Encrypted message search indexing/caching (opt-in, default OFF) search backlog
Remind Me Later · Mobile Bookmarks access K1 / E5
In-Call Soundboard (P5-15) · Quality Controls (P5-31) · Permissions (P5-31) D2-7 / D2-8 / D2-9
Desktop proactive update notifications (P5-40) J1
OIDC/SSO login (P4-6, needs an MSC3861 server — pick mozilla.org on login) OIDC
Windows native WinRT toast quick-reply / click-to-open (D6, AUMID) rich-toast (§backlog)
Inline media embeds (16 providers: video/audio/post + click-to-play facade) Q1 / Q2 / Q3 / Q4

🔴 Open — Actionable

Discovery pass (2026-07) — DP1DP18 DONE

Agent-surveyed + TPVR-verified correctness / a11y / tech-debt fixes (DP1DP18) are implemented, review-fixed, and gate-green (tsc + eslint + 737 tests + build). Verify per LOTUS_TESTING.md §R, then remove this note. Full detail in git history — commits 8eb961b6 db864326 6cf18c3b 165714e1 8c0e2b42 e545706c b1ee3ada 4fc3f7a3 101e4116 4fa4327a c2598d21.

🔍 Discovery pass 2 (2026-07) — perf / security / correctness (agent-surveyed, single-pass — NOT yet TPVR'd)

Three agents swept fresh lenses (performance, security/privacy, correctness in under-covered subsystems); each verified its own findings against the code + SDK. Sev/Eff = S/M/L. Confirm before fixing (no independent TPVR yet).

Performance / rendering:

  • PERF-1 — Presence: 3 global client listeners registered PER avatar (Sev High · Eff M). hooks/useUserPresence.ts:43-53 (via PresenceRingAvatar.tsx:19, MembersDrawer.tsx:142) — every timeline/member avatar adds mx.on for Presence/CurrentlyActive/LastPresenceTs → 100250 global listeners; each presence event fans out across all, and fast scroll churns add/remove per Message mount. Fix: one shared presence store (context + useSyncExternalStore or a jotai atomFamily) with 3 listeners total; components select by userId.
  • PERF-2 — #-mention autocomplete re-sorts AND mutates the room list every keystroke (Sev Med · Eff S). components/editor/autocomplete/RoomMentionAutocomplete.tsx:83useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx)) is unmemoized (O(N log N) mx.getRoom per compare, N=all joined rooms, per keypress) and .sort() mutates the shared allRoomsAtom array in place. Fix: useMemo(() => [...rooms].sort(...), [rooms, mx]).
  • PERF-3 — Read-receipt rows attach per-instance global RoomStateEvent.Members listeners (Sev Med · Eff M). components/read-receipt-avatars/ReadReceiptAvatars.tsx:74 + hooks/useMemberAvatar.ts:44 — up to ~6 global Members listeners per receipt row; fires on any membership/name/avatar change in any room. Fix: one shared room-member-change subscription fanning out by roomId+userId (shares with PERF-1).
  • PERF-4 — Message-search room filter re-sorts full room list every render (Sev LowMed · Eff S). features/message-search/SearchFilters.tsx:155Array.from(...).sort(factoryRoomIdByAtoZ(mx)) outside useMemo (re-sorts as the user types). Fix: useMemo.
  • PERF-5 — DM-preview hook subscribes a global Decrypted listener for EVERY nav item incl. non-DMs (Sev LowMed · Eff S). hooks/useRoomLatestRenderedEvent.ts:64-67 (called at RoomNavItem.tsx:681) — preview is only used for direct rooms but the hook + its MatrixEventEvent.Decrypted listener run for all; wasteful, and fans out across all during bulk decryption. Fix: gate the hook/subscription on direct.
  • PERF-6 — Avatar-decoration profile-fetch on first render of large member/timeline views (Sev Low · Eff M, likely leave). hooks/useAvatarDecoration.ts — one /profile/{userId} per new user (well-guarded: module cache + in-flight dedupe + backoff). Network/HS-load note only; batch/skip only if it proves costly.

Security / privacy: (no exploitable XSS found — sanitize.ts strips javascript:, forces mxc <img> + rel=noopener <a>; embeds use host-allowlist + origin-scoped postMessage; KaTeX trust:false. These are storage-hygiene / URL items.)

  • SEC-1 — Scheduled-message plaintext persists in localStorage, survives logout (Sev Med · Eff S). state/scheduledMessages.ts:12-19 — full IContent (incl. E2EE cleartext body) under cinny_scheduled_messages_v1; normal logout (removeFallbackSession, N98) keeps it. Leak on shared/kiosk browser. Fix: clear on logout (add to removeFallbackSession) — or encrypt-at-rest with N97.
  • SEC-2 — Recent search terms persist in localStorage, survive logout (Sev Low · Eff S). state/recentSearches.ts:4-12 — last 10 queries (often PII) under cinny_recent_searches_v1, survive normal logout. Fix: clear on logout.
  • SEC-3 — window.open(url, '_blank') without 'noopener' → reverse tab-nabbing (Sev Low · Eff S). components/user-profile/UserChips.tsx:117 (+ SSOStage.tsx:23, settings/devices/Verification.tsx:276, OtherDevices.tsx:36,49, OidcManageAccount.tsx:23) — window.open doesn't null window.opener. UserChips opens a user/room-derived server URL. Fix: pass 'noopener,noreferrer' (or set w.opener = null).
  • SEC-4 — /acl has no */self-lockout guard or glob validation (Sev Low · Eff SM). hooks/useCommands.ts:489-538/acl -d * writes deny:['*'] (can brick the room); empty/whitespace globs pushed unvalidated into m.room.server_acl. PL-gated + matches upstream, but a destructive footgun. Fix: reject a */empty deny matching the local server, validate glob syntax, confirm on *.
  • SEC-5 — Embeds get allow-popups-to-escape-sandbox (Sev Low, informational · Eff M). components/url-preview/UrlPreviewCard.tsx:590-591 (EMBED_SANDBOX) — a hijacked provider script could spawn an un-sandboxed popup (phishing). Main-app hijack already prevented (no allow-top-navigation); popups arguably needed for "open in provider." Fix (if desired): drop the escape flag + verify per-provider.

Correctness (under-covered subsystems):

  • COR-1 — Space-child UNLINK nukes a room's OTHER parents + orphans descendants (Sev Med · Eff M). state/room/roomToParents.ts:102-103 (DELETE reducer :53-66) — removing one m.space.child fires the same DELETE used for "room left," wiping ALL of the child's parents and stripping it as a parent everywhere. Remove C from space A only → C also loses parent B, and C's own children lose C, until a full resync. Fix: a targeted "remove one parent from one child" action. (Reducer inherited from upstream.)
  • COR-2 — useCallJoined stays true across a direct embed swap (answering a 2nd call) (Sev Med · Eff S). hooks/useCallEmbed.ts:140-158 — the reset effect only clears joined when !embed; answering call B from the banner swaps embed A→B directly, so joined stays true → B renders as live/joined before EC actually joins, skipping the loading/watchdog UI. Fix: useEffect(() => setJoined(embed?.joined ?? false), [embed]).
  • COR-3 — Incoming-call lifetime guard only corrects FUTURE clock-skew, not past (Sev Low · Eff S). components/CallEmbedProvider.tsx:476-477 — distrusts sender_ts only when >20 s ahead of origin_server_ts; a caller clock that's behind leaves sender_ts in the past → remaining ≤ 0 → ring auto-dismisses/never shows for a fresh invite. Fix: Math.abs(...) > 20000 or just use event.getTs().
  • COR-4 — Shared notify-dedupe slot double-notifies a re-fired main message after an interleaving thread reply (Sev Low · Eff S). pages/client/ClientNonUIFeatures.tsx:521,530lastNotifiedEventRef is one Map<roomId,eventId> slot shared by the main + thread paths; a thread reply overwrites it, so a re-emitted main message (decrypt/edit re-fire, common in E2EE) mis-dedupes and notifies again. Fix: separate dedupe keys per path (or key by roomId|eventId).
  • COR-5 — Upload cancel during retry back-off is silently ignored (upload resurrects) (Sev Low · Eff M). utils/matrix.ts:204-238 + state/upload.ts:125-129 — during the 130 s back-off the atom holds the previous (rejected) promise, so cancelUpload is a no-op and the loop wakes to start the next attempt. Fix: thread an abort flag into the retry loop.
  • COR-6 — CallControl.forceState resets screenshareAudioMuted to false (Sev Low · Eff S). plugins/call/CallControl.ts:155-162forceState passes only 5 args so the 6th defaults false; low impact today (runs at join, no screenshare yet) but silently discards the field. Fix: pass this.screenshareAudioMuted.

Unread/read-receipt flakiness (reported 2026-07) — FIXED (pending prod QA)

Room unread dots were inconsistent: reading a message sometimes cleared the dot, sometimes left it stuck, sometimes it resurrected. Root cause (confirmed by tracing + diffing upstream cinny dev): our own "N4" change. handleReceipt recomputed via getUnreadInfo, which reads room.getUnreadNotificationCount() — server-computed and stale on the synchronous synthetic receipt echo (SDK only zeroes it immediately when the last event is your own message) → it PUT the stale non-zero count back → stuck/resurrecting. Compounded by hasUnread = !!unread lighting the dot on any present map entry, incl. phantom {0,0} PUTs from our UnreadNotifications listener. Plus a Mark-as-Unread (MSC2867) flag that never cleared on opening an already-read room (no receipt → no auto-clear).

Fix: roomToUnread.tshandleReceipt reverts to upstream's optimistic DELETE on own receipt; reducer collapses {0,0} PUT → DELETE. notifications.ts markAsRead clears the marked-unread flag directly. markedUnread.ts onReceipt gated to main/unthreaded receipts (myMainReceiptPresent). Unit tests added; 700/700 pass, typecheck + build clean. Deploy + manual QA (read → dot clears & stays; thread read; mark-unread → open → clears; reconnect no resurrect).

🧨 Encryption / E2EE — ⚠️ EXTREME COMPLEXITY · 🧠 PLANNING SESSION REQUIRED

Observed live in prod 2026-06-30 during a 2-person Element Call (E2EE). These span client rust-crypto (matrix-js-sdk@41.7.0) ↔ Synapse ↔ EC MatrixRTC E2EE and are interrelated — do NOT spot-fix. Capture first: run Settings → Developer Tools → Crypto Diagnostics during the next affected call + a synapse-side trace before any fix. (Full runbook was in LOTUS_E2EE_INVESTIGATION.md, now in git history.) None are caused by the EC fork work.

  • KE-1 — OTK upload conflict storm (CRITICAL, root-cause candidate). POST /keys/upload returns 400 M_UNKNOWN: One time key … already exists continuously — the rust-crypto store and Synapse have diverged OTK state (upstream matrix-rust-sdk#5200, OPEN: on the 400 the SDK never marks the request sent → re-uploads forever; not fixed in 41.7.0). Leading web trigger: cinny never calls navigator.storage.persist(), so the IndexedDB crypto store is evictable while the localStorage session survives → device resurrects with a blank store. Buildable preventive fix (no call needed): request persistent storage on login (+ optional multi-tab guard + a 400-loop→recovery prompt). Healing an already-diverged device still needs a clean logout+login.
  • KE-2 — EC media keys not arriving/decrypting → audio/video cut out (CRITICAL). MissingKey … for participant, unexpected encrypted to-device io.element.call.encryption_keys. Almost certainly downstream of KE-1 (broken Olm sessions). This is the "friend's audio cuts out" symptom.
  • KE-3 — Timeline decrypt error: missing algorithm field (HIGH). rust-crypto can't parse a malformed/legacy encrypted event — capture the offending event id + raw content.
  • KE-4 — MatrixRTC delayed-event / membership timeouts (MEDIUM-HIGH). Restart delayed event timed out, repeated msc4157.update_delayed_event — may be partly HS responsiveness; correlate with synapse latency. Same planning session (shares the call-reliability surface).

Security & Privacy

  • N97 — Access token + device id in plaintext localStorage (state/sessions.ts), XSS-exposed. Architectural — needs a token-protection / session-storage redesign.
  • Persisted PII without encryption: user status message + expiry (Profile.tsx), unsent composer drafts (RoomInput.tsx). Leak risk on shared devices.

PWA / Offline / Web Push

  • N107 — Web Push is non-functional: src/sw.ts has no push handler. Needs a push listener + Matrix push-gateway integration. The one substantive remaining feature (session/crypto groundwork it waited on has landed).
  • No app-asset caching strategy in src/sw.ts — no offline capability.

Dependencies / Build / Hygiene

  • Build-time: lotusDenoise does heavy sequential fs in closeBundle; viteStaticCopy has redundant renames — could be streamlined.
  • patch-folds.mjs edits node_modules directly (robust today; patch-package considered but more brittle to folds restructuring — WON'T-DO unless it breaks).
  • types/matrix/ mirrors SDK types instead of importing them — drift risk; spot-fix highest-risk only.
  • contrib/nginx/contrib/caddy examples: headers + try_files already synced with prod; the prod nginx add_header isn't inherited by cache location blocks (pre-existing; SPA entry / still gets all headers).
  • as any casts across src/ — gradual typing cleanup. Keep commits scoped (bisect-friendly). Keep README fork-sync version/logo current.

🌐 Matrix Protocol Gaps

Genuine Matrix client-spec / MSC features Lotus does not yet implement (audited 2026-07 against the codebase — almost everything else is built: pinning, stickers+picker, room directory, mutual rooms MSC2666, blurhash, key backup/recovery/SSSS, SAS verification, ignore list, invite spam-filter, voice messages, polls, threads, spaces, OIDC, extended profiles, delayed events, authed media). Build each fully — spec-correct events, native-Cinny folds UI, tests. Order = clean wins first.

Phase A (2026-07, gate-green 683 tests):

  • Mark as Unread — MSC2867 m.marked_unread. Room account data { unread: true } (+ unstable com.famedly.marked_unread) via mx.setRoomAccountData; clear on read. Context-menu item in RoomNavItem + light the existing unread dot; integrate state/room/roomToUnread.ts.
  • Low Priority rooms — m.lowpriority tag. Mirror the favourite impl (RoomNavItem.tsx:331-337 setRoomTag/deleteRoomTag + the favourites category in home/Home.tsx): context-menu toggle + a collapsed "Low Priority" category sorted to the bottom, excluded from normal unread nudging.

Phase B (2026-07, gate-green 688 tests):

  • Disappearing Messages — MSC1763 m.room.retention. PL-gated room-settings SettingTile to set { max_lifetime }; retention badge; a client-side sweep hides/self-redacts own expired events (pattern like the mute-timer restore in ClientNonUIFeatures.tsx). True server deletion also wants Synapse retention: (LXC 151).
  • QR Device Verification — reciprocate QR. Add the QR path beside emoji-SAS in components/DeviceVerification.tsx: render with qrcode.react (already a dep), scan via BarcodeDetector (fallback jsQR); uses the SDK VerificationRequest QR/reciprocate support.

Phase C (Room Widgets 2026-07; Sliding Sync evaluated — parked):

  • Room Widgets — MSC1236 + widget API. No general widget UI exists (only the PL entry im.vector.modular.widgets; the EC call widget is hardcoded). Read im.vector.modular.widgets/m.widget state, add an Add/Manage panel + sandboxed iframe renderer via matrix-widget-apiextend the existing EC widget plumbing (plugins/call/CallEmbed.ts). Enables Etherpad/notes/dashboards/integrations.
  • [PARKED] Sliding Sync — MSC3575 / simplified MSC4186 (evaluated 2026-07, 3 research passes). Server side is GA (simplified_msc3575), but the client side is not viable for a safe rollout: matrix-js-sdk's SlidingSync/SlidingSyncSdk are _internal_/@experimental (Element shipped labs-only, never GA in ~2 yrs, moved to the Rust SDK); presence isn't delivered over sliding sync (regresses Lotus presence badges/rings/status); no upstream Cinny impl to follow; and Cinny's whole nav (sidebar/spaces/DM/unread) is derived from the full local room set (allRoomsAtommx.getRooms()), so ~14 subsystems (4 core) need re-architecting to a server-windowed list. ~10% confidence a full rollout wouldn't break/regress (missing rooms/messages/unread = worst failure class). Revisit only if we adopt the Rust SDK or accounts grow large enough that startup latency is a real complaint; an off-by-default experimental spike is possible but not recommended. Full assessment: git plan history.

Room Widgets v1 follow-ups: capability-approval consent prompt (let widgets request send/read room events); Jitsi/stickerpicker special types; account-data (user/sticker) widgets; per-widget popout / always-on-screen. Requires the prod CSP frame-src widening (done in matrix/cinny/nginx.confnginx -s reload) or external widgets are blocked.

Server-gated / advanced (capture, don't build yet): QR sign-in for a new device (MSC4108 rendezvous — needs an HS-side endpoint); dehydrated devices (MSC3814 — offline key delivery, also helps the E2EE KE cluster); E2EE history key sharing on invite (MSC3061 shared_history, niche); voice broadcast (Element MSC3888, low value — skip).

Remaining spec/MSC gaps (2026-07 full-surface survey)

After Phases AC the client spec is ~complete. What's left, flagged by what unblocks it:

Buildable NOW (client-only, no server/infra change):

  • Custom room tags / sections — user-defined room categories in the sidebar via standard u.* room tags (beyond the built-in Favourite / Low-Priority). Mirrors the favourite/low-priority category pattern (RoomNavItem context-menu + Home.tsx categories). Medium. The only substantive client-only feature left.

🔧 Needs INFRASTRUCTURE (NOT a Synapse-flag flip — you'd have to stand it up):

  • Invite by email / 3PID invite — we invite by Matrix user-ID only (mx.invite is user-ID-only). Email invites need an identity server (lotusguild runs none). Build only if an identity server is deployed.
  • QR sign-in for a new device (MSC4108) — needs a rendezvous endpoint. Dehydrated devices (MSC3814) — needs server support. (Also listed above.)

🚫 BLOCKED until a Synapse upgrade enables the flag — re-run /_matrix/client/versions unstable_features after each upgrade; client work is ready the moment the flag flips. See the Blocked Features section below:

  • Live Location Sharing (MSC3489 + MSC3672 — both false)
  • Reaction / relation redaction (MSC3892false)
  • Room preview before joining (MSC3266)DONE (client was always built; unstable im.nheko.summary endpoint returns 200 — verified on 1.156)
  • Thread subscriptions (MSC4306false)

Niche / low-value (noted, not planned): E2EE history-key-on-invite (MSC3061), voice broadcast (MSC3888), a native account-deactivation flow (currently delegated to the OIDC provider for OIDC accounts).

Already implemented (verified, not gaps): space reordering (drag — confirmed working in the desktop client), pinning, stickers + picker, room directory, mutual rooms (MSC2666), blurhash, key backup / recovery / SSSS / cross-signing / key export-import, SAS and QR verification, ignore list, invite spam-filter, voice messages, polls, threads + per-thread notifs, spaces, OIDC, extended profiles, delayed/scheduled events, authed media, report user/room/message, 3PID contact-info display, disappearing messages, mark-unread, low-priority, room widgets.


📋 Open Feature Backlog

[ ] Basic in-app audio editor / video→audio extractor (LARGE PROJECT)

A minimal audio editor for soundboard clips and voice content. Scope: (1) trim/clip an audio file to a chosen start/end (waveform scrubber, in/out handles); (2) upload a video file → strip and discard the video track, keep only the audio (extract audio, then the source video is dropped — never uploaded/stored); (3) minimal edits only (trim, maybe gain/normalize, fade in/out) — not a full DAW. Likely Web Audio API (AudioContext.decodeAudioData → trim AudioBuffer → re-encode) + MediaRecorder/an encoder for output; video demux via a <video>+MediaElementSource capture or ffmpeg.wasm (weigh bundle cost). Feeds the soundboard uploader (utils/soundboardClips.ts, SoundboardPackEditor) and attachments. Design under TDS + native-cinny law. Big build — plan a dedicated session; evaluate ffmpeg.wasm size/CSP (wasm) before committing.

[x] P4-4 · Math / LaTeX Rendering — DONE

Rendering shipped (KaTeX, $…$/$$…$$ + spec data-mx-maths, lazy-loaded, <pre>/<code>-guarded) — see LOTUS_FEATURES.md. Outgoing cross-client interop added (2026-07): the composer now emits spec data-mx-maths HTML on send (editor/output.ts, reusing splitMathSegments), so math a Lotus user types renders on Element and every other client, not just Lotus. Deferred: multi-line block $$…$$ (spans editor paragraph nodes) still renders on Lotus via the plain-body $…$ path only.

[~] P5-20 · Quick Reply from Browser Notification (partial)

Done: notifications show the real body, click navigates to the specific event + focuses the tab. Remaining: inline reply via Notification Actions API needs the SW push+notificationclick pipeline (switch new Notification()serviceWorkerRegistration.showNotification() so the SW receives notificationclick; on event.action==='reply' POST m.room.message with the stored {roomId, threadId}). Ties into N107.

[~] P5-30 · Advanced ML Noise Suppression — open verification

Shipped in the EC fork (DeepFilterNet3 default-capable / DTLN / RNNoise / Speex; AEC on, AGC off for ML tier; never-silent watchdog). Open: real-call by-ear A/B — model choice, lotusDenoiseFloor, AGC on/off (LOTUS_TESTING §D2-1 / J2). GTCRN (deferred): tiny MIT 16 kHz model beating RNNoise, but no drop-in browser package — needs onnxruntime-web in a Web Worker behind a custom AudioWorklet ring-buffer (ORT can't run in an AudioWorklet, issue #13072); ~1-week build. Revisit only if low-power quality proves insufficient. HW-gated (FRCRN/Maxine) = desktop-Rust-only future.

[~] P6-2 · Element Call fork — retire remaining DOM hacks (Phase 2 needs publish)

Phase 1 shipped: io.lotus.set_deafen (LiveKit-source deafen/screenshare-audio-mute) replaces the brittle <audio>.muted iframe hack; cinny sends it join-gated alongside the transitional DOM fallback. Phase 2 (blocked on user npm publish): publish fork 0.20.1-lotus.2 → bump cinny pin lotus.1lotus.2 → delete the CallControl.ts .muted fallback + the EC1EC6 fixes ship. Deferred pieces (P6-2b): the useCallSpeakers DOM-scrape is a dormant fallback behind io.lotus.call_state; .click()-by-data-testid UI toggles are low-value fork surface. Divergence to confirm: deafen doesn't silence soundboard/Unknown-source audio (setVolume type limit).

[ ] Mobile audit

Comprehensive audit of all LOTUS_FEATURES.md features for mobile PWA usability + responsiveness. Method: 44px touch targets, no horizontal overflow, full-screen modals/drawers on mobile, composer not obscured by keyboard.

[ ] Inline media embeds — remaining providers (LOW PRIORITY)

The inline embed system (videoEmbed.ts) covers 16 providers; three more were deliberately deferred (verified against 2026 docs by review agents):

  • Bandcamp (highest-value audio add) — needs an oEmbed round-trip: the player URL requires numeric album/track item ids that aren't in the page URL (bandcamp.com/oembed is the resolver; mirror the TikTokEmbedCard on-click oEmbed pattern). CSP frame-src: bandcamp.com. Classify kind: 'audio'.
  • SoundCloud on.soundcloud.com short links — the w.soundcloud widget resolver does not follow the redirect; needs the same on-click oEmbed resolve (soundcloud.com/oembed, CORS-enabled) to get the canonical URL. (Canonical soundcloud.com/{user}/{track} links already work.)
  • Vimeo event/{id} (live events) + ondemand/… — event embed host is vimeo.com (not player.vimeo.com, so it needs a new CSP frame-src host); on-demand is paywalled and doesn't embed for non-purchasers. Low ROI — only do the event case if vimeo.com is widened for another reason.

Also open (from the quality review): a real onError/error-state fallback for iframes that fail to load (deleted post / region lock / X login-wall) — cross-origin frames don't fire onError reliably, so this needs a load-timeout heuristic; the Close button + badge link are the current escape hatch.

Deferred / dropped (decided — kept for context)

  • [DEFERRED] P5-51 Federated "Identity Contexts" (session isolation) — multi-sprint, touches auth/crypto/storage core; smaller intermediate step = plain multi-account switch. [DROPPED] P5-52 per-room sync governor — js-sdk can't truly per-room filter /sync; only a cosmetic hide. [DEFERRED] P5-53 local scripting plugin — prefer a declarative automation-rules feature (no arbitrary code). [DEFERRED] Audit-3 profile banner — MSC4427 open/unmerged; revisit on merge. [WON'T FIX] P5-50 Windows HW media pipeline (WebRTC decode lives in WebView2; not injectable). [MOVED] P5-9 LFG → LotusBot !lfg.

🚫 Blocked Features (server / upstream gated)

Re-run /_matrix/client/versions + unstable_features after each Synapse upgrade.

  • [BLOCKED] Live Location Sharing (MSC3489 + MSC3672 both false) — real-time GPS beacons over the existing static share.
  • [BLOCKED] Reaction/Relation Redaction (MSC3892 false) — remove a reaction without redacting the parent; current full-redaction fallback is acceptable.
  • [DONE 2026-07] Room Preview before joining (MSC3266) — the client was always built (JoinBeforeNavigateRoomCard via mx.getRoomSummary). The earlier "blocked" flag was a misdiagnosis: it tested /v1/rooms/{id}/summary (404), but the SDK calls the unstable im.nheko.summary/summary/{id} path, which returns 200 with name/topic/members/join_rule. Verified live after the 1.156 upgrade; also added a join-rule/encryption chip + Request-to-join for knock rooms to the preview card.
  • [BLOCKED] Thread Subscriptions (MSC4306 false) — "Follow thread" button (depends on the shipped Thread Panel).

📖 Reference

Server Capabilities (as of 2026-07)

  • Homeserver matrix.lotusguild.org · Synapse 1.156.0+trixie1 (upgraded 2026-07-07 from 1.155; apt package on Debian 13, LXC 151) · Matrix spec up to v1.12 (Synapse still advertises v1.12; MSC features via unstable_features).
  • MSC ON: msc4140 · msc3771 · msc3440.stable · msc4133.stable · simplified_msc3575 · msc4222 · msc3266 (room summary live at unstable im.nheko.summary/summary/{id} — 200; the /v1/rooms/{id}/summary path is still 404) · msc3401_matrix_rtc. OFF/blocked: msc4306 · msc3882 · msc3912 · msc4155 · msc3489/msc3672 · msc3892.
  • Live endpoints: Report User (MSC4260) 200 · Report Room (MSC4151) .
  • Homeserver access (audits): Synapse = LXC 151 (pct exec 151 -- bash), config /etc/matrix-synapse/homeserver.yaml. Web deploy = LXC 106. Voice guard = voice-limit-guard.py on LXC 151.
  • SDK notes: no arbitrary profile-field methods (use mx.http.authedRequest() for MSC4133); js-sdk can't per-room filter /sync; sanitizer strips <math>/MathML; SW exists at src/sw.ts; getMatrixToRoom() builds invite URLs; EC audio-inject unblocked via the fork's io.lotus.inject_audio.

Key File Reference

What File Lines
Global keydown / room nav hooks/useKeyDown.ts · hooks/useRoomNavigate.ts whole / 19-72
Room unread counts atom state/room/roomToUnread.ts roomToUnreadAtom
Overlay portal provider pages/App.tsx · index.html 65 / 101
Room settings tabs features/room-settings/RoomSettings.tsx 27-56
State event read/write pattern features/common-settings/general/RoomEncryption.tsx 42-52
Power levels hooks/usePowerLevels.ts whole
Slash commands hooks/useCommands.ts 140-537
Chat background picker/defs features/settings/general/General.tsx · lotus/chatBackground.ts 945-981 / whole
Matrix.to URL builder plugins/matrix-to.ts getMatrixToRoom()
Media URL conversion utils/matrix.ts mxcUrlToHttp()
Search pagination / virtual features/message-search/{useMessageSearch,MessageSearch}.tsx 74-121 / 234-365
Call mic control plugins/call/CallControl.ts 206-212
Knock support check utils/matrix.ts 376-391
Notification mute push rules hooks/useRoomsNotificationPreferences.ts 110-150

Element Call fork — operational reference

Fork = LotusGuild/element-call (branch lotus, from upstream tag v0.20.1); cinny consumes the npm package @lotusguild/element-call-embedded (built bundle copied into public/element-call/).

Publish a new version (manual; needs the Gitea npm token): bump embedded/web/package.json (current unpublished 0.20.1-lotus.2) → pnpm run build:embedded (Node 24, pnpm 10.33) → cd embedded/web && npm version <tag> --no-git-tag-version && npm publish (Gitea registry) → in cinny bump the @lotusguild/element-call-embedded pin (currently 0.20.1-lotus.1) → npm install → build.

io.lotus.* widget actions (add new toWidget actions to the enum + LOTUS_TO_WIDGET_ACTIONS in src/lotus/lotusActions.ts; only send AFTER call-join or a 10s timeout fires):

Action Dir Purpose Module
io.lotus.call_state EC→host speaker/mute/camera stream (lotusCallState=1) lotusCallState.ts
io.lotus.focus_participant host→EC spotlight (works during screenshare) lotusFocus.ts
io.lotus.inject_audio host→EC soundboard clip mixed into call (lotusAudioInject=1) lotusAudioInject.ts
io.lotus.set_quality host→EC audio/screenshare bitrate/fps caps lotusQuality.ts
io.lotus.decorations host→EC in-call avatar decorations lotusDecorations.ts
io.lotus.set_deafen host→EC LiveKit-source deafen (P6-2) lotusDeafen.ts

Also flag-gated: lotusTransparent/lotusTheme, lotusDenoiseSource=1 (in-source ML denoise).

CI/CD + per-feature checklist

edit → commit → git push origin lotus
→ Gitea Actions: tsc --noEmit, eslint, prettier (~3 min)
→ lotus_deploy.sh on LXC 106 polls CI → npm ci && npm run build → rsync → live (~11 min)

Before marking a feature complete: npx tsc --noEmit (0 errors) · npx eslint src/ (0 new) · npx prettier --check src/ · npm test (Node runner via tsx, hard CI gate — colocated *.test.ts) · update README.md/landing/index.html for Lotus-custom features · visually verify on chat.lotusguild.org.