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>
27 KiB
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.cssCSS 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 (matchsrc/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
foldsdesign 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 foldsIcon/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
participatingdetection 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
expiresAton persist (persistTokenscan't reach the expiry without SDK-internal plumbing; refresh is reactive on 401). - Native/desktop: D7 Unity badge
application://cinny.desktopid may not match the installed.desktopbasename — 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 (EC1–EC6 fixed on
element-call:lotus, needs a republish): re-applysetTimeoutcleanup, remote-gated subscription →allConnections$, per-call decoration state leak, re-subscribe-every-render, focus-clear on missinguserId. 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.ts — handleReceipt 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/uploadreturns400 M_UNKNOWN: One time key … already existscontinuously — the rust-crypto store and Synapse have diverged OTK state (upstreammatrix-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 callsnavigator.storage.persist(), so the IndexedDB crypto store is evictable while thelocalStoragesession 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-deviceio.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
algorithmfield (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, repeatedmsc4157.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.tshas nopushhandler. Needs apushlistener + 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:
lotusDenoisedoes heavy sequentialfsincloseBundle;viteStaticCopyhas redundant renames — could be streamlined. patch-folds.mjseditsnode_modulesdirectly (robust today;patch-packageconsidered 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/caddyexamples: headers +try_filesalready synced with prod; the prod nginxadd_headerisn't inherited by cachelocationblocks (pre-existing; SPA entry/still gets all headers).as anycasts acrosssrc/— 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 }(+ unstablecom.famedly.marked_unread) viamx.setRoomAccountData; clear on read. Context-menu item inRoomNavItem+ light the existing unread dot; integratestate/room/roomToUnread.ts. - Low Priority rooms —
m.lowprioritytag. Mirror the favourite impl (RoomNavItem.tsx:331-337setRoomTag/deleteRoomTag+ the favourites category inhome/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-settingsSettingTileto set{ max_lifetime }; retention badge; a client-side sweep hides/self-redacts own expired events (pattern like the mute-timer restore inClientNonUIFeatures.tsx). True server deletion also wants Synapseretention:(LXC 151). - QR Device Verification — reciprocate QR. Add the QR path beside emoji-SAS in
components/DeviceVerification.tsx: render withqrcode.react(already a dep), scan viaBarcodeDetector(fallbackjsQR); uses the SDKVerificationRequestQR/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). Readim.vector.modular.widgets/m.widgetstate, add an Add/Manage panel + sandboxed iframe renderer viamatrix-widget-api— extend 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'sSlidingSync/SlidingSyncSdkare_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 (allRoomsAtom←mx.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.conf → nginx -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 A–C 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 (RoomNavItemcontext-menu +Home.tsxcategories). 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.inviteis 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 (MSC3892 —
false) - Room preview before joining (MSC3266 — summary endpoint 404s on 1.155)
- Thread subscriptions (MSC4306 —
false)
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 patched — src/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.1→lotus.2 → delete the CallControl.ts .muted fallback + the EC1–EC6 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}/summaryreturns 404M_UNRECOGNIZEDon Synapse 1.155 despitemsc3266_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· Synapse1.155.0· Matrix spec up tov1.12(+ MSCunstable_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.pyon 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 atsrc/sw.ts;getMatrixToRoom()builds invite URLs; EC audio-inject unblocked via the fork'sio.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.