The deterministic capacity forecast queried utilization history using the
canonical resource ID (storage-<hash>), but the monitoring layer records
metrics under the metrics-target/source ID (MetricsTarget.ResourceID). For
most storage sources these two IDs diverge, so the forecast query always
found nothing and the feature stayed dormant even for pools with full
ingestion history (Proxmox, TrueNAS, PBS, agent-backed storage).
This splits the two ID roles in the precompute path: a new metricsID field
on patrolStoragePoolRow / patrolPrecomputeStorageSource carries the
metrics-target ID (resolved from SourceID() in the readState branch, or
MetricsTarget.ResourceID in the unified-resource branch) for history
queries, while the canonical id is still used for stamp-matching findings.
Adds a test proving the forecast fires when history exists only under the
metrics-target ID and that the resulting forecast resourceID stays
canonical so StampCapacityForecasts can still match it.
Contract (ai-runtime): the forecast must query history under the
metrics-target ID (StoragePoolView.SourceID()), not the canonical ID; the
forecast's own resourceID stays canonical for stamp-matching.
Refs lane: protection-posture-attention-queue
Capacity forecasts (days-to-full, current usage, trend) were previously
computed deterministically but only fed into LLM prompt text. They never
reached the finding as structured data, so the frontend could not render a
verified urgency signal and the model's speculation was the only thing the
operator saw.
This persists CapacityForecast on Finding (marshal-mirror pair) and stamps
it post-analysis via a service join (StampCapacityForecasts). The forecast
filter now also keeps stable-high (>=80%) pools so the deterministic "no
fill trend" reading wins over model speculation, and fixes isQuiet wrongly
treating stable (-1) trends as filling.
Frontend maps the forecast through the UnifiedFinding view model and renders
a deterministic urgency line (Filling up / Stable / days-to-full · % used)
in the expanded finding detail.
Note: forecasts only populate for resources whose usage is ingested into
metrics history (Proxmox/Ceph storage, nodes, guests). Agent-host storage
(unraid pools) is not yet ingested as a time-series and remains a follow-up
to activate the feature for those pools.
Refs lane: protection-posture-attention-queue
When a Proxmox node has an agent installed, SMART/temperature metrics are
written by the host agent under the agent's disk source ID. But
BuildMetricsTarget preferred the Proxmox source first, and Proxmox API
does not expose detailed SMART data. The mismatch meant the frontend
queried a metrics key that nothing writes, so disk metrics appeared empty.
Move the Agent source check ahead of Proxmox/TrueNAS for
ResourceTypePhysicalDisk, mirroring the existing pattern for
ResourceTypeAgent where agent source already wins over platform sources.
When a disk has a serial number, it is used as the canonical metric ID
regardless of source (PreferredPhysicalDiskMetricID), so the reorder
only affects disks without serials — exactly the case where source
priority determines the key.
Refs #1487
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