Commit Graph

8660 Commits

Author SHA1 Message Date
courtmanr@gmail.com f45c08902e Prepare v6.2.0-rc.1 release
First candidate on the v6.2.0 line, following stable v6.1.2. Carries
the External Probes Pro feature, the three-part multi-site Proxmox
identity isolation work, cloned machine-id collapse detection,
unattended-update service recovery, the TrueNAS same-host redirect
follow, and verified telemetry outcomes. Version pins move to
6.2.0-rc.1 across the repo root, Docker bootstrap defaults, and Helm
metadata per the deployment-installability contract; stable install
pointers remain on v6.1.2 until governed promotion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v6.2.0-rc.1
2026-07-27 14:08:29 +01:00
courtmanr@gmail.com c654eb702d Raise AvailabilitySettingsPanel bundle baseline for the probe-attribution UI
c2c51019d grew the chunk to 7.08 kB gzip, past the 15% headroom over
the 5.94 kB baseline, which has failed the bundle budget on every main
push since. The growth is the intentional probe assignment and source
attribution feature, so re-baseline at the measured size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:17:06 +01:00
courtmanr@gmail.com b200955ab8 Backfill monitoring contract for the TrueNAS handshake-redirect upgrade
bf24a9a9c changed truenas runtime transport behavior without the
canonical paperwork, so its governance run failed the completion guard.
Document the redirect rule in the monitoring subsystem contract — a
plaintext handshake redirect never wakes the REST bridge; a same-host
https redirect upgrades once to wss and the upgrade is scheme-only and
monotonic — and move the Issue1631 regression tests into
transport_test.go, which is a registered verification artifact for the
truenas runtime path policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:16:52 +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 17117c32ef Close the unclassified-node window in cross-instance node aggregation
Support evidence from the MSP case showed the clusters mix without ever
sharing a corosync name and without the config-layer consolidation
firing, which rules out the same-name path and points one layer down.
Two mechanisms combine there. A newly added connection whose add-time
cluster detection failed (#437) commits its first polls' nodes with an
empty cluster name, because pollPVEInstance ran membership detection
after the node-state commit - and every aggregation guard deliberately
lets empty cluster names merge freely. And host agents key their
identity on /etc/machine-id, which cloned template deployments reuse
across sites, so two different pve01 machines collapse into one agent
row whose shared LinkedAgentID then folds the unclassified node into
the established cluster's slot, overwriting it - the reported "enacon
appeared renamed to rewo" data loss.

Weak-evidence folds across connection instances - a bare-hostname
endpoint alias or a shared linked-agent identity - now require positive
same-machine proof (matching non-empty cluster identity or matching TLS
fingerprints) whenever cluster identity is in play on either side. Two
views that are both unclassified still dedup freely, and address-based
endpoint aliases keep folding on the contradiction checks alone, so the
designed standalone-into-cluster folds survive. PVE polling now also
runs cluster membership detection before the cycle's node-state commit
and re-reads the refreshed instance config, so nodes carry their
cluster identity from the first state write whenever detection
succeeds instead of transiting aggregation unclassified.

Reported via support by an MSP running clusters enacon and rewo that
reuse pve01/pve02 node names across sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:07:44 +01:00
courtmanr@gmail.com 7f3106b6bd Format generated self-hosted feature catalog with prettier in the generator
The generator emitted JSON-marshaled TypeScript (quoted keys, no trailing
commas) while the committed file is prettier-formatted, so every run dirtied
frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts with a
324-line formatting-only diff. The generator now runs the frontend's own
prettier on the file after writing it, so runs at a clean checkout leave the
tree clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:58:05 +01:00
courtmanr@gmail.com c2c51019d7 Add probe assignment and source attribution to the availability UI
The availability check editor gains a "Run from" control listing
connected agent hosts, locked behind the external_probe entitlement
with the standard feature-gate affordance - visible for
discoverability, disabled without Pro, and never in the way of
saving local checks. Statuses show a "via agent" chip when the
latest observation came from a probe, stale probe reports render as
indeterminate, and clearing an assignment serializes an explicit
empty probeAgentId so the backend merge-decode clears it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com 3efca89525 Document external probes and count adoption in telemetry
Add availability_probe_targets and availability_probe_agents to the
telemetry ping - counts only, no agent names or addresses - with the
disclosure table updated in both privacy doc copies. Document the
feature in the availability-checks configuration guide and the
unified agent guide, including the ICMP capability caveat for
containerized probes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com a9dad6a29c Run assigned availability checks from the host agent
The unified agent gains an availability module: probe assignments
arrive through the signed remote-config channel (missing key clears
the schedule), each enabled target runs on its own clamped interval
through the shared probe core, and results queue in a bounded
drop-oldest buffer. A result is offered to the primary server until
one delivery succeeds and never again after - buffered offline
reports are stripped of availability results so the disk buffer
cannot replay observations the queue still holds. ApplyHostReport
feeds accepted reports into the probe ingestion path, where the
ownership check and failure accounting live, and the probe agent id
is projected onto unified availability resources for source
attribution in the UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +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 fc5aaef391 Register the external_probe Pro entitlement
Add external_probe to the licensing feature enumeration, Pro tier
membership, self-hosted feature catalog, feature map, upgrade matrix
and pricing handoff, with the entitlement contract goldens updated.
The capability is served by the community binary - the entitlement
alone gates it, like relay. No existing free functionality changes:
local availability checks stay ungated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com ca3e4b5709 Extract availability probe execution into a shared package
Move the ICMP, TCP, HTTP and UDP probe core from the monitoring
availability poller into internal/availabilityprobe so the host agent
can execute the same checks without importing the monitoring package.
Monitoring keeps type aliases and thin wrappers, so callers and the
canonical guardrail contracts are unchanged. Pure refactor for the
upcoming external probe feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com 0966ae9594 Measure verified telemetry outcomes 2026-07-27 10:15:48 +01:00
courtmanr@gmail.com 07d2f98455 Gate same-name cross-instance node aggregation on TLS identity
The config layer already refuses to consolidate two same-named clusters
whose TOFU-captured TLS fingerprints contradict, and node aggregation
keeps any two same-named clusters from different connection instances
apart unconditionally. Two gaps remained one layer down. First, the
endpoint-IP agent match bypassed the contradiction guard entirely, so
two sites reusing RFC1918 addressing (the MSP support case: pve01 on
192.168.1.11 at both sites) still bound the second site's node to the
first site's host agent, attaching the wrong machine's telemetry.
Second, the unconditional split had no way to recognize the legitimate
duplicate - the same cluster added twice through different member
addresses with no config-level endpoint overlap could never fold back
into one node slot.

The aggregation layer now receives the config layer's identity
evidence: each PVE node carries the TLS certificate fingerprint of its
own named endpoint record (standalone nodes carry the instance
fingerprint; a cluster member never inherits the instance-level
fingerprint, which pins whichever member the connection URL reaches).
Same-named clusters from different instances merge only when both views
carry the same fingerprint; contradicting or unknown evidence keeps the
fail-safe split. Agent binding applies the identical doctrine: hostname
and address matches are rejected when the candidate agent's linked
nodes live in a different named cluster or carry a different
fingerprint, closing the previously unguarded endpoint-IP path.

Reported via support by an MSP whose sites reuse cluster names, node
names, and RFC1918 ranges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 10:00:33 +01:00
courtmanr@gmail.com 802986add4 Bump postcss to 8.5.23 in both frontend lockfiles
Resolves the two open Dependabot alerts for GHSA-r28c-9q8g-f849
(source-map auto-loading path traversal, patched in 8.5.18). postcss
is a build-time dependency, so no shipped runtime code was affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:46:28 +01:00
courtmanr@gmail.com f21d05e3cb Veto PVE cluster consolidation on contradicting TLS fingerprints
Two clusters at different sites that reuse the same corosync cluster
name and the same RFC1918 addressing presented colliding member IPs,
which ConsolidatePVEInstances took as strong endpoint overlap and folded
the second site's connection into the first. The standalone-into-cluster
merge had the same hole for a standalone whose address collides with a
cluster endpoint at another site. The v6.1.1 node-aggregation guard
never fires in this case because it keys on cluster-name conflict, and
same-name clusters do not conflict.

Address coincidence is weak evidence across sites, but the TOFU-captured
TLS certificate fingerprints already stored on instances and cluster
endpoints are strong evidence: contradicting fingerprints for the same
authority, node name, or endpoint address mean different machines.
Consolidation now refuses to merge in that case. The fail-safe direction
is deliberate - a certificate rotation may leave a genuinely duplicated
cluster as two views, but two distinct clusters are never silently
folded into one.

Reported via support by an MSP whose two customer clusters kept merging
after the v6.1.1 aggregation fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:43:43 +01:00
courtmanr@gmail.com 806cbe83dc Fix auto-update leaving service down after installer failure
Two bugs from #1630 that combined to take installs down silently:

1. perform_update()'s install-failed rollback branch restored the backup
   but never restarted pulse.service. Since the generated
   pulse-update.service gates on ExecCondition=systemctl is-active,
   every later timer run was then skipped and the install stayed down
   until manual intervention. Restart is now guaranteed by a
   service_was_active-guarded restart in that branch plus an
   ensure_service_restarted RETURN trap so no exit path can miss it
   (re-fix of #1323, originally c0b3a0e66, lost in 778a2577b and only
   partially restored in 672e81985).

2. install.sh aborted under errexit when writing the /bin/update helper
   on a read-only filesystem - after the new binary was installed and
   the service stopped, landing in bug 1's no-restart branch. The stock
   pulse-update.service uses ProtectSystem=strict, so /bin and
   /usr/local/bin are read-only on stock unattended updates; transient
   read-only remounts hit the same path. The helper write, PATH
   appends, and the /usr/local/bin/pulse symlink are now idempotent and
   non-fatal with a warning (install_binary_symlink).

Contract: deployment-installability now pins fail-closed service
availability for unattended updates and non-fatal writes outside the
hardened unit's writable set, with proofs in pulse_auto_update_test.go
and root_install_sh_test.go plus shell regression coverage in
scripts/tests/test-pulse-auto-update.sh (installer-exits-nonzero path)
and scripts/tests/test-install-update-resilience.sh (read-only helper
and symlink paths, verified under set -e, root-safe via ENOTDIR).

Fixes #1630

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:38:37 +01:00
courtmanr@gmail.com bf24a9a9cf Follow same-host https redirects when dialing the TrueNAS websocket
The v6.1.2 JSON-RPC migration (b81ba7dd0) broke http-configured
appliances sitting behind TrueNAS's HTTP -> HTTPS redirect: the REST
transport followed the redirect transparently, but a websocket
handshake cannot, so every poll failed with status=302 bad handshake.

When the plaintext handshake answers with a 3xx whose Location is an
https URL on the same host, retry once over TLS (honouring the
configured skip-verify/fingerprint settings) and keep the upgraded
wss endpoint for the client's lifetime. Cross-host and downgrade
redirects are refused with an actionable error naming the target, as
is a redirect whose TLS retry fails.

Fixes #1631

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 09:07:08 +01:00
courtmanr@gmail.com fe33d23210 Document websocket lifecycle synchronization v6.1.2 2026-07-26 22:02:22 +01:00
courtmanr@gmail.com 27ba37982e Synchronize websocket client lifecycle signal 2026-07-26 21:56:52 +01:00
courtmanr@gmail.com f7ff8d7452 Fix Docker agent test timer race 2026-07-26 21:32:35 +01:00
courtmanr@gmail.com 81974c7f62 Authorize v6.1.2 unsigned Windows release exception 2026-07-26 21:07:47 +01:00
courtmanr@gmail.com a9d215401e Reset ephemeral Windows state before native lifecycle proof 2026-07-26 20:51:21 +01:00
courtmanr@gmail.com 3a6af11e86 Stabilize Patrol shell checks across desktop and mobile 2026-07-26 20:49:22 +01:00
courtmanr@gmail.com 9bb5a8bee1 Isolate Windows native lifecycle self-test state 2026-07-26 20:45:20 +01:00
courtmanr@gmail.com e405270e89 Prepare v6.1.2 stable patch release 2026-07-26 20:38:42 +01:00
courtmanr@gmail.com 7f4db466be Fix operator-state null capabilityNames crash and stopped-container gating
Issue #1621: saving only "intentionally offline" left the SQLite
auto_remediation_policy_json column NULL, and the read path only
normalized the policy inside the non-NULL branch, so the API served
"capabilityNames": null. The section's dirty-state memo spread that
value during render and crashed the expanded row into the route error
boundary. Normalize the policy unconditionally after the scan so the
wire always carries [], type capabilityNames as nullable in the TS API
surface, and guard the spread in the component.

Issue #1622: the inline table-row presentation gated the entire
operator-overrides section on the resource exposing an auto-authorizable
capability, so stopped Docker containers (which only expose start,
never auto-authorized) lost intentionally-offline and
never-auto-remediate exactly when they matter most. Render the section
whenever the resource has an id; the automatic-actions block already
self-gates on eligible capabilities. Also add min-w-0 to the section's
flex text columns and overflow-x-clip to the inline row content box so
the long never-auto-remediate copy wraps instead of escaping the row
border below the lg breakpoint.

Regression coverage: issue-named Go test for the NULL-column read path
and a render test feeding the section the real pre-fix wire payload
with capabilityNames: null.

Fixes #1621
Fixes #1622

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:19:59 +01:00
courtmanr@gmail.com 0b9385f2f2 Guard agent installs against tight disks and rotate appliance agent logs
The agent installer (scripts/install.sh) had no free-space preflight, so on
RAM-rooted appliances (QNAP QTS, Unraid) it ran all the way to the download
before dying with an unhelpful ENOSPC. Lift the server installer's disk
headroom check into the agent installer: verify temp and install-dir space
(including the shared-filesystem case) before downloading and in
--preflight-only mode, with a TMPDIR hint in the failure message.

The QNAP and Unraid watchdog loops also shell-appended agent stdout to
/var/log/pulse-agent.log with no rotation, which could fill the RAM root on
its own. Pass --log-file so the agent's rotating writer engages (QNAP: data
volume state dir; Unraid: /var/log/pulse-agent with size-capped rotation),
discard the now-duplicate stdout mirror, and keep the watchdogs' own messages
in a small self-trimming log.

Document the TMPDIR override for constrained roots in docs/UNIFIED_AGENT.md.

Fixes #1617 (space half; CPU half pending reporter diagnostics)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:16:05 +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 c08da19ae7 fix(ceph): parse Quincy+/Squid status schema for MON and MGR counts
Ceph Quincy and later (including Squid on PVE 9) dropped the monmap
mons array and the mgrmap active_name/standbys arrays from ceph status
output, replacing them with monmap.num_mons and mgrmap.num_standbys,
with quorum membership reported at the top level of the payload. Both
the host agent parser and the Proxmox API path only understood the
legacy arrays, so modern clusters showed 0 monitors and undercounted
managers.

- hostagent: read num_mons/num_standbys and top-level quorum data,
  taking the largest available signal, and base the mon/mgr service
  rows on the same counts
- pkg/proxmox: decode mgrmap num_standbys and top-level
  quorum_names/quorum on CephStatus
- monitoring: fall back to the new fields when counting MON/MGR
  daemons, and log Ceph 401/403 failures at warn level with a hint to
  grant Sys.Audit on / instead of hiding them at debug
- models: prefer the larger non-zero MON/MGR counts when merging Ceph
  cluster records from multiple sources

Fixes #1626, Refs discussion #1290

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:31:15 +01:00
courtmanr@gmail.com 28e884e33c docs: mark retired Docker update endpoints as 410 in the API reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:30:45 +01:00
courtmanr@gmail.com 33b4d629f6 docs: bring Docker update section in line with the reviewed-action flow
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:26:29 +01:00
courtmanr@gmail.com bf6dfe320c docs: document container lifecycle actions and the --enable-commands requirement
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:21:09 +01:00
courtmanr@gmail.com eb665b2a0d fix(ai): make Patrol readiness probes representative of the Patrol runtime
The readiness advisor failed capable local models for adapter and probe
defects rather than model incapability (#1624 Ollama, #1614 llama.cpp):

- Send num_ctx sized to the haystack fixtures (clamped to the model's
  trained window) so Ollama no longer truncates ~25KB prompts at its
  4096-token server default; the trained-window guard alone passed while
  the runtime request was being truncated.
- Forward an explicitly pinned temperature 0 instead of dropping it to
  Ollama's 0.8 default against a nonce-exact validator (ChatRequest gains
  TemperatureSet; Ollama options temperature is now a pointer).
- Raise the probe generation cap from 256 to 2048 tokens so qwen3-style
  <think> reasoning cannot exhaust the budget before the tool call, and
  surface the provider done_reason when validation fails.
- Synthesise tool-call IDs in the OpenAI-compatible adapter (streaming
  finalizer and buffered path) when the server omits them, as llama.cpp
  commonly does, mirroring the Ollama adapter instead of failing tool
  protocol 0/3 on transport shape.
- Probe with the Patrol loop's 60s stream stall allowance instead of the
  12s chat default (chat.PatrolProviderStreamIdleTimeout is now exported).
- Stop discarding probe and validator errors: log them, carry them in a
  new PatrolModelReadinessResult.Details field surfaced through the API
  snapshot and Settings UI, and keep a transport-level probe failure's
  specific diagnosis instead of overwriting it with the generic
  capability wording.

Builds on 4a2335ce7, which already reclassifies protocol failure as
"provider connected; Patrol capability not verified".

Fixes #1624
Fixes #1614

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:49:24 +01:00
courtmanr@gmail.com acfe84d9cf Fix issue version parser dropping capitalised V prefixes
normalizeVersion matched /\bv?(\d+\.\d+\.\d+...)/ without the i flag, so a
reported version written as "V6.0.4" never parsed.

The heading regexes in extractPulseVersion already carry the i flag and did
capture "V6.0.4" correctly, but they hand the captured string to
normalizeVersion, which then returned null. That collapsed every extraction
path -- inline scan, heading match, legacy match, and title fallback -- so the
issue was labelled needs-version-info despite stating its version.

Adding the i flag to normalizeVersion fixes all four paths at once. It is a
no-op on the rest of the pattern, since [0-9A-Za-z.-] already spans both cases.

Seen on #1538, which stated "V6.0.4" under the "Pulse version" heading and was
wrongly flagged as missing version metadata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:49:24 +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
courtmanr@gmail.com 5a9e20d971 Scale connection staleness cutoff with the configured polling cadence
The connections ledger marked any PVE/PBS/PMG/VMware/TrueNAS connection
stale once its last successful poll was more than a fixed 2 minutes old,
so polling cadences above 120s read as permanently stale and latched the
connection-degraded alert with no path back to active (#1620).

Derive the active->stale cutoff as max(3 x polling interval, 2 minutes),
mirroring how availability probes already scale. PVE/PBS/PMG cadences
come from the server config; TrueNAS uses the per-instance interval and
VMware the poller summary interval. The 3x multiplier absorbs the
evaluation-order skew where connection alerts are checked concurrently
with the poll on the same scheduler tick, leaving LastSuccess roughly
one interval old even when healthy.

Fixes #1620

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:42:22 +01:00
courtmanr@gmail.com c9600eace1 fix(test): use relative timestamp in incident restart persistence test
TestUnifiedProviderIncidentRecoveryConfirmationSurvivesRestart hardcoded
observedAt as 2026-07-24, but the restore path drops persisted alerts
older than 24h, so the test began failing once the calendar moved past
the fixed date. Use a recent relative timestamp instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:04:30 +01:00
rcourtman bbd333631c Cover the alert read-model, guest identity and action-planner validators
Branch-coverage tests for twenty partially-covered pure helpers that the
existing suites only reached through higher-level paths, so their arms were
never pinned directly.

internal/alerts read model and filter evaluation: metadataBoolValue 25.0 to
100, metadataIntValue 27.8 to 100 and numericConditionValue 22.2 to 100, each
covering every accepted underlying type plus the wrong-type and missing-key
arms; the three sort-rank helpers now assert an exact rank for every input
class including the default, which is what pins the ordering.

internal/alerts guest and backup identity: snapshotAlertStillTriggered 15.4 to
100 with the threshold pinned at, below and above; parseStableGuestOverrideKey
30.0 to 100; extractGuestSnapshot 35.7 to 100 across its type switch; and
hasActiveAlertTrackingKeyNoLock 40.0 to 100 including the canonical-scan and
nil-skip arms.

internal/alerts/specs: both Validate methods 57.1 and 58.8 to 100 with every
rejection reason asserted on its specific error, and metricTriggered and
metricStillLatched 50.0 to 100 with the metric pinned at, below and above the
threshold in both latch states.

internal/actionplanner: validateParamType 29.4 to 100 over every parameter type
plus the unknown default, validateParamValue 38.9 to 100 per rejection reason,
and both normalize helpers 38.5 and 40.0 to 100 over nil, empty, duplicate and
well-formed input. enumString also moved 50.0 to 100 as a consequence.

Package statement coverage moves to 87.1 percent for internal/alerts, 81.8 for
internal/alerts/specs and 88.5 for internal/actionplanner. Tests only, no
source change.
2026-07-25 06:34:48 +01:00
rcourtman ac5b595e97 Cover the alert-spec matchers and the audit, email and update guards
A third pass on partially covered functions, led by the alert evaluation
predicates where a wrong arm means a missed or spurious alert.

- internal/alerts/specs: matches 47.9 to 100, and all six matches helpers
  (severity threshold, change threshold, baseline anomaly, health assessment,
  posture threshold, and the severity latch) from 50 to 75 percent up to 100.
  Each threshold is pinned at, just below and just above, and the latch arm is
  exercised both latched and unlatched with concrete verdicts.
- pkg/audit: exportCSV 76 to 88 with commas, quotes and newlines in the detail
  field asserted through a parsed round-trip; NewSigner error arms, both
  IsPersistent predicates and VerifySignature against a tampered payload and a
  wrong key.
- internal/agentupdate: retryBackoffDelay, sleepWithContext, Snapshot and
  writeSelfTestTokenFile to 100, the token file exercised under t.TempDir
  including the unwritable-directory arm.
- internal/notifications: writeMultipartBodyPart and alertNodeDisplay to 100,
  attachment handling to 69, all asserted on the produced MIME text. No test
  opens a network or SMTP connection.
- internal/unifiedresources: the three pure action-dispatch helpers to 100.
- internal/alerts/config: CanonicalResourceTypeKeys 34.3 to 78.4.

Five targets deliberately did not move and are recorded rather than faked:
the error arms of writeEmailThreadingHeaders, buildMultipartEmailMessage and
copyWebhookConfig are unreachable because those functions write only into a
local bytes.Buffer, which never errors; exportJSON's only gap is a
json.MarshalIndent failure that its event struct cannot produce; and
verifyBinaryMagic's remaining gap is a deferred close-error handler.

No source file is modified. Adversarial review returned no rejects and flagged
seven near-duplicate subtests; all were removed and every target function
re-measured at an identical percentage.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 02:17:36 +01:00
rcourtman ae1f9c2ad3 Cover eight frontend modules no prior coverage pass had reached
These are pure modules that already had a spec but had never been through a
branch-coverage pass, so their cold arms had never been measured. Counts are
uncovered branches, module scoped, existing spec versus existing plus new.

- resourceTypeCompat 12 -> 0, the legacy type-alias paths: unrecognised type
  strings, casing variants and the fallback identity mapping.
- summaryCardInteraction 8 -> 0 and diskDetailPresentation 6 -> 0.
- commercialBillingModel: asUnlimitedLimit, buildHostedCommercialPlanModel and
  buildHostedCommercialUsageModel had never been called by any test. All three
  are now covered across unlimited and numeric limits and usage at, below and
  above the limit.
- alertDetailPresentation 4 -> 0, resourceTypePresentation 2 -> 0,
  unifiedResourceTableStateModel 4 -> 0, assistantSlashCommands 4 -> 1.

No source file is modified. Adversarial review returned no rejects but found
the heaviest padding of this run, around twenty-five near-duplicate cases plus
one assertion that mirrored the source's own date formatting. Every one was
removed or replaced with a concrete literal, and each module's coverage was
re-measured as identical afterwards, proving the removed cases carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 01:35:31 +01:00
rcourtman c035c5b514 Cover the missing arms of the SSRF, clone and classifier guards
A second pass targeting PARTIALLY covered functions rather than untouched
ones, so every case here is an arm the existing suites never reached.
Percentages are per-function coverage, measured before and after.

- pkg/securityutil: the SSRF guards, which is where the uncovered arms
  actually matter. isCarrierGradeNATIPv4 and isLocalNetworkIP are pinned at
  the first and last address of 100.64.0.0/10 and just outside both ends,
  across loopback, link-local, every RFC1918 range and IPv6 unique-local.
  joinURLPath, IsLocalNetworkHost, resolveOutboundIPAddrs and
  cloneRestrictedTransport to 100 percent, resolvePermittedOutboundIPs to
  96.8, with the transport clone asserted independent of its source.
- internal/models: eleven deep-copy helpers from as low as 25 percent to 100.
  Every one asserts real independence, mutating each nested slice, map and
  pointer field of the clone and checking the original is untouched, which is
  the failure mode a deep-copy helper actually has.
- internal/alerts: metricClearThreshold 28.6 to 100, resourceTypeLabel and
  alertspecsMetricTriggered 50 to 100, the four canonical spec-id and
  tracking-key builders 66.7 to 100, inferCanonicalKindFromLegacyAlert to 100,
  and the backup-snapshot and ack-identity predicates.
- internal/servicediscovery: the four fingerprint generators to 100, each
  asserted for both stability and sensitivity; the three command builders and
  ValidateResourceID on their exact output and each rejection reason.
- internal/storagehealth: zfsScanActive and firstNonEmpty.

cephClusterSourceRank is deliberately left at 75 percent: its default arm is
unreachable because normalizeCephClusterSource can only return the two cases
above it. That is recorded rather than faked.

No source file is modified. Adversarial review returned no rejects and flagged
nine re-hit subtests; all nine were removed and every target function
re-measured at an identical percentage, proving they carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 00:39:15 +01:00
rcourtman c483d2c014 Close the small residual branch gaps in five frontend modules
A second pass over modules whose own specs left only a handful of cold arms.
Counts are uncovered branches, module scoped, existing specs versus existing
plus new.

- diskResourceUtils 6 -> 0, the densest remaining gap in the frontend by
  ratio: missing capacity fields, absent SMART evidence and the no-candidate
  node arms.
- proxmoxPageModel 6 -> 0, availabilitySettingsModel 5 -> 0 and
  standalonePageModel 5 -> 0, all empty-collection guards, absent optional
  metadata and validation rejection arms.
- connectionsTableModel 8 -> 2 across eleven existing specs.

truenasPageModel was DISCARDED rather than committed: its remaining eight arms
measured no coverage gain and the agent correctly reported seven of them as
unreachable by construction, so there was nothing honest to land.

No source file is modified. Adversarial review returned no rejects and flagged
eight near-duplicate cases across four files; all eight were removed and the
module coverage re-measured as unchanged, proving they carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 00:27:00 +01:00
rcourtman 6060936cb3 Cover pure helpers and persistence round-trips across the backend
Seventeen files closing the zero-coverage functions the current source drop
left behind. Every named target was measured off 0.0 percent by a per-function
coverage delta, re-measured against current main.

- config: the five new durable Proxmox cluster-node identity helpers
  (deterministic id, endpoint equality, alias lookup, id existence, lookup by
  id) to 100 percent; VMware and agent-profile persistence round-trips under
  t.TempDir including that AppendProfileChangeLog appends rather than
  replaces; AI chat session save, load, delete, per-user scoping and age
  cleanup, with explicit timestamps rather than time.Now-relative fuzz;
  PVEInstance.DeepCopy asserted for nested independence.
- truenas: incidentFromPoolStatus over every pool health string,
  RecordsFromSnapshot over nil, empty and populated snapshots, both
  TransportStatus accessors, and the RPC handshake and auth typed errors
  through errors.Is and errors.As.
- unifiedresources: the maintenance-window operator-state lifecycle on
  MemoryStore including the not-found and already-cleared arms, plus the
  four remaining View accessors asserted on their exact formatted output.
- api: restoreAgentExecMetadata, buildAlertConnectionSnapshotsWithRuntimeSources
  and both mock series generators, asserted on shape, ordering and
  determinism rather than non-emptiness.
- cmd/pulse-control-plane: the four remaining MSP and mobile proof report
  printers, asserted on the concrete strings in captured stdout.
- ai: cost.EmptySummary, approval.emptyExecutionState, demo.IsDemoRuntimeIntended
  and tools.findCanonicalAppContainerResourceByReferences across no-match,
  first-match, later-match and ambiguous references.
- monitoring, models, alerts: trueNASAppRunning,
  supplementalProviderOwnedSourcesForOrg, IOCounterPresence.Effective,
  ValidAlertIntentSignal and intentTimePointer.

No source file is modified. Adversarial review returned no rejects across all
seventeen files and flagged four padding cases plus one dead table field; all
were removed and the per-function coverage re-measured as identical, proving
they carried nothing.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-25 00:21:35 +01:00
rcourtman ec6922b702 Cover the largest remaining frontend branch gaps
Seven modules carrying the biggest measured branch residue in the frontend.
Counts are uncovered branches, module scoped, existing specs versus existing
plus new, re-measured against current main after the TrueNAS topology
discriminator change landed under them.

- resourceDetailDrawerTrueNASModel 28 -> 3, and hasTrueNASDetailSections,
  which no test had ever called, is now covered on both verdicts.
- agentMachineTableModel 25 -> 5, missing metrics, absent agent metadata,
  blank status strings and the comparator ties.
- workloadSelectors 18 -> 1, comparator ties on every sort key so the
  tiebreak arm runs, plus the blank-search and undefined-metric arms.
- resourceDetailDiscoveryModel 18 -> 0, resources with no discovery metadata
  and the label and confidence fallbacks.
- proxmoxBackupRecoveryModel 17 -> 0, guests with no backups, unparseable
  timestamps and the recency classifier boundaries.
- api/truenas 15 -> 0, real request building and response handling arms.
- webInterfaceUrlFieldModel covers its three never-called functions.

No source file is modified. Adversarial review returned no rejects and flagged
six near-duplicate cases plus two test titles that overclaimed what their
bodies checked. All six were deleted and the coverage re-measured as identical,
proving they carried nothing; both titles were corrected without touching an
assertion.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-24 23:50:05 +01:00
rcourtman ab0b0b14ae Align the service availability fixture test with the check-row contract
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.
2026-07-24 23:31:50 +01:00
rcourtman 9449a56318 Rebuild SMART scan fixtures from real --scan output
6e93cb3b5 switched SMART discovery from `smartctl --scan-open` to `--scan`
but did not recapture its fixtures. It edited the comments and the command
string and left the payloads, so the tests asserted that `--scan` returns
`-d sat # /dev/sda [SAT], ATA device`, and the variable was still named
scanOpenPVESATA.

A capture from Unraid (tower, smartctl 7.5 2025-04-30 r5714) shows the real
shape. The non-opening scan cannot interrogate a device, so every libata
disk is reported as a generic SCSI device:

	/dev/sdb -d scsi # /dev/sdb, SCSI device
	/dev/nvme0 -d nvme # /dev/nvme0, NVMe device

and sysfs on the same host carries no protocol, transport or sas_address for
those disks, only vendor ATA under an /ataN/ path.

The production path is correct: smartctlDeviceTypeMatchesTransport rejects
the scan's scsi type against a sata transport, so probe selection falls back
to untyped plus the inferred -d sat. Only the fixtures were wrong, but they
were wrong in the direction that hid whether this worked at all.

Fixtures now carry the real scan shape with its provenance, the misleading
scanOpen prefix is gone, and the identity-only test stubs the vendor ATA a
real libata host exposes instead of leaving transport unknowable.

The new end-to-end test drives the captured shape and fails if a SATA disk
is ever probed as scsi or never reaches -d sat.

The megaraid claim in the Unraid fixture remains unverified: tower is plain
AHCI, so it cannot show whether `--scan` enumerates /dev/bus/N -d megaraid,M
controller members or whether that needs --scan-open. Left as-is pending a
capture from a host with an HBA.
2026-07-24 23:07:17 +01:00
rcourtman 4274befe55 Cover the last cold arms in seven frontend modules
Seven modules from the current source drop still had a measured branch
residue their own specs never reached. Counts are uncovered branches,
module scoped, existing specs versus existing plus new.

- resourceDetailMappers 13 -> 0, absent nested metadata, empty arrays and
  the unknown-discriminator default arm.
- infrastructureAgentUpdateCommandsModel 9 -> 0 on top of three prior
  coverage files.
- stackedMemoryBarModel 9 -> 1, zero and missing totals, over-total and
  negative segments, the clamping boundaries.
- api/vmware 9 -> 0 and api/nodes 8 -> 0, request building and response
  handling arms rather than mocked-fetch echoes. api/nodes also covers
  createHostAgentInstallToken, which had never been called.
- nodeModalModel 8 -> 0, blank fields and the validation rejection arms.
- utils/workloads 8 -> 0.

No source file is modified. Adversarial review returned no rejects and
flagged ten near-duplicate cases across the vmware and workloads files;
nine were deleted and the module coverage re-measured as identical, proving
they carried nothing. The tenth was restored because re-measuring showed it
did carry a branch.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no source or contract change
2026-07-24 23:06:13 +01:00