Some local models (llama.cpp, LM Studio) output internal control tokens
like <|channel|>, <|constrain|>, <|message|> instead of using proper
function calling. These tokens leak into the UI creating a poor UX.
This adds sanitization to strip these control tokens from both streaming
and non-streaming responses before they reach the user.
- Fixed early return in handleAlertResolved that skipped incident recording
when quiet hours suppressed recovery notifications
- Added Host Agent alert delay configuration (backend + UI)
- Host Agents now have dedicated time threshold settings like other resource types
Related to #1179
The UI only showed a bash uninstall command which doesn't work on Windows.
Added PULSE_UNINSTALL env var support to install.ps1 and updated the UI
to display platform-specific uninstall commands for both Linux/macOS and
Windows.
Related to #1176
Two issues fixed:
1. Custom base URL wasn't being passed to the OpenAI client in
createProviderForModel() - requests went to api.openai.com instead
of the configured endpoint (e.g., LM Studio, llama.cpp)
2. Tool schemas were missing the "properties" field when tools had no
parameters. OpenAI API requires "properties" to always be present
as an object, even if empty.
Fixes#1154
This improves the UX for setting up unattended displays by:1. Automatically enabling visual Kiosk mode when a token has monitoring-only scope (unless explicitly disabled).2. Providing a ready-to-use 'Magic Link' (with ?token=...&kiosk=1) upon token creation.
When querying short time ranges (1h, 6h), the metrics store only looked
in TierRaw and TierMinute which were empty in mock mode. The seeded data
was stored in TierHourly and TierDaily.
Updated tierFallbacks to include coarser tiers as fallbacks:
- TierRaw now falls back to TierMinute, then TierHourly
- TierMinute now falls back to TierRaw, then TierHourly
This ensures sparkline data is available in mock/demo mode where
historical data is seeded into coarser tiers.
- Reduce minimum seed duration from 7 days to 1 hour for faster startup
on resource-constrained systems (like demo server 1GB droplet)
- Reduce sleep times from 200ms to 50ms between resource processing
- Add diagnostic logging throughout mock metrics seeding to help debug
issues where sparklines show no data
- Add progress logging for nodes, VMs, containers, storage, docker hosts
- Remove standalone pulse-assistant architecture doc (content lives in CLAUDE.md)
- Add CountdownTimer component for patrol schedule display
- Rewrite patrol handler test to focus on interval persistence
- Extract MockStateProvider to shared test file
The PBS backup snapshot cache only compared BackupCount and LastBackup
timestamp to decide whether to re-fetch. When PBS verify jobs complete,
neither field changes — only the Verification field on individual
snapshots changes — so the cache served stale data indefinitely.
Add a 10-minute TTL per backup group so verification status changes are
picked up periodically. Also add panic recovery to PBS and PVE backup
goroutines, and use runtimeCtx for PBS backup polling to respect
monitor shutdown.
Closes#1174
The "Every" dropdown on the Patrol page was not being respected. Setting
15 min would show "Runs every 6 hours" and the countdown timer was wrong.
Root cause: PatrolSchedulePreset and PatrolIntervalMinutes had omitempty
JSON tags. When the API handler cleared the preset to "", json.Marshal
dropped the field. On reload, NewDefaultAIConfig() re-introduced "6hr"
as the preset, which took priority over the user's custom minutes.
Additional fixes in the same area:
- Track nextScheduledAt explicitly in the patrol loop so next_patrol_at
reflects the actual ticker schedule, not a stale lastPatrol + interval
calculation that diverges when the interval changes mid-cycle.
- Refetch patrol status in the frontend after an interval change so the
countdown timer updates immediately.
- Seed lastPatrol from persisted run history on startup so the header
countdown timer appears immediately after a backend restart.
Refactor Router to allow HTTP client injection for install script proxying. Add tests for unified agent install mechanism and additional metrics store coverage.
The ToastContainer was only rendered inside the authenticated app shell,
making all toast notifications invisible during the first-run setup wizard.
Users clicking "Create Account" with an invalid password saw no feedback
at all — just silent 400 errors in the browser console.
- Move ToastContainer outside the needsAuth conditional so it renders
unconditionally, including during setup
- Add client-side password length validation (>= 12 chars) in SecurityStep
to catch the most common case before hitting the server
- Fix WelcomeStep to check response.ok after bootstrap token validation
so the wizard won't advance with an invalid token
Related to #1173
The quiet hours fix (07b4765b) added ShouldSuppressResolvedNotification()
to handleAlertResolved, which acquires m.mu.RLock(). Five clear*OfflineAlert
functions call the resolved callback synchronously while holding m.mu.Lock().
Go's RWMutex is not reentrant, so this deadlocks permanently when any
node/PBS/PMG/storage/guest comes back online after being offline.
The deadlock prevents recovery notifications from being sent and freezes
the monitoring goroutine, cascading to block all subsequent polling.
Fix: change the five affected functions to fire the resolved callback
asynchronously (matching the pattern already used by clearAlertNoLock),
so it runs after m.mu is released.
Related to #1068
- Move all SQLite pragmas from db.Exec() to DSN parameters so every
connection the pool creates gets busy_timeout and other settings.
Previously only the first connection had these applied.
- Set MaxOpenConns(1) on audit, RBAC, and notification databases
(metrics already had this). Fixes potential for multiple connections
where new ones lack busy_timeout.
- Increase busy_timeout from 5s to 30s across all databases to
tolerate disk I/O pressure during backup windows.
- Fix nested query deadlocks in GetRoles(), GetUserAssignments(), and
CancelByAlertIDs() that would deadlock with MaxOpenConns(1).
- Fix circuit breaker retryInterval not resetting on recovery, which
caused the next trip to start at 5-minute backoff instead of 5s.
Related to #1156
Two bugs prevented per-volume host disk thresholds from working:
1. The override parsing effect in Alerts.tsx had no handler for
host:*/disk:* override keys, so they were silently dropped when
reloading config — making overrides appear to not save.
2. The frontend sanitized mountpoints with underscores while the
backend used hyphens, so even if parsing worked, the backend
alert evaluator would never match the saved keys.
Also adds migration to normalize any orphaned underscore-style keys
on config load, and includes hostDisk in the disabled-state change
detection.
Related to #1103
The Global Defaults card in VMs & Containers rendered number inputs for
Backup and Snapshot columns. These wrote to guestDefaults which has no
backup/snapshot fields, so values were silently lost on save — appearing
to "reset to 0." Filter these special toggle columns out of the Global
Defaults card since backup/snapshot thresholds are configured in the
dedicated Backups/Snapshots sections.
Also fix saveEdit not preserving backup/snapshot in the raw override
config (hysteresisThresholds), which caused per-resource backup overrides
to be silently dropped when editing other thresholds on the same resource.
Related to #1126
sendMessage now returns a boolean so handleSubmit only clears the
finding context when the request actually succeeded. Failed sends
preserve the findingId for retries.
- Sync UserNote, AcknowledgedAt, SnoozedUntil, DismissedReason, Suppressed,
and TimesRaised from ai.Finding to unified store in both callback and
startup sync paths. Mirror note writes to unified store immediately.
- Dim acknowledged findings (opacity-60), add "Acknowledged" badge, hide
acknowledge button once acknowledged, sort below unacknowledged in
severity mode.
- Pass finding_id through frontend chat API → backend ChatRequest →
ExecuteRequest. Look up full finding from unified store (mutex-guarded)
and prepend structured context to the prompt.
In single-node setups, guest alerts had Instance == Node, causing
reevaluateActiveAlertsLocked to evaluate them against NodeDefaults
instead of GuestDefaults. Setting guest memory threshold to 0 (disabled)
wouldn't clear existing guest alerts because they were being kept alive
by the still-enabled node memory threshold.
- Add resourceID colon check to distinguish guest IDs (instance:node:vmid)
from node IDs (instance-node) in reevaluateActiveAlertsLocked
- Clear stale alerts in checkMetric when threshold is nil or disabled
- Skip hysteresis validation for disabled thresholds (Trigger <= 0)
- Fix frontend tooltip: "0" not "-1" disables a threshold
- Add comprehensive tests for DiscoveryMCPAdapter in internal/ai/tools/discovery_adapter_test.go
- Validate strict delegation to DiscoverySource and data transformation
- Add unit tests for internal/ai/eval package
- Validate configuration, retry logic, and custom SSE parsing
- Enables coverage for eval framework without requiring live Pulse server
- Add comprehensive tests for discovery_handlers.go (~75% coverage)
- Add tests for chat_service_adapter.go (previously 0% coverage)
- Fix missing API key issues in chat adapter tests by using ollama model configuration
- Updates eslint to v9.20.0 to resolve Dependabot alert #50
- Migrates config to flat format (eslint.config.js)
- Updates typescript-eslint and eslint-plugin-solid
- Fixes lint error in UnifiedBackups.tsx
Instead of hardcoding PLAIN auth or switching on provider name, query
the server's EHLO response for advertised AUTH mechanisms and pick the
best one (PLAIN preferred, LOGIN as fallback). This properly handles
Microsoft 365 which only advertises LOGIN, and any future server with
non-standard auth support.
Also adds TLS safety check to LOGIN auth (matching PlainAuth behavior)
and moves auth negotiation into each send method so it happens after
the connection and STARTTLS upgrade, when capabilities are accurate.
Microsoft 365 advertises AUTH LOGIN but not AUTH PLAIN, causing
"504 5.7.4 Unrecognized authentication type" for users with valid
credentials. Add a loginAuth implementation and use it automatically
when the Microsoft 365 / Outlook provider is selected.