Commit Graph

9543 Commits

Author SHA1 Message Date
rcourtman 054ef7de13 Merge metrics-only resource deltas by patch shape
The 2026-08-25 evening perf pass re-profiled the remaining frontend
costs on the 50-node mock rig after d5440ff43. Idle main-thread burn on
a throttled phone (~10.5s of long tasks per 30s on the worst-case mock)
and residual warm tab-entry cost both decomposed into per-changed-row
work that ignores what actually changed: every metrics tick deep-cloned,
re-canonicalized, and fully re-merged each patched row, and both store
commits then deep-unwrapped the merged rows again through whole-row
keyed reconciles.

The server's resource delta is a JSON merge patch, so the change shape
is already known. applyResourceStateDelta now records the top-level
keys each patch touched (platformData expanded one level), the
connection store publishes them with the resource revision and unions
them across the bounded history and the hidden-tab deferral set, and
mergeCanonicalResourceDeltaSnapshot takes a fast path for changed
non-host rows whose keys stay within the pass-through metric fields,
the proxmox facet mirror, and the four platformData metric mirror
leaves: the previous display row with just those subtrees cloned in
(a manual plain-data clone — structuredClone's per-invocation setup
dominates at this size). Both commit sites write fast rows as per-key
subtree patches (nested reconcile for records, direct sets for
primitives and platformData leaves) instead of whole-row reconciles,
and the connection store commits aligned ticks per index. Any other
change shape — additions, removals, repeated patches in one frame,
structural keys, agent rows (host coalescing) — keeps the full
clone-canonicalize-merge path, and the fast output is pinned
content-equivalent to it in resourceStateAdapters tests; a websocket
store test pins the per-key change shapes, the meta-history union, and
its unknown-shape contamination.

Measured on the pulse-dev rig (VM-to-VM, 50-node/1500-resource mock,
worst-case RandomMetrics, vs two d5440ff43 baseline runs): mobile-4x
idle long tasks 10.8/10.2s -> 3.9s per 30s; warm Alerts entry 5-rep
median long-task total 3.85s -> 1.52s; mobile warm Overview return
3.8/1.7s -> 1.0s; desktop Backups entry settle 1.06/1.20s -> 0.71s;
warm Storage/Overview sub-tab switch medians flat within rep spread;
desktop cold load and /api/state size unchanged.

Browser-verified on the rig build of this tree: live ticks render
through the fast path with per-cell DOM-vs-store consistency at two
samples 30s apart, zero console errors, desktop and 390px/4x mobile.
v6.4.0-rc.1 helm-chart-6.4.0-rc.1
2026-08-25 15:40:05 +01:00
rcourtman 6e5571997e fix: show RAID controller member disks in Proxmox storage 2026-08-25 15:19:28 +01:00
rcourtman d5440ff43e Slim the resource stream and catch up realtime gaps incrementally
The 2026-08-25 audit follow-up profiled the remaining frontend costs on
the 50-node mock rig. Entering a tab more than one websocket tick behind
the shared cache fell off the incremental path onto a full-estate
deep-unwrap merge — a 2.1s unwrap dominated warm Alerts entry at 4x
throttle — and the client-facing resource stream re-shipped ~3KB of
mostly-static metadata per resource on every snapshot, REST recovery,
and reconnect (governed gap resource-payload-static-metadata).

The connection store now keeps a bounded per-tick changed-id history and
serves unions from it, so an instance that mounts or resumes a few
revisions behind catches up with a delta merge over the union instead of
remerging the estate. Uncovered gaps and full-snapshot commits still
fall back to keyed full reconciliation.

Broadcast payloads dedupe the estate's distinct capability blobs into a
content-addressed state-level capabilityCatalog referenced per resource
by capabilitiesRef; default-posture resources (internal sensitivity,
cloud-summary routing, no redactions) omit policy and aiSafeSummary; and
canonicalIdentity.aliases no longer duplicates supersededIds. Ingestion
reverses all three: refs expand through the catalog as per-row clones
(reconcile mutates adopted objects in place), missing policies
synthesize the default posture so a policy:null transition patch cannot
strand a stale governed policy, and identity alias resolution consults
supersededIds explicitly. Slimming edits only the per-broadcast copy,
never stored monitor state, and the delta engine's generic top-level
field diff carries catalog changes in the same frame as the first
resource referencing a new entry.

Rig A/B on the pinned 50-node mock (same VM as the morning baseline,
single runs, +-30% variance; RandomMetrics mutates nearly every row per
tick, so this estate is the worst case for the catch-up union): mobile
4x Alerts entry settles 3.9s -> 2.0s with long tasks 5.5s -> 3.3s,
idle-30s long tasks drop 12.6s -> 10.8s mobile and 3.4s -> 2.8s
desktop, remaining steps flat within variance. Sparse-delta estates
gain more, because their catch-up unions stay small.

/api/state on the pinned mock estate drops 4.75MB -> 4.09MB (-13.9%);
the capability catalog is 7 entries totalling 2.5KB where 946 resources
previously inlined the blobs, and 806 default-posture resources shed
policy and aiSafeSummary. Browser-verified against a live build of this
tree: byte-identical UI outcomes vs the unmodified baseline across
overview/storage/docker/alerts probes at both viewports, with docker
container lifecycle actions enabled through catalog-expanded
capabilities. pulse-mobile and pulse-enterprise do not read the slimmed
fields; the AI runtime consumes the internal model, which is unchanged.

Contracts for monitoring, api-contracts, unified-resources,
performance-and-scalability, alerts, agent-lifecycle, and
storage-recovery are amended with the slimmed wire shape, the ingestion
synthesis obligations, the catch-up history, and superseded-id
resolution.
2026-08-25 12:24:32 +01:00
rcourtman 51b7357285 Fix mobile platform tab scrolling
Contract-Neutral: Mobile platform tab rail gesture fix preserves the existing frontend-primitives contract.
2026-08-25 11:46:16 +01:00
rcourtman 7bac525af3 Cut per-tick realtime merge and workload row churn
The 2026-08-25 frontend perf audit measured 21-31% idle main-thread
saturation on phones (4x throttle, 50-node mock estate) with the dominant
costs in the websocket tick pipeline rather than rendering.

useUnifiedResources deep-unwrapped the entire resource store on every
websocket tick in every realtime hook instance (1.7s per 20s idle window on
the Alerts page alone). The realtime effect now skips the store read
entirely when the tick's revision is already applied, and the merging
instance unwraps only the subtrees the delta merge will clone: flagged ids,
host-merge members, and ids absent from the shared cache.

mergeCanonicalResourceDeltaSnapshot re-cloned and re-canonicalized every
agent resource on every tick because a host-merge group could change
without its surviving member being flagged. Host-key groups now refresh
only when a member id is in the delta; a flagged id absent from the
incoming snapshot (removal, or a partner a previous coalesce folded away)
conservatively refreshes every group.

useWorkloads rebuilt every row object each refresh, churning row identity
estate-wide per metrics tick. Rows whose serialized form is unchanged now
reuse the previous object via WeakMap-cached signatures, and refreshes
that change nothing return the previous array identity. The joined
signature cache field this replaces is removed.

Remeasured (mobile 4x, dev attribution, 20s windows): Alerts-page tick
unwrap 1713ms -> 200ms, Overview app busy -27%, workload signature cost
-72%. Contracts for unified-resources, performance-and-scalability, and
storage-recovery are amended to state the group-scoped coalesce refresh
and lazy store-read obligations.
2026-08-25 10:37:20 +01:00
rcourtman 831e55f011 Prepare v6.4.0-rc.1 release 2026-08-25 09:51:51 +01:00
rcourtman 67a9f33273 Accept useUnifiedResources bundle growth from windowing work
The table windowing and touch scroll commits grew the
useUnifiedResources chunk from 5.75 kB to 6.67 kB gzip, past its
budget max and failing the frontend job. Raise only that chunk's
baseline, other chunks keep their existing headroom.
2026-08-25 08:50:06 +01:00
rcourtman fa27530a9b Clear the edit flow focus-return timer on disposal
closeEditFlow schedules a 250ms focus-return timeout that can outlive
the component. In CI the jsdom environment is torn down first and the
stray callback dereferences document, failing the frontend job as an
unhandled error with every test passing. Clear the timer in onCleanup
and drop any pending one before rescheduling.

Contract-Neutral: timer lifecycle guard in edit flow close, no contract or behavior delta, browser receipt in commit
2026-08-25 08:11:21 +01:00
rcourtman d3bfb3bb7b Tolerate heartbeat pings in websocket initial state test
TestWebSocketSendsInitialState asserted initialState arrives as the
second message, but the hub interleaves 30s heartbeat pings with the
connect sequence, so a slow state build on a loaded runner put a ping
there first and reddened main twice in three runs. Wait for each
expected type the way TestWebsocketPayloadContractShape already does.
The delivery and payload assertions are unchanged.
2026-08-25 07:19:24 +01:00
rcourtman a28b3535b0 Stack agent token TLS and command execution checkboxes
The two inline-flex labels in the advanced install options rendered on
one text line with no gap, hiding the command execution option behind
the TLS label. Block-level flex puts each checkbox on its own row under
the existing space-y-3 rhythm. Refs #1775

Contract-Neutral: layout-only checkbox stacking fix in advanced install options, no contract or behavior delta, browser receipt in commit (Refs #1775)
2026-08-25 07:11:45 +01:00
rcourtman 020bfb111b Track windowed scroll canonicalization in boundary pins
c00811b6f moved direct window scroll listeners in the workloads and
infrastructure viewport sync hooks into the shared
bindWindowedPageScrollEvents helper but left the boundary pins
asserting the old direct-listener shape, failing
frontendResourceTypeBoundaries. Pin the helper as the canonical
listener owner and assert the hooks no longer bind directly.
2026-08-25 06:50:08 +01:00
rcourtman 67a54864c3 Restore prettier formatting broken by table gesture commits
format:check on main fails on three files touched by the recent
table-gesture and relay commits, keeping build-and-test red.
Mechanical prettier --write, no functional changes.

Contract-Neutral: prettier-only reformat of committed content, zero behavioral or contract delta; restores format:check on main
2026-08-25 06:13:06 +01:00
rcourtman a5e879c0c9 Keep Android table gestures on the page 2026-08-24 23:51:18 +01:00
rcourtman c00811b6f1 Keep virtualized tables on native touch scrolling 2026-08-24 23:23:11 +01:00
rcourtman 5a941e8d3a Keep phone table gestures on the page 2026-08-24 22:56:17 +01:00
rcourtman 2ca84cddd7 Prevent connection alerts from bypassing offline policy 2026-08-24 22:45:59 +01:00
rcourtman 2f882a569a Close frontend canonical verification gaps 2026-08-24 22:41:14 +01:00
rcourtman 0a58e77bc6 Eliminate remaining frontend template drift 2026-08-24 21:52:47 +01:00
rcourtman a3d7a90b1b Canonicalize platform frontend contracts 2026-08-24 18:56:56 +01:00
rcourtman 164e03ad3c Keep Storage table headers plain 2026-08-24 17:30:21 +01:00
rcourtman baabed91a6 Remove mobile table chevron exception 2026-08-24 17:18:55 +01:00
rcourtman 3d77ac9cec Canonicalize sortable table indicators 2026-08-24 17:06:20 +01:00
rcourtman e140ee2071 Canonicalize object detail history navigation 2026-08-24 16:59:57 +01:00
rcourtman 32b721503c Canonicalize mobile row disclosure affordances 2026-08-24 16:41:06 +01:00
rcourtman b67ac28f25 fix(monitoring): clear host removal blocks from every store on re-enroll
allowHostAgentReenrollLocked trusted only the durable continuity store
when one was attached, so a removal block living only in persisted
monitor state (written by releases predating the store) could never be
cleared: every fresh re-enroll read as already-consumed and the host
was rejected with 400 on every report indefinitely, regardless of new
tokens or reinstalls.

Refs #1772
2026-08-24 16:36:22 +01:00
rcourtman 8d9d567f62 fix(monitoring): honor fixed poll intervals when adaptive scheduling is off
With the adaptive scheduler disabled (the default), every planning pass
on the main poll tick rebuilt each instance task with NextRun=now and
Upsert overwrote the pending slot, so availability targets polled at
the tick cadence regardless of their configured interval. Reproduced
at exactly 10s polling on a 60s ICMP target; c70431caa only covered
the adaptive-scheduler path. Planning now preserves a pending future
slot, tightening it only when a freshly shortened interval justifies
an earlier run.

Refs #1745
2026-08-24 16:36:22 +01:00
rcourtman faf6425d0f Disable hover tooltips for touch interactions 2026-08-24 16:32:38 +01:00
rcourtman 714b26b69f Canonicalize platform table row density 2026-08-24 16:19:21 +01:00
rcourtman 9c7cb9ed9b Require demand_evidence on candidate_lane registration
Every candidate_lanes record now carries demand_evidence: dated pointers
to the demand signals justifying the proposed lane (issues, discussions,
support threads, telemetry findings, or a demand-ledger entry in
pulse-pro/FEATURE_REQUESTS.md), or an explicit named-bet declaration for
maintainer-originated lanes with no external signal yet. The schema
requires the field, status_audit rejects missing or empty lists and
surfaces the evidence in the candidate_lanes and candidate_lane_queue
pretty blocks, and the canonical development protocol documents the
rule. No data migration needed; candidate_lanes is currently empty.
2026-08-24 15:59:32 +01:00
rcourtman 7e363e40e7 Make drawer headers full-width collapse targets 2026-08-24 15:40:29 +01:00
rcourtman 6991208ad6 Register host-identity fork-heal coverage gap
PR #1715 fixed the discovery symptom of forked host identities
(0e8f63cdd): lookups now resolve across the '<base>-<hex>' fork
spellings. The identity defect underneath remains: a re-enrollment
under a new token forks the host and the only heal path
(hostRenameHealSource) requires the same token plus a changed hostname
plus three missed health windows, so a re-enrollment fork fails two of
the three conditions structurally and is held open forever. Every
identity consumer must resolve equivalent spellings indefinitely or
silently miss records.

Healing at the source is clone-safety territory (#1584), so it is
registered as a triaged coverage gap for an owned agent-lifecycle
slice rather than an opportunistic patch. The evidence record carries
the verified heal-path conditions and the field-observed fork shapes,
including the truncated double fork.
2026-08-24 15:25:38 +01:00
Martin Minkus 0e8f63cdd5 Resolve discovery lookups across forked host identities
Host identities fork onto "<base>-<hex>" when an agent re-enrolls under
a new token (re-running an install with --proxmox is enough), and
discovery then stores records under the forked host ID while the PVE
node keeps reporting the base agent UUID as its linked agent.
Store.ListByTarget matches TargetID byte-for-byte and
ListDiscoveriesByTarget did no alias resolution, so the resource
drawer's list call returned an empty 200 and the Discovery tab reported
"no saved discovery run" for hosts that had been analyzed.
GetDiscoveryByResource's normalization missed too: a base UUID matches
neither the forked host ID nor its hostname.

Expand the requested target into every spelling that provably names the
same target (equivalentDiscoveryTargetIDs): a matched node's linked
agent ID and name, a matched host's own ID/hostname pairing, and hex
fork suffixes of any of those, including the twice-forked
truncated-base shape. Filter the list path against that set and seed
the same spellings into the single-record alias path.

Trimmed from PR #1715 as submitted: the transitive hostname closure
(node name -> hostnames -> other hosts' IDs) is dropped. Multi-estate
deployments legitimately reuse node names and hostnames, and the hop
would have surfaced another estate's records in the drawer, discovery
readiness, and AI context. A regression test pins the non-bridging
behavior.
2026-08-24 15:18:30 +01:00
rcourtman 08e501bbc8 Unify drawer detail density 2026-08-24 14:59:02 +01:00
rcourtman f1aee5ccd1 Hide disabled Discovery from drawers 2026-08-24 13:15:45 +01:00
rcourtman 66fd37ee6f Simplify object drawer headers 2026-08-24 12:48:31 +01:00
rcourtman 9e622c291d Improve object drawer information hierarchy 2026-08-24 12:37:39 +01:00
rcourtman a73abe909c Show healthy backup shields in compact rows 2026-08-24 12:04:07 +01:00
rcourtman 78be1b1c2a Raise adaptive node preview thresholds 2026-08-24 11:52:56 +01:00
rcourtman 1fbf10e2c2 Move node preview control below table 2026-08-24 11:33:10 +01:00
rcourtman 095b012455 Align dev-runtime manifest smoke pin with the frontend-build-output policy
6417319b1 split frontend-modern/vite.config.ts out of the
dev-runtime-orchestration path policy into its own frontend-build-output
policy so production build output counts as proof. The
test-hot-dev-bg.sh manifest governance check still pinned the old
policy id, so the Script smoke tests job on main has failed since that
commit. Pin vite.config.ts to frontend-build-output; every other
manifest expectation is unchanged.

Reproduced the [FAIL] locally before the change; the full
test-hot-dev-bg.sh suite passes after it.
2026-08-24 11:29:57 +01:00
rcourtman b932dbf975 Align Guests with table header chrome 2026-08-24 11:25:04 +01:00
rcourtman a585eab712 Canonicalize mobile row disclosures 2026-08-24 11:09:53 +01:00
rcourtman d50f5c664e Hide mobile workload disclosure chevron 2026-08-24 10:56:08 +01:00
rcourtman 791e29ad8d Fix mobile workload column spacing 2026-08-24 10:55:32 +01:00
rcourtman e9fdd18570 Make large-estate navigation and updates incremental 2026-08-24 10:39:15 +01:00
rcourtman 30c07922b4 Fix expanded guest table scrolling 2026-08-24 10:28:16 +01:00
rcourtman dec1c94c27 Define recovery list pagination bounds once in the recovery model
Follow-up to 6686cdce2: the 100-default/500-max list page bounds existed
as separate untyped constants in internal/api and internal/recovery/store,
relying on a contract clause to keep them aligned. Export
DefaultListPageLimit / MaxListPageLimit from internal/recovery/model
(re-exported through internal/recovery) and alias both consumers to them,
so drift is impossible by construction. store_test.go and
recovery_handlers_test.go pin both consumers to the shared constants;
the three pagination-meta contract clauses now name the single source.
2026-08-24 10:21:10 +01:00
rcourtman 9e37d629ac Measure node connection test outcomes
Telemetry could see only saved connections, so an install that tried to
reach a node and could not was indistinguishable from one that never
opened the add-node dialog. Both report zero configured connections and
stall at the same activation stage. Fleet data shows that population is
real and concentrated three to one in container deployments, and nothing
recorded whether those installs attempted a connection at all.

Record node connection test attempts and failures in a bounded,
day-bucketed tally in the config directory, pruned to a 31-day retention
window, and report both over the install-ID rotation window as
node_test_attempts_30d and node_test_failures_30d.

Recording starts only once a request carries a target and credentials, so
an incomplete form is never counted as a node that could not be reached.
A host string that turns out to be unusable does count, because the
attempt was made and it failed. Only the add-node dialog endpoint is
instrumented: instrumenting the unused test-config endpoint as well would
double-count a single operator action.

The tally holds counts alone. Hosts, credentials, and error text never
enter it, which is why it is plain JSON rather than encrypted history.
2026-08-24 10:17:17 +01:00
rcourtman a3f25507bf Restore check-bundle-size.mjs file mode to 100644
The previous commit unintentionally flipped the mode to 100755 via a
temp-index cacheinfo; the script is invoked as 'node scripts/...'
everywhere, so keep the non-executable mode it has always had.
2026-08-24 09:55:11 +01:00
rcourtman 42f3668c25 Walk the entry's transitive static import graph in the preload posture check
Vite injects modulepreload links for the entry chunk's transitive
static import graph, not just its direct imports. The posture check in
check-bundle-size.mjs allowed only direct imports, so a legitimate
future chunk-graph change (a preloaded vendor chunk statically
importing another chunk) would have false-failed the build and invited
a hasty weakening of the check. Compute the allowed preload set as the
static-import closure reachable from the entry instead; lazy route
chunks stay excluded because they are only reachable dynamically.

Re-verified: healthy build passes; injected lazy-chunk preload fails;
rebuilding with preloadDynamicChunks: true fails with 47 lazy-chunk
violations.
2026-08-24 09:54:13 +01:00