1002 Commits

Author SHA1 Message Date
rcourtman 2420c2affb feat: Commands disabled by default, require --enable-commands to opt-in
BREAKING CHANGE: AI command execution on agents is now disabled by default.
Users who want AI auto-fix must explicitly enable it with --enable-commands
flag or PULSE_ENABLE_COMMANDS=true environment variable.

Changes:
- Add --enable-commands flag (opt-in for command execution)
- Commands disabled by default for security (defense-in-depth)
- --disable-commands is now deprecated (logs warning, no longer needed)
- PULSE_DISABLE_COMMANDS deprecated in favor of PULSE_ENABLE_COMMANDS
- Update installer script to use --enable-commands
- Backwards compatibility: PULSE_DISABLE_COMMANDS=false still enables commands

This addresses community feedback about secure defaults for arbitrary
command execution on production infrastructure.

Related to #889
2025-12-24 17:36:44 +00:00
rcourtman a9078e96d1 fix: apply duplicate hostname fix to HandleAddNode (manual UI)
Extended Issue #891 fix to cover manual node addition via the UI:

1. HandleAddNode now checks for duplicates by Host URL (not name)
2. Disambiguator applied to PVE, PBS, and PMG node creation
3. Error message updated: 'host URL already exists' instead of 'name already exists'

This ensures the fix works whether nodes are added via:
- Agent auto-registration ✓
- Manual UI setup ✓

All node creation paths now consistently:
- Match by Host URL only
- Disambiguate duplicate hostnames with IP: 'px1' → 'px1 (10.0.2.224)'
2025-12-24 16:17:37 +00:00
rcourtman 44b25b43ac fix: handle DHCP IP changes without creating duplicates
Follow-up to #891 fix - also match by name+tokenID to handle the case
where the same physical host gets a new IP (DHCP). This ensures:

1. Same hostname + DIFFERENT token = different physical hosts → create separate nodes
2. Same hostname + SAME token = same host with new IP → update existing node

Also updates the host URL when an existing node is matched, so IP changes
are properly reflected in the saved configuration.
2025-12-24 16:09:22 +00:00
rcourtman 92988ae0e6 fix: allow duplicate hostnames for different Proxmox hosts. Related to #891
PROBLEM:
When two Proxmox hosts have the same hostname (e.g., 'px1' on different networks),
the auto-registration was matching by name and overwriting the first with the second.
This has been a recurring issue (#104) with at least 3 prior fix attempts.

ROOT CAUSE:
The auto-register handler matched existing nodes by BOTH Host URL and Name.
Matching by name is incorrect - different physical hosts can share hostnames.

FIXES:
1. Remove name-based matching in auto-registration - match by Host URL only
2. Add disambiguateNodeName() to append IP when duplicate hostnames exist
3. Add regression tests to prevent this from breaking again

Now when registering two hosts named 'px1':
- First becomes: px1
- Second becomes: px1 (10.0.2.224)
Both are stored as separate nodes with their own credentials.
2025-12-24 16:05:07 +00:00
rcourtman e86998ec58 fix: AI Patrol only runs when AI is enabled. Related to #885
Users who haven't enabled AI were seeing AI patrol findings from
heuristic analysis that they couldn't dismiss (license-gated).

- IsPatrolEnabled() now checks if Enabled is true
- IsAlertTriggeredAnalysisEnabled() also checks Enabled
- Updated tests to reflect new behavior

AI patrol and alert-triggered analysis require AI to be enabled
as a master switch. This prevents confusing UX where users see
AI features without having configured them.
2025-12-24 16:05:07 +00:00
rcourtman e5c7cf7d1e fix: AI request timeout setting not persisting. Related to #884
Added request_timeout_seconds field to:
- AISettingsResponse struct (for GET responses)
- AISettingsUpdateRequest struct (for PUT requests)
- HandleUpdateAISettings handler logic (validation + persistence)
- HandleGetAISettings response builder

The frontend was already sending request_timeout_seconds but the
backend was ignoring it. Now the setting persists correctly.
2025-12-24 16:05:07 +00:00
rcourtman 85382a79e9 feat: Add configurable AI request timeout for slow hardware. Related to #880
Adds RequestTimeoutSeconds to AI config (default 300s / 5 min).
Users with low-power hardware running Ollama can increase this
value in Settings to prevent timeouts on slower inference.
2025-12-24 10:52:11 +00:00
rcourtman f1853e354b fix: license persistence survives Docker container recreation
Docker users were losing their license activation on every update because
/etc/machine-id changes when the container is recreated.

Changes:
- Store persistent encryption key in /data/.license-key (survives container updates)
- Fall back to machine-id for backwards compatibility with existing installations
- Existing users only need to re-enter their key once, then it persists forever

Fixes issue reported by customer upgrading from RC to v5.0.0.
2025-12-24 10:32:26 +00:00
rcourtman e0dc6695fc fix: Per-node TLS fingerprints for cluster peers (TOFU)
When a PVE cluster has unique self-signed certificates on each node, Pulse
would mark secondary nodes as unhealthy because only the primary node's
fingerprint was used for all connections.

Now, during cluster discovery, Pulse captures each node's TLS fingerprint
and uses it when connecting to that specific node. This enables
"Trust On First Use" (TOFU) for clusters with unique per-node certs.

Changes:
- Add Fingerprint field to ClusterEndpoint config
- Add FetchFingerprint() to tlsutil for capturing node certs
- validateNodeAPI() now captures and returns fingerprints during discovery
- NewClusterClient() accepts endpointFingerprints map for per-node certs
- All client creation paths use per-endpoint fingerprints when available

Related to #879
2025-12-24 10:05:03 +00:00
rcourtman 980f7e7517 fix(ai): prevent false-positive backup findings from alert-triggered analysis
Alert-triggered AI analysis was passing nil for lastBackup when analyzing
guests, causing 'Never backed up' findings even when backup data existed.

- Pass actual LastBackup timestamp from VM/Container state in analyzeGuestFromAlert
- Add regression test to verify backup data is correctly passed through

Fixes false positive 'Never backed up' alerts appearing when CPU/memory alerts fire.
2025-12-23 21:11:35 +00:00
rcourtman 83cb858c15 fix(agentexec): eliminate race conditions in WebSocket message sending
- Move deadline/pong handler setup BEFORE registering agent in map
- Use writeMu mutex consistently for all WebSocket writes
- Prevents race between registration response and ExecuteCommand calls
- Fixes flaky TestExecuteCommand_RoundTripViaWebSocket in CI
2025-12-23 20:05:46 +00:00
rcourtman 1527e962fe fix(demo): allow AI chat in read-only mode
Whitelists /api/ai/execute in the DemoModeMiddleware so users can
interact with the mock AI assistant while keeping the rest of the
system read-only and hardened.
2025-12-23 18:52:13 +00:00
rcourtman d74eae3a3e fix(demo): support patrol analysis mock
Adds structured XML finding responses to the demo mock AI service.
This prevents the background patrol service from failing with 'Analysis failed'
when running in demo mode without a real LLM provider.
2025-12-23 18:48:50 +00:00
rcourtman c9296af4d6 fix(demo): generate realistic spaced patrol history
Updates the demo data generator to create patrol runs spaced 6 hours apart
over the last 3 days, rather than clustering them all in the last hour.
This provides a more authentic viewing experience on the demo dashboard.
2025-12-23 18:42:49 +00:00
rcourtman 03d7147615 fix(ai): force enabled state in demo mode
Ensures the AI settings endpoint reports enabled=true and configured=true
when running in demo mode (PULSE_MOCK_MODE=true), even if no provider is
configured. This unlocks the frontend UI to allow interaction with the
mock AI assistant.
2025-12-23 18:39:34 +00:00
rcourtman ead3e9ec7e feat(ai): add mock chat response for demo mode
Allows the AI Assistant to provide realistic canned responses on the
live demo server without needing a real API key. Handled automatically
when PULSE_MOCK_MODE=true and no provider is configured.
2025-12-23 18:34:38 +00:00
rcourtman cd9b3bfce5 feat(license): grant Pro features in demo/mock mode
When PULSE_MOCK_MODE=true, automatically grant all Pro features
so the demo server can showcase AI Patrol findings without needing
a license. This is specifically for public demo instances.
2025-12-23 18:18:22 +00:00
rcourtman 145403b068 fix: Update tier display name test for Pro Intelligence rename 2025-12-23 10:11:45 +00:00
rcourtman cb3444dd9b fix: Prevent re-migration of deleted env-based API tokens
When a user deletes an API token that was migrated from .env, track
the hash in a suppression list to prevent it from being re-migrated
on the next restart.

Changes:
- Add SuppressedEnvMigrations field to Config
- Add env_token_suppressions.json persistence
- Check suppression list during env token migration
- Record suppressed hash when deleting "Migrated from .env" tokens
- Update RemoveAPIToken to return the removed record

Related to #871
2025-12-23 05:10:47 +00:00
rcourtman 45016430b7 feat(license): rename premium tier to Pro Intelligence 2025-12-23 00:30:55 +00:00
rcourtman 77db408114 feat(ai): add finding validation layer to reduce patrol noise
- Add validateAIFindings() that cross-checks AI findings against actual metrics
- Filter out low-confidence findings (CPU <50%, memory <60%, disk <70%)
- Always allow critical findings, backup issues, and reliability findings through
- Update AI system prompt with stricter thresholds and explicit noise examples
- Add 'before creating a finding' checklist for AI (the 3am test)
- Update AI.md docs with clear value proposition and expectations
- Add comprehensive tests for the validation layer

This ensures paying users get immediate value without noise.
2025-12-22 23:28:09 +00:00
rcourtman b65f42acb5 chore: AI patrol and baseline improvements
- Enhanced patrol finding display in Alerts.tsx
- Improved baseline store with better error handling
- Added clean thinking test coverage
- Updated patrol logic for better finding management
2025-12-22 23:12:11 +00:00
rcourtman c2c352a004 feat: add PBS datastores to unified Storage view (#869)
PBS datastores are now displayed in the Storage overview alongside PVE
storage. Each PBS datastore is converted to a Storage entry with:
- type: 'pbs'
- content: 'backup'
- shared: true
- active: based on PBS instance status

This provides a complete picture of all storage resources in one view
while keeping detailed PBS info in the dedicated PBS section.

Closes #869
2025-12-22 22:12:04 +00:00
rcourtman e4732af0f5 fix: use configured Guest URLs for PVE/PBS/PMG navigation (#870)
- Fix PVE nodes: buildNodeUrl in ProxmoxNodesSection.tsx now prioritizes
  guestURL over host (was ignoring guestURL entirely)
- Add PBS support: GuestURL field added to PBSInstance config, model,
  and API handlers
- Add PMG support: GuestURL field added to PMGInstance config, model,
  and API handlers
- Update NodeSummaryTable to use guestURL for PBS nodes
- Frontend types updated for PBS/PMG guestURL support

The Guest URL setting in node configuration now works correctly across
all node types. When set, it takes priority over the Host URL when
clicking on node names to navigate to the Proxmox/PBS/PMG web UI.

Closes #870
2025-12-22 22:05:25 +00:00
rcourtman c9115efeb8 fix: prevent demo findings from being persisted to disk 2025-12-22 21:45:07 +00:00
rcourtman 5b997b8f2e Add clean separation between mock and real AI findings
- Add IsMockResource() helper to detect mock data resources by naming patterns
- Filter out heuristic findings from mock resources when PULSE_MOCK_MODE is off
- Mock findings still appear when demo mode is enabled
- Ensures users don't see mock data findings in production
2025-12-22 20:23:15 +00:00
rcourtman c04b60c550 Fix demo mode detection to use PULSE_MOCK_MODE env var
The AI demo/mock findings were using a non-existent MOCK_ENABLED
env var. Changed to PULSE_MOCK_MODE which is the actual env var
used by the mock data system.
2025-12-22 20:09:56 +00:00
rcourtman 828b092ea4 Unify AI patrol system: add Kubernetes analysis, enhance Docker/Podman detection
- Add AnalyzeKubernetes to PatrolConfig and enable by default
- Add analyzeKubernetesCluster() for K8s heuristic analysis
  - Detect offline clusters, nodes not ready, CrashLoopBackOff pods
  - Detect failed/pending pods and unavailable deployments
- Add K8s clusters to LLM context in buildInfrastructureSummary()
- Add kubernetes_cluster as valid resource type in AI prompt

- Enhance analyzeDockerHost() for Docker/Podman
  - Podman-aware messaging based on runtime
  - Add unhealthy container detection (health checks)
  - Add exited container with error detection
  - Add high CPU detection (>90%)
  - Add stale host detection (10+ minutes)
  - Tiered severity for restarts (>10 = critical)

- Add 20 new tests for K8s and Docker analysis
2025-12-22 18:03:46 +00:00
rcourtman b75728922c feat: add demo AI findings for mock mode
When MOCK_ENABLED=true, Pulse now injects realistic AI patrol
findings to showcase the AI features without requiring actual
LLM API calls. This enables the demo instance to demonstrate:

- Critical/warning/info findings with realistic content
- Patrol run history
- Actionable recommendations

Also includes refinements to dismissal logic from earlier work:
- Only 'not_an_issue' creates permanent suppression
- 'expected_behavior' and 'will_fix_later' just acknowledge
2025-12-22 17:16:26 +00:00
rcourtman f67e7b3e97 fix: clean up debug logging and fix flaky encryption test
1. Fixed TestNewConfigPersistenceFailsWhenEncryptedDataPresentWithoutKey
   - Test was picking up real encryption key from /etc/pulse during migration
   - Now temporarily moves system key during test for proper isolation
   - Uses t.Cleanup to ensure key is restored even on failure

2. Cleaned up console.log statements in production code
   - Dashboard.tsx: replaced console.log with logger.debug for metadata events
   - CompleteStep.tsx: removed verbose agent detection debug logs

These changes reduce log noise in production while maintaining debug
capability in development mode.
2025-12-22 14:35:48 +00:00
rcourtman 28ac86c8ab fix: reduce WebSocket reconnection log noise in host agent
Addresses #866 - agents were logging 'WebSocket connection failed' warnings
even during normal reconnection scenarios (server restart, network blip, etc).

Changes:
- Normal close errors (1000, 1001, connection reset) now log at Debug level
- Only log Warning after 3+ consecutive failures
- Changed 'Connecting to Pulse' from Info to Debug to reduce noise
- Successful connections still log at Info level

The WebSocket is only used for AI command execution, not metrics, so
transient disconnections don't affect monitoring functionality.
2025-12-22 14:11:23 +00:00
rcourtman 59a4843f20 fix: persist finding dismissal state across restarts
User feedback fields (DismissedReason, UserNote, TimesRaised, Suppressed, Source)
were not being saved to disk, causing 'expected behavior' dismissals to be lost
after Pulse restarted.

- Add missing fields to AIFindingRecord in persistence.go
- Update FindingsPersistenceAdapter to save/load these fields
- Add comprehensive tests for dismissal persistence round-trip

Fixes issue where Frigate storage warning kept reappearing despite being
marked as expected behavior.
2025-12-22 11:18:43 +00:00
rcourtman 0837c46f5a test: Add unit tests for guest tag filtering 2025-12-22 10:24:39 +00:00
rcourtman 71d0401c80 feat: Add guest filtering by tag and name prefix via Alert Configuration. Resolves #863 2025-12-22 10:03:12 +00:00
rcourtman c9fc827f4c fix: Prevent buffering and log actionable error for host agent 403s. Related to discussion #845 2025-12-22 09:51:27 +00:00
rcourtman a0580db62d fix: exclude watch from patrol status summary
Since watch/info findings are filtered from the UI and never shown
to users, don't include them in the patrol run status summary.
This makes the summary consistent with what users actually see.
2025-12-21 23:31:21 +00:00
rcourtman 78c3434061 fix: include VMID in AI context to prevent incorrect references
The LLM was confusing VMIDs because they weren't included in the
context. Now the formatted context shows:

  ### Container: ollama (VMID 200) on minipc

This prevents the AI from referencing the wrong VMID when generating
findings and recommendations.
2025-12-21 23:13:47 +00:00
rcourtman 8ee315eee4 perf: skip initial patrol if one ran recently
When the service restarts, it now checks if a patrol ran within the
last hour. If so, it skips the initial patrol to avoid wasting API
tokens during development/maintenance when the service is restarted
frequently.

The scheduled patrol runs (every 6 hours) are not affected.
2025-12-21 23:03:41 +00:00
rcourtman 9c58bfa127 perf: reduce MetricSamples from 100 to 24 points
100 samples was causing 326k+ input tokens which is expensive.
24 samples (hourly resolution) still provides good pattern visibility
while significantly reducing token cost.

Estimated reduction: ~75% fewer metric tokens.
2025-12-21 22:56:19 +00:00
rcourtman c85814345d feat: surface AI patrol errors as findings
When AI patrol fails due to API issues like insufficient balance, invalid
API key, or rate limiting, we now create a finding that appears in the
AI Insights tab. This makes the issue visible to users rather than hidden
in logs.

The finding includes:
- Clear description of the issue (e.g., 'Insufficient API credits')
- Recommendation for how to fix it
- Evidence showing the actual error message
2025-12-21 22:45:29 +00:00
rcourtman 901f04a7b2 fix: don't show 'All healthy' when patrol run had errors
When a patrol run encounters errors (e.g., LLM call failed), don't
display 'All healthy' in the summary as that's misleading - the
analysis didn't complete properly.

Now shows 'Analysis incomplete (N errors)' instead, which correctly
explains why the status badge shows red/error.
2025-12-21 22:35:39 +00:00
rcourtman c15f260280 feat: increase MetricSamples to 100 points (~15 min resolution)
Modern LLMs have 100k+ token contexts. 100 samples over 24h gives
~15 minute resolution while adding minimal token overhead.

This lets the LLM see fine-grained patterns, short spikes, and
accurately distinguish anomalies from normal behavior.
2025-12-21 22:25:54 +00:00
rcourtman d23f1c78de fix: increase MetricSamples to 24 points for hourly resolution
12 samples was too coarse (2-hour intervals could miss spikes).
24 samples gives ~hourly resolution while still being compact.
2025-12-21 22:24:02 +00:00
rcourtman 5877ce00c3 fix: use 24h window for MetricSamples (matches in-memory retention)
The in-memory MetricsHistory only retains 24 hours of data, not 7 days.
Changed computeGuestMetricSamples to use trendWindow24h instead of
trendWindow7d, and reduced sample count from 24 to 12 points.

This ensures the LLM actually receives metric samples in the context,
which wasn't happening before because the 7-day query returned empty data.
2025-12-21 22:19:40 +00:00
rcourtman f6b1414ed6 debug: add logging to verify MetricSamples population for LLM context 2025-12-21 22:14:54 +00:00
rcourtman 4e893117cd fix: correct patrol interval logging
The log was showing QuickCheckInterval (deprecated, always 0) instead of
the actual Interval field. This caused confusing 'interval: 0' logs.
2025-12-21 21:52:57 +00:00
rcourtman 07c5880b0a fix: AI settings persistence and UI improvements
Bug Fixes:
- Fix boolean fields with 'omitempty' not persisting false values
  - AlertTriggeredAnalysis, PatrolAnalyzeNodes/Guests/Docker/Storage
  - omitempty causes Go to skip false (zero value) when marshaling JSON
  - On reload, NewDefaultAIConfig() sets true, and missing field stays true

- Fix model dropdown losing selection after save (SolidJS reactivity issue)
  - Added explicit 'selected' attribute to option elements
  - Ensures browser maintains selection with optgroups during re-renders

Improvements:
- Change patrol type label from 'Quick' to 'Patrol' in history table
- Add chat_model and patrol_model to AI settings update log
- Add alert_triggered_analysis to AI config load log for debugging
2025-12-21 21:48:09 +00:00
rcourtman 2928fad643 feat(ai): pass raw metric samples to LLM for pattern interpretation
Instead of relying on pre-computed trend heuristics (which can be misleading
for edge cases like step changes vs continuous growth), we now pass downsampled
raw data points to the LLM so it can interpret patterns directly.

Changes:
- Add MetricSamples field to ResourceContext
- Add DownsampleMetrics() to reduce data points for LLM consumption
- Add formatMetricSamples() to format data compactly (e.g., 'Disk: 26→26→31%')
- Add computeGuestMetricSamples() to gather 7-day sampled history
- Populate MetricSamples for VMs and containers during context build
- Add History section to formatted context output

The LLM now sees actual patterns like 'stable for 6 days then jumped' rather
than just '45.8%/day growth rate' - allowing for much more nuanced interpretation.

This approach:
- Leverages LLM's pattern recognition instead of hard-coded heuristics
- Provides 7 days of data (~24 samples) for context on normal behavior
- Uses minimal tokens due to compact formatting with deduplication
- Is more future-proof as LLMs improve

Example output:
  **History (7d sampled, oldest→newest)**: Disk: 26→26→26→26→26→31%

Refs: Frigate disk usage false positive investigation
2025-12-21 21:09:24 +00:00
rcourtman e604e4bb8a Add more AI test coverage
- findings_test.go: Add edge case tests for Acknowledge, Dismiss, SetUserNote, Suppress, Resolve, DeleteSuppressionRule, GetSummary, GetDismissedForContext (+20 tests)
- intelligence_test.go: Add tests for calculateResourceHealth with anomalies/predictions/notes, FormatContext with various subsystems, generateHealthPrediction, GetSummary with patterns/learning (+17 tests)

Coverage improvements:
- internal/ai: 63.1% -> 64.4%
- Overall AI module coverage now averages >80%
2025-12-21 20:31:24 +00:00
rcourtman 185f1ef682 Improve AI test coverage
- baseline/store_test.go: Add tests for CheckResourceAnomalies, formatAnomalyDescription, formatRatio, GetAllAnomalies, floatToStr (67.9% -> 92.2%)
- memory/incidents_test.go: Add tests for RecordAlertUnacknowledged, RecordRunbook, ListIncidentsByResource, FormatForAlert, FormatForResource, FormatForPatrol (66.8% -> 81.1%)
- intelligence_test.go: Add tests for SetStateProvider, FormatGlobalContext, RecordLearning, severityOrder, CheckBaselinesForResource with baselines (61.4% -> 63.1%)
2025-12-21 20:22:47 +00:00