Commit graph

7625 commits

Author SHA1 Message Date
rcourtman
028e8c8df2 fix(unifiedresources): add space reclamation and retention for all append-only tables
The unified_resources.db grew without bound (2GB reported) because:

1. No VACUUM: DELETE freed rows internally but never shrank the file.
   Added auto_vacuum(INCREMENTAL) to the DSN for new databases, plus a
   one-time migrateAutoVacuum() that converts existing databases.
   reclaimFreePages() now runs after each prune cycle to return freed
   pages to the OS via PRAGMA incremental_vacuum.

2. Missing retention: action_lifecycle_events, export_audits, and
   loop_reports had no retention at all. Added 90-day retention for
   lifecycle/export audits and 30-day for loop_reports, matching the
   existing action_audits/resource_changes cadence.

3. Slow cleanup cadence: the retention loop ran every 6h and never on
   startup. Reduced to hourly and added an initial prune 30s after
   startup so a restart with a bloated DB starts recovering immediately.

Mirrors the proven pattern from metrics.db (auto_vacuum INCREMENTAL +
incremental_vacuum + WAL checkpoint).

Refs #1496
2026-06-27 18:38:56 +01:00
rcourtman
d6aed650b1 fix(docker): stop manual update check from looping on ack failure
getDockerCommandPayload returned dispatched commands on every report
fetch, causing the agent to re-execute check-updates on every poll
cycle. When the ack also failed, the report was buffered and retried,
creating an infinite loop.

- Only return command payload on the queued->dispatched transition;
  subsequent fetches return nil (agent already received it).
- Don't propagate ack errors from handleCheckUpdatesCommand; the report
  was delivered and check-updates is fire-and-forget. Command expires
  if ack never succeeds.

Refs #1504
2026-06-27 18:26:23 +01:00
rcourtman
b5ddbd5606 fix(monitoring): use MemAvailable instead of MemTotal-MemFree for Linux guest/node memory
When the Proxmox API's meminfo/status payload omits Available, Buffers,
and Cached (common for QEMU guest-agent and node status responses), the
code derived 'available' from Free alone — producing used = Total-Free
which counts reclaimable page cache as used memory (e.g. 94% instead of
the correct 76%).

Guest path (deriveGuestMemInfoAvailable): return 0 when cache metrics
are completely missing and only Free is available, so resolveGuestStatusMemory
tries better sources: guest-agent /proc/meminfo file-read (returns
MemAvailable), RRD memavailable, or the linked host agent.

Node path (resolveNodeMemory): always try RRD memavailable when cache
metrics are missing, not only when effectiveAvailable == 0 — previously
a non-zero Free value blocked the RRD fallback.

Refs #1501
2026-06-27 17:41:00 +01:00
rcourtman
22ed97f562 fix(email): eliminate data race on provider username resolution
sendViaProviderWithAddresses mutated the shared e.config.Username for
provider-specific defaults (SendGrid, Postmark, SparkPost, Resend).
If concurrent goroutines sent email simultaneously, this was a data
race on the config struct.

Move the resolution into negotiateAuth via a local variable
(resolveProviderUsername helper) so the shared config is never mutated.
2026-06-27 17:36:51 +01:00
rcourtman
c07fce0c33 fix: clean up copy-to-clipboard timeouts on unmount
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Add timer cleanup to copy-to-clipboard handlers in TokenRevealDialog,
UpdateBanner, QuickSecuritySetup, SetupCompletionPanel, and WelcomeStep.
Rapid clicks no longer accumulate dangling setTimeout callbacks.
2026-06-27 17:25:54 +01:00
rcourtman
cc1ebb569f fix(keyboard): Cmd+K command palette works from editable fields
The isEditableTarget early-return blocked Cmd+K when focus was in any
input, textarea, or select element. The command palette shortcut must
work from anywhere — it is the primary keyboard navigation path.

Move the Cmd+K check before the editable-target guard.
2026-06-27 17:20:28 +01:00
rcourtman
28d2413c71 fix(hostagent): clear stale Unraid sync action when resync position is zero
Unraid's mdResyncAction field can retain its last value (e.g. "check")
after a parity check is canceled, causing Pulse to report a stale sync
action indefinitely. The mdResync/mdResyncPos field is the authoritative
indicator: it drops to 0 when no resync is running. Gate SyncAction on
a non-zero position so the alert clears once the sync actually stops.

Refs #1485
2026-06-27 17:08:02 +01:00
rcourtman
5bc209d919 fix(alerts): deduplicate flapping history entries on load and periodic cleanup
Add deduplicateHistory() to HistoryManager that collapses consecutive
same-alert entries within a 5-minute window. The flapping/re-fire bugs
(lifecycle and stateful paths) created many duplicate history entries
before the cooldown fixes were deployed. This cleanup runs on startup
after loadHistory and periodically alongside cleanOldEntries, so both
existing noise and any future edge-case duplicates are handled.

On this install: 332 entries → 130 (60% reduction).
2026-06-27 16:36:29 +01:00
rcourtman
125959e4e0 fix(dev): login endpoint respects admin bypass mode
handleLogin rejected all credentials when ALLOW_ADMIN_BYPASS=1 because
it validated against config AuthUser/AuthPass directly without checking
the bypass flag. This made dev-mode browser testing impossible after a
backend restart with bypass env vars — the API middleware accepted all
requests but the login page could never obtain a session cookie.

When bypass is enabled, accept any credentials and create a session as
'admin'.
2026-06-27 16:09:23 +01:00
rcourtman
fa7e4711e7 fix(alerts): re-fire cooldown for stateful alerts to prevent duplicate history
Stateful alerts (ZFS pool/device, storage topology) were creating
duplicate history entries every poll cycle because SyncStorageAlertsForInstance
clears alerts between evaluations, making the existing-alert check miss.
Add the same 5-minute re-fire cooldown used in the lifecycle path: when a
stateful alert re-fires within 5 minutes of resolution, reactivate the
original (preserving StartTime) and update the existing history entry's
LastSeen instead of appending a new entry.
2026-06-27 15:58:22 +01:00
rcourtman
40ad4178ba fix(alerts): prevent duplicate history entries for flapping alerts
When an alert fires, resolves, and re-fires within 5 minutes (the
recently-resolved retention window), the previous implementation created
a new history entry for each fire cycle. For a flapping connection like
an unreachable Proxmox node, this produced hundreds of duplicate entries
in a single night — 302 identical 'pi unreachable' alerts in one case.

The fix checks the recently-resolved map before creating a new history
entry. If the alert was resolved within the cooldown window, it
reactivates the original alert (preserving StartTime) and updates the
existing history entry's LastSeen instead of appending a new one.
2026-06-27 15:38:13 +01:00
rcourtman
f7f573fe98 fix(security): resolve all Dependabot vulnerabilities
Upgrade dompurify 3.4.1 -> 3.4.11 fixing 8 XSS/sanitization bypass CVEs.
Add form-data ^4.0.6 npm override fixing CRLF injection (high severity).
All 6727 frontend tests pass.
2026-06-27 15:37:37 +01:00
rcourtman
e197fa3db5 Handle JSON marshal error in agentic question tool response
Unchecked json.Marshal error could silently return empty string instead
of surfacing the encoding failure to the caller.
2026-06-27 15:28:32 +01:00
rcourtman
ad645b2ef6 Fix timeout leak in copy-to-clipboard handler
Rapid clicks on the copy button accumulated setTimeout callbacks that
continued firing after component unmount. Clear the previous timer
before setting a new one, and clean up on unmount.
2026-06-27 15:22:48 +01:00
rcourtman
e7de730c71 feat(alerts): per-group acknowledge button for related alert groups
Adds an 'Ack all (N)' button next to the '+N related' toggle on grouped
alerts. Acknowledges the primary and all related alerts in one action
using the existing bulkAcknowledge API endpoint, without touching
unrelated alerts.
2026-06-27 14:58:00 +01:00
rcourtman
48cf663237 Strengthen evidence and impact guidance in patrol finding tool
The patrol_report_finding tool descriptions for evidence, impact, and
recommendation were too permissive, causing the LLM to frequently omit
them. Future patrol runs will produce better-scaffolded findings.

1. Rewrite tool descriptions to emphasize evidence as a trust anchor
   that should always be included, and impact as expected whenever the
   data supports it.
2. Add 'Authoring Evidence' section to the patrol system prompt with
   concrete examples, matching the existing 'Authoring Impact' section.
3. Add test verifying trust scaffolding guidance is in the system prompt.

Addresses checklist L48-50 (trust scaffolding).
2026-06-27 14:53:48 +01:00
rcourtman
3e477b25d9 feat(alerts): group related alerts by resource hierarchy on overview
Alerts that share a resource ancestor (e.g., ZFS pool + device within
that pool) are now grouped on the alerts overview page. The primary
alert (highest severity, most recent) is shown by default with a
'+N related' toggle that expands to reveal the related alerts.

Grouping uses resourceId nesting: alerts with 3+ path segments are
sub-components and share the group key of their parent (first 2
segments). Alerts on different subsystems remain separate.

Single-alert groups render exactly as before — no visual change when
there's nothing to group.
2026-06-27 14:51:45 +01:00
rcourtman
63d7640a06 Fix missing impact text and incomplete alert type coverage in unified findings
1. Add Impact field to findingView struct — was completely missing from
   the unified findings API response serialization.
2. Expand generateImpact() to cover all ~25 alert types the system
   produces. Previously only 7 had curated impact text.
3. Fix naming mismatches: poweredOff/nodeOffline never matched actual
   alert types powered-off/host-offline.
4. Expand TypeCategoryMap with all missing alert types.
2026-06-27 14:43:09 +01:00
rcourtman
24ce95e2ad fix(alerts): regenerate message on re-evaluation, not just initial fire
Existing alerts kept their stale message from when they first fired.
Now the message is regenerated on every evaluation cycle so label
fixes and wording improvements take effect immediately.
2026-06-27 14:38:42 +01:00
rcourtman
fa5a874986 feat(alerts): clean alert messages + highlight all platform node tables
Backend:
- Map internal resource types to human-readable labels in alert messages
  ('agent-disk disk at 95.8%' -> 'Disk at 95.8%')
- Fix redundant Unraid message ('running check' -> 'check in progress')

Frontend:
- Add alert row highlighting to Docker, Kubernetes, TrueNAS, and vSphere
  host/system tables (same pattern as the Proxmox nodes table fix)
2026-06-27 14:34:33 +01:00
rcourtman
5fc30e2060 fix(alerts): surface node-level alerts on Proxmox node table
getAlertStyles now accepts an optional nodeMatch parameter. Node table
rows pass their node name so storage/topology/disk alerts (which have
a different resourceId than the node) correctly highlight the affected
node row. Previously ZFS pool errors on minipc were invisible on the
overview page.
2026-06-27 12:25:47 +01:00
rcourtman
58d0dd8c29 feat(alerts): click-through from alert card to affected resource
Alert resource names are now clickable links that navigate to the
appropriate platform overview page (Proxmox, Docker, Machines, etc.)
based on the alert's resource type. Lets users jump directly from an
alert to the affected resource for investigation.
2026-06-27 12:21:04 +01:00
rcourtman
18b3342d02 Enhance calm-day Patrol empty state with coverage context
When the latest Patrol run is available, the healthy empty state body
now includes the coverage summary (e.g. 'Checked 42 resources.') as a
prefix, giving the user immediate freshness and coverage context on a
calm day instead of just 'no action needed'. Addresses checklist L51-53.
2026-06-27 12:14:25 +01:00
rcourtman
dcce40706e feat(alerts): severity-first sorting, severity badges, threshold context
- Sort active alerts by severity (critical before warning) then recency
- Add explicit Critical/Warning text badge on each alert card
- Show 'limit: 90%' annotation for threshold-based alerts
- Add critical count breakdown to stats card

Makes alerts more scannable and actionable at a glance.
2026-06-27 12:12:21 +01:00
rcourtman
6c5139c16d feat: surface AI confidence on expanded finding card
The backend UnifiedFinding model and API already return ai_confidence
as a 0-1 float for AI-enhanced findings, but the frontend
UnifiedFinding type in aiIntelligence.ts never mapped it.

Added aiConfidence to the UnifiedFinding interface, mapped it from
the API ai_confidence field in normalizeUnifiedFindingRecord, and
rendered it on the expanded finding card as a compact trust
indicator: 'Pulse confidence: High (87%)' / 'Moderate' / 'Low'.

Only shown when aiConfidence > 0 — patrol findings without AI
enhancement do not show a confidence line.

Addresses checklist L48-50: 'Recommendations expose trust
scaffolding: evidence, confidence/reason, scope or blast radius,
proposed action, approval requirement, verification result, and
attempt history.'
2026-06-27 12:01:58 +01:00
rcourtman
bb7fe8e0ed feat(workloads): availability probe status card in guest drawer
Shows probe status (Up/Down), latency, method, target, last-checked
time, and error details for guests that already have an availability
probe. Sits alongside the suggestion card which only shows when no
probe exists yet.
2026-06-27 11:46:19 +01:00
rcourtman
4072297e4c fix: replace module-level BrainCircuitIcon with inline SVG
BrainCircuitIcon from lucide-solid was used as JSX inside the
module-level GUEST_COLUMNS constant. In SolidJS, component JSX
compiles to createComponent(), which was being called at module
evaluation time — outside any reactive root — triggering the
console warning: 'computations created outside a createRoot or
render will never be disposed'.

Replaced with an inline SVG matching the pattern used by all other
column icons in the same file. Removed the lucide-solid import.
2026-06-27 11:31:50 +01:00
rcourtman
ca299ec729 test(workloads): update GuestRow tests for new Availability column
Update column count (21→22), column ordering assertions, and
tablet/compact visible-column expectations to include the new
'availability' column.
2026-06-27 11:27:46 +01:00
rcourtman
c0411dd0aa feat(workloads): dedicated Availability column in workload table
Availability probe badges (latency, failed, timed out) previously
rendered inline in the Name column. This moves them to a dedicated
'Avail' column right after Name with center-aligned badge layout
(kind: 'badge'), giving users:

- Vertical scannability: latency values align in a clean column
  instead of floating at variable x-positions after guest names
- Conceptual separation: Name answers 'what is this?', Availability
  answers 'is this up?'

Removes AvailabilityProbeCell from both the Name column and the OS
column fallback. The column is always visible (min layout: mobile)
across all view modes (all, vm, system-container, container,
app-container).

Verified via Playwright: 'Avail' header renders after 'Name', 31 cells
populated, jellyfin shows '6ms' in its own column.
2026-06-27 11:10:33 +01:00
rcourtman
1a1184d27f Auto-recover corrupted unified_resources.db instead of looping 500s
When the SQLite resource database is corrupted (malformed disk image),
NewSQLiteResourceStore now backs up the corrupted file to
*.corrupted.<timestamp> and recreates a fresh database. Without this,
every /api/resources request returned 500 with no recovery path
until the admin manually deleted the file.

Resource data is derived from monitor state and repopulated on the
next poll cycle; user-authored metadata (links, notes) in the corrupted
file is preserved in the backup.
2026-06-27 11:08:21 +01:00
rcourtman
24b16fe6c5 Refresh RC7 release packet after install-default fix 2026-06-27 10:24:01 +01:00
rcourtman
55204cde9b Align RC7 Docker install defaults 2026-06-27 10:18:40 +01:00
rcourtman
dd5d6b9ad1 Use product language for Patrol control telemetry disclosure 2026-06-27 09:47:39 +01:00
rcourtman
b8d5fa8828 Disclose Patrol control telemetry proof signals 2026-06-27 09:38:45 +01:00
rcourtman
34c60f0c68 Refresh frontend bundle-size baseline 2026-06-27 09:26:03 +01:00
rcourtman
0a6460e32a Fix discovery disabled-state test expectation
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
2026-06-27 09:16:43 +01:00
rcourtman
da14b88d9f Prepare v6.0.0-rc.7 release candidate 2026-06-27 09:06:02 +01:00
rcourtman
5c2e465cde fix(discovery): show availability probe suggestion card for undiscovered guests
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
hasMeaningfulDiscoveryContext returned false for records without deep
scan data (empty service_type, no facts, no ports), which prevented the
GuestDrawerOverview from rendering the availability probe suggestion
card — even when suggested_availability_probe was populated.

Add hasSuggestedProbe to the meaningful context check so the suggestion
card renders for guests that have a backfilled probe suggestion but
haven't been deep-scanned yet.

Verified via Playwright: bazarr (no existing probe, no deep scan data)
now shows the suggestion card with HTTP :6767 at 192.168.0.78.
2026-06-27 00:08:32 +01:00
rcourtman
bd0220ca58 Clear alerts activation config on org switch
The alertsActivation store (config, activation state, active alerts,
error) was not reset on org switch. Stale config from the previous
org (thresholds, activation state) persisted until the next page
navigation triggered refreshConfig.
2026-06-27 00:04:51 +01:00
rcourtman
3fa0a316e2 Immediately refresh patrol findings after org switch
The patrol polling effect did not depend on activeOrgID, so it
continued using the old interval without immediately loading new
org data. With the intelligence state now cleared on org switch,
this caused up to 30s of empty findings until the next poll cycle.
2026-06-27 00:03:48 +01:00
rcourtman
eebe0b5f74 Clear AI intelligence state on org switch to prevent data bleed
The aiIntelligence store (findings, patrol findings, remediation plans,
pending approvals, circuit breaker, correlations) was not reset when
switching organizations. Findings from the previous org would remain
visible until the next polling cycle refreshed them. Added org_switched
event listener that resets all signals and clears the pending approval
expiry timer.
2026-06-27 00:00:10 +01:00
rcourtman
fa3f57e6ba fix(discovery): backfill availability suggestions for existing discoveries
The refreshSuggestedAvailabilityProbeFromState method existed but was
never called, so existing discovery records never received availability
probe suggestions. This adds backfillAvailabilitySuggestions which:

- Triggers from SetReadState (goroutine) and runDiscoveryLoop (ticker)
- Retries with exponential backoff when the state snapshot is empty,
  waiting for the monitor to populate Proxmox data before proceeding
- Matches containers by VMID only (was VMID+Node, which failed in
  clusters where the discovery targetID differs from the container's
  node)

Also extends SuggestAvailabilityProbe with a hostname fallback: when
ServiceType is empty (deep scan not yet run), checks the discovery's
Hostname against webServiceDefaults/tcpServiceDefaults. This covers
containers like jellyfin, grafana, frigate, esphome, zigbee2mqtt,
homeassistant, mqtt, etc. in environments where background AI is
disabled.

Adds zigbee2mqtt and ntfy to webServiceDefaults.
2026-06-26 23:56:15 +01:00
rcourtman
3ea61e117d Add per-route error boundary to keep app shell alive on page crash
Previously a single ErrorBoundary wrapped the entire authenticated
section including sidebar and navigation. If any page component threw,
the full-screen fallback replaced everything — the user was stranded
with no way to navigate.

Added RouteErrorBoundary that wraps only the route content inside
AppLayout. On error it renders an inline error card with a Try Again
button while keeping the sidebar, header, and navigation functional.
2026-06-26 23:50:08 +01:00
rcourtman
83f99f1428 Clear pending ACK safety-valve timeouts on WebSocket store disposal
The 15s safety-valve timeout for pending alert acknowledgments was not
cleared in onCleanup. If the store was disposed while an ack was
pending, the timeout would fire after disposal and show a spurious
error toast. Added disposal cleanup for the timeout map and an
isDisposed guard in the callback.
2026-06-26 23:46:06 +01:00
rcourtman
9cf6c98fca Fix leaked alerts activation event listener on unmount
onCleanup referenced handleAlertsActivation (dead code, never
registered) instead of handleAlertsActivationEvent (the actual
listener). removeEventListener was a no-op, leaking the listener
on every component unmount unless shutdown() was called first.
2026-06-26 23:35:53 +01:00
rcourtman
7b2849d16d Fix NaN propagation in alert grouping window and AI duration display
setGroupingWindow stored Number.parseInt result without checking for
NaN, while the adjacent setEscalationAfter properly guards against it.
If an invalid value reached the parser, NaN would propagate into the
grouping config and could cascade into alert delivery issues.

formatDuration in AIModelSelectionSection displayed raw NaN/Infinity/
negative values without guarding. Now returns '-' for non-finite or
negative inputs.
2026-06-26 23:25:56 +01:00
rcourtman
9d2df76b3f Fix physical disk I/O metrics skipped when SMART data is empty (#1487)
writeHostPhysicalDiskIOMetrics gated ALL per-disk I/O metrics on
host.Sensors.SMART being non-empty. When the agent's SMART collection
fails (smartctl not installed, LXC container can't see /sys/block),
DiskIO data from gopsutil is valid but silently discarded.

Fix: remove the SMART requirement. When no SMART match is found for a
DiskIO entry, try matching against Proxmox API physical disks by device
name (via LinkedNodeID), then fall back to hostID:device as the metric
resource ID. This matches the fallback scheme already used by
HostSMARTDiskSourceID and PhysicalDiskMetaMetricID.

Refs #1487
2026-06-26 23:18:41 +01:00
rcourtman
ad3b90e605 fix: ensure availability probes report at least 1ms latency on success
TCP probes to fast local-network services can complete in under 1ms,
causing latency.Milliseconds() to return 0. Combined with omitempty on
the poller status struct, this made some probes show 'Online' with no
latency in the settings panel. Now successful probes always report at
least 1ms.
2026-06-26 22:52:28 +01:00
rcourtman
e09fe06201 Fix unreachable critical escalation for high percentage thresholds
The critical (severity-escalation) threshold was hardcoded as
Trigger + 10 for all metric types. For percentage metrics (cpu,
memory, disk, usage) with high triggers, this made critical
escalation unreachable: a CPU trigger of 95% produced a critical
threshold of 105%, which is impossible for a 0-100% metric.

Fix: add computeCriticalThreshold() helper that caps the critical
threshold at 99 for percentage metrics. Non-percentage metrics
(temperature, diskRead, diskWrite, networkIn, networkOut) keep
the Trigger + 10 offset unchanged.

Applied to all three code paths that derive critical from trigger:
- buildCanonicalMetricSpec (canonical evaluation)
- checkMetric new alert creation (legacy path)
- checkMetric existing alert update (legacy path)
2026-06-26 22:42:02 +01:00
rcourtman
06ea25e5ce Fix webhook private CIDR allowlist lost after monitor reload (#1507)
When a monitor reload was triggered by node auto-registration, the
reloadFunc in server.go recreated the monitor (and its notification
manager) but never re-applied system settings. The new notification
manager started with an empty webhook private CIDR allowlist, causing
webhook notifications to private IPs to fail until the allowlist was
manually re-saved in Settings.

Fix: call router.ReloadSystemSettings() at the end of reloadFunc,
after the new monitor references are set. This re-applies all
persisted system settings — including the webhook CIDR allowlist —
to the freshly created notification manager.

Refs #1507
2026-06-26 22:15:48 +01:00