Three rules chose a node's single CPU temperature. Node history took
CPUMax first, agent history took cpu_package first, and the node drawer
recomputed the maximum of package, CPUMax and every core in the
browser. On an agent-fed node CPUMax is built from cores alone and can
sit below the package, and elsewhere a core can run above the package,
so the drawer's CPU temperature headline could disagree with the line
drawn beneath it and with the History tab. Node alerts and the CPU
low/record tracking used yet another package-first rule.
models.PrimaryCPUTemperatureCelsius is now the one rule: the hottest
usable package, CPU-maximum or core reading, with a sensor-map form for
agents. Node and agent history, the router's live fallbacks, the
unified resource temperature, node temperature alerts and the CPU
low/record tracking all call it. The drawer reads the resource value
through a new Node.cpuTemperature field instead of recomputing it, and
getCpuTemperature is removed.
Mock mode wrote a second package reading under a cpu.package key into
every agent host, including node-linked ones, from a series unrelated
to the node's static reading. A node and its linked agent now carry one
reading, sampled from the agent's series under the agent's real keys,
and both histories seed from that series, so the current reading
continues the line.
Demo fixture assembly duplicated freshness and operator-link ordering.
Reuse the live monitor builder, retain explicit in-memory links and
capture indexed metrics targets without persisting fixture identities.
vSphere group keys also changed when display metadata arrived, remounting
open drawers and discarding History selection. Keep groups on canonical
parent IDs and use explicit cluster metadata instead of a VM's own name.
AI Discovery ran a separate model-backed service scanner beside Patrol
and the Assistant: its own store, scheduler, model route, API, tool and
drawer UI. No telemetry counter ever measured its use (the telemetry
discovery_enabled flag OR'd it with network subnet discovery), the
maintainer judged it a flop, and the context it produced is already
available to the models through pulse_query, pulse_read and the
unified-resource context sections.
This deletes internal/servicediscovery, internal/ai/infradiscovery, the
pulse_discovery tool, the /api/discovery routes, the mock Discovery
fixtures, the Discovery tabs in the resource, guest, node and Docker host
drawers, the identified-service cards, readiness badges, provenance
markers, suggested web-interface URLs, the Workloads AI Context column,
the chat "Discovery is off" hint and the AI Service Context settings page.
The old settings route redirects to Assistant settings. The model-bound
sanitizer allow-list existed only to pass Discovery context through, so it
goes too, along with the CopyableCodeRow primitive that only the
Discovery tab used.
Network subnet discovery (/api/discover, the Infrastructure discovery
settings) and the discoveryTarget agent coordinates are separate features
and stay. Stored discovery_enabled, discovery_interval_hours and
discovery_model AI settings are ignored on load; the settings API decoder
is lenient, so older frontends saving those fields still succeed.
A VM that Pulse can only read through Proxmox's own memory value (no guest
agent, no Pulse agent inside it) shows a cache-inclusive percentage that can
sit near 100% on a healthy guest. Patrol already treats those sources as "may
include cache" for pressure and recovery; the Workloads row and drawer gave no
hint, so the red bar read as memory pressure (#2762, #2639).
A current reading from status-mem, status-freemem, cluster-resources or
derived-total-minus-used keeps its numbers, colour and thresholds, and is now
qualified: VM rows show "May include cache" ("Cache?" in compact layouts), the
bar tooltip and the drawer's "About this reading" explain it, and the drawer
says what can supply the guest's own figure (a Pulse agent in the guest, or
the QEMU guest agent on a Linux VM, worded as "can" because source selection
does not guarantee a replacement). Every running container reads the cgroup
value, so containers carry the note in the tooltip and drawer only, not a
label on every row. Retained, unknown and unavailable readings keep their own
labels and are never called current.
Mock guests now carry memory provenance and a refreshed observation time;
before, every mock VM and container read "Freshness unknown", which hid this
cue from the demo and from screenshots. Windows VMs never claim Linux meminfo
provenance, and scenario-stopped guests read powered-off.
Refs #2762, #2639
The monitor's rebuild holds the operator-link pass back until the
snapshot and every record source except availability are ingested, then
judges each chain once, so a fold cannot be locked in before a later
source brings a member that outranks it. The mock-mode fixture build
still ran a pass per ingest call, so a chain whose answer depends on the
order its members' sources arrive in (a Proxmox storage, a TrueNAS VM
and a vSphere VM linked in a cycle) could settle on a different row than
the live rebuild: mock mode kept the vSphere VM where the rebuild keeps
the TrueNAS VM.
Export the registry's deferral as DeferManualLinks and
ApplyDeferredManualLinks (the latter takes the stale thresholds the
ingests were given, as the Ingest* calls do) and have the fixture build
use them: defer, ingest the snapshot and the TrueNAS and vSphere
records, run the held-back pass once, then ingest availability so a check linked
to a folded resource still projects onto its primary. A registry with no
links defers nothing, so the shared memoized snapshot is built exactly as
before; a linked build pays one extra refresh round (about 3 ms of 52 ms
on the default estate), the same round the rebuild runs.
TestFixtureGraphAppliesManualLinksAtTheRebuildsBoundary replaces the
per-ingest-stage test: it keeps the availability projection and pins the
cycle against a real MonitorAdapter rebuild, with a per-call control so a
fixture change cannot silently stop separating the two. The unifiedresources
tests pin the exported pair, including that the deferred pass judges
freshness by the thresholds it is given.
The contract also drops a stale line: a check that names a folded
resource by canonical ID follows the fold to the primary, as
TestAvailabilityLinkToALinkFoldedResourceFollowsItsPrimary pins.
Alert evaluation resolves a metrics target for every resource and
windowed metric. In mock mode that went through the unified view cache,
keyed on the fixture data version, which advances on every 2 s metric
tick. Building the view for the default 1,776-resource estate costs about
half a second of CPU, so a process starved of CPU published each build
under an already-stale version and the next lookup built again. Startup
runs the first alert pass before the listener opens, so it never got
there: 675 of 929 view requests missed at a 16 s average build, and the
process had not served at 606 s.
A target depends only on a resource's type and source mappings, which a
metric tick never changes. Key the lookup on a new structure revision
(mock.FixtureStructureRevision, the existing structural-only counter)
and the link list instead, and answer from the targets the cached view
captured. An ID the estate does not list resolves to nothing without a
build. The view itself stays keyed on the data version for every other
consumer. The fixture now advances its data version before its structure
revision, and a cache fill reads the revision first, so a reader that sees
a new revision cannot cache the previous estate's snapshot under it.
This is not a regression from 1a094646d7..d2ac0c5112: both commits stall
identically under equal CPU contention and both start in 14 to 16 s at
normal load. Real mode does not pay the cost in steady state either; a
full store pass over 1,694 resources takes about 108 ms.
On pulse-dev, with the process pinned to one core behind four busy
loops, the fixed binary serves at 148 s where the previous behaviour had
not served at 606 s. The remaining startup time there is one view build
per consumer per tick, and the monitor's repeated full resource-store
passes; neither is changed here.
In mock mode the websocket broadcast and /api/state serve a view of the
fixture graph, and the fixture graph was built in a registry with no
store, so it never loaded the operator's manual links. The resources API
seeds its own store-backed registry from that list and applied them, so
a standalone agent linked into a VM showed merged in /api/resources and
stayed a separate row in the broadcast, even after a restart. Peers
verifying link UI on mock stacks had to block the websocket to see it.
The mock view now builds the fixture graph with the links the monitor
adapter's current registry generation loaded (MonitorAdapter.ManualLinks),
through NewRegistryWithManualLinks: a registry with those links and no
store, so fixture ingest cannot persist identity pins, successions or
change rows. The links apply at each ingest stage, as in the live
rebuild, so an availability check that names the folded resource by
source still projects onto the primary. Without links the shared
memoized snapshot is used as before.
The cached view is keyed on the fixture version and the link list, so a
link change rebuilds it without a fixture tick. Its freshness, which the
resources API keys its registry cache on, is assigned in publication
order: a view with the published fixture freshness and links keeps the
published value, and any other view gets a later one than every view
before it.
The fence view behind currentModeReadState still omits links; it only
covers the window after leaving mock mode, before the registry rebuilds.
SetMockMode(false) switched the mock package off and then cleared active
alerts, but every path that evaluates mode-dependent data checked the
mode once, up front. A mock alert pass (started from the ticker with go,
so passes overlap), a frontend or ingest refresh evaluating a GetState
snapshot, a backup-age check over fixture recovery rollups, or a
connection check over the mock ledger could already hold fixture data
when the switch happened. Its evaluations then landed after the clear,
in live mode, where nothing evaluates or resolves fixture resources, so
the alerts stayed until the 24-hour stale sweep.
A pass-level lock around checkMockAlerts was tried and reverted: it left
the other paths open and made the mock-mode API wait behind a whole pass
of synchronous event-log writes.
The monitor now owns a mock-mode epoch fence. The mock pass, backup-age
and snapshot checks, connection checks, Docker pruning and the
resource-store refresh take a scope before reading anything
mode-dependent and send each alert-manager call through it. SetMockMode,
now serialized, flips the mode, ends the epoch, waits only for calls
already admitted, and then clears. Unified metrics are evaluated one
resource per call, so the wait covers single calls, not passes.
Registry rebuilds are tracked rather than waited on. A refresh evaluates
the registry it read back only if no rebuild from an ended epoch
overlapped it, and the readers that open or remove alerts from the
inventory use current state until a clean rebuild in the new epoch
replaces the previous mode's registry. The mock supplemental fixture
sources now serve nothing outside mock mode: the router swaps them for
the real pollers only after the switch, refreshing between sources, so
a single switch to live mode used to raise fixture vSphere incidents.
The new tests park a real evaluation at a fixed point (a held lock or a
blocking supplier), switch modes and release it. Against the unfenced
code the mock pass left about 40 fixture alerts, the frontend refresh 5
vSphere incidents and the backup check 137 backup-age alerts in live
mode, and a mock-mode connection observation counted toward a live
connection's confirmation. The drain, republish and pruning tests fail
against a fence without its wait, overlap check or registry tracking.
Switching mock mode with several tenant monitors running remains
uncovered and is noted in the monitoring contract.
checkMockAlerts evaluated mock nodes, guests, storage, PBS, PMG and
Docker hosts but never host agents. Live agents are evaluated by
CheckHost as each report lands; mock mode discards reports and the host
health sweep never sees fixture agents, so no agent CPU, memory, disk,
temperature or offline alert could open against mock data. A mock agent
at 94% memory sat under an 85% trigger with no alert, and alert work on
agents needed a private build to reproduce anything.
Mock passes now evaluate every fixture agent with the live lifecycle
boundaries:
- offline fixtures go through HandleHostOfflineWithCorrelation, since
CheckHost treats its input as a fresh report and marks it online;
- agents run before nodes, so a node with an online linked agent hands
its metric alerts to the agent on the first tick;
- an agent that leaves the estate on a runtime mock config change goes
through HandleHostRemoved, like a deleted live agent;
- leaving mock mode routes the fixture agents through HandleHostRemoved,
releasing hostname registrations that ClearActiveAlerts leaves behind
and that would otherwise suppress a real node named pve1; a pass already
in flight finds mock mode off and evaluates nothing.
Evaluating agents exposed a mock-only reading: every Kubernetes pod took
a single-pod share (10-80%) of its node's allocatable memory, so any
node running four or more pods, and its linked agent, read a constant
100%. On the public demo's estate that became two permanent critical
memory alerts no real cluster would raise. Pods now share 0.7 single-pod
footprints per node (at most 58% of allocatable in steady state), and a
60% per-node ceiling clips the transient while rescheduled pods'
readings settle, so those nodes stay under 82%.
The demo scenario renamed every generated deployment to one of four
curated names and every pod to one of ten, cycling by index. With the
default 14 deployments and 40 pods per cluster the Kubernetes overview
listed checkout-api four times in one namespace, and the curated
pending and crash-looping pods appeared more than once. Kubernetes
itself refuses duplicate names in a namespace, so the public demo
looked broken.
Ten more healthy services fill out the default deployment count, a
numeric suffix keeps any larger count unique, and pods past the curated
story become healthy ReplicaSet-style replicas of the cluster's own
deployments on ready nodes, so each curated failure appears once.
Contract-Neutral: Mock fixture naming only: demo Kubernetes deployments and pods get unique names; no runtime contract delta.
Rebuilds #2432 as one commit on current main. Its branch conflicted
with the Tailwind 4 move (#2443) in StackedDiskBar.tsx and
DockerTasksTable.tsx; the resolution keeps this change and main's class
renames (rounded-sm to rounded-xs). The eight original commits' reasons
follow.
Read PBS datastore status in the storage vocabulary:
The PBS poller reports a datastore as "available", or "unavailable"
when PBS returns an error for it. The datastore adapter mapped that
string through the generic status mapper, which only knows online,
offline and warning, so every healthy PBS datastore showed as
"Unknown" in the Storage tab and an unavailable one was not flagged.
PVE storage already maps the same vocabulary through its own storage
mapper. Share that mapping so a PBS datastore reads online or offline
like any other storage.
Carry shared PBS storage as one cluster row in the mock estate:
The mock emitted each cluster's two shared PBS storages once per node,
each with its own usage figure. The real poller folds a shared storage
into one cluster-scoped entry, which users asked for in issue 1049, so
the demo's Storage tab contradicted the shipped behaviour: ten rows of
"west-backup-primary" with different usage and growth on a five-node
estate, and a hundred on the public demo.
The fixture now carries the merged shape: one row per store with the
reporting nodes listed and a cluster-scoped id, offline only when no
cluster node is online.
Name Proxmox storage types and shared hosts as operators know them:
The Storage tab title-cased any storage type it did not map, so the
default local-lvm on every Proxmox install read "Lvmthin" and an NFS
share read "Nfs". Those types now carry the names the Proxmox UI uses:
LVM-Thin, LVM, NFS, SMB/CIFS, iSCSI, ZFS, BTRFS, GlusterFS and ESXi.
A storage the poller merged across the cluster showed the synthetic
node marker "cluster" as its host. It now reads "Shared", with the
reporting node count when the storage lists its nodes.
Carry PMG metadata on REST-mapped resources:
The Mail Gateway table reads resource.pmg. The REST-to-Resource mapper
set every other provider facet at the top level but kept pmg only under
platformData, so a row sourced from the REST snapshot rendered every
PMG column (version, nodes, mail in, spam, virus, quarantine, queue,
deferred) as a dash. The websocket snapshot carries pmg at the top
level, so the table filled in only when a websocket row happened to
replace the REST row; on a fresh load the two transports race, and
about half the loads stayed blank for good.
The mapper now carries pmg like the other facets, and a test pins it.
Label multi-disk hosts with their fullest disk in Bars mode:
A host with more than one agent-reported disk draws its Disk cell as
one micro-bar per disk and no figure, while a single-disk host shows a
percentage and Trends mode shows one for every host. Bars is the
default view, and most Proxmox nodes have at least local-zfs beside a
data pool, so the question "which node is running out of space?" had
no at-a-glance answer for the rows that matter most; the column's own
sort key was a number the cell never showed.
The cell now carries the fullest disk's usage beside the micro-bars.
Every bar keeps equal weight and no disk is named in the row; the
mount and the per-disk breakdown stay in the title and the tooltip.
Carry source status, action readiness and technology on REST rows:
The same race that blanked the Mail Gateway table applies to three
more fields the API returns: sourceStatus, actionReadiness and
technology. The REST mapper set sourceStatus only under platformData
and dropped the other two, while websocket rows carry all three at the
top level. Docker container lifecycle actions read sourceStatus and
actionReadiness to decide refusals, storage rows read sourceStatus for
freshness, and search suggestions and the assistant context read
technology, so whichever transport won a fresh load decided whether
those consumers saw the data.
The mapper now carries all three, the REST type declares the two it
omitted, and the PMG mapping test pins them.
Fit the Docker containers table and read Swarm state as words:
On a 1280-1536px laptop window the containers table clipped its short
values: the engine read "docker 2…", host names lost their tail, and
the Update control itself was cut off, while the percentage-only CPU
and memory bars had room to spare. The compact widths now give those
columns their share. On a phone the row dropped to "runni…" with an
unreadable Restarts header beside a wrapped Update button; the phone
row now keeps container, state, CPU, memory and Update, and the
restart count stays in the row expansion until mobile width. The
images table on a phone showed "Not c…" for the update verdict and a
clipped host; it keeps image, size and update check there and defers
host and consumers to the expansion.
Swarm services showed the rollout state as the raw token
"rollback_started" and Swarm tasks showed a raw RFC3339 stamp with
microseconds as their start time. The state reads as words and the
start as an age, with the raw values in the title.
Keep demo Docker container names unique per host:
The demo scenario reused its curated container profiles for a second
round on hosts with more containers than profiles, so a host showed
two running "customer-portal" rows and every image's "used by" list
read "customer-portal, customer-portal". Docker refuses two containers
with one name on a host, so the duplicates read as a bug to anyone
evaluating the demo. A reused profile now takes a numbered suffix.
Contract-Neutral: Storage type labels, shared-host label, REST mapper carrying fields websocket rows already carry, Bars-mode disk label and Docker column fits are presentation and mapping only; owning unified-resources and storage-recovery clauses updated; no frontend-primitives primitive, API or runtime boundary change
The mock generator stamped each job's last and next sync once at
startup, with the last sync up to an hour old on a 15-minute schedule.
Most jobs therefore started out overdue, and on a long-running demo
every job read as hours overdue in red while still reporting Healthy.
The demo's Replication tab looked broken to anyone evaluating Pulse.
Replication timing now follows the fixture clock. Each job keeps a
stable phase inside its schedule window, so a healthy job last synced
less than one interval ago and is next due in the future, and a
failing job's last good sync falls behind by its failure count.
The target node was picked by index and could equal the source, which
showed a job replicating pve1 to pve1. The source is no longer a
candidate, and an estate without a second node gets no jobs.
The full API race run timed out while cloning the entire mock estate and rebuilding every persona for each chart series. Cache one immutable sampler against the canonical graph data version under its publication lock. Preserve values, current revisions, disabled-mode fallback and isolation from private/global persona updates. Pin zero-allocation warm acquisition, reference equivalence and concurrent snapshot binding; compare both paths with the same thousand-VM sample.
Contract-Neutral: Avoid repeated canonical graph cloning and persona reconstruction without changing metric formulas, identity, graph freshness, retention, endpoints or permissions.
Change-source: pulse-maintainer
Rejected active-alert writes still reached history, firing events and
notification dispatch. Make admission explicit across detector and restore
paths so repeated suppressed observations cannot create those side effects.
Reconcile restored alerts when persisted resource policy attaches at startup,
reconcile again after native aliases become canonical, and refresh shared
state before those alerts reach clients.
Preserve occurrence identity when retained alerts re-fire, including durable
history reconstruction and delayed lifecycle replay.
Keep retained table and card components mounted when scrolling shifts the
visible window, so policy editors do not lose unsaved state. Show actual
future maintenance times and the paused-attention effect.
Keep synthetic host machine identities stable across restarts so saved
operator policy remains attached during verification.
Qualify server update and rollback instructions by helper ownership, and
stop signed installer recipes before execution if a download or signature
check fails.
Refs #2237
The public demo runs PULSE_MOCK_UPDATE_INTERVAL=60s, which stretches the
10-cohort supplemental rotation to 600s while the unified registry still
marks TrueNAS/VMware/availability sources stale after 120s. Every row those
sources own spent ~80% of each rotation flagged as a degraded stale source,
so the TrueNAS Storage tab read Healthy 0 / Attention 21 (and the vSphere
page all-Attention) despite healthy fixture data.
Rebase provider-backed fixtures every tick once the rotation would outlive
the 120s freshness budget (at slow ticks that is cheaper per wall-clock
second than the default 2s config's once-per-20s cadence), and derive mock
supplemental stale thresholds from the actual refresh cadence the same way
PVE/PBS/PMG thresholds already derive from their polling intervals. Split
the supplemental uptime step so per-tick refreshes do not advance PBS
uptime by the rotation-scaled step.
Pause the automatic mock metric ticker while the test asserts same-version snapshot identity, then restore it during cleanup. This prevents the full race suite from invalidating the fixture between the two reads it is comparing.
Change-source: pulse-maintainer
Contract-Neutral: The storage wearout sentinel contract is unchanged; mock fixtures now encode unreported endurance as the existing canonical -1 value and carry canonical mock proof.
Mock mode never exercised the backup-running states from "Stop showing
an in-flight backup as a completed one": no guest carried
BackupInProgress, no PBS listing showed an in-flight snapshot, and no
vzdump task was ever running, so the blue Running badge and the Running
artifact chips had no local reproduction.
A post-scenario fixture now flags roughly one guest in twenty-five per
last-backup age bucket - never, fresh, stale, overdue - so the Running
badge appears alongside every badge story, and gives each flagged guest
the evidence a live run actually produces: an in-flight PBS snapshot
shaped the way the PBS API lists one still being written (no size, no
verification, only the guest config blob in files) and a running vzdump
task with no end time. Running after applyDemoBackupScenario keeps the
per-tick reapplication idempotent, because the scenario's curated sweep
drops the previous pass's artifacts before the fixture re-adds them.
The first-cycle showcase guests are excluded: their exact rows are
pinned by the demo posture tests (a failed run three hours ago, a guest
with no recovery evidence at all), and a backup running right now would
mask exactly the states those rows exist to demonstrate. Later profile
cycles repeat the same stories under suffixed names, so the fixture
draws its running guests from them.
The first cut of the fixture ran inside buildFixtureState, but every
fixture graph build and metric tick then applies the demo scenario
layer, which renames guests, aliases cluster instances, forces guest
states, and reprofiles every Docker host. The nested hosts got swept
into the Docker fleet profile cycle: renamed away from their parent
guests, handed native engine inventory the production pct exec path
never reports, and one could be forced offline as the demo's
disconnected host.
The fixture now runs from applyDemoScenarioGraph after the workload
and Docker scenarios, so nested hosts bind to the guests' final names,
instances, and states, and it is idempotent across ticks: an existing
nested host re-syncs from its guest instead of duplicating, one whose
guest stops running goes offline like a real probe target, and the
sync strips every native-agent surface each pass. The Docker scenario
now skips proxmox-lxc-docker: hosts entirely, with the forced-offline
index counted over native hosts only.
The Proxmox page's nested-container row cue, the drawer's nested Docker
card, and the proxmox-pve platform scoping of nested workloads had no
local reproduction at all: mock mode never generated a Docker host with
the proxmox-lxc-docker: identity prefix, so the surface could only be
seen against a live estate with node agents, exec-scoped tokens, and
the server-side inventory opt-in. That is how it shipped invisible.
The fixture now marks the first two running LXC guests as probed
Docker hosts and appends a nested Docker host for each, following the
production proxmoxGuestDockerAgentID convention of
proxmox-lxc-docker:<instance>:<node>:<vmid>. One guest nests a single
container and one nests several, so the row cue renders both its
singular and plural counts. The nested hosts mirror the shape of the
real pct exec inventory: guest-derived sizing and usage, one rootfs
disk, and no native engine inventory, host I/O rates, temperature,
machine ID, or Swarm membership.
The mock unified snapshot resolved a metrics target per resource on a
freshly ingested registry whose view caches were still dirty, so every
resolution fell back to a scan over all source mappings, turning the
loop quadratic at demo scale. One view read builds the caches first,
making each resolution an index lookup.
Contract-Neutral: performance-only cache warm before target resolution, resolved targets unchanged
Stock Proxmox reports no per-mount LXC usage through the status API, so
v6's API-polled containers listed only rootfs. The v5.1.32 fallback that
synthesized mount rows from the container config never crossed to the v6
line, and the v6.2.0 pct-df agent path only covers nodes running the
unified agent. Restore the fallback and improve it: parse size= so
config-only rows carry capacity, mark live usage unknown with the -1
sentinel, and merge without displacing the aggregate-seeded rootfs row.
Frontend consumers stop fabricating percents for sentinel rows: the
workloads row bar and summary math exclude them (tooltip lists them with
capacity), the drawer Filesystems block renders ?/<size> with no percent,
disk normalization preserves the sentinel, and per-machine max-disk
derivations skip them. Mock mode seeds one running container in this
exact shape so the surfaces stay exercised.
Related to #1477
Mock history is seeded for 48h, so chart windows longer than that fall through
to the synthetic generator in mock_chart_history.go. That generator produced
cpu, memory, disk and the I/O pairs but never memoryused, so a 7d workloads
read returned 64 points for every other series and zero for memoryused. The
memory column in host-capacity mode had no series to draw at all, which reads
as a broken column rather than missing mock data. Real installs are unaffected:
the live PVE tick writes memoryused to the metrics store and the store rollup
groups by metric_type without an allowlist.
The synthetic generator now derives memoryused from the sampled memory
percentage and the fixture memory capacity, the same derivation live mock ticks
and the seeder already use, so the series stays continuous across the seed
boundary. Capacity comes from a new fixture registry beside the existing metric
role registry rather than a per-call fixture graph clone. Docker containers and
pods stay out of it, matching the Proxmox-only memoryused contract.
In mock mode every unified read-state access built two throwaway
registries: mock.UnifiedResourceSnapshot constructed one to derive the
resource list, and the monitor's currentUnifiedStateView ingested that
list into another, deep-cloning all resources both ways. Chart requests,
broadcasts, alert evaluation, and API reads each repaid that full cost —
the dominant share of the demo's 76TB/9.5d allocation churn, since every
one of those reads runs against a world that only changes on the 2-second
mock tick.
Introduce fixtureDataVersion, a token that advances on every observable
mock-graph change (metric ticks and the structural changes that bump
fixtureRevision, which stays structural-only so seeded trend history
remains reusable). Memoize the package-level UnifiedResourceSnapshot and
the monitor's mock-branch state view against it, so consumers between
ticks share one immutable build. Sharing mirrors the semantics the
persistent-store ReadState path has always had in real mode: all
consumers were audited — they ingest (which clones), copy before
top-level writes, or build fresh outputs. Real-mode paths are untouched.
Contract-Neutral: mock snapshot memoization: identical data served from cache, no contract delta
A live heap profile of the demo (9.5 days uptime) showed 76TB of
cumulative allocations, with inferMetricRole accounting for 12% of the
total: the classifier table and its keyword slices were rebuilt on every
call, and normalizeMetricRoleTokens constructed a fresh strings.Replacer
per token, paying the lazy trie build each time. Both run per resource
per 2-second mock tick, so the demo spent a measurable share of its
single vCPU feeding the garbage collector.
Both structures are static; make them package-level.
Contract-Neutral: mock allocation hoists: behavior-identical, no contract delta
The agent intentionally reports the digest-pinned image sentinel for
image@sha256 references, where there is no tag to resolve against the
registry. Every surface funneled that through the error branch and
rendered a danger-toned Check failed badge for a state that is not a
failure (raised in #1666). The containers table, the images table, and
the container drawer now render a neutral Pinned state with a tooltip
saying why checks do not apply.
Mock containers also gain update check states (current, update
available, digest-pinned) so these badges are exercisable in mock mode
and on the demo, which previously never populated updateStatus at all.
Contract-Neutral: frontend presentation of the existing digest-pinned sentinel plus mock fixture variety; no wire contract change
v6.2.0-rc.5 shipped with an empty Proxmox workloads table and a crashing
Thresholds page (#1663) while the assertions that catch exactly that were
failing in CI: spec 64 red in the non-gating probation tier two hours
before the tag, and the gating Core E2E verdict red on the release commit
itself — which the release pipeline never consults. integration_tests is
also skipped entirely for prereleases, so the builds users test shipped
with no integration coverage at all.
Close the hole with a release_smoke job that runs for every cut,
prereleases included, and blocks create_release and the release verdict:
four interaction-free render assertions (Proxmox nodes+workloads, Docker
hosts+containers, Kubernetes clusters+pods, Alert thresholds) against the
mock-mode image built from the verified frontend bundle. The mock fixture
graph now always contains one freshly provisioned zero-used guest
filesystem, so the omitted-zero-numerics wire shape that crashed rc.5
stays exercised on every mock-backed surface. Verified locally: the suite
passes on main and fails on the rc.5 frontend for exactly the two shipped
regressions.
8d23529c0 made a configured availability check a first-class, source-owned
resource. It no longer collapses into the resource it matches: the
network-endpoint row survives and owns probe status, incidents, history and
the outgoing checks relationship, while the matched resource carries an
additive facet. The unified-resources contract states that explicitly.
TestFixtureGraphAttachesServiceAvailabilityFixturesToServiceResources still
asserted the old collapsing model. It failed any service target that remained
a network endpoint, selected the matched service by target ID alone even
though the check row now carries the same ID, and looked for the checks edge
on the matched resource rather than on the check that owns it.
The test now pins the documented behaviour. Both the Docker and Kubernetes
checks must keep their source-owned endpoint row, the matched service is
selected by resource type, and the outgoing checks edge is asserted on the
check row for both targets rather than only for Docker.
This failure was invisible in CI. Build and Test runs the frontend suite
before the Go suites, and the frontend has been red since 2026-07-23, so no
Go package ran on main for over a day.
Verified: internal/mock, internal/dockeragent and internal/websocket, the
three packages the first completed post-frontend-fix run reported, plus
gofmt, the canonical completion guard, the status, control-plane, registry
and contract audits, and all thirteen release-control unit test modules.
Adds branch-coverage tests for eight packages whose target functions were
measured at 0% before this change. Every named target was verified to move
by running each package's coverage with and without the new file.
- internal/ai/eval: all 36 Scenario constructors and the four PatrolScenario
constructors 0% -> 100%. These are catalog-invariant tests, not literal
echoes: unique names, populated required fields, runnable assertions, tool
references checked against the agentcapabilities registry, and exact
assertion-count deltas across the env-gated conditional appends. A parity
test scans scenarios.go itself, so adding a constructor without registering
it in the table now fails rather than silently going untested.
- cmd/pulse-control-plane: nine MSP and tenant-runtime print helpers
0% -> 100%, covering the nil, empty-slice and optional-field arms.
- internal/ai/memory: RemediationLog GetByID, MarkRolledBack and
GetRollbackable 0% -> 100%, pinning the overwrite-vs-preserve contract on
RollbackInfo and each falsy arm of the rollbackable predicate.
- internal/alerts/config: AlertConfig.UnmarshalJSON 0% -> 90% and
NormalizeAlertConfigAliases 52.9% -> 94.1%.
- internal/config: RunMigrationIfNeeded 0% -> 100%, copyFile 0% -> 88.9%.
- internal/mock: AvailabilityFixtures, FixtureGraph.SupplementalChanges and
generateMockHostRate 0% -> 100%.
- internal/api: testProxmoxPlatformConnection 0% -> 100% through its injected
connect func, so no network is involved.
- internal/servicediscovery: needsDeepScan 0% -> 100% across every return arm
including the confidence boundary.
No source file is modified. Adversarial review found no rejects; two findings
were acted on before committing, replacing a circular catalog-count assertion
with the real source-parity scan and reducing an AllPatrolScenarios test that
compared the function against the same constructors it calls to the ordering
and completeness signal that is actually independent.
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
Contract-Neutral: test-only branch coverage, no source or contract change
Contract-Neutral: RC qualification fixes preserve existing public API, tenant, monitoring, and organization contracts while correcting canonical runtime ownership and test fixtures.