Commit Graph

3989 Commits

Author SHA1 Message Date
rcourtman ff2bfbb320 Make platform telemetry presentation evidence-aware 2026-08-14 00:18:23 +01:00
rcourtman 903b579f8a Add durable Patrol objectives 2026-08-13 23:59:33 +01:00
rcourtman a7646e5f86 Fix Patrol autonomy and refusal reporting 2026-08-13 23:26:08 +01:00
rcourtman 3981ce552b Honor explicit cluster member address overrides and surface recovery failures
An explicit connection address override on a cluster member was silently
discarded when VerifySSL was enabled and the member had no per-endpoint
fingerprint: the hostname-for-TLS preference displaced the operator's
address, so overriding an undialable discovered hostname changed nothing.
The override now wins in every TLS mode, and the hostname preference
applies only to auto-discovered addresses.

Failed endpoint recovery attempts also logged their cause at debug level
only, leaving the recurring 'No endpoints recovered' warning without a
reason. The warning now carries per-endpoint failure reasons and the
sanitized error is stored so the UI health status shows it too.

Refs #1665

Contract-Neutral: behavioral bug fix refs #1665: cluster member IPOverride honored in all TLS modes, recovery failure reasons surfaced; no public contract delta
2026-08-13 22:06:29 +01:00
Martin cd02d9f3c0 Honor the configured request timeout for discovery AI analysis (#1713)
* Honor the configured request timeout for discovery AI analysis

Both discovery services accepted an AIAnalysisTimeout config field, but
neither production construction site ever set it, so every AI analysis
call ran under the hardcoded 45s fallback. RequestTimeoutSeconds reached
the provider HTTP client but never the per-analysis context deadline, so
raising it could not help a slow local model: the outer 45s deadline
always fired first and surfaced "AI analysis timed out after 45s".

Add GetDiscoveryAIAnalysisTimeout, which returns the configured request
timeout when one is set and keeps the tighter 45s discovery default
otherwise, and wire it into both service constructors. Both services also
gain SetAIAnalysisTimeout so a settings change applies live instead of
requiring a restart; the timeout is now read through a mutex-guarded
accessor since it can be mutated while scans are running.

* Inherit the configured request timeout for discovery analysis

Returning 45 seconds when RequestTimeoutSeconds is zero left the
discovery deadline disagreeing with what operators are shown. Zero is
omitted from the settings response, the frontend displays 300 via
request_timeout_seconds ?? 300 and states that value applies to
discovery, and it only submits the field when the form value differs
from the displayed current value. An installation showing 300 seconds
therefore still timed out discovery after 45, and saving the visible
default did not correct it.

GetDiscoveryAIAnalysisTimeout now delegates to GetRequestTimeout so the
persisted default canonicalizes to 300 seconds. The discovery services
keep their own 45-second fallback for construction without an AI
configuration.
2026-08-13 21:56:53 +01:00
rcourtman a39935182d Parse smartctl 7.5 power_mode object on guarded probes
smartmontools 7.5 emits power_mode as an {ata_value, name} object whenever
the -n guard runs CHECK POWER MODE, which is every rotational-disk probe.
The parser declared the field as a string, so json.Unmarshal failed for the
whole document and healthy spinning disks degraded to the lossy text
fallback, surfacing as no usable SMART data while guard-free SSD probes
kept working. That is the exact rotational-only failure split in the
discussion 1690 debug log (SAS3224 HBA, smartctl 7.5). Decode both shapes
the field has used, key standby on the reported name, and never fail the
document over this field. Also broaden the text fallback standby match to
the EPC names (STANDBY_Y, STANDBY (OS), SLEEP) that the mode-suffixed
match missed.

Refs #1690

Contract-Neutral: smartctl 7.5 power_mode JSON parse fix, internal decoder only, no DiskSMART payload or subsystem contract delta (discussion #1690)
2026-08-13 10:25:28 +01:00
rcourtman 3355f7a671 Read host Docker credentials for private registry update checks
Container update detection only ever negotiated anonymous pull tokens, so
containers from registries that reject anonymous digest HEADs pinned a
permanent "authentication required" badge (#1706). The agent already runs
on the Docker host, so the checker now resolves the same credential store
docker pull uses - config.json auths entries, credsStore/credHelpers
credential helpers (docker-credential-<name> get), and Podman's auth.json -
and presents the stored login: Basic auth on Bearer token negotiation and
on the hardcoded Docker Hub / ghcr.io token endpoints, direct answers to
Basic challenges, and the refresh-token grant for identity-token logins
such as Azure ACR.

Credentials never leave the host: they are only presented to the registry
or its token endpoint, helper output stays out of reported check errors,
and lookups are cached in memory for five minutes. Helper names are
validated before exec, and a stale login falls back to the anonymous path
so checks that used to work keep working. Set
PULSE_DISABLE_REGISTRY_CREDENTIALS=true (--disable-registry-credentials)
to keep detection anonymous-only. The agent-lifecycle and security-privacy
subsystem contracts pin the host-local credential boundary.
2026-08-13 10:06:26 +01:00
rcourtman d6cf090737 Apply agent LXC filesystem data on the cluster/resources poll path
The host agent's node-local pct df inventory was only applied by the
per-node container poll fallback. Cluster instances are served by the
efficient cluster/resources path, which never called the enrichment, so
installs on that path showed rootfs-only container filesystems (and,
since the config-mount restoration, config-listed mounts with unknown
usage) no matter how healthy the linked agent was. Reported on #1477
after the reporter installed agents specifically to get per-mount usage.

Reproduced end to end against a live PVE cluster: the agent shipped the
full inventory, the server linked the agent and populated the
filesystem cache every report, and the poll path never read it. With
the enrichment applied after metadata enrichment, mirroring the
per-node path's ordering, the same rig surfaces real per-mount usage.

Refs #1477

Contract-Neutral: behavioral fix: cluster/resources poll path now applies agent pct df enrichment like the per-node path (#1477), no public contract delta
2026-08-12 14:45:38 +01:00
rcourtman 5632ee10e8 Keep the pending planned poll slot instead of compounding it
Every poll tick rebuilds the adaptive plan, and BuildPlan anchored the
next run on the previously planned NextRun by unconditionally adding the
selected interval. While a planned slot has not elapsed, each planning
pass therefore pushed it another full interval into the future. With the
default 10s tick that compounds NextRun ahead of wall-clock as soon as
the adaptive interval stretches beyond the tick cadence, so an instance
whose data was fresh at startup was never due again: API polling starved
permanently, the connection dropped to Agent and stale exactly when the
stretch began, and no error was ever logged. Matches the fourth #1437
reproduction (standalone degrades at ~2 minutes, cluster at ~4, stays
degraded), whose bundle shows a clean log with no polls after startup.

A pending future slot is now kept as planned, and it tightens to
now+interval when a staleness-driven interval shrink justifies an
earlier run. An elapsed slot still advances exactly one interval, so
ordinary cadence is unchanged.

Refs #1437

Contract-Neutral: behavioral fix: pending planned poll slot no longer compounds NextRun past wall-clock (#1437), no public contract delta
2026-08-12 11:22:29 +01:00
rcourtman 0c3f475dea Warn when proxy role gating falls back to the default admin role
Configuring PROXY_AUTH_ROLE_HEADER without PROXY_AUTH_ADMIN_ROLE now gates
admin access on an exact-case match against the "admin" default. That closes
the fail-open, but it also means an IdP sending "Admins", "Admin", or
"authentik Admins" grants nobody admin — and a proxy-auth-only deployment has
no local credential to fall back on. The change is recoverable by setting
PROXY_AUTH_ADMIN_ROLE and restarting, but only if the operator can tell that
is what happened.

Log a startup warning naming the header, the effective admin role, the
case-sensitivity, and both ways out. The warning is scoped to deployments
that actually configured a role header; defaulting the value with no role
header is inert and stays at info. The troubleshooting entry in
docs/PROXY_AUTH.md now leads with the exact-match rule, since that is where
a locked-out operator looks first.
2026-08-12 09:43:52 +01:00
rcourtman 11a8aa3b2f Fail closed when proxy auth configures a role header but no admin role
A reverse-proxy deployment that set PROXY_AUTH_ROLE_HEADER without also
setting PROXY_AUTH_ADMIN_ROLE granted every proxy-authenticated user full
administrator access. CheckProxyAuth only evaluated roles when both values
were non-empty, so the half-configuration skipped role gating entirely and
returned isAdmin=true. docs/PROXY_AUTH.md has always documented an `admin`
default for that variable, but the Config struct's envconfig `default` tags
are legacy and never applied (config.go), so nothing ever populated it.

CheckProxyAuth is the single admin verdict all 20+ proxy-auth gates consume,
so the fail-open reached every one of them. Verified on a scratch instance
with PROXY_AUTH_ROLE_HEADER set and no admin role: a request carrying only
`X-Proxy-Roles: user` received HTTP 200 and the full admin payload from
GET /api/system/settings, HTTP 200 from POST /api/system/settings/update,
and proxyAuthIsAdmin=true from /api/security/status. All three now return
403 / false, while `X-Proxy-Roles: admin` still passes.

Resolve the documented default in both layers that can produce the verdict:
config load populates ProxyAuthAdminRole when proxy auth is configured, and
CheckProxyAuth now keys role gating on the role header alone, resolving an
empty admin role through config.DefaultProxyAuthAdminRole. Configuring a
role header is the operator's signal that admin access is role-gated;
leaving the admin role unset must not switch that off.

Deployments that intentionally treat every proxied user as an admin are
unaffected: that is still expressed by leaving the role header unset.
2026-08-12 09:38:02 +01:00
rcourtman 34194e57be Show the real monitoring cadence to non-admin sessions
Non-admin sessions cannot read GET /api/system/settings, so the Settings
General Monitoring Cadence card fell back to the Realtime (10s) preset
regardless of the configured interval; an issue #1601 reporter read that
as the server polling faster for non-admins. Publish the effective
pvePollingInterval on the authenticated runtime-display projection
(runtime config first, persisted value only as fallback, matching the
admin route's precedence), consume it in the viewer fallback of the
settings state, and run that initialization for sessions without
infrastructureRead too, whose ungated General panel previously never
initialized presentation state at all.
2026-08-12 09:20:01 +01:00
rcourtman 3adeb77d60 Secure configuration transfer authorization (#1714)
Co-authored-by: Pulse Autonomous Maintainer <rcourtman@users.noreply.github.com>
2026-08-12 07:32:50 +01:00
rcourtman 359a372cdf Make per-guest backup/snapshot toggles inherit global thresholds
The per-guest Backup/Snapshot toggle persisted a full copy of the current
global defaults just to flip enabled, freezing the threshold values into
the override. Later global edits (a 32-day backup warning) then silently
never applied to toggled guests, which kept firing at the frozen factory
7-day warning. Reported twice in discussion #1126.

- Guest overrides now resolve against the globals at evaluation time:
  zero-valued fields inherit the global value, explicit values still win,
  and hand-written sparse overrides stop decoding as accidental zeros.
- The toggles write enabled-only overrides instead of freezing a copy.
- Normalization rewrites stored overrides whose thresholds exactly match
  the current globals into sparse form, which is behavior-preserving at
  migration time and un-freezes existing installs.
- The Backups/Snapshots global editors reconcile warning/critical pairs
  by adjusting the untouched field, so typing a 32-day warning no longer
  silently snaps back to the 14-day critical default.

Refs #1126
2026-08-12 06:38:25 +01:00
rcourtman e851253f3e Pin the config-only LXC mount wire shape end to end
Integration test for the exact production path: stock PVE answers the
LXC status query with an empty diskinfo map, enrichContainerMetadata
discovers the mpX mount from the container config alone, and the unified
resource projection serializes it with capacity, the -1 unknown-usage
sentinel, and an omitted used field while the live rootfs row survives.

Related to #1477
2026-08-11 21:03:27 +01:00
rcourtman 63f1a14f31 Show LXC mount points from container config on the API path again
Stock Proxmox reports no per-mount LXC usage through the status API, so
v6's API-polled containers listed only rootfs. The v5.1.32 fallback that
synthesized mount rows from the container config never crossed to the v6
line, and the v6.2.0 pct-df agent path only covers nodes running the
unified agent. Restore the fallback and improve it: parse size= so
config-only rows carry capacity, mark live usage unknown with the -1
sentinel, and merge without displacing the aggregate-seeded rootfs row.

Frontend consumers stop fabricating percents for sentinel rows: the
workloads row bar and summary math exclude them (tooltip lists them with
capacity), the drawer Filesystems block renders ?/<size> with no percent,
disk normalization preserves the sentinel, and per-machine max-disk
derivations skip them. Mock mode seeds one running container in this
exact shape so the surfaces stay exercised.

Related to #1477
2026-08-11 20:46:11 +01:00
Pulse Autonomous Maintainer 986a281006 Reflect global settings for non-admin viewers 2026-08-11 19:57:47 +01:00
rcourtman 56262c6368 Fix provider MSP evaluation setup flow 2026-08-11 16:51:15 +01:00
courtmanr@gmail.com 1549216b6e Handle registry responses without digest headers 2026-08-11 16:39:36 +01:00
courtmanr@gmail.com 791a2f86bf Route Pulse images through product update checks 2026-08-11 16:39:12 +01:00
courtmanr@gmail.com 66d8e90c0c Show Patrol findings in alert emails 2026-08-11 16:38:21 +01:00
courtmanr@gmail.com 16f9dbc709 Add push preferences to relay connect frames 2026-08-11 16:38:21 +01:00
courtmanr@gmail.com e21a3c4434 Classify normal WebSocket shutdowns as informational 2026-08-11 16:38:21 +01:00
courtmanr@gmail.com b3fdab4cae Persist first-run auth for systemd installs 2026-08-11 16:38:21 +01:00
courtmanr@gmail.com 1d0a0b7f7e Allow existing API tokens to be renamed 2026-08-11 16:38:21 +01:00
courtmanr@gmail.com 83dc17bf71 Avoid PVE linkage warnings for PBS agent profiles 2026-08-11 16:37:49 +01:00
courtmanr@gmail.com 0ac02e43c5 Allow insecure TLS for Proxmox agent installs 2026-08-11 16:37:49 +01:00
courtmanr@gmail.com 70469660b3 Clear Docker alerts when removing unified hosts 2026-08-11 16:37:37 +01:00
courtmanr@gmail.com 4dac4dd163 Allow agents to include filtered disk mounts 2026-08-11 16:37:37 +01:00
courtmanr@gmail.com df606ee81a Describe External Probes server-side alerting accurately 2026-08-11 16:37:37 +01:00
rcourtman 32d1fcbe42 Update canonical write-path guardrails for WriteBatchBounded
2bc4ed725 moved the unified metrics sync sites to WriteBatchBounded but
left the three source guardrails pinning the literal WriteBatchSync
call, so they failed. The guardrails' intent is that pipeline writes go
through the canonical batched ingestion path, which WriteBatchBounded
is; pin the new name.

Contract-Neutral: test-only: guardrail pins follow the WriteBatchBounded pipeline rename from 2bc4ed725
2026-08-11 14:58:09 +01:00
rcourtman 98f0711504 Sample mock seed assertions through the seeder's graph sampler
TestSeedMockMetricsHistory_SeedsVMwareMetricsStore and the TrueNAS
variant compared seeded storage values against the package-level
mock.SampleMetric, which resolves resource roles from the global
registry that only other tests populate. The assertions therefore
passed or failed depending on which tests ran earlier in the process:
isolated runs failed deterministically, and today's CI reshard flipped
the rest-0 shard red for commits that never touched the mock layer.

Both tests now build the same graph-aware sampler the seeder uses, so
the expectation is self-contained and order-independent.

Contract-Neutral: test-only: seed assertions sample via the seeder's graph sampler, removes cross-test registry dependence
2026-08-11 14:58:09 +01:00
rcourtman 2bc4ed7254 Split bounded pipeline metrics writes from synchronous batch writes
6b79aa997 bounded WriteBatchSync itself, which broke its read-your-writes
contract on slow disks: CI's metrics write-amplification and 500-node
load tests count committed rows after writing, and mock seeding reads
store coverage straight back, so the 2-second early return failed both
(runs 31475700902, 31494553977). Fast local disks masked it.

WriteBatchSync returns to a full commit wait. The monitoring pipeline's
four sync sites move to WriteBatchBounded, which carries the bounded
enqueue-plus-wait semantics, so the #1437 slow-disk stall fix stays
exactly where the hazard is. Both paths share prepareWriteBatch
validation, and a new regression test pins WriteBatchSync waiting past
the bounded budget.

Refs #1437

Contract-Neutral: behavioral fix: split bounded pipeline writes from synchronous batch writes, restores read-your-writes (#1437 follow-up), no public contract delta
2026-08-11 14:42:42 +01:00
rcourtman 52489e8926 Scale connection stale cutoff by the adaptive planned poll interval
Adaptive polling deliberately stretches an instance's cadence toward its
max interval (5 minutes by default) while data is fresh, but the
connections aggregator judged staleness against the configured cadence
with a 2-minute floor. Any adaptive-enabled install therefore cycled
healthy PVE/PBS/PMG connections into stale for the back half of every
stretched poll gap: the Infrastructure page dropped the source badge
from API + Agent to Agent and connection-degraded alerts fired against
a schedule the poller was honoring.

The aggregator now scales the active-to-stale cutoff by the scheduler's
currently planned interval when that exceeds the configured cadence,
via Monitor.PlannedPollInterval and per-instance planned intervals in
the aggregator inputs. A plan tighter than the configured cadence never
tightens the cutoff, so genuine poll outages still trip the floor on
time. Connection-degraded alerts and the runtime inventory gate consume
the same derived state and inherit the fix.

Refs #1437

Contract-Neutral: behavioral fix: stale cutoff follows adaptive planned interval (#1437), no public contract delta
2026-08-11 14:05:06 +01:00
rcourtman a04941b55a Implement canonical resource monitoring policy 2026-08-11 11:52:01 +01:00
rcourtman dfcfe3fbd1 Fix tenant-scoped service discovery execution 2026-08-10 19:53:08 +01:00
rcourtman 44a3f19454 Fix large Proxmox cluster poll exhaustion 2026-08-10 14:39:01 +01:00
rcourtman 04d6c43f20 Stop stamping Patrol attention rows with evidence-timing noise
Every attention row on every install (and all 38 rows on the public demo)
carried a warning 'Evidence incomplete; timing unavailable' / 'Evidence
timing unavailable' metadata line. Unknown freshness just means the evidence
source publishes no validity window - only availability-poller evidence does
- so it is not an operator-facing warning, and the 'timing unavailable' claim
was false with per-envelope observation times rendered beside it.

- Presentation: the scan row now surfaces an evidence phrase only for states
  worth a glance (unavailable, incomplete, stale, or window-backed current);
  complete evidence with unknown freshness stays quiet on the row and shows
  a muted 'Evidence recorded' badge in the detail, mirroring the existing
  unavailable-protection row rule.
- Alerts bridge: provider incidents without a backing availability check now
  carry a first-class complete/confirmed envelope derived from the observed
  incident payload instead of falling to the legacy partial shim, and each
  sync merges the cycle's freshly observed envelopes into the active alert so
  raise-time evidence does not age out while the condition is still observed.

Verified live on a scratch mock instance at 1280px and 744px: all 44+ mock
attention rows now project complete evidence and render without the noise
line; detail shows 'Evidence recorded' with per-observation timestamps.
2026-08-10 09:47:06 +01:00
rcourtman f3bfc7c72a Keep offline agents removable after restart 2026-08-10 09:34:42 +01:00
rcourtman 766253ed04 feat(commercial): treat the compiled Pro edition as commercial context
The Plans & Billing navigation entry is suppressed for sessions without
commercial context, which hid the only activation-form entry point from
exactly the customers who need it: a fresh Pro install has no license yet,
so hideUpgrade stayed true until the key the customer could not find a
place to paste was pasted. The compiled Pro binary is only distributed
through the paid broker flow, so a Pro-edition session is commercial
context by construction — edition.IsPro() now sets commercialContext in
securityStatusPresentationPolicy.

Ordinary free self-hosted sessions run the community binary and keep the
opt-in default (the edition marker keys off the compiled binary, never
license state). Demo mode and white-label keep full suppression via
hideCommercial, which wins over edition-derived context. Complements
1b9bc9482 (setup-completion activation pointer); together a fresh Pro
install gets both the first-run pointer and a permanently visible
Plans & Billing entry. api-contracts, storage-recovery, and
agent-lifecycle contracts record the policy delta.
2026-08-10 00:32:14 +01:00
rcourtman a7849c9ef0 Fix strict subscription tool schemas
Refs #1697
2026-08-09 21:00:07 +01:00
rcourtman 6c150973b4 Fix Agent Doctor credential recovery 2026-08-09 20:47:11 +01:00
rcourtman 1213b6d918 Stabilize remaining API performance proofs 2026-08-09 16:52:19 +01:00
rcourtman fe14b195d0 Stabilize API performance release proofs 2026-08-09 16:12:19 +01:00
rcourtman be18f99d24 fix(rbac): make SSO user access manageable 2026-08-09 13:36:07 +01:00
rcourtman 70f7ca9f6f fix(pbs): skip superuser-only node lookup for tokens 2026-08-09 12:25:07 +01:00
rcourtman f962fe83b1 fix(tests): stop alerts manager leak and settle frontend guardrails
Clears the red main streak that began at 63a0adf9a:
- newTestConfigHandlers never stopped its alerts.Manager, leaking a
  periodicSaveAlerts goroutine that logs through the global zerolog
  logger and races with the authorization-refusal contract test's
  logger swap under -race
- InfrastructureAgentDoctorPage used a raw select, tripping the
  settings native-select guardrail; converted to the shared labelled
  FormSelect primitive
- the metric-title residual pin still expected the removed -1 sentinel
  copy; aligned with the Off-control wording from 16179dd0a

Contract-Neutral: test-only CI red fix: stops alerts.Manager goroutine leak in api test helper, converts agent doctor raw select to the canonical FormSelect primitive the frontend-primitives contract already mandates, updates a stale test pin; no public contract delta
2026-08-09 11:00:51 +01:00
rcourtman 37e3c81b03 test(alerts): prove stopped guests preserve posture incidents 2026-08-09 10:20:08 +01:00
rcourtman c33ed56f1f fix(alerts): preserve backup posture for offline guests (#1693) 2026-08-09 10:08:40 +01:00
rcourtman 63a0adf9ac Repair rejected agent credentials safely 2026-08-09 00:51:38 +01:00