diff --git a/qa/audit_idle_poll.mjs b/qa/audit_idle_poll.mjs new file mode 100644 index 0000000..31f58fd --- /dev/null +++ b/qa/audit_idle_poll.mjs @@ -0,0 +1,80 @@ +// Idle-poll audit. User explicitly complained "way too many GETs / non-stop". +// For each scene: visit, settle, then watch network for 30s with no user input. +// Pass if total requests in that 30s window is reasonable (<= 5). +// The topbar identity-hydration + chat/queue badge polls at 60s, so a 30s +// window should see at most 1 such fire. +import { chromium } from "playwright"; + +const URL = "https://canvas.flow-master.ai/"; +const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"]; + +const SCENES = [ + { chip: null, label: "landing" }, + { chip: /Approvals queue/, label: "approvals" }, + { chip: /^Documents$/, label: "documents" }, + { chip: /Procurement Hub/, label: "procurement_hub" }, + { chip: /Attendance Map/, label: "geo" }, + { chip: /Command Assistant/, label: "assistant" }, + { chip: /Team Chat/, label: "chat" }, + { chip: /What is FlowMaster/, label: "explainer" }, +]; + +const IDLE_WINDOW_MS = 30_000; +const MAX_REQ_PER_WINDOW = 5; + +const results = []; +function record(name, ok, detail = "") { + results.push({ name, ok, detail }); + console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); +} + +const b = await chromium.launch({ headless: true, args }); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); + +await p.goto(URL, { waitUntil: "networkidle" }); +await p.waitForTimeout(800); +await p.locator(".persona-chip", { hasText: /Mariana/ }).click(); +await p.locator(".dev-login-btn").click(); +await p.waitForSelector(".hero-actions", { timeout: 15000 }); +await p.waitForFunction(() => document.querySelectorAll(".hub-chip").length >= 8, undefined, { timeout: 15000 }).catch(() => {}); + +for (const s of SCENES) { + if (s.chip) { + await p.goto(URL, { waitUntil: "networkidle" }).catch(() => {}); + await p.waitForSelector(".hub-chip", { timeout: 15000 }).catch(() => {}); + await p.locator(".hub-chip", { hasText: s.chip }).click().catch(() => {}); + await p.waitForTimeout(2500); + } + + // Settle period — the scene's initial fetch burst should complete here. + await p.waitForLoadState("networkidle").catch(() => {}); + await p.waitForTimeout(1500); + + // Now record every request fired for the next IDLE_WINDOW_MS. + const requests = []; + const onReq = (req) => { + const u = req.url(); + if (/\/api\/|\/internal\//.test(u)) { + requests.push({ method: req.method(), url: u.replace("https://canvas.flow-master.ai", "") }); + } + }; + p.on("request", onReq); + await p.waitForTimeout(IDLE_WINDOW_MS); + p.off("request", onReq); + + const ok = requests.length <= MAX_REQ_PER_WINDOW; + const summary = requests.length + ? `${requests.length} reqs · ${requests.slice(0, 3).map((r) => `${r.method} ${r.url.slice(0, 50)}`).join(", ")}${requests.length > 3 ? "…" : ""}` + : "0 reqs · quiet"; + record(`idle_${s.label}_quiet_at_30s`, ok, summary); +} + +await b.close(); +const fails = results.filter((r) => !r.ok); +console.log(`\n=== idle audit: ${results.length - fails.length}/${results.length} passed ===`); +if (fails.length) { + console.log("FAILED:"); + fails.forEach((f) => console.log(` - ${f.name}: ${f.detail}`)); + process.exit(1); +} diff --git a/src/App.tsx b/src/App.tsx index 3dd1c09..25c025d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,7 +76,7 @@ export default function App() { const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null; - const [chatUnread, setChatUnread] = useState(0); + const [chatThreadCount, setChatThreadCount] = useState(0); const [queueCount, setQueueCount] = useState(0); const tenantId = useApp((s) => s.tenantId); useEffect(() => { @@ -91,24 +91,8 @@ export default function App() { apiMod.workItems().catch(() => []), ]); if (cancelled) return; - const readMap: Record = (() => { - try { return JSON.parse(localStorage.getItem(`fm.canvas.chat.read.${userEmail}`) || "{}"); } catch { return {}; } - })(); - let unread = 0; - for (const t of threads) { - try { - const msgs = await chatApi.listMessages(t._key); - const last = msgs[msgs.length - 1]; - if (!last) continue; - if (last.author_email === userEmail) continue; - const readAt = readMap[t._key]; - if (!readAt || last.created_at > readAt) unread++; - } catch { /* ignore */ } - } - if (!cancelled) { - setChatUnread(unread); - setQueueCount(items.filter((it) => (it as any).tenant_id === tenantId).length); - } + setChatThreadCount(threads.length); + setQueueCount(items.filter((it) => (it as any).tenant_id === tenantId).length); } catch { /* ignore */ } }; void refresh(); @@ -143,7 +127,7 @@ export default function App() {