docs(todo): add DP1-DP18 discovery-pass findings (agent-surveyed + TPVR-verified)

Three discovery agents swept src/ for correctness/a11y/tech-debt issues beyond the
tracked backlog; a separate TPVR pass independently confirmed all 18 (5 refined to
partial with count/scope corrections). Filed as DP1-DP18 under Open - Actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:13:23 -04:00
parent f9c03d5e33
commit 2614e6b92d
+123
View File
@@ -63,6 +63,129 @@ Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then gr
## 🔴 Open — Actionable
### 🔍 Discovery pass (2026-07) — agent-surveyed, TPVR-verified
Three discovery agents swept the fork's own code (`src/`) for correctness / a11y /
tech-debt items beyond the tracked backlog; every item was then independently
re-verified (TPVR, 2026-07) against the code + installed SDK. **All 18 are real**
`✅` = confirmed as claimed, `◐` = real but the claim was imprecise (correction inline).
`Sev` = user/maintenance impact, `Eff` = S/M/L. IDs `DP1DP18`.
**Correctness (user-facing):**
- [ ] **DP1 — Silent slash-command failures (Sev High · Eff SM) · ✅.** `RoomInput.tsx:542`
calls `commandContent.exe(plainText)` (all `exe` are async, several reject) with no
`await`/`.catch`, then unconditionally resets the editor as if it succeeded; no global
`unhandledrejection` handler exists. `/kick` `/ban` `/invite` `/join` `/leave` `/ignore`
`/myroomnick` `/acl` `/delete` etc. fail with **zero** feedback. **TPVR context:** `/kick`
& `/ban` also route through `rateLimitedActions` (`utils/matrix.ts:452`) whose `to()`
swallows all non-429 errors — so a real fix must surface errors **per-command**, not just
await line 542.
- [ ] **DP2 — Pending invites re-notify on every reload (Sev Med · Eff S) · ✅.**
`ClientNonUIFeatures.tsx:202-217` + `usePreviousValue(invites.length, 0)` (ref inits `0`).
**TPVR context:** on warm reload the invite atom is populated synchronously from cached
`mx.getRooms()` while the client is already `SYNCING`, so the sync-state gate (`:203`)
doesn't protect → `notify(N)` + sound fire for pre-existing invites. Fix: seed previous to
`invites.length` and/or a first-render ref.
- [ ] **DP3 — Status cleared on another device is resurrected (Sev Med · Eff S) · ✅.**
`Profile.tsx:370` gates on `presence?.status` truthy, so a remote **clear** (falsy) skips
both the input reset and the `localStorage` write; `usePresenceUpdater.readStatus()`
re-sends the stale `status_msg` on the next heartbeat. Fix: treat `status !== undefined`
as an explicit clear + `localStorage.removeItem(STATUS_MSG_KEY)`.
- [ ] **DP4 — Favourite / Low-priority toggles fail silently (Sev LowMed · Eff S) · ✅.**
`RoomNavItem.tsx:337-351``setRoomTag`/`deleteRoomTag` fire-and-forget with no optimistic
UI (state derives from the account-data echo), so a rejected write leaves the menu closed,
no change, no toast. UX-only, bites on failure.
- [ ] **DP5 — Soundboard pack hooks go stale on room/space change (Sev Low · Eff S) · ✅.**
`useSoundboardPacks.ts:80,103,125``useState(() => …)` inits once. **TPVR context:** both
consumers usually remount when the room changes so the stale window is narrow, but it's a
real footgun. Fix: derive via `useMemo`/resync effect keyed on the room list.
- [ ] **DP6 — Call-decline event fire-and-forget (Sev Low · Eff S) · ✅.**
`CallEmbedProvider.tsx:547` — uncaught `mx.sendEvent(RTCDecline)`; UI dismisses
unconditionally (`:553`), so a failed decline looks handled locally while the caller keeps
ringing. Content shape verified spec-correct. Fix: best-effort `.catch` + still dismiss.
**a11y / UX / mobile:**
- [ ] **DP7 — Deafen button announces the OPPOSITE action to screen readers (Sev High · Eff S) · ✅.**
`call-status/CallControl.tsx:70``aria-label={enabled ? 'Undeafen' : 'Deafen'}` is
inverted; the tooltip (`:58`) is the **correct** one. When sound is ON, pressing deafens →
label should say "Deafen". aria-only fix, no visual risk.
- [ ] **DP8 — Sound/Video/ScreenShare toggles missing `aria-pressed` (Sev Med · Eff S) · ✅.**
Only `MicrophoneButton` (`CallControl.tsx:38`) exposes it; `:52,90,131` omit. **TPVR note:**
also pick a consistent pressed-semantics across all four (mic's `aria-pressed={!enabled}`
= "pressed means muted" is debatable).
- [ ] **DP9 — GifPicker drops focus + fixed width overflow (Sev Med · Eff S/M) · ✅ focus / ◐ width.**
`GifPicker.tsx:106` `returnFocusOnDeactivate:false` (+ `initialFocus:false`) drops focus to
`<body>` on dismiss — confirmed. **Correction:** `PICKER_WIDTH=312` is ~314px total and only
overflows *below* ~314 CSS-px or when PopOut positions near a screen edge (not a flat
"≤320px"). Fix: return focus + `min(312px, calc(100vw - 16px))`.
- [ ] **DP10 — Search-filter "clear" icons: redundant mouse-only affordance (Sev LowMed · Eff S) · ◐.**
`message-search/SearchFilters.tsx:800,826,850` (+ `DateRangeButton:648`) — `<Icon onClick>`
inside a `Chip`(`<button>`). **Correction:** the `<Icon>` is a bare `<svg>` (no interactive
ARIA role) so it's **not** an invalid nested control, and the clear is **not**
keyboard-inaccessible — the parent Chip's own `onClick` (and DateRangeButton's menu "Clear")
performs the identical toggle. Real defect = a **redundant mouse-only** clear icon. Fix: drop
the icon onClick, or make it a focusable `IconButton`.
- [ ] **DP11 — Voice-recorder can overflow composer + no announced duration (Sev Med · Eff M) · ◐.**
`VoiceMessageRecorder.tsx:33,190-269`. **Correction:** the waveform is ~236px (not ~360); the
whole widget ~380px. Outer `Box`(`:192`) has no width cap + waveform `flexShrink:0` → can
overflow a ~360px composer. **aria-live caveat:** duration ticks every 100ms, so a naive
`aria-live="polite"` would spam a screen reader — expose a snapshot/less-frequent value. Fix:
cap/allow-shrink outer width + responsive bar count (verify on a real ~360px device).
- [ ] **DP12 — Live-call participant count not announced (Sev Low · Eff S) · ✅.**
`call-status/LiveChip.tsx:131` / `CallStatus.tsx:45` — the "{count} Live" chip updates
silently. Fix: a separate visually-hidden `role="status"` mirroring the count (cleaner than
making the button's own label live).
**Tech debt / maintainability:**
- [ ] **DP13 — Triplicated account-data list engine (Sev High · Eff M) · ✅.**
`hooks/useBookmarks.ts`(144) / `useReminders.ts`(132) / `useUserNotes.ts`(122) are
near-verbatim copies of a concurrency-critical engine (module `moduleState`,
`ensureModuleState`, `enqueue*Write` serialization to avoid `setAccountData` lost-update
clobbering, `listeners` set, per-client teardown). **TPVR context:** factory is clean — the
only real diff is the account-data key + payload shape (list `{items:[]}` vs notes' flat
`Record`), so parameterize container-vs-record. Fix: extract
`createAccountDataListStore<T>(key, extract, serialize)`; hooks become thin wrappers.
**Directly unblocks the custom-room-tags feature.**
- [ ] **DP14 — TDS hardcoded colors → wrong colors in TDS *light* mode (Sev High-small · Eff S) · ✅.**
`room/message/Message.tsx:107,111,115,131` (`#FF3B3B`, `rgba(0,212,255,…)`) and
`read-receipt-avatars/ReadReceiptAvatars.tsx:109,112,114` hardcode dark-theme accent
hex/rgba gated on `lotusTerminal`. TDS **overrides** `--lt-accent-cyan``#0062b8` and
`--lt-accent-red``#b5001f` in the light theme (`lotus-terminal.css.ts:410,418`), so these
stay bright cyan/red instead of the light theme's darker blue/red — a **real wrong-color
bug**, direct TDS-Design-Law violation. `EventReaders.tsx:75,86` is the correct
`var(--lt-accent-*)` / `var(--lt-box-glow-*)` template.
- [ ] **DP15 — `sendStateEvent(…, StateEvent.X as any, …)` cluster (Sev Med · Eff M) · ◐ (count).**
**Correction:** real count is **32** casts across 20 files (not 28); `as any` on arg 2 also
collapses content typing to `any`. Plus 2 *dynamic* casts in dev tools (`SendRoomEvent.tsx:54`,
`StateEventEditor.tsx:62`) that an enum-typed helper can't remove. Fix: one typed
`sendState(mx, roomId, type: StateEvent, content, stateKey?)` helper eliminates the 32.
- [ ] **DP16 — `(mx as any).get/setAccountData` cluster (Sev Med · Eff SM) · ✅.**
**TPVR context:** real count **19** casts across 13 files; the existing `utils/room.ts:45
getAccountData` helper itself casts internally and is bypassed by most callers, and there's
**no** `setAccountData` helper. Fix: typed `getAccountData<T>` / `setAccountData<T>` wrappers;
route all callers (incl. the DP13 hooks) through them.
- [ ] **DP17 — `UrlPreviewCard.tsx` literal-emoji icons + hardcoded brand hex (Sev Med · Eff S / L) · ✅.**
Exactly **2250 lines**. Literal glyphs `♫`(`:477,520,1476`) `▶`(`:515`) `🎮`(`:862`)
`💬`(`:1008`) `⚙`(`:1536`) where folds `Icon`/`Icons` is mandated; inline brand hex `#EE1D52`
(TikTok) `#1db954` (Spotify) `#c7d5e0` (Steam). Fix (S): glyphs → folds Icons + hoist brand
consts. Optional (L): split the per-provider render branches.
- [ ] **DP18 — Member-avatar render trio duplicated (Sev LowMed · Eff M) · ◐ (count).**
**Correction:** the full trio (`getMemberDisplayName(room,id) ?? getMxIdLocalPart(id) ?? id`
+ `getMember(id)?.getMxcAvatarUrl()` + `mxcUrlToHttp(…)`) is in **8** components
(`EventReaders`, `UserMentionAutocomplete`, `ReadReceiptAvatars`, `MemberTile`,
`ClientNonUIFeatures`, `SearchInput`, `ReactionViewer`, `MembersDrawer`) — not 9
(`ForwardMessageDialog` uses a *room* avatar). Separately the name-fallback subpattern alone
(`getMemberDisplayName ?? getMxIdLocalPart`) is in ~29 sites / 24 files. Fix: two extractions
`useMemberAvatar(room, userId)` (8 sites, also standardizes the DP-N6 reactivity story) +
`getMemberName(room, userId)` (24 sites).
_Minor cleared non-issues (checked, NOT bugs): commented-out `console.log` in `ASCIILexicalTable.ts`;
grep-flagged `addEventListener` "leaks" that are transient `Audio` elements; `useModalStyle` already
gives mobile full-screen dialogs app-wide._
### ✅ 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).