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
The Tailscale system extension on macOS applies an NECP policy that captures
the Pulse process outbound Go TCP connections, routing them through utun4.
RFC 1918 addresses (192.168.0.x) fail with EHOSTUNREACH; Tailscale CGNAT
addresses (100.x.x.x) work normally. exec.Command subprocesses bypass NECP.
Add a subprocessConn type (subprocess_conn.go) that wraps nc stdin/stdout
as a net.Conn with real deadline enforcement (closes the connection on
timeout). Modify DialContextWithCache (dnscache.go) to route RFC 1918
addresses through the subprocess relay on macOS, while Tailscale IPs and
public IPs continue using the normal dialer.
Result: pbs-docker and pi Proxmox/PBS nodes now connect at the transport
layer. Remaining pi poll timeouts are caused by a pre-existing server-side
reverse DNS lookup delay (3s per HTTPS request); fix is NO_RESOLVE=1 in
/etc/default/pveproxy on each Proxmox node.
The resource_changes and action_audits tables grew without bound
because no retention mechanism existed. In production deployments with
frequent monitoring cycles, the database could reach multiple GB within
weeks.
Add a background goroutine that runs every 6 hours and deletes:
- resource_changes older than 30 days
- action_audits older than 90 days
The goroutine starts in NewSQLiteResourceStore and is stopped via the
retentionStop channel in Close(). The first prune runs on the first
ticker interval (6h after startup), not immediately, so test fixtures
with historical timestamps are not affected.
Fixes#1496.
The StandalonePageSurface createEffect that set document.title was being
overridden by AppLayout's competing createEffect, which always set
'Machines' for any standalone path. Consolidated the title resolution
into AppLayout with sub-tab awareness for standalone paths, so
/standalone/availability correctly shows 'Availability checks' and
/standalone/machines shows 'Machines'.
Removed the competing effect from StandalonePageSurface.
The finding.evidence field (data/commands/metrics that support the finding)
was threaded through the Go backend, API types, and tool schema but was
never rendered in the UI. The expanded finding card now shows it between
Impact and Recommended next step, matching the checklist order from the
strong-version record: problem, affected thing, why it matters, what
Pulse checked, recommended next step.
- Add evidence field to UnifiedFinding interface and both normalization
functions in aiIntelligence.ts
- Render Show when={finding.evidence} in FindingsPanel renderExpandedContent
- Add source-level guard test and rendering test with evidence data
Refs protection-posture-attention-queue strong-version checklist L35-36, L48-50.
Newly created alerts arriving via WebSocket can have missing or invalid
startTime values. new Date(undefined).getTime() returns NaN, which fails
the age >= 0 check, so these alerts were excluded from total24h but
still counted in active — creating a visible discrepancy between the
'Triggered (24h)' stat and the 'Acknowledge all (N)' button count.
Treat NaN timestamps as recent (count them in total24h) since an active
alert with no known start time is obviously recent.
The badge now shows '22ms' in green instead of 'HTTP' with a colored
dot. The protocol was an implementation detail the user doesn't need
while scanning a table. The tooltip carries the full detail.
Badge: 22ms (green) / failed (red) / timed out (amber)
Tooltip: 31 ms - checked 3s ago
Availability checks tab under Machines showed 'Machines · Pulse'
in the browser tab instead of 'Availability checks · Pulse'.
The tab-level fallback in AppLayout mapped all standalone routes
to 'Machines', and unlike Alerts/Settings pages, StandalonePageSurface
did not use PageHeader (which overrides document.title with the
specific page name). Add a createEffect to set the document title
from the active sub-tab label.
All TCP and HTTP availability probes were failing with 'no route to host'
because the Pulse process's Go networking stack cannot reach local subnet
IPs when a Tailscale system extension is active. The ICMP probe already
worked because it uses exec.Command('ping'). Apply the same pattern:
- probeTCP: try Go net.Dialer first, fall back to exec.Command('nc')
- probeHTTP: try Go http.Client first, fall back to exec.Command('curl')
- Both fallbacks use OS-appropriate timeout flags (macOS: -G, Linux: -w)
- HTTP probes now skip TLS verification (availability check, not identity)
- Add AvailabilityProbeHTTPS protocol constant and wire it through
Validate, HTTPURL, Normalize, and the probe switch
- Replace bulky TooltipPortal tooltip on AvailabilityProbeCell with a
clean native title attribute ('14 ms - checked 3s ago')
Before: 0/20 probes green (all 'no route to host')
After: 14/20 probes green (6 legitimately down hosts)
Discovery already identifies services and their default ports. Now it
also suggests an availability probe configuration for each discovered
service with a known web interface (webServiceDefaults) or TCP service
(tcpServiceDefaults). The suggestion appears in the resource drawer as a
card with a 'Monitor availability' button. On approval, it calls the
existing POST /api/availability-targets API — one canonical system, no
second management surface.
Backend:
- New SuggestAvailabilityProbe() generates protocol/port/path from the
same webServiceDefaults map used for URL suggestions, with a
tcpServiceDefaults fallback for databases and message brokers
- New AvailabilityProbeSuggestion type on ResourceDiscovery
- Wired into both discovery paths (DiscoverResource + Docker background)
- Cached discoveries get the suggestion via refreshSuggestedAvailabilityProbe
on read, so existing data picks it up without re-discovery
Frontend:
- AvailabilityProbeSuggestionCard in GuestDrawerOverview with one-click
creation via AvailabilityTargetsAPI.create()
- Card hidden when the resource already has an availability facet
- Added 'https' to AvailabilityProbeProtocol type (backend already
supports it)
TestMonitorPollGuestSnapshots_RefreshesStaleCanonicalStoreForClusterGuest
passed context.Background() to pollGuestSnapshots, which creates an
internal 60s snapshot timeout. The backupStorageTimeoutSnapshotClient
mock blocks GetStorage until context cancellation, so the test wasted
60s waiting for the timeout to expire. Pass a 200ms deadline context
so pollGuestSnapshots caps its budget down — test now runs in 0.2s
instead of 60s and no longer times out under -race.
isGuestDrawerVM now returns 'guest is VM' instead of boolean, allowing
TypeScript to narrow the union and safely access VM-only properties.
Removes the as-any cast on diskStatusReason access in
GuestDrawerOverview.
Workload rows now show a compact protocol badge (HTTP/TCP/ICMP) with a
status-colored dot next to the resource name when an availability probe
facet is attached. Badge tone reflects probe result: green for available,
red for failing, amber for degraded. Tooltip shows full probe detail
(method, target, result, check timing, failure count).
Data flows from the unified resource availability facet through
WorkloadGuest.availability into the shared Workloads table, making
probe status visible on any platform page (Proxmox, Docker, etc.).
Per governance: protocol badge in the row identity area, probe detail
in tooltip — no per-row fetch, no duplication across cells.
FindingsPanel: The Investigate button was unreachable dead code.
runtimeState is typed as PatrolRuntimeState (a string union) but the
code accessed .autonomy_level on it. Added a proper autonomyLevel prop
typed as PatrolAutonomyLevel and passed it from
PatrolIntelligenceWorkspace. Tests updated to use the typed prop
instead of as-any casts.
websocket-global: getGlobalWebSocketStore returned T | undefined
because window.__pulseWsStore is optional. Added explicit return type
annotation with non-null assertion. This resolved 15 downstream TS
errors across useResources, useUnifiedResources, and TagBadges.
useWorkloads: Added availability field to local APIResource type to
match existing usage in mapResourceToWorkload.
chooseStatus() had a special case for SourceAvailability that always
returned 'incoming', overriding SourceAgent (priority 3) and Proxmox
(priority 2). When an availability probe reported offline, it overwrote
the Proxmox API's online status on every poll cycle, causing node-level
CPU/Mem/Disk to show — because isOnline() gated metric rendering.
Also fix markStaleLocked() to recompute status from remaining fresh
sources via aggregateStatus() instead of blindly downgrading online to
warning. This ensures correct status when high-priority sources go stale
but lower-priority sources are still fresh.
Browser autofill on the email username/password fields in
EmailProviderSelect fired onInput events on page load, which
called onChange -> setHasUnsavedChanges(true) before any user
interaction.
Two-layer fix:
- autocomplete="off" / "new-password" on the username/password
inputs to discourage browser autofill
- suppressDirtyFlag guard in useAlertsConfigurationState that
ignores dirty-marking calls during initial config load, re-enabled
via queueMicrotask after the load cycle completes
Verified in browser: no false banner on load, real edits still
mark unsaved changes correctly.
The backend broadcasts individual 'alert' WebSocket messages on every
poll cycle for recurring alerts. These are intentionally not processed
by the frontend (handled via state sync), but were logged at warn level,
producing 12+ console warnings per page load. Downgraded to debug.
Also wrapped global WebSocket store creation in createRoot to suppress
SolidJS 'computations created outside createRoot' disposal warning.
The opencode shell wrapper sets HTTPS_PROXY for z.ai API egress, and
child processes inherit it. The proxy rejects local IPs with 403,
breaking Proxmox API calls from the backend. Explicitly clear all
proxy env vars so the backend always talks direct to local infra.
SyncStorageAlertsForInstance matched alert.ResourceID against valid
storage IDs (plain inventory IDs like 'inst-node-local-zfs'). But ZFS
health assessment alerts carry a composite spec resource ID (e.g.
'inst-node-local-zfs/zfs-pool:data') set via params.Spec.ResourceID in
evaluateCanonicalStatefulAlert. Every ZFS alert was immediately cleared
by the sync sweep right after creation, then re-fired on the next poll —
an infinite flapping loop every ~10 seconds.
Fix: when the exact resource ID doesn't match, check whether the prefix
before the first '/' is a valid storage ID. Storage IDs use hyphens
(never slashes), so the prefix cleanly extracts the inventory ID from
the composite spec resource ID.
Added two test cases:
- preserves zfs alerts with composite spec resource ids
- clears zfs alerts when storage is removed (ensures fix doesn't
prevent legitimate cleanup)
The /api/health endpoint gated scheduler health on
DeadLetterCount()==0, causing a 503 whenever any single monitored
node became unreachable. Dead-lettered tasks are the scheduler
working as designed — after repeated failures the task is moved to
the dead-letter queue to prevent infinite retries.
In the live lab this manifested as a persistent 503 on /api/health
because the Pi node (192.168.0.2) times out on every poll cycle,
dead-lettering its task while the other 20+ tasks continue normally.
The scheduler is healthy when the monitor is running. Dead-letter
counts remain available as diagnostic info via the
/api/monitoring/scheduler/health endpoint.
The telemetry Ping struct fields were renamed from 'Patrol mode decision'
to 'Patrol control completed operations loop' etc., and PRIVACY.md was
updated to match. Update the frontend test expectations accordingly.
1. Regenerate pulse-mcp README from canonical manifest (doc drift)
2. Add 31 missing Pulse Intelligence telemetry fields to both PRIVACY.md
copies to match current Ping struct JSON tags
3. Rebuild portal frontend bundle to update build_manifest.json hash
4. Update action execution contract test to match current code structure
(handler wrapped with withExternalAgentCapabilityActivity, error codes
referenced via agentcapabilities constants instead of literal strings)
Full Go test suite now passes clean: 126 packages, 0 failures.
NewRestrictedOutboundHTTPClient validates target IPs via a custom
DialContext, but when an HTTP proxy is configured (HTTP_PROXY env var)
DialContext only validates the proxy's address — the actual target host
is never checked. This allows requests to cloud metadata service
addresses (169.254.169.254) and other blocked IPs through the proxy.
Add a restrictedRoundTripper wrapper that validates the request URL
hostname against resolvePermittedOutboundIP before forwarding. This
provides defense-in-depth that works regardless of proxy configuration,
while the existing DialContext guard continues to prevent DNS rebinding
for direct connections.
The 'Link to resource' field in the availability target form was a
plain text input requiring the user to know and type a resource ID.
Replace it with a FormSelect dropdown that lists all known resources
grouped by platform (Agent, Docker, PVE, PBS, etc.), with resource
type labels in parentheses for context.
Default option remains 'Auto-detect by IP (recommended)'. A fallback
option appears for linked resources not currently in the discovery
list.
Also fixes a pre-existing platformType typo ('proxmox' → 'proxmox-pve')
in the availability probe presentation test.
Agentless availability checks (ICMP/TCP/HTTP) were always minting
standalone network-endpoint resources, leaving them disconnected from
the known Proxmox/Docker guest they actually monitor. This made
availability evidence invisible on the platform resource row where the
user expects it, per the performance-and-scalability bounded-row
contract.
Backend (unified-resources ingest):
- Add LinkedResourceID field to AvailabilityData and AvailabilityTarget
- resolveAvailabilityLink: explicit link first, then exact-IP unambiguous
correlation; skip hostname-only (lossy); guard against overwriting a
different target's facet
- Unlinked/unmatched probes still mint network-endpoint (fallback)
Frontend:
- Relax getAvailabilityProbePresentation for any resource with availability
- Add compact protocol badge to UnifiedResourceHostTableCard name cell
- Add optional 'Link to resource' field to availability target form
- Add linkedResourceId to frontend types
Contracts: api-contracts, unified-resources, monitoring,
performance-and-scalability, storage-recovery.
Governance: coverage_gap + candidate_lane in status.json.
parseProxmoxOnBoot returned nil when the onboot key was absent from a
valid config, but Proxmox's default is onboot=0 (do not start on boot).
This caused stopped VMs without an explicit onboot line (like
windows-runner and tails-anon) to still generate powered-off alerts
despite not being configured to autostart.
Now returns false for absent keys in non-empty configs. nil is reserved
for empty/failed config fetches and unrecognised values, preserving the
'unknown' fallback for genuine uncertainty.
recoveryDatePresentation: locale-independent date assertion (en-GB
systems produce '9 Mar 2026' not 'Mar 9, 2026')
quickstartCopyContract: update 'tools' to 'capabilities' matching
docs/AI.md copy change
AIIntelligence: update stale empty-state text to full current copy
SharedPrimitives.guardrails: align guardrail + registry with
d79640c41 migration of ProxmoxBackupsTable from FilterButtonGroup to
FilterSegmentedControl
Full suite: 659 files, 6719 tests, 0 failures.