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
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
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
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.
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.
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
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).
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'.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
- 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.
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.'
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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)
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