Commit Graph

6903 Commits

Author SHA1 Message Date
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 1bf0ffde55 Surface Assistant workflow progress status 2026-06-05 12:17:35 +01:00
rcourtman 86de23cc78 Focus Assistant composer on chat entry 2026-06-05 12:08:47 +01:00
rcourtman cd24716f90 Make Assistant new chat a local reset 2026-06-05 11:57:58 +01:00
rcourtman 479ac6db2c Stream Assistant cold sessions directly 2026-06-05 11:54:01 +01:00
rcourtman 7103a27ee1 Tighten Assistant chat cancellation flow 2026-06-05 11:39:34 +01:00
rcourtman 48caf059dd Improve Assistant chat responsiveness and composer 2026-06-05 11:22:31 +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 1e303cf507 Add copy-to-clipboard button on Assistant answers
Every modern chat surfaces a copy affordance; Pulse had none. Add a
hover-revealed copy button (top-right of the assistant card, which already
carries the group-hover hook) that copies the answer's markdown and flips to a
'Copied' check for 1.5s. Shown only for completed answers with content, so it
never appears mid-stream or on error-only turns. Clipboard failures (insecure
context / permissions) fail quietly.

Verified by component tests (renders + copies exact content, hidden while
streaming and when empty). Live screenshot skipped: the configured OpenRouter
default is out of credits and can't produce an answer to copy right now.
2026-06-05 09:28:08 +01:00
rcourtman 6bbbf185bb Make workload discovery repair known service results 2026-06-05 09:21:53 +01:00
rcourtman 182e317acf Render Assistant errors as a recoverable block with Retry
Failed turns rendered the error as plain "Error: ..." message content, which
read like a normal answer and dead-ended: the user had to retype the prompt.

Carry the failure in a dedicated ChatMessage.error field and render it as a
distinct error block (alert styling, preserves any partially streamed answer)
with a Try again button. Retry drops the failed assistant turn and its prompt
from the view and re-sends, so the conversation shows one clean attempt instead
of stacked dead-ends. All three error paths (stream error event, request catch,
session-create failure) now use the error field.

Verified live against the OpenRouter 402: clean message + working retry, no
duplicate user bubble, no raw provider payload. Adds tests for the error block,
the retry button, and retryMessage.

Known follow-up: a failed turn already persisted its user prompt server-side, so
retry double-records the prompt in session history; a backend retry-in-place
path would remove that. Tracked in the Assistant UX goal.
2026-06-05 09:13:27 +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 da7fc95cb3 Render Assistant answer as one block when reasoning interleaves
Reasoning models reached through gateways like OpenRouter (e.g.
deepseek/deepseek-v4-pro) interleave reasoning and answer tokens rather than
sending all reasoning first. The chat display grouped only CONSECUTIVE content
events, so each interleaved content delta became its own markdown block. Block
level whitespace trimming then collapsed every inter-word space and a markdown
table split across blocks never parsed, so the answer rendered as unreadable
run-on text with a dead table.

Extract the grouping into groupStreamEventsForDisplay and merge content (and
reasoning) each into a single block across intervening thinking events, while
keeping tool/approval/question events as hard boundaries so text before and
after an action stays ordered. Direct DeepSeek (reasoning-then-content) is
unchanged. Adds unit tests for the interleaved, tool-boundary, and consecutive
cases.
2026-06-04 23:40:17 +01:00
rcourtman 82bf3138dc Make discovery scan scope fully selectable
Turn the discovery settings scan-scope choices into full-row controls so Custom subnets can be selected reliably from the dialog.

Add regression coverage for custom scope selection and common subnet chips.
2026-06-04 23:37:47 +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 e7c091b63f Surface discovery scan status in infrastructure settings
Make network discovery observable from Connected systems with status, scope, candidate review, and settings actions.

Update the continuous-discovery follow-up and subsystem contracts to capture the visibility requirement.
2026-06-04 23:19:55 +01:00
rcourtman 3c24304826 Update subsystem lookup line expectation 2026-06-04 21:58:35 +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 c9414e5be8 Record v6.0.0 GA rehearsal
Capture the passing Release Dry Run metadata, rollback command, GA date, v5 support window, and follow-up state for the v6.0.0 publication gate.
2026-06-04 14:47:18 +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 a73a24259c Make the #1320 model-grouping tests actually teethy
A test-efficacy (mutation) check found the #1320 grouping tests were
vacuous: they used opaque ids like 'llama3-8b' that getProviderFromModelId
defaults to 'ollama' anyway, so they passed even without the provider-field
fix. Switched to ids the heuristic MIS-detects ('gpt-oss-20b' -> openai,
'my-claude-clone' -> anthropic) with provider='ollama', so the tests now
fail without the fix and pass with it — verified by reverse-applying the
fix and confirming failure.
2026-06-04 12:04:19 +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 723fb3e23c Prefer server provider in the chat + picker model grouping too (#1320)
Completes 171366237, which only updated patrolFormat.ts's
groupModelsByProvider. The chat model selector (aiChatUtils.ts) and the
settings model picker (AIModelPicker.tsx) — the two most user-visible
dropdowns — still grouped by getProviderFromModelId(model.id), so opaque
Ollama model ids were still mis-grouped there. Both now prefer the
server-supplied model.provider, plus the picker's search filter. Caught by
independent re-review. Adds a grouping test.
2026-06-04 11:44:18 +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 99532e21fa Cap QNAP autorun encrypted-volume wait at 30 minutes (#1422)
Back-port v5 fix 9fe622b88 to v6's append_qnap_autorun_block. The autorun
waiter now polls for the wrapper every 2s up to a 1800s cap, logging the
wait duration on success and a timeout message if the encrypted data
volume never unlocks — instead of v6's unbounded 'while [ ! -x ]; do sleep
5; done' loop that hangs forever. Runtime counters are escaped so they
evaluate at QNAP boot, not install time; marker-based removal keeps it
uninstall-safe.
2026-06-04 10:24:33 +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 6b6684a119 Always apply the server full-width mode after auth (#1130)
Back-port v5 fix 6c3a2cd70 to v6. Adds layoutStore.applyServerMode, which
applies the server's canonical fullWidthMode authoritatively (even when a
local preference exists), and the post-auth path now calls it with the
already-fetched system settings instead of loadFromServer() — which made a
redundant API call and short-circuited on any stale localStorage entry, so
a user's server-side full-width setting was not honored after login/reload.
Exports createLayoutStore and adds a regression test.
2026-06-04 10:02:14 +01:00
rcourtman 2aec933897 Normalize empty/non-array chat session payloads at the source (#1149)
Back-port v5 fix 69f44d382 to v6, but fix the owning layer instead of the
call site: AIChatAPI.listSessions() now coerces a null/non-array response
to an empty array, so every caller (useAISettingsState and AI/Chat both
consume it unguarded in v6) is protected from a .length/.some()/.map()
crash on a malformed server response, not just one. Adds API-level tests.
2026-06-04 09:57:22 +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 ae8c2558cc Stop persisting the plaintext admin password in setup handoff
Port the v5 code-scanning fix (a8ee51fb9) to v6's reworked setup flow:
the first-run setup wizard no longer writes the plaintext admin password
to sessionStorage. The password is shown once on the completion screen
from in-memory wizard state, which never touches browser storage, so the
"save your credentials" surface is unaffected.

The SETUP_HANDOFF now carries only username + admin API token. That token
is the credential the infrastructure-onboarding page actually needs (it
auto-generates the first scoped install token via the already-persisted
apiClient auth token), so dropping the password costs no functionality.
The infra handoff block drops the Password card / Copy password button and
notes where the password was shown; the credentials download links to
Settings -> Security -> Change password instead of printing it.

SecurityStep.test.tsx now asserts the handoff persists username + apiToken
and that the plaintext password is never written to storage.
2026-06-04 09:36:53 +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