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#1621Fixes#1622
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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#1624Fixes#1614
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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.
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
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
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
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
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
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
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.
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.
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
linuxBlockDeviceTransportEvidence tested sas_address before vendor. A SATA
disk attached to an LSI/mpt3sas HBA, which is the common Unraid and TrueNAS
layout, exposes sas_address on its scsi_device while reporting vendor ATA,
so it classified as SAS. linuxInferredSmartctlDeviceTypes then returned only
"scsi" and smartctlDeviceTypeMatchesTransport discarded the correct "-d sat"
hint, steering a direct-ATA disk back to the "-d scsi" probe that 6e93cb3b5
exists to avoid.
Vendor ATA is the SCSI layer's marker for a device reached through a SAT
translation layer, so it is the more specific signal and is now tested
first. A genuine SAS disk reports its own vendor and never ATA, so the SAS
arm is unaffected.
Regression coverage pins all three shapes (SATA behind an HBA, a genuine SAS
disk, and a direct SATA disk) plus the resulting probe hint, and was
verified against the old ordering: it reports transport "sas" and inferred
types [scsi] for the HBA case.
d235aab3c put MigrateFromFiles on a production path for the default org and
failed NewSQLiteManager whenever the import was rejected. That propagated to
GetManager, so every RBAC route returned 503 including ResetAdminRole, which
is the operator's only way back. The realistic trigger is a v5 install that
recreated a role the v6 store already holds under the same ID, so an
ordinary upgrade could leave an operator with no route to repair it short of
hand-editing the legacy JSON.
Rejecting the import is right and stays: importLegacyRBAC is transactional
and the legacy files are left in place, so a failure leaves the database
un-migrated rather than half-migrated. Missing roles deny access rather than
granting it, which is why the store is safe to keep serving.
The store now stays live and records the failure on MigrationError. The
deliberate fail-closed behaviour of the management surface is preserved
rather than removed: the handler manager accessors surface the migration
failure as the same 503 rbac_store_unavailable as before. Recovery reaches
the provider directly, so it is exempt by construction.
The existing 503 contract test is what caught the first attempt at this,
which simply let the surface serve un-migrated data. It now additionally
asserts recovery is reachable, so the two halves are pinned together.
ac0fb263c changed preserved guests to carry their real runtime status
("running") instead of the stringified aggregate status ("online"). The
three guest metric recording loops gate on Status == "running", so guests
carried forward while their node sits in the grace period started passing
that gate. The carried-forward projection has no counters, so every cycle
wrote CPU, disk and network zeroes into the history and the persistent
store. Sparklines and stored metrics showed a collapse to zero for a guest
Pulse could not see, rather than a gap. Memory escaped only by accident,
because the projection leaves usage unknown and historyMemoryUsage returns
the -1 sentinel.
Freshly built guests are stamped with the cycle's sample time while
preserved guests keep the LastSeen of the cycle that observed them, so that
field already distinguishes the two. All three loops now gate on
guestObservedInCycle: the efficient path in recordGuestMetrics, and the
traditional VM and container paths, which had the same defect inline.
The guard fails open when LastSeen is absent. Dropping a real sample is the
worse error and the harder one to notice, so no evidence means record.
Regression coverage asserts both directions, and both were verified by
breaking them: removing the guard reports the fabricated zero sample, and
inverting it reports the observed guest losing its sample entirely.
Discovery fingerprints hosts on the scanned subnet, and its PBS fallback
probes api2/json/status and api2/json/config/datastore with no credentials,
which PBS records as authentication failures. Configured connections are meant
to be skipped for exactly that reason, so every gap in the skip writes noise
into an operator's own server log on every scan (#1616, split from #1606).
Two paths failed open. Hostname resolution kept only the first IPv4 and broke
out of the loop, leaving a multi-homed host partly suppressed while discovery
reaches whichever address it reaches. Resolution failure returned nothing at
all with no record that suppression had been skipped, so the host stayed
scannable and looked identical to one that was never configured.
Resolution now covers every IPv4 a configured host answers with, and both a
lookup failure and an IPv4-less answer are logged as the suppression gaps they
are. An unresolvable host still yields no entry, which is unavoidable, but it
no longer does so silently and cannot affect the other configured hosts.
This does not by itself explain the reporter's case in #1616. Their sanitized
export rewrites the PBS host to a generated placeholder, so it cannot show
whether the connection was configured by address or by name.
Verified: internal/monitoring, gofmt, canonical completion guard, status,
control-plane, registry and contract audits, active-target automated and
hybrid readiness proofs, and all thirteen release-control unit test modules.
c77571685 added a per-instance org_switched handler to
createNonSuspendingQuery that only called reset(). reset() bumps the
request generation and writes value, loading, error and resolvedOnce, none
of which the source effect tracks, so the effect cannot re-run and the
handler never issued a new request. A consumer with a constant source and
no polling therefore emptied on an org switch and stayed empty until it
remounted. Two such consumers exist, connectionsSnapshot in
useWorkloadsState and patrol-status in usePatrolIntelligenceState; polling
consumers self-healed on their next tick, which is why this was easy to
miss.
The handler now refetches after resetting, matching the clear-and-refetch
shape useWorkloads, useUnifiedResources and useStorageSummaryCharts already
use. Stale pre-switch responses are still discarded by request generation.
The existing late-resolution test captured a single resolver, which the new
refetch would overwrite, so it now keeps every resolver and settles the
pre-switch one specifically. That keeps it aimed at the stale-response
guarantee it names rather than silently testing the newest request.
4815a5c80 split PBSDiagnostic into canonical fields plus a nested probe
object, moving probe failure text to pbs.probe.error and adding
pbs.stateReason. sanitizeDiagnosticsData spreads the PBS entry and redacts
only host, name, id and the top-level error, so transport strings such as
"dial tcp 192.168.1.20:8007: connect: connection refused" stopped being
IP-redacted. That text used to live in pbs.error and was scrubbed; it now
rode unredacted into the sanitized bundle operators attach to support
requests.
The new fields are spread conditionally so an entry without them keeps its
exact previous shape, which leaves the existing strict-equality coverage
untouched.
Both proofs assert on the serialized bundle rather than on the sanitizer's
source, because the failure mode is a payload field added later that the
sanitizer never learned about. Verified to fail with the redaction removed,
reporting the raw address inside the exported JSON.
f744e0700 moved webhook URL redaction onto the canonical
RedactWebhookURLSecrets helper and applied it at five log sites, but missed
two. checkWebhookRateLimit logged the raw webhookURL, and the enhanced
sender logged the raw webhook.URL on the same event. The enhanced path is
the one Gotify and other token-in-URL destinations actually take, and a
rate-limit drop is the event most likely to repeat for a misconfigured
destination, so the token reached the logs on every retry.
Both sites now redact. The reason the sweep missed them is that no test
asserted log content, so the new proof captures zerolog output and fails on
the raw token rather than reading the call sites, which a source scan cannot
do reliably.
Regression coverage: TestWebhookRateLimitLogsRedactURLSecrets, verified to
fail against the unredacted call site with the token visible in the captured
log line.
removeAvailabilityProjectionLocked compacted each resource's relationships
in place with a Relationships[:0] filter. That is only safe if registry
clones own their backing array, and they did not: cloneResource deep-copies
roughly twenty-five slice and map fields but Relationships was left as the
shallow copy from the struct assignment, so a clone shared the original's
array.
The reachable path is ordinary. GET /api/resources rebuilds a registry from
Monitor.UnifiedResourceSnapshot, whose records come from GetAll clones, so
the filter wrote through into the live monitor registry from an HTTP
goroutine without holding that registry's lock. Every request performed
unconditional same-value writes, and whenever a legacy checks edge was
genuinely dropped the compaction left a duplicated trailing relationship on
the live host resource. The corrupting case is the post-upgrade seed the
projection exists to repair.
cloneResource now clones Relationships like every other slice, and the
filter builds a fresh slice so correctness no longer depends on each caller
having cloned first.
Regression coverage:
- TestCloneResourceGivesRelationshipsTheirOwnBackingArray pins the clone
isolation, and fails with the clone line removed
- TestAvailabilityRepeatedProjectionKeepsHostRelationshipsStable drives the
real ingest path across repeated re-projection
599c8e634 changed the TrueNAS pool projection to publish the ZFS data vdev
layout ("mirror", "raidz2", "stripe") as Storage.Topology. Topology is the
closed discriminator vocabulary every storage consumer keys on, and TrueNAS
pools travel as ResourceTypeStorage, so the frontend identifies them solely
by storage.topology === 'pool'. On any appliance whose pools report data
vdevs, every pool therefore fell out of the TrueNAS page, its datasets
orphaned, and per-system pool counts read zero.
Nothing caught it because internal/truenas/fixtures.go carries no VDevs, so
the projection still returned "pool" for every fixture while the producer
side pinned the new "mirror" value. Both suites stayed green and disagreed
with each other.
Pools now publish Topology "pool" again, with the layout in the new
Storage.VDevLayout field. poolVDevLayout returns an empty string when the
native report carries no data vdevs so callers fall back rather than
inventing a layout, and the presentation surfaces that previously read the
layout out of topology now prefer vdevLayout and fall back to topology.
Alert typing was unaffected (unified_eval.go already routes non-dataset
storage to truenas-pool through its default arm); incident metadata carried
the layout string and now carries the discriminator again.
Regression coverage pins the boundary from both sides, driving real vdev
layouts rather than the vdev-less fixtures:
- TestPoolTopologyStaysDiscriminatorAcrossVDevLayouts (producer, five layouts)
- TestStorageTopologyAndVDevLayoutAreDistinctIdentityFields (clone and wire)
- pool identity boundary cases in truenasPageModel.test.ts (consumer)
- topology label cases in storageAdapters.test.ts and the TrueNAS drawer model
The alerts-frontend-surface accepted proof list is asserted verbatim by
canonical_completion_guard_test.py, so registering
useAlertOverridesState.test.tsx in registry.json without updating the pinned
expectation failed the guard unit tests.
Verified by running every step of the canonical-governance workflow locally
rather than only the guard I expected to trip: status, control-plane, registry
and contract audits, the Pulse Intelligence gate schema, active-target
automated and hybrid readiness proofs, and all twelve release-control unit
test modules.
The registry audit requires exact_files to be sorted, and 5715ec754 inserted
useAlertOverridesState.test.tsx before useAlertDestinationsTabState.test.tsx
rather than after it, which failed the audit.
Verified: registry_audit.py --check and contract_audit.py --check both clean.
2118e3345 moved the Virtualization Hosts threshold section off a bare
`type === 'agent'` selection and onto provider-owned nodes, but landed without
the contract delta or a registered verification artifact, so the completion
guard blocked it.
The alerts contract now states the rule the fix depends on. Threshold sections
are keyed by override identity rather than resource type, and a resource that
appears in two sections has two different override keys, so a threshold saved
from the wrong row is written where alerting never reads it. Frontend
primitives records that surfaces classify through the shared
`isPulseAgentPlatformResource` classifier instead of re-deriving the standalone
versus provider-owned distinction from a raw resource type.
`useAlertOverridesState.test.tsx` already carried the right proof and simply
was not on the alerts-frontend-surface accepted list, so it is registered
rather than duplicated elsewhere.
Verified: canonical completion guard passes on the staged set, registry.json
parses.
A machine monitored only by the Pulse agent was listed under Alerts,
Thresholds, Virtualization Hosts as well as under Machines, because the
section was fed by every resource of type `agent` rather than by
provider-owned virtualization nodes.
The two rows do not share an override identity. The Machines row resolves
through `hostOverrideIdCandidates`, which prefers the agent-derived id, while
the Virtualization Hosts row reads and writes `overridesMap.get(node.id)` on
the bare resource id. A custom threshold saved from the Virtualization Hosts
row therefore landed under a key agent alerting never reads, so the machine
kept alerting on the global default with no visible reason.
The section now takes `virtualizationHostResources`, which excludes standalone
Pulse-agent resources via the existing `isPulseAgentPlatformResource`
classifier already used by the standalone page, platform navigation and the
resource drawer. Provider-owned nodes are unaffected, since they carry
provider-owner platform evidence. Those machines lose nothing: they keep their
Machines row, which is the one alerting honours.
Reported via support by a customer whose cloud host kept firing at the 85%
global agent memory limit despite a 100% override saved on the wrong row.
Adds the subsystem contract deltas and verification artifacts for the
preceding wearout and physical-disk-node fixes, which landed without them.
The contracts now state the rule rather than leaving it implicit in each
caller. Wearout has two absent states and one real zero, -1 is the unreported
sentinel, and 0 is evidence only from a device that reports endurance at all.
storagehealth.WearoutReported is the single authority, and alerting, the
unified risk projection, and the frontend all gate on it. Storage-recovery
records that disk-to-node association is evidence-directional, so a disk with
no Proxmox scope still matches on node name while a scoped disk stays inside
its instance.
Proofs cover the predicate itself, a spent SSD alerting where it previously
stayed silent, absent evidence staying quiet on both the unreported sentinel
and a rotational zero, the view returning the sentinel rather than the struct
zero value, and the presentation helpers agreeing with the server.
Verified: full frontend suite 1061 files / 19405 tests, canonical completion
guard, gofmt, go build ./..., and internal/alerts, internal/monitoring,
internal/storagehealth, internal/unifiedresources.
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.
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.