Files
pulse/internal/api
rcourtman 3465804dd1 Stop mock-mode callers that read identity rebuilding the estate view (#2798)
Follow-up to #2778. Stacks cleanly on main; the contract prose
cross-references the wiring change in #2796, so land that first if both
are open.

## What was wrong

In mock mode the unified view is keyed on the fixture data version,
which advances on every metric tick, and building it for the default
1,776-resource estate costs about half a second of CPU (many seconds on
a starved process). #2778 moved the metrics-target lookup onto the
fixture structure revision. The other callers that read only what the
estate lists and how it connects still rebuilt the whole view after each
tick: at startup they made 42 view requests, all misses (the
Docker-alert prune 18, the connection snapshots 13, the host-agent
evaluation 8, the chart prewarm 2, the metrics-target lookup 1), and
with no client connected the monitor tick alone rebuilt the estate about
every nine seconds.

## What changes

- Callers that read identity and topology share the view built for the
current structure revision and operator links
(`currentStructureUnifiedStateView`, `GetUnifiedStructureReadState`,
`currentModeStructureReadState`, in
`internal/monitoring/unified_structure_view.go`; `mockMetricsTarget`
uses the same cache reader): the Docker-alert prune, the workload chart
prewarm, the demo Patrol operator-state resolver, and the host-agent
evaluation. `evaluateHostAgents`, which runs on every monitor tick, now
lists nodes only when an offline host names a linked node.
- The connection-degraded alert feed keeps only platform rows
(`snapshotConnectionsForAlerts` drops every agent row, and a PBS row's
reported node name only becomes a host alias that its snapshot does not
carry). It now builds the aggregator inputs with `platformRowsOnly`, so
it no longer reads the hosts, their desired-config fingerprints or the
PBS instances. The connections endpoint, diagnostics and runtime
inventory still read them.
- Deliberately not moved:
- The mock storage chart (`resolveMockStorageTotal`): a linked pool
takes its capacity from whichever source was seen last, which a tick can
move without a structure change. It stays on the data-version view, read
once per request instead of once per uncached pool.
- `linkedHostForNode`, `getHostAgentTemperatureForNode` and
`carriedTemperatureOutlivesAgentLease` (only reachable with
`PULSE_MOCK_KEEP_REAL_POLLING`): they read a host's status, disks,
sensors and report lease, which a tick moves. Hoisting them would need
the read state threaded through the node poller, and they run after
startup, so they stay as they were.

Real mode is unchanged.

## Measured (pulse-dev, probe builds, one pinned core, GOMAXPROCS=1,
host load 30 to 110, single runs)

| | #2796 alone | with this change |
|---|---|---|
| estate view builds before the listener answers | 2 | 1 |
| estate builds, no client connected, 299 s of life | 33 | 2 (both at
startup) |
| process CPU over a 180 s steady window | 12.3 s | 3.3 s |

A separate run of the pre-#2796 build used 9.0 s over that window, so
read 12.3 against 3.3 as the comparison rather than as exact
percentages. The public demo host runs a mock estate, so the
steady-state saving is the one that matters there.

## Verification

- New tests (in registered proof files): no consumer of the structure
view rebuilds the view after a fixture tick (Docker prune, structure
node listing, workload prewarm, host-agent evaluation including an
offline host that names a linked node); the data-version view still
rebuilds; the structure view follows a structural fixture change and an
operator link; lazy correlation lists nodes only for a linked host; a
storage chart reads the estate only when it is not cached; the alert
feed does not rebuild the view, its platform rows equal the full
aggregator's, and configured PVE/PBS/PMG rows survive without the hosts;
the Patrol resolver does not rebuild the view.
- Negative controls (each fails the named test): prune, prewarm, eager
node listing, structure node listing, alert feed and Patrol resolver
each moved back to the data-version view, and a storage chart that reads
the estate before checking its cache.
- `go vet` and the targeted tests on pulse-dev.

## Known limits

- The structure view can be several ticks old, so a caller on it must
not read status, metric values, timestamps, sensors or anything a link
merges by freshness; the contract says so. The fixture sets pod node
names (which feed the platform aliases a Patrol reference can resolve
through) when the estate is built, not on a tick.
- `pbsInstanceFromReadStateView` never copies `NodeName`, so
`pbsReportedNodeNamesByInstance` appears to feed empty host aliases on
the connections endpoint. That is a separate bug, not changed here.
- The batch sharing of the storage chart's read (`sync.OnceValue` in
`GetStorageMetricsForChartBatch`) is not pinned by a test; the
single-chart laziness is.

Not a feature: no demand-ledger entry.
2026-10-10 13:44:54 +01:00
..
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-08-31 19:47:01 +01:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-04-26 17:26:16 +01:00
2026-01-22 00:30:15 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-03-18 16:06:30 +00:00
2026-09-01 01:02:17 +01:00
2026-03-18 16:06:30 +00:00

Internal API Package

This directory contains the API server implementation for Pulse.

Important Note About frontend-modern/

The frontend-modern/ subdirectory that appears here is:

  • AUTO-GENERATED during builds
  • NOT the source code - just a build artifact
  • IN .gitignore - never committed
  • REQUIRED BY GO - The embed directive needs it here

Frontend Development Location

👉 Edit frontend files at: ${PULSE_REPOS_DIR}/pulse/frontend-modern/src/

Why This Structure?

Go's //go:embed directive has limitations:

  1. Cannot use ../ paths to access parent directories
  2. Cannot follow symbolic links
  3. Must embed files within the Go module

This is a known Go limitation and our structure works around it.