Commit Graph

344 Commits

Author SHA1 Message Date
courtmanr@gmail.com 0c5944b8eb Preserve full poll budget for large PVE clusters
Do not let a short per-request connection timeout shrink the whole inventory cycle below its 90-second default. The seven-node support bundle for #1437 showed hundreds of successful guest reads followed by exact 30-second cancellation and dead-lettering, preventing a coherent generation from publishing. Larger request timeouts can still expand the cycle up to MAX_POLL_TIMEOUT.\n\nFixes #1437
2026-07-29 23:11:45 +01:00
courtmanr@gmail.com a53d45e2d3 Harden external probe outage alerting 2026-07-29 20:09:23 +01:00
courtmanr@gmail.com 1dc19bfec0 Remove dead cache-aware RRD fields from the guest RRD path
Recorded PVE 8 and PVE 9 guest rrddata responses (fixtures under
pkg/proxmox/testdata/rrd/) prove guest RRD never carries the cache-aware
memused/memavailable columns — they exist only in node RRD — so every
consumer branch reading them was dead code that #1634's listing fallback
(7d7d2b6a3) had already routed around.

Drop the two fields from GuestRRDPoint (now time/maxmem only, matching
the recordings), delete the dead VM RRD memory fallback and its
getVMRRDMetrics/getVMRRDMemory helpers plus the vmRRDMemCache they fed,
remove the pointless per-poll guest RRD fetch from the LXC memory path,
and retire the guest RRD lookups from PVEClientInterface. VMMemoryRaw
loses its never-populated RRD diagnostic fields, and guest reliability
scoring no longer treats the node-only rrd-* sources as trusted guest
evidence. The knownDeadGuestRRDFields allowlist in the fixture
alignment test is gone; a new reflection guard in
code_standards_test.go keeps GuestRRDPoint pinned to recorded columns,
and cleanupRRDCache pruning of the guest-agent meminfo cache gains
direct coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:36:50 +01:00
courtmanr@gmail.com 13371a6b6c Surface host-agent identity collapse from cloned machine-ids
Host agents key their identity on the machine-derived agent ID, so MSP
template deployments that clone /etc/machine-id fold two physical
machines at different sites into one host row whose reports overwrite
each other (hostname, report IP, and interfaces flapping between
sites), silently poisoning node-agent linking.

Mirror the Docker host identity-collapse doctrine (#1584) for host
report ingest: track hostname and report-IP revisits per resolved
agent identity inside the monitoring-owned flap window, publish an
active conflict as models.Host.IdentityConflict through unified
resources, and warn on the Machines page. The report IP is tracked
alongside the hostname because template fleets often reuse hostnames
across sites (pve01 at two customers), leaving the address as the only
field that betrays the clone. A one-time hostname rename never
revisits and is not flagged; the conflict clears on its own once only
one machine keeps reporting for the window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:26:18 +01:00
courtmanr@gmail.com a5b3816fd3 Add Pro-gated probe assignment to availability targets
Availability targets gain an optional probe agent assignment. Setting
it requires the external_probe entitlement, enforced only at the
moment of assignment - local targets never consult the license path.
Assigned targets are delivered to their agent through the signed
agent-config channel, skipped by the local poller, and resume local
execution automatically if the entitlement lapses. Probe-reported
results are accepted only from the currently assigned agent, share
the local failure-threshold accounting, carry source attribution,
and derive to indeterminate at read time when reports go stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com a402d23503 fix(backups): synthesize per-guest task status from vzdump job logs
Scheduled multi-guest vzdump jobs run under a single UPID whose VMID slot
is empty, so pollBackupTasks stored them with VMID 0 and the guest-centric
backups coverage view dropped them entirely: only individually backed-up
guests ever showed task status. (Regressed with the v6.0.0 guest-centric
redesign, which removed the flat task table that used to render job runs.)

pollBackupTasks now fetches the job task's log and parses the per-guest
markers ("Starting Backup of VM", "Finished Backup of VM (duration)",
"Backup of VM failed - reason") into synthetic per-guest BackupTask
entries. Their IDs embed the parent UPID, keeping them stable across polls
and distinct from individually-run backups; per-guest times are
reconstructed from the job start plus the printed durations. Finished
jobs' logs are immutable, so results are cached per instance|UPID and each
finished run is fetched at most once, with a per-cycle fetch cap so a
historical backlog trickles in without stalling the backup poll budget.

The task listing now uses source=all + typefilter=vzdump, so running jobs
are visible too: guests covered by an in-progress job get a "running"
synthetic task, which also feeds resolveBackupIntentContext and
suppresses offline/backup alerts for guests the job is actively backing
up. The frontend needs no changes - synthetic tasks carry real VMIDs and
flow through the existing coverage model, recovery mapper, and alert
intent paths.

Contract: monitoring.md completion obligation 13 records the per-guest
synthesis boundary; proofs land in monitor_backup_job_tasks_test.go,
monitor_alert_intent_test.go, and cluster_client_api_test.go.

Reported by Johannes Strasser (support thread "PBS Bug").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:03:46 +01:00
courtmanr@gmail.com 83581a4cfb Restore Monitor struct alignment broken by the cadence overrides
Adding the long-named override fields to the Monitor struct widened
gofmt's alignment for the whole field block, which rewrote ~60
unrelated lines and broke four canonical-guardrail tests that pin
struct fields verbatim (TestAvailabilityProviderStaysOnCanonical
MonitoringPath and friends). Fold the overrides into a nested
runtimePollingOverrides struct with short field names so the block's
alignment — set by guestMetadataRefreshJitter — is untouched, leaving
monitor.go a six-line functional diff against main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:23 +01:00
courtmanr@gmail.com df72955144 Cover pbsPollingInterval in the live polling-cadence setters
The live-setter fix for #1619 pushed backup and PMG cadences into
running monitors but left pbsPollingInterval out entirely: it was
neither applied to the handler's base config nor pushed to live
monitors, so a saved PBS interval still waited for an unrelated
restart. Add the matching override, setter and scheduler wiring, apply
the value to the base config, and fan it out from the settings handler
like the other cadence settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:22 +01:00
courtmanr@gmail.com 61df0814ea Push polling cadence settings into live monitors on save
Saving backupPollingInterval, pmgPollingInterval or backupPollingEnabled
only mutated the API handler's base config. Monitors poll against a
detached DeepCopy of that config, and only pvePollingInterval set the
reload flag, so the saved values never reached shouldRunBackupPoll or
the PMG scheduler until an unrelated monitor reload happened.

Add mutex-guarded runtime overrides on Monitor with live setters, fan
them out to every tenant monitor from the settings handler (mirroring
forEachNotificationManager), and clear the per-instance last-poll
timestamps when lowering the backup interval or re-enabling backup
polling so the next cycle runs an immediate catch-up poll.

Fixes #1619

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:22 +01:00
rcourtman b254d0f19d Restore physical disk node matching and unify the wearout sentinel
Physical disks reported by a host agent carry no Proxmox scope, so tightening
matchesPhysicalDiskNode to require an instance on both sides dropped every
agent-reported disk off the Proxmox node it lives on. The Physical Disks node
filter rendered no options at all, grouping lost the node, and metric target
resolution could not find it. Match on node name when the disk side has no
instance evidence, and keep rejecting a Proxmox-scoped disk against a node
outside that instance, so cluster isolation still holds in the direction that
motivated the change.

Wearout had two competing readings of 0. The presentation layer treated it as
no endurance remaining while the alert path treated it as absent and stayed
silent, so a disk could read critical in the UI and never alert. Two sources
also published 0 for a disk that reported nothing: PhysicalDiskView.Wearout
returned the struct zero value on a nil view, as did the nil-view branch of
physicalDiskFromReadStateView. Both now return the documented -1 sentinel.

storagehealth.WearoutReported is the single authority for whether a reading is
evidence at all: -1 is absent, 0 is real only from a device that reports
endurance, and rotational disks never do. Risk assessment, alerting, and the
frontend all gate on it rather than each reinventing the boundary.

Verified: full frontend suite 1061 files / 19402 tests, eslint, theme and
canonical-platform audits, tsc, go build ./..., and internal/alerts,
internal/monitoring, internal/storagehealth, internal/unifiedresources.
2026-07-24 20:58:09 +01:00
rcourtman afc419ddab Add durable Proxmox node display names 2026-07-24 15:46:56 +01:00
rcourtman 478a9e933d Complete canonical web interface links
Preserve tenant-scoped metadata through partial URL updates and project stable URLs across runtime identities. Use a safe adjacent launch control across overview tables with desktop and mobile regression coverage.
2026-07-24 12:42:16 +01:00
rcourtman 3a913752de Fix Proxmox cluster membership reconciliation 2026-07-24 11:21:27 +01:00
rcourtman bf67ba9201 Fix cache-aware Linux memory fallbacks 2026-07-24 00:19:40 +01:00
rcourtman 8d23529c02 Fix availability check identity projection 2026-07-23 23:46:34 +01:00
rcourtman 15dbabfe9a Preserve resource URLs across runtime identity changes
Contract-Neutral: Docker URL migration touches shared monitor ingest files without changing the host deletion or re-enrollment contract.
2026-07-23 22:33:07 +01:00
rcourtman 0a95b25f41 Harden host agent re-enrollment lifecycle 2026-07-23 22:29:39 +01:00
rcourtman b8ea840f11 fix(monitoring): make Unraid task state freshness-safe 2026-07-23 22:17:25 +01:00
rcourtman 8c341753ae Normalize physical disk endurance evidence 2026-07-23 22:11:22 +01:00
rcourtman 6a527ba9f2 Fix Proxmox physical disk inventory continuity 2026-07-23 22:05:09 +01:00
rcourtman 7354d8d19f Keep integration-monitored machines out of Agent Doctor and surface workload-only agents
The connections ledger derives agent rows from the unified fabric, which
includes machines whose telemetry comes from platform integrations (vSphere
ESXi hosts, TrueNAS). Agent Doctor rendered every one as a permanent
'Unknown / no structured reason' row, while agents the ledger does not carry
(Docker-only, Kubernetes-only) were silently dropped from the fleet view.

- Expose HostView.IntegrationSource() (source-set based: only SourceAgent
  ingest counts, since integration providers fabricate an Agent payload) and
  plumb it through models.Host to the connections ledger as the optional
  integrationSource field.
- Agent Doctor skips integration-backed connections and appends
  diagnostics-only agents, honoring scope, so the doctor covers exactly the
  real Pulse Agent fleet.
- Update readiness agent checks no longer count integration-backed machines
  as registered agents.
- Humanize doctor copy: plain-language stale message with '10m 2s'-style
  durations, offline wording without enum leakage, no 'Supported target:
  Unknown' cell when no target is published, host-local command banner only
  when a command is actually offered, and a compact non-zero summary strip.

Contracts updated for unified-resources, monitoring, api-contracts,
agent-lifecycle, and dependent storage-recovery; verification via
views_test.go, monitor_host_agents_test.go, state_host_test.go,
contract_test.go, and the frontend connections API test.
2026-07-21 21:24:15 +01:00
rcourtman 59e6f25a65 Detect SAS transport and parse SCSI attributes in SMART collection
smartctl reports SAS drives with device protocol SCSI, so detectDiskType
fell through to its blanket sata default, and that non-empty type also
masked the text-output transport evidence the fallback parser had
already extracted. The wrong sata label then blocked the merge layer
from promoting the smartctl serial over the SAS transport address
Proxmox reports.

Classify SCSI-protocol devices via the scsi_transport_protocol
descriptor, let the text and sysfs refinements upgrade a generic scsi
label, and apply the legacy sata default only after all evidence is
exhausted. Parse the SCSI log-page fields (power-on hours, grown defect
count, endurance used) that SCSI drives report instead of an ATA
attribute table, and let agent-reported sas replace coarse hdd/ssd/sata
types during the disk merge.

Refs #1595

Contract-Neutral: behavioral fix: SAS transport detection and SCSI attribute parsing in host agent SMART collection; no public contract delta (#1595)
2026-07-21 17:36:14 +01:00
rcourtman 057cf74629 Add alert intent policies and delivery receipts 2026-07-20 20:27:39 +01:00
rcourtman b6a74576bc Integrate trust-gate reliability fixes 2026-07-20 16:03:29 +01:00
rcourtman 72c409743d Warn when two machines share one Docker agent identity
Cloned VMs that keep the same /etc/machine-id collapse into a single
Docker host in Pulse, with each clone's report silently overwriting the
other (#1584). The server now tracks per-identity hostname and machine
ID observations and flags a conflict when a value flaps back to one
already seen inside a 15-minute window, a signature a one-time rename
never produces. The conflict rides the DockerHost model through unified
resources, and the Docker page shows a warning naming the flapping
hostnames with the machine-id remedy. The warning self-clears once one
clone stops reporting for the window.
2026-07-16 20:55:09 +01:00
rcourtman 5867d439a3 Reconcile recovery points against source enumerations
The recovery store was upsert-only: backups and snapshots deleted at
the source lingered as recovery_points rows until the 90-day retention
prune. ListRollups kept returning a rollup with a frozen LastSuccessAt,
so the backup-age alert for a deleted guest re-raised every poll cycle
and acknowledging or clearing it could never stick (#1580).

Each backup poll already publishes a complete per-instance enumeration
(partial failures early-return or carry previous entries forward), so
attach a reconcile scope to that ingest batch. After the upsert, points
in the scope (provider + id class + instance) that were not part of the
enumeration are deleted, which lets the existing per-cycle alert sweep
resolve the alert. An empty enumeration is meaningful and clears the
scope, covering the delete-all-backups case from the report.

Also make the async ingest queue batches instead of overwriting the
single pending slot, which silently dropped a full poll cycle whenever
two sources coalesced behind an active batch.
2026-07-16 09:38:53 +01:00
rcourtman c70431caaf Honor configured availability poll interval in the scheduler
An availability target's configured poll interval only seeded the
adaptive scheduler: BuildPlan derived every instance's cadence from the
global adaptive bounds, and a failing probe raised the staleness score
and error penalty, collapsing the probe interval toward the global
5-second minimum. With interval 120s and failure threshold 4 the alert
was promised after ~8 minutes of downtime but fired within the first
minute because the four consecutive failures accumulated at the
collapsed cadence (#1582).

Availability checks promise pollInterval x failureThreshold as the
detection window, so the cadence is a user contract, not a scheduling
hint. Add a FixedIntervalPollProvider extension that pins an instance
to its configured interval, implement it for availability targets, and
bypass adaptive selection wherever the next run is computed (plan
building, rescheduling, and the non-adaptive fallback paths).
2026-07-16 09:26:53 +01:00
rcourtman e56561b76a Refresh canonical resources after headless agent reports 2026-07-14 17:03:14 +01:00
rcourtman 8f475cbf58 Fold runtime-key Docker URL metadata into the stable guest key
URLs saved through the resource drawer historically landed in the docker
store under the runtime container key, which any stable record (including
an intentionally empty cleared one) outranks in the unified customUrl
projection, and which orphans on container recreation. On report ingest,
copy those records into the stable app-container guest key when it is
missing (cleared links stay cleared), healing saves stranded before the
drawer moved to the stable identity. Also read the runtime key before its
copy-if-missing container-name snapshot so the freshest write wins among
the docker-store fallbacks.

Refs #1556
2026-07-14 12:00:57 +01:00
rcourtman cb310d9932 Hydrate container customUrl in REST resource snapshot
The websocket broadcast path applies docker metadata (container
customUrl) via applyDockerMetadataToUnifiedResources, but the REST
/api/resources registry seed read the raw unified state view, so the
two payloads drifted: a container web-interface URL saved in the
drawer never appeared in REST-hydrated tables. Apply the same
hydration at the UnifiedResourceSnapshot provider boundary.
2026-07-10 22:48:49 +01:00
rcourtman 58ece3c1b8 Fix physical disk SMART/Proxmox merge identity
Refs #1516

Refs #1483

Refs #1471
2026-07-07 09:46:51 +01:00
rcourtman 7edfa1bb86 Fix Docker container URL metadata identity
Refs #1490
2026-07-06 15:48:26 +01:00
rcourtman 21b15e6755 Fix Proxmox read-state CPU normalization
Refs #1525
2026-07-06 14:33:41 +01:00
rcourtman fce4317176 Fix PBS backup memory retention
Refs #1524
2026-07-06 10:32:38 +01:00
rcourtman 806fd7409c Show cluster labels in Proxmox node names
Refs #1475
2026-07-03 10:52:18 +01:00
rcourtman 875e414b4b Align resource staleness with poll cadence
Refs #1468
2026-07-03 09:05:12 +01:00
rcourtman dcf541784d Bound server state broadcast memory
Refs #1442
2026-07-02 23:27:49 +01:00
rcourtman 97b9369333 Guard infrastructure import plan approvals
- add candidate import approval and preview gate for node onboarding
- document metrics, availability, and proxy role behavior
- lock config auth reads and preserve storage metrics fallback
2026-07-02 21:59:26 +01:00
rcourtman 7c75b13d2c Carry host power sensor readings 2026-06-30 10:02:49 +01:00
rcourtman d393ccf310 Add typed NVIDIA GPU stats 2026-06-30 09:43:33 +01:00
rcourtman 83e8f53ac5 Recheck guest Docker inventory after checker setup
Force stale negative LXC Docker detections to re-run after the Docker checker is configured so explicit Proxmox guest Docker inventory repopulates after backend restarts.
2026-06-25 20:30:38 +01:00
rcourtman c47560ad69 Fix websocket state broadcast retention
Refs #1442
2026-06-24 11:16:27 +01:00
rcourtman da0693b3b1 monitoring: resolve configured-host IPs off the monitor lock
getConfiguredHostIPs() and the new discoveryConfigSnapshot() now deep-copy config under a brief RLock and release it before resolving Proxmox/PBS/PMG hostnames via the injectable lookupConfiguredHostIP seam, so slow or blocked DNS can no longer stall monitor writers. The discovery IPBlocklist is the deduplicated merge of the operator-configured blocklist and the resolved configured-host IPs. Adds lock-freedom and merge coverage tests; updates the monitoring contract obligation.
2026-06-23 17:31:37 +01:00
rcourtman 6a162a1736 Add macOS thermal state reporting
Report Darwin pmset pressure separately from Celsius readings and carry it through host sensor state and resource details.
2026-06-14 11:07:47 +01:00
rcourtman c9ee0b3392 Fix alert threshold and disk temperature regressions 2026-06-12 19:19:16 +01:00
rcourtman b53e1a3e0e storage(pools): carry the full ZFS pool payload to the storage surface
The unified-resource adapter flattened models.ZFSPool to four scalars
(state + R/W/C totals), so scan status and the per-device report never
reached the frontend. The storage detail's ZFS card, the usage bar's
scrub/resilver overlay and ZFS tooltip, and ZFS-device-to-disk matching
were all dead code on the unified path: a degraded pool rendered as a
one-word State cell with no way to see which device failed or that a
resilver was running. Found by the v5-parity audit of the pools table
(same pipeline disease as #1471 on the disks table).

- Add StorageMeta.ZFSPool and populate it in resourceFromStorage
  (normalized copy, not aliased).
- Serialize it in monitorStoragePlatformData so the /api/state path
  carries it alongside the existing flat scalars.
- Map it through normalizeStorageMeta and the storage record adapter
  (meta first, flat platformData fallback).
- Render the per-device report (name, vdev type, state, R/W/C errors,
  message) in the detail's ZFS Pool card - the v6 home for what v5
  showed via the health-map dots and the degraded-pool warning row.
- Pin the end-to-end path in code_standards_test and update the
  unified-resources, monitoring, storage-recovery, and
  frontend-primitives contracts.

Verified live in mock mode: degraded pool now shows the resilver scan,
pool error totals, per-device states, and the bar pulse overlay.
2026-06-11 16:48:57 +01:00
rcourtman b1114f4986 Keep /api/health off the monitor write lock; tolerate load stalls in the dev watchdog
The liveness handler called SchedulerHealth(), which refreshes provider
instance caches under the monitor's write lock and snapshots queues and
breakers on every probe — it only needed the dead-letter count. Add
Monitor.DeadLetterCount() (queue-local mutex only) and use it.

The hot-dev watchdog killed the backend after 2 missed 3s probes; on a
dev box running race-detector test suites and builds that is routine
CPU starvation, and each kill destroyed any in-flight Assistant chat
stream (observed repeatedly during the chat-feel audit). Default the
miss threshold to 4 (~20s) — genuine wedges still die quickly.
2026-06-11 15:24:00 +01:00
rcourtman e2a036ce2e fix(unifiedresources): carry real poll timestamps for storage and docker container sightings
resourceFromStorage and resourceFromDockerContainer stamped LastSeen with
time.Now() at conversion because their source models carried no poll
timestamp. The registry rebuilds from the retained state snapshot every
cycle, so those resources re-reported a fresh sighting each rebuild even
after their upstream source (PVE instance, docker host agent) stopped
delivering, and their per-source SourceStatus could never go stale via
markStaleLocked. 53faa4e46 fixed this fabrication at the ingest layer but
left these two adapter-level stamps.

- models.Storage gains LastSeen (omitzero), stamped where entries are
  built: the PVE storage poll (poll start time, including synthesized
  cluster-shared entries; preserved entries for unpolled nodes keep their
  old stamp), the PBS datastore conversion (PBS instance sighting), Ceph
  pool projection (cluster LastUpdated), and the mock generator (offline
  mock nodes get a backdated stamp so the stale path renders).
- resourceFromStorage passes storage.LastSeen through; zero stays zero
  ("never seen") instead of becoming conversion time.
- resourceFromDockerContainer uses host.LastSeen: containers are delivered
  wholesale with each host report, so the host report timestamp is the
  container sighting. This matches every other docker sub-resource adapter
  (services, tasks, volumes, networks, images already use host.LastSeen).
- ingestStorage routes PBS-poller datastore entries (instance "pbs-<name>",
  type pbs) to SourcePBS, parented to the PBS instance. Keying them
  SourceProxmox would judge their freshness against the 60s Proxmox stale
  threshold while PBS polls every 60s by default, flapping healthy
  datastores stale between polls; SourcePBS carries the cadence-matched
  120s threshold. PVE-reported pbs-typed storage.cfg backends stay
  SourceProxmox. Side effect: syncUnifiedStorageMetrics no longer skips
  PBS datastore storage, so those entries gain usage history.
- storageFromReadStateView round-trips LastSeen so the legacy storage API
  reports the honest sighting; mock refresh re-stamps available storage on
  each simulated poll.

The parent host/node staleness was already honest, so platform pages
reflected outages at the parent level; this makes the per-resource source
freshness honest too.
2026-06-11 10:57:22 +01:00
rcourtman 53faa4e46a fix(unifiedresources): stop fabricating sightings and online stamps for never-seen resources
Ingest stamped every resource's SourceStatus "online" and replaced a zero
LastSeen with time.Now(), so a source that never delivered a resource
(synthesized offline PVE placeholders, never-polled instances) reported a
fresh sighting and a healthy source forever: markStaleLocked skips zero
LastSeen entries, so the stamp was permanently exempt from stale-marking.
That fabricated freshness fed monitor previous-state reads (the
resurrection loop mitigated in 8372a22c5), the frontend lastSeen display,
the monitored-systems ledger, and AI/agent context.

- ingest/mergeInto/IngestResources/presentation coalesce: stamp per-source
  delivery status from the actual sighting ("unknown" when the source has
  never delivered, "online" otherwise) and preserve zero LastSeen instead
  of substituting ingest time.
- replaceRegistry: run MarkStale over the fully assembled registry. The
  IngestSnapshot stale pass ran before record sources (TrueNAS, VMware)
  were ingested, leaving them stamped "online" at any age.
- monitorLastSeenUnix: report 0 for never-seen instead of time.Now(), so
  the frontend renders a dash rather than "just now".
- connected infrastructure: clamp zero LastSeen to 0 ms instead of the
  negative UnixMilli of the zero time.

preserveOrExpireNodes keeps requiring a real online sighting for grace;
the registry no longer manufactures the timestamps that made that guard
necessary, so the 8372a22c5 failure mode is impossible by construction.
2026-06-11 10:23:49 +01:00
rcourtman 9bcf767b1b fix(reporting): resolve report subjects through the canonical unified view and paginate the metric card grid
Two regressions surfaced by generating a real-mode agent report after a
backend restart:

- Report subject lookups used Monitor.GetUnifiedResources (the raw
  resource store), but the raw store's canonical IDs depend on per-boot
  ingest order for merged-source hosts: after a restart the same host
  resolved to a different agent-<hash> than the one the UI and
  /api/state advertise, so reports lost the resource name, availability,
  and metrics translation entirely. Subject enrichment now reads
  Monitor.UnifiedResourceSnapshot and MetricsTargetForResource resolves
  through GetUnifiedReadStateOrSnapshot first (raw store as fallback) -
  the same re-ingested registry every other read surface uses.
- The performance summary card grid positions cards absolutely and never
  paginated: an agent host reporting 8+ metric families walked off the
  page bottom, fought fpdf's auto page break, and scattered one orphan
  element per page (a 7-day delly report rendered 18 pages, ten of them
  near-blank). The grid now starts a new page before a row that will not
  fit; the same report renders 9 pages with intact cards.

Verified live: real-mode delly report shows name, 288 data points,
availability, charts, and correctly paginated cards. The underlying
canonical-ID instability (raw store vs re-ingested view, and the
resource_changes journal fragmenting across boot eras) is a separate
root issue tracked for its own fix.
2026-06-10 22:02:48 +01:00