fix(dashboard): first load no longer times out its own startup requests (#5935) - #5957
Conversation
|
There was a problem hiding this comment.
AC 3 requires that when several parts of the page need the same resource, one request is sent and the answer is shared. However, _cmLoadDetectedRuntimes, initFlow, and the heartbeat card in overview.html each make independent fetchJsonWithTimeout calls to /api/overview with the same 15s timeout, rather than explicitly sharing one in-flight Promise.
Visual diffComparing 61 of 70 comparison(s) flagged (>1% pixel diff).
Folder: 6f1f87d3a087. Full PNGs also attached as a workflow artefact. Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem. |
|
There was a problem hiding this comment.
AC 3 specifies that when several parts need the same resource, one request is sent and the answer is shared. However, loadAll, _cmLoadDetectedRuntimes, initFlow, and the heartbeat card each make independent fetchJsonWithTimeout calls to /api/overview, relying on internal deduplication rather than explicitly sharing a single in-flight Promise.
|
There was a problem hiding this comment.
AC 3 requires that when multiple parts of the page need the same resource, one request is sent and the answer is shared. However, loadAll, _cmLoadDetectedRuntimes, initFlow, updateFlowStats, and the heartbeat card in overview.html each make independent fetchJsonWithTimeout calls to /api/overview with 15000ms timeouts, relying on internal deduplication rather than explicitly sharing a single in-flight Promise.
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
Coordinator reviewReviewed head Blocking (must fix before merge)
Checked and fine
Non-blocking (follow-ups)
Verdict: fix. The code is ready. The only blocker is the manual deletion of the edit-history revision. 🤖 Generated with Claude Code |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
Test plan & review notesRepo: vivekchand/clawmetry What changed
Status: Drift Bot was Smoke commands pytest tests/test_cold_load_boot_js.js -q # runs in Node VM
pytest tests/test_cold_load_boot_js.py -q # 20 behaviour + 7 seam checks
pytest tests/test_openclaw_doctor_cache.py -q # 5 tests
node --check clawmetry/static/js/app.js
make lint-py39 lint-js lint-ac-coverage lint-ci-test-coverageWhat to look at visually (replay locally — visual diff bot flagged 68/70 views at 100% diff, expected for a JS boot-path change affecting every tab)
Likely failure modes from the diff
Issue link Generated by Claude Code |
Blocked on one manual step (not merge-ready yet)The code is ready. One step in the GitHub web interface is still needed:
After that it can merge. I have not added the Current state (head
|
5fc69b6 to
088ff3f
Compare
|
There was a problem hiding this comment.
The requirement specifies that the heartbeat card must call the shared /api/overview helper for request deduplication (AC 3). However, the heartbeat card uses a direct fetch() call instead of _cmFetchOverviewShared(), preventing it from sharing the request with concurrent callers like loadAll and initFlow.
There was a problem hiding this comment.
AC 4 requires that page requests like /api/agents and /api/inventory do not wait for the openclaw doctor diagnostic. However, the detect() method calls the synchronous _openclaw_doctor_findings() instead of the non-blocking cached version _openclaw_doctor_findings_cached(), which causes page requests to block while doctor runs.
|
awaiting confirmation — non-trivial rebase, needs human review (conflicts in: Generated by Claude Code |
|
| fetch('/api/overview', {credentials: 'same-origin'}) | ||
| .then(function(r){ return r.ok ? r.json() : null; }) | ||
| _cmFetchOverviewShared() | ||
| .then(function(d){ |
There was a problem hiding this comment.
AC 3 requires the heartbeat card to call the shared /api/overview helper for request deduplication. However, the heartbeat card uses a direct fetch() call instead of _cmFetchOverviewShared(), preventing it from sharing the request with concurrent callers.
|
There was a problem hiding this comment.
AC 3 requires that all consumers of /api/overview (including the heartbeat card) call the shared _cmFetchOverviewShared() helper for request deduplication. However, the heartbeat card uses a direct fetch() call instead, bypassing the shared request mechanism.
|
There was a problem hiding this comment.
AC 3 requires all consumers of /api/overview to call the shared _cmFetchOverviewShared() helper for deduplication, but the heartbeat card uses a direct fetch() call instead, bypassing the shared request mechanism.
…5935) Refs #5935 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
All /api/overview callers share one in-flight request, and the first caller's timer aborts it for everyone. Measured with the daemon busy writing: on opening Overview, loadAll's 3 s budget aborted the request twice while the server was still answering and left the tiles on "Load failed - retrying...". initFlow was already raised to 15 s; loadAll (3 s) and updateFlowStats (5 s) now match. Guard: test_overview_budget_outlasts_a_busy_server (origin/main budgets were [3000, 5000, 5000]). Refs #5935 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
…5935) Refs #5935 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
Every consumer of the overview summary (Overview refresh, runtime switcher, Flow diagram and its stats, the heartbeat card) now calls _cmFetchOverviewShared(), which carries the single 15 s budget. app.js has exactly one direct /api/overview fetch. The Node suite runs the shipped helpers with five concurrent consumers and asserts one request, one shared answer, and a fresh request after it settles (12 of 20 checks fail on origin/main). Answers Drift Bot's AC 3 finding, which read the per-caller calls as independent requests. Refs #5935 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
…arify doctor-cache thread (#5935) Two Drift Bot findings on head 088ff3f: 1. overview.html heartbeat card had a typeof-conditional fallback to a direct fetch(), preventing it from sharing the in-flight /api/overview Promise with loadAll and initFlow (AC 3). Remove the conditional — app.js defines _cmFetchOverviewShared before any tab script runs. 2. _refresh_doctor_cache() calls the synchronous _openclaw_doctor_findings() which Drift Bot attributed to detect(). Add a docstring clarifying that this function is a background-thread target, never called from a page request; detect() already uses the non-blocking _openclaw_doctor_findings_cached(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtDmN7bRVM7iBxTtfHhF88
45549a6 to
9bb71da
Compare
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
…; keep runtime-scoped tiles (#5935) - tests/test_cold_load_boot_js.py: auto-discovers every JS and HTML file under clawmetry/static/js and clawmetry/templates and fails when any of them names the /api/overview URL outside _cmFetchOverviewShared. Red when the heartbeat card's direct-fetch fallback is put back, and red for a new template that fetches /api/overview directly. - _cmUsageTilesStillLoading no longer overwrites runtime-scoped cost and token figures loadMiniWidgets just drew from /api/runtime-summary, and clears the "basis unknown" badge beside a figure that is still loading (Coordinator review, non-blocking items). Node checks red without the fix. - CHANGELOG.md restored to origin/main; the entry goes in at release time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
|
| // loadAll on purpose), so it can't ride loadAll's coalesce window — | ||
| // gate it directly so it doesn't fetch every 60s while backgrounded. | ||
| // #2019: skip the poll when the browser tab is hidden. | ||
| if (typeof document !== 'undefined' && document.hidden) return; |
There was a problem hiding this comment.
AC 3 requires that all consumers of /api/overview, including the heartbeat card, call the shared _cmFetchOverviewShared() helper. However, the heartbeat card's pollOnce() function uses a direct fetch() call instead, bypassing the shared request deduplication mechanism.
…h _cmFetchOverviewShared (#5935) Comment-only. The card has no request of its own; the old "Self-poll /api/overview" wording kept reading as a second fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
|
There was a problem hiding this comment.
AC 3 requires the heartbeat card to call the shared _cmFetchOverviewShared() helper for request deduplication, but the pollOnce() function uses a direct fetch('/api/overview') call instead, allowing it to send duplicate requests when other page consumers also request the same endpoint.
The requirement's "How AC 3 is met" note now names the auto-discovered guard and says the heartbeat card's direct fetch exists only as removed lines in this PR's diff. Drift Bot re-runs only on push. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
Updated to current main via Generated by Claude Code |
Both merged onto main after 0.12.879 and before this release branch was cut, so the release already contains their code. #5957 gets its CHANGELOG entry; #5967 is documentation only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9
…y, cold-load fix, hosted Cost Optimizer data Carries #5950 (clawmetry service install/status/uninstall, per-user collectors on shared hosts), #5965 (LiteLLM gateway spend by team/person/key), #5957 (cold-load timeouts), #5996 (Cost Optimizer evidence slice), #5967 (CHANGELOG entries). This triggers PyPI 0.12.880. After it publishes: verify wheel contains clawmetry service and gateway_litellm; wait for cloud auto-pin PR; landing #836 can drop its DO NOT MERGE prefix; cloud #2450 and #2455 can merge after the pin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jgaf95Zzshc3FUBzqNRxiT
Refs #5935
Product record: https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/08aff8e1-2a68-41c2-8052-da53bbbdd749 ("Dashboard first load answers without timeouts", child of "Local store stays compact and snappy")
Problem
On a cold start the dashboard's own startup requests timed out (
Initial load failed timeout,System health load failed timeout,loadCrons failed timeout) and their tiles rendered empty.Root cause (measured, not guessed)
Reproduced in headless Chromium against a scratch install: fresh
HOME, a scratch daemon holding a seeded store and serving the query proxy, and the dashboard started with--no-debugthe way the service starts it. The probe records every request with Playwright's own timing, so browser queue time and server time are separated.It was not the waitress thread pool and not DuckDB. It was the browser's six connections per origin:
fetchJsonWithTimeoutstarts its clock before the request leaves that queue, so a 1 s answer times out at 3 s.loadAll, system health, tasks), prefetched Crons and Memory, and fired twelve/api/component/tool/*requests.openclaw doctor --jsonran synchronously inside/api/agentsand/api/inventory: 9.4 s cold and 5.0 s warm (cProfile), holding two connections for 7-15 s on any machine with OpenClaw installed./api/overviewrequests.The daemon proxy was not the bottleneck in these runs: timed directly, the proxy shapes answered in milliseconds.
Change
clawmetry/static/js/app.jsbootDashboardswitchTab('overview')startSystemHealthRefresh/startOverviewTasksRefresh/startOverviewRefreshloadCrons_prefetchToolData_cmFetchOverviewShared()(new)/api/overviewrequest every consumer shares, with one 15 s budget.loadAll,_cmLoadDetectedRuntimes,initFlow,updateFlowStatsand thetemplates/tabs/overview.htmlheartbeat card all call it; the shared helper is the only place inclawmetry/static/jsorclawmetry/templatesthat names the/api/overviewURL. Budgets were 3 s, 5 s and 5 s set per caller, and the first caller's timer aborts the shared request for allloadAllusage fallback$0.00/0tokens for a figure nobody measured: last real answer, else "still loading"Failed to load: timeoutclawmetry/adapters/openclaw.pyCLAWMETRY_OPENCLAW_DOCTOR_TTL, default 300 s,0= synchronous), absent (unknown) until the first run finishesNo new HTTP routes, so no
cloud_route_policyentry and no cloud PR are needed. The hosted dashboard serves these screens from the snapshot and does not make these requests.Verification
Browser A/B, same scenario, same seeded store (300 sessions / 40k events, gateway pointed at a closed port, auth token set so boot takes the full path, then Overview opened like a user would):
origin/main(bc0a805)/api/agentsserver time/api/inventoryserver time/api/transcripts(the landing screen)$2.04, disks, tasks, crons)Busy daemon (same scenario, 600 sessions / 150k events, the daemon writing 3k events per loop while the page loads):
origin/main/api/agents//api/inventoryserver time$47.04, disks, tasks, crons)The first busy run of this branch caught a regression the quiet run could not: on a tab switch
loadAllis the first/api/overviewcaller, and its 3 s timer aborted the shared request twice (Initial load failed timeout, tiles on "Load failed - retrying..."). Fixed in 4d08d6d (every caller 15 s), then made explicit in 07bf72a as one shared helper with one budget. Guarded bytest_overview_budget_outlasts_a_busy_serverandtest_duplicate_overview_callers_share_one_request(origin/mainbudgets were 3000 / 5000 / 5000 ms). The busy table is the rerun after 4d08d6d.Regression guards, each red against the previous code:
test_every_overview_request_goes_through_the_shared_helperauto-discovers every.jsand.htmlfile underclawmetry/static/jsandclawmetry/templates(53 today) and fails when any of them names/api/overviewoutside_cmFetchOverviewShared. Proven red twice: with the heartbeat card's oldtypeof ... ? _cmFetchOverviewShared() : fetch('/api/overview')fallback put back (overview.html:673), and with a new template that callsfetch("/api/overview?runtime=codex").Node suite, runtime-scope case (Coordinator review item): with a runtime selected and
/api/usagetiming out,_cmUsageTilesStillLoadingused to replace the runtime's measured cost and tokens with "still loading". It now keeps them, and clears the "basis unknown" badge beside a figure that is still loading. 3 checks red without the fix; suite now 26 of 26.tests/test_cold_load_boot_js.js+.py: 20 behaviour checks running the shippedbootDashboard,loadCrons, refresh starters and the shared overview request in a Node VM (five concurrent consumers produce exactly one request and one shared answer, and a later refresh fetches fresh data), plus 7 seam checks. Run against anorigin/maincopy: 12 of 20 checks and every seam check fail. On this branch all pass. New CI step "Dashboard cold load boot".tests/test_openclaw_doctor_cache.py: 5 tests (no wait, one shared run under 8 concurrent readers, TTL=0, no openclaw on PATH, anddetect()using the cached path). Withopenclaw.pyreverted: 1 failed + 4 errors; restored: 5 passed. Added to the CI list besidetest_openclaw_detection_real.py.Also green locally:
test_first_run_gate_js.py,test_queue_lanes_panel_is_reachable.py,test_backend_outage_overlay.py,make lint-py39 lint-js lint-ac-coverage lint-ci-test-coverage,node --check.Drift Bot: its one finding (AC 3, "callers make independent requests relying on internal deduplication") is answered by making the sharing explicit: one helper, one budget, one direct fetch in
app.js, and a behaviour test for it. The requirement now says where to check it.Remaining (not in this PR)
On Overview and Flow,
updateFlowStatsstill reads/api/overviewevery 2 s (sequential, through the shared helper, never concurrent). Same interval asorigin/main.auth-bootstrap.jsprobes/api/auth/detected-token, which answers 404 on a plain local boot and leaves one "Failed to load resource" console line. Unchanged by this PR.CHANGELOG: the entry was taken out of this PR and goes in with the release.
The live log and health streams still open at startup and hold two of the six connections on every screen. Gating them per tab changes reconnect behaviour and is a separate change.
On a machine where OpenClaw is installed but has no gateway token, boot returns before its refresh timers and streams start (pre-existing). Opening Overview now loads system health and tasks there too, but the early return itself is untouched.
/api/subagentsbehind Overview tasks keeps its 4 s client timeout and a 10 s server cache. In a harness pointed at this machine's real gateway with a rejected token it went past 4 s; with a closed gateway port it did not. Not seen as a real-install failure, so not changed.With the daemon writing continuously, the Sessions list (
/api/transcripts, about 29 s on both sides) and/api/inventory(12 s, was 24 s) are still slow on the server. That is store contention under a synthetic worst-case writer, not startup fan-out, and it is unchanged here.Under the same busy daemon, Overview's outcome and activity-today tiles abort their first request at their 3-4 s client timeouts, exactly as on
origin/main, with no console error; the next refresh fills them.Not browser-verified separately: a machine with no OpenClaw at all. That path skips the gateway token check, which only makes startup lighter; the boot logic is the same.
Hosted walkthrough after release, including Cost Optimizer: 33s spinner on cold load, debug label and unlabelled figures #5934's optimizer acceptance, happens after merge and release.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jm9d7s4fN55hN3YzQo75o9