From a02cc70e684e4b1f1ff82cb0eb7cb46379cc1cba Mon Sep 17 00:00:00 2001 From: shad Date: Sun, 14 Jun 2026 19:08:44 +0400 Subject: [PATCH] fix(oracle-l8): close 3 polish caveats (dispatch labels / chat unread / leftrail leak) Oracle PASS-with-caveats; closing all 3: 1. Wizard dispatch labels were 'human'/'agent'/'system' (impl-ish). Now 'Person'/'Assistant'/'System' in both the dropdown and the preview-pane badge. 2. Chat badge was thread COUNT (a buyer with 5 chats sees '5' forever). Now real unread count, computed locally from localStorage heads + read maps that the Chat scene already persists. App.tsx topbar poll still only fires 2 network calls (listThreads + workItems); per-thread unread math is zero-network. 3. LeftRail toast leaked 'POST /api/runtime/transactions/{id}/actions/ submit' / save_draft. Rewritten as 'Switch to live mode and sign in to submit this action against EA2.' Plus engineering-string sweep elsewhere: 'demo.flow-master.ai' / 'bundled JSON' / 'in-browser fetch' references removed from: - App.tsx mode toggle title - MissionControl loading spinner - Landing mode-button title - state/store snapshot toast - data/live.ts + data/synthetic.ts tour-step bodies - buildScenarios.ts tagline Source-grep confirms no remaining user-visible engineering jargon referencing the backend hostname or raw endpoints. --- src/App.tsx | 28 ++++++++++++++++++++++++---- src/components/LeftRail.tsx | 4 ++-- src/data/live.ts | 4 ++-- src/data/synthetic.ts | 2 +- src/lib/buildScenarios.ts | 2 +- src/scenes/Chat.tsx | 9 +++++++-- src/scenes/Landing.tsx | 2 +- src/scenes/MissionControl.tsx | 2 +- src/scenes/Wizard.tsx | 6 +++--- src/state/store.ts | 2 +- 10 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 25c025d..4ba0359 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,12 +76,32 @@ export default function App() { const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null; - const [chatThreadCount, setChatThreadCount] = useState(0); + const [chatUnreadCount, setChatUnreadCount] = useState(0); const [queueCount, setQueueCount] = useState(0); const tenantId = useApp((s) => s.tenantId); + + // Topbar badge poll. listThreads + workItems only (one request each, no + // per-thread reads — that's what caused the idle-poll regression earlier). + // Unread count is computed locally from heads + read maps persisted by the + // Chat scene to localStorage, so no extra network is needed. useEffect(() => { if (!isAuthed || !tenantId || !userEmail) return; let cancelled = false; + const computeUnread = (threadKeys: string[]): number => { + let heads: Record = {}; + let readMap: Record = {}; + try { heads = JSON.parse(localStorage.getItem(`fm.canvas.chat.heads.${userEmail}`) || "{}"); } catch { /* ignore */ } + try { readMap = JSON.parse(localStorage.getItem(`fm.canvas.chat.read.${userEmail}`) || "{}"); } catch { /* ignore */ } + let n = 0; + for (const k of threadKeys) { + const head = heads[k]; + if (!head) continue; + if (head.by === userEmail) continue; + const readAt = readMap[k]; + if (!readAt || head.at > readAt) n++; + } + return n; + }; const refresh = async () => { try { const { chatApi } = await import("./lib/chatApi"); @@ -91,7 +111,7 @@ export default function App() { apiMod.workItems().catch(() => []), ]); if (cancelled) return; - setChatThreadCount(threads.length); + setChatUnreadCount(computeUnread(threads.map((t) => t._key))); setQueueCount(items.filter((it) => (it as any).tenant_id === tenantId).length); } catch { /* ignore */ } }; @@ -127,7 +147,7 @@ export default function App() {