Files
cinny/LOTUS_TODO.md
T
jared f12175e76f
CI / Build & Quality Checks (push) Successful in 10m53s
CI / Trigger Desktop Build (push) Successful in 9s
fix(unread): stop stuck/resurrecting read indicators
handleReceipt recomputed unread from getUnreadNotificationCount, which is
server-computed and stale on the synchronous synthetic receipt echo (the SDK
only zeroes it immediately when the last event is our own message). Reading
someone else's message therefore PUT the stale non-zero count back -> dot stuck
or resurrected on the ack-sync ordering race. Restore upstream cinny's
optimistic DELETE on our own receipt; the UnreadNotifications listener re-asserts
the accurate badge on the server ack.

Also collapse a {total:0,highlight:0} PUT to a DELETE in the reducer (a present
map entry lights the dot via hasUnread=!!unread, so phantom {0,0} PUTs from the
UnreadNotifications listener left stuck dots).

Mark-as-Unread (MSC2867): clear the flag directly in markAsRead (opening an
already-read room sends no receipt, so the receipt-driven auto-clear never
fired), and gate the receipt auto-clear to main/unthreaded receipts so reading
one thread no longer wipes the whole-room flag.

Tests: 700/700 pass; typecheck + prod build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:07:21 -04:00

27 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)

🔴 Open — Actionable

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 — summary endpoint 404s on 1.155)
  • 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

[ ] P4-4 · Math / LaTeX Rendering (LOW PRIORITY)

Render $…$ / $$…$$ via KaTeX; graceful fallback to raw text. Sanitizer must be patchedsrc/app/utils/sanitize.ts (sanitize-html, disallowedTagsMode:'discard') strips all MathML: add <math><mi><mo><mn><mrow><mfrac><msqrt><mroot><msub><msup><msubsup><munder><mover><mtable><mtr><mtd>… + annotation to permittedHtmlTags, and xmlns/display/mathvariant to permittedTagToAttributes. Parser: split text nodes on /(\$\$.*?\$\$|\$.*?\$)/g in react-custom-html-parser.tsx<KaTeX>. Lazy-import katex/dist/katex.min.css only when a math block renders. Verify KaTeX bundle-size impact.

[~] 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.

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.
  • [BLOCKED] Room Preview before joining (MSC3266) — GET /v1/rooms/{id}/summary returns 404 M_UNRECOGNIZED on Synapse 1.155 despite msc3266_enabled:true.
  • [BLOCKED] Thread Subscriptions (MSC4306 false) — "Follow thread" button (depends on the shipped Thread Panel).

📖 Reference

Server Capabilities (as of 2026-06)

  • Homeserver matrix.lotusguild.org · Synapse 1.155.0 · Matrix spec up to v1.12 (+ MSC unstable_features).
  • MSC ON: msc4140 · msc3771 · msc3440.stable · msc4133.stable · simplified_msc3575 · msc4222 · msc3266 (flag on but v1 summary 404s) · 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.