Full-surface protocol survey. Flags each remaining gap by what unblocks it: buildable now (custom room tags/sections — the only substantive client-only one left), needs infra (email/3PID invites → identity server; MSC4108/3814), and blocked-until-Synapse-upgrade (live location 3489/3672, reaction redaction 3892, room preview 3266, thread subs 4306). Space reordering already works (drag) — not a gap. Corrected per user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
25 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
🧨 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.