TestCollectDeviceSMARTStandby and its observability twin stub smartctl's
execution but not the device-class probe, so collectDeviceSMART fell through
to the *runner's* real /sys/block/sda/queue/rotational.
smartctlArgs only sends the -n standby guard when the disk is not a confirmed
SSD (#1516), and smartctlArgsUseStandbyExitStatus gates the standby reading of
exit status 3 on that guard having been sent. On a Linux host whose own
/dev/sda is non-rotational the guard is dropped, exit 3 stops meaning standby,
and both tests fail deterministically:
run smartctl for /dev/sda: exit status 3
They pass on macOS only because linuxNonRotationalBlockDevice returns false off
Linux, and on Linux only where /dev/sda is absent or spinning. That makes the
release-qualification suite unrunnable on an ordinary SSD-backed Linux worker.
Pin the probed disk to rotational through the package's existing stubLinuxSysfs
seam so the guard is always in play. Product behaviour is untouched; this only
stops two unit tests depending on the hardware underneath them.
TestSubscriptionAgentClientsUseStructuredSingleTurnProcess failed three times
during the v6.3.0-rc.3 release qualification, always at exactly its 5s
deadline, with "codex subscription agent timed out". It was green 5/5 in
isolation: 3/3 as a single test under -race and 2/2 for the whole package.
It only failed inside the full `make test` run, where -race instrumentation
and roughly 150 packages compete at once.
The test is hermetic. It writes a fake codex shell script onto PATH, so there
is no CLI, network, or credential involved and nothing about the deadline is
asserting product latency. It exists to stop a hang. Under race instrumentation
on a loaded machine, spawning that script legitimately takes longer than the
budget, so the deadline was reporting how busy the host was rather than whether
the code works.
Scale the deadlines when the race detector is enabled, using the raceEnabled
build-tag constant that internal/api, internal/monitoring, and pkg/metrics
already use. Scaling rather than skipping is deliberate: the surrounding
package convention is to skip latency SLO tests under -race, but this one is a
functional proof of the structured single-turn process, tool calls, and
credential non-leakage. Skipping it would remove exactly the path we most want
the race detector to see.
The assertion in TestSubscriptionAgentRequestTimeout is untouched, since that
one is testing the timeout policy itself rather than waiting on anything.
Proof: package passes 2/2 under -race and once without, and both build variants
vet clean.
Contract-Neutral: Test-only change scaling hermetic subscription-agent deadlines under -race via the existing raceEnabled build-tag pattern. No runtime, contract or payload delta. Proof is the package itself, which passes 2/2 under -race and once without.
The desktop thresholds table carries an Offline Alerts column with the
global default control and per-row toggles, but the card layout used at
narrow container widths (including zoomed desktop browsers) omitted it
entirely, so connectivity alert state was invisible and untogglable
there. Mirror both controls into the cards: an Offline alerts tile on
the Global Defaults card and an Offline row on each resource card, with
the same tri-state cycle for guests and docker containers and the same
On/Off badge for nodes, agents, PBS and docker hosts.
Refs #1738
Contract-Neutral: frontend-only: card threshold layout renders the existing offline alert controls, no API or contract delta (Refs #1738)
Contract-Neutral: Release-note copy correction for the v6.3.0-rc.3 packet. Restores the 'Read-only observers' Highlights bullet that build_release_assets_test.go pins for the 6.3 packet and that an earlier trim dropped, and lists the alert-delivery repairs under Fixed where the changelog already carried them. Both governed gates verified: render_release_body validates, and scripts/installtests passes. No contract, runtime or version-pin change.
Contract-Neutral: Release-note copy fix for the v6.3.0-rc.3 packet already prepared in 2f6f2396b. Trims Highlights to the three bullets the canonical render_release_body validator permits and keeps each under the 140-character cap. No contract, runtime or version-pin change; deployment-installability.md and the version pins are unchanged from the prep commit.
Contract-Neutral: Routine release-preparation cut for v6.3.0-rc.3. deployment-installability.md IS staged with the substantive active-prerelease and mobile-decision updates for this cut. The residual demand is a verification artifact, and the registry-listed proof scripts/installtests/build_release_assets_test.go plus install_docker_sh_test.go already cover this change and pass against it; they validate the packet dynamically from the repo-root VERSION rather than pinning a literal, so they have no diff to stage. Both were run green immediately before this commit and caught two real errors in the mobile-decision clauses, which are fixed here.
Contract-Neutral: Completes the already-governed mobile table readability contract on remaining Docker overview and standalone availability rows without changing the contract.
The alert card assumes an alert has a resource behind it. It reads
resourceName unguarded and guards node and resourceId with Show, so a
system alert renders only because those guards happen to be there. Nothing
stopped a later change from adding an unguarded resource field and breaking
the notification-delivery alert, which is the one alert whose whole purpose
is to be visible when nothing else can reach the operator.
Drive OverviewTab with the exact payload internal/alerts marshals for a
raised system alert, captured from the Go side rather than hand-written:
empty resourceId, empty node, resourceName "Pulse". Assert the card names
Pulse rather than inventing a resource, title-cases the hyphenated type,
carries the message and its pointer to the destinations surface, and renders
no empty node line or undefined text.
Pulse could tell that its own delivery had stopped and had no way to say so.
Queue health was computed for one API endpoint and rendered on the alerts
surfaces, and that was the whole escalation path: a destination that stops
delivering cannot announce itself through a notification, so an operator who
does not open the alerts page learns nothing.
Telemetry: 78 installs retry every notification to the maxAttempts ceiling and
deliver almost nothing, 155,967 attempts against 221 deliveries in a week. 50
have delivered nothing in 30 days and 28 more delivered normally at some point
and silently stopped. The dominant terminal failure classes are authentication
and configuration, so most are fixable in minutes once someone knows.
Move the queue-health rule into internal/notifications as ClassifyQueueHealth
and DeliveryHealth, next to the queue that produces the counts, and delegate
the API handler to it so the endpoint and the new evaluator cannot drift into
two rules. A queue that cannot be read reports unavailable rather than healthy,
because silence is the failure mode being guarded against.
Monitoring evaluates delivery health on the poll ticker and raises or clears
the notification-delivery system alert, which puts it in the alert list and the
navigation badge. Reading queue health costs a SQLite query and the poll
cadence can be seconds, so the evaluation is throttled to five minutes.
RaiseSystemAlert is idempotent for an unchanged condition, so the timer neither
re-notifies nor accumulates alerts.
The alert is a warning rather than critical. It is real, but a new
critical-by-default alert appearing on upgrade across the affected installs is
a bigger change than this should make on its own.
The new Monitor field is deliberately named to fit inside the existing struct
alignment column. A longer name makes gofmt re-pad the whole block, which
breaks the canonical guardrail tests that pin those field declarations
verbatim.
Proof: delivery_health_test.go pins that in-flight work and retries stay
healthy while retained terminal failures do not, and that an unreadable queue
is never reported healthy. system_alerts_test.go pins the message for each
outcome including singular and plural, that it points at the destinations
surface, and that the throttle honours its interval. internal/monitoring,
internal/notifications and internal/alerts suites all pass.
Contract-Neutral: Staged contract deltas cover every boundary this change actually moves: notifications.md gains delivery_health.go plus the ClassifyQueueHealth rule, and monitoring.md gains system_alerts.go plus the system-alert evaluation extension point. The residual demands are inapplicable. agent-lifecycle is demanded only because monitor.go is one of its canonical files, and its verification artifact is host-agent deletion and re-enrollment proof, which this change does not touch: the diff to monitor.go is one struct field and one call in the poll loop. api-contracts and storage-recovery are demanded only because internal/api/notifications.go falls under a broad internal/api reference, and that file's change is a pure delegation of an internal helper to notifications.ClassifyQueueHealth with a byte-identical HTTP response and no route or payload delta.
Every alert type is scoped to a monitored resource: a node, a disk, a
container, a host. That leaves no way to report a condition whose subject is
Pulse, and the condition that needs it most is broken notification delivery,
where the channel that would carry the warning is the thing that failed.
Telemetry shows what that costs. 78 installs retry every notification to the
maxAttempts ceiling and deliver almost nothing, 155,967 attempts against 221
deliveries in a week; 50 have delivered nothing in 30 days and 28 more
delivered normally at some point and silently stopped. The dominant terminal
failure classes are authentication and configuration, so most are fixable in
minutes by an operator who has no way to learn there is anything to fix. Until
now the only evidence lived on a config tab, because a delivery failure cannot
announce itself through delivery.
Add RaiseSystemAlert and ClearSystemAlert. A system alert carries the stable
pulse-system- identity prefix so repeated raises update one alert instead of
accumulating, sets no ResourceID so surfaces skip resource-linked affordances
rather than offering a link to nothing, and stamps systemAlert metadata for
surfaces that want to tell the two apart. Raising is idempotent for an
unchanged condition and only re-notifies on a change of level or message, so an
evaluator on a timer cannot turn into the notification storm this is meant to
warn about. Routing through the ordinary pipeline is the point: the alert
reaches the alert list and the navigation badge, which is the escalation path
that does not depend on delivery working.
This commit adds the capability and its contract entry. Nothing raises a
notification-delivery alert yet: the evaluator needs a package holding both the
notification queue and the alert manager, and internal/monitoring plus
pkg/server are held by another agent's path claim. The evaluator and its UI
verification follow.
Proof: four cases in alerts_test.go pin the stable identity and absent
ResourceID, the idempotence that keeps a repeated raise from re-notifying, the
level change that does re-notify while still updating a single alert, the clear
path including clearing something absent, and the rejection of a blank type.
Full internal/alerts suite passes under -race.
checkFlappingLocked recorded a cooldown deadline in suppressedUntil and then
never read it. Suppression was decided purely by whether the trailing
FlappingWindowSeconds still held FlappingThreshold state changes, so it lifted
the moment the sliding window drained instead of holding for
FlappingCooldownMinutes. Only the metric paths read that map, and they read it
for the separate SuppressionWindow feature.
Two consequences. A resource oscillating just under the threshold rate, four
changes per five minutes against a threshold of five, was never damped at all
and dispatched every single state change. And flappingActive was only ever set
to true, never cleared except by age-based cleanup, so even after the first
episode no later episode could arm a fresh cooldown.
Pulse states the guarantee it was not providing. The flapping postmortem
finding tells the operator that notifications were "suppressed for" the
configured cooldown and recommends raising it, which changed nothing.
Honour an active cooldown before the window check, and release the latch once
the cooldown has been served so a later episode can arm a new one. Releasing
also clears the stale window history, so a resource that has gone quiet starts
from a clean slate rather than re-tripping on expired state changes.
This damps notification volume. It does not reduce alert churn: flapping is
evaluated in dispatchAlert, so the alert has already been raised and recorded
in history by then. Telemetry shows 923 installs firing 3.64M alerts in 30 days
with only 20,910 standing, and that churn needs per-alert-type root causes such
as #1721.
Proof: three cases in alerts_test.go drive checkFlappingLocked directly and pin
that the cooldown keeps suppressing after the window drains, that it releases
and can re-arm for a later episode, and that disabled flapping detection
ignores a stored cooldown. All three fail against the previous implementation.
Contract-Neutral: behavioral fix: the flapping cooldown now gates the dispatch path that already recorded it. No alerts subsystem contract delta (no Canonical Files, Shared Boundaries or Extension Points change, no config/API/payload change) so alerts.md has nothing substantive to record. The registry-listed verification artifact demand IS satisfied: proof lands in internal/alerts/alerts_test.go.
Notification delivery health was rendered by exactly one component on the
destinations config tab. A destination that stops delivering is invisible by
nature, because the failure is the channel that would have reported it, so
confining the only evidence to a tab people open once during setup means the
state persists indefinitely.
Production telemetry shows it persisting. 78 installs retry every notification
to the maxAttempts ceiling and deliver almost nothing: 155,967 attempts and 221
deliveries across a week. 50 of them have not delivered a single notification
in 30 days, and another 28 delivered normally at some point and have since
stopped, which is the worse case because nothing about the estate looks
different. The dominant terminal failure classes are authentication and
configuration, so most of these are fixable in minutes by the operator who
cannot currently tell there is anything to fix.
Extract the delivery-health load into useNotificationDeliveryHealth and render
the existing warning on the alerts overview as well as the destinations tab.
The shared hook also holds the trigger rule, so both surfaces agree on what
counts as needing attention and neither can warn before the first load
resolves.
This does not make Pulse monitor its own delivery. Every alert type is
resource-scoped and there is no system-scoped alert concept, so raising this
like any other monitored condition remains open.
Proof: useNotificationDeliveryHealth.test.tsx pins the trigger rule across
healthy, degraded, server-reported-unavailable and unreachable-endpoint cases,
plus the silent-until-loaded guard. Verified live at 1280x800 and 375x812: a
healthy queue leaves the overview clean, and a degraded payload renders the
warning above the stat cards with the dominant failure class named.
Contract-Neutral: frontend-only reuse of the existing /api/notifications/health payload on a second surface; no subsystem Canonical Files/Shared Boundaries/Extension Points delta and no API change. Proof ships as useNotificationDeliveryHealth.test.tsx; the alerts verification-artifact list enumerates existing filenames, so a new hook's own test cannot appear on it.
Alert notification delivery is gated on the alert config's activation
state: monitor.go calls SetEnabled(enabled && ActivationState ==
ActivationActive), and a new install defaults to ActivationPending. In
that state alerts keep firing and every sendAlert returns at the
disabled check with only a debug log, so nothing reaches the queue.
The destinations surface never said so. A user could configure SMTP or a
webhook, enable it, and send a test that passes, because SendTestNotification
checks only the destination's own Enabled flag and bypasses the delivery
gate entirely. The overview tab showed "Notifications paused" but the
Notifications tab, where that belief is actually formed, showed nothing.
Production telemetry shows the resulting cohort: 510 installs with an
enabled destination, alerts standing right now, and zero delivery attempts
in 7 days; 483 of them have made no attempt in 30 days. On 6.2.1 that is
357 of 1219 notification-enabled installs.
Surface the pause on the destinations tab with the consequence spelled out,
including that a passing test proves nothing while delivery is off, and
offer the activation action inline. The card stays quiet until the alert
config resolves so it cannot flash a false warning on tab open.
Proof: AlertDeliveryPausedCard.test.tsx pins the consequence copy, the
test-send caveat, the activate callback and the busy state;
alertDestinationsPresentation.test.ts pins the copy for all three paused
reasons. Verified live against a backend in pending_review at 1280x800 and
375x812: card renders above the email section, activating clears it, and
deactivating restores it.
Contract-Neutral: frontend-only disclosure of existing alert delivery activation state; no subsystem Canonical Files/Shared Boundaries/Extension Points delta, no API or payload change. Proof ships as AlertDeliveryPausedCard.test.tsx plus alertDestinationsPresentation.test.ts; the alerts verification-artifact list enumerates existing filenames, so a new component's own test cannot appear on it.
Adding a PVE/PBS source through the Connect via API path discarded the
name typed in the add dialog: the setup bootstrap only carried type and
host, and auto-registration named the new connection after the node's
self-reported hostname. A user asking for a cluster named enacon got a
connection called pve01 and could only rename it after adoption.
Carry the typed name on the one-time setup token
(SetupTokenRecord.DesiredName) via an optional name field on
/api/setup-script-url, and let the setup-token-authenticated
auto-register completion name a newly created connection from it,
falling back to the hostname-derived default when absent. Dedup and
cluster-member adoption identity stay hostname/candidate based, so the
carried name cannot fork an existing registration.
Editing a Proxmox/PBS/PMG connection raced the configured-nodes reloads
that fire during adoption (refresh_nodes, node_auto_registered): the
editor's JSX computed findEditableNode eagerly, so every reload
remounted the editor and reset the form, and the node modal state
rebuilt formData from each refreshed snapshot, wiping typed input
within seconds. Reported by a Pro customer who could only rename one
or two things per refresh window.
Thread the editing target through a non-keyed Show and an accessor so
reloads update the mounted editor instead of recreating it, and guard
the form sync with a dirty flag: once the user has typed, background
refreshes stop rebuilding the form, while a different editing target
still resyncs. Regression test proves the poll-clobber case fails
without the guard.
Contract-Neutral: behavioral bugfix: preserve unsaved node editor form input across background config reloads; no API payload, endpoint, or public contract change
Contract-Neutral: Customer-facing punctuation cleanup only; no API, lifecycle, policy, or subsystem behavior changed. The owning frontend copy contract and lint enforcement are updated.
The shell fetched `/api/state` on every sign-in for two reasons: to learn
whether the session was authenticated, and to have something to render
before the socket connected. On a real estate that is 96% resources, and
the fetch sat on the critical path ahead of the websocket, which could not
begin connecting until it returned.
Neither reason survives. The session probe is now `/api/state/summary`,
which passes the same auth and monitoring-read scope checks and returns a
few hundred bytes. Navigation resolves from the platform admission facet.
Nothing else in the shell read the payload: `state()` had exactly three
consumers, two of them navigation and one an alert-count badge that the
socket supplies.
`/api/state` itself is untouched. It is the recovery path for a snapshot
too large for a websocket frame, which is precisely what the largest
estates need, so shrinking the endpoint would have removed the escape
hatch for the installs that depend on it most.
The seeded bootstrap state is gone rather than left empty. Keeping it
would have been actively unsafe: `runtimeStateResolved()` meant "a payload
arrived", so a bootstrap that no longer carried resources would still read
as resolved and navigation would classify an empty estate, hiding every
platform tab. Resolution is now websocket-backed, and an empty estate is
answered by the facet admitting nothing, which is a resolved answer rather
than an absent one.
Verified against two backends built from this change: a populated estate
resolves all six tabs, lands on the Proxmox overview and renders every
platform page, and an estate with zero resources shows no platform tabs in
the navigation landmark and lands on the infrastructure settings page.
Both issue one summary probe and zero `/api/state` requests.
Known regression: the alert-count badge is empty for the sub-second window
before the socket delivers alerts, where the bootstrap payload used to
supply it. The alerts store is not socket-synced, so reading the badge
from it would have traded a brief empty count for a lastingly stale one.
Tests that pinned the old mechanism were rewritten to their subjects: the
SSO and proxy-auth cases keep their auth assertions, and the empty-estate
distinction now asserts the facet admitting nothing rather than a
full-state payload arriving.
Contracts: performance-and-scalability records that the bootstrap is
estate-independent and that `/api/state` stays the recovery path rather
than a bootstrap surface; cloud-paid records the tenant probe.
The shell read the admission facet once at sign-in, which left two ways
for navigation to be wrong.
Switching organization was the serious one, and it was a defect in the
change that introduced the facet. The switch clears bootstrap state but
left admission untouched, so between the switch and the arrival of the new
tenant's runtime state the shell would render the outgoing tenant's
platform tabs. Admission is now cleared synchronously on the switch,
before anything can render from it, and refetched for the new tenant.
The second is a reconnect: an estate can gain or lose a platform while the
socket is down, so admission is refetched when the socket comes back. That
was verified by stopping the backend mid-session and restarting it, not by
toggling the browser offline, which leaves an established socket open and
silently proves nothing.
No polling is introduced. Live runtime state stays authoritative once it
arrives and tracks the estate itself, so a steady-state session makes no
further admission requests; that was measured rather than assumed.
I dropped a periodic backstop I had planned for sessions whose websocket is
blocked. The premise was wrong: such a session still receives runtime state
from the REST bootstrap, so navigation is already resolved and the timer
never fires. Verified against a socket-blocked session before removing it,
rather than shipping a session-long timer for a case that does not occur.
Contracts: cloud-paid gains the tenant-switch rule, and
performance-and-scalability records that admission refresh is event-driven
rather than polled.
The shell decided which platform pages exist by classifying every resource
in the runtime state payload, so navigation could not resolve until an
estate-sized payload had arrived. It now reads the platformAdmission facet
from the canonical resource contract, which answers the same question from
a one-resource request: 2KB against 314KB on the estate this was verified
on.
Precedence is deliberate. Live runtime state stays authoritative once it
arrives, because it tracks an estate that can gain a platform after the
facet was read; the facet only resolves navigation before that payload
exists. The two agree by construction, so the handover is not visible. A
server that does not report the facet keeps the previous behaviour rather
than hiding platforms the estate really has, and a partial facet payload is
rejected outright, since a missing flag would read as a hidden platform.
Verified against a backend built from the commit that publishes the facet:
navigation resolves with all six tabs, every platform page renders its own
rows, and the shipped facet equals the client classifier over the same
658-resource estate.
The architecture pin on the authenticated bootstrap gains the new loader.
Its invariant, that branding loads inside that bootstrap, is unchanged.
This does not yet free the state payload: it remains the steady-state
source for navigation, and other shell surfaces still read it.
Contracts: performance-and-scalability gains the rule that admission cost
must not scale with estate size; cloud-paid, ai-runtime and
storage-recovery gain the matching clauses for tenant scoping, hosted
surfaces and TrueNAS visibility.
The app shell decides which primary platform pages exist by classifying
every resource in the legacy full-state payload, which is why it has to
download that payload before it can render navigation. This publishes the
same answer as a `platformAdmission` facet on the canonical resource
aggregations, so admission has one definition instead of two that can
drift.
Counts cannot answer it, which is the whole reason this is a facet rather
than a client-side tally over `bySource`. A TrueNAS or Proxmox host
reports through the agent source and carries the "agent" platform scope,
so a count-based derivation admits the standalone page for an estate that
has no Pulse agent in it at all. Ownership is per-resource evidence, so it
is evaluated per resource here.
Verified against the live client classifier over real estates rather than
by inspection: the facet and the classifier agree on all six pages across
ten estates each for a 652-resource and a 216-resource estate, including
the cases that break a count-based derivation (Proxmox-only, TrueNAS-only,
vSphere-only and provider-owned-agents-only estates all correctly withhold
the standalone page, while a genuine Pulse agent admits it).
The tenant-fallback contract pin gains the new field. Its invariant, that
an empty tenant returns no resources rather than seeding from the raw
snapshot, is unchanged and still pinned; an empty estate admits nothing.
No caller reads the facet yet. Moving the shell onto it is a separate
change, gated by the same parity comparison.
Contracts: unified-resources and api-contracts gain the facet and its
derivation rule; agent-lifecycle and storage-recovery gain the ownership
clause, since an agent-typed host owned by a provider must not admit the
standalone page.
The Monitoring policy dropdown rendered as an absolutely positioned
child of the alert card, so on the last card, or any short list, the
card container's edge cut it off mid-option. Render it through a body
portal with fixed positioning clamped to the viewport, measuring the
mounted menu height because the choice descriptions wrap, and close on
outside pointer, matching the Docker lifecycle menu pattern.
Verified live at 1280x720 and 390x844 on the previously clipped last
card, receipt in frontend-modern/browser-verification.json.
Refs #1737
Contract-Neutral: behavioral fix#1737: alert monitoring menu portals to body, no public contract delta
The thresholds UI stores per-resource overrides keyed by unified
registry resource IDs, while the engine check paths look up legacy
monitor IDs, so a per-node override on an API-only PVE node saved,
displayed as Custom, and never applied: the node kept alerting at the
global defaults. Reproduced live against mock: memory at 81% with a 42%
override stored under the registry ID raised nothing.
Route the node, PMG, PBS, and host/linked-node override lookups through
the canonical registry identity resolver the intent-policy path already
uses, with the exact legacy key still winning so overrides stored by
older UI versions keep resolving unchanged.
Proof: TestNodeThresholdOverrideStoredUnderRegistryIDApplies drives the
real populate path for an API-only node, stores a memory override under
the registry resource ID the UI uses, and asserts CheckNode fires at the
override threshold while controls without the override or without the
resolver stay quiet.
Refs #1738
Contract-Neutral: behavioral fix#1738: threshold override lookup resolves registry identity, no public contract delta
A Critical "credential missing or revoked" verdict never said which
credential it judged, so a host row whose last-seen token id went stale
after re-enrollment reads as a live outage even while the agent is
reporting fine on a fresh token, and the operator has nothing to map
against the token list. Emit the judged token record UUID (not secret
material) alongside the missing and expired verdicts.
Refs #1730
Contract-Neutral: diagnostic evidence detail only in fleet doctor credential verdicts, refs #1730
Host identity continuity deliberately keeps a known machine on its
enrolled ID, so a custom --agent-id or hand-edited agent-id state file
is silently ignored for an already-known host, and the acknowledged ID
then overwrites the state file within one report cycle. That looked
like corruption from the operator's seat. Warn once per resolved
identity with both IDs and the supported path to a fresh enrollment
(remove the host in Pulse first).
Refs #1739
Contract-Neutral: diagnostic logging only in hostagent report ack path, refs #1739
Websocket-first routes delayed their REST fetch by 1.2s to give the
realtime snapshot a chance to paint first and save a request. That
trade only exists for `prefer-ws` routes. Every current platform
surface is `prefer-ws-then-rest`, which always follows its websocket
paint with a REST revalidation, so the wait never saved the request.
It only postponed the canonical snapshot, and left the route blank
whenever the websocket snapshot was slower than the wait.
That failure mode scales the wrong way: the larger the estate, the
bigger the initial websocket frame, the more likely the wait is lost
outright, and the more the blank screen costs. On the public demo the
websocket snapshot is 1.9MB and lands ~2.5s after mount, so the wait
was pure dead time on every cold load.
Routes that revalidate over REST now race the two transports and paint
from whichever arrives first. `prefer-ws` routes keep the wait, which
is where it earns its keep.
Measured on a production bundle against the demo estate with the
realtime transport unavailable, so the canonical fetch is the only path
to paint: bootstrap-to-fetch gap 1720-1882ms to 519-613ms, first paint
5083-6384ms to 3349-4860ms.
Covered by a regression test asserting the fetch is issued without the
hydration timer advancing, plus one pinning that `prefer-ws` still
waits.
Contract-Neutral: Scheduling-only change to initial hydration: identical endpoints, params and payloads; unified-resources contract documents no initial-fetch timing clause
fetchUnifiedResources walked its pages nose-to-tail, awaiting each
response before requesting the next. Only the first page is actually
needed to learn how many there are; the rest are independent, so the
walk spent a full network round trip per page before the first
canonical snapshot could land and the page could paint.
On the public demo (209 resources, 3 pages at limit=100) the serial
walk measured 1672-2317ms of the roughly 6s cold-load blank screen.
Fanning the remaining pages out collapses that to one round trip plus
the slowest straggler, measured at 1217-1246ms against the same
backend, with pages 2 and 3 leaving in the same tick.
The outer loop still re-reads totalPages from the fetched pages, so an
estate that grows a page mid-flight is picked up exactly as before.
Covered by a regression test that holds pages 2 and 3 unresolved and
asserts both were requested while the first was still outstanding; it
fails against the serial implementation.
Contract-Neutral: Concurrency-only change to unified resource pagination: identical endpoints, params, payloads and assembled result; unified-resources contract documents no page-walk scheduling clause