Commit Graph

3061 Commits

Author SHA1 Message Date
rcourtman 05d00a65a6 Hide raw Assistant tool-call output 2026-06-05 14:05:38 +01:00
rcourtman c5bee4a785 Surface Assistant provider readiness 2026-06-05 12:50:02 +01:00
rcourtman 6ba21c7a2b Clean up Assistant provider error messages 2026-06-05 12:25:56 +01:00
rcourtman 479ac6db2c Stream Assistant cold sessions directly 2026-06-05 11:54:01 +01:00
rcourtman 2b24c375e0 Abstain instead of fabricating discovery when commands can't run
When a deep scan was attempted for a workload (container/VM) but produced no
command output — e.g. the host agent rejects exec — the AI analyzer was still
asked to identify the service from metadata alone. It confabulated confident,
false identities: an ESPHome LXC and an influxdb-telegraf LXC were both
"identified" as Pi-hole at 0.95 confidence, with invented facts carrying
fabricated command sources (source: "pihole -v" for a command that never ran)
and a docker exec CLI for an LXC.

When a command scan was attempted for a command-dependent resource type but
yielded nothing, abstain: skip the analyzer entirely and return a not-determined
result (no service, no facts, zero confidence) with guidance to enable Pulse
Commands. Host agents are unaffected (identified from their own metadata), and
deployments without command scanning keep the metadata-only path. Abstaining
also avoids a wasted model call.

Verified live: re-running discovery on the esphome LXC now returns an empty,
zero-confidence result with the correct container CLI instead of fabricated
Pi-hole facts. Adds a regression test.
2026-06-05 09:57:57 +01:00
rcourtman 6bbbf185bb Make workload discovery repair known service results 2026-06-05 09:21:53 +01:00
rcourtman 71b1762d53 Sanitize provider stream errors shown in the Assistant chat
On a provider/transport failure the chat rendered the raw upstream error. An
OpenRouter 402, for example, dumped the provider's billing JSON and a
workspace-key dashboard URL straight into the message: unreadable, and a leak
of provider routing plus a key fragment.

Classify known failures (billing/402, auth, rate-limit, timeout, cancel) into
clean actionable messages, and for anything else strip the embedded JSON body
and any URL so raw provider payloads never reach the user. Applied to both the
in-stream error-event path and the transport-failure fallback. Adds tests,
including the exact OpenRouter 402 string.

Verified live: the 402 now reads "The AI provider rejected the request for
billing or quota reasons..." instead of the raw JSON+URL dump.
2026-06-05 09:00:50 +01:00
rcourtman 91528f5a07 Fix Assistant resource context and model route labels 2026-06-04 23:45:15 +01:00
rcourtman 80b20cb1ff Stream OpenRouter reasoning tokens in the OpenAI client
OpenRouter and other OpenAI-compatible gateways normalize chain-of-thought
into a "reasoning" delta field, while DeepSeek's direct API uses
"reasoning_content". The OpenAI-compatible client only read
"reasoning_content", so reasoning models routed via OpenRouter (e.g.
deepseek/deepseek-v4-pro, the configured Assistant default) had every
thinking token dropped. The Assistant showed a long dead pause and then the
answer instead of a live thinking stream, which read as "DeepSeek doesn't
stream".

Parse "reasoning" on both the streaming delta and the non-streaming message,
and surface it as a thinking event alongside the existing reasoning_content
path. Direct DeepSeek (reasoning_content) is unchanged. Adds a regression
test feeding an OpenRouter-style reasoning stream.
2026-06-04 23:25:50 +01:00
rcourtman a43f7cbe7f Add discovery readiness to Assistant context 2026-06-04 21:56:36 +01:00
rcourtman 29ac5945bc Fix resource-context Assistant tool targeting 2026-06-04 19:18:08 +01:00
rcourtman d305d602a6 Add resource-context Assistant eval 2026-06-04 18:44:47 +01:00
rcourtman 3834bc1a78 Add resource-context safety tests 2026-06-04 17:05:57 +01:00
rcourtman 3f8525a7c2 Implement resource-aware Assistant context 2026-06-04 16:36:04 +01:00
rcourtman bd6f77e093 Prepare v6.0.0 release candidate
Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate.
2026-06-04 14:07:14 +01:00
rcourtman efb6976069 Diagnostics: handle blank-name instances; add report-token router test
Two follow-ups caught by independent re-review:
- #29 (8855b78c0): GetConnectionStatuses keys a name-less instance by host,
  but the diagnostics lookup used 'pve-'/'pbs-'+Name only, so the
  monitor-state merge was a silent no-op for unnamed instances. Fall back
  to host to match.
- #22 (d9da9b18f, #1254): add a full-router test asserting an UNBOUND
  agent:report token gets 404 (not 200) on config fetch — continuously
  verifying the token-binding security boundary that the scope change
  relies on.
2026-06-04 11:49:17 +01:00
rcourtman ec74894982 Don't clear Ceph pool alerts in the stale-storage sweep
Follow-up to a85ec40a4 (#19), caught by independent re-review. Ceph pool
storage ("<instance>-ceph-pool-<name>") is raised/cleared by the separate
Ceph poll path (checkCephPoolStorage) and is NOT in the allStorage
inventory passed to SyncStorageAlertsForInstance — which runs before
pollCephCluster. Without a guard the sweep cleared the ceph pool usage
alert every storage poll and pollCephCluster re-raised it every cycle
(flapping: notification spam + StartTime reset). Skip any alert whose
ResourceID contains '-ceph-pool-'. Adds a regression test.
2026-06-04 11:43:03 +01:00
rcourtman d4edd1264d Fix TestRestart_StartIfServiceMissing expectation for single config load
#23 (a6b773a85) made Restart load the AI config once and pass it to
startWithConfig, so the start path no longer re-loads it. The test still
expected LoadAIConfig().Twice(); updated to .Once() to match the fixed
single-load behavior. (Caught by running the full internal/api suite,
which the per-fix targeted runs had missed.)
2026-06-04 11:23:05 +01:00
rcourtman fb91fbe9fe Revert #38 QuickAnalysis change (introduced a patrol regression)
Reverts b39cfc3a0. That port of v5 fix #1360 made QuickAnalysis build a
per-model provider via NewForModel, but v6 deliberately uses the configured
s.provider (it already has createPatrolProviderForModel for the per-model
case elsewhere), and NewForModel resolves unknown prefixes to a real Ollama
client rather than erroring — so the fallback never fired. This broke the
existing TestPatrolService_AskAIAboutAlert tests (panic via an empty
response) and changed v6's intended behavior. Restored to v6's original
QuickAnalysis. #1360 needs re-evaluation against v6's provider model before
any re-attempt.
2026-06-04 11:15:43 +01:00
rcourtman 1713662379 Stamp AI models with their provider for correct grouping (#1320)
Back-port the core of v5 fix 1de1392c9 to v6 (minimal, not the full
dedup refactor). The model list API now sets ModelInfo.Provider (stamped
in prefixProviderModels and passed through HandleListModels), and the
frontend groupModelsByProvider prefers the server-supplied provider over
splitting the model id, so models with opaque ids (e.g. Ollama-hosted
llama3-8b) group under the correct provider. Adds a grouping regression
test.
2026-06-04 10:47:40 +01:00
rcourtman 2db82fc302 Reject Docker host match on conflicting physical identity (#1366)
Back-port v5 fix 333e66a8e to v6, adapted to v6's DockerHostView accessors.
findMatchingDockerHost now skips an agentID/token match when
dockerHostIdentityConflicts detects a different MachineID, Swarm NodeID, or
(absent stronger IDs) hostname, so two physically different Docker nodes
that share a token+agentID are not collapsed into one host record. Adds a
regression test.
2026-06-04 10:35:35 +01:00
rcourtman a6b773a85f Reuse the loaded AI config when restarting the service
Back-port v5 fix 716340dcd to v6. Extracts startWithConfig from Start so
Restart passes the config it already loaded down to the start path, instead
of Start re-reading loadAIConfig a second time. Avoids starting the AI
service with a stale/inconsistent config if it changed between Restart's
load and the start. Both Restart start paths now use startWithConfig.
2026-06-04 10:30:45 +01:00
rcourtman 3928b19803 Attach ZFS pool status to dir storages on ZFS dataset paths
Back-port v5 fix ae6b663e9 to v6. Removes the 'only fetch ZFS pools when a
zfspool/zfs/local-zfs storage exists' gate (you cannot tell whether a dir
storage sits on a ZFS dataset without the pool list; fetch is gated by
PULSE_DISABLE_ZFS_MONITORING and empty-fast on non-ZFS nodes) and the
type guard before matchZFSPoolForStorage, so a dir storage whose path
resolves to a pool (e.g. /rpool/data) now gets ZFS health attached. The
guardrail-pinned 'if pool := matchZFSPoolForStorage(...)' call form is
preserved. Adds a poll integration test.
2026-06-04 10:27:59 +01:00
rcourtman b8a60db6fa Fall back to argv0 for agent self-update path on FreeBSD (#1457)
Back-port the agentupdate half of v5 fix 8600706da to v6 (the install.sh
FreeBSD rc.d supervisor-pidfile change is already present in substance).
performUpdate now resolves the executable via resolveExecutablePath, which
falls back to an absolute, existing os.Args[0] when os.Executable() fails
or returns empty — the condition that breaks agent self-update on
FreeBSD/OPNsense. Adds an injectable osArgsFn and a unit test.
2026-06-04 10:19:45 +01:00
rcourtman 67a8c4188b Use node-local task counts for node-scoped swarm services
Back-port v5 fix 48bdfdc30 to v6. In node scope, collectSwarmDataFromManager
now derives each service's Desired/Running/Completed task counts from the
node-local task list and drops services with no tasks on this node, instead
of reusing the cluster-wide ServiceStatus and a 'keep all when none match'
fallback. Without this, a node-scoped Swarm view showed wrong counts and
services that aren't running on that node. Adds two regression tests.
2026-06-04 10:16:09 +01:00
rcourtman b39cfc3a0e Respect the patrol model's provider in QuickAnalysis (#1360)
Back-port v5 fix 5f372e257 to v6. QuickAnalysis now builds a provider for
the configured PatrolModel via providers.NewForModel (matching the 8 other
call sites that already do so), falling back to the default provider if it
cannot. Previously a patrol model on a different provider than the default
(e.g. default Gemini, patrol OpenAI) was sent to the wrong backend. Guard
changed from provider==nil to cfg disabled, with a re-check after provider
selection. Updates the existing fallback test and adds a positive
patrol-provider test (httptest OpenAI server).
2026-06-04 10:12:07 +01:00
rcourtman 8855b78c0d Prefer live monitor connection state in diagnostics
Back-port v5 fix d310c257a to v6, adapted to v6's connection-status key
format. computeDiagnostics now merges a failed PVE/PBS diagnostics probe
with the monitor's live connection state: if the long-running poller still
reports the instance connected, a transient probe failure (network blip,
TLS re-check) no longer flips it to 'disconnected' in the UI. Uses v6's
'pve-<name>'/'pbs-<name>' status keys (v5 used a bare node name, which
would not match in v6). Adds a merge-logic regression test.
2026-06-04 10:07:10 +01:00
rcourtman 97c6cad01c Normalize typed SMART device labels for temperature merge
Back-port v5 fix 22f8dd3bb to v6. Adds normalizeSMARTDeviceIdentifier
(strips a trailing ' [scsi]'/'[ata]'-style type annotation and the /dev/
prefix) and uses it in the device-path match for both mergeNVMeTempsIntoDisks
and mergeHostAgentSMARTIntoDisks, with a non-empty guard. Without it, disks
whose SMART entries carry a typed label never matched by device path, so
their temperature stayed 0. Function signatures/call sites are unchanged
(canonical guardrail still passes). Adds a unit test.
2026-06-04 09:54:23 +01:00
rcourtman 9fe769ecdb Parse OPNsense 'Current Drive Temperature' SMART output
Back-port the smartctl regex half of v5 fix 0c2de2938 to v6 (the
prerelease-aware CompareVersions half is already present). smartctl text
fallback now matches 'Current Drive Temperature:' in addition to
'Current Temperature:', so OPNsense/pfSense disk temperatures are read
instead of reported as missing. Adds a regression test.
2026-06-04 09:50:37 +01:00
rcourtman 6b5f57a866 Prefer the root-mounted dataset for ZFS pool summaries
Back-port v5 fix b83f717ce to v6. zfsMountpointScore now gives top
priority (score 0) to a dataset whose mountpoint is exactly '/', even a
child dataset like rpool/ROOT/pve-1, so it outranks a pool-root dataset
mounted elsewhere (e.g. rpool at /mnt/storage). Previously a pool-root
dataset won unconditionally, so the pool summary could reflect the wrong
dataset's usage. Updates scoring tests and adds a regression case.
2026-06-04 09:43:07 +01:00
rcourtman 0fcad7c67a Normalize PBS alert threshold defaults on config update
Back-port v5 fix 117ec901d to v6, adapted to v6's config subpackage.
Adds NormalizePBSDefaults (called from UpdateConfig alongside the other
Normalize*Defaults) and pbs.cpu/pbs.memory to ValidateHysteresisThresholds.
PBS was the only monitored resource type whose thresholds were not
normalized on UpdateConfig, so user-supplied configs with nil/negative
PBS thresholds were never defaulted and a Trigger=0 'disable' sentinel
did not clear the Clear value. Adds regression test.
2026-06-04 09:37:28 +01:00
rcourtman 15657d9a0a Disambiguate linked host-agent alert names
Back-port v5 fix a4834ed80 to v6. hostDisplayName now appends
'(Host Agent)' (or returns 'Host Agent' for an unnamed agent) when the
host is linked to a Proxmox node/VM/container, and the disk-temperature
resource name uses hostDisplayName(host) instead of host.DisplayName.
Without this, a host agent running alongside the Proxmox node/VM it is
linked to produced alerts with an identical name, making them
indistinguishable. Adapted to v6's 'Agent' fallback. Adds test cases.
2026-06-04 09:33:02 +01:00
rcourtman 85ec355268 Preserve webhook mentions in list API and resolved notifications (#1118)
Back-port v5 fixes 5997fd81f and 0a7b93a84 to v6:
- GetWebhooks list response now includes the configured mention so the UI
  shows it after reload instead of blanking it.
- sendResolvedWebhook now assigns data.Mention (v6 set it for grouped/
  firing webhooks but dropped it on resolved), and the Discord/Slack/Teams/
  Mattermost ResolvedPayloadTemplate strings gained {{if .Mention}} guards.

Without these, a configured @everyone/@channel was silently omitted from
resolved/cleared notifications. Adds list-API and per-service resolved
mention regression tests.
2026-06-04 09:29:47 +01:00
rcourtman a648153717 Scope cluster TLS fingerprints to their own endpoints (#1199)
Back-port v5 fix ffaeea18d to v6. clusterEndpointEffectiveURL now derives
hasFingerprint strictly from the endpoint's own Fingerprint and ignores
the cluster-level value the callers pass. Previously, when only the
primary node had a fingerprint, every fingerprint-less cluster member was
routed to its per-node IP — bypassing hostname TLS certificate validation
while effectively pinning the wrong cert. Adds direct regression cases.

(v6 has no buildClusterClientEndpoints, so the v5 builder-level test is
covered by the unit cases instead.)
2026-06-04 09:11:07 +01:00
rcourtman 5134b39800 Keep snapshot polling independent of backup scans (#1437)
Back-port v5 fix 0dca8a037 to v6. Extract pollPVEBackupsAndSnapshots:
the two backup-inventory scans share a bounded backupCtx, which is then
cancelled, and pollGuestSnapshots runs on the parent context. Because
the parent has no deadline, pollGuestSnapshots establishes its own
60s-4min budget instead of inheriting an already-exhausted backup
deadline and skipping entirely (the v6 early-return at
monitor_backups.go made this strictly worse). Adds a regression test
proving snapshots still poll after the storage scan exhausts its budget.
2026-06-04 09:00:30 +01:00
rcourtman a85ec40a4e Clear stale storage alerts when storage leaves the inventory
Back-port v5 fix 398ef8117 to v6, adapted to v6's canonical alert keying.
pollStorageWithNodes now calls SyncStorageAlertsForInstance after the
per-storage checks, clearing storage-inventory alerts whose ResourceID is
no longer present. Matches by ResourceID (every v6 storage alert sets
ResourceID=storage.ID) rather than reconstructed alert keys, so it is
robust to v6's composite alert-state IDs (e.g. ...::metric-threshold:usage).
Conservative: live storage keeps its alert (ResourceID still valid),
non-storage alerts are untouched, Ceph alias IDs are treated as valid.

Without this, a storage that disappears or changes identity left its
alert visible until the multi-day stale-alert TTL. Adds unit + poll
integration regression tests.
2026-06-04 08:55:29 +01:00
rcourtman 9c8218ef0d Make backup orphan detection type-aware (vm vs ct VMID collision)
Back-port v5 fix 9fb76579c to v6. CheckBackupsWithInventory now filters
same-VMID guest candidates by backup kind (qemu/lxc) before attributing
a backup, in both the canonical-key lookup and the bare-VMID secondary
path. Without this, a vm backup with vmid=101 was attributed to an lxc
container with the same vmid (or vice-versa), suppressing the
orphaned-backup alert and attaching the wrong guest identity.

Adds guestMatchesBackupType/filterGuestsByBackupType helpers and a
regression test.
2026-06-04 08:45:13 +01:00
rcourtman d9da9b18f2 Allow legacy report-only agent tokens to fetch their own config (#1254)
Back-port v5 fix 04a828264 to v6. canReadConfig now accepts
ScopeAgentReport (legacy host-agent:report) in addition to the config/
manage/settings scopes; resolveConfigAgent already restricts a
report-only token to the host it is bound to, so it can only read its
own config. Without this, a v5 agent upgraded to a v6 server while still
holding a report-only token gets HTTP 403 on config fetch and breaks.

Retargets TestHandleConfigMissingConfigScope to monitoring:read and adds
TestHandleConfigAllowsBoundAgentReportScope as a #1254 regression guard.
2026-06-04 08:39:31 +01:00
rcourtman 545a6f22a0 Default legacy alert schedule settings on load
Back-port v5 fix af486b6f5 to v6. LoadAlertConfig now back-fills
Schedule.Cooldown=5, MaxAlertsHour=10, NotifyOnResolve=true and the
Grouping defaults for legacy alerts.json files saved before these
fields existed, probing raw JSON so an explicit 0/false is preserved.
Without this, a v5 user migrating to v6 loads with zeroed cooldown and
rate limit, silently losing notification-spam protection.
2026-06-04 08:32:06 +01:00
rcourtman 5a21f0c288 Make rate limiter sliding-window test deterministic 2026-06-03 23:14:25 +01:00
rcourtman ab2c2453c2 Synchronize auto-register reload test 2026-06-03 22:40:30 +01:00
rcourtman 67fb10fa63 Stabilize TrueNAS poller backend gate
Wait for enabled TrueNAS records to be cached before stopping the poller in the disabled-connection coverage test.

Refs release run 26911797444.
2026-06-03 22:08:36 +01:00
rcourtman b97fe12911 Stabilize hostagent backend release tests
Isolate hostagent coverage tests from failed websocket upgrades and runner-local device state.

Refs release run 26910672866.
2026-06-03 21:41:09 +01:00
rcourtman 01f8e9a697 Preserve empty guest metadata slices
Keep non-nil empty guest metadata slices intact when cloning store values so import/export replacement preserves the existing JSON shape.
2026-06-03 20:08:24 +01:00
rcourtman cf65853108 Fix guest metadata race in release backend
Copy guest metadata at the store boundary so asynchronous monitor persistence cannot race with caller-owned metadata during release backend race tests.
2026-06-03 19:40:57 +01:00
rcourtman 0d0eb4bf11 Stabilize v6 release dry-run backend gate 2026-06-03 18:12:42 +01:00
rcourtman 7440208163 Project Kubernetes agent versions onto node rows 2026-06-03 15:57:51 +01:00
rcourtman 17e854e7b6 Use agent update target for stale notices 2026-06-03 14:42:43 +01:00
rcourtman 2c360f7793 Merge dead-code cleanup: remove 198 unreachable Go funcs + 103 unused frontend exports
Verified via deadcode/ts-prune detection + 317-agent cross-repo verification swarm.
Gated: go build + go vet + test-compile (pulse module), tsc --noEmit (full project
incl tests), npm run lint. Net -2549 lines across 161 files.
2026-06-03 12:57:57 +01:00
rcourtman bdc232cd07 Fix alert re-notification rate limiting
Apply max alerts/hour to cooldown and critical re-notification paths so active alerts cannot bypass the UI delivery cap.

Persist LastNotified on active alert records when dispatching, including clone dispatches after reload.

Refs #1444
2026-06-03 12:43:17 +01:00