Repository navigation
fix(panels): deliver boot data to panels that mount after the pass - #6567
Merged
Merged
Conversation
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
Contributor
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
IntersectionObserverwhose margin is 700px on mobile, so a deferred panel is usually not inctx.panelswhen its data arrives, and not yet in the DOM when its own constructor-started fetch resolves.loadNewsused an optional-chainedctx.panels[key]lookup, which evaluates toundefinedand discards the payload — no error, no failed request, nothing in Sentry. They now go throughcallPanel(), which enqueues when the panel is absent and letspanel-layoutreplay at lazy-mount.StrategicPosturePanel.init()starts its fetch from the constructor, whichpanel-layoutruns before it inserts the element. A warm posture cache resolves in a microtask, so the post-awaitisConnectedguard fired and dropped the render with no retry, stranding the skeleton until the 15-minute scheduled refresh. It now waits for the mount viarunWhenConnected()— the idiomLatestBriefPanel,McpDataPanel,TechReadinessPanel,AirlineIntelPanelandRegionalIntelligenceBoardalready use.Note the asymmetry that makes the
isConnectedguard 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.
runGuardeddedupes only concurrent runs (ctx.inFlight, released infinally) — it never remembers a completed one.afterPanelMountedcallsprimeVisiblePanelData()and schedules aloadAllDatapass, andisPanelNearViewportreadsctx.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.loadMarketsacross nine market panels andloadIntelligenceSignalsacross eleven can both drop a payload for an unmounted sibling and recover on the next scroll.loadNewsis the only loader that cannot, becauseshouldHydrateNewsskips it onceloadedNewsSignaturematches the work-list. Auditing everything it delivers gives the complete list:NewsPanellazy factory (#5376)geo-hubs,tech-hubsctx.latestClusters, gated onclustersSettledinsights,threat-timelinemonitors(viaupdateMonitorResults)The
NewsPanelbackfill 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
isConnectedbail — three entry points: a constructor-started fetch, a lazy factory kickingvoid p.refresh(), andreplayPendingCalls, which runs before the element is inserted. Every queued(panel, method)pair was checked against its class: none of the replayed methods contains anisConnectedbail, and both factory-kicked panels already wait.StrategicPosturePanelwas the only gap.The audit is a test, not a claim
tests/one-shot-loader-panel-delivery.test.mtsencodes the scoping argument above so it cannot rot silently:runGuardedmust stay concurrency-only — if it ever remembers completed runs, every loader becomes one-shot and this guard is scoped to the wrong method.loadedNewsSignaturemust remain the only completion latch — a new one fails with "extend this guard to cover its panel fan-out".panels[key]lookup insideloadNews()/updateMonitorResults()unless allowlisted, and each allowlist entry is re-verified againstpanel-layout.tsfor the mount-time pull that justifies it.tests/panel-attached-fetch-guard.test.mtsalready guarded the outbound half —StrategicPosturePanelwas simply missing from its list, which is exactly why this shipped. It is now included.Test plan
tests/dom/late-mounted-panel-hydration.test.mtsruns 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.Waiting for intelligence insight data.torenderState'sNo 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:dom33 files / 298 tests, scoped node sweep 103 tests,tsc --noEmit, biome — all clean.Related: #6557 — a different defect surfaced by the same production sweep (
ciiandstrategic-riskrender correct data under a stale redErrorchip, because onlysetContentHtmlclears the error state and both panels writethis.contentdirectly). Not touched here.