Skip to content

fix(panels): deliver boot data to panels that mount after the pass - #6567

Merged
koala73 merged 3 commits into
mainfrom
worktree-stateless-scribbling-engelbart
Aug 13, 2026
Merged

koala73 merged 3 commits into
mainfrom
worktree-stateless-scribbling-engelbart

Conversation

@koala73

@koala73 koala73 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

On mobile, Threat Timeline showed "Waiting for intelligence insight data." for the entire session, AI Strategic Posture sat on a "Scanning Theaters" radar with the elapsed counter climbing past 76s, and the Monitors panel showed its keywords with an empty results area — which reads as "nothing matched" rather than "this panel never got the news". All three now populate.

The cause is one window seen from two ends. Panels below the fold mount lazily on an IntersectionObserver whose margin is 700px on mobile, so a deferred panel is usually not in ctx.panels when its data arrives, and not yet in the DOM when its own constructor-started fetch resolves.

  • Inbound. Three deliveries in loadNews used an optional-chained ctx.panels[key] lookup, which evaluates to undefined and discards the payload — no error, no failed request, nothing in Sentry. They now go through callPanel(), which enqueues when the panel is absent and lets panel-layout replay at lazy-mount.
  • Outbound. StrategicPosturePanel.init() starts its fetch from the constructor, which panel-layout runs before it inserts the element. A warm posture cache resolves in a microtask, so the post-await isConnected guard fired and dropped the render with no retry, stranding the skeleton until the 15-minute scheduled refresh. It now waits for the mount via runWhenConnected() — the idiom LatestBriefPanel, McpDataPanel, TechReadinessPanel, AirlineIntelPanel and RegionalIntelligenceBoard already use.

Note the asymmetry that makes the isConnected guard easy to get wrong: it is genuinely correct after mount (a torn-down panel must not paint) and wrong before it. Only moving it ahead of the fetch separates "never attached yet" from "detached again".

Why only these panels

The obvious reviewer question is whether the other ~90 panels have this. They don't, and the reason bounds the fix.

Every other loader is re-entrant, so its drops self-heal. runGuarded dedupes only concurrent runs (ctx.inFlight, released in finally) — it never remembers a completed one. afterPanelMounted calls primeVisiblePanelData() and schedules a loadAllData pass, and isPanelNearViewport reads ctx.panels[key] (the Panel object, not the shell), so a panel's gate flips true exactly when it mounts and its loader re-delivers on that pass. loadMarkets across nine market panels and loadIntelligenceSignals across eleven can both drop a payload for an unmounted sibling and recover on the next scroll.

loadNews is the only loader that cannot, because shouldHydrateNews skips it once loadedNewsSignature matches the work-list. Auditing everything it delivers gives the complete list:

delivered to status
news category panels already fixed — mount-time backfill in the NewsPanel lazy factory (#5376)
geo-hubs, tech-hubs safe — their lazy factories pull ctx.latestClusters, gated on clustersSettled
insights, threat-timeline were broken — fixed here
monitors (via updateMonitorResults) was broken — fixed here

The NewsPanel backfill comment already names this exact cause ("now that the load runs once per work-list that second chance is gone"). The class was known; three call sites were left out of that fix.

Outbound has a smaller surface still. Only a panel that starts async work before mount can hit a pre-mount isConnected bail — three entry points: a constructor-started fetch, a lazy factory kicking void p.refresh(), and replayPendingCalls, which runs before the element is inserted. Every queued (panel, method) pair was checked against its class: none of the replayed methods contains an isConnected bail, and both factory-kicked panels already wait. StrategicPosturePanel was the only gap.

The audit is a test, not a claim

tests/one-shot-loader-panel-delivery.test.mts encodes the scoping argument above so it cannot rot silently:

  • runGuarded must stay concurrency-only — if it ever remembers completed runs, every loader becomes one-shot and this guard is scoped to the wrong method.
  • loadedNewsSignature must remain the only completion latch — a new one fails with "extend this guard to cover its panel fan-out".
  • No direct panels[key] lookup inside loadNews() / updateMonitorResults() unless allowlisted, and each allowlist entry is re-verified against panel-layout.ts for the mount-time pull that justifies it.

tests/panel-attached-fetch-guard.test.mts already guarded the outbound half — StrategicPosturePanel was simply missing from its list, which is exactly why this shipped. It is now included.

Test plan

  • tests/dom/late-mounted-panel-hydration.test.mts runs the real production order (construct detached → data arrives → append → notifyConnected()), which is what makes the bug reproducible: a test that mounts before feeding cannot see it. It went red before the posture fix and green after.
  • Six mutants planted, all killed: try-arm revert, catch-arm revert, monitors revert, posture-guard removal, a second completion latch appearing, and removal of the geo-hubs mount-time pull.
  • Live confirmation: on a local dev server the late-mounted Threat Timeline copy changed from the constructor's Waiting for intelligence insight data. to renderState's No recent threat metadata available from the intelligence snapshot. — proof the queued call replayed on mount, no working API required (localhost origin is not in the API's CORS allowlist).
  • npm run test:dom 33 files / 298 tests, scoped node sweep 103 tests, tsc --noEmit, biome — all clean.

Related: #6557 — a different defect surfaced by the same production sweep (cii and strategic-risk render correct data under a stale red Error chip, because only setContentHtml clears the error state and both panels write this.content directly). Not touched here.


Compound Engineering
Claude Code

loadNews is the only loader in loadAllData carrying a completion latch
(loadedNewsSignature), so everything it hands a panel is one-shot. Three of
its deliveries used an optional-chained ctx.panels[key] lookup, which
silently discards the payload whenever the panel is still a deferred shell —
the normal case on mobile, where the IntersectionObserver margin is 700px.
Threat Timeline sat on "Waiting for intelligence insight data." for the whole
session, AI Insights on its shell, and the Monitors results area stayed blank
(reading as "your keywords matched nothing"). Route all three through
callPanel(), which queues for replay at lazy-mount.

StrategicPosturePanel is the same window seen from the other end: init()
starts its fetch from the constructor, which panel-layout runs BEFORE it
inserts the element, and a warm posture cache resolves in a microtask. The
post-await isConnected guard then discarded the render with no retry,
stranding "Scanning Theaters" until the 15-minute scheduled refresh. Wait for
the mount via runWhenConnected(), the idiom the sibling self-starting panels
already use.

Every other loader is re-entrant (runGuarded dedupes concurrent runs only,
and mounting a panel schedules a fresh pass), so their drops self-heal — the
new guard pins that scoping argument so a second latch cannot appear
unnoticed.

Claude-Session: https://claude.ai/code/session_017jKtKRp6oyy5mSJ7vmMgNh
Captures why the class is bounded: loadNews is the only one-shot loader, so
its fan-out is the whole broken set, and only pre-mount async work can hit an
isConnected bail. Adds the Late-Mount Window concept alongside the existing
Deferred Tier / Deferred-Shell Contract entries.

Claude-Session: https://claude.ai/code/session_017jKtKRp6oyy5mSJ7vmMgNh
The late-mount test covered Panel's 150ms content debounce with fixed 200ms
sleeps. The pre-push gate runs the whole DOM suite in parallel workers on a
5s per-test timeout, so dead wall-clock in one file surfaces as timeouts in
unrelated ones. Poll for the expected text with a bounded deadline instead:
same assertions, ~3x less test time, and the deadline still throws loudly so
a real regression fails rather than hangs (re-verified by re-killing the
posture-guard mutant).

Claude-Session: https://claude.ai/code/session_017jKtKRp6oyy5mSJ7vmMgNh
@mintlify

mintlify Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Aug 13, 2026, 4:06 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview Aug 13, 2026 4:09pm

Request Review

@koala73
koala73 merged commit 0a6aefa into main Aug 13, 2026
35 checks passed
@koala73
koala73 deleted the worktree-stateless-scribbling-engelbart branch August 13, 2026 16:38

This branch was successfully deployed

2 active deployments
Preview — 3a60d18f Deployed Aug 13, 2026 by vercel[bot]
staging - docs — 3a60d18f Deployed Aug 13, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant