{"owner":"rcourtman","repo":"Pulse","hasSkills":true,"totalSkillsCount":13,"totalTokensCount":20002,"categories":["anthropic-skill","project-spec"],"hasMcp":false,"mcpConfig":null,"found":["tests/integration/evals/tasks/ai-patrol-lifecycle.md","tests/integration/evals/tasks/agent-registration.md","tests/integration/evals/tasks/audit-reporting.md","tests/integration/evals/scenarios.json","ARCHITECTURE.md","docs/API.md","tests/integration/evals/tasks/retired-trial-acquisition.md","tests/integration/evals/tasks/multi-tenant.md","tests/integration/evals/tasks/relay-pairing.md","tests/integration/evals/tasks/cloud-hosting.md","tests/integration/evals/tasks/cloud-billing-lifecycle.md","tests/integration/evals/tasks/saml-sso.md","tests/integration/evals/tasks/truenas-node-add.md"],"skills":{"tests/integration/evals/tasks/ai-patrol-lifecycle.md":"# AI Patrol Finding Lifecycle\n\n## Objective\nValidate the full AI Patrol closed-loop lifecycle: patrol status, findings,\nforce run, run history, autonomy settings, approval queue, investigation,\nfinding acknowledge/resolve, suppression/dismissed endpoints, and AI page UI.\n\n## Steps\n\n### Patrol Status & Configuration\n1. Log in as admin.\n2. GET `/api/ai/patrol/status` — verify response includes `enabled`, `running`, `healthy`, `summary` (with severity counts), `findings_count`, `interval_ms`.\n3. GET `/api/ai/patrol/autonomy` — verify `autonomy_level` is one of `monitor|approval|assisted|full`.\n\n### Patrol Execution\n4. POST `/api/ai/patrol/run` — trigger patrol run (accept 200, 429 rate-limited, or 503 unavailable).\n5. GET `/api/ai/patrol/runs` — verify run history array with `id`, `started_at`, `status`, `resources_checked`.\n\n### Findings & Investigation\n6. GET `/api/ai/patrol/findings` — verify array of findings with `id`, `severity`, `category`, `resource_id`, `title`.\n7. For any finding: GET `/api/ai/findings/{id}/investigation` — verify 200 (investigation exists) or 404 (no investigation yet).\n\n### Finding Lifecycle (Closed-Loop)\n8. POST `/api/ai/patrol/acknowledge` with `{ finding_id }` — verify success.\n9. GET `/api/ai/patrol/findings` — verify finding still present (acknowledged ≠ removed).\n10. POST `/api/ai/patrol/resolve` with `{ finding_id }` — verify success.\n11. GET `/api/ai/patrol/findings` — verify finding is removed from active list.\n\n### Approval Queue\n12. GET `/api/ai/approvals` — verify 200 with `approvals` + `stats`, or 402 paywall.\n\n### Supplementary Endpoints\n13. GET `/api/ai/patrol/suppressions` — verify 200 response.\n14. GET `/api/ai/patrol/dismissed` — verify 200 response.\n\n### UI Verification\n15. Navigate to `/ai` — verify AI page renders with patrol/intelligence content.\n\n## Expected Outcomes\n- Patrol status reflects actual service state.\n- Force patrol triggers or rate-limits correctly.\n- Finding lifecycle completes: acknowledge → resolve → removed.\n- Approval queue responds (200 or 402 paywall).\n- AI page renders meaningful content.\n","tests/integration/evals/tasks/agent-registration.md":"# Agent Registration Journey\n\n## Goal\n\nValidate the agent registration flow end-to-end: an agent sends its first\nreport, Pulse registers the agent, the agent appears in the unified state API, and\nthe agent is visible in the infrastructure UI.\n\n## Steps\n\n1. **Login** — Authenticate as admin via the Pulse UI or API.\n2. **Register agent** — Send a POST to `/api/agents/agent/report` with a\n   synthetic unified agent report payload containing `agent.type = \"unified\"`\n   plus hostname, OS info, CPU, memory, disks, and network interfaces.\n3. **Verify API state** — GET `/api/state` and confirm the new agent appears in\n   the canonical `resources[]` collection as an `agent` resource with correct\n   hostname, platform, and OS metadata.\n4. **Verify metrics** — Confirm the agent's CPU usage and memory metrics are\n   populated on that unified resource entry.\n5. **Send follow-up report** — POST a second report with different CPU/memory\n   values and verify `lastSeen` updates.\n6. **Verify UI** — Navigate to `/infrastructure` and confirm the agent's\n   hostname or display name is visible on the page.\n7. **Delete agent** — DELETE `/api/agents/agent/{agentId}` and verify the agent is\n   removed from `/api/state` `resources[]`.\n\n## Success Criteria\n\n- Agent report returns `{ success: true, agentId: \"...\" }`.\n- Agent appears in `/api/state` `resources[]` with correct metadata.\n- Agent is visible on the infrastructure page in the browser.\n- After deletion, agent no longer appears in state.\n","tests/integration/evals/tasks/audit-reporting.md":"# Audit Log + Reporting Lifecycle\n\n## Objective\nValidate audit logging and reporting enterprise features: audit event\nlisting, export, summary, webhook configuration, notification webhooks,\nsingle/multi-resource report generation, and UI rendering.\n\n## Steps\n\n### Audit Logging\n1. Log in as admin.\n2. GET `/api/audit?limit=10` — verify events list or 402 paywall.\n3. GET `/api/audit/export?format=json` — verify file download (or 501 for no logger).\n4. GET `/api/audit/summary` — verify summary statistics.\n5. GET/POST `/api/admin/webhooks/audit` — CRUD audit webhook URLs.\n6. Navigate to `/settings/security-audit` — verify audit log viewer renders.\n\n### Notification Webhooks (not license-gated)\n7. GET `/api/notifications/webhooks` — list webhooks.\n8. GET `/api/notifications/webhook-templates` — verify templates available.\n9. GET `/api/notifications/webhook-history` — verify history available.\n10. GET `/api/notifications/health` — verify notification system health.\n\n### Reporting\n11. GET `/api/state` — find a resource to report on.\n12. GET `/api/admin/reports/generate?resourceType=node&resourceId=...&format=csv` — generate report.\n13. POST `/api/admin/reports/generate-multi` — generate fleet report.\n14. Navigate to `/operations/reporting` — verify reporting page renders.\n\n## Expected Outcomes\n- Audit events and exports work when `audit_logging` is licensed.\n- Reports generate when `advanced_reporting` is licensed.\n- 402 paywall responses have correct format when not licensed.\n- UI pages render relevant content or upgrade messaging.\n","tests/integration/evals/scenarios.json":"{\n  \"scenarios\": [\n    {\n      \"id\": \"multi-tenant\",\n      \"name\": \"Multi-tenant Isolation\",\n      \"description\": \"Validates org lifecycle, switching, and isolation boundaries.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/03-multi-tenant.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_MULTI_TENANT_ENABLED\": \"true\",\n          \"PULSE_E2E_ENTITLEMENT_PROFILE\": \"multi-tenant\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/multi-tenant.md\"\n      }\n    },\n    {\n      \"id\": \"retired-trial-acquisition\",\n      \"name\": \"Retired Trial Acquisition\",\n      \"description\": \"Ensures self-hosted Pulse no longer initiates hosted Pro trial acquisition from the ordinary runtime: POST /api/license/trial/start returns 404 and preserves local entitlements.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/07-retired-trial-acquisition.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/retired-trial-acquisition.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-hosting\",\n      \"name\": \"Pulse Cloud Hosted Signup\",\n      \"description\": \"Validates public cloud signup, Stripe sandbox checkout completion, and magic-link request behavior.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/08-cloud-hosting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-hosting.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-billing-lifecycle\",\n      \"name\": \"Cloud Billing Lifecycle\",\n      \"description\": \"Validates post-checkout cloud lifecycle transitions (active to canceled) through Stripe + webhook processing.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/09-cloud-billing-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-billing-lifecycle.md\"\n      }\n    },\n    {\n      \"id\": \"truenas-node-add\",\n      \"name\": \"TrueNAS Node Addition\",\n      \"description\": \"Validates TrueNAS connection add, pool/dataset discovery, and unified resource visibility.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/02-node-add-truenas.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_E2E_TRUENAS_INSECURE\": \"true\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/truenas-node-add.md\"\n      }\n    },\n    {\n      \"id\": \"relay-pairing\",\n      \"name\": \"Relay Pairing & Mobile Readiness\",\n      \"description\": \"Validates relay configuration, connection to relay server, QR code generation, and mobile pairing readiness.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/03-relay-pairing.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/relay-pairing.md\"\n      }\n    },\n    {\n      \"id\": \"agent-registration\",\n      \"name\": \"Agent Install & Registration\",\n      \"description\": \"Validates host agent registration via report API, host visibility in unified state, and infrastructure page rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/04-agent-install-registration.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/agent-registration.md\"\n      }\n    },\n    {\n      \"id\": \"saml-sso-login\",\n      \"name\": \"SAML SSO Provider Lifecycle\",\n      \"description\": \"Validates SAML SSO provider CRUD, SP metadata generation, test-connection, and SSO settings UI rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/05-saml-sso-login.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/saml-sso.md\"\n      }\n    },\n    {\n      \"id\": \"audit-log-reporting\",\n      \"name\": \"Audit Log + Reporting Lifecycle\",\n      \"description\": \"Validates audit event list/export/summary, audit webhook CRUD, notification webhooks, report generation, and reporting UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/06-audit-log-reporting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/audit-reporting.md\"\n      }\n    },\n    {\n      \"id\": \"ai-patrol-lifecycle\",\n      \"name\": \"AI Patrol Finding Lifecycle\",\n      \"description\": \"Validates AI Patrol status, findings, force run, autonomy settings, approval queue, investigation, finding acknowledge/resolve lifecycle, and AI page UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/07-ai-patrol-finding-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/ai-patrol-lifecycle.md\"\n      }\n    }\n  ]\n}\n","ARCHITECTURE.md":"# Pulse Architecture\n\nPulse is a real-time infrastructure monitoring platform for **Proxmox VE**,\n**Proxmox Backup Server**, **Proxmox Mail Gateway**, **Docker**, **machine\nagents**, **Kubernetes**, **TrueNAS**, and early-access **VMware vSphere**\nenvironments. It uses a **Go 1.26** backend and a **SolidJS / TypeScript**\nfrontend.\n\n## 🏗 High-Level Overview\n\nThe system runs as a single binary that serves both the API and the embedded frontend assets. It connects to infrastructure via platform-supported HTTP and WebSocket APIs and lightweight push-based agents, normalises everything into a **Unified Resource model**, and delivers real-time updates to clients over WebSocket.\n\n```mermaid\nflowchart TD\n    User[Browser / Mobile] <-->|WebSocket + HTTP| Pulse[Pulse Server]\n    Mobile[Mobile App] <-->|E2E Encrypted| Relay[Relay Server]\n    Relay <-->|WebSocket| Pulse\n\n    subgraph \"Pulse Server (Go)\"\n        API[Decomposed API Router]\n        WS[WebSocket Hub]\n        Monitor[Reloadable Monitor]\n        UR[Unified Resource Registry]\n        AI[AI Subsystem]\n        Config[Config Manager]\n        License[Entitlements Engine]\n        Audit[Audit Logger]\n    end\n\n    Pulse -->|HTTPS :8006| PVE[Proxmox VE]\n    Pulse -->|HTTPS :8007| PBS[Proxmox Backup Server]\n    Pulse -->|HTTPS| PMG[Proxmox Mail Gateway]\n    Pulse -->|HTTPS| TrueNAS[TrueNAS SCALE/CORE]\n    Pulse -->|HTTPS| vSphere[VMware vCenter]\n    DockerAgent[Docker Agent] -->|HTTPS POST| API\n    HostAgent[Host Agent] -->|HTTPS POST| API\n    K8sAgent[Kubernetes Agent] -->|HTTPS POST| API\n\n    Monitor --> UR\n    UR --> WS\n    UR --> API\n```\n\n## 🔌 Backend Architecture (Go)\n\nAll backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is assembled from `cmd/pulse/main.go` which delegates to `pkg/server.Run()`.\n\n### Core Components\n\n1. **Entry Point (`cmd/pulse/main.go` → `pkg/server/server.go`)**\n   - Loads unified configuration via `internal/config.Load()`.\n   - Initialises the RBAC manager (`pkg/auth`), audit logger (`pkg/audit`), and crypto layer (`internal/crypto`).\n   - Creates a `ReloadableMonitor` that manages the lifecycle of all monitoring goroutines.\n   - Starts the HTTP/HTTPS server, WebSocket hub, AI services (Patrol + Chat), and the Relay client.\n   - Supports graceful hot-reload via `SIGHUP` and `.env` file watching.\n\n2. **Unified Resource Registry (`internal/unifiedresources`)**\n   - Central data model that normalises resources from Proxmox, PBS, PMG,\n     Docker, machine agents, Kubernetes, TrueNAS, and VMware providers into a\n     shared `Resource` contract.\n   - **Canonical v6 resource types**: `agent`, `vm`, `system-container`, `app-container`, `docker-host`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `storage`, `pbs`, `pmg`, `ceph`, `physical_disk`.\n   - Identity-matching engine: merges resources across sources using machine IDs, DMI UUIDs, hostnames, IPs, and MAC addresses.\n   - Provides typed **views** (`NodeView`, `K8sClusterView`, etc.) for consumer-specific queries.\n   - Canonical API endpoint: `GET /api/resources`.\n\n3. **Monitoring Engine (`internal/monitoring`)**\n   - **Polymorphic monitors**: Each Proxmox VE/PBS/PMG node runs in its own goroutine, polling via the platform REST API.\n   - **Agent receivers** (`internal/api`): Docker, Host, and Kubernetes agents push metrics via HTTP POST to `/api/agents/{type}/report`.\n   - **TrueNAS provider** (`internal/truenas`): Uses the versioned JSON-RPC 2.0\n     WebSocket API on supported SCALE releases for system info, pools,\n     datasets, disks, alerts, ZFS snapshots, and replication tasks. A\n     version-gated REST path remains only for legacy SCALE and CORE systems.\n   - Enterprise/internal multi-org aware: when `PULSE_MULTI_TENANT_ENABLED=true`, each organisation gets a separate shared-process monitor namespace with its own configuration.\n\n4. **WebSocket Hub (`internal/websocket`)**\n   - Manages active browser connections with per-message compression (deflate).\n   - Broadcasts state diffs to all subscribed clients; supports per-organization broadcasts for internal multi-org setups.\n   - Enforces origin validation, organization-level authorization, and Enterprise/internal multi-org license gating.\n\n5. **Decomposed API Router (`internal/api`)**\n   - The router is split into focused registration files for maintainability:\n     - `router_routes_auth_security.go` — Auth, OIDC, SAML, SSO, security tokens, recovery, agent install scripts.\n     - `router_routes_monitoring.go` — Unified resources, metrics history, charts, recovery points, alerts, notifications, discovery.\n     - `router_routes_ai_relay.go` — AI settings, Patrol, Intelligence, Chat sessions, Relay config, and the approval workflow.\n     - `router_routes_org_license.go` — Organisations, RBAC, audit logs, license/entitlements, billing.\n     - `router_routes_registration.go` — Config CRUD, TrueNAS connections, update management, agent profiles, system settings.\n     - `router_routes_hosted.go` — Hosted/SaaS-specific signup and org admin routes.\n   - Every endpoint enforces **scoped access control** (e.g., `monitoring:read`, `settings:write`, `ai:execute`, `ai:chat`).\n\n6. **Relay Client (`internal/relay`)**\n   - Maintains a persistent WebSocket tunnel to a managed relay server for **mobile remote access**.\n   - Uses an ECDH key-exchange for per-channel **end-to-end encryption**.\n   - Multiplexes multiple mobile sessions over a single connection with back-pressure (data limiter) and per-channel authentication.\n   - Supports push notifications through the relay.\n   - Gated by the `relay` license feature.\n\n7. **AI Subsystem (`internal/ai`)**\n   - **AI runtime (`internal/ai/chat`)**: The governed tool loop behind Patrol investigations and Pulse Mobile chat (the in-app chat window has been retired). Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. External agents reach the same tools through `pulse-mcp`.\n   - **Pulse Patrol**: Scheduled background analysis that produces findings, predictions, and remediation plans. Autonomy levels: `monitor`, `approval`, `assisted`, `full`.\n   - **Intelligence Services**: Patterns, correlations, anomalies, baselines, forecasts, and incident recording. All surfaced via `/api/ai/intelligence/*`.\n   - **Safety gates**: Command execution disabled by default (`--enable-commands` opt-in); circuit breakers and scoped permissions at every layer.\n\n8. **Entitlements & Licensing (`pkg/licensing`)**\n   - Capability-key based gating: `ai_autofix`, `rbac`, `multi_tenant`, `relay`, `agent_profiles`, `kubernetes_ai`, `ai_alerts`, etc.\n   - Core tiers include **Community** (free), **Relay**, **Pro**, a reserved\n     hosted **Cloud** capability tier, request-assisted **MSP**, and\n     Enterprise/custom entitlements. The Cloud service is not generally\n     available.\n   - Activation, grant refresh, renewal, expiry, and legacy-license migration.\n     Active state is exposed through `/api/license/*`.\n\n9. **Provider-hosted MSP control plane (`internal/cloudcp`)**\n   - A Stripe-free provider control plane can run one isolated Pulse runtime/container per client workspace.\n   - A signed MSP license sets the provider plan and client workspace cap.\n   - Pulse Account handles the provider workspace roster, sign-in-once handoff, client-bound agent install paths, and provider access surfaces.\n   - Each client runtime remains a normal Pulse instance for monitoring data, alerts, webhooks, reports, users, and audit history. Shared-process organizations are for Enterprise/internal multi-org, not the default MSP boundary.\n\n10. **Recovery Engine (`internal/recovery`)**\n   - Aggregates backup data (PBS snapshots, ZFS snapshots, replication tasks) into a unified recovery point timeline.\n   - Provides faceted queries: filter by type, source, status, and source health.\n   - API: `/api/recovery/points`, `/api/recovery/series`, `/api/recovery/facets`, `/api/recovery/rollups`.\n\n11. **Audit Logging (`pkg/audit`)**\n    - Defence-in-depth: every mutation is logged to SQLite, optionally signed with per-tenant encryption keys.\n    - Async audit logging is on by default (`PULSE_AUDIT_ASYNC=false` disables it); see `docs/CONFIGURATION.md`.\n    - Tenant-aware logger manager for isolated per-org audit trails.\n\n### Data Flow\n\n1. **Collection**:\n   - **Proxmox VE / PBS / PMG**: Monitoring engine polls platform REST APIs (configurable interval, default 2 s for PVE).\n   - **Docker / Host / Kubernetes**: Lightweight agents push metrics via HTTP POST on their configured interval.\n   - **TrueNAS**: Provider polls the versioned JSON-RPC WebSocket API on\n     supported SCALE releases, with REST compatibility limited to recognized\n     legacy SCALE and CORE systems.\n2. **Normalisation**: Platform-specific responses are mapped into `unifiedresources.Resource` structs by adapters in `internal/unifiedresources/adapters.go`.\n3. **Registration**: Resources are inserted into the in-memory registry, which handles deduplication, identity matching, and status computation.\n4. **Broadcast**: The latest state snapshot is serialised to JSON and pushed to all connected WebSocket clients by the Hub.\n5. **Persistence**: Metrics history is stored in a SQLite-backed metrics store (`pkg/metrics`) with configurable retention.\n\n## 🎨 Frontend Architecture (SolidJS)\n\nThe frontend is a modern SPA in `frontend-modern/`, built with **SolidJS** and **TypeScript**. It uses fine-grained reactivity (no Virtual DOM) for maximum performance.\n\n### Key Technologies\n- **SolidJS**: Reactive UI framework.\n- **TailwindCSS** with a **semantic design token layer**: All structural colours use CSS custom-property tokens (`bg-base`, `bg-surface`, `text-base-content`, `border-border`, etc.) defined in `index.css`— components never hardcode hex values. Light/dark mode switches automatically via class-based theme resolution.\n- **Vite**: Build tooling (dev server + production bundling).\n- **Lucide Icons**: Icon library (imported as solid components).\n\n### Routing & Navigation\n\nNavigation is organised around platform-shaped pages with cross-platform\noperational surfaces:\n\n| Route | Page | Purpose |\n|---|---|---|\n| `/` and `/infrastructure` | Runtime home | Monitor-first authenticated entry point |\n| `/proxmox/*` | Proxmox | PVE, PBS, PMG, guests, storage, recovery, and Ceph |\n| `/docker/*` | Docker | Hosts, containers, Compose projects, Swarm, images, and storage |\n| `/kubernetes/*` | Kubernetes | Clusters, workloads, networking, storage, and events |\n| `/truenas/*` | TrueNAS | Systems, pools, datasets, disks, apps, VMs, and recovery |\n| `/vmware/*` | vSphere | Early-access vCenter, host, cluster, VM, datastore, and network views |\n| `/standalone/*` | Machines | Agent-backed machines and availability checks |\n| `/alerts/*` | Alerts | Alert rules, active alerts, history |\n| `/actions/*` | Actions | Governed action proposals, approvals, delivery, and audit state |\n| `/patrol/*` | Patrol | Attention queue, findings, investigations, and run history |\n| `/settings/*` | Settings | Infrastructure, security, notifications, plans, and Intelligence |\n\nThe retired aggregate `/workloads`, `/storage`, and `/recovery` top-level\nroutes are not canonical navigation. Shared resource, storage, and recovery\ncontracts remain backend building blocks consumed inside platform pages.\n\n### State Management\n- **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching.\n- **Metrics collector** (`stores/metricsCollector.ts`): Buffers time-series data for sparklines and historical charts.\n- **AI stores** (`stores/aiAvailability.ts`, `stores/aiIntelligence.ts`): Hold whether AI is enabled for the session and the Patrol findings.\n- **License store** (`stores/license.ts`): Tracks plan tier, feature flags, and multi-tenant state.\n- **System settings store** (`stores/systemSettings.ts`): Caches server-side preferences (for example theme and feature toggles).\n\n### Component Design\n- **Shared primitives** in `components/shared/`: `Card`, `Button`, `Toggle`, `FilterButtonGroup`, `Table` — all mapped to the semantic design tokens.\n- **Lazy-loaded pages**: All top-level pages are loaded via `lazy()` with optional preloading after initial render.\n- **Virtual table windowing**: Large resource lists use virtualised rendering for smooth scrolling at scale.\n- **Command Palette** (`Cmd/Ctrl+K`): Quick-access command launcher.\n- **Keyboard shortcuts**: `g p` → Proxmox, `g d` → Docker, `g k` →\n  Kubernetes, `g n` → TrueNAS, `g v` → vSphere, `g s` → Machines, `g a` →\n  Alerts, `g r` → Patrol, `g t` → Settings, `/` → Search.\n\n### Mobile Experience\n- **MobileNavBar** component: Bottom tab bar for touch navigation.\n- **Relay integration**: The mobile app connects through the relay protocol for encrypted remote access.\n\n## 🔒 Security\n\n- **Encryption at Rest**: Sensitive config (passwords, API keys) encrypted with AES-GCM via `internal/crypto`. Each tenant can have its own encryption key.\n- **Scoped API Tokens**: Tokens carry explicit scopes (`monitoring:read`, `settings:write`, `ai:chat`, `ai:execute`, `docker:report`, etc.). Endpoints enforce scope checks before processing.\n- **RBAC**: Role-based access control via `pkg/auth` with file-backed persistence. Resources: `users`, `settings`, `monitoring`.\n- **SSO**: OIDC and SAML providers with per-provider configuration and multi-provider support.\n- **Agent Commands**: Disabled by default for security. Operators must opt in with `--enable-commands`.\n- **Audit Trail**: Every mutation logged to per-tenant SQLite databases with optional cryptographic signatures.\n- **Rate Limiting**: Per-endpoint and per-tenant rate limiting with configurable thresholds.\n- **CSRF Protection**: Token-based CSRF prevention on mutation endpoints.\n- **Recovery Mode**: Localhost-only endpoint for emergency auth recovery with time-limited tokens.\n\n## 🚀 Deployment\n\nPulse is distributed as:\n\n1. **Docker Container**: Multi-stage build producing a minimal image with the Go binary + embedded frontend.\n2. **Single Binary**: The frontend is compiled into the Go binary using `go:embed` (`internal/api/frontend_embed.go`), enabling single-file deployment.\n3. **Kubernetes Helm Chart**: For cluster deployments with configurable replicas and persistence.\n4. **Systemd Service**: For bare-metal or VM installations with `.env`-based configuration and `SIGHUP` reload support.\n5. **Proxmox LXC Helper Script**: One-line install inside a Proxmox helper-scripts container.\n","docs/API.md":"# 🔌 Pulse API Reference\n\nPulse provides a comprehensive REST API for automation and integration.\n\n**Base URL**: `http://<your-pulse-ip>:7655/api`\n\n## 🔐 Authentication\n\nMost API requests require authentication via one of the following methods:\n\n### API Token (Recommended)\n\nCreate an API token in Pulse's **API Access** settings with only the scopes\nneeded for your task. The read-only example below requires `monitoring:read`.\nPass the token in the `X-API-Token` header, using a private header file so the\nsecret does not appear in shell history or process arguments.\n\nOn a trusted machine, as the account running curl, prepare the file and open\nit in an editor. This preserves an existing file, restricts its directory and\nstops before editing if preparation fails or a credential path is symlinked or\nnot a regular file:\n\n```bash\n(\n  set -eu\n  umask 077\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ]; then\n    printf 'Refusing a symlinked credential path.\\n' >&2\n    exit 1\n  fi\n  if [ -e \"$auth_file\" ] && [ ! -f \"$auth_file\" ]; then\n    printf 'Credential file must be a regular file.\\n' >&2\n    exit 1\n  fi\n  mkdir -p \"$auth_dir\"\n  chmod 700 \"$auth_dir\"\n  touch \"$auth_file\"\n  chmod 600 \"$auth_file\"\n  vi \"$auth_file\"\n)\n```\n\nIn the editor, save just this line, replacing `<token>` with the API token:\n\n```text\nX-API-Token: <token>\n```\n\nDefine this helper in the same Bash session (curl 7.76 or later). The examples\non this page use it to make **one request**, print only the HTTP status and save\neach response to a new owner-only file. This keeps resource names, audit details\nand credential-bearing error bodies out of terminal output and recordings.\n\n```bash\npulse_api() (\n  set -eu\n  umask 077\n  if [ \"$#\" -ne 2 ]; then\n    printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\\n' >&2\n    exit 2\n  fi\n  method=$1\n  api_path=$2\n  case \"$api_path\" in\n    /api/*) ;;\n    *) printf 'Use an API path on this Pulse instance, not a full URL.\\n' >&2; exit 2 ;;\n  esac\n  case \"$method\" in\n    GET) set -- ;;\n    POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;\n    PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;\n    DELETE) set -- --request DELETE ;;\n    *) printf 'This example helper accepts only GET, POST, PUT and DELETE.\\n' >&2; exit 2 ;;\n  esac\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ] || [ ! -f \"$auth_file\" ]; then\n    printf 'Prepare a regular, private header file first.\\n' >&2\n    exit 1\n  fi\n  chmod 700 \"$auth_dir\" || exit \"$?\"\n  chmod 600 \"$auth_file\" || exit \"$?\"\n  result_file=$(mktemp \"$auth_dir/api-response.XXXXXX\") || exit \"$?\"\n  curl_exit=0\n  status=$(curl --disable --fail-with-body --silent --show-error --globoff \\\n    --proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \\\n    --header \"@$auth_file\" --output \"$result_file\" --write-out '%{http_code}' \\\n    \"$@\" \"http://127.0.0.1:7655$api_path\") || curl_exit=$?\n  printf 'HTTP %s\\nPrivate response: %s\\n' \"$status\" \"$result_file\"\n  [ \"$curl_exit\" -eq 0 ] || exit \"$curl_exit\"\n  case \"$status\" in\n    2??) ;;\n    *) exit 1 ;;\n  esac\n)\n```\n\nThen make a read-only request with a `monitoring:read` token:\n\n```bash\npulse_api GET /api/state/summary\n```\n\nThe default loopback origin applies only when curl runs on the Pulse host and\nexplicitly bypasses proxy environment settings. For remote access, replace\n`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace\n`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate\nverification enabled; for a private CA, add `--cacert` with a CA file you\nverified separately, never `--insecure`. Do not put credentials in the URL.\n\nOpen the reported response file privately to inspect it, including on failure;\nshare only the relevant redacted error, not the whole response or file. Each\ncall preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit\nand retain the error body; redirects are not followed and are not success.\nThe connection limit is five seconds and the whole request limit is twenty\nseconds. A partial file after a transport error is not a complete result.\nNo request is retried automatically, especially a POST whose outcome is\nuncertain: inspect the existing action and target before deciding what to do.\nA 2xx response is only HTTP success, not proof that an action completed.\n\nThe helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).\nPOST and PUT read their JSON body from standard input; GET and DELETE send no\nbody. A method being supported is not permission to change a resource. Choose\neach administrative change separately and check its current state afterwards.\nA timeout or incomplete response can occur **after a change was applied**;\ndo not repeat a POST, PUT or DELETE merely because the response was lost.\n\nKeep `--globoff`: shell quotes alone do not disable curl's URL globbing. Without\nit, a path containing a brace list such as `{review,ops}` or a range such as\n`[1-3]` can send several requests, including several POST, PUT or DELETE\nrequests. The helper treats these characters literally, not as a batch of\ntargets. Still quote the whole path in the shell, and URL-encode dynamic resource\nIDs and query values; `--globoff` does not encode them for you.\n\nKeep `--disable` first: it ignores local curl configuration that could enable\ntrace output, bypass TLS or follow redirects. On an authentication-enabled\ninstance, the protected summary checks token access; `/api/health` is public\nand does not verify authentication. The helper is local example code, not an\ninstalled Pulse command; define it again in a new shell before using these\nexamples. Dispose of response files under your normal private-data policy.\n\nDo not paste tokens into command lines, URLs or issue reports. Keep the header\nfile private and outside shared repositories and diagnostics; do not use curl\nverbose/trace output when sharing a result. Share only the relevant redacted\nerror, not the whole infrastructure response. Revoke tokens that are no longer\nneeded in Pulse's API Access settings.\n\n### Bearer Token\n\nThe same API token can use a Bearer header instead. Replace the header file's\nline in the editor with the following, then use the same helper above;\ndo not send both authentication headers:\n\n```text\nAuthorization: Bearer <token>\n```\n\n### Session Cookie\n\nFor one-off read-only diagnostics, use your signed-in Pulse browser to open\nthe API path on that same instance. Do not extract or paste its session cookie\ninto a command or report; use the scoped token-file method above for automation.\nState-changing session requests also require Pulse's CSRF protection, which\nthe UI handles.\n\nSession endpoints:\n- `POST /api/login` (sets the session cookie, `pulse_session` over plain HTTP or `__Host-pulse_session` over HTTPS, plus `pulse_csrf`)\n- `POST /api/logout` (clears session)\n\nLogin body:\n```json\n{ \"username\": \"admin\", \"password\": \"secret\", \"rememberMe\": true }\n```\n\nPublic endpoints include:\n- `GET /api/health`\n- `GET /api/version`\n- `GET /api/agent/version` (agent update checks)\n- `GET /api/setup-script` (public; the generated script prompts for its setup token at runtime and skips auto-registration if none is supplied)\n\n## 🔏 Scopes and Admin Access\n\nSome endpoints require admin privileges and/or scopes. Common scopes include:\n- `monitoring:read`\n- `monitoring:write`\n- `settings:read`\n- `settings:write`\n- `agent:config:read`\n- `agent:manage`\n- `actions:plan`, `actions:approve`, `actions:execute`\n- `audit:read`\n\nEndpoints that require admin access are noted below.\n\nAdmin endpoints are instance administration unless they act only on the\nrequest organization. On instance administration an API token must not be\nbound to any organization other than the default one: a token bound to a\ntenant organization gets `403` whatever its scopes, as that organization's\nowner does. Endpoints that serve only the request organization, such as nodes\nand platform connections, agents, install commands, configuration transfer and\nreports, accept tokens bound to that organization with the listed scope.\n\n---\n\n## 📡 Core Endpoints\n\n### System Health\n`GET /api/health`\nCheck if Pulse is running.\n```json\n{\n  \"status\": \"healthy\",\n  \"timestamp\": 1700000000,\n  \"uptime\": 3600,\n  \"devModeSSH\": false\n}\n```\n\n### System State\n`GET /api/state`\nReturns the complete state of your infrastructure (Nodes, VMs, Containers, Storage, Alerts). This is the main endpoint used by the dashboard.\n\n`GET /api/state/summary`\nReturns a lightweight integration summary for external dashboards and checks. Requires `monitoring:read`.\n\n```json\n{\n  \"activeAlerts\": 1,\n  \"nodes\": 2,\n  \"vms\": 8,\n  \"containers\": 12,\n  \"dockerHosts\": [\n    {\n      \"name\": \"Docker Host\",\n      \"containers\": 5,\n      \"uptimeSeconds\": 86400,\n      \"cpuUsagePercent\": 12.5\n    }\n  ],\n  \"verdicts\": {\n    \"ok\": 17,\n    \"attention\": 1,\n    \"critical\": 1,\n    \"stale\": 1,\n    \"off\": 2,\n    \"unknown\": 0\n  },\n  \"attention\": [\n    {\n      \"id\": \"node-1\",\n      \"name\": \"PVE Node 1\",\n      \"type\": \"agent\",\n      \"platformType\": \"proxmox\",\n      \"verdict\": \"critical\",\n      \"topReason\": { \"code\": \"offline\" }\n    }\n  ],\n  \"lastUpdate\": \"2026-05-24T10:11:12Z\"\n}\n```\n\n`verdicts` uses the canonical `ok`, `attention`, `critical`, `stale`, `off`,\nand `unknown` fleet-health vocabulary. `attention` is severity ordered and\ncapped at 30 entries. Powered-off workloads are neutral (`off`), and stale or\nmissing telemetry is never reported as `ok`.\n\n### Simple Stats (HTML)\n`GET /simple-stats`\nLightweight HTML status page for quick checks.\n\n### Unified Resources\n`GET /api/resources`\nReturns the unified resource list with pagination + aggregations. Requires `monitoring:read`.\n\nQuery params:\n- `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `app-container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`)\n- `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`)\n- `excludeSource`: comma-separated list of sources that must not be present on a matching resource\n- `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`)\n- `parent`: parent resource ID\n- `cluster`: cluster name\n- `namespace`: Kubernetes namespace (filters Kubernetes resources only)\n- `q`: name search (contains match)\n- `tags`: comma-separated tags\n- `page`: page number (default `1`)\n- `limit`: page size (default `50`, max `100`)\n- `sort`: `name` (default), `status`, `type`, `lastSeen`\n- `order`: `asc` (default) or `desc`\n\nNote: `GET /api/resources` is optimized for list views. Some large, platform-specific fields may be omitted from the list response and are only returned by `GET /api/resources/{id}`.\n\nEach list resource can include a backend-owned `health` envelope with a\ncanonical `verdict` and stable `reasons` (`code` plus optional compact\n`detail`). Consumers should use this envelope for cross-platform posture\ninstead of deriving health from provider-specific status strings.\n\n#### Guest readings: availability and age\n\nIn legacy VM payloads, `disk.usage: -1` means unavailable, not negative usage.\nIn `GET /api/resources` and `GET /api/resources/{id}`, an unavailable disk\nreading is omitted as `metrics.disk`. **Do not replace an absent metric with\nzero**; a genuinely observed zero is valid. Allocated virtual-disk capacity\nalone does not establish used space inside the guest.\n\nRead `proxmox.diskStatusReason` alongside the value. A `prev-` reason means\nretained disk evidence, not a fresh reading. `agent-not-running` can also\nrepresent a general guest-agent HTTP 500 error: it does not establish that the\nguest's service is absent or stopped. An absent reason alone does not prove\nfreshness either.\n\nFor memory, use `metrics.memory.observation` for the **selected metric's**\nsource and age, not a different source's retained `proxmox.memory` facet:\n\n| Observation `state` | Meaning for the consumer |\n| --- | --- |\n| `current` | Accepted source observation; a normal cache hit can keep the original `observedAt`. Check its age for your use. |\n| `last-known` | Retained evidence, not a new measurement. Keep its original age visible; do not record it as a fresh sample. |\n| `unavailable` | No usable usage observation; do not treat retained numbers or zero-valued fields as a measurement. |\n| Missing or unrecognised | Freshness is unknown, including older payloads without an observation. Do not infer `current`. |\n\n`observedAt` belongs to that source observation. If it is absent or in the\nfuture, its age is unknown; do not substitute the resource's `lastSeen` or\n`updatedAt`. In the raw `proxmox.memory` facet, `usageUnavailable: true` can\npreserve known capacity without measured usage. An absent `metrics.memory`\nis not 0% usage. Conversely, an independently observed PVE or Pulse-agent\nmemory reading can remain current while QEMU disk collection is deferred;\ndisk state is not memory provenance.\n\nA running VM, an advancing row timestamp or an OK backup does not prove fresh\nguest readings or successful thaw. Do not install or restart an agent or send\nlive guest-agent probes merely to fill these gaps, especially during a backup,\nfreeze/thaw or an unresponsive-guest incident. Follow [Backup safety](VM_DISK_MONITORING.md#backup-safety)\nand [missing-reading guidance](VM_DISK_MONITORING.md#a-missing-reading-is-not-an-installation-diagnosis).\nRecovery needs independent thaw, fresh successful writes to every covered\nfilesystem and workload liveness; retained API values cannot establish it.\n\nAvailability is an additive resource facet. `availability` is the compatibility\nsummary used by existing clients; `availabilityChecks` contains every check\nattached to the resource. Each check can include `correlationState`\n(`attached`, `standalone`, `ambiguous`, or `unresolved`), its correlation\nrule/reason/candidate count, and an `evidence` envelope with observation and\nvalidity timestamps. Attached targets also add a `checks` relationship and do\nnot appear as separate `network-endpoint` rows.\n\n`GET /api/resources/stats`\nReturns aggregations (counts + health rollups).\n\n`GET /api/resources/k8s/namespaces?cluster=<clusterName>`\nReturns namespace-level rollups (pods + deployments) for a Kubernetes cluster. Requires `monitoring:read`.\n```json\n{\n  \"cluster\": \"prod-k8s\",\n  \"data\": [\n    {\n      \"namespace\": \"default\",\n      \"pods\": { \"total\": 12, \"online\": 10, \"warning\": 2, \"offline\": 0, \"unknown\": 0 },\n      \"deployments\": { \"total\": 3, \"online\": 3, \"warning\": 0, \"offline\": 0, \"unknown\": 0 }\n    }\n  ]\n}\n```\n\n`GET /api/resources/{id}`\nFetch a single resource by ID.\n\n`GET /api/resources/{id}/children`\nReturns child resources for the parent ID.\n\n`GET /api/resources/{id}/metrics`\nReturns the resource metrics payload.\n\n`POST /api/resources/{id}/link` (`monitoring:write`)\nManually link two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/unlink` (`monitoring:write`)\nManually unlink two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/report-merge` (`monitoring:write`)\nReport an incorrect merge (creates exclusions).\n```json\n{ \"sources\": [\"proxmox\", \"agent\"], \"notes\": \"optional note\" }\n```\n\n### Resource Maintenance and Operator State\n\nUse a timed maintenance window to suppress alerts during planned work on a\nresource. This API, including descendant scope, is available in stable v6.4.1.\nChoose the resource's `id` from `GET /api/resources` and URL-encode it in the\nrequest path. Do not substitute a display name or a Proxmox VM number.\n\n| Endpoint | Required scope | Result |\n| --- | --- | --- |\n| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |\n| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |\n| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |\n\n**PUT replaces the whole record, not just the fields supplied.** Read the current\nstate first and preserve unrelated settings, such as monitoring mode, lifecycle,\nremediation policy, criticality and notes. Treat only `operator_state_not_set`\nas an empty record, and stop on other read errors. Coordinate concurrent writers\nso a read followed by a PUT does not overwrite someone else's changes.\n\nFor a one-time window, merge these fields into that record, replacing the example\ntimes with your intended start and end:\n\n```json\n{\n  \"maintenanceStartAt\": \"2026-10-15T20:00:00Z\",\n  \"maintenanceEndAt\": \"2026-10-15T20:30:00Z\",\n  \"maintenanceScope\": \"resource_and_descendants\",\n  \"maintenanceReason\": \"Proxmox node maintenance\"\n}\n```\n\nThis is a set of fields to merge, not a complete replacement for an existing\nrecord. Remove any `maintenanceRecurrence` when replacing a recurring schedule\nwith this one-time window. The API rejects a record containing both.\n\n| Field | Meaning |\n| --- | --- |\n| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |\n| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |\n| `maintenanceReason` | Optional explanation of the planned work. |\n\nThe response includes `maintenanceWindowActive` and, while active,\n`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the\nrecord's `setAt` and `setBy` fields from the request, ignoring client values.\n\nDuring an active window, matching new alerts and firing/recovery notifications\nare suppressed. Applying an active window also clears matching existing alerts\nfrom the active list, retaining history. This is broader than pausing delivery\nfor an existing incident. Resources outside the scope remain monitored normally.\n\nThe window expires automatically, with no re-enable request needed. Subsequent\nobservations can raise alerts again, subject to normal alert policy and any other\nactive maintenance window. Previously cleared alerts are not automatically\nrestored, and expiry does not replay a backlog of maintenance notifications.\n\nTo end maintenance early, read the latest record, remove its maintenance fields\nand PUT the remaining state back. Do not DELETE the record unless you also intend\nto remove its other operator settings. An inherited window must be changed on\nthe ancestor that owns it.\n\n### Fleet Connections\n`GET /api/connections`\nReturns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.\n\nThe payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.\n\nDefine the private-response helper in [Authentication](#-authentication),\nwith `settings:read` on the token:\n\n```bash\npulse_api GET /api/connections\n```\n\n### Unified Action Planning\n\nThe existing action API separates planning, approval and execution. Run these\nsteps separately, inspecting each response before moving on; do not paste the\nwhole sequence as an unattended recovery script. Use the private header file\nand `pulse_api` helper from [Authentication](#-authentication), not a token in a\ncommand or environment assignment. Inspect each reported private response file\nbefore proceeding. For remote access, change the helper as described there,\nusing your Pulse HTTPS origin with certificate verification enabled.\n\n| Endpoint | Token scope and access |\n| --- | --- |\n| `GET /api/agent/resource-capabilities/{id}` | `monitoring:read`; lists the resource's advertised capabilities and parameter schemas. |\n| `POST /api/actions/plan` | `actions:plan` and permission to plan actions; creates a plan but does not approve or execute it. |\n| `POST /api/actions/{id}/decision` | `actions:approve` and permission to approve actions; records an explicit `approved` or `rejected` decision for a pending action, without executing it. |\n| `POST /api/actions/{id}/execute` | `actions:execute` and permission to execute actions; dispatches only an approved action or an approval-free executable plan. Dry-run-only plans cannot execute. |\n| `GET /api/audit/actions` | `audit:read`, audit-log read permission and licensed audit logging. |\n| `GET /api/audit/actions/{id}/events` | The same audit access; returns the action's lifecycle evidence. |\n\nThe legacy `ai:execute` scope also permits planning, approval and execution,\nbut use the narrower action scopes when possible. Approval and execution tokens\nmust be bound to an authorised user; a scope alone does not bypass approval\npolicy, separation of duties or step-up requirements. Audit reads through a\nbrowser session additionally require admin access. A `monitoring:read` token is\nnot an action-control or audit token.\n\n1. **Inspect capabilities.** Choose the canonical resource `id` from\n   `GET /api/resources`, not a display name or a VM number. Replace `vm:42`\n   throughout these examples and URL-encode it in request paths (`vm%3A42` here).\n   An empty capabilities list means there is nothing to plan for that resource.\n\n```bash\npulse_api GET /api/agent/resource-capabilities/vm%3A42\n```\n\n2. **Plan only.** Use an advertised capability and its actual parameter schema;\n   `restart` and `mode=graceful` are examples, not universal capabilities. Choose\n   a request ID for this specific intent. Pulse derives the actor from the\n   authenticated credential, not a caller-supplied `requestedBy` value.\n\n```bash\npulse_api POST /api/actions/plan <<'JSON'\n{\n  \"requestId\": \"manual-recovery-123\",\n  \"resourceId\": \"vm:42\",\n  \"capabilityName\": \"restart\",\n  \"params\": { \"mode\": \"graceful\" },\n  \"reason\": \"Recover after confirmed outage\"\n}\nJSON\n```\n\nInspect the returned approval policy, blast radius, expiry and preflight checks.\nKeep its `actionId` and reviewed `planHash`; replace `act_...` and `sha256:...`\nbelow with those returned values. Planning is not proof that execution is safe\nor currently available.\n\n3. **Decide only if approval is required.** Use an authorised approver's private\n   header file. Approve only the reviewed plan; stop on a refusal or stale-plan\n   response rather than weakening the policy or creating another recovery action.\n\n```bash\npulse_api POST /api/actions/act_.../decision <<'JSON'\n{\n  \"outcome\": \"approved\",\n  \"reason\": \"Inside maintenance window\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\n4. **Execute separately.** Only after reviewing a successful decision, or an\n   approval-free executable plan, use the executor's authorised header file:\n\n```bash\npulse_api POST /api/actions/act_.../execute <<'JSON'\n{\n  \"reason\": \"Execute approved recovery\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\nIf the response is lost or times out, inspect the existing action and target\nbefore retrying: losing the connection does not prove that execution stopped.\nAn HTTP success is not a substitute for checking the terminal result and the\nresource's observed state.\n\n5. **Read the audit and events**, using a private header file with audit access:\n\n```bash\npulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'\n```\n\n```bash\npulse_api GET /api/audit/actions/act_.../events\n```\n\nWhen audit logging is unavailable, use Pulse's action detail in the signed-in\nUI to inspect the existing action; do not infer success or repeat execution\nfrom an unavailable audit response.\n\nResponse:\n```json\n{\n  \"actionId\": \"act_...\",\n  \"requestId\": \"manual-recovery-123\",\n  \"allowed\": true,\n  \"requiresApproval\": true,\n  \"approvalPolicy\": \"admin\",\n  \"predictedBlastRadius\": [\"vm:42\", \"node-1\"],\n  \"rollbackAvailable\": false,\n  \"message\": \"Plan created for restart on web-42. Execution requires admin approval and is not performed by this endpoint.\",\n  \"plannedAt\": \"2026-05-03T10:00:00Z\",\n  \"expiresAt\": \"2026-05-03T10:05:00Z\",\n  \"resourceVersion\": \"resource:sha256:...\",\n  \"policyVersion\": \"policy:sha256:...\",\n  \"planHash\": \"sha256:...\",\n  \"preflight\": {\n    \"target\": \"vm:42\",\n    \"currentState\": \"web-42 is warning\",\n    \"intendedChange\": \"Restart the VM\",\n    \"dryRunAvailable\": false,\n    \"dryRunSummary\": \"No provider-supported dry run is advertised for this capability.\",\n    \"safetyChecks\": [\n      \"Resource was resolved from the unified resource registry.\",\n      \"Capability is advertised by the resource contract.\",\n      \"This endpoint plans only; it does not approve or execute the action.\",\n      \"Execution requires admin approval.\"\n    ],\n    \"verificationSteps\": [\n      \"Refresh the resource and confirm the expected state after execution.\",\n      \"Review /api/audit/actions/{actionId}/events for lifecycle evidence.\"\n    ],\n    \"generatedAt\": \"2026-05-03T10:00:00Z\"\n  }\n}\n```\n\n### Resource Metadata\nUser notes, tags, and custom URLs for resources.\n\nThese routes do not require admin access. Any authenticated browser session can call them, and API tokens need the scope shown.\n\n- `GET /api/agents/metadata` (`monitoring:read`)\n- `GET /api/agents/metadata/{agentId}` (`monitoring:read`)\n- `PUT /api/agents/metadata/{agentId}` (`monitoring:write`)\n- `DELETE /api/agents/metadata/{agentId}` (`monitoring:write`)\n\n- `GET /api/guests/metadata` (`monitoring:read`)\n- `GET /api/guests/metadata/{guestId}` (`monitoring:read`)\n- `PUT /api/guests/metadata/{guestId}` (`monitoring:write`)\n- `DELETE /api/guests/metadata/{guestId}` (`monitoring:write`)\n\n- `GET /api/docker/metadata` (`monitoring:read`)\n- `GET /api/docker/metadata/{containerId}` (`monitoring:read`)\n- `PUT /api/docker/metadata/{containerId}` (`monitoring:write`)\n- `DELETE /api/docker/metadata/{containerId}` (`monitoring:write`)\n\n- `GET /api/docker/runtimes/metadata` (`monitoring:read`)\n- `GET /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:read`)\n- `PUT /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n- `DELETE /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n\n### Version Info\n`GET /api/version`\nReturns version, build time, and update status.\nExample response:\n```json\n{\n  \"version\": \"6.0.0\",\n  \"buildTime\": \"2026-02-21T00:00:00Z\",\n  \"channel\": \"stable\",\n  \"deploymentType\": \"systemd\",\n  \"updateAvailable\": false,\n  \"latestVersion\": \"6.0.0\"\n}\n```\nVersion fields are returned as plain semantic versions (no leading `v`).\n\n---\n\n## 🖥️ Nodes & Config\n\n### Public Config\n`GET /api/config`\nReturns a small public config payload (update channel, auto-update enabled).\n\n### List Nodes\n`GET /api/config/nodes` (admin, `settings:read`)\n\nThis administrative inventory covers saved **PVE, PBS and PMG connections**,\nnot individual VMs or agents. Keep its response private. Use the returned\nconnection `id` for updates, deletion and tests; a display name, VMID or another\ninstallation's node name is not that ID. Re-read the inventory before a change\nrather than guessing an ID from a list position.\n\n### Add Node\n`POST /api/config/nodes` (admin, `settings:write`)\n\nAdding a connection saves credentials and enables collection; it is not a\npassive validation step. Use an existing dedicated, least-privilege platform\ntoken. The **Pulse API token** in the private header file authorises this\nconfiguration request; the **Proxmox token** in the JSON body authorises reads\nfrom the monitored platform. They are different credentials. Do not use a root\npassword or widen a department-scoped token to make denied peer reads succeed;\nfollow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).\nFor PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)\nand [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can\nalso try to create a token and ACLs on the PBS server; it is not read-only.\n\nPrepare the request in a private editor, not a credential-bearing shell\nassignment, here-document or command argument. Use the same Bash session as\nthe [private-response helper](#api-token-recommended):\n\n```bash\nnode_request_dir=$(umask 077; mktemp -d \"${TMPDIR:-/tmp}/pulse-node.XXXXXX\") &&\nnode_request_file=\"$node_request_dir/node.json\" &&\n(umask 077; set -C; : > \"$node_request_file\") &&\nvi \"$node_request_file\"\n```\n\nIf preparation or the editor fails, stop. In that editor, enter the following\nJSON, replacing the example hostname, connection name, full platform token ID\n(`user@realm!token`) and token-secret placeholder with your intended values:\n\n```json\n{\n  \"type\": \"pve\",\n  \"name\": \"PVE connection A\",\n  \"host\": \"https://pve.example.net:8006\",\n  \"tokenName\": \"pulse-monitor@pve!pulse-readonly\",\n  \"tokenValue\": \"<platform-token-secret>\",\n  \"verifySSL\": true\n}\n```\n\nSet `verifySSL` explicitly to `true`: omission on these new-connection and\ntest request paths does **not** enable certificate verification. The example\nrequires a certificate trusted by the Pulse server for that hostname. For a\nself-signed/private certificate, also supply `fingerprint` with the SHA-256\ncertificate fingerprint verified through an independent trusted channel.\nA fingerprint merely fetched from the endpoint is not established trust.\nDo not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to\nwork around a certificate failure.\n\nAfter reviewing this one intended configuration change, send the file:\n\n```bash\npulse_api POST /api/config/nodes < \"$node_request_file\"\n```\n\nThe helper retains the response privately and makes one bounded request,\nwithout retries. A lost response can follow a successful save: re-read the\nsaved inventory before deciding whether another request is needed. HTTP success\ndoes not establish fresh readings for every node, guest, backup or History view.\nKeep both request and response files out of reports and shared repositories;\nremove the temporary request under your credential policy when no longer needed.\n\n### Test Connection\n`POST /api/config/nodes/test-connection` (admin, `settings:write`)\n\nTests supplied credentials with live platform requests without saving the\nconnection. Use the same private JSON-file transport and explicit TLS settings,\nonly when a live connection test is appropriate; it is not a required precursor\nto filing a report. Success covers the reads performed by that test, not all\ncollection permissions, guest recovery or sustained monitoring.\n\n### Test Node Config (No Save, Live Request)\n`POST /api/config/nodes/test-config` (admin, `settings:write`)\n\nThis also contacts the platform using the supplied credentials; **no save**\ndoes not mean offline schema validation. Do not run either test merely to\ncollect new evidence during a freeze/thaw, backup or unresponsive-host incident.\nUse existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).\n\n### Update Node\n`PUT /api/config/nodes/{id}` (admin, `settings:write`)\n\nReview the current connection and submit only the intended supported changes\nfrom a private JSON file. Do not copy masked credentials from a GET response\nback as new secrets. Credential rotation and trust changes are separate\ndecisions; preserve the existing credential and fingerprint when not changing\nthem. A response failure does not prove the update was unapplied.\n\n### Delete Node\n`DELETE /api/config/nodes/{id}` (admin, `settings:write`)\n\nRemoves the Pulse connection, not the platform workloads or the upstream token.\nCollection and related alert coverage can stop. Check the exact connection and\narrange any needed independent coverage before an intentional removal; do not\ndelete and re-add a connection to diagnose missing readings or identity mixing.\n\n### Test Node (Legacy)\n`POST /api/config/nodes/{id}/test` (admin, `settings:write`)\n\nUses the saved connection for a live test. It is not a passive read of its\nlast result or a guarantee that ordinary collection recovered.\n\nAn optional JSON body can override `verifySSL` and `fingerprint` for this test\nwithout changing the saved connection or credentials. Omitted fields keep their\nsaved values. Send `false` to disable CA verification or `\"\"` to clear the saved\nfingerprint for the test. A nonempty fingerprint remains enforced regardless of\n`verifySSL`. Empty-body requests keep the saved TLS settings. For example,\n`{\"verifySSL\":true,\"fingerprint\":\"<SHA-256 fingerprint>\"}` tests an edited pin.\n\n### Refresh Cluster Nodes\n`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)\n\nActively rediscovers a saved PVE connection's cluster membership. It does not\nselect authorised nodes, grant missing permissions or repair an unrelated\nstandalone installation. Keep intentional token restrictions intact.\n\n### Export Configuration\n`POST /api/config/export` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:read`)\nRequest body:\n```json\n{ \"passphrase\": \"use-a-strong-passphrase\" }\n```\nReturns an encrypted export bundle in `data`. Passphrases must be at least 12 characters.\n\n### Import Configuration\n`POST /api/config/import` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:write`)\nRequest body:\n```json\n{\n  \"data\": \"<exported-bundle>\",\n  \"passphrase\": \"use-a-strong-passphrase\"\n}\n```\n\n---\n\n## 🧭 Setup & Discovery\n\n### Setup Script (Public)\n`GET /api/setup-script`\nReturns the Proxmox/PBS setup script as a shell-script download. Accepts an\noptional legacy `setup_token` query for compatibility. Current downloads\ncontain no token: paste the separately revealed token only at the silent\nterminal prompt, or supply `PULSE_SETUP_TOKEN_FILE` pointing to a mode-0600\nregular file in a mode-0700 directory owned by the script's user. Never put a\ntoken in a copied command or URL. Canonical callers must send a supported `type` of `pve` or\n`pbs` plus non-empty `host` and `pulse_url`; the route no longer generates\nplaceholder-host scripts for later repair or reconstructs Pulse identity from\nthe request origin. The route now shares the same canonical type boundary as\n`/api/setup-script-url`, rejecting unsupported node types instead of treating\nunknown values as PBS, and it normalizes the supplied `host` before script\ngeneration so downloaded artifacts and rerun URLs preserve the same canonical\nnode identity as the bootstrap response. The optional `backup_perms=true`\nquery is supported only for `type=pve`.\n\n### Setup Script URL\n`POST /api/setup-script-url` (admin, `settings:write`)\nGenerates a one-time setup token and URL for `/api/setup-script`.\nCanonical callers must send a supported `type` of `pve` or `pbs` plus a\nnon-empty `host`; the backend normalizes that host before minting the setup\ntoken and now returns the canonical bootstrap identity back in the response as\n`type`, `host`, `url`, `downloadURL`, `scriptFileName`, `command`, `commandWithEnv`,\n`commandWithoutEnv`, `setupToken`, `tokenHint`, and `expires`. The returned\ncommands are canonical root-or-sudo `curl -fsSL` bootstrap commands for the\ngenerated setup script, while `url`, `downloadURL`, and `scriptFileName` are\nthe runtime-owned artifact metadata used by copy and manual download surfaces.\nThe request body is a single canonical JSON object only; unknown fields and trailing JSON are\nrejected as invalid request shape, and `backupPerms:true` is supported only for\n`type:\"pve\"`. This route stays on the normal\nauthenticated bootstrap boundary: when Pulse auth is already configured it\nrequires a real authenticated session or API token, and setup tokens do not\nauthorize the request itself. Pulse-managed Proxmox monitor-token names on the\nsetup/bootstrap path derive from the canonical Pulse endpoint, not request-local\nhost fallbacks, so setup-script and turnkey node-add flows stay on one\ndeterministic `pulse-<canonical-scope-slug>` identity per Pulse instance.\nThe `command`, `commandWithEnv`, and `commandWithoutEnv` fields now contain\nidentical credential-free commands. They download the complete script before\nrunning it and prompt silently in the root-or-sudo process. The token crosses\nthe installer boundary through a private file, not process arguments or an\nexported secret. `downloadURL` equals the tokenless `url`, so a manual download\nalso needs the separately revealed token at runtime. `setupToken` is used for\n`/api/auto-register`; `tokenHint` remains masked on the setup page. Settings\nreveals the token in a separate dialog: run the command first, then copy and\npaste the token only at its prompt. The artifact is reused only for the same\nhost and options while its five-minute expiry is live, and discarded when the\nsetup modal closes.\nNon-frontend consumers must validate the complete artifact, including the\ncanonical host, type, URLs, filename, masked hint and live expiry. Current\nUnified Agents and the shell installer also accept the coherent older-server\nartifact during upgrades, but never execute its command text. For new\nProxmox agent enrolment against a newer server, use its current installer;\nalready enrolled agents keep reporting normally.\n\n### Auto-Register (Public)\n`POST /api/auto-register`\nRegisters a node through the canonical `/api/auto-register` contract using a temporary\nsetup token carried in the JSON `authToken` field. Canonical callers must send\na supported `type` of `pve` or `pbs`, an explicit `source` marker of `agent` or\n`script`, a canonical Pulse-managed `tokenId` in the form\n`pulse-monitor@{pve|pbs}!pulse-<canonical-scope-slug>` matching the requested\ntype, an explicit `serverName`, and missing-token requests now fail with\n`Pulse setup token required`. Incomplete token completion requests now fail\nwith `tokenId and tokenValue must be provided together`, and other missing\ncanonical request fields fail with explicit `Missing required canonical\nauto-register fields: ...` guidance.\nSuccess responses now carry the canonical stored identity and caller boundary\nback to the installer or runtime-side Unified Agent:\n`{\"status\":\"success\",\"action\":\"use_token\",\"type\":\"pve|pbs\",\"source\":\"agent|script\",\"host\":\"https://...\",\"nodeId\":\"<stored-name>\",\"nodeName\":\"<stored-name>\",...}`.\n\n### Agent Install Command\n`POST /api/agent-install-command` (admin, `settings:write`)\nGenerates an API token and install command for agent-based Proxmox setup.\n\n### Discovery\n`GET /api/discover` (admin, `settings:write`)\nReturns the last cached network discovery result from the background discovery service. It does not scan.\n\n`POST /api/discover` (admin, `settings:write`)\nRuns a manual network scan. JSON body: `{\"subnet\": \"...\", \"use_cache\": false}`. `subnet` defaults to `auto`; `use_cache: true` returns the cached result instead of scanning.\n\n### Test Notification\n`POST /api/test-notification` (admin, `settings:write`)\nBroadcasts a WebSocket test event.\n\n---\n\n## 📊 Metrics & Charts\n\n### Chart Data\n`GET /api/charts?range=1h`\nReturns time-series data for CPU, Memory, and Storage.\n**Ranges**: `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `24h`, `7d`\n\n### Storage Charts\n`GET /api/storage-charts`\nReturns storage chart data.\n\n### Storage Stats\n`GET /api/storage/{storageId}` (`monitoring:read`)\nReturns one storage resource by its `id`, wrapped as `{\"data\": {...}, \"timestamp\": 1700000000}`. The `data` object includes `id`, `name`, `node`, `instance`, `type`, `status`, `total`, `used`, `free`, `usage`, `content`, `shared`, `enabled` and `active`. Returns HTTP 404 with code `storage_not_found` for an unknown ID, and HTTP 405 for methods other than GET. A request without an ID (`GET /api/storage/`) returns HTTP 400 with code `missing_storage_id`.\n\n### Recovery (formerly Backups / Snapshots)\nPulse v6 uses the recovery API to provide a platform-agnostic view of backup and snapshot artifacts.\nThe endpoints below carry the provider-neutral contract covering subjects,\npoints, rollups, posture, and filter semantics.\n\n- `GET /api/recovery/points`\n  - Query params:\n    - Core filters: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`\n    - Time window: `from` (RFC3339), `to` (RFC3339)\n    - Paging: `page`, `limit`\n    - Normalized filters: `q`, `cluster`, `node`, `namespace`, `scope=workload`, `verification` (`verified` | `unverified` | `unknown`)\n- `GET /api/recovery/rollups`\n  - Query params: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`, `from` (RFC3339), `to` (RFC3339), `page`, `limit`\n- `GET /api/recovery/postures`\n  - Returns server-derived per-resource protection posture and provider evidence quality.\n  - Query params: repeated `resourceId` values (maximum 200), `state` (`protected` | `attention` | `unprotected` | `unknown`), `page`, `limit` (maximum 200)\n  - Batch clients must make one bounded request per 200 resource ids, never one request per table row.\n  - Unknown identity, permission, history, or collection completeness remains `unknown`; clients must not infer a healthier state from raw backup or snapshot artifacts.\n- `GET /api/recovery/series`\n  - Returns per-day counts for the activity chart.\n  - Query params: same filters as `/api/recovery/points` (except paging), plus `tzOffsetMinutes` (integer; UTC offset minutes for day bucketing)\n- `GET /api/recovery/facets`\n  - Returns distinct filter values (clusters/nodes/namespaces) and capability flags (size/verification/entity id present).\n  - Query params: same filters as `/api/recovery/points` (except paging)\n\n---\n\n## 🔔 Notifications\n\n### Send Test Notification\n`POST /api/notifications/test` (admin, `settings:write`)\nSends one test notification through a single channel per request, not all configured channels. The JSON body `method` (or `type`) selects `email`, `webhook`, or `apprise` and uses the saved configuration. `webhookId` targets one specific saved webhook. `config` tests unsaved email or Apprise settings. If alert delivery is paused, the response includes `deliveryPaused: true`.\n\n### Email, Apprise, and Webhooks\n- `GET /api/notifications/email` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/email` (admin, `settings:write`)\n- `GET /api/notifications/apprise` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/apprise` (admin, `settings:write`)\n- `GET /api/notifications/webhooks` (admin, `settings:read`; `settings:write` also accepted)\n- `POST /api/notifications/webhooks` (admin, `settings:write`)\n- `PUT /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `DELETE /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `POST /api/notifications/webhooks/test` (admin, `settings:write`)\n- `GET /api/notifications/webhook-templates` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/webhook-history` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/email-providers` (admin, `settings:read`; `settings:write` also accepted)\n\nEmail and webhook configurations accept optional `tagFilter` (an array of\nresource-tag strings) and `tagFilterMode` (`\"all\"` or `\"any\"`). An omitted\nfield is preserved on update; an empty `tagFilter` clears routing and restores\ndelivery for all resources.\n\n- `GET /api/notifications/health` (admin, `settings:read`; `settings:write` also accepted)\n  - Queue health is `degraded` whenever any retained `failed` or `dlq`\n    delivery exists, and `unavailable` when queue state cannot be read.\n    Recoverable failed attempts that returned to `pending` for retry do not\n    degrade health.\n  - `queue.attention_required` is the retained terminal-failure count.\n    `reason_codes` identifies failed and/or dead-letter state without exposing\n    notification content. Counts are retention-bounded: sent, failed, and\n    cancelled rows are retained for 7 days; dead-letter rows for 30 days.\n\n### Audit Webhooks (Pro)\n- `GET /api/admin/webhooks/audit` (admin, `settings:read`)\n- `POST /api/admin/webhooks/audit` (admin, `settings:write`)\n  - Body: `{ \"urls\": [\"https://...\"] }`\n  - Strict JSON contract: unknown fields and trailing payload are rejected.\n  - Maximum `20` webhook URLs per update request.\n  - URLs are normalized (trimmed) and duplicate entries are ignored.\n  - Endpoint fails closed if URL validation runtime is unavailable.\n\n### Advanced Reporting (Pro)\n\nReport generation uses `settings:read`. For single-resource and fleet PDF\nnarration, an API token also needs `ai:execute` (or full access). Without it,\nthe report still succeeds with a deterministic summary and no AI provider call;\na single-resource PDF also explains the missing scope. Browser sessions and\nserver-run schedules keep their existing narration behaviour. AI budgets still\napply. GET and CSV never use AI narration.\n\n- `GET /api/admin/reports/catalog` (admin, `settings:read`)\n  - Returns the canonical reporting catalog for the settings surface, including locked-state teaser copy, enabled-surface guidance copy, performance-report options, canonical single-report filename subject, canonical fallback filename date style, and the nested VM inventory export definition.\n  - Metadata route: readable without the `advanced_reporting` feature so locked admin surfaces can render the same reporting definition before upsell.\n- `POST /api/admin/reports/generate` (admin, `settings:read`)\n  - Body fields: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default title for that resource report.\n  - When narration is permitted and Pulse Assistant is configured, the configured AI provider narrates a PDF's executive summary; each narration counts against the AI budget and appears in AI usage. If the call fails, times out or the budget is spent, the PDF falls back to the deterministic summary. CSV output is never narrated. The settings UI uses this method.\n  - API-token callers also need the `ai:execute` scope for AI narration. With only `settings:read` the report is still produced, with the deterministic summary and a note saying why.\n- `GET /api/admin/reports/generate` (admin, `settings:read`)\n  - Takes the same fields as query params and returns the same report, except that it never calls an AI provider: a PDF carries the deterministic summary and a note saying AI narration needs `POST`. Other methods return `405` with `Allow: GET, POST`.\n- `POST /api/admin/reports/generate-multi` (admin, `settings:read`)\n  - Body fields: `resources` (1-50 entries of `{resourceType,resourceId}`), `format`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default fleet report title.\n  - AI narration of the fleet summary follows the same rule as single reports: API-token callers need `ai:execute`, otherwise the deterministic summary is used. CSV output is never narrated.\n- `GET /api/admin/reports/inventory/vms/export` (admin, `settings:read`)\n  - Query params: `format` (`csv` only; optional and defaults to `csv`)\n  - Exports the current fleet-wide VM inventory as spreadsheet-friendly CSV using the canonical runtime model.\n\nValidation and limits:\n- `start` and `end` must be RFC3339 when provided.\n- Malformed `start`/`end` values, `end` not strictly after `start`, or report windows over 366 days return `400 invalid_time_range`.\n- `metricType` must match `[a-zA-Z0-9._:-]+` and be <= 64 chars, otherwise `400 invalid_metric_type`.\n- `title` must be <= 256 chars, otherwise `400 invalid_title`.\n- Report body max size is 1MB; oversized payloads return `400 body_too_large`.\n- Report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.\n- VM inventory export only accepts `csv`.\n\nCommon reporting error codes:\n- `invalid_format`, `missing_params`, `invalid_resource_type`, `invalid_resource_id`\n- `invalid_metric_type`, `invalid_title`, `invalid_time_range`\n- `no_resources`, `too_many_resources`, `body_too_large`, `invalid_body`\n\n### Queue and Dead-Letter Tools\n- `GET /api/notifications/delivery-log?limit=200` (admin, `settings:read`)\n  - Returns recent per-attempt delivery evidence with destination identifiers,\n    alert identifiers, outcomes, timestamps, failure classes, and redacted error\n    text. Completed attempts are retained for 7 days; dead-letter attempts are\n    retained for 30 days, so the response reports both windows explicitly.\n  - `limit` defaults to 50 and is capped at 200.\n- `GET /api/notifications/queue/stats` (admin, `settings:read` and `monitoring:read`)\n  - Returns counts for all rows still retained by the queue. Status counts have\n    different retention windows and are not a delivery rate or lifetime total.\n- `GET /api/notifications/dlq` (admin)\n- `POST /api/notifications/dlq/retry` (admin)\n- `POST /api/notifications/dlq/delete` (admin, `settings:write`; `DELETE` also accepted)\n  - Body: `{ \"id\": \"<notification id>\" }`. Marks that queue row `cancelled`\n    with the note \"Manually deleted from DLQ\"; the row is not removed and is\n    retained for 7 days like other cancelled rows. The endpoint does not check\n    that the row is in the dead-letter queue.\n  - Returns `{ \"success\": true, \"message\": \"Notification deleted from DLQ\",\n    \"id\": \"<notification id>\" }`. An empty or missing `id`, or an invalid JSON\n    body, returns `400`. An unknown `id` returns `500`.\n- `POST /api/notifications/terminal-failures/retry` (admin, `settings:write`)\n  - Returns every retained `failed` or ","tests/integration/evals/tasks/retired-trial-acquisition.md":"# Scenario: Retired Trial Acquisition\n\n## Goal\nProve the self-hosted Pulse instance no longer initiates a hosted Pro trial from the ordinary in-app runtime and does not mutate local entitlements when the retired route is probed.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Capture current entitlements.\n2. Probe `POST /api/license/trial/start`.\n3. Confirm the response is HTTP `404`.\n4. Confirm the response does not contain legacy hosted-signup or trial-rate-limit acquisition payloads.\n5. Verify post-probe entitlements match the pre-probe entitlement summary.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/multi-tenant.md":"# Scenario: Multi-tenant Isolation\n\n## Goal\nProve that multi-tenant controls and isolation behave correctly from the user surface.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Sign in and open organization management.\n2. Create two organizations.\n3. Switch between orgs and confirm org-scoped data does not leak across tenants.\n4. Verify organization CRUD behavior (create/update/delete) works for the active org.\n5. Record any failures, flaky transitions, or unexpected UI states.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/relay-pairing.md":"# Scenario: Relay Pairing & Mobile Readiness\n\n## Goal\n\nProve that relay pairing works end-to-end: configure relay settings, verify connection to the relay server, generate a QR code for mobile pairing, and confirm the onboarding deep link is available.\n\n## Environment\n\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n- Relay Host: provided via `PULSE_E2E_RELAY_HOST`\n\n## Required checks\n\n1. Relay feature is available in license entitlements (`GET /api/license/entitlements` includes `relay` capability).\n2. Relay settings can be configured with the test relay server (`PUT /api/settings/relay` with `enabled: true, server_url: \"...\"` returns 200).\n3. Relay status becomes connected within 60s (`GET /api/settings/relay/status` → `connected: true`).\n4. Onboarding QR code returns required fields (`GET /api/onboarding/qr` → `schema`, `instance_url`, `relay`, `auth_token`, `deep_link`).\n5. Onboarding deep link is available (`GET /api/onboarding/deep-link` → `url` field non-empty).\n6. Relay settings page in the UI shows relay-related content (Relay, Connected, Enabled, etc.).\n\n## Output contract\n\nWrite JSON to `{{result_json}}` with this shape:\n\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/cloud-hosting.md":"# Scenario: Pulse Cloud Hosted Signup\n\n## Goal\nProve hosted cloud signup and magic-link request flows work end-to-end using real Stripe sandbox checkout from the public signup page.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Open `/cloud/signup` and verify form renders.\n2. Submit valid workspace creation details.\n3. Complete Stripe sandbox checkout and verify return to hosted signup completion page.\n4. Request a magic-link email from the same page.\n5. Validate the magic-link request endpoint responds with success contract.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/cloud-billing-lifecycle.md":"# Scenario: Cloud Billing Lifecycle\n\n## Goal\nProve post-checkout billing lifecycle behavior works end-to-end for Pulse Cloud by validating tenant provisioning and cancellation transitions.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Create a real Stripe sandbox checkout from `/cloud/signup`.\n2. Complete checkout with Stripe test card details.\n3. Verify tenant reaches active state in control-plane admin tenant listing.\n4. Trigger subscription cancellation and deliver corresponding webhook event.\n5. Verify tenant transitions to canceled state after webhook processing.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/saml-sso.md":"# SAML SSO Provider Lifecycle\n\n## Objective\nValidate the full SAML SSO provider lifecycle: create a SAML provider,\nverify it appears in the provider list, check SP metadata generation,\ntest-connection validation, enable/disable toggling, and cleanup via delete.\n\n## Steps\n\n1. Log in as admin.\n2. GET `/api/security/sso/providers` — verify the endpoint responds with 200.\n3. POST `/api/security/sso/providers` — create a SAML provider with stub IdP metadata.\n4. GET `/api/security/sso/providers` — verify the new provider appears.\n5. GET `/api/saml/{providerID}/metadata` — verify SP metadata XML is returned.\n6. POST `/api/security/sso/providers/test` — validate the IdP metadata.\n7. Navigate to `/settings/security-sso` — verify the SSO settings page renders.\n8. PUT `/api/security/sso/providers/{id}` — disable the provider, verify disabled.\n9. PUT `/api/security/sso/providers/{id}` — re-enable, verify enabled.\n10. DELETE `/api/security/sso/providers/{id}` — delete and verify removal.\n\n## Expected Outcomes\n- Provider CRUD works end-to-end on the Community SSO capability.\n- SSO settings page renders SSO-related content.\n\n## Environment Variables\n- `PULSE_E2E_SAML_IDP_METADATA_URL` — optional live IdP metadata URL.\n","tests/integration/evals/tasks/truenas-node-add.md":"# Tasks Skill\n\nPath: `tests/integration/evals/tasks/truenas-node-add.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/truenas-node-add.md)"},"files":{"tests/integration/evals/tasks/ai-patrol-lifecycle.md":"# AI Patrol Finding Lifecycle\n\n## Objective\nValidate the full AI Patrol closed-loop lifecycle: patrol status, findings,\nforce run, run history, autonomy settings, approval queue, investigation,\nfinding acknowledge/resolve, suppression/dismissed endpoints, and AI page UI.\n\n## Steps\n\n### Patrol Status & Configuration\n1. Log in as admin.\n2. GET `/api/ai/patrol/status` — verify response includes `enabled`, `running`, `healthy`, `summary` (with severity counts), `findings_count`, `interval_ms`.\n3. GET `/api/ai/patrol/autonomy` — verify `autonomy_level` is one of `monitor|approval|assisted|full`.\n\n### Patrol Execution\n4. POST `/api/ai/patrol/run` — trigger patrol run (accept 200, 429 rate-limited, or 503 unavailable).\n5. GET `/api/ai/patrol/runs` — verify run history array with `id`, `started_at`, `status`, `resources_checked`.\n\n### Findings & Investigation\n6. GET `/api/ai/patrol/findings` — verify array of findings with `id`, `severity`, `category`, `resource_id`, `title`.\n7. For any finding: GET `/api/ai/findings/{id}/investigation` — verify 200 (investigation exists) or 404 (no investigation yet).\n\n### Finding Lifecycle (Closed-Loop)\n8. POST `/api/ai/patrol/acknowledge` with `{ finding_id }` — verify success.\n9. GET `/api/ai/patrol/findings` — verify finding still present (acknowledged ≠ removed).\n10. POST `/api/ai/patrol/resolve` with `{ finding_id }` — verify success.\n11. GET `/api/ai/patrol/findings` — verify finding is removed from active list.\n\n### Approval Queue\n12. GET `/api/ai/approvals` — verify 200 with `approvals` + `stats`, or 402 paywall.\n\n### Supplementary Endpoints\n13. GET `/api/ai/patrol/suppressions` — verify 200 response.\n14. GET `/api/ai/patrol/dismissed` — verify 200 response.\n\n### UI Verification\n15. Navigate to `/ai` — verify AI page renders with patrol/intelligence content.\n\n## Expected Outcomes\n- Patrol status reflects actual service state.\n- Force patrol triggers or rate-limits correctly.\n- Finding lifecycle completes: acknowledge → resolve → removed.\n- Approval queue responds (200 or 402 paywall).\n- AI page renders meaningful content.\n","tests/integration/evals/tasks/agent-registration.md":"# Agent Registration Journey\n\n## Goal\n\nValidate the agent registration flow end-to-end: an agent sends its first\nreport, Pulse registers the agent, the agent appears in the unified state API, and\nthe agent is visible in the infrastructure UI.\n\n## Steps\n\n1. **Login** — Authenticate as admin via the Pulse UI or API.\n2. **Register agent** — Send a POST to `/api/agents/agent/report` with a\n   synthetic unified agent report payload containing `agent.type = \"unified\"`\n   plus hostname, OS info, CPU, memory, disks, and network interfaces.\n3. **Verify API state** — GET `/api/state` and confirm the new agent appears in\n   the canonical `resources[]` collection as an `agent` resource with correct\n   hostname, platform, and OS metadata.\n4. **Verify metrics** — Confirm the agent's CPU usage and memory metrics are\n   populated on that unified resource entry.\n5. **Send follow-up report** — POST a second report with different CPU/memory\n   values and verify `lastSeen` updates.\n6. **Verify UI** — Navigate to `/infrastructure` and confirm the agent's\n   hostname or display name is visible on the page.\n7. **Delete agent** — DELETE `/api/agents/agent/{agentId}` and verify the agent is\n   removed from `/api/state` `resources[]`.\n\n## Success Criteria\n\n- Agent report returns `{ success: true, agentId: \"...\" }`.\n- Agent appears in `/api/state` `resources[]` with correct metadata.\n- Agent is visible on the infrastructure page in the browser.\n- After deletion, agent no longer appears in state.\n","tests/integration/evals/tasks/audit-reporting.md":"# Audit Log + Reporting Lifecycle\n\n## Objective\nValidate audit logging and reporting enterprise features: audit event\nlisting, export, summary, webhook configuration, notification webhooks,\nsingle/multi-resource report generation, and UI rendering.\n\n## Steps\n\n### Audit Logging\n1. Log in as admin.\n2. GET `/api/audit?limit=10` — verify events list or 402 paywall.\n3. GET `/api/audit/export?format=json` — verify file download (or 501 for no logger).\n4. GET `/api/audit/summary` — verify summary statistics.\n5. GET/POST `/api/admin/webhooks/audit` — CRUD audit webhook URLs.\n6. Navigate to `/settings/security-audit` — verify audit log viewer renders.\n\n### Notification Webhooks (not license-gated)\n7. GET `/api/notifications/webhooks` — list webhooks.\n8. GET `/api/notifications/webhook-templates` — verify templates available.\n9. GET `/api/notifications/webhook-history` — verify history available.\n10. GET `/api/notifications/health` — verify notification system health.\n\n### Reporting\n11. GET `/api/state` — find a resource to report on.\n12. GET `/api/admin/reports/generate?resourceType=node&resourceId=...&format=csv` — generate report.\n13. POST `/api/admin/reports/generate-multi` — generate fleet report.\n14. Navigate to `/operations/reporting` — verify reporting page renders.\n\n## Expected Outcomes\n- Audit events and exports work when `audit_logging` is licensed.\n- Reports generate when `advanced_reporting` is licensed.\n- 402 paywall responses have correct format when not licensed.\n- UI pages render relevant content or upgrade messaging.\n","tests/integration/evals/scenarios.json":"{\n  \"scenarios\": [\n    {\n      \"id\": \"multi-tenant\",\n      \"name\": \"Multi-tenant Isolation\",\n      \"description\": \"Validates org lifecycle, switching, and isolation boundaries.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/03-multi-tenant.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_MULTI_TENANT_ENABLED\": \"true\",\n          \"PULSE_E2E_ENTITLEMENT_PROFILE\": \"multi-tenant\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/multi-tenant.md\"\n      }\n    },\n    {\n      \"id\": \"retired-trial-acquisition\",\n      \"name\": \"Retired Trial Acquisition\",\n      \"description\": \"Ensures self-hosted Pulse no longer initiates hosted Pro trial acquisition from the ordinary runtime: POST /api/license/trial/start returns 404 and preserves local entitlements.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/07-retired-trial-acquisition.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/retired-trial-acquisition.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-hosting\",\n      \"name\": \"Pulse Cloud Hosted Signup\",\n      \"description\": \"Validates public cloud signup, Stripe sandbox checkout completion, and magic-link request behavior.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/08-cloud-hosting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-hosting.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-billing-lifecycle\",\n      \"name\": \"Cloud Billing Lifecycle\",\n      \"description\": \"Validates post-checkout cloud lifecycle transitions (active to canceled) through Stripe + webhook processing.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/09-cloud-billing-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-billing-lifecycle.md\"\n      }\n    },\n    {\n      \"id\": \"truenas-node-add\",\n      \"name\": \"TrueNAS Node Addition\",\n      \"description\": \"Validates TrueNAS connection add, pool/dataset discovery, and unified resource visibility.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/02-node-add-truenas.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_E2E_TRUENAS_INSECURE\": \"true\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/truenas-node-add.md\"\n      }\n    },\n    {\n      \"id\": \"relay-pairing\",\n      \"name\": \"Relay Pairing & Mobile Readiness\",\n      \"description\": \"Validates relay configuration, connection to relay server, QR code generation, and mobile pairing readiness.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/03-relay-pairing.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/relay-pairing.md\"\n      }\n    },\n    {\n      \"id\": \"agent-registration\",\n      \"name\": \"Agent Install & Registration\",\n      \"description\": \"Validates host agent registration via report API, host visibility in unified state, and infrastructure page rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/04-agent-install-registration.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/agent-registration.md\"\n      }\n    },\n    {\n      \"id\": \"saml-sso-login\",\n      \"name\": \"SAML SSO Provider Lifecycle\",\n      \"description\": \"Validates SAML SSO provider CRUD, SP metadata generation, test-connection, and SSO settings UI rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/05-saml-sso-login.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/saml-sso.md\"\n      }\n    },\n    {\n      \"id\": \"audit-log-reporting\",\n      \"name\": \"Audit Log + Reporting Lifecycle\",\n      \"description\": \"Validates audit event list/export/summary, audit webhook CRUD, notification webhooks, report generation, and reporting UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/06-audit-log-reporting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/audit-reporting.md\"\n      }\n    },\n    {\n      \"id\": \"ai-patrol-lifecycle\",\n      \"name\": \"AI Patrol Finding Lifecycle\",\n      \"description\": \"Validates AI Patrol status, findings, force run, autonomy settings, approval queue, investigation, finding acknowledge/resolve lifecycle, and AI page UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/07-ai-patrol-finding-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/ai-patrol-lifecycle.md\"\n      }\n    }\n  ]\n}\n","ARCHITECTURE.md":"# Pulse Architecture\n\nPulse is a real-time infrastructure monitoring platform for **Proxmox VE**,\n**Proxmox Backup Server**, **Proxmox Mail Gateway**, **Docker**, **machine\nagents**, **Kubernetes**, **TrueNAS**, and early-access **VMware vSphere**\nenvironments. It uses a **Go 1.26** backend and a **SolidJS / TypeScript**\nfrontend.\n\n## 🏗 High-Level Overview\n\nThe system runs as a single binary that serves both the API and the embedded frontend assets. It connects to infrastructure via platform-supported HTTP and WebSocket APIs and lightweight push-based agents, normalises everything into a **Unified Resource model**, and delivers real-time updates to clients over WebSocket.\n\n```mermaid\nflowchart TD\n    User[Browser / Mobile] <-->|WebSocket + HTTP| Pulse[Pulse Server]\n    Mobile[Mobile App] <-->|E2E Encrypted| Relay[Relay Server]\n    Relay <-->|WebSocket| Pulse\n\n    subgraph \"Pulse Server (Go)\"\n        API[Decomposed API Router]\n        WS[WebSocket Hub]\n        Monitor[Reloadable Monitor]\n        UR[Unified Resource Registry]\n        AI[AI Subsystem]\n        Config[Config Manager]\n        License[Entitlements Engine]\n        Audit[Audit Logger]\n    end\n\n    Pulse -->|HTTPS :8006| PVE[Proxmox VE]\n    Pulse -->|HTTPS :8007| PBS[Proxmox Backup Server]\n    Pulse -->|HTTPS| PMG[Proxmox Mail Gateway]\n    Pulse -->|HTTPS| TrueNAS[TrueNAS SCALE/CORE]\n    Pulse -->|HTTPS| vSphere[VMware vCenter]\n    DockerAgent[Docker Agent] -->|HTTPS POST| API\n    HostAgent[Host Agent] -->|HTTPS POST| API\n    K8sAgent[Kubernetes Agent] -->|HTTPS POST| API\n\n    Monitor --> UR\n    UR --> WS\n    UR --> API\n```\n\n## 🔌 Backend Architecture (Go)\n\nAll backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is assembled from `cmd/pulse/main.go` which delegates to `pkg/server.Run()`.\n\n### Core Components\n\n1. **Entry Point (`cmd/pulse/main.go` → `pkg/server/server.go`)**\n   - Loads unified configuration via `internal/config.Load()`.\n   - Initialises the RBAC manager (`pkg/auth`), audit logger (`pkg/audit`), and crypto layer (`internal/crypto`).\n   - Creates a `ReloadableMonitor` that manages the lifecycle of all monitoring goroutines.\n   - Starts the HTTP/HTTPS server, WebSocket hub, AI services (Patrol + Chat), and the Relay client.\n   - Supports graceful hot-reload via `SIGHUP` and `.env` file watching.\n\n2. **Unified Resource Registry (`internal/unifiedresources`)**\n   - Central data model that normalises resources from Proxmox, PBS, PMG,\n     Docker, machine agents, Kubernetes, TrueNAS, and VMware providers into a\n     shared `Resource` contract.\n   - **Canonical v6 resource types**: `agent`, `vm`, `system-container`, `app-container`, `docker-host`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `storage`, `pbs`, `pmg`, `ceph`, `physical_disk`.\n   - Identity-matching engine: merges resources across sources using machine IDs, DMI UUIDs, hostnames, IPs, and MAC addresses.\n   - Provides typed **views** (`NodeView`, `K8sClusterView`, etc.) for consumer-specific queries.\n   - Canonical API endpoint: `GET /api/resources`.\n\n3. **Monitoring Engine (`internal/monitoring`)**\n   - **Polymorphic monitors**: Each Proxmox VE/PBS/PMG node runs in its own goroutine, polling via the platform REST API.\n   - **Agent receivers** (`internal/api`): Docker, Host, and Kubernetes agents push metrics via HTTP POST to `/api/agents/{type}/report`.\n   - **TrueNAS provider** (`internal/truenas`): Uses the versioned JSON-RPC 2.0\n     WebSocket API on supported SCALE releases for system info, pools,\n     datasets, disks, alerts, ZFS snapshots, and replication tasks. A\n     version-gated REST path remains only for legacy SCALE and CORE systems.\n   - Enterprise/internal multi-org aware: when `PULSE_MULTI_TENANT_ENABLED=true`, each organisation gets a separate shared-process monitor namespace with its own configuration.\n\n4. **WebSocket Hub (`internal/websocket`)**\n   - Manages active browser connections with per-message compression (deflate).\n   - Broadcasts state diffs to all subscribed clients; supports per-organization broadcasts for internal multi-org setups.\n   - Enforces origin validation, organization-level authorization, and Enterprise/internal multi-org license gating.\n\n5. **Decomposed API Router (`internal/api`)**\n   - The router is split into focused registration files for maintainability:\n     - `router_routes_auth_security.go` — Auth, OIDC, SAML, SSO, security tokens, recovery, agent install scripts.\n     - `router_routes_monitoring.go` — Unified resources, metrics history, charts, recovery points, alerts, notifications, discovery.\n     - `router_routes_ai_relay.go` — AI settings, Patrol, Intelligence, Chat sessions, Relay config, and the approval workflow.\n     - `router_routes_org_license.go` — Organisations, RBAC, audit logs, license/entitlements, billing.\n     - `router_routes_registration.go` — Config CRUD, TrueNAS connections, update management, agent profiles, system settings.\n     - `router_routes_hosted.go` — Hosted/SaaS-specific signup and org admin routes.\n   - Every endpoint enforces **scoped access control** (e.g., `monitoring:read`, `settings:write`, `ai:execute`, `ai:chat`).\n\n6. **Relay Client (`internal/relay`)**\n   - Maintains a persistent WebSocket tunnel to a managed relay server for **mobile remote access**.\n   - Uses an ECDH key-exchange for per-channel **end-to-end encryption**.\n   - Multiplexes multiple mobile sessions over a single connection with back-pressure (data limiter) and per-channel authentication.\n   - Supports push notifications through the relay.\n   - Gated by the `relay` license feature.\n\n7. **AI Subsystem (`internal/ai`)**\n   - **AI runtime (`internal/ai/chat`)**: The governed tool loop behind Patrol investigations and Pulse Mobile chat (the in-app chat window has been retired). Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. External agents reach the same tools through `pulse-mcp`.\n   - **Pulse Patrol**: Scheduled background analysis that produces findings, predictions, and remediation plans. Autonomy levels: `monitor`, `approval`, `assisted`, `full`.\n   - **Intelligence Services**: Patterns, correlations, anomalies, baselines, forecasts, and incident recording. All surfaced via `/api/ai/intelligence/*`.\n   - **Safety gates**: Command execution disabled by default (`--enable-commands` opt-in); circuit breakers and scoped permissions at every layer.\n\n8. **Entitlements & Licensing (`pkg/licensing`)**\n   - Capability-key based gating: `ai_autofix`, `rbac`, `multi_tenant`, `relay`, `agent_profiles`, `kubernetes_ai`, `ai_alerts`, etc.\n   - Core tiers include **Community** (free), **Relay**, **Pro**, a reserved\n     hosted **Cloud** capability tier, request-assisted **MSP**, and\n     Enterprise/custom entitlements. The Cloud service is not generally\n     available.\n   - Activation, grant refresh, renewal, expiry, and legacy-license migration.\n     Active state is exposed through `/api/license/*`.\n\n9. **Provider-hosted MSP control plane (`internal/cloudcp`)**\n   - A Stripe-free provider control plane can run one isolated Pulse runtime/container per client workspace.\n   - A signed MSP license sets the provider plan and client workspace cap.\n   - Pulse Account handles the provider workspace roster, sign-in-once handoff, client-bound agent install paths, and provider access surfaces.\n   - Each client runtime remains a normal Pulse instance for monitoring data, alerts, webhooks, reports, users, and audit history. Shared-process organizations are for Enterprise/internal multi-org, not the default MSP boundary.\n\n10. **Recovery Engine (`internal/recovery`)**\n   - Aggregates backup data (PBS snapshots, ZFS snapshots, replication tasks) into a unified recovery point timeline.\n   - Provides faceted queries: filter by type, source, status, and source health.\n   - API: `/api/recovery/points`, `/api/recovery/series`, `/api/recovery/facets`, `/api/recovery/rollups`.\n\n11. **Audit Logging (`pkg/audit`)**\n    - Defence-in-depth: every mutation is logged to SQLite, optionally signed with per-tenant encryption keys.\n    - Async audit logging is on by default (`PULSE_AUDIT_ASYNC=false` disables it); see `docs/CONFIGURATION.md`.\n    - Tenant-aware logger manager for isolated per-org audit trails.\n\n### Data Flow\n\n1. **Collection**:\n   - **Proxmox VE / PBS / PMG**: Monitoring engine polls platform REST APIs (configurable interval, default 2 s for PVE).\n   - **Docker / Host / Kubernetes**: Lightweight agents push metrics via HTTP POST on their configured interval.\n   - **TrueNAS**: Provider polls the versioned JSON-RPC WebSocket API on\n     supported SCALE releases, with REST compatibility limited to recognized\n     legacy SCALE and CORE systems.\n2. **Normalisation**: Platform-specific responses are mapped into `unifiedresources.Resource` structs by adapters in `internal/unifiedresources/adapters.go`.\n3. **Registration**: Resources are inserted into the in-memory registry, which handles deduplication, identity matching, and status computation.\n4. **Broadcast**: The latest state snapshot is serialised to JSON and pushed to all connected WebSocket clients by the Hub.\n5. **Persistence**: Metrics history is stored in a SQLite-backed metrics store (`pkg/metrics`) with configurable retention.\n\n## 🎨 Frontend Architecture (SolidJS)\n\nThe frontend is a modern SPA in `frontend-modern/`, built with **SolidJS** and **TypeScript**. It uses fine-grained reactivity (no Virtual DOM) for maximum performance.\n\n### Key Technologies\n- **SolidJS**: Reactive UI framework.\n- **TailwindCSS** with a **semantic design token layer**: All structural colours use CSS custom-property tokens (`bg-base`, `bg-surface`, `text-base-content`, `border-border`, etc.) defined in `index.css`— components never hardcode hex values. Light/dark mode switches automatically via class-based theme resolution.\n- **Vite**: Build tooling (dev server + production bundling).\n- **Lucide Icons**: Icon library (imported as solid components).\n\n### Routing & Navigation\n\nNavigation is organised around platform-shaped pages with cross-platform\noperational surfaces:\n\n| Route | Page | Purpose |\n|---|---|---|\n| `/` and `/infrastructure` | Runtime home | Monitor-first authenticated entry point |\n| `/proxmox/*` | Proxmox | PVE, PBS, PMG, guests, storage, recovery, and Ceph |\n| `/docker/*` | Docker | Hosts, containers, Compose projects, Swarm, images, and storage |\n| `/kubernetes/*` | Kubernetes | Clusters, workloads, networking, storage, and events |\n| `/truenas/*` | TrueNAS | Systems, pools, datasets, disks, apps, VMs, and recovery |\n| `/vmware/*` | vSphere | Early-access vCenter, host, cluster, VM, datastore, and network views |\n| `/standalone/*` | Machines | Agent-backed machines and availability checks |\n| `/alerts/*` | Alerts | Alert rules, active alerts, history |\n| `/actions/*` | Actions | Governed action proposals, approvals, delivery, and audit state |\n| `/patrol/*` | Patrol | Attention queue, findings, investigations, and run history |\n| `/settings/*` | Settings | Infrastructure, security, notifications, plans, and Intelligence |\n\nThe retired aggregate `/workloads`, `/storage`, and `/recovery` top-level\nroutes are not canonical navigation. Shared resource, storage, and recovery\ncontracts remain backend building blocks consumed inside platform pages.\n\n### State Management\n- **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching.\n- **Metrics collector** (`stores/metricsCollector.ts`): Buffers time-series data for sparklines and historical charts.\n- **AI stores** (`stores/aiAvailability.ts`, `stores/aiIntelligence.ts`): Hold whether AI is enabled for the session and the Patrol findings.\n- **License store** (`stores/license.ts`): Tracks plan tier, feature flags, and multi-tenant state.\n- **System settings store** (`stores/systemSettings.ts`): Caches server-side preferences (for example theme and feature toggles).\n\n### Component Design\n- **Shared primitives** in `components/shared/`: `Card`, `Button`, `Toggle`, `FilterButtonGroup`, `Table` — all mapped to the semantic design tokens.\n- **Lazy-loaded pages**: All top-level pages are loaded via `lazy()` with optional preloading after initial render.\n- **Virtual table windowing**: Large resource lists use virtualised rendering for smooth scrolling at scale.\n- **Command Palette** (`Cmd/Ctrl+K`): Quick-access command launcher.\n- **Keyboard shortcuts**: `g p` → Proxmox, `g d` → Docker, `g k` →\n  Kubernetes, `g n` → TrueNAS, `g v` → vSphere, `g s` → Machines, `g a` →\n  Alerts, `g r` → Patrol, `g t` → Settings, `/` → Search.\n\n### Mobile Experience\n- **MobileNavBar** component: Bottom tab bar for touch navigation.\n- **Relay integration**: The mobile app connects through the relay protocol for encrypted remote access.\n\n## 🔒 Security\n\n- **Encryption at Rest**: Sensitive config (passwords, API keys) encrypted with AES-GCM via `internal/crypto`. Each tenant can have its own encryption key.\n- **Scoped API Tokens**: Tokens carry explicit scopes (`monitoring:read`, `settings:write`, `ai:chat`, `ai:execute`, `docker:report`, etc.). Endpoints enforce scope checks before processing.\n- **RBAC**: Role-based access control via `pkg/auth` with file-backed persistence. Resources: `users`, `settings`, `monitoring`.\n- **SSO**: OIDC and SAML providers with per-provider configuration and multi-provider support.\n- **Agent Commands**: Disabled by default for security. Operators must opt in with `--enable-commands`.\n- **Audit Trail**: Every mutation logged to per-tenant SQLite databases with optional cryptographic signatures.\n- **Rate Limiting**: Per-endpoint and per-tenant rate limiting with configurable thresholds.\n- **CSRF Protection**: Token-based CSRF prevention on mutation endpoints.\n- **Recovery Mode**: Localhost-only endpoint for emergency auth recovery with time-limited tokens.\n\n## 🚀 Deployment\n\nPulse is distributed as:\n\n1. **Docker Container**: Multi-stage build producing a minimal image with the Go binary + embedded frontend.\n2. **Single Binary**: The frontend is compiled into the Go binary using `go:embed` (`internal/api/frontend_embed.go`), enabling single-file deployment.\n3. **Kubernetes Helm Chart**: For cluster deployments with configurable replicas and persistence.\n4. **Systemd Service**: For bare-metal or VM installations with `.env`-based configuration and `SIGHUP` reload support.\n5. **Proxmox LXC Helper Script**: One-line install inside a Proxmox helper-scripts container.\n","docs/API.md":"# 🔌 Pulse API Reference\n\nPulse provides a comprehensive REST API for automation and integration.\n\n**Base URL**: `http://<your-pulse-ip>:7655/api`\n\n## 🔐 Authentication\n\nMost API requests require authentication via one of the following methods:\n\n### API Token (Recommended)\n\nCreate an API token in Pulse's **API Access** settings with only the scopes\nneeded for your task. The read-only example below requires `monitoring:read`.\nPass the token in the `X-API-Token` header, using a private header file so the\nsecret does not appear in shell history or process arguments.\n\nOn a trusted machine, as the account running curl, prepare the file and open\nit in an editor. This preserves an existing file, restricts its directory and\nstops before editing if preparation fails or a credential path is symlinked or\nnot a regular file:\n\n```bash\n(\n  set -eu\n  umask 077\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ]; then\n    printf 'Refusing a symlinked credential path.\\n' >&2\n    exit 1\n  fi\n  if [ -e \"$auth_file\" ] && [ ! -f \"$auth_file\" ]; then\n    printf 'Credential file must be a regular file.\\n' >&2\n    exit 1\n  fi\n  mkdir -p \"$auth_dir\"\n  chmod 700 \"$auth_dir\"\n  touch \"$auth_file\"\n  chmod 600 \"$auth_file\"\n  vi \"$auth_file\"\n)\n```\n\nIn the editor, save just this line, replacing `<token>` with the API token:\n\n```text\nX-API-Token: <token>\n```\n\nDefine this helper in the same Bash session (curl 7.76 or later). The examples\non this page use it to make **one request**, print only the HTTP status and save\neach response to a new owner-only file. This keeps resource names, audit details\nand credential-bearing error bodies out of terminal output and recordings.\n\n```bash\npulse_api() (\n  set -eu\n  umask 077\n  if [ \"$#\" -ne 2 ]; then\n    printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\\n' >&2\n    exit 2\n  fi\n  method=$1\n  api_path=$2\n  case \"$api_path\" in\n    /api/*) ;;\n    *) printf 'Use an API path on this Pulse instance, not a full URL.\\n' >&2; exit 2 ;;\n  esac\n  case \"$method\" in\n    GET) set -- ;;\n    POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;\n    PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;\n    DELETE) set -- --request DELETE ;;\n    *) printf 'This example helper accepts only GET, POST, PUT and DELETE.\\n' >&2; exit 2 ;;\n  esac\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ] || [ ! -f \"$auth_file\" ]; then\n    printf 'Prepare a regular, private header file first.\\n' >&2\n    exit 1\n  fi\n  chmod 700 \"$auth_dir\" || exit \"$?\"\n  chmod 600 \"$auth_file\" || exit \"$?\"\n  result_file=$(mktemp \"$auth_dir/api-response.XXXXXX\") || exit \"$?\"\n  curl_exit=0\n  status=$(curl --disable --fail-with-body --silent --show-error --globoff \\\n    --proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \\\n    --header \"@$auth_file\" --output \"$result_file\" --write-out '%{http_code}' \\\n    \"$@\" \"http://127.0.0.1:7655$api_path\") || curl_exit=$?\n  printf 'HTTP %s\\nPrivate response: %s\\n' \"$status\" \"$result_file\"\n  [ \"$curl_exit\" -eq 0 ] || exit \"$curl_exit\"\n  case \"$status\" in\n    2??) ;;\n    *) exit 1 ;;\n  esac\n)\n```\n\nThen make a read-only request with a `monitoring:read` token:\n\n```bash\npulse_api GET /api/state/summary\n```\n\nThe default loopback origin applies only when curl runs on the Pulse host and\nexplicitly bypasses proxy environment settings. For remote access, replace\n`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace\n`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate\nverification enabled; for a private CA, add `--cacert` with a CA file you\nverified separately, never `--insecure`. Do not put credentials in the URL.\n\nOpen the reported response file privately to inspect it, including on failure;\nshare only the relevant redacted error, not the whole response or file. Each\ncall preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit\nand retain the error body; redirects are not followed and are not success.\nThe connection limit is five seconds and the whole request limit is twenty\nseconds. A partial file after a transport error is not a complete result.\nNo request is retried automatically, especially a POST whose outcome is\nuncertain: inspect the existing action and target before deciding what to do.\nA 2xx response is only HTTP success, not proof that an action completed.\n\nThe helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).\nPOST and PUT read their JSON body from standard input; GET and DELETE send no\nbody. A method being supported is not permission to change a resource. Choose\neach administrative change separately and check its current state afterwards.\nA timeout or incomplete response can occur **after a change was applied**;\ndo not repeat a POST, PUT or DELETE merely because the response was lost.\n\nKeep `--globoff`: shell quotes alone do not disable curl's URL globbing. Without\nit, a path containing a brace list such as `{review,ops}` or a range such as\n`[1-3]` can send several requests, including several POST, PUT or DELETE\nrequests. The helper treats these characters literally, not as a batch of\ntargets. Still quote the whole path in the shell, and URL-encode dynamic resource\nIDs and query values; `--globoff` does not encode them for you.\n\nKeep `--disable` first: it ignores local curl configuration that could enable\ntrace output, bypass TLS or follow redirects. On an authentication-enabled\ninstance, the protected summary checks token access; `/api/health` is public\nand does not verify authentication. The helper is local example code, not an\ninstalled Pulse command; define it again in a new shell before using these\nexamples. Dispose of response files under your normal private-data policy.\n\nDo not paste tokens into command lines, URLs or issue reports. Keep the header\nfile private and outside shared repositories and diagnostics; do not use curl\nverbose/trace output when sharing a result. Share only the relevant redacted\nerror, not the whole infrastructure response. Revoke tokens that are no longer\nneeded in Pulse's API Access settings.\n\n### Bearer Token\n\nThe same API token can use a Bearer header instead. Replace the header file's\nline in the editor with the following, then use the same helper above;\ndo not send both authentication headers:\n\n```text\nAuthorization: Bearer <token>\n```\n\n### Session Cookie\n\nFor one-off read-only diagnostics, use your signed-in Pulse browser to open\nthe API path on that same instance. Do not extract or paste its session cookie\ninto a command or report; use the scoped token-file method above for automation.\nState-changing session requests also require Pulse's CSRF protection, which\nthe UI handles.\n\nSession endpoints:\n- `POST /api/login` (sets the session cookie, `pulse_session` over plain HTTP or `__Host-pulse_session` over HTTPS, plus `pulse_csrf`)\n- `POST /api/logout` (clears session)\n\nLogin body:\n```json\n{ \"username\": \"admin\", \"password\": \"secret\", \"rememberMe\": true }\n```\n\nPublic endpoints include:\n- `GET /api/health`\n- `GET /api/version`\n- `GET /api/agent/version` (agent update checks)\n- `GET /api/setup-script` (public; the generated script prompts for its setup token at runtime and skips auto-registration if none is supplied)\n\n## 🔏 Scopes and Admin Access\n\nSome endpoints require admin privileges and/or scopes. Common scopes include:\n- `monitoring:read`\n- `monitoring:write`\n- `settings:read`\n- `settings:write`\n- `agent:config:read`\n- `agent:manage`\n- `actions:plan`, `actions:approve`, `actions:execute`\n- `audit:read`\n\nEndpoints that require admin access are noted below.\n\nAdmin endpoints are instance administration unless they act only on the\nrequest organization. On instance administration an API token must not be\nbound to any organization other than the default one: a token bound to a\ntenant organization gets `403` whatever its scopes, as that organization's\nowner does. Endpoints that serve only the request organization, such as nodes\nand platform connections, agents, install commands, configuration transfer and\nreports, accept tokens bound to that organization with the listed scope.\n\n---\n\n## 📡 Core Endpoints\n\n### System Health\n`GET /api/health`\nCheck if Pulse is running.\n```json\n{\n  \"status\": \"healthy\",\n  \"timestamp\": 1700000000,\n  \"uptime\": 3600,\n  \"devModeSSH\": false\n}\n```\n\n### System State\n`GET /api/state`\nReturns the complete state of your infrastructure (Nodes, VMs, Containers, Storage, Alerts). This is the main endpoint used by the dashboard.\n\n`GET /api/state/summary`\nReturns a lightweight integration summary for external dashboards and checks. Requires `monitoring:read`.\n\n```json\n{\n  \"activeAlerts\": 1,\n  \"nodes\": 2,\n  \"vms\": 8,\n  \"containers\": 12,\n  \"dockerHosts\": [\n    {\n      \"name\": \"Docker Host\",\n      \"containers\": 5,\n      \"uptimeSeconds\": 86400,\n      \"cpuUsagePercent\": 12.5\n    }\n  ],\n  \"verdicts\": {\n    \"ok\": 17,\n    \"attention\": 1,\n    \"critical\": 1,\n    \"stale\": 1,\n    \"off\": 2,\n    \"unknown\": 0\n  },\n  \"attention\": [\n    {\n      \"id\": \"node-1\",\n      \"name\": \"PVE Node 1\",\n      \"type\": \"agent\",\n      \"platformType\": \"proxmox\",\n      \"verdict\": \"critical\",\n      \"topReason\": { \"code\": \"offline\" }\n    }\n  ],\n  \"lastUpdate\": \"2026-05-24T10:11:12Z\"\n}\n```\n\n`verdicts` uses the canonical `ok`, `attention`, `critical`, `stale`, `off`,\nand `unknown` fleet-health vocabulary. `attention` is severity ordered and\ncapped at 30 entries. Powered-off workloads are neutral (`off`), and stale or\nmissing telemetry is never reported as `ok`.\n\n### Simple Stats (HTML)\n`GET /simple-stats`\nLightweight HTML status page for quick checks.\n\n### Unified Resources\n`GET /api/resources`\nReturns the unified resource list with pagination + aggregations. Requires `monitoring:read`.\n\nQuery params:\n- `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `app-container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`)\n- `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`)\n- `excludeSource`: comma-separated list of sources that must not be present on a matching resource\n- `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`)\n- `parent`: parent resource ID\n- `cluster`: cluster name\n- `namespace`: Kubernetes namespace (filters Kubernetes resources only)\n- `q`: name search (contains match)\n- `tags`: comma-separated tags\n- `page`: page number (default `1`)\n- `limit`: page size (default `50`, max `100`)\n- `sort`: `name` (default), `status`, `type`, `lastSeen`\n- `order`: `asc` (default) or `desc`\n\nNote: `GET /api/resources` is optimized for list views. Some large, platform-specific fields may be omitted from the list response and are only returned by `GET /api/resources/{id}`.\n\nEach list resource can include a backend-owned `health` envelope with a\ncanonical `verdict` and stable `reasons` (`code` plus optional compact\n`detail`). Consumers should use this envelope for cross-platform posture\ninstead of deriving health from provider-specific status strings.\n\n#### Guest readings: availability and age\n\nIn legacy VM payloads, `disk.usage: -1` means unavailable, not negative usage.\nIn `GET /api/resources` and `GET /api/resources/{id}`, an unavailable disk\nreading is omitted as `metrics.disk`. **Do not replace an absent metric with\nzero**; a genuinely observed zero is valid. Allocated virtual-disk capacity\nalone does not establish used space inside the guest.\n\nRead `proxmox.diskStatusReason` alongside the value. A `prev-` reason means\nretained disk evidence, not a fresh reading. `agent-not-running` can also\nrepresent a general guest-agent HTTP 500 error: it does not establish that the\nguest's service is absent or stopped. An absent reason alone does not prove\nfreshness either.\n\nFor memory, use `metrics.memory.observation` for the **selected metric's**\nsource and age, not a different source's retained `proxmox.memory` facet:\n\n| Observation `state` | Meaning for the consumer |\n| --- | --- |\n| `current` | Accepted source observation; a normal cache hit can keep the original `observedAt`. Check its age for your use. |\n| `last-known` | Retained evidence, not a new measurement. Keep its original age visible; do not record it as a fresh sample. |\n| `unavailable` | No usable usage observation; do not treat retained numbers or zero-valued fields as a measurement. |\n| Missing or unrecognised | Freshness is unknown, including older payloads without an observation. Do not infer `current`. |\n\n`observedAt` belongs to that source observation. If it is absent or in the\nfuture, its age is unknown; do not substitute the resource's `lastSeen` or\n`updatedAt`. In the raw `proxmox.memory` facet, `usageUnavailable: true` can\npreserve known capacity without measured usage. An absent `metrics.memory`\nis not 0% usage. Conversely, an independently observed PVE or Pulse-agent\nmemory reading can remain current while QEMU disk collection is deferred;\ndisk state is not memory provenance.\n\nA running VM, an advancing row timestamp or an OK backup does not prove fresh\nguest readings or successful thaw. Do not install or restart an agent or send\nlive guest-agent probes merely to fill these gaps, especially during a backup,\nfreeze/thaw or an unresponsive-guest incident. Follow [Backup safety](VM_DISK_MONITORING.md#backup-safety)\nand [missing-reading guidance](VM_DISK_MONITORING.md#a-missing-reading-is-not-an-installation-diagnosis).\nRecovery needs independent thaw, fresh successful writes to every covered\nfilesystem and workload liveness; retained API values cannot establish it.\n\nAvailability is an additive resource facet. `availability` is the compatibility\nsummary used by existing clients; `availabilityChecks` contains every check\nattached to the resource. Each check can include `correlationState`\n(`attached`, `standalone`, `ambiguous`, or `unresolved`), its correlation\nrule/reason/candidate count, and an `evidence` envelope with observation and\nvalidity timestamps. Attached targets also add a `checks` relationship and do\nnot appear as separate `network-endpoint` rows.\n\n`GET /api/resources/stats`\nReturns aggregations (counts + health rollups).\n\n`GET /api/resources/k8s/namespaces?cluster=<clusterName>`\nReturns namespace-level rollups (pods + deployments) for a Kubernetes cluster. Requires `monitoring:read`.\n```json\n{\n  \"cluster\": \"prod-k8s\",\n  \"data\": [\n    {\n      \"namespace\": \"default\",\n      \"pods\": { \"total\": 12, \"online\": 10, \"warning\": 2, \"offline\": 0, \"unknown\": 0 },\n      \"deployments\": { \"total\": 3, \"online\": 3, \"warning\": 0, \"offline\": 0, \"unknown\": 0 }\n    }\n  ]\n}\n```\n\n`GET /api/resources/{id}`\nFetch a single resource by ID.\n\n`GET /api/resources/{id}/children`\nReturns child resources for the parent ID.\n\n`GET /api/resources/{id}/metrics`\nReturns the resource metrics payload.\n\n`POST /api/resources/{id}/link` (`monitoring:write`)\nManually link two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/unlink` (`monitoring:write`)\nManually unlink two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/report-merge` (`monitoring:write`)\nReport an incorrect merge (creates exclusions).\n```json\n{ \"sources\": [\"proxmox\", \"agent\"], \"notes\": \"optional note\" }\n```\n\n### Resource Maintenance and Operator State\n\nUse a timed maintenance window to suppress alerts during planned work on a\nresource. This API, including descendant scope, is available in stable v6.4.1.\nChoose the resource's `id` from `GET /api/resources` and URL-encode it in the\nrequest path. Do not substitute a display name or a Proxmox VM number.\n\n| Endpoint | Required scope | Result |\n| --- | --- | --- |\n| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |\n| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |\n| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |\n\n**PUT replaces the whole record, not just the fields supplied.** Read the current\nstate first and preserve unrelated settings, such as monitoring mode, lifecycle,\nremediation policy, criticality and notes. Treat only `operator_state_not_set`\nas an empty record, and stop on other read errors. Coordinate concurrent writers\nso a read followed by a PUT does not overwrite someone else's changes.\n\nFor a one-time window, merge these fields into that record, replacing the example\ntimes with your intended start and end:\n\n```json\n{\n  \"maintenanceStartAt\": \"2026-10-15T20:00:00Z\",\n  \"maintenanceEndAt\": \"2026-10-15T20:30:00Z\",\n  \"maintenanceScope\": \"resource_and_descendants\",\n  \"maintenanceReason\": \"Proxmox node maintenance\"\n}\n```\n\nThis is a set of fields to merge, not a complete replacement for an existing\nrecord. Remove any `maintenanceRecurrence` when replacing a recurring schedule\nwith this one-time window. The API rejects a record containing both.\n\n| Field | Meaning |\n| --- | --- |\n| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |\n| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |\n| `maintenanceReason` | Optional explanation of the planned work. |\n\nThe response includes `maintenanceWindowActive` and, while active,\n`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the\nrecord's `setAt` and `setBy` fields from the request, ignoring client values.\n\nDuring an active window, matching new alerts and firing/recovery notifications\nare suppressed. Applying an active window also clears matching existing alerts\nfrom the active list, retaining history. This is broader than pausing delivery\nfor an existing incident. Resources outside the scope remain monitored normally.\n\nThe window expires automatically, with no re-enable request needed. Subsequent\nobservations can raise alerts again, subject to normal alert policy and any other\nactive maintenance window. Previously cleared alerts are not automatically\nrestored, and expiry does not replay a backlog of maintenance notifications.\n\nTo end maintenance early, read the latest record, remove its maintenance fields\nand PUT the remaining state back. Do not DELETE the record unless you also intend\nto remove its other operator settings. An inherited window must be changed on\nthe ancestor that owns it.\n\n### Fleet Connections\n`GET /api/connections`\nReturns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.\n\nThe payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.\n\nDefine the private-response helper in [Authentication](#-authentication),\nwith `settings:read` on the token:\n\n```bash\npulse_api GET /api/connections\n```\n\n### Unified Action Planning\n\nThe existing action API separates planning, approval and execution. Run these\nsteps separately, inspecting each response before moving on; do not paste the\nwhole sequence as an unattended recovery script. Use the private header file\nand `pulse_api` helper from [Authentication](#-authentication), not a token in a\ncommand or environment assignment. Inspect each reported private response file\nbefore proceeding. For remote access, change the helper as described there,\nusing your Pulse HTTPS origin with certificate verification enabled.\n\n| Endpoint | Token scope and access |\n| --- | --- |\n| `GET /api/agent/resource-capabilities/{id}` | `monitoring:read`; lists the resource's advertised capabilities and parameter schemas. |\n| `POST /api/actions/plan` | `actions:plan` and permission to plan actions; creates a plan but does not approve or execute it. |\n| `POST /api/actions/{id}/decision` | `actions:approve` and permission to approve actions; records an explicit `approved` or `rejected` decision for a pending action, without executing it. |\n| `POST /api/actions/{id}/execute` | `actions:execute` and permission to execute actions; dispatches only an approved action or an approval-free executable plan. Dry-run-only plans cannot execute. |\n| `GET /api/audit/actions` | `audit:read`, audit-log read permission and licensed audit logging. |\n| `GET /api/audit/actions/{id}/events` | The same audit access; returns the action's lifecycle evidence. |\n\nThe legacy `ai:execute` scope also permits planning, approval and execution,\nbut use the narrower action scopes when possible. Approval and execution tokens\nmust be bound to an authorised user; a scope alone does not bypass approval\npolicy, separation of duties or step-up requirements. Audit reads through a\nbrowser session additionally require admin access. A `monitoring:read` token is\nnot an action-control or audit token.\n\n1. **Inspect capabilities.** Choose the canonical resource `id` from\n   `GET /api/resources`, not a display name or a VM number. Replace `vm:42`\n   throughout these examples and URL-encode it in request paths (`vm%3A42` here).\n   An empty capabilities list means there is nothing to plan for that resource.\n\n```bash\npulse_api GET /api/agent/resource-capabilities/vm%3A42\n```\n\n2. **Plan only.** Use an advertised capability and its actual parameter schema;\n   `restart` and `mode=graceful` are examples, not universal capabilities. Choose\n   a request ID for this specific intent. Pulse derives the actor from the\n   authenticated credential, not a caller-supplied `requestedBy` value.\n\n```bash\npulse_api POST /api/actions/plan <<'JSON'\n{\n  \"requestId\": \"manual-recovery-123\",\n  \"resourceId\": \"vm:42\",\n  \"capabilityName\": \"restart\",\n  \"params\": { \"mode\": \"graceful\" },\n  \"reason\": \"Recover after confirmed outage\"\n}\nJSON\n```\n\nInspect the returned approval policy, blast radius, expiry and preflight checks.\nKeep its `actionId` and reviewed `planHash`; replace `act_...` and `sha256:...`\nbelow with those returned values. Planning is not proof that execution is safe\nor currently available.\n\n3. **Decide only if approval is required.** Use an authorised approver's private\n   header file. Approve only the reviewed plan; stop on a refusal or stale-plan\n   response rather than weakening the policy or creating another recovery action.\n\n```bash\npulse_api POST /api/actions/act_.../decision <<'JSON'\n{\n  \"outcome\": \"approved\",\n  \"reason\": \"Inside maintenance window\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\n4. **Execute separately.** Only after reviewing a successful decision, or an\n   approval-free executable plan, use the executor's authorised header file:\n\n```bash\npulse_api POST /api/actions/act_.../execute <<'JSON'\n{\n  \"reason\": \"Execute approved recovery\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\nIf the response is lost or times out, inspect the existing action and target\nbefore retrying: losing the connection does not prove that execution stopped.\nAn HTTP success is not a substitute for checking the terminal result and the\nresource's observed state.\n\n5. **Read the audit and events**, using a private header file with audit access:\n\n```bash\npulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'\n```\n\n```bash\npulse_api GET /api/audit/actions/act_.../events\n```\n\nWhen audit logging is unavailable, use Pulse's action detail in the signed-in\nUI to inspect the existing action; do not infer success or repeat execution\nfrom an unavailable audit response.\n\nResponse:\n```json\n{\n  \"actionId\": \"act_...\",\n  \"requestId\": \"manual-recovery-123\",\n  \"allowed\": true,\n  \"requiresApproval\": true,\n  \"approvalPolicy\": \"admin\",\n  \"predictedBlastRadius\": [\"vm:42\", \"node-1\"],\n  \"rollbackAvailable\": false,\n  \"message\": \"Plan created for restart on web-42. Execution requires admin approval and is not performed by this endpoint.\",\n  \"plannedAt\": \"2026-05-03T10:00:00Z\",\n  \"expiresAt\": \"2026-05-03T10:05:00Z\",\n  \"resourceVersion\": \"resource:sha256:...\",\n  \"policyVersion\": \"policy:sha256:...\",\n  \"planHash\": \"sha256:...\",\n  \"preflight\": {\n    \"target\": \"vm:42\",\n    \"currentState\": \"web-42 is warning\",\n    \"intendedChange\": \"Restart the VM\",\n    \"dryRunAvailable\": false,\n    \"dryRunSummary\": \"No provider-supported dry run is advertised for this capability.\",\n    \"safetyChecks\": [\n      \"Resource was resolved from the unified resource registry.\",\n      \"Capability is advertised by the resource contract.\",\n      \"This endpoint plans only; it does not approve or execute the action.\",\n      \"Execution requires admin approval.\"\n    ],\n    \"verificationSteps\": [\n      \"Refresh the resource and confirm the expected state after execution.\",\n      \"Review /api/audit/actions/{actionId}/events for lifecycle evidence.\"\n    ],\n    \"generatedAt\": \"2026-05-03T10:00:00Z\"\n  }\n}\n```\n\n### Resource Metadata\nUser notes, tags, and custom URLs for resources.\n\nThese routes do not require admin access. Any authenticated browser session can call them, and API tokens need the scope shown.\n\n- `GET /api/agents/metadata` (`monitoring:read`)\n- `GET /api/agents/metadata/{agentId}` (`monitoring:read`)\n- `PUT /api/agents/metadata/{agentId}` (`monitoring:write`)\n- `DELETE /api/agents/metadata/{agentId}` (`monitoring:write`)\n\n- `GET /api/guests/metadata` (`monitoring:read`)\n- `GET /api/guests/metadata/{guestId}` (`monitoring:read`)\n- `PUT /api/guests/metadata/{guestId}` (`monitoring:write`)\n- `DELETE /api/guests/metadata/{guestId}` (`monitoring:write`)\n\n- `GET /api/docker/metadata` (`monitoring:read`)\n- `GET /api/docker/metadata/{containerId}` (`monitoring:read`)\n- `PUT /api/docker/metadata/{containerId}` (`monitoring:write`)\n- `DELETE /api/docker/metadata/{containerId}` (`monitoring:write`)\n\n- `GET /api/docker/runtimes/metadata` (`monitoring:read`)\n- `GET /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:read`)\n- `PUT /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n- `DELETE /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n\n### Version Info\n`GET /api/version`\nReturns version, build time, and update status.\nExample response:\n```json\n{\n  \"version\": \"6.0.0\",\n  \"buildTime\": \"2026-02-21T00:00:00Z\",\n  \"channel\": \"stable\",\n  \"deploymentType\": \"systemd\",\n  \"updateAvailable\": false,\n  \"latestVersion\": \"6.0.0\"\n}\n```\nVersion fields are returned as plain semantic versions (no leading `v`).\n\n---\n\n## 🖥️ Nodes & Config\n\n### Public Config\n`GET /api/config`\nReturns a small public config payload (update channel, auto-update enabled).\n\n### List Nodes\n`GET /api/config/nodes` (admin, `settings:read`)\n\nThis administrative inventory covers saved **PVE, PBS and PMG connections**,\nnot individual VMs or agents. Keep its response private. Use the returned\nconnection `id` for updates, deletion and tests; a display name, VMID or another\ninstallation's node name is not that ID. Re-read the inventory before a change\nrather than guessing an ID from a list position.\n\n### Add Node\n`POST /api/config/nodes` (admin, `settings:write`)\n\nAdding a connection saves credentials and enables collection; it is not a\npassive validation step. Use an existing dedicated, least-privilege platform\ntoken. The **Pulse API token** in the private header file authorises this\nconfiguration request; the **Proxmox token** in the JSON body authorises reads\nfrom the monitored platform. They are different credentials. Do not use a root\npassword or widen a department-scoped token to make denied peer reads succeed;\nfollow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).\nFor PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)\nand [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can\nalso try to create a token and ACLs on the PBS server; it is not read-only.\n\nPrepare the request in a private editor, not a credential-bearing shell\nassignment, here-document or command argument. Use the same Bash session as\nthe [private-response helper](#api-token-recommended):\n\n```bash\nnode_request_dir=$(umask 077; mktemp -d \"${TMPDIR:-/tmp}/pulse-node.XXXXXX\") &&\nnode_request_file=\"$node_request_dir/node.json\" &&\n(umask 077; set -C; : > \"$node_request_file\") &&\nvi \"$node_request_file\"\n```\n\nIf preparation or the editor fails, stop. In that editor, enter the following\nJSON, replacing the example hostname, connection name, full platform token ID\n(`user@realm!token`) and token-secret placeholder with your intended values:\n\n```json\n{\n  \"type\": \"pve\",\n  \"name\": \"PVE connection A\",\n  \"host\": \"https://pve.example.net:8006\",\n  \"tokenName\": \"pulse-monitor@pve!pulse-readonly\",\n  \"tokenValue\": \"<platform-token-secret>\",\n  \"verifySSL\": true\n}\n```\n\nSet `verifySSL` explicitly to `true`: omission on these new-connection and\ntest request paths does **not** enable certificate verification. The example\nrequires a certificate trusted by the Pulse server for that hostname. For a\nself-signed/private certificate, also supply `fingerprint` with the SHA-256\ncertificate fingerprint verified through an independent trusted channel.\nA fingerprint merely fetched from the endpoint is not established trust.\nDo not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to\nwork around a certificate failure.\n\nAfter reviewing this one intended configuration change, send the file:\n\n```bash\npulse_api POST /api/config/nodes < \"$node_request_file\"\n```\n\nThe helper retains the response privately and makes one bounded request,\nwithout retries. A lost response can follow a successful save: re-read the\nsaved inventory before deciding whether another request is needed. HTTP success\ndoes not establish fresh readings for every node, guest, backup or History view.\nKeep both request and response files out of reports and shared repositories;\nremove the temporary request under your credential policy when no longer needed.\n\n### Test Connection\n`POST /api/config/nodes/test-connection` (admin, `settings:write`)\n\nTests supplied credentials with live platform requests without saving the\nconnection. Use the same private JSON-file transport and explicit TLS settings,\nonly when a live connection test is appropriate; it is not a required precursor\nto filing a report. Success covers the reads performed by that test, not all\ncollection permissions, guest recovery or sustained monitoring.\n\n### Test Node Config (No Save, Live Request)\n`POST /api/config/nodes/test-config` (admin, `settings:write`)\n\nThis also contacts the platform using the supplied credentials; **no save**\ndoes not mean offline schema validation. Do not run either test merely to\ncollect new evidence during a freeze/thaw, backup or unresponsive-host incident.\nUse existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).\n\n### Update Node\n`PUT /api/config/nodes/{id}` (admin, `settings:write`)\n\nReview the current connection and submit only the intended supported changes\nfrom a private JSON file. Do not copy masked credentials from a GET response\nback as new secrets. Credential rotation and trust changes are separate\ndecisions; preserve the existing credential and fingerprint when not changing\nthem. A response failure does not prove the update was unapplied.\n\n### Delete Node\n`DELETE /api/config/nodes/{id}` (admin, `settings:write`)\n\nRemoves the Pulse connection, not the platform workloads or the upstream token.\nCollection and related alert coverage can stop. Check the exact connection and\narrange any needed independent coverage before an intentional removal; do not\ndelete and re-add a connection to diagnose missing readings or identity mixing.\n\n### Test Node (Legacy)\n`POST /api/config/nodes/{id}/test` (admin, `settings:write`)\n\nUses the saved connection for a live test. It is not a passive read of its\nlast result or a guarantee that ordinary collection recovered.\n\nAn optional JSON body can override `verifySSL` and `fingerprint` for this test\nwithout changing the saved connection or credentials. Omitted fields keep their\nsaved values. Send `false` to disable CA verification or `\"\"` to clear the saved\nfingerprint for the test. A nonempty fingerprint remains enforced regardless of\n`verifySSL`. Empty-body requests keep the saved TLS settings. For example,\n`{\"verifySSL\":true,\"fingerprint\":\"<SHA-256 fingerprint>\"}` tests an edited pin.\n\n### Refresh Cluster Nodes\n`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)\n\nActively rediscovers a saved PVE connection's cluster membership. It does not\nselect authorised nodes, grant missing permissions or repair an unrelated\nstandalone installation. Keep intentional token restrictions intact.\n\n### Export Configuration\n`POST /api/config/export` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:read`)\nRequest body:\n```json\n{ \"passphrase\": \"use-a-strong-passphrase\" }\n```\nReturns an encrypted export bundle in `data`. Passphrases must be at least 12 characters.\n\n### Import Configuration\n`POST /api/config/import` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:write`)\nRequest body:\n```json\n{\n  \"data\": \"<exported-bundle>\",\n  \"passphrase\": \"use-a-strong-passphrase\"\n}\n```\n\n---\n\n## 🧭 Setup & Discovery\n\n### Setup Script (Public)\n`GET /api/setup-script`\nReturns the Proxmox/PBS setup script as a shell-script download. Accepts an\noptional legacy `setup_token` query for compatibility. Current downloads\ncontain no token: paste the separately revealed token only at the silent\nterminal prompt, or supply `PULSE_SETUP_TOKEN_FILE` pointing to a mode-0600\nregular file in a mode-0700 directory owned by the script's user. Never put a\ntoken in a copied command or URL. Canonical callers must send a supported `type` of `pve` or\n`pbs` plus non-empty `host` and `pulse_url`; the route no longer generates\nplaceholder-host scripts for later repair or reconstructs Pulse identity from\nthe request origin. The route now shares the same canonical type boundary as\n`/api/setup-script-url`, rejecting unsupported node types instead of treating\nunknown values as PBS, and it normalizes the supplied `host` before script\ngeneration so downloaded artifacts and rerun URLs preserve the same canonical\nnode identity as the bootstrap response. The optional `backup_perms=true`\nquery is supported only for `type=pve`.\n\n### Setup Script URL\n`POST /api/setup-script-url` (admin, `settings:write`)\nGenerates a one-time setup token and URL for `/api/setup-script`.\nCanonical callers must send a supported `type` of `pve` or `pbs` plus a\nnon-empty `host`; the backend normalizes that host before minting the setup\ntoken and now returns the canonical bootstrap identity back in the response as\n`type`, `host`, `url`, `downloadURL`, `scriptFileName`, `command`, `commandWithEnv`,\n`commandWithoutEnv`, `setupToken`, `tokenHint`, and `expires`. The returned\ncommands are canonical root-or-sudo `curl -fsSL` bootstrap commands for the\ngenerated setup script, while `url`, `downloadURL`, and `scriptFileName` are\nthe runtime-owned artifact metadata used by copy and manual download surfaces.\nThe request body is a single canonical JSON object only; unknown fields and trailing JSON are\nrejected as invalid request shape, and `backupPerms:true` is supported only for\n`type:\"pve\"`. This route stays on the normal\nauthenticated bootstrap boundary: when Pulse auth is already configured it\nrequires a real authenticated session or API token, and setup tokens do not\nauthorize the request itself. Pulse-managed Proxmox monitor-token names on the\nsetup/bootstrap path derive from the canonical Pulse endpoint, not request-local\nhost fallbacks, so setup-script and turnkey node-add flows stay on one\ndeterministic `pulse-<canonical-scope-slug>` identity per Pulse instance.\nThe `command`, `commandWithEnv`, and `commandWithoutEnv` fields now contain\nidentical credential-free commands. They download the complete script before\nrunning it and prompt silently in the root-or-sudo process. The token crosses\nthe installer boundary through a private file, not process arguments or an\nexported secret. `downloadURL` equals the tokenless `url`, so a manual download\nalso needs the separately revealed token at runtime. `setupToken` is used for\n`/api/auto-register`; `tokenHint` remains masked on the setup page. Settings\nreveals the token in a separate dialog: run the command first, then copy and\npaste the token only at its prompt. The artifact is reused only for the same\nhost and options while its five-minute expiry is live, and discarded when the\nsetup modal closes.\nNon-frontend consumers must validate the complete artifact, including the\ncanonical host, type, URLs, filename, masked hint and live expiry. Current\nUnified Agents and the shell installer also accept the coherent older-server\nartifact during upgrades, but never execute its command text. For new\nProxmox agent enrolment against a newer server, use its current installer;\nalready enrolled agents keep reporting normally.\n\n### Auto-Register (Public)\n`POST /api/auto-register`\nRegisters a node through the canonical `/api/auto-register` contract using a temporary\nsetup token carried in the JSON `authToken` field. Canonical callers must send\na supported `type` of `pve` or `pbs`, an explicit `source` marker of `agent` or\n`script`, a canonical Pulse-managed `tokenId` in the form\n`pulse-monitor@{pve|pbs}!pulse-<canonical-scope-slug>` matching the requested\ntype, an explicit `serverName`, and missing-token requests now fail with\n`Pulse setup token required`. Incomplete token completion requests now fail\nwith `tokenId and tokenValue must be provided together`, and other missing\ncanonical request fields fail with explicit `Missing required canonical\nauto-register fields: ...` guidance.\nSuccess responses now carry the canonical stored identity and caller boundary\nback to the installer or runtime-side Unified Agent:\n`{\"status\":\"success\",\"action\":\"use_token\",\"type\":\"pve|pbs\",\"source\":\"agent|script\",\"host\":\"https://...\",\"nodeId\":\"<stored-name>\",\"nodeName\":\"<stored-name>\",...}`.\n\n### Agent Install Command\n`POST /api/agent-install-command` (admin, `settings:write`)\nGenerates an API token and install command for agent-based Proxmox setup.\n\n### Discovery\n`GET /api/discover` (admin, `settings:write`)\nReturns the last cached network discovery result from the background discovery service. It does not scan.\n\n`POST /api/discover` (admin, `settings:write`)\nRuns a manual network scan. JSON body: `{\"subnet\": \"...\", \"use_cache\": false}`. `subnet` defaults to `auto`; `use_cache: true` returns the cached result instead of scanning.\n\n### Test Notification\n`POST /api/test-notification` (admin, `settings:write`)\nBroadcasts a WebSocket test event.\n\n---\n\n## 📊 Metrics & Charts\n\n### Chart Data\n`GET /api/charts?range=1h`\nReturns time-series data for CPU, Memory, and Storage.\n**Ranges**: `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `24h`, `7d`\n\n### Storage Charts\n`GET /api/storage-charts`\nReturns storage chart data.\n\n### Storage Stats\n`GET /api/storage/{storageId}` (`monitoring:read`)\nReturns one storage resource by its `id`, wrapped as `{\"data\": {...}, \"timestamp\": 1700000000}`. The `data` object includes `id`, `name`, `node`, `instance`, `type`, `status`, `total`, `used`, `free`, `usage`, `content`, `shared`, `enabled` and `active`. Returns HTTP 404 with code `storage_not_found` for an unknown ID, and HTTP 405 for methods other than GET. A request without an ID (`GET /api/storage/`) returns HTTP 400 with code `missing_storage_id`.\n\n### Recovery (formerly Backups / Snapshots)\nPulse v6 uses the recovery API to provide a platform-agnostic view of backup and snapshot artifacts.\nThe endpoints below carry the provider-neutral contract covering subjects,\npoints, rollups, posture, and filter semantics.\n\n- `GET /api/recovery/points`\n  - Query params:\n    - Core filters: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`\n    - Time window: `from` (RFC3339), `to` (RFC3339)\n    - Paging: `page`, `limit`\n    - Normalized filters: `q`, `cluster`, `node`, `namespace`, `scope=workload`, `verification` (`verified` | `unverified` | `unknown`)\n- `GET /api/recovery/rollups`\n  - Query params: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`, `from` (RFC3339), `to` (RFC3339), `page`, `limit`\n- `GET /api/recovery/postures`\n  - Returns server-derived per-resource protection posture and provider evidence quality.\n  - Query params: repeated `resourceId` values (maximum 200), `state` (`protected` | `attention` | `unprotected` | `unknown`), `page`, `limit` (maximum 200)\n  - Batch clients must make one bounded request per 200 resource ids, never one request per table row.\n  - Unknown identity, permission, history, or collection completeness remains `unknown`; clients must not infer a healthier state from raw backup or snapshot artifacts.\n- `GET /api/recovery/series`\n  - Returns per-day counts for the activity chart.\n  - Query params: same filters as `/api/recovery/points` (except paging), plus `tzOffsetMinutes` (integer; UTC offset minutes for day bucketing)\n- `GET /api/recovery/facets`\n  - Returns distinct filter values (clusters/nodes/namespaces) and capability flags (size/verification/entity id present).\n  - Query params: same filters as `/api/recovery/points` (except paging)\n\n---\n\n## 🔔 Notifications\n\n### Send Test Notification\n`POST /api/notifications/test` (admin, `settings:write`)\nSends one test notification through a single channel per request, not all configured channels. The JSON body `method` (or `type`) selects `email`, `webhook`, or `apprise` and uses the saved configuration. `webhookId` targets one specific saved webhook. `config` tests unsaved email or Apprise settings. If alert delivery is paused, the response includes `deliveryPaused: true`.\n\n### Email, Apprise, and Webhooks\n- `GET /api/notifications/email` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/email` (admin, `settings:write`)\n- `GET /api/notifications/apprise` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/apprise` (admin, `settings:write`)\n- `GET /api/notifications/webhooks` (admin, `settings:read`; `settings:write` also accepted)\n- `POST /api/notifications/webhooks` (admin, `settings:write`)\n- `PUT /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `DELETE /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `POST /api/notifications/webhooks/test` (admin, `settings:write`)\n- `GET /api/notifications/webhook-templates` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/webhook-history` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/email-providers` (admin, `settings:read`; `settings:write` also accepted)\n\nEmail and webhook configurations accept optional `tagFilter` (an array of\nresource-tag strings) and `tagFilterMode` (`\"all\"` or `\"any\"`). An omitted\nfield is preserved on update; an empty `tagFilter` clears routing and restores\ndelivery for all resources.\n\n- `GET /api/notifications/health` (admin, `settings:read`; `settings:write` also accepted)\n  - Queue health is `degraded` whenever any retained `failed` or `dlq`\n    delivery exists, and `unavailable` when queue state cannot be read.\n    Recoverable failed attempts that returned to `pending` for retry do not\n    degrade health.\n  - `queue.attention_required` is the retained terminal-failure count.\n    `reason_codes` identifies failed and/or dead-letter state without exposing\n    notification content. Counts are retention-bounded: sent, failed, and\n    cancelled rows are retained for 7 days; dead-letter rows for 30 days.\n\n### Audit Webhooks (Pro)\n- `GET /api/admin/webhooks/audit` (admin, `settings:read`)\n- `POST /api/admin/webhooks/audit` (admin, `settings:write`)\n  - Body: `{ \"urls\": [\"https://...\"] }`\n  - Strict JSON contract: unknown fields and trailing payload are rejected.\n  - Maximum `20` webhook URLs per update request.\n  - URLs are normalized (trimmed) and duplicate entries are ignored.\n  - Endpoint fails closed if URL validation runtime is unavailable.\n\n### Advanced Reporting (Pro)\n\nReport generation uses `settings:read`. For single-resource and fleet PDF\nnarration, an API token also needs `ai:execute` (or full access). Without it,\nthe report still succeeds with a deterministic summary and no AI provider call;\na single-resource PDF also explains the missing scope. Browser sessions and\nserver-run schedules keep their existing narration behaviour. AI budgets still\napply. GET and CSV never use AI narration.\n\n- `GET /api/admin/reports/catalog` (admin, `settings:read`)\n  - Returns the canonical reporting catalog for the settings surface, including locked-state teaser copy, enabled-surface guidance copy, performance-report options, canonical single-report filename subject, canonical fallback filename date style, and the nested VM inventory export definition.\n  - Metadata route: readable without the `advanced_reporting` feature so locked admin surfaces can render the same reporting definition before upsell.\n- `POST /api/admin/reports/generate` (admin, `settings:read`)\n  - Body fields: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default title for that resource report.\n  - When narration is permitted and Pulse Assistant is configured, the configured AI provider narrates a PDF's executive summary; each narration counts against the AI budget and appears in AI usage. If the call fails, times out or the budget is spent, the PDF falls back to the deterministic summary. CSV output is never narrated. The settings UI uses this method.\n  - API-token callers also need the `ai:execute` scope for AI narration. With only `settings:read` the report is still produced, with the deterministic summary and a note saying why.\n- `GET /api/admin/reports/generate` (admin, `settings:read`)\n  - Takes the same fields as query params and returns the same report, except that it never calls an AI provider: a PDF carries the deterministic summary and a note saying AI narration needs `POST`. Other methods return `405` with `Allow: GET, POST`.\n- `POST /api/admin/reports/generate-multi` (admin, `settings:read`)\n  - Body fields: `resources` (1-50 entries of `{resourceType,resourceId}`), `format`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default fleet report title.\n  - AI narration of the fleet summary follows the same rule as single reports: API-token callers need `ai:execute`, otherwise the deterministic summary is used. CSV output is never narrated.\n- `GET /api/admin/reports/inventory/vms/export` (admin, `settings:read`)\n  - Query params: `format` (`csv` only; optional and defaults to `csv`)\n  - Exports the current fleet-wide VM inventory as spreadsheet-friendly CSV using the canonical runtime model.\n\nValidation and limits:\n- `start` and `end` must be RFC3339 when provided.\n- Malformed `start`/`end` values, `end` not strictly after `start`, or report windows over 366 days return `400 invalid_time_range`.\n- `metricType` must match `[a-zA-Z0-9._:-]+` and be <= 64 chars, otherwise `400 invalid_metric_type`.\n- `title` must be <= 256 chars, otherwise `400 invalid_title`.\n- Report body max size is 1MB; oversized payloads return `400 body_too_large`.\n- Report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.\n- VM inventory export only accepts `csv`.\n\nCommon reporting error codes:\n- `invalid_format`, `missing_params`, `invalid_resource_type`, `invalid_resource_id`\n- `invalid_metric_type`, `invalid_title`, `invalid_time_range`\n- `no_resources`, `too_many_resources`, `body_too_large`, `invalid_body`\n\n### Queue and Dead-Letter Tools\n- `GET /api/notifications/delivery-log?limit=200` (admin, `settings:read`)\n  - Returns recent per-attempt delivery evidence with destination identifiers,\n    alert identifiers, outcomes, timestamps, failure classes, and redacted error\n    text. Completed attempts are retained for 7 days; dead-letter attempts are\n    retained for 30 days, so the response reports both windows explicitly.\n  - `limit` defaults to 50 and is capped at 200.\n- `GET /api/notifications/queue/stats` (admin, `settings:read` and `monitoring:read`)\n  - Returns counts for all rows still retained by the queue. Status counts have\n    different retention windows and are not a delivery rate or lifetime total.\n- `GET /api/notifications/dlq` (admin)\n- `POST /api/notifications/dlq/retry` (admin)\n- `POST /api/notifications/dlq/delete` (admin, `settings:write`; `DELETE` also accepted)\n  - Body: `{ \"id\": \"<notification id>\" }`. Marks that queue row `cancelled`\n    with the note \"Manually deleted from DLQ\"; the row is not removed and is\n    retained for 7 days like other cancelled rows. The endpoint does not check\n    that the row is in the dead-letter queue.\n  - Returns `{ \"success\": true, \"message\": \"Notification deleted from DLQ\",\n    \"id\": \"<notification id>\" }`. An empty or missing `id`, or an invalid JSON\n    body, returns `400`. An unknown `id` returns `500`.\n- `POST /api/notifications/terminal-failures/retry` (admin, `settings:write`)\n  - Returns every retained `failed` or ","tests/integration/evals/tasks/retired-trial-acquisition.md":"# Scenario: Retired Trial Acquisition\n\n## Goal\nProve the self-hosted Pulse instance no longer initiates a hosted Pro trial from the ordinary in-app runtime and does not mutate local entitlements when the retired route is probed.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Capture current entitlements.\n2. Probe `POST /api/license/trial/start`.\n3. Confirm the response is HTTP `404`.\n4. Confirm the response does not contain legacy hosted-signup or trial-rate-limit acquisition payloads.\n5. Verify post-probe entitlements match the pre-probe entitlement summary.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/multi-tenant.md":"# Scenario: Multi-tenant Isolation\n\n## Goal\nProve that multi-tenant controls and isolation behave correctly from the user surface.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Sign in and open organization management.\n2. Create two organizations.\n3. Switch between orgs and confirm org-scoped data does not leak across tenants.\n4. Verify organization CRUD behavior (create/update/delete) works for the active org.\n5. Record any failures, flaky transitions, or unexpected UI states.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/relay-pairing.md":"# Scenario: Relay Pairing & Mobile Readiness\n\n## Goal\n\nProve that relay pairing works end-to-end: configure relay settings, verify connection to the relay server, generate a QR code for mobile pairing, and confirm the onboarding deep link is available.\n\n## Environment\n\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n- Relay Host: provided via `PULSE_E2E_RELAY_HOST`\n\n## Required checks\n\n1. Relay feature is available in license entitlements (`GET /api/license/entitlements` includes `relay` capability).\n2. Relay settings can be configured with the test relay server (`PUT /api/settings/relay` with `enabled: true, server_url: \"...\"` returns 200).\n3. Relay status becomes connected within 60s (`GET /api/settings/relay/status` → `connected: true`).\n4. Onboarding QR code returns required fields (`GET /api/onboarding/qr` → `schema`, `instance_url`, `relay`, `auth_token`, `deep_link`).\n5. Onboarding deep link is available (`GET /api/onboarding/deep-link` → `url` field non-empty).\n6. Relay settings page in the UI shows relay-related content (Relay, Connected, Enabled, etc.).\n\n## Output contract\n\nWrite JSON to `{{result_json}}` with this shape:\n\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/cloud-hosting.md":"# Scenario: Pulse Cloud Hosted Signup\n\n## Goal\nProve hosted cloud signup and magic-link request flows work end-to-end using real Stripe sandbox checkout from the public signup page.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Open `/cloud/signup` and verify form renders.\n2. Submit valid workspace creation details.\n3. Complete Stripe sandbox checkout and verify return to hosted signup completion page.\n4. Request a magic-link email from the same page.\n5. Validate the magic-link request endpoint responds with success contract.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/cloud-billing-lifecycle.md":"# Scenario: Cloud Billing Lifecycle\n\n## Goal\nProve post-checkout billing lifecycle behavior works end-to-end for Pulse Cloud by validating tenant provisioning and cancellation transitions.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Create a real Stripe sandbox checkout from `/cloud/signup`.\n2. Complete checkout with Stripe test card details.\n3. Verify tenant reaches active state in control-plane admin tenant listing.\n4. Trigger subscription cancellation and deliver corresponding webhook event.\n5. Verify tenant transitions to canceled state after webhook processing.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","tests/integration/evals/tasks/saml-sso.md":"# SAML SSO Provider Lifecycle\n\n## Objective\nValidate the full SAML SSO provider lifecycle: create a SAML provider,\nverify it appears in the provider list, check SP metadata generation,\ntest-connection validation, enable/disable toggling, and cleanup via delete.\n\n## Steps\n\n1. Log in as admin.\n2. GET `/api/security/sso/providers` — verify the endpoint responds with 200.\n3. POST `/api/security/sso/providers` — create a SAML provider with stub IdP metadata.\n4. GET `/api/security/sso/providers` — verify the new provider appears.\n5. GET `/api/saml/{providerID}/metadata` — verify SP metadata XML is returned.\n6. POST `/api/security/sso/providers/test` — validate the IdP metadata.\n7. Navigate to `/settings/security-sso` — verify the SSO settings page renders.\n8. PUT `/api/security/sso/providers/{id}` — disable the provider, verify disabled.\n9. PUT `/api/security/sso/providers/{id}` — re-enable, verify enabled.\n10. DELETE `/api/security/sso/providers/{id}` — delete and verify removal.\n\n## Expected Outcomes\n- Provider CRUD works end-to-end on the Community SSO capability.\n- SSO settings page renders SSO-related content.\n\n## Environment Variables\n- `PULSE_E2E_SAML_IDP_METADATA_URL` — optional live IdP metadata URL.\n","tests/integration/evals/tasks/truenas-node-add.md":"# Tasks Skill\n\nPath: `tests/integration/evals/tasks/truenas-node-add.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/truenas-node-add.md)"},"items":[{"name":"ARCHITECTURE.md","path":"ARCHITECTURE.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/ARCHITECTURE.md","title":"System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# Pulse Architecture\n\nPulse is a real-time infrastructure monitoring platform for **Proxmox VE**,\n**Proxmox Backup Server**, **Proxmox Mail Gateway**, **Docker**, **machine\nagents**, **Kubernetes**, **TrueNAS**, and early-access **VMware vSphere**\nenvironments. It uses a **Go 1.26** backend and a **SolidJS / TypeScript**\nfrontend.\n\n## 🏗 High-Level Overview\n\nThe system runs as a single binary that serves both the API and the embedded frontend assets. It connects to infrastructure via platform-supported HTTP and WebSocket APIs and lightweight push-based agents, normalises everything into a **Unified Resource model**, and delivers real-time updates to clients over WebSocket.\n\n```mermaid\nflowchart TD\n    User[Browser / Mobile] <-->|WebSocket + HTTP| Pulse[Pulse Server]\n    Mobile[Mobile App] <-->|E2E Encrypted| Relay[Relay Server]\n    Relay <-->|WebSocket| Pulse\n\n    subgraph \"Pulse Server (Go)\"\n        API[Decomposed API Router]\n        WS[WebSocket Hub]\n        Monitor[Reloadable Monitor]\n        UR[Unified Resource Registry]\n        AI[AI Subsystem]\n        Config[Config Manager]\n        License[Entitlements Engine]\n        Audit[Audit Logger]\n    end\n\n    Pulse -->|HTTPS :8006| PVE[Proxmox VE]\n    Pulse -->|HTTPS :8007| PBS[Proxmox Backup Server]\n    Pulse -->|HTTPS| PMG[Proxmox Mail Gateway]\n    Pulse -->|HTTPS| TrueNAS[TrueNAS SCALE/CORE]\n    Pulse -->|HTTPS| vSphere[VMware vCenter]\n    DockerAgent[Docker Agent] -->|HTTPS POST| API\n    HostAgent[Host Agent] -->|HTTPS POST| API\n    K8sAgent[Kubernetes Agent] -->|HTTPS POST| API\n\n    Monitor --> UR\n    UR --> WS\n    UR --> API\n```\n\n## 🔌 Backend Architecture (Go)\n\nAll backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is assembled from `cmd/pulse/main.go` which delegates to `pkg/server.Run()`.\n\n### Core Components\n\n1. **Entry Point (`cmd/pulse/main.go` → `pkg/server/server.go`)**\n   - Loads unified configuration via `internal/config.Load()`.\n   - Initialises the RBAC manager (`pkg/auth`), audit logger (`pkg/audit`), and crypto layer (`internal/crypto`).\n   - Creates a `ReloadableMonitor` that manages the lifecycle of all monitoring goroutines.\n   - Starts the HTTP/HTTPS server, WebSocket hub, AI services (Patrol + Chat), and the Relay client.\n   - Supports graceful hot-reload via `SIGHUP` and `.env` file watching.\n\n2. **Unified Resource Registry (`internal/unifiedresources`)**\n   - Central data model that normalises resources from Proxmox, PBS, PMG,\n     Docker, machine agents, Kubernetes, TrueNAS, and VMware providers into a\n     shared `Resource` contract.\n   - **Canonical v6 resource types**: `agent`, `vm`, `system-container`, `app-container`, `docker-host`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `storage`, `pbs`, `pmg`, `ceph`, `physical_disk`.\n   - Identity-matching engine: merges resources across sources using machine IDs, DMI UUIDs, hostnames, IPs, and MAC addresses.\n   - Provides typed **views** (`NodeView`, `K8sClusterView`, etc.) for consumer-specific queries.\n   - Canonical API endpoint: `GET /api/resources`.\n\n3. **Monitoring Engine (`internal/monitoring`)**\n   - **Polymorphic monitors**: Each Proxmox VE/PBS/PMG node runs in its own goroutine, polling via the platform REST API.\n   - **Agent receivers** (`internal/api`): Docker, Host, and Kubernetes agents push metrics via HTTP POST to `/api/agents/{type}/report`.\n   - **TrueNAS provider** (`internal/truenas`): Uses the versioned JSON-RPC 2.0\n     WebSocket API on supported SCALE releases for system info, pools,\n     datasets, disks, alerts, ZFS snapshots, and replication tasks. A\n     version-gated REST path remains only for legacy SCALE and CORE systems.\n   - Enterprise/internal multi-org aware: when `PULSE_MULTI_TENANT_ENABLED=true`, each organisation gets a separate shared-process monitor namespace with its own configuration.\n\n4. **WebSocket Hub (`internal/websocket`)**\n   - Manages active browser connections with per-message compression (deflate).\n   - Broadcasts state diffs to all subscribed clients; supports per-organization broadcasts for internal multi-org setups.\n   - Enforces origin validation, organization-level authorization, and Enterprise/internal multi-org license gating.\n\n5. **Decomposed API Router (`internal/api`)**\n   - The router is split into focused registration files for maintainability:\n     - `router_routes_auth_security.go` — Auth, OIDC, SAML, SSO, security tokens, recovery, agent install scripts.\n     - `router_routes_monitoring.go` — Unified resources, metrics history, charts, recovery points, alerts, notifications, discovery.\n     - `router_routes_ai_relay.go` — AI settings, Patrol, Intelligence, Chat sessions, Relay config, and the approval workflow.\n     - `router_routes_org_license.go` — Organisations, RBAC, audit logs, license/entitlements, billing.\n     - `router_routes_registration.go` — Config CRUD, TrueNAS connections, update management, agent profiles, system settings.\n     - `router_routes_hosted.go` — Hosted/SaaS-specific signup and org admin routes.\n   - Every endpoint enforces **scoped access control** (e.g., `monitoring:read`, `settings:write`, `ai:execute`, `ai:chat`).\n\n6. **Relay Client (`internal/relay`)**\n   - Maintains a persistent WebSocket tunnel to a managed relay server for **mobile remote access**.\n   - Uses an ECDH key-exchange for per-channel **end-to-end encryption**.\n   - Multiplexes multiple mobile sessions over a single connection with back-pressure (data limiter) and per-channel authentication.\n   - Supports push notifications through the relay.\n   - Gated by the `relay` license feature.\n\n7. **AI Subsystem (`internal/ai`)**\n   - **AI runtime (`internal/ai/chat`)**: The governed tool loop behind Patrol investigations and Pulse Mobile chat (the in-app chat window has been retired). Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. External agents reach the same tools through `pulse-mcp`.\n   - **Pulse Patrol**: Scheduled background analysis that produces findings, predictions, and remediation plans. Autonomy levels: `monitor`, `approval`, `assisted`, `full`.\n   - **Intelligence Services**: Patterns, correlations, anomalies, baselines, forecasts, and incident recording. All surfaced via `/api/ai/intelligence/*`.\n   - **Safety gates**: Command execution disabled by default (`--enable-commands` opt-in); circuit breakers and scoped permissions at every layer.\n\n8. **Entitlements & Licensing (`pkg/licensing`)**\n   - Capability-key based gating: `ai_autofix`, `rbac`, `multi_tenant`, `relay`, `agent_profiles`, `kubernetes_ai`, `ai_alerts`, etc.\n   - Core tiers include **Community** (free), **Relay**, **Pro**, a reserved\n     hosted **Cloud** capability tier, request-assisted **MSP**, and\n     Enterprise/custom entitlements. The Cloud service is not generally\n     available.\n   - Activation, grant refresh, renewal, expiry, and legacy-license migration.\n     Active state is exposed through `/api/license/*`.\n\n9. **Provider-hosted MSP control plane (`internal/cloudcp`)**\n   - A Stripe-free provider control plane can run one isolated Pulse runtime/container per client workspace.\n   - A signed MSP license sets the provider plan and client workspace cap.\n   - Pulse Account handles the provider workspace roster, sign-in-once handoff, client-bound agent install paths, and provider access surfaces.\n   - Each client runtime remains a normal Pulse instance for monitoring data, alerts, webhooks, reports, users, and audit history. Shared-process organizations are for Enterprise/internal multi-org, not the default MSP boundary.\n\n10. **Recovery Engine (`internal/recovery`)**\n   - Aggregates backup data (PBS snapshots, ZFS snapshots, replication tasks) into a unified recovery point timeline.\n   - Provides faceted queries: filter by type, source, status, and source health.\n   - API: `/api/recovery/points`, `/api/recovery/series`, `/api/recovery/facets`, `/api/recovery/rollups`.\n\n11. **Audit Logging (`pkg/audit`)**\n    - Defence-in-depth: every mutation is logged to SQLite, optionally signed with per-tenant encryption keys.\n    - Async audit logging is on by default (`PULSE_AUDIT_ASYNC=false` disables it); see `docs/CONFIGURATION.md`.\n    - Tenant-aware logger manager for isolated per-org audit trails.\n\n### Data Flow\n\n1. **Collection**:\n   - **Proxmox VE / PBS / PMG**: Monitoring engine polls platform REST APIs (configurable interval, default 2 s for PVE).\n   - **Docker / Host / Kubernetes**: Lightweight agents push metrics via HTTP POST on their configured interval.\n   - **TrueNAS**: Provider polls the versioned JSON-RPC WebSocket API on\n     supported SCALE releases, with REST compatibility limited to recognized\n     legacy SCALE and CORE systems.\n2. **Normalisation**: Platform-specific responses are mapped into `unifiedresources.Resource` structs by adapters in `internal/unifiedresources/adapters.go`.\n3. **Registration**: Resources are inserted into the in-memory registry, which handles deduplication, identity matching, and status computation.\n4. **Broadcast**: The latest state snapshot is serialised to JSON and pushed to all connected WebSocket clients by the Hub.\n5. **Persistence**: Metrics history is stored in a SQLite-backed metrics store (`pkg/metrics`) with configurable retention.\n\n## 🎨 Frontend Architecture (SolidJS)\n\nThe frontend is a modern SPA in `frontend-modern/`, built with **SolidJS** and **TypeScript**. It uses fine-grained reactivity (no Virtual DOM) for maximum performance.\n\n### Key Technologies\n- **SolidJS**: Reactive UI framework.\n- **TailwindCSS** with a **semantic design token layer**: All structural colours use CSS custom-property tokens (`bg-base`, `bg-surface`, `text-base-content`, `border-border`, etc.) defined in `index.css`— components never hardcode hex values. Light/dark mode switches automatically via class-based theme resolution.\n- **Vite**: Build tooling (dev server + production bundling).\n- **Lucide Icons**: Icon library (imported as solid components).\n\n### Routing & Navigation\n\nNavigation is organised around platform-shaped pages with cross-platform\noperational surfaces:\n\n| Route | Page | Purpose |\n|---|---|---|\n| `/` and `/infrastructure` | Runtime home | Monitor-first authenticated entry point |\n| `/proxmox/*` | Proxmox | PVE, PBS, PMG, guests, storage, recovery, and Ceph |\n| `/docker/*` | Docker | Hosts, containers, Compose projects, Swarm, images, and storage |\n| `/kubernetes/*` | Kubernetes | Clusters, workloads, networking, storage, and events |\n| `/truenas/*` | TrueNAS | Systems, pools, datasets, disks, apps, VMs, and recovery |\n| `/vmware/*` | vSphere | Early-access vCenter, host, cluster, VM, datastore, and network views |\n| `/standalone/*` | Machines | Agent-backed machines and availability checks |\n| `/alerts/*` | Alerts | Alert rules, active alerts, history |\n| `/actions/*` | Actions | Governed action proposals, approvals, delivery, and audit state |\n| `/patrol/*` | Patrol | Attention queue, findings, investigations, and run history |\n| `/settings/*` | Settings | Infrastructure, security, notifications, plans, and Intelligence |\n\nThe retired aggregate `/workloads`, `/storage`, and `/recovery` top-level\nroutes are not canonical navigation. Shared resource, storage, and recovery\ncontracts remain backend building blocks consumed inside platform pages.\n\n### State Management\n- **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching.\n- **Metrics collector** (`stores/metricsCollector.ts`): Buffers time-series data for sparklines and historical charts.\n- **AI stores** (`stores/aiAvailability.ts`, `stores/aiIntelligence.ts`): Hold whether AI is enabled for the session and the Patrol findings.\n- **License store** (`stores/license.ts`): Tracks plan tier, feature flags, and multi-tenant state.\n- **System settings store** (`stores/systemSettings.ts`): Caches server-side preferences (for example theme and feature toggles).\n\n### Component Design\n- **Shared primitives** in `components/shared/`: `Card`, `Button`, `Toggle`, `FilterButtonGroup`, `Table` — all mapped to the semantic design tokens.\n- **Lazy-loaded pages**: All top-level pages are loaded via `lazy()` with optional preloading after initial render.\n- **Virtual table windowing**: Large resource lists use virtualised rendering for smooth scrolling at scale.\n- **Command Palette** (`Cmd/Ctrl+K`): Quick-access command launcher.\n- **Keyboard shortcuts**: `g p` → Proxmox, `g d` → Docker, `g k` →\n  Kubernetes, `g n` → TrueNAS, `g v` → vSphere, `g s` → Machines, `g a` →\n  Alerts, `g r` → Patrol, `g t` → Settings, `/` → Search.\n\n### Mobile Experience\n- **MobileNavBar** component: Bottom tab bar for touch navigation.\n- **Relay integration**: The mobile app connects through the relay protocol for encrypted remote access.\n\n## 🔒 Security\n\n- **Encryption at Rest**: Sensitive config (passwords, API keys) encrypted with AES-GCM via `internal/crypto`. Each tenant can have its own encryption key.\n- **Scoped API Tokens**: Tokens carry explicit scopes (`monitoring:read`, `settings:write`, `ai:chat`, `ai:execute`, `docker:report`, etc.). Endpoints enforce scope checks before processing.\n- **RBAC**: Role-based access control via `pkg/auth` with file-backed persistence. Resources: `users`, `settings`, `monitoring`.\n- **SSO**: OIDC and SAML providers with per-provider configuration and multi-provider support.\n- **Agent Commands**: Disabled by default for security. Operators must opt in with `--enable-commands`.\n- **Audit Trail**: Every mutation logged to per-tenant SQLite databases with optional cryptographic signatures.\n- **Rate Limiting**: Per-endpoint and per-tenant rate limiting with configurable thresholds.\n- **CSRF Protection**: Token-based CSRF prevention on mutation endpoints.\n- **Recovery Mode**: Localhost-only endpoint for emergency auth recovery with time-limited tokens.\n\n## 🚀 Deployment\n\nPulse is distributed as:\n\n1. **Docker Container**: Multi-stage build producing a minimal image with the Go binary + embedded frontend.\n2. **Single Binary**: The frontend is compiled into the Go binary using `go:embed` (`internal/api/frontend_embed.go`), enabling single-file deployment.\n3. **Kubernetes Helm Chart**: For cluster deployments with configurable replicas and persistence.\n4. **Systemd Service**: For bare-metal or VM installations with `.env`-based configuration and `SIGHUP` reload support.\n5. **Proxmox LXC Helper Script**: One-line install inside a Proxmox helper-scripts container.\n","isInternal":false,"tokens":3376,"sizeBytes":14586},{"name":"API.md","path":"docs/API.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/docs/API.md","title":"docs - API Interface Contract & Specifications","category":"project-spec","format":"markdown","content":"# 🔌 Pulse API Reference\n\nPulse provides a comprehensive REST API for automation and integration.\n\n**Base URL**: `http://<your-pulse-ip>:7655/api`\n\n## 🔐 Authentication\n\nMost API requests require authentication via one of the following methods:\n\n### API Token (Recommended)\n\nCreate an API token in Pulse's **API Access** settings with only the scopes\nneeded for your task. The read-only example below requires `monitoring:read`.\nPass the token in the `X-API-Token` header, using a private header file so the\nsecret does not appear in shell history or process arguments.\n\nOn a trusted machine, as the account running curl, prepare the file and open\nit in an editor. This preserves an existing file, restricts its directory and\nstops before editing if preparation fails or a credential path is symlinked or\nnot a regular file:\n\n```bash\n(\n  set -eu\n  umask 077\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ]; then\n    printf 'Refusing a symlinked credential path.\\n' >&2\n    exit 1\n  fi\n  if [ -e \"$auth_file\" ] && [ ! -f \"$auth_file\" ]; then\n    printf 'Credential file must be a regular file.\\n' >&2\n    exit 1\n  fi\n  mkdir -p \"$auth_dir\"\n  chmod 700 \"$auth_dir\"\n  touch \"$auth_file\"\n  chmod 600 \"$auth_file\"\n  vi \"$auth_file\"\n)\n```\n\nIn the editor, save just this line, replacing `<token>` with the API token:\n\n```text\nX-API-Token: <token>\n```\n\nDefine this helper in the same Bash session (curl 7.76 or later). The examples\non this page use it to make **one request**, print only the HTTP status and save\neach response to a new owner-only file. This keeps resource names, audit details\nand credential-bearing error bodies out of terminal output and recordings.\n\n```bash\npulse_api() (\n  set -eu\n  umask 077\n  if [ \"$#\" -ne 2 ]; then\n    printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\\n' >&2\n    exit 2\n  fi\n  method=$1\n  api_path=$2\n  case \"$api_path\" in\n    /api/*) ;;\n    *) printf 'Use an API path on this Pulse instance, not a full URL.\\n' >&2; exit 2 ;;\n  esac\n  case \"$method\" in\n    GET) set -- ;;\n    POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;\n    PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;\n    DELETE) set -- --request DELETE ;;\n    *) printf 'This example helper accepts only GET, POST, PUT and DELETE.\\n' >&2; exit 2 ;;\n  esac\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ] || [ ! -f \"$auth_file\" ]; then\n    printf 'Prepare a regular, private header file first.\\n' >&2\n    exit 1\n  fi\n  chmod 700 \"$auth_dir\" || exit \"$?\"\n  chmod 600 \"$auth_file\" || exit \"$?\"\n  result_file=$(mktemp \"$auth_dir/api-response.XXXXXX\") || exit \"$?\"\n  curl_exit=0\n  status=$(curl --disable --fail-with-body --silent --show-error --globoff \\\n    --proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \\\n    --header \"@$auth_file\" --output \"$result_file\" --write-out '%{http_code}' \\\n    \"$@\" \"http://127.0.0.1:7655$api_path\") || curl_exit=$?\n  printf 'HTTP %s\\nPrivate response: %s\\n' \"$status\" \"$result_file\"\n  [ \"$curl_exit\" -eq 0 ] || exit \"$curl_exit\"\n  case \"$status\" in\n    2??) ;;\n    *) exit 1 ;;\n  esac\n)\n```\n\nThen make a read-only request with a `monitoring:read` token:\n\n```bash\npulse_api GET /api/state/summary\n```\n\nThe default loopback origin applies only when curl runs on the Pulse host and\nexplicitly bypasses proxy environment settings. For remote access, replace\n`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace\n`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate\nverification enabled; for a private CA, add `--cacert` with a CA file you\nverified separately, never `--insecure`. Do not put credentials in the URL.\n\nOpen the reported response file privately to inspect it, including on failure;\nshare only the relevant redacted error, not the whole response or file. Each\ncall preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit\nand retain the error body; redirects are not followed and are not success.\nThe connection limit is five seconds and the whole request limit is twenty\nseconds. A partial file after a transport error is not a complete result.\nNo request is retried automatically, especially a POST whose outcome is\nuncertain: inspect the existing action and target before deciding what to do.\nA 2xx response is only HTTP success, not proof that an action completed.\n\nThe helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).\nPOST and PUT read their JSON body from standard input; GET and DELETE send no\nbody. A method being supported is not permission to change a resource. Choose\neach administrative change separately and check its current state afterwards.\nA timeout or incomplete response can occur **after a change was applied**;\ndo not repeat a POST, PUT or DELETE merely because the response was lost.\n\nKeep `--globoff`: shell quotes alone do not disable curl's URL globbing. Without\nit, a path containing a brace list such as `{review,ops}` or a range such as\n`[1-3]` can send several requests, including several POST, PUT or DELETE\nrequests. The helper treats these characters literally, not as a batch of\ntargets. Still quote the whole path in the shell, and URL-encode dynamic resource\nIDs and query values; `--globoff` does not encode them for you.\n\nKeep `--disable` first: it ignores local curl configuration that could enable\ntrace output, bypass TLS or follow redirects. On an authentication-enabled\ninstance, the protected summary checks token access; `/api/health` is public\nand does not verify authentication. The helper is local example code, not an\ninstalled Pulse command; define it again in a new shell before using these\nexamples. Dispose of response files under your normal private-data policy.\n\nDo not paste tokens into command lines, URLs or issue reports. Keep the header\nfile private and outside shared repositories and diagnostics; do not use curl\nverbose/trace output when sharing a result. Share only the relevant redacted\nerror, not the whole infrastructure response. Revoke tokens that are no longer\nneeded in Pulse's API Access settings.\n\n### Bearer Token\n\nThe same API token can use a Bearer header instead. Replace the header file's\nline in the editor with the following, then use the same helper above;\ndo not send both authentication headers:\n\n```text\nAuthorization: Bearer <token>\n```\n\n### Session Cookie\n\nFor one-off read-only diagnostics, use your signed-in Pulse browser to open\nthe API path on that same instance. Do not extract or paste its session cookie\ninto a command or report; use the scoped token-file method above for automation.\nState-changing session requests also require Pulse's CSRF protection, which\nthe UI handles.\n\nSession endpoints:\n- `POST /api/login` (sets the session cookie, `pulse_session` over plain HTTP or `__Host-pulse_session` over HTTPS, plus `pulse_csrf`)\n- `POST /api/logout` (clears session)\n\nLogin body:\n```json\n{ \"username\": \"admin\", \"password\": \"secret\", \"rememberMe\": true }\n```\n\nPublic endpoints include:\n- `GET /api/health`\n- `GET /api/version`\n- `GET /api/agent/version` (agent update checks)\n- `GET /api/setup-script` (public; the generated script prompts for its setup token at runtime and skips auto-registration if none is supplied)\n\n## 🔏 Scopes and Admin Access\n\nSome endpoints require admin privileges and/or scopes. Common scopes include:\n- `monitoring:read`\n- `monitoring:write`\n- `settings:read`\n- `settings:write`\n- `agent:config:read`\n- `agent:manage`\n- `actions:plan`, `actions:approve`, `actions:execute`\n- `audit:read`\n\nEndpoints that require admin access are noted below.\n\nAdmin endpoints are instance administration unless they act only on the\nrequest organization. On instance administration an API token must not be\nbound to any organization other than the default one: a token bound to a\ntenant organization gets `403` whatever its scopes, as that organization's\nowner does. Endpoints that serve only the request organization, such as nodes\nand platform connections, agents, install commands, configuration transfer and\nreports, accept tokens bound to that organization with the listed scope.\n\n---\n\n## 📡 Core Endpoints\n\n### System Health\n`GET /api/health`\nCheck if Pulse is running.\n```json\n{\n  \"status\": \"healthy\",\n  \"timestamp\": 1700000000,\n  \"uptime\": 3600,\n  \"devModeSSH\": false\n}\n```\n\n### System State\n`GET /api/state`\nReturns the complete state of your infrastructure (Nodes, VMs, Containers, Storage, Alerts). This is the main endpoint used by the dashboard.\n\n`GET /api/state/summary`\nReturns a lightweight integration summary for external dashboards and checks. Requires `monitoring:read`.\n\n```json\n{\n  \"activeAlerts\": 1,\n  \"nodes\": 2,\n  \"vms\": 8,\n  \"containers\": 12,\n  \"dockerHosts\": [\n    {\n      \"name\": \"Docker Host\",\n      \"containers\": 5,\n      \"uptimeSeconds\": 86400,\n      \"cpuUsagePercent\": 12.5\n    }\n  ],\n  \"verdicts\": {\n    \"ok\": 17,\n    \"attention\": 1,\n    \"critical\": 1,\n    \"stale\": 1,\n    \"off\": 2,\n    \"unknown\": 0\n  },\n  \"attention\": [\n    {\n      \"id\": \"node-1\",\n      \"name\": \"PVE Node 1\",\n      \"type\": \"agent\",\n      \"platformType\": \"proxmox\",\n      \"verdict\": \"critical\",\n      \"topReason\": { \"code\": \"offline\" }\n    }\n  ],\n  \"lastUpdate\": \"2026-05-24T10:11:12Z\"\n}\n```\n\n`verdicts` uses the canonical `ok`, `attention`, `critical`, `stale`, `off`,\nand `unknown` fleet-health vocabulary. `attention` is severity ordered and\ncapped at 30 entries. Powered-off workloads are neutral (`off`), and stale or\nmissing telemetry is never reported as `ok`.\n\n### Simple Stats (HTML)\n`GET /simple-stats`\nLightweight HTML status page for quick checks.\n\n### Unified Resources\n`GET /api/resources`\nReturns the unified resource list with pagination + aggregations. Requires `monitoring:read`.\n\nQuery params:\n- `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `app-container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`)\n- `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`)\n- `excludeSource`: comma-separated list of sources that must not be present on a matching resource\n- `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`)\n- `parent`: parent resource ID\n- `cluster`: cluster name\n- `namespace`: Kubernetes namespace (filters Kubernetes resources only)\n- `q`: name search (contains match)\n- `tags`: comma-separated tags\n- `page`: page number (default `1`)\n- `limit`: page size (default `50`, max `100`)\n- `sort`: `name` (default), `status`, `type`, `lastSeen`\n- `order`: `asc` (default) or `desc`\n\nNote: `GET /api/resources` is optimized for list views. Some large, platform-specific fields may be omitted from the list response and are only returned by `GET /api/resources/{id}`.\n\nEach list resource can include a backend-owned `health` envelope with a\ncanonical `verdict` and stable `reasons` (`code` plus optional compact\n`detail`). Consumers should use this envelope for cross-platform posture\ninstead of deriving health from provider-specific status strings.\n\n#### Guest readings: availability and age\n\nIn legacy VM payloads, `disk.usage: -1` means unavailable, not negative usage.\nIn `GET /api/resources` and `GET /api/resources/{id}`, an unavailable disk\nreading is omitted as `metrics.disk`. **Do not replace an absent metric with\nzero**; a genuinely observed zero is valid. Allocated virtual-disk capacity\nalone does not establish used space inside the guest.\n\nRead `proxmox.diskStatusReason` alongside the value. A `prev-` reason means\nretained disk evidence, not a fresh reading. `agent-not-running` can also\nrepresent a general guest-agent HTTP 500 error: it does not establish that the\nguest's service is absent or stopped. An absent reason alone does not prove\nfreshness either.\n\nFor memory, use `metrics.memory.observation` for the **selected metric's**\nsource and age, not a different source's retained `proxmox.memory` facet:\n\n| Observation `state` | Meaning for the consumer |\n| --- | --- |\n| `current` | Accepted source observation; a normal cache hit can keep the original `observedAt`. Check its age for your use. |\n| `last-known` | Retained evidence, not a new measurement. Keep its original age visible; do not record it as a fresh sample. |\n| `unavailable` | No usable usage observation; do not treat retained numbers or zero-valued fields as a measurement. |\n| Missing or unrecognised | Freshness is unknown, including older payloads without an observation. Do not infer `current`. |\n\n`observedAt` belongs to that source observation. If it is absent or in the\nfuture, its age is unknown; do not substitute the resource's `lastSeen` or\n`updatedAt`. In the raw `proxmox.memory` facet, `usageUnavailable: true` can\npreserve known capacity without measured usage. An absent `metrics.memory`\nis not 0% usage. Conversely, an independently observed PVE or Pulse-agent\nmemory reading can remain current while QEMU disk collection is deferred;\ndisk state is not memory provenance.\n\nA running VM, an advancing row timestamp or an OK backup does not prove fresh\nguest readings or successful thaw. Do not install or restart an agent or send\nlive guest-agent probes merely to fill these gaps, especially during a backup,\nfreeze/thaw or an unresponsive-guest incident. Follow [Backup safety](VM_DISK_MONITORING.md#backup-safety)\nand [missing-reading guidance](VM_DISK_MONITORING.md#a-missing-reading-is-not-an-installation-diagnosis).\nRecovery needs independent thaw, fresh successful writes to every covered\nfilesystem and workload liveness; retained API values cannot establish it.\n\nAvailability is an additive resource facet. `availability` is the compatibility\nsummary used by existing clients; `availabilityChecks` contains every check\nattached to the resource. Each check can include `correlationState`\n(`attached`, `standalone`, `ambiguous`, or `unresolved`), its correlation\nrule/reason/candidate count, and an `evidence` envelope with observation and\nvalidity timestamps. Attached targets also add a `checks` relationship and do\nnot appear as separate `network-endpoint` rows.\n\n`GET /api/resources/stats`\nReturns aggregations (counts + health rollups).\n\n`GET /api/resources/k8s/namespaces?cluster=<clusterName>`\nReturns namespace-level rollups (pods + deployments) for a Kubernetes cluster. Requires `monitoring:read`.\n```json\n{\n  \"cluster\": \"prod-k8s\",\n  \"data\": [\n    {\n      \"namespace\": \"default\",\n      \"pods\": { \"total\": 12, \"online\": 10, \"warning\": 2, \"offline\": 0, \"unknown\": 0 },\n      \"deployments\": { \"total\": 3, \"online\": 3, \"warning\": 0, \"offline\": 0, \"unknown\": 0 }\n    }\n  ]\n}\n```\n\n`GET /api/resources/{id}`\nFetch a single resource by ID.\n\n`GET /api/resources/{id}/children`\nReturns child resources for the parent ID.\n\n`GET /api/resources/{id}/metrics`\nReturns the resource metrics payload.\n\n`POST /api/resources/{id}/link` (`monitoring:write`)\nManually link two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/unlink` (`monitoring:write`)\nManually unlink two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/report-merge` (`monitoring:write`)\nReport an incorrect merge (creates exclusions).\n```json\n{ \"sources\": [\"proxmox\", \"agent\"], \"notes\": \"optional note\" }\n```\n\n### Resource Maintenance and Operator State\n\nUse a timed maintenance window to suppress alerts during planned work on a\nresource. This API, including descendant scope, is available in stable v6.4.1.\nChoose the resource's `id` from `GET /api/resources` and URL-encode it in the\nrequest path. Do not substitute a display name or a Proxmox VM number.\n\n| Endpoint | Required scope | Result |\n| --- | --- | --- |\n| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |\n| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |\n| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |\n\n**PUT replaces the whole record, not just the fields supplied.** Read the current\nstate first and preserve unrelated settings, such as monitoring mode, lifecycle,\nremediation policy, criticality and notes. Treat only `operator_state_not_set`\nas an empty record, and stop on other read errors. Coordinate concurrent writers\nso a read followed by a PUT does not overwrite someone else's changes.\n\nFor a one-time window, merge these fields into that record, replacing the example\ntimes with your intended start and end:\n\n```json\n{\n  \"maintenanceStartAt\": \"2026-10-15T20:00:00Z\",\n  \"maintenanceEndAt\": \"2026-10-15T20:30:00Z\",\n  \"maintenanceScope\": \"resource_and_descendants\",\n  \"maintenanceReason\": \"Proxmox node maintenance\"\n}\n```\n\nThis is a set of fields to merge, not a complete replacement for an existing\nrecord. Remove any `maintenanceRecurrence` when replacing a recurring schedule\nwith this one-time window. The API rejects a record containing both.\n\n| Field | Meaning |\n| --- | --- |\n| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |\n| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |\n| `maintenanceReason` | Optional explanation of the planned work. |\n\nThe response includes `maintenanceWindowActive` and, while active,\n`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the\nrecord's `setAt` and `setBy` fields from the request, ignoring client values.\n\nDuring an active window, matching new alerts and firing/recovery notifications\nare suppressed. Applying an active window also clears matching existing alerts\nfrom the active list, retaining history. This is broader than pausing delivery\nfor an existing incident. Resources outside the scope remain monitored normally.\n\nThe window expires automatically, with no re-enable request needed. Subsequent\nobservations can raise alerts again, subject to normal alert policy and any other\nactive maintenance window. Previously cleared alerts are not automatically\nrestored, and expiry does not replay a backlog of maintenance notifications.\n\nTo end maintenance early, read the latest record, remove its maintenance fields\nand PUT the remaining state back. Do not DELETE the record unless you also intend\nto remove its other operator settings. An inherited window must be changed on\nthe ancestor that owns it.\n\n### Fleet Connections\n`GET /api/connections`\nReturns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.\n\nThe payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.\n\nDefine the private-response helper in [Authentication](#-authentication),\nwith `settings:read` on the token:\n\n```bash\npulse_api GET /api/connections\n```\n\n### Unified Action Planning\n\nThe existing action API separates planning, approval and execution. Run these\nsteps separately, inspecting each response before moving on; do not paste the\nwhole sequence as an unattended recovery script. Use the private header file\nand `pulse_api` helper from [Authentication](#-authentication), not a token in a\ncommand or environment assignment. Inspect each reported private response file\nbefore proceeding. For remote access, change the helper as described there,\nusing your Pulse HTTPS origin with certificate verification enabled.\n\n| Endpoint | Token scope and access |\n| --- | --- |\n| `GET /api/agent/resource-capabilities/{id}` | `monitoring:read`; lists the resource's advertised capabilities and parameter schemas. |\n| `POST /api/actions/plan` | `actions:plan` and permission to plan actions; creates a plan but does not approve or execute it. |\n| `POST /api/actions/{id}/decision` | `actions:approve` and permission to approve actions; records an explicit `approved` or `rejected` decision for a pending action, without executing it. |\n| `POST /api/actions/{id}/execute` | `actions:execute` and permission to execute actions; dispatches only an approved action or an approval-free executable plan. Dry-run-only plans cannot execute. |\n| `GET /api/audit/actions` | `audit:read`, audit-log read permission and licensed audit logging. |\n| `GET /api/audit/actions/{id}/events` | The same audit access; returns the action's lifecycle evidence. |\n\nThe legacy `ai:execute` scope also permits planning, approval and execution,\nbut use the narrower action scopes when possible. Approval and execution tokens\nmust be bound to an authorised user; a scope alone does not bypass approval\npolicy, separation of duties or step-up requirements. Audit reads through a\nbrowser session additionally require admin access. A `monitoring:read` token is\nnot an action-control or audit token.\n\n1. **Inspect capabilities.** Choose the canonical resource `id` from\n   `GET /api/resources`, not a display name or a VM number. Replace `vm:42`\n   throughout these examples and URL-encode it in request paths (`vm%3A42` here).\n   An empty capabilities list means there is nothing to plan for that resource.\n\n```bash\npulse_api GET /api/agent/resource-capabilities/vm%3A42\n```\n\n2. **Plan only.** Use an advertised capability and its actual parameter schema;\n   `restart` and `mode=graceful` are examples, not universal capabilities. Choose\n   a request ID for this specific intent. Pulse derives the actor from the\n   authenticated credential, not a caller-supplied `requestedBy` value.\n\n```bash\npulse_api POST /api/actions/plan <<'JSON'\n{\n  \"requestId\": \"manual-recovery-123\",\n  \"resourceId\": \"vm:42\",\n  \"capabilityName\": \"restart\",\n  \"params\": { \"mode\": \"graceful\" },\n  \"reason\": \"Recover after confirmed outage\"\n}\nJSON\n```\n\nInspect the returned approval policy, blast radius, expiry and preflight checks.\nKeep its `actionId` and reviewed `planHash`; replace `act_...` and `sha256:...`\nbelow with those returned values. Planning is not proof that execution is safe\nor currently available.\n\n3. **Decide only if approval is required.** Use an authorised approver's private\n   header file. Approve only the reviewed plan; stop on a refusal or stale-plan\n   response rather than weakening the policy or creating another recovery action.\n\n```bash\npulse_api POST /api/actions/act_.../decision <<'JSON'\n{\n  \"outcome\": \"approved\",\n  \"reason\": \"Inside maintenance window\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\n4. **Execute separately.** Only after reviewing a successful decision, or an\n   approval-free executable plan, use the executor's authorised header file:\n\n```bash\npulse_api POST /api/actions/act_.../execute <<'JSON'\n{\n  \"reason\": \"Execute approved recovery\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\nIf the response is lost or times out, inspect the existing action and target\nbefore retrying: losing the connection does not prove that execution stopped.\nAn HTTP success is not a substitute for checking the terminal result and the\nresource's observed state.\n\n5. **Read the audit and events**, using a private header file with audit access:\n\n```bash\npulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'\n```\n\n```bash\npulse_api GET /api/audit/actions/act_.../events\n```\n\nWhen audit logging is unavailable, use Pulse's action detail in the signed-in\nUI to inspect the existing action; do not infer success or repeat execution\nfrom an unavailable audit response.\n\nResponse:\n```json\n{\n  \"actionId\": \"act_...\",\n  \"requestId\": \"manual-recovery-123\",\n  \"allowed\": true,\n  \"requiresApproval\": true,\n  \"approvalPolicy\": \"admin\",\n  \"predictedBlastRadius\": [\"vm:42\", \"node-1\"],\n  \"rollbackAvailable\": false,\n  \"message\": \"Plan created for restart on web-42. Execution requires admin approval and is not performed by this endpoint.\",\n  \"plannedAt\": \"2026-05-03T10:00:00Z\",\n  \"expiresAt\": \"2026-05-03T10:05:00Z\",\n  \"resourceVersion\": \"resource:sha256:...\",\n  \"policyVersion\": \"policy:sha256:...\",\n  \"planHash\": \"sha256:...\",\n  \"preflight\": {\n    \"target\": \"vm:42\",\n    \"currentState\": \"web-42 is warning\",\n    \"intendedChange\": \"Restart the VM\",\n    \"dryRunAvailable\": false,\n    \"dryRunSummary\": \"No provider-supported dry run is advertised for this capability.\",\n    \"safetyChecks\": [\n      \"Resource was resolved from the unified resource registry.\",\n      \"Capability is advertised by the resource contract.\",\n      \"This endpoint plans only; it does not approve or execute the action.\",\n      \"Execution requires admin approval.\"\n    ],\n    \"verificationSteps\": [\n      \"Refresh the resource and confirm the expected state after execution.\",\n      \"Review /api/audit/actions/{actionId}/events for lifecycle evidence.\"\n    ],\n    \"generatedAt\": \"2026-05-03T10:00:00Z\"\n  }\n}\n```\n\n### Resource Metadata\nUser notes, tags, and custom URLs for resources.\n\nThese routes do not require admin access. Any authenticated browser session can call them, and API tokens need the scope shown.\n\n- `GET /api/agents/metadata` (`monitoring:read`)\n- `GET /api/agents/metadata/{agentId}` (`monitoring:read`)\n- `PUT /api/agents/metadata/{agentId}` (`monitoring:write`)\n- `DELETE /api/agents/metadata/{agentId}` (`monitoring:write`)\n\n- `GET /api/guests/metadata` (`monitoring:read`)\n- `GET /api/guests/metadata/{guestId}` (`monitoring:read`)\n- `PUT /api/guests/metadata/{guestId}` (`monitoring:write`)\n- `DELETE /api/guests/metadata/{guestId}` (`monitoring:write`)\n\n- `GET /api/docker/metadata` (`monitoring:read`)\n- `GET /api/docker/metadata/{containerId}` (`monitoring:read`)\n- `PUT /api/docker/metadata/{containerId}` (`monitoring:write`)\n- `DELETE /api/docker/metadata/{containerId}` (`monitoring:write`)\n\n- `GET /api/docker/runtimes/metadata` (`monitoring:read`)\n- `GET /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:read`)\n- `PUT /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n- `DELETE /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n\n### Version Info\n`GET /api/version`\nReturns version, build time, and update status.\nExample response:\n```json\n{\n  \"version\": \"6.0.0\",\n  \"buildTime\": \"2026-02-21T00:00:00Z\",\n  \"channel\": \"stable\",\n  \"deploymentType\": \"systemd\",\n  \"updateAvailable\": false,\n  \"latestVersion\": \"6.0.0\"\n}\n```\nVersion fields are returned as plain semantic versions (no leading `v`).\n\n---\n\n## 🖥️ Nodes & Config\n\n### Public Config\n`GET /api/config`\nReturns a small public config payload (update channel, auto-update enabled).\n\n### List Nodes\n`GET /api/config/nodes` (admin, `settings:read`)\n\nThis administrative inventory covers saved **PVE, PBS and PMG connections**,\nnot individual VMs or agents. Keep its response private. Use the returned\nconnection `id` for updates, deletion and tests; a display name, VMID or another\ninstallation's node name is not that ID. Re-read the inventory before a change\nrather than guessing an ID from a list position.\n\n### Add Node\n`POST /api/config/nodes` (admin, `settings:write`)\n\nAdding a connection saves credentials and enables collection; it is not a\npassive validation step. Use an existing dedicated, least-privilege platform\ntoken. The **Pulse API token** in the private header file authorises this\nconfiguration request; the **Proxmox token** in the JSON body authorises reads\nfrom the monitored platform. They are different credentials. Do not use a root\npassword or widen a department-scoped token to make denied peer reads succeed;\nfollow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).\nFor PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)\nand [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can\nalso try to create a token and ACLs on the PBS server; it is not read-only.\n\nPrepare the request in a private editor, not a credential-bearing shell\nassignment, here-document or command argument. Use the same Bash session as\nthe [private-response helper](#api-token-recommended):\n\n```bash\nnode_request_dir=$(umask 077; mktemp -d \"${TMPDIR:-/tmp}/pulse-node.XXXXXX\") &&\nnode_request_file=\"$node_request_dir/node.json\" &&\n(umask 077; set -C; : > \"$node_request_file\") &&\nvi \"$node_request_file\"\n```\n\nIf preparation or the editor fails, stop. In that editor, enter the following\nJSON, replacing the example hostname, connection name, full platform token ID\n(`user@realm!token`) and token-secret placeholder with your intended values:\n\n```json\n{\n  \"type\": \"pve\",\n  \"name\": \"PVE connection A\",\n  \"host\": \"https://pve.example.net:8006\",\n  \"tokenName\": \"pulse-monitor@pve!pulse-readonly\",\n  \"tokenValue\": \"<platform-token-secret>\",\n  \"verifySSL\": true\n}\n```\n\nSet `verifySSL` explicitly to `true`: omission on these new-connection and\ntest request paths does **not** enable certificate verification. The example\nrequires a certificate trusted by the Pulse server for that hostname. For a\nself-signed/private certificate, also supply `fingerprint` with the SHA-256\ncertificate fingerprint verified through an independent trusted channel.\nA fingerprint merely fetched from the endpoint is not established trust.\nDo not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to\nwork around a certificate failure.\n\nAfter reviewing this one intended configuration change, send the file:\n\n```bash\npulse_api POST /api/config/nodes < \"$node_request_file\"\n```\n\nThe helper retains the response privately and makes one bounded request,\nwithout retries. A lost response can follow a successful save: re-read the\nsaved inventory before deciding whether another request is needed. HTTP success\ndoes not establish fresh readings for every node, guest, backup or History view.\nKeep both request and response files out of reports and shared repositories;\nremove the temporary request under your credential policy when no longer needed.\n\n### Test Connection\n`POST /api/config/nodes/test-connection` (admin, `settings:write`)\n\nTests supplied credentials with live platform requests without saving the\nconnection. Use the same private JSON-file transport and explicit TLS settings,\nonly when a live connection test is appropriate; it is not a required precursor\nto filing a report. Success covers the reads performed by that test, not all\ncollection permissions, guest recovery or sustained monitoring.\n\n### Test Node Config (No Save, Live Request)\n`POST /api/config/nodes/test-config` (admin, `settings:write`)\n\nThis also contacts the platform using the supplied credentials; **no save**\ndoes not mean offline schema validation. Do not run either test merely to\ncollect new evidence during a freeze/thaw, backup or unresponsive-host incident.\nUse existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).\n\n### Update Node\n`PUT /api/config/nodes/{id}` (admin, `settings:write`)\n\nReview the current connection and submit only the intended supported changes\nfrom a private JSON file. Do not copy masked credentials from a GET response\nback as new secrets. Credential rotation and trust changes are separate\ndecisions; preserve the existing credential and fingerprint when not changing\nthem. A response failure does not prove the update was unapplied.\n\n### Delete Node\n`DELETE /api/config/nodes/{id}` (admin, `settings:write`)\n\nRemoves the Pulse connection, not the platform workloads or the upstream token.\nCollection and related alert coverage can stop. Check the exact connection and\narrange any needed independent coverage before an intentional removal; do not\ndelete and re-add a connection to diagnose missing readings or identity mixing.\n\n### Test Node (Legacy)\n`POST /api/config/nodes/{id}/test` (admin, `settings:write`)\n\nUses the saved connection for a live test. It is not a passive read of its\nlast result or a guarantee that ordinary collection recovered.\n\nAn optional JSON body can override `verifySSL` and `fingerprint` for this test\nwithout changing the saved connection or credentials. Omitted fields keep their\nsaved values. Send `false` to disable CA verification or `\"\"` to clear the saved\nfingerprint for the test. A nonempty fingerprint remains enforced regardless of\n`verifySSL`. Empty-body requests keep the saved TLS settings. For example,\n`{\"verifySSL\":true,\"fingerprint\":\"<SHA-256 fingerprint>\"}` tests an edited pin.\n\n### Refresh Cluster Nodes\n`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)\n\nActively rediscovers a saved PVE connection's cluster membership. It does not\nselect authorised nodes, grant missing permissions or repair an unrelated\nstandalone installation. Keep intentional token restrictions intact.\n\n### Export Configuration\n`POST /api/config/export` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:read`)\nRequest body:\n```json\n{ \"passphrase\": \"use-a-strong-passphrase\" }\n```\nReturns an encrypted export bundle in `data`. Passphrases must be at least 12 characters.\n\n### Import Configuration\n`POST /api/config/import` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:write`)\nRequest body:\n```json\n{\n  \"data\": \"<exported-bundle>\",\n  \"passphrase\": \"use-a-strong-passphrase\"\n}\n```\n\n---\n\n## 🧭 Setup & Discovery\n\n### Setup Script (Public)\n`GET /api/setup-script`\nReturns the Proxmox/PBS setup script as a shell-script download. Accepts an\noptional legacy `setup_token` query for compatibility. Current downloads\ncontain no token: paste the separately revealed token only at the silent\nterminal prompt, or supply `PULSE_SETUP_TOKEN_FILE` pointing to a mode-0600\nregular file in a mode-0700 directory owned by the script's user. Never put a\ntoken in a copied command or URL. Canonical callers must send a supported `type` of `pve` or\n`pbs` plus non-empty `host` and `pulse_url`; the route no longer generates\nplaceholder-host scripts for later repair or reconstructs Pulse identity from\nthe request origin. The route now shares the same canonical type boundary as\n`/api/setup-script-url`, rejecting unsupported node types instead of treating\nunknown values as PBS, and it normalizes the supplied `host` before script\ngeneration so downloaded artifacts and rerun URLs preserve the same canonical\nnode identity as the bootstrap response. The optional `backup_perms=true`\nquery is supported only for `type=pve`.\n\n### Setup Script URL\n`POST /api/setup-script-url` (admin, `settings:write`)\nGenerates a one-time setup token and URL for `/api/setup-script`.\nCanonical callers must send a supported `type` of `pve` or `pbs` plus a\nnon-empty `host`; the backend normalizes that host before minting the setup\ntoken and now returns the canonical bootstrap identity back in the response as\n`type`, `host`, `url`, `downloadURL`, `scriptFileName`, `command`, `commandWithEnv`,\n`commandWithoutEnv`, `setupToken`, `tokenHint`, and `expires`. The returned\ncommands are canonical root-or-sudo `curl -fsSL` bootstrap commands for the\ngenerated setup script, while `url`, `downloadURL`, and `scriptFileName` are\nthe runtime-owned artifact metadata used by copy and manual download surfaces.\nThe request body is a single canonical JSON object only; unknown fields and trailing JSON are\nrejected as invalid request shape, and `backupPerms:true` is supported only for\n`type:\"pve\"`. This route stays on the normal\nauthenticated bootstrap boundary: when Pulse auth is already configured it\nrequires a real authenticated session or API token, and setup tokens do not\nauthorize the request itself. Pulse-managed Proxmox monitor-token names on the\nsetup/bootstrap path derive from the canonical Pulse endpoint, not request-local\nhost fallbacks, so setup-script and turnkey node-add flows stay on one\ndeterministic `pulse-<canonical-scope-slug>` identity per Pulse instance.\nThe `command`, `commandWithEnv`, and `commandWithoutEnv` fields now contain\nidentical credential-free commands. They download the complete script before\nrunning it and prompt silently in the root-or-sudo process. The token crosses\nthe installer boundary through a private file, not process arguments or an\nexported secret. `downloadURL` equals the tokenless `url`, so a manual download\nalso needs the separately revealed token at runtime. `setupToken` is used for\n`/api/auto-register`; `tokenHint` remains masked on the setup page. Settings\nreveals the token in a separate dialog: run the command first, then copy and\npaste the token only at its prompt. The artifact is reused only for the same\nhost and options while its five-minute expiry is live, and discarded when the\nsetup modal closes.\nNon-frontend consumers must validate the complete artifact, including the\ncanonical host, type, URLs, filename, masked hint and live expiry. Current\nUnified Agents and the shell installer also accept the coherent older-server\nartifact during upgrades, but never execute its command text. For new\nProxmox agent enrolment against a newer server, use its current installer;\nalready enrolled agents keep reporting normally.\n\n### Auto-Register (Public)\n`POST /api/auto-register`\nRegisters a node through the canonical `/api/auto-register` contract using a temporary\nsetup token carried in the JSON `authToken` field. Canonical callers must send\na supported `type` of `pve` or `pbs`, an explicit `source` marker of `agent` or\n`script`, a canonical Pulse-managed `tokenId` in the form\n`pulse-monitor@{pve|pbs}!pulse-<canonical-scope-slug>` matching the requested\ntype, an explicit `serverName`, and missing-token requests now fail with\n`Pulse setup token required`. Incomplete token completion requests now fail\nwith `tokenId and tokenValue must be provided together`, and other missing\ncanonical request fields fail with explicit `Missing required canonical\nauto-register fields: ...` guidance.\nSuccess responses now carry the canonical stored identity and caller boundary\nback to the installer or runtime-side Unified Agent:\n`{\"status\":\"success\",\"action\":\"use_token\",\"type\":\"pve|pbs\",\"source\":\"agent|script\",\"host\":\"https://...\",\"nodeId\":\"<stored-name>\",\"nodeName\":\"<stored-name>\",...}`.\n\n### Agent Install Command\n`POST /api/agent-install-command` (admin, `settings:write`)\nGenerates an API token and install command for agent-based Proxmox setup.\n\n### Discovery\n`GET /api/discover` (admin, `settings:write`)\nReturns the last cached network discovery result from the background discovery service. It does not scan.\n\n`POST /api/discover` (admin, `settings:write`)\nRuns a manual network scan. JSON body: `{\"subnet\": \"...\", \"use_cache\": false}`. `subnet` defaults to `auto`; `use_cache: true` returns the cached result instead of scanning.\n\n### Test Notification\n`POST /api/test-notification` (admin, `settings:write`)\nBroadcasts a WebSocket test event.\n\n---\n\n## 📊 Metrics & Charts\n\n### Chart Data\n`GET /api/charts?range=1h`\nReturns time-series data for CPU, Memory, and Storage.\n**Ranges**: `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `24h`, `7d`\n\n### Storage Charts\n`GET /api/storage-charts`\nReturns storage chart data.\n\n### Storage Stats\n`GET /api/storage/{storageId}` (`monitoring:read`)\nReturns one storage resource by its `id`, wrapped as `{\"data\": {...}, \"timestamp\": 1700000000}`. The `data` object includes `id`, `name`, `node`, `instance`, `type`, `status`, `total`, `used`, `free`, `usage`, `content`, `shared`, `enabled` and `active`. Returns HTTP 404 with code `storage_not_found` for an unknown ID, and HTTP 405 for methods other than GET. A request without an ID (`GET /api/storage/`) returns HTTP 400 with code `missing_storage_id`.\n\n### Recovery (formerly Backups / Snapshots)\nPulse v6 uses the recovery API to provide a platform-agnostic view of backup and snapshot artifacts.\nThe endpoints below carry the provider-neutral contract covering subjects,\npoints, rollups, posture, and filter semantics.\n\n- `GET /api/recovery/points`\n  - Query params:\n    - Core filters: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`\n    - Time window: `from` (RFC3339), `to` (RFC3339)\n    - Paging: `page`, `limit`\n    - Normalized filters: `q`, `cluster`, `node`, `namespace`, `scope=workload`, `verification` (`verified` | `unverified` | `unknown`)\n- `GET /api/recovery/rollups`\n  - Query params: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`, `from` (RFC3339), `to` (RFC3339), `page`, `limit`\n- `GET /api/recovery/postures`\n  - Returns server-derived per-resource protection posture and provider evidence quality.\n  - Query params: repeated `resourceId` values (maximum 200), `state` (`protected` | `attention` | `unprotected` | `unknown`), `page`, `limit` (maximum 200)\n  - Batch clients must make one bounded request per 200 resource ids, never one request per table row.\n  - Unknown identity, permission, history, or collection completeness remains `unknown`; clients must not infer a healthier state from raw backup or snapshot artifacts.\n- `GET /api/recovery/series`\n  - Returns per-day counts for the activity chart.\n  - Query params: same filters as `/api/recovery/points` (except paging), plus `tzOffsetMinutes` (integer; UTC offset minutes for day bucketing)\n- `GET /api/recovery/facets`\n  - Returns distinct filter values (clusters/nodes/namespaces) and capability flags (size/verification/entity id present).\n  - Query params: same filters as `/api/recovery/points` (except paging)\n\n---\n\n## 🔔 Notifications\n\n### Send Test Notification\n`POST /api/notifications/test` (admin, `settings:write`)\nSends one test notification through a single channel per request, not all configured channels. The JSON body `method` (or `type`) selects `email`, `webhook`, or `apprise` and uses the saved configuration. `webhookId` targets one specific saved webhook. `config` tests unsaved email or Apprise settings. If alert delivery is paused, the response includes `deliveryPaused: true`.\n\n### Email, Apprise, and Webhooks\n- `GET /api/notifications/email` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/email` (admin, `settings:write`)\n- `GET /api/notifications/apprise` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/apprise` (admin, `settings:write`)\n- `GET /api/notifications/webhooks` (admin, `settings:read`; `settings:write` also accepted)\n- `POST /api/notifications/webhooks` (admin, `settings:write`)\n- `PUT /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `DELETE /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `POST /api/notifications/webhooks/test` (admin, `settings:write`)\n- `GET /api/notifications/webhook-templates` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/webhook-history` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/email-providers` (admin, `settings:read`; `settings:write` also accepted)\n\nEmail and webhook configurations accept optional `tagFilter` (an array of\nresource-tag strings) and `tagFilterMode` (`\"all\"` or `\"any\"`). An omitted\nfield is preserved on update; an empty `tagFilter` clears routing and restores\ndelivery for all resources.\n\n- `GET /api/notifications/health` (admin, `settings:read`; `settings:write` also accepted)\n  - Queue health is `degraded` whenever any retained `failed` or `dlq`\n    delivery exists, and `unavailable` when queue state cannot be read.\n    Recoverable failed attempts that returned to `pending` for retry do not\n    degrade health.\n  - `queue.attention_required` is the retained terminal-failure count.\n    `reason_codes` identifies failed and/or dead-letter state without exposing\n    notification content. Counts are retention-bounded: sent, failed, and\n    cancelled rows are retained for 7 days; dead-letter rows for 30 days.\n\n### Audit Webhooks (Pro)\n- `GET /api/admin/webhooks/audit` (admin, `settings:read`)\n- `POST /api/admin/webhooks/audit` (admin, `settings:write`)\n  - Body: `{ \"urls\": [\"https://...\"] }`\n  - Strict JSON contract: unknown fields and trailing payload are rejected.\n  - Maximum `20` webhook URLs per update request.\n  - URLs are normalized (trimmed) and duplicate entries are ignored.\n  - Endpoint fails closed if URL validation runtime is unavailable.\n\n### Advanced Reporting (Pro)\n\nReport generation uses `settings:read`. For single-resource and fleet PDF\nnarration, an API token also needs `ai:execute` (or full access). Without it,\nthe report still succeeds with a deterministic summary and no AI provider call;\na single-resource PDF also explains the missing scope. Browser sessions and\nserver-run schedules keep their existing narration behaviour. AI budgets still\napply. GET and CSV never use AI narration.\n\n- `GET /api/admin/reports/catalog` (admin, `settings:read`)\n  - Returns the canonical reporting catalog for the settings surface, including locked-state teaser copy, enabled-surface guidance copy, performance-report options, canonical single-report filename subject, canonical fallback filename date style, and the nested VM inventory export definition.\n  - Metadata route: readable without the `advanced_reporting` feature so locked admin surfaces can render the same reporting definition before upsell.\n- `POST /api/admin/reports/generate` (admin, `settings:read`)\n  - Body fields: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default title for that resource report.\n  - When narration is permitted and Pulse Assistant is configured, the configured AI provider narrates a PDF's executive summary; each narration counts against the AI budget and appears in AI usage. If the call fails, times out or the budget is spent, the PDF falls back to the deterministic summary. CSV output is never narrated. The settings UI uses this method.\n  - API-token callers also need the `ai:execute` scope for AI narration. With only `settings:read` the report is still produced, with the deterministic summary and a note saying why.\n- `GET /api/admin/reports/generate` (admin, `settings:read`)\n  - Takes the same fields as query params and returns the same report, except that it never calls an AI provider: a PDF carries the deterministic summary and a note saying AI narration needs `POST`. Other methods return `405` with `Allow: GET, POST`.\n- `POST /api/admin/reports/generate-multi` (admin, `settings:read`)\n  - Body fields: `resources` (1-50 entries of `{resourceType,resourceId}`), `format`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default fleet report title.\n  - AI narration of the fleet summary follows the same rule as single reports: API-token callers need `ai:execute`, otherwise the deterministic summary is used. CSV output is never narrated.\n- `GET /api/admin/reports/inventory/vms/export` (admin, `settings:read`)\n  - Query params: `format` (`csv` only; optional and defaults to `csv`)\n  - Exports the current fleet-wide VM inventory as spreadsheet-friendly CSV using the canonical runtime model.\n\nValidation and limits:\n- `start` and `end` must be RFC3339 when provided.\n- Malformed `start`/`end` values, `end` not strictly after `start`, or report windows over 366 days return `400 invalid_time_range`.\n- `metricType` must match `[a-zA-Z0-9._:-]+` and be <= 64 chars, otherwise `400 invalid_metric_type`.\n- `title` must be <= 256 chars, otherwise `400 invalid_title`.\n- Report body max size is 1MB; oversized payloads return `400 body_too_large`.\n- Report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.\n- VM inventory export only accepts `csv`.\n\nCommon reporting error codes:\n- `invalid_format`, `missing_params`, `invalid_resource_type`, `invalid_resource_id`\n- `invalid_metric_type`, `invalid_title`, `invalid_time_range`\n- `no_resources`, `too_many_resources`, `body_too_large`, `invalid_body`\n\n### Queue and Dead-Letter Tools\n- `GET /api/notifications/delivery-log?limit=200` (admin, `settings:read`)\n  - Returns recent per-attempt delivery evidence with destination identifiers,\n    alert identifiers, outcomes, timestamps, failure classes, and redacted error\n    text. Completed attempts are retained for 7 days; dead-letter attempts are\n    retained for 30 days, so the response reports both windows explicitly.\n  - `limit` defaults to 50 and is capped at 200.\n- `GET /api/notifications/queue/stats` (admin, `settings:read` and `monitoring:read`)\n  - Returns counts for all rows still retained by the queue. Status counts have\n    different retention windows and are not a delivery rate or lifetime total.\n- `GET /api/notifications/dlq` (admin)\n- `POST /api/notifications/dlq/retry` (admin)\n- `POST /api/notifications/dlq/delete` (admin, `settings:write`; `DELETE` also accepted)\n  - Body: `{ \"id\": \"<notification id>\" }`. Marks that queue row `cancelled`\n    with the note \"Manually deleted from DLQ\"; the row is not removed and is\n    retained for 7 days like other cancelled rows. The endpoint does not check\n    that the row is in the dead-letter queue.\n  - Returns `{ \"success\": true, \"message\": \"Notification deleted from DLQ\",\n    \"id\": \"<notification id>\" }`. An empty or missing `id`, or an invalid JSON\n    body, returns `400`. An unknown `id` returns `500`.\n- `POST /api/notifications/terminal-failures/retry` (admin, `settings:write`)\n  - Returns every retained `failed` or ","isInternal":false,"tokens":12099,"sizeBytes":50018},{"name":"scenarios.json","path":"tests/integration/evals/scenarios.json","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/scenarios.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"{\n  \"scenarios\": [\n    {\n      \"id\": \"multi-tenant\",\n      \"name\": \"Multi-tenant Isolation\",\n      \"description\": \"Validates org lifecycle, switching, and isolation boundaries.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/03-multi-tenant.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_MULTI_TENANT_ENABLED\": \"true\",\n          \"PULSE_E2E_ENTITLEMENT_PROFILE\": \"multi-tenant\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/multi-tenant.md\"\n      }\n    },\n    {\n      \"id\": \"retired-trial-acquisition\",\n      \"name\": \"Retired Trial Acquisition\",\n      \"description\": \"Ensures self-hosted Pulse no longer initiates hosted Pro trial acquisition from the ordinary runtime: POST /api/license/trial/start returns 404 and preserves local entitlements.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/07-retired-trial-acquisition.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/retired-trial-acquisition.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-hosting\",\n      \"name\": \"Pulse Cloud Hosted Signup\",\n      \"description\": \"Validates public cloud signup, Stripe sandbox checkout completion, and magic-link request behavior.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/08-cloud-hosting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-hosting.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-billing-lifecycle\",\n      \"name\": \"Cloud Billing Lifecycle\",\n      \"description\": \"Validates post-checkout cloud lifecycle transitions (active to canceled) through Stripe + webhook processing.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/09-cloud-billing-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-billing-lifecycle.md\"\n      }\n    },\n    {\n      \"id\": \"truenas-node-add\",\n      \"name\": \"TrueNAS Node Addition\",\n      \"description\": \"Validates TrueNAS connection add, pool/dataset discovery, and unified resource visibility.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/02-node-add-truenas.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_E2E_TRUENAS_INSECURE\": \"true\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/truenas-node-add.md\"\n      }\n    },\n    {\n      \"id\": \"relay-pairing\",\n      \"name\": \"Relay Pairing & Mobile Readiness\",\n      \"description\": \"Validates relay configuration, connection to relay server, QR code generation, and mobile pairing readiness.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/03-relay-pairing.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/relay-pairing.md\"\n      }\n    },\n    {\n      \"id\": \"agent-registration\",\n      \"name\": \"Agent Install & Registration\",\n      \"description\": \"Validates host agent registration via report API, host visibility in unified state, and infrastructure page rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/04-agent-install-registration.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/agent-registration.md\"\n      }\n    },\n    {\n      \"id\": \"saml-sso-login\",\n      \"name\": \"SAML SSO Provider Lifecycle\",\n      \"description\": \"Validates SAML SSO provider CRUD, SP metadata generation, test-connection, and SSO settings UI rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/05-saml-sso-login.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/saml-sso.md\"\n      }\n    },\n    {\n      \"id\": \"audit-log-reporting\",\n      \"name\": \"Audit Log + Reporting Lifecycle\",\n      \"description\": \"Validates audit event list/export/summary, audit webhook CRUD, notification webhooks, report generation, and reporting UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/06-audit-log-reporting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/audit-reporting.md\"\n      }\n    },\n    {\n      \"id\": \"ai-patrol-lifecycle\",\n      \"name\": \"AI Patrol Finding Lifecycle\",\n      \"description\": \"Validates AI Patrol status, findings, force run, autonomy settings, approval queue, investigation, finding acknowledge/resolve lifecycle, and AI page UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/07-ai-patrol-finding-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/ai-patrol-lifecycle.md\"\n      }\n    }\n  ]\n}\n","frontmatter":{"scenarios":[{"id":"multi-tenant","name":"Multi-tenant Isolation","description":"Validates org lifecycle, switching, and isolation boundaries.","deterministic":{"command":["npx","playwright","test","tests/03-multi-tenant.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{"PULSE_MULTI_TENANT_ENABLED":"true","PULSE_E2E_ENTITLEMENT_PROFILE":"multi-tenant"}},"agentic":{"task_file":"evals/tasks/multi-tenant.md"}},{"id":"retired-trial-acquisition","name":"Retired Trial Acquisition","description":"Ensures self-hosted Pulse no longer initiates hosted Pro trial acquisition from the ordinary runtime: POST /api/license/trial/start returns 404 and preserves local entitlements.","deterministic":{"command":["npx","playwright","test","tests/07-retired-trial-acquisition.spec.ts","--project=chromium","--reporter=list","--retries=1"]},"agentic":{"task_file":"evals/tasks/retired-trial-acquisition.md"}},{"id":"cloud-hosting","name":"Pulse Cloud Hosted Signup","description":"Validates public cloud signup, Stripe sandbox checkout completion, and magic-link request behavior.","deterministic":{"command":["npx","playwright","test","tests/08-cloud-hosting.spec.ts","--project=chromium","--reporter=list","--retries=1"]},"agentic":{"task_file":"evals/tasks/cloud-hosting.md"}},{"id":"cloud-billing-lifecycle","name":"Cloud Billing Lifecycle","description":"Validates post-checkout cloud lifecycle transitions (active to canceled) through Stripe + webhook processing.","deterministic":{"command":["npx","playwright","test","tests/09-cloud-billing-lifecycle.spec.ts","--project=chromium","--reporter=list","--retries=1"]},"agentic":{"task_file":"evals/tasks/cloud-billing-lifecycle.md"}},{"id":"truenas-node-add","name":"TrueNAS Node Addition","description":"Validates TrueNAS connection add, pool/dataset discovery, and unified resource visibility.","deterministic":{"command":["npx","playwright","test","tests/journeys/02-node-add-truenas.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{"PULSE_E2E_TRUENAS_INSECURE":"true"}},"agentic":{"task_file":"evals/tasks/truenas-node-add.md"}},{"id":"relay-pairing","name":"Relay Pairing & Mobile Readiness","description":"Validates relay configuration, connection to relay server, QR code generation, and mobile pairing readiness.","deterministic":{"command":["npx","playwright","test","tests/journeys/03-relay-pairing.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{}},"agentic":{"task_file":"evals/tasks/relay-pairing.md"}},{"id":"agent-registration","name":"Agent Install & Registration","description":"Validates host agent registration via report API, host visibility in unified state, and infrastructure page rendering.","deterministic":{"command":["npx","playwright","test","tests/journeys/04-agent-install-registration.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{}},"agentic":{"task_file":"evals/tasks/agent-registration.md"}},{"id":"saml-sso-login","name":"SAML SSO Provider Lifecycle","description":"Validates SAML SSO provider CRUD, SP metadata generation, test-connection, and SSO settings UI rendering.","deterministic":{"command":["npx","playwright","test","tests/journeys/05-saml-sso-login.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{}},"agentic":{"task_file":"evals/tasks/saml-sso.md"}},{"id":"audit-log-reporting","name":"Audit Log + Reporting Lifecycle","description":"Validates audit event list/export/summary, audit webhook CRUD, notification webhooks, report generation, and reporting UI.","deterministic":{"command":["npx","playwright","test","tests/journeys/06-audit-log-reporting.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{}},"agentic":{"task_file":"evals/tasks/audit-reporting.md"}},{"id":"ai-patrol-lifecycle","name":"AI Patrol Finding Lifecycle","description":"Validates AI Patrol status, findings, force run, autonomy settings, approval queue, investigation, finding acknowledge/resolve lifecycle, and AI page UI.","deterministic":{"command":["npx","playwright","test","tests/journeys/07-ai-patrol-finding-lifecycle.spec.ts","--project=chromium","--reporter=list","--retries=1"],"env":{}},"agentic":{"task_file":"evals/tasks/ai-patrol-lifecycle.md"}}]},"isInternal":false,"tokens":1499,"sizeBytes":6087},{"name":"agent-registration.md","path":"tests/integration/evals/tasks/agent-registration.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/agent-registration.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Agent Registration Journey\n\n## Goal\n\nValidate the agent registration flow end-to-end: an agent sends its first\nreport, Pulse registers the agent, the agent appears in the unified state API, and\nthe agent is visible in the infrastructure UI.\n\n## Steps\n\n1. **Login** — Authenticate as admin via the Pulse UI or API.\n2. **Register agent** — Send a POST to `/api/agents/agent/report` with a\n   synthetic unified agent report payload containing `agent.type = \"unified\"`\n   plus hostname, OS info, CPU, memory, disks, and network interfaces.\n3. **Verify API state** — GET `/api/state` and confirm the new agent appears in\n   the canonical `resources[]` collection as an `agent` resource with correct\n   hostname, platform, and OS metadata.\n4. **Verify metrics** — Confirm the agent's CPU usage and memory metrics are\n   populated on that unified resource entry.\n5. **Send follow-up report** — POST a second report with different CPU/memory\n   values and verify `lastSeen` updates.\n6. **Verify UI** — Navigate to `/infrastructure` and confirm the agent's\n   hostname or display name is visible on the page.\n7. **Delete agent** — DELETE `/api/agents/agent/{agentId}` and verify the agent is\n   removed from `/api/state` `resources[]`.\n\n## Success Criteria\n\n- Agent report returns `{ success: true, agentId: \"...\" }`.\n- Agent appears in `/api/state` `resources[]` with correct metadata.\n- Agent is visible on the infrastructure page in the browser.\n- After deletion, agent no longer appears in state.\n","isInternal":false,"tokens":348,"sizeBytes":1508},{"name":"ai-patrol-lifecycle.md","path":"tests/integration/evals/tasks/ai-patrol-lifecycle.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/ai-patrol-lifecycle.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# AI Patrol Finding Lifecycle\n\n## Objective\nValidate the full AI Patrol closed-loop lifecycle: patrol status, findings,\nforce run, run history, autonomy settings, approval queue, investigation,\nfinding acknowledge/resolve, suppression/dismissed endpoints, and AI page UI.\n\n## Steps\n\n### Patrol Status & Configuration\n1. Log in as admin.\n2. GET `/api/ai/patrol/status` — verify response includes `enabled`, `running`, `healthy`, `summary` (with severity counts), `findings_count`, `interval_ms`.\n3. GET `/api/ai/patrol/autonomy` — verify `autonomy_level` is one of `monitor|approval|assisted|full`.\n\n### Patrol Execution\n4. POST `/api/ai/patrol/run` — trigger patrol run (accept 200, 429 rate-limited, or 503 unavailable).\n5. GET `/api/ai/patrol/runs` — verify run history array with `id`, `started_at`, `status`, `resources_checked`.\n\n### Findings & Investigation\n6. GET `/api/ai/patrol/findings` — verify array of findings with `id`, `severity`, `category`, `resource_id`, `title`.\n7. For any finding: GET `/api/ai/findings/{id}/investigation` — verify 200 (investigation exists) or 404 (no investigation yet).\n\n### Finding Lifecycle (Closed-Loop)\n8. POST `/api/ai/patrol/acknowledge` with `{ finding_id }` — verify success.\n9. GET `/api/ai/patrol/findings` — verify finding still present (acknowledged ≠ removed).\n10. POST `/api/ai/patrol/resolve` with `{ finding_id }` — verify success.\n11. GET `/api/ai/patrol/findings` — verify finding is removed from active list.\n\n### Approval Queue\n12. GET `/api/ai/approvals` — verify 200 with `approvals` + `stats`, or 402 paywall.\n\n### Supplementary Endpoints\n13. GET `/api/ai/patrol/suppressions` — verify 200 response.\n14. GET `/api/ai/patrol/dismissed` — verify 200 response.\n\n### UI Verification\n15. Navigate to `/ai` — verify AI page renders with patrol/intelligence content.\n\n## Expected Outcomes\n- Patrol status reflects actual service state.\n- Force patrol triggers or rate-limits correctly.\n- Finding lifecycle completes: acknowledge → resolve → removed.\n- Approval queue responds (200 or 402 paywall).\n- AI page renders meaningful content.\n","isInternal":false,"tokens":547,"sizeBytes":2128},{"name":"audit-reporting.md","path":"tests/integration/evals/tasks/audit-reporting.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/audit-reporting.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Audit Log + Reporting Lifecycle\n\n## Objective\nValidate audit logging and reporting enterprise features: audit event\nlisting, export, summary, webhook configuration, notification webhooks,\nsingle/multi-resource report generation, and UI rendering.\n\n## Steps\n\n### Audit Logging\n1. Log in as admin.\n2. GET `/api/audit?limit=10` — verify events list or 402 paywall.\n3. GET `/api/audit/export?format=json` — verify file download (or 501 for no logger).\n4. GET `/api/audit/summary` — verify summary statistics.\n5. GET/POST `/api/admin/webhooks/audit` — CRUD audit webhook URLs.\n6. Navigate to `/settings/security-audit` — verify audit log viewer renders.\n\n### Notification Webhooks (not license-gated)\n7. GET `/api/notifications/webhooks` — list webhooks.\n8. GET `/api/notifications/webhook-templates` — verify templates available.\n9. GET `/api/notifications/webhook-history` — verify history available.\n10. GET `/api/notifications/health` — verify notification system health.\n\n### Reporting\n11. GET `/api/state` — find a resource to report on.\n12. GET `/api/admin/reports/generate?resourceType=node&resourceId=...&format=csv` — generate report.\n13. POST `/api/admin/reports/generate-multi` — generate fleet report.\n14. Navigate to `/operations/reporting` — verify reporting page renders.\n\n## Expected Outcomes\n- Audit events and exports work when `audit_logging` is licensed.\n- Reports generate when `advanced_reporting` is licensed.\n- 402 paywall responses have correct format when not licensed.\n- UI pages render relevant content or upgrade messaging.\n","isInternal":false,"tokens":361,"sizeBytes":1577},{"name":"cloud-billing-lifecycle.md","path":"tests/integration/evals/tasks/cloud-billing-lifecycle.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/cloud-billing-lifecycle.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Scenario: Cloud Billing Lifecycle\n\n## Goal\nProve post-checkout billing lifecycle behavior works end-to-end for Pulse Cloud by validating tenant provisioning and cancellation transitions.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Create a real Stripe sandbox checkout from `/cloud/signup`.\n2. Complete checkout with Stripe test card details.\n3. Verify tenant reaches active state in control-plane admin tenant listing.\n4. Trigger subscription cancellation and deliver corresponding webhook event.\n5. Verify tenant transitions to canceled state after webhook processing.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","isInternal":false,"tokens":201,"sizeBytes":914},{"name":"cloud-hosting.md","path":"tests/integration/evals/tasks/cloud-hosting.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/cloud-hosting.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Scenario: Pulse Cloud Hosted Signup\n\n## Goal\nProve hosted cloud signup and magic-link request flows work end-to-end using real Stripe sandbox checkout from the public signup page.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Open `/cloud/signup` and verify form renders.\n2. Submit valid workspace creation details.\n3. Complete Stripe sandbox checkout and verify return to hosted signup completion page.\n4. Request a magic-link email from the same page.\n5. Validate the magic-link request endpoint responds with success contract.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","isInternal":false,"tokens":205,"sizeBytes":871},{"name":"multi-tenant.md","path":"tests/integration/evals/tasks/multi-tenant.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/multi-tenant.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Scenario: Multi-tenant Isolation\n\n## Goal\nProve that multi-tenant controls and isolation behave correctly from the user surface.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Sign in and open organization management.\n2. Create two organizations.\n3. Switch between orgs and confirm org-scoped data does not leak across tenants.\n4. Verify organization CRUD behavior (create/update/delete) works for the active org.\n5. Record any failures, flaky transitions, or unexpected UI states.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","isInternal":false,"tokens":201,"sizeBytes":822},{"name":"relay-pairing.md","path":"tests/integration/evals/tasks/relay-pairing.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/relay-pairing.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Scenario: Relay Pairing & Mobile Readiness\n\n## Goal\n\nProve that relay pairing works end-to-end: configure relay settings, verify connection to the relay server, generate a QR code for mobile pairing, and confirm the onboarding deep link is available.\n\n## Environment\n\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n- Relay Host: provided via `PULSE_E2E_RELAY_HOST`\n\n## Required checks\n\n1. Relay feature is available in license entitlements (`GET /api/license/entitlements` includes `relay` capability).\n2. Relay settings can be configured with the test relay server (`PUT /api/settings/relay` with `enabled: true, server_url: \"...\"` returns 200).\n3. Relay status becomes connected within 60s (`GET /api/settings/relay/status` → `connected: true`).\n4. Onboarding QR code returns required fields (`GET /api/onboarding/qr` → `schema`, `instance_url`, `relay`, `auth_token`, `deep_link`).\n5. Onboarding deep link is available (`GET /api/onboarding/deep-link` → `url` field non-empty).\n6. Relay settings page in the UI shows relay-related content (Relay, Connected, Enabled, etc.).\n\n## Output contract\n\nWrite JSON to `{{result_json}}` with this shape:\n\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","isInternal":false,"tokens":351,"sizeBytes":1386},{"name":"retired-trial-acquisition.md","path":"tests/integration/evals/tasks/retired-trial-acquisition.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/retired-trial-acquisition.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Scenario: Retired Trial Acquisition\n\n## Goal\nProve the self-hosted Pulse instance no longer initiates a hosted Pro trial from the ordinary in-app runtime and does not mutate local entitlements when the retired route is probed.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Capture current entitlements.\n2. Probe `POST /api/license/trial/start`.\n3. Confirm the response is HTTP `404`.\n4. Confirm the response does not contain legacy hosted-signup or trial-rate-limit acquisition payloads.\n5. Verify post-probe entitlements match the pre-probe entitlement summary.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n","isInternal":false,"tokens":220,"sizeBytes":904},{"name":"saml-sso.md","path":"tests/integration/evals/tasks/saml-sso.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/saml-sso.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# SAML SSO Provider Lifecycle\n\n## Objective\nValidate the full SAML SSO provider lifecycle: create a SAML provider,\nverify it appears in the provider list, check SP metadata generation,\ntest-connection validation, enable/disable toggling, and cleanup via delete.\n\n## Steps\n\n1. Log in as admin.\n2. GET `/api/security/sso/providers` — verify the endpoint responds with 200.\n3. POST `/api/security/sso/providers` — create a SAML provider with stub IdP metadata.\n4. GET `/api/security/sso/providers` — verify the new provider appears.\n5. GET `/api/saml/{providerID}/metadata` — verify SP metadata XML is returned.\n6. POST `/api/security/sso/providers/test` — validate the IdP metadata.\n7. Navigate to `/settings/security-sso` — verify the SSO settings page renders.\n8. PUT `/api/security/sso/providers/{id}` — disable the provider, verify disabled.\n9. PUT `/api/security/sso/providers/{id}` — re-enable, verify enabled.\n10. DELETE `/api/security/sso/providers/{id}` — delete and verify removal.\n\n## Expected Outcomes\n- Provider CRUD works end-to-end on the Community SSO capability.\n- SSO settings page renders SSO-related content.\n\n## Environment Variables\n- `PULSE_E2E_SAML_IDP_METADATA_URL` — optional live IdP metadata URL.\n","isInternal":false,"tokens":294,"sizeBytes":1243},{"name":"truenas-node-add.md","path":"tests/integration/evals/tasks/truenas-node-add.md","rawUrl":"https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/truenas-node-add.md","title":"Tasks Skill","category":"anthropic-skill","format":"markdown","content":"# Tasks Skill\n\nPath: `tests/integration/evals/tasks/truenas-node-add.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/truenas-node-add.md)","tokens":300,"sizeBytes":208}],"systemPromptSnippet":"<yakaai_skills repo=\"rcourtman/Pulse\">\n<!-- File: ARCHITECTURE.md (Tokens: ~3376 | Category: project-spec) -->\n# Pulse Architecture\n\nPulse is a real-time infrastructure monitoring platform for **Proxmox VE**,\n**Proxmox Backup Server**, **Proxmox Mail Gateway**, **Docker**, **machine\nagents**, **Kubernetes**, **TrueNAS**, and early-access **VMware vSphere**\nenvironments. It uses a **Go 1.26** backend and a **SolidJS / TypeScript**\nfrontend.\n\n## 🏗 High-Level Overview\n\nThe system runs as a single binary that serves both the API and the embedded frontend assets. It connects to infrastructure via platform-supported HTTP and WebSocket APIs and lightweight push-based agents, normalises everything into a **Unified Resource model**, and delivers real-time updates to clients over WebSocket.\n\n```mermaid\nflowchart TD\n    User[Browser / Mobile] <-->|WebSocket + HTTP| Pulse[Pulse Server]\n    Mobile[Mobile App] <-->|E2E Encrypted| Relay[Relay Server]\n    Relay <-->|WebSocket| Pulse\n\n    subgraph \"Pulse Server (Go)\"\n        API[Decomposed API Router]\n        WS[WebSocket Hub]\n        Monitor[Reloadable Monitor]\n        UR[Unified Resource Registry]\n        AI[AI Subsystem]\n        Config[Config Manager]\n        License[Entitlements Engine]\n        Audit[Audit Logger]\n    end\n\n    Pulse -->|HTTPS :8006| PVE[Proxmox VE]\n    Pulse -->|HTTPS :8007| PBS[Proxmox Backup Server]\n    Pulse -->|HTTPS| PMG[Proxmox Mail Gateway]\n    Pulse -->|HTTPS| TrueNAS[TrueNAS SCALE/CORE]\n    Pulse -->|HTTPS| vSphere[VMware vCenter]\n    DockerAgent[Docker Agent] -->|HTTPS POST| API\n    HostAgent[Host Agent] -->|HTTPS POST| API\n    K8sAgent[Kubernetes Agent] -->|HTTPS POST| API\n\n    Monitor --> UR\n    UR --> WS\n    UR --> API\n```\n\n## 🔌 Backend Architecture (Go)\n\nAll backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is assembled from `cmd/pulse/main.go` which delegates to `pkg/server.Run()`.\n\n### Core Components\n\n1. **Entry Point (`cmd/pulse/main.go` → `pkg/server/server.go`)**\n   - Loads unified configuration via `internal/config.Load()`.\n   - Initialises the RBAC manager (`pkg/auth`), audit logger (`pkg/audit`), and crypto layer (`internal/crypto`).\n   - Creates a `ReloadableMonitor` that manages the lifecycle of all monitoring goroutines.\n   - Starts the HTTP/HTTPS server, WebSocket hub, AI services (Patrol + Chat), and the Relay client.\n   - Supports graceful hot-reload via `SIGHUP` and `.env` file watching.\n\n2. **Unified Resource Registry (`internal/unifiedresources`)**\n   - Central data model that normalises resources from Proxmox, PBS, PMG,\n     Docker, machine agents, Kubernetes, TrueNAS, and VMware providers into a\n     shared `Resource` contract.\n   - **Canonical v6 resource types**: `agent`, `vm`, `system-container`, `app-container`, `docker-host`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `storage`, `pbs`, `pmg`, `ceph`, `physical_disk`.\n   - Identity-matching engine: merges resources across sources using machine IDs, DMI UUIDs, hostnames, IPs, and MAC addresses.\n   - Provides typed **views** (`NodeView`, `K8sClusterView`, etc.) for consumer-specific queries.\n   - Canonical API endpoint: `GET /api/resources`.\n\n3. **Monitoring Engine (`internal/monitoring`)**\n   - **Polymorphic monitors**: Each Proxmox VE/PBS/PMG node runs in its own goroutine, polling via the platform REST API.\n   - **Agent receivers** (`internal/api`): Docker, Host, and Kubernetes agents push metrics via HTTP POST to `/api/agents/{type}/report`.\n   - **TrueNAS provider** (`internal/truenas`): Uses the versioned JSON-RPC 2.0\n     WebSocket API on supported SCALE releases for system info, pools,\n     datasets, disks, alerts, ZFS snapshots, and replication tasks. A\n     version-gated REST path remains only for legacy SCALE and CORE systems.\n   - Enterprise/internal multi-org aware: when `PULSE_MULTI_TENANT_ENABLED=true`, each organisation gets a separate shared-process monitor namespace with its own configuration.\n\n4. **WebSocket Hub (`internal/websocket`)**\n   - Manages active browser connections with per-message compression (deflate).\n   - Broadcasts state diffs to all subscribed clients; supports per-organization broadcasts for internal multi-org setups.\n   - Enforces origin validation, organization-level authorization, and Enterprise/internal multi-org license gating.\n\n5. **Decomposed API Router (`internal/api`)**\n   - The router is split into focused registration files for maintainability:\n     - `router_routes_auth_security.go` — Auth, OIDC, SAML, SSO, security tokens, recovery, agent install scripts.\n     - `router_routes_monitoring.go` — Unified resources, metrics history, charts, recovery points, alerts, notifications, discovery.\n     - `router_routes_ai_relay.go` — AI settings, Patrol, Intelligence, Chat sessions, Relay config, and the approval workflow.\n     - `router_routes_org_license.go` — Organisations, RBAC, audit logs, license/entitlements, billing.\n     - `router_routes_registration.go` — Config CRUD, TrueNAS connections, update management, agent profiles, system settings.\n     - `router_routes_hosted.go` — Hosted/SaaS-specific signup and org admin routes.\n   - Every endpoint enforces **scoped access control** (e.g., `monitoring:read`, `settings:write`, `ai:execute`, `ai:chat`).\n\n6. **Relay Client (`internal/relay`)**\n   - Maintains a persistent WebSocket tunnel to a managed relay server for **mobile remote access**.\n   - Uses an ECDH key-exchange for per-channel **end-to-end encryption**.\n   - Multiplexes multiple mobile sessions over a single connection with back-pressure (data limiter) and per-channel authentication.\n   - Supports push notifications through the relay.\n   - Gated by the `relay` license feature.\n\n7. **AI Subsystem (`internal/ai`)**\n   - **AI runtime (`internal/ai/chat`)**: The governed tool loop behind Patrol investigations and Pulse Mobile chat (the in-app chat window has been retired). Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. External agents reach the same tools through `pulse-mcp`.\n   - **Pulse Patrol**: Scheduled background analysis that produces findings, predictions, and remediation plans. Autonomy levels: `monitor`, `approval`, `assisted`, `full`.\n   - **Intelligence Services**: Patterns, correlations, anomalies, baselines, forecasts, and incident recording. All surfaced via `/api/ai/intelligence/*`.\n   - **Safety gates**: Command execution disabled by default (`--enable-commands` opt-in); circuit breakers and scoped permissions at every layer.\n\n8. **Entitlements & Licensing (`pkg/licensing`)**\n   - Capability-key based gating: `ai_autofix`, `rbac`, `multi_tenant`, `relay`, `agent_profiles`, `kubernetes_ai`, `ai_alerts`, etc.\n   - Core tiers include **Community** (free), **Relay**, **Pro**, a reserved\n     hosted **Cloud** capability tier, request-assisted **MSP**, and\n     Enterprise/custom entitlements. The Cloud service is not generally\n     available.\n   - Activation, grant refresh, renewal, expiry, and legacy-license migration.\n     Active state is exposed through `/api/license/*`.\n\n9. **Provider-hosted MSP control plane (`internal/cloudcp`)**\n   - A Stripe-free provider control plane can run one isolated Pulse runtime/container per client workspace.\n   - A signed MSP license sets the provider plan and client workspace cap.\n   - Pulse Account handles the provider workspace roster, sign-in-once handoff, client-bound agent install paths, and provider access surfaces.\n   - Each client runtime remains a normal Pulse instance for monitoring data, alerts, webhooks, reports, users, and audit history. Shared-process organizations are for Enterprise/internal multi-org, not the default MSP boundary.\n\n10. **Recovery Engine (`internal/recovery`)**\n   - Aggregates backup data (PBS snapshots, ZFS snapshots, replication tasks) into a unified recovery point timeline.\n   - Provides faceted queries: filter by type, source, status, and source health.\n   - API: `/api/recovery/points`, `/api/recovery/series`, `/api/recovery/facets`, `/api/recovery/rollups`.\n\n11. **Audit Logging (`pkg/audit`)**\n    - Defence-in-depth: every mutation is logged to SQLite, optionally signed with per-tenant encryption keys.\n    - Async audit logging is on by default (`PULSE_AUDIT_ASYNC=false` disables it); see `docs/CONFIGURATION.md`.\n    - Tenant-aware logger manager for isolated per-org audit trails.\n\n### Data Flow\n\n1. **Collection**:\n   - **Proxmox VE / PBS / PMG**: Monitoring engine polls platform REST APIs (configurable interval, default 2 s for PVE).\n   - **Docker / Host / Kubernetes**: Lightweight agents push metrics via HTTP POST on their configured interval.\n   - **TrueNAS**: Provider polls the versioned JSON-RPC WebSocket API on\n     supported SCALE releases, with REST compatibility limited to recognized\n     legacy SCALE and CORE systems.\n2. **Normalisation**: Platform-specific responses are mapped into `unifiedresources.Resource` structs by adapters in `internal/unifiedresources/adapters.go`.\n3. **Registration**: Resources are inserted into the in-memory registry, which handles deduplication, identity matching, and status computation.\n4. **Broadcast**: The latest state snapshot is serialised to JSON and pushed to all connected WebSocket clients by the Hub.\n5. **Persistence**: Metrics history is stored in a SQLite-backed metrics store (`pkg/metrics`) with configurable retention.\n\n## 🎨 Frontend Architecture (SolidJS)\n\nThe frontend is a modern SPA in `frontend-modern/`, built with **SolidJS** and **TypeScript**. It uses fine-grained reactivity (no Virtual DOM) for maximum performance.\n\n### Key Technologies\n- **SolidJS**: Reactive UI framework.\n- **TailwindCSS** with a **semantic design token layer**: All structural colours use CSS custom-property tokens (`bg-base`, `bg-surface`, `text-base-content`, `border-border`, etc.) defined in `index.css`— components never hardcode hex values. Light/dark mode switches automatically via class-based theme resolution.\n- **Vite**: Build tooling (dev server + production bundling).\n- **Lucide Icons**: Icon library (imported as solid components).\n\n### Routing & Navigation\n\nNavigation is organised around platform-shaped pages with cross-platform\noperational surfaces:\n\n| Route | Page | Purpose |\n|---|---|---|\n| `/` and `/infrastructure` | Runtime home | Monitor-first authenticated entry point |\n| `/proxmox/*` | Proxmox | PVE, PBS, PMG, guests, storage, recovery, and Ceph |\n| `/docker/*` | Docker | Hosts, containers, Compose projects, Swarm, images, and storage |\n| `/kubernetes/*` | Kubernetes | Clusters, workloads, networking, storage, and events |\n| `/truenas/*` | TrueNAS | Systems, pools, datasets, disks, apps, VMs, and recovery |\n| `/vmware/*` | vSphere | Early-access vCenter, host, cluster, VM, datastore, and network views |\n| `/standalone/*` | Machines | Agent-backed machines and availability checks |\n| `/alerts/*` | Alerts | Alert rules, active alerts, history |\n| `/actions/*` | Actions | Governed action proposals, approvals, delivery, and audit state |\n| `/patrol/*` | Patrol | Attention queue, findings, investigations, and run history |\n| `/settings/*` | Settings | Infrastructure, security, notifications, plans, and Intelligence |\n\nThe retired aggregate `/workloads`, `/storage`, and `/recovery` top-level\nroutes are not canonical navigation. Shared resource, storage, and recovery\ncontracts remain backend building blocks consumed inside platform pages.\n\n### State Management\n- **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching.\n- **Metrics collector** (`stores/metricsCollector.ts`): Buffers time-series data for sparklines and historical charts.\n- **AI stores** (`stores/aiAvailability.ts`, `stores/aiIntelligence.ts`): Hold whether AI is enabled for the session and the Patrol findings.\n- **License store** (`stores/license.ts`): Tracks plan tier, feature flags, and multi-tenant state.\n- **System settings store** (`stores/systemSettings.ts`): Caches server-side preferences (for example theme and feature toggles).\n\n### Component Design\n- **Shared primitives** in `components/shared/`: `Card`, `Button`, `Toggle`, `FilterButtonGroup`, `Table` — all mapped to the semantic design tokens.\n- **Lazy-loaded pages**: All top-level pages are loaded via `lazy()` with optional preloading after initial render.\n- **Virtual table windowing**: Large resource lists use virtualised rendering for smooth scrolling at scale.\n- **Command Palette** (`Cmd/Ctrl+K`): Quick-access command launcher.\n- **Keyboard shortcuts**: `g p` → Proxmox, `g d` → Docker, `g k` →\n  Kubernetes, `g n` → TrueNAS, `g v` → vSphere, `g s` → Machines, `g a` →\n  Alerts, `g r` → Patrol, `g t` → Settings, `/` → Search.\n\n### Mobile Experience\n- **MobileNavBar** component: Bottom tab bar for touch navigation.\n- **Relay integration**: The mobile app connects through the relay protocol for encrypted remote access.\n\n## 🔒 Security\n\n- **Encryption at Rest**: Sensitive config (passwords, API keys) encrypted with AES-GCM via `internal/crypto`. Each tenant can have its own encryption key.\n- **Scoped API Tokens**: Tokens carry explicit scopes (`monitoring:read`, `settings:write`, `ai:chat`, `ai:execute`, `docker:report`, etc.). Endpoints enforce scope checks before processing.\n- **RBAC**: Role-based access control via `pkg/auth` with file-backed persistence. Resources: `users`, `settings`, `monitoring`.\n- **SSO**: OIDC and SAML providers with per-provider configuration and multi-provider support.\n- **Agent Commands**: Disabled by default for security. Operators must opt in with `--enable-commands`.\n- **Audit Trail**: Every mutation logged to per-tenant SQLite databases with optional cryptographic signatures.\n- **Rate Limiting**: Per-endpoint and per-tenant rate limiting with configurable thresholds.\n- **CSRF Protection**: Token-based CSRF prevention on mutation endpoints.\n- **Recovery Mode**: Localhost-only endpoint for emergency auth recovery with time-limited tokens.\n\n## 🚀 Deployment\n\nPulse is distributed as:\n\n1. **Docker Container**: Multi-stage build producing a minimal image with the Go binary + embedded frontend.\n2. **Single Binary**: The frontend is compiled into the Go binary using `go:embed` (`internal/api/frontend_embed.go`), enabling single-file deployment.\n3. **Kubernetes Helm Chart**: For cluster deployments with configurable replicas and persistence.\n4. **Systemd Service**: For bare-metal or VM installations with `.env`-based configuration and `SIGHUP` reload support.\n5. **Proxmox LXC Helper Script**: One-line install inside a Proxmox helper-scripts container.\n\n<!-- File: docs/API.md (Tokens: ~12099 | Category: project-spec) -->\n# 🔌 Pulse API Reference\n\nPulse provides a comprehensive REST API for automation and integration.\n\n**Base URL**: `http://<your-pulse-ip>:7655/api`\n\n## 🔐 Authentication\n\nMost API requests require authentication via one of the following methods:\n\n### API Token (Recommended)\n\nCreate an API token in Pulse's **API Access** settings with only the scopes\nneeded for your task. The read-only example below requires `monitoring:read`.\nPass the token in the `X-API-Token` header, using a private header file so the\nsecret does not appear in shell history or process arguments.\n\nOn a trusted machine, as the account running curl, prepare the file and open\nit in an editor. This preserves an existing file, restricts its directory and\nstops before editing if preparation fails or a credential path is symlinked or\nnot a regular file:\n\n```bash\n(\n  set -eu\n  umask 077\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ]; then\n    printf 'Refusing a symlinked credential path.\\n' >&2\n    exit 1\n  fi\n  if [ -e \"$auth_file\" ] && [ ! -f \"$auth_file\" ]; then\n    printf 'Credential file must be a regular file.\\n' >&2\n    exit 1\n  fi\n  mkdir -p \"$auth_dir\"\n  chmod 700 \"$auth_dir\"\n  touch \"$auth_file\"\n  chmod 600 \"$auth_file\"\n  vi \"$auth_file\"\n)\n```\n\nIn the editor, save just this line, replacing `<token>` with the API token:\n\n```text\nX-API-Token: <token>\n```\n\nDefine this helper in the same Bash session (curl 7.76 or later). The examples\non this page use it to make **one request**, print only the HTTP status and save\neach response to a new owner-only file. This keeps resource names, audit details\nand credential-bearing error bodies out of terminal output and recordings.\n\n```bash\npulse_api() (\n  set -eu\n  umask 077\n  if [ \"$#\" -ne 2 ]; then\n    printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\\n' >&2\n    exit 2\n  fi\n  method=$1\n  api_path=$2\n  case \"$api_path\" in\n    /api/*) ;;\n    *) printf 'Use an API path on this Pulse instance, not a full URL.\\n' >&2; exit 2 ;;\n  esac\n  case \"$method\" in\n    GET) set -- ;;\n    POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;\n    PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;\n    DELETE) set -- --request DELETE ;;\n    *) printf 'This example helper accepts only GET, POST, PUT and DELETE.\\n' >&2; exit 2 ;;\n  esac\n  auth_dir=\"$HOME/.config/pulse\"\n  auth_file=\"$auth_dir/api-header\"\n  if [ -L \"$HOME/.config\" ] || [ -L \"$auth_dir\" ] || [ -L \"$auth_file\" ] || [ ! -f \"$auth_file\" ]; then\n    printf 'Prepare a regular, private header file first.\\n' >&2\n    exit 1\n  fi\n  chmod 700 \"$auth_dir\" || exit \"$?\"\n  chmod 600 \"$auth_file\" || exit \"$?\"\n  result_file=$(mktemp \"$auth_dir/api-response.XXXXXX\") || exit \"$?\"\n  curl_exit=0\n  status=$(curl --disable --fail-with-body --silent --show-error --globoff \\\n    --proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \\\n    --header \"@$auth_file\" --output \"$result_file\" --write-out '%{http_code}' \\\n    \"$@\" \"http://127.0.0.1:7655$api_path\") || curl_exit=$?\n  printf 'HTTP %s\\nPrivate response: %s\\n' \"$status\" \"$result_file\"\n  [ \"$curl_exit\" -eq 0 ] || exit \"$curl_exit\"\n  case \"$status\" in\n    2??) ;;\n    *) exit 1 ;;\n  esac\n)\n```\n\nThen make a read-only request with a `monitoring:read` token:\n\n```bash\npulse_api GET /api/state/summary\n```\n\nThe default loopback origin applies only when curl runs on the Pulse host and\nexplicitly bypasses proxy environment settings. For remote access, replace\n`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace\n`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate\nverification enabled; for a private CA, add `--cacert` with a CA file you\nverified separately, never `--insecure`. Do not put credentials in the URL.\n\nOpen the reported response file privately to inspect it, including on failure;\nshare only the relevant redacted error, not the whole response or file. Each\ncall preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit\nand retain the error body; redirects are not followed and are not success.\nThe connection limit is five seconds and the whole request limit is twenty\nseconds. A partial file after a transport error is not a complete result.\nNo request is retried automatically, especially a POST whose outcome is\nuncertain: inspect the existing action and target before deciding what to do.\nA 2xx response is only HTTP success, not proof that an action completed.\n\nThe helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).\nPOST and PUT read their JSON body from standard input; GET and DELETE send no\nbody. A method being supported is not permission to change a resource. Choose\neach administrative change separately and check its current state afterwards.\nA timeout or incomplete response can occur **after a change was applied**;\ndo not repeat a POST, PUT or DELETE merely because the response was lost.\n\nKeep `--globoff`: shell quotes alone do not disable curl's URL globbing. Without\nit, a path containing a brace list such as `{review,ops}` or a range such as\n`[1-3]` can send several requests, including several POST, PUT or DELETE\nrequests. The helper treats these characters literally, not as a batch of\ntargets. Still quote the whole path in the shell, and URL-encode dynamic resource\nIDs and query values; `--globoff` does not encode them for you.\n\nKeep `--disable` first: it ignores local curl configuration that could enable\ntrace output, bypass TLS or follow redirects. On an authentication-enabled\ninstance, the protected summary checks token access; `/api/health` is public\nand does not verify authentication. The helper is local example code, not an\ninstalled Pulse command; define it again in a new shell before using these\nexamples. Dispose of response files under your normal private-data policy.\n\nDo not paste tokens into command lines, URLs or issue reports. Keep the header\nfile private and outside shared repositories and diagnostics; do not use curl\nverbose/trace output when sharing a result. Share only the relevant redacted\nerror, not the whole infrastructure response. Revoke tokens that are no longer\nneeded in Pulse's API Access settings.\n\n### Bearer Token\n\nThe same API token can use a Bearer header instead. Replace the header file's\nline in the editor with the following, then use the same helper above;\ndo not send both authentication headers:\n\n```text\nAuthorization: Bearer <token>\n```\n\n### Session Cookie\n\nFor one-off read-only diagnostics, use your signed-in Pulse browser to open\nthe API path on that same instance. Do not extract or paste its session cookie\ninto a command or report; use the scoped token-file method above for automation.\nState-changing session requests also require Pulse's CSRF protection, which\nthe UI handles.\n\nSession endpoints:\n- `POST /api/login` (sets the session cookie, `pulse_session` over plain HTTP or `__Host-pulse_session` over HTTPS, plus `pulse_csrf`)\n- `POST /api/logout` (clears session)\n\nLogin body:\n```json\n{ \"username\": \"admin\", \"password\": \"secret\", \"rememberMe\": true }\n```\n\nPublic endpoints include:\n- `GET /api/health`\n- `GET /api/version`\n- `GET /api/agent/version` (agent update checks)\n- `GET /api/setup-script` (public; the generated script prompts for its setup token at runtime and skips auto-registration if none is supplied)\n\n## 🔏 Scopes and Admin Access\n\nSome endpoints require admin privileges and/or scopes. Common scopes include:\n- `monitoring:read`\n- `monitoring:write`\n- `settings:read`\n- `settings:write`\n- `agent:config:read`\n- `agent:manage`\n- `actions:plan`, `actions:approve`, `actions:execute`\n- `audit:read`\n\nEndpoints that require admin access are noted below.\n\nAdmin endpoints are instance administration unless they act only on the\nrequest organization. On instance administration an API token must not be\nbound to any organization other than the default one: a token bound to a\ntenant organization gets `403` whatever its scopes, as that organization's\nowner does. Endpoints that serve only the request organization, such as nodes\nand platform connections, agents, install commands, configuration transfer and\nreports, accept tokens bound to that organization with the listed scope.\n\n---\n\n## 📡 Core Endpoints\n\n### System Health\n`GET /api/health`\nCheck if Pulse is running.\n```json\n{\n  \"status\": \"healthy\",\n  \"timestamp\": 1700000000,\n  \"uptime\": 3600,\n  \"devModeSSH\": false\n}\n```\n\n### System State\n`GET /api/state`\nReturns the complete state of your infrastructure (Nodes, VMs, Containers, Storage, Alerts). This is the main endpoint used by the dashboard.\n\n`GET /api/state/summary`\nReturns a lightweight integration summary for external dashboards and checks. Requires `monitoring:read`.\n\n```json\n{\n  \"activeAlerts\": 1,\n  \"nodes\": 2,\n  \"vms\": 8,\n  \"containers\": 12,\n  \"dockerHosts\": [\n    {\n      \"name\": \"Docker Host\",\n      \"containers\": 5,\n      \"uptimeSeconds\": 86400,\n      \"cpuUsagePercent\": 12.5\n    }\n  ],\n  \"verdicts\": {\n    \"ok\": 17,\n    \"attention\": 1,\n    \"critical\": 1,\n    \"stale\": 1,\n    \"off\": 2,\n    \"unknown\": 0\n  },\n  \"attention\": [\n    {\n      \"id\": \"node-1\",\n      \"name\": \"PVE Node 1\",\n      \"type\": \"agent\",\n      \"platformType\": \"proxmox\",\n      \"verdict\": \"critical\",\n      \"topReason\": { \"code\": \"offline\" }\n    }\n  ],\n  \"lastUpdate\": \"2026-05-24T10:11:12Z\"\n}\n```\n\n`verdicts` uses the canonical `ok`, `attention`, `critical`, `stale`, `off`,\nand `unknown` fleet-health vocabulary. `attention` is severity ordered and\ncapped at 30 entries. Powered-off workloads are neutral (`off`), and stale or\nmissing telemetry is never reported as `ok`.\n\n### Simple Stats (HTML)\n`GET /simple-stats`\nLightweight HTML status page for quick checks.\n\n### Unified Resources\n`GET /api/resources`\nReturns the unified resource list with pagination + aggregations. Requires `monitoring:read`.\n\nQuery params:\n- `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `app-container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`)\n- `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`)\n- `excludeSource`: comma-separated list of sources that must not be present on a matching resource\n- `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`)\n- `parent`: parent resource ID\n- `cluster`: cluster name\n- `namespace`: Kubernetes namespace (filters Kubernetes resources only)\n- `q`: name search (contains match)\n- `tags`: comma-separated tags\n- `page`: page number (default `1`)\n- `limit`: page size (default `50`, max `100`)\n- `sort`: `name` (default), `status`, `type`, `lastSeen`\n- `order`: `asc` (default) or `desc`\n\nNote: `GET /api/resources` is optimized for list views. Some large, platform-specific fields may be omitted from the list response and are only returned by `GET /api/resources/{id}`.\n\nEach list resource can include a backend-owned `health` envelope with a\ncanonical `verdict` and stable `reasons` (`code` plus optional compact\n`detail`). Consumers should use this envelope for cross-platform posture\ninstead of deriving health from provider-specific status strings.\n\n#### Guest readings: availability and age\n\nIn legacy VM payloads, `disk.usage: -1` means unavailable, not negative usage.\nIn `GET /api/resources` and `GET /api/resources/{id}`, an unavailable disk\nreading is omitted as `metrics.disk`. **Do not replace an absent metric with\nzero**; a genuinely observed zero is valid. Allocated virtual-disk capacity\nalone does not establish used space inside the guest.\n\nRead `proxmox.diskStatusReason` alongside the value. A `prev-` reason means\nretained disk evidence, not a fresh reading. `agent-not-running` can also\nrepresent a general guest-agent HTTP 500 error: it does not establish that the\nguest's service is absent or stopped. An absent reason alone does not prove\nfreshness either.\n\nFor memory, use `metrics.memory.observation` for the **selected metric's**\nsource and age, not a different source's retained `proxmox.memory` facet:\n\n| Observation `state` | Meaning for the consumer |\n| --- | --- |\n| `current` | Accepted source observation; a normal cache hit can keep the original `observedAt`. Check its age for your use. |\n| `last-known` | Retained evidence, not a new measurement. Keep its original age visible; do not record it as a fresh sample. |\n| `unavailable` | No usable usage observation; do not treat retained numbers or zero-valued fields as a measurement. |\n| Missing or unrecognised | Freshness is unknown, including older payloads without an observation. Do not infer `current`. |\n\n`observedAt` belongs to that source observation. If it is absent or in the\nfuture, its age is unknown; do not substitute the resource's `lastSeen` or\n`updatedAt`. In the raw `proxmox.memory` facet, `usageUnavailable: true` can\npreserve known capacity without measured usage. An absent `metrics.memory`\nis not 0% usage. Conversely, an independently observed PVE or Pulse-agent\nmemory reading can remain current while QEMU disk collection is deferred;\ndisk state is not memory provenance.\n\nA running VM, an advancing row timestamp or an OK backup does not prove fresh\nguest readings or successful thaw. Do not install or restart an agent or send\nlive guest-agent probes merely to fill these gaps, especially during a backup,\nfreeze/thaw or an unresponsive-guest incident. Follow [Backup safety](VM_DISK_MONITORING.md#backup-safety)\nand [missing-reading guidance](VM_DISK_MONITORING.md#a-missing-reading-is-not-an-installation-diagnosis).\nRecovery needs independent thaw, fresh successful writes to every covered\nfilesystem and workload liveness; retained API values cannot establish it.\n\nAvailability is an additive resource facet. `availability` is the compatibility\nsummary used by existing clients; `availabilityChecks` contains every check\nattached to the resource. Each check can include `correlationState`\n(`attached`, `standalone`, `ambiguous`, or `unresolved`), its correlation\nrule/reason/candidate count, and an `evidence` envelope with observation and\nvalidity timestamps. Attached targets also add a `checks` relationship and do\nnot appear as separate `network-endpoint` rows.\n\n`GET /api/resources/stats`\nReturns aggregations (counts + health rollups).\n\n`GET /api/resources/k8s/namespaces?cluster=<clusterName>`\nReturns namespace-level rollups (pods + deployments) for a Kubernetes cluster. Requires `monitoring:read`.\n```json\n{\n  \"cluster\": \"prod-k8s\",\n  \"data\": [\n    {\n      \"namespace\": \"default\",\n      \"pods\": { \"total\": 12, \"online\": 10, \"warning\": 2, \"offline\": 0, \"unknown\": 0 },\n      \"deployments\": { \"total\": 3, \"online\": 3, \"warning\": 0, \"offline\": 0, \"unknown\": 0 }\n    }\n  ]\n}\n```\n\n`GET /api/resources/{id}`\nFetch a single resource by ID.\n\n`GET /api/resources/{id}/children`\nReturns child resources for the parent ID.\n\n`GET /api/resources/{id}/metrics`\nReturns the resource metrics payload.\n\n`POST /api/resources/{id}/link` (`monitoring:write`)\nManually link two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/unlink` (`monitoring:write`)\nManually unlink two resources.\n```json\n{ \"targetId\": \"resource-id\", \"reason\": \"optional note\" }\n```\n\n`POST /api/resources/{id}/report-merge` (`monitoring:write`)\nReport an incorrect merge (creates exclusions).\n```json\n{ \"sources\": [\"proxmox\", \"agent\"], \"notes\": \"optional note\" }\n```\n\n### Resource Maintenance and Operator State\n\nUse a timed maintenance window to suppress alerts during planned work on a\nresource. This API, including descendant scope, is available in stable v6.4.1.\nChoose the resource's `id` from `GET /api/resources` and URL-encode it in the\nrequest path. Do not substitute a display name or a Proxmox VM number.\n\n| Endpoint | Required scope | Result |\n| --- | --- | --- |\n| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |\n| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |\n| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |\n\n**PUT replaces the whole record, not just the fields supplied.** Read the current\nstate first and preserve unrelated settings, such as monitoring mode, lifecycle,\nremediation policy, criticality and notes. Treat only `operator_state_not_set`\nas an empty record, and stop on other read errors. Coordinate concurrent writers\nso a read followed by a PUT does not overwrite someone else's changes.\n\nFor a one-time window, merge these fields into that record, replacing the example\ntimes with your intended start and end:\n\n```json\n{\n  \"maintenanceStartAt\": \"2026-10-15T20:00:00Z\",\n  \"maintenanceEndAt\": \"2026-10-15T20:30:00Z\",\n  \"maintenanceScope\": \"resource_and_descendants\",\n  \"maintenanceReason\": \"Proxmox node maintenance\"\n}\n```\n\nThis is a set of fields to merge, not a complete replacement for an existing\nrecord. Remove any `maintenanceRecurrence` when replacing a recurring schedule\nwith this one-time window. The API rejects a record containing both.\n\n| Field | Meaning |\n| --- | --- |\n| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |\n| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |\n| `maintenanceReason` | Optional explanation of the planned work. |\n\nThe response includes `maintenanceWindowActive` and, while active,\n`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the\nrecord's `setAt` and `setBy` fields from the request, ignoring client values.\n\nDuring an active window, matching new alerts and firing/recovery notifications\nare suppressed. Applying an active window also clears matching existing alerts\nfrom the active list, retaining history. This is broader than pausing delivery\nfor an existing incident. Resources outside the scope remain monitored normally.\n\nThe window expires automatically, with no re-enable request needed. Subsequent\nobservations can raise alerts again, subject to normal alert policy and any other\nactive maintenance window. Previously cleared alerts are not automatically\nrestored, and expiry does not replay a backlog of maintenance notifications.\n\nTo end maintenance early, read the latest record, remove its maintenance fields\nand PUT the remaining state back. Do not DELETE the record unless you also intend\nto remove its other operator settings. An inherited window must be changed on\nthe ancestor that owns it.\n\n### Fleet Connections\n`GET /api/connections`\nReturns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.\n\nThe payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.\n\nDefine the private-response helper in [Authentication](#-authentication),\nwith `settings:read` on the token:\n\n```bash\npulse_api GET /api/connections\n```\n\n### Unified Action Planning\n\nThe existing action API separates planning, approval and execution. Run these\nsteps separately, inspecting each response before moving on; do not paste the\nwhole sequence as an unattended recovery script. Use the private header file\nand `pulse_api` helper from [Authentication](#-authentication), not a token in a\ncommand or environment assignment. Inspect each reported private response file\nbefore proceeding. For remote access, change the helper as described there,\nusing your Pulse HTTPS origin with certificate verification enabled.\n\n| Endpoint | Token scope and access |\n| --- | --- |\n| `GET /api/agent/resource-capabilities/{id}` | `monitoring:read`; lists the resource's advertised capabilities and parameter schemas. |\n| `POST /api/actions/plan` | `actions:plan` and permission to plan actions; creates a plan but does not approve or execute it. |\n| `POST /api/actions/{id}/decision` | `actions:approve` and permission to approve actions; records an explicit `approved` or `rejected` decision for a pending action, without executing it. |\n| `POST /api/actions/{id}/execute` | `actions:execute` and permission to execute actions; dispatches only an approved action or an approval-free executable plan. Dry-run-only plans cannot execute. |\n| `GET /api/audit/actions` | `audit:read`, audit-log read permission and licensed audit logging. |\n| `GET /api/audit/actions/{id}/events` | The same audit access; returns the action's lifecycle evidence. |\n\nThe legacy `ai:execute` scope also permits planning, approval and execution,\nbut use the narrower action scopes when possible. Approval and execution tokens\nmust be bound to an authorised user; a scope alone does not bypass approval\npolicy, separation of duties or step-up requirements. Audit reads through a\nbrowser session additionally require admin access. A `monitoring:read` token is\nnot an action-control or audit token.\n\n1. **Inspect capabilities.** Choose the canonical resource `id` from\n   `GET /api/resources`, not a display name or a VM number. Replace `vm:42`\n   throughout these examples and URL-encode it in request paths (`vm%3A42` here).\n   An empty capabilities list means there is nothing to plan for that resource.\n\n```bash\npulse_api GET /api/agent/resource-capabilities/vm%3A42\n```\n\n2. **Plan only.** Use an advertised capability and its actual parameter schema;\n   `restart` and `mode=graceful` are examples, not universal capabilities. Choose\n   a request ID for this specific intent. Pulse derives the actor from the\n   authenticated credential, not a caller-supplied `requestedBy` value.\n\n```bash\npulse_api POST /api/actions/plan <<'JSON'\n{\n  \"requestId\": \"manual-recovery-123\",\n  \"resourceId\": \"vm:42\",\n  \"capabilityName\": \"restart\",\n  \"params\": { \"mode\": \"graceful\" },\n  \"reason\": \"Recover after confirmed outage\"\n}\nJSON\n```\n\nInspect the returned approval policy, blast radius, expiry and preflight checks.\nKeep its `actionId` and reviewed `planHash`; replace `act_...` and `sha256:...`\nbelow with those returned values. Planning is not proof that execution is safe\nor currently available.\n\n3. **Decide only if approval is required.** Use an authorised approver's private\n   header file. Approve only the reviewed plan; stop on a refusal or stale-plan\n   response rather than weakening the policy or creating another recovery action.\n\n```bash\npulse_api POST /api/actions/act_.../decision <<'JSON'\n{\n  \"outcome\": \"approved\",\n  \"reason\": \"Inside maintenance window\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\n4. **Execute separately.** Only after reviewing a successful decision, or an\n   approval-free executable plan, use the executor's authorised header file:\n\n```bash\npulse_api POST /api/actions/act_.../execute <<'JSON'\n{\n  \"reason\": \"Execute approved recovery\",\n  \"planHash\": \"sha256:...\"\n}\nJSON\n```\n\nIf the response is lost or times out, inspect the existing action and target\nbefore retrying: losing the connection does not prove that execution stopped.\nAn HTTP success is not a substitute for checking the terminal result and the\nresource's observed state.\n\n5. **Read the audit and events**, using a private header file with audit access:\n\n```bash\npulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'\n```\n\n```bash\npulse_api GET /api/audit/actions/act_.../events\n```\n\nWhen audit logging is unavailable, use Pulse's action detail in the signed-in\nUI to inspect the existing action; do not infer success or repeat execution\nfrom an unavailable audit response.\n\nResponse:\n```json\n{\n  \"actionId\": \"act_...\",\n  \"requestId\": \"manual-recovery-123\",\n  \"allowed\": true,\n  \"requiresApproval\": true,\n  \"approvalPolicy\": \"admin\",\n  \"predictedBlastRadius\": [\"vm:42\", \"node-1\"],\n  \"rollbackAvailable\": false,\n  \"message\": \"Plan created for restart on web-42. Execution requires admin approval and is not performed by this endpoint.\",\n  \"plannedAt\": \"2026-05-03T10:00:00Z\",\n  \"expiresAt\": \"2026-05-03T10:05:00Z\",\n  \"resourceVersion\": \"resource:sha256:...\",\n  \"policyVersion\": \"policy:sha256:...\",\n  \"planHash\": \"sha256:...\",\n  \"preflight\": {\n    \"target\": \"vm:42\",\n    \"currentState\": \"web-42 is warning\",\n    \"intendedChange\": \"Restart the VM\",\n    \"dryRunAvailable\": false,\n    \"dryRunSummary\": \"No provider-supported dry run is advertised for this capability.\",\n    \"safetyChecks\": [\n      \"Resource was resolved from the unified resource registry.\",\n      \"Capability is advertised by the resource contract.\",\n      \"This endpoint plans only; it does not approve or execute the action.\",\n      \"Execution requires admin approval.\"\n    ],\n    \"verificationSteps\": [\n      \"Refresh the resource and confirm the expected state after execution.\",\n      \"Review /api/audit/actions/{actionId}/events for lifecycle evidence.\"\n    ],\n    \"generatedAt\": \"2026-05-03T10:00:00Z\"\n  }\n}\n```\n\n### Resource Metadata\nUser notes, tags, and custom URLs for resources.\n\nThese routes do not require admin access. Any authenticated browser session can call them, and API tokens need the scope shown.\n\n- `GET /api/agents/metadata` (`monitoring:read`)\n- `GET /api/agents/metadata/{agentId}` (`monitoring:read`)\n- `PUT /api/agents/metadata/{agentId}` (`monitoring:write`)\n- `DELETE /api/agents/metadata/{agentId}` (`monitoring:write`)\n\n- `GET /api/guests/metadata` (`monitoring:read`)\n- `GET /api/guests/metadata/{guestId}` (`monitoring:read`)\n- `PUT /api/guests/metadata/{guestId}` (`monitoring:write`)\n- `DELETE /api/guests/metadata/{guestId}` (`monitoring:write`)\n\n- `GET /api/docker/metadata` (`monitoring:read`)\n- `GET /api/docker/metadata/{containerId}` (`monitoring:read`)\n- `PUT /api/docker/metadata/{containerId}` (`monitoring:write`)\n- `DELETE /api/docker/metadata/{containerId}` (`monitoring:write`)\n\n- `GET /api/docker/runtimes/metadata` (`monitoring:read`)\n- `GET /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:read`)\n- `PUT /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n- `DELETE /api/docker/runtimes/metadata/{runtimeId}` (`monitoring:write`)\n\n### Version Info\n`GET /api/version`\nReturns version, build time, and update status.\nExample response:\n```json\n{\n  \"version\": \"6.0.0\",\n  \"buildTime\": \"2026-02-21T00:00:00Z\",\n  \"channel\": \"stable\",\n  \"deploymentType\": \"systemd\",\n  \"updateAvailable\": false,\n  \"latestVersion\": \"6.0.0\"\n}\n```\nVersion fields are returned as plain semantic versions (no leading `v`).\n\n---\n\n## 🖥️ Nodes & Config\n\n### Public Config\n`GET /api/config`\nReturns a small public config payload (update channel, auto-update enabled).\n\n### List Nodes\n`GET /api/config/nodes` (admin, `settings:read`)\n\nThis administrative inventory covers saved **PVE, PBS and PMG connections**,\nnot individual VMs or agents. Keep its response private. Use the returned\nconnection `id` for updates, deletion and tests; a display name, VMID or another\ninstallation's node name is not that ID. Re-read the inventory before a change\nrather than guessing an ID from a list position.\n\n### Add Node\n`POST /api/config/nodes` (admin, `settings:write`)\n\nAdding a connection saves credentials and enables collection; it is not a\npassive validation step. Use an existing dedicated, least-privilege platform\ntoken. The **Pulse API token** in the private header file authorises this\nconfiguration request; the **Proxmox token** in the JSON body authorises reads\nfrom the monitored platform. They are different credentials. Do not use a root\npassword or widen a department-scoped token to make denied peer reads succeed;\nfollow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).\nFor PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)\nand [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can\nalso try to create a token and ACLs on the PBS server; it is not read-only.\n\nPrepare the request in a private editor, not a credential-bearing shell\nassignment, here-document or command argument. Use the same Bash session as\nthe [private-response helper](#api-token-recommended):\n\n```bash\nnode_request_dir=$(umask 077; mktemp -d \"${TMPDIR:-/tmp}/pulse-node.XXXXXX\") &&\nnode_request_file=\"$node_request_dir/node.json\" &&\n(umask 077; set -C; : > \"$node_request_file\") &&\nvi \"$node_request_file\"\n```\n\nIf preparation or the editor fails, stop. In that editor, enter the following\nJSON, replacing the example hostname, connection name, full platform token ID\n(`user@realm!token`) and token-secret placeholder with your intended values:\n\n```json\n{\n  \"type\": \"pve\",\n  \"name\": \"PVE connection A\",\n  \"host\": \"https://pve.example.net:8006\",\n  \"tokenName\": \"pulse-monitor@pve!pulse-readonly\",\n  \"tokenValue\": \"<platform-token-secret>\",\n  \"verifySSL\": true\n}\n```\n\nSet `verifySSL` explicitly to `true`: omission on these new-connection and\ntest request paths does **not** enable certificate verification. The example\nrequires a certificate trusted by the Pulse server for that hostname. For a\nself-signed/private certificate, also supply `fingerprint` with the SHA-256\ncertificate fingerprint verified through an independent trusted channel.\nA fingerprint merely fetched from the endpoint is not established trust.\nDo not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to\nwork around a certificate failure.\n\nAfter reviewing this one intended configuration change, send the file:\n\n```bash\npulse_api POST /api/config/nodes < \"$node_request_file\"\n```\n\nThe helper retains the response privately and makes one bounded request,\nwithout retries. A lost response can follow a successful save: re-read the\nsaved inventory before deciding whether another request is needed. HTTP success\ndoes not establish fresh readings for every node, guest, backup or History view.\nKeep both request and response files out of reports and shared repositories;\nremove the temporary request under your credential policy when no longer needed.\n\n### Test Connection\n`POST /api/config/nodes/test-connection` (admin, `settings:write`)\n\nTests supplied credentials with live platform requests without saving the\nconnection. Use the same private JSON-file transport and explicit TLS settings,\nonly when a live connection test is appropriate; it is not a required precursor\nto filing a report. Success covers the reads performed by that test, not all\ncollection permissions, guest recovery or sustained monitoring.\n\n### Test Node Config (No Save, Live Request)\n`POST /api/config/nodes/test-config` (admin, `settings:write`)\n\nThis also contacts the platform using the supplied credentials; **no save**\ndoes not mean offline schema validation. Do not run either test merely to\ncollect new evidence during a freeze/thaw, backup or unresponsive-host incident.\nUse existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).\n\n### Update Node\n`PUT /api/config/nodes/{id}` (admin, `settings:write`)\n\nReview the current connection and submit only the intended supported changes\nfrom a private JSON file. Do not copy masked credentials from a GET response\nback as new secrets. Credential rotation and trust changes are separate\ndecisions; preserve the existing credential and fingerprint when not changing\nthem. A response failure does not prove the update was unapplied.\n\n### Delete Node\n`DELETE /api/config/nodes/{id}` (admin, `settings:write`)\n\nRemoves the Pulse connection, not the platform workloads or the upstream token.\nCollection and related alert coverage can stop. Check the exact connection and\narrange any needed independent coverage before an intentional removal; do not\ndelete and re-add a connection to diagnose missing readings or identity mixing.\n\n### Test Node (Legacy)\n`POST /api/config/nodes/{id}/test` (admin, `settings:write`)\n\nUses the saved connection for a live test. It is not a passive read of its\nlast result or a guarantee that ordinary collection recovered.\n\nAn optional JSON body can override `verifySSL` and `fingerprint` for this test\nwithout changing the saved connection or credentials. Omitted fields keep their\nsaved values. Send `false` to disable CA verification or `\"\"` to clear the saved\nfingerprint for the test. A nonempty fingerprint remains enforced regardless of\n`verifySSL`. Empty-body requests keep the saved TLS settings. For example,\n`{\"verifySSL\":true,\"fingerprint\":\"<SHA-256 fingerprint>\"}` tests an edited pin.\n\n### Refresh Cluster Nodes\n`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)\n\nActively rediscovers a saved PVE connection's cluster membership. It does not\nselect authorised nodes, grant missing permissions or repair an unrelated\nstandalone installation. Keep intentional token restrictions intact.\n\n### Export Configuration\n`POST /api/config/export` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:read`)\nRequest body:\n```json\n{ \"passphrase\": \"use-a-strong-passphrase\" }\n```\nReturns an encrypted export bundle in `data`. Passphrases must be at least 12 characters.\n\n### Import Configuration\n`POST /api/config/import` (instance admin for the default organization, tenant\nmanager for a selected tenant, or an API token bound to the selected\norganization with `settings:write`)\nRequest body:\n```json\n{\n  \"data\": \"<exported-bundle>\",\n  \"passphrase\": \"use-a-strong-passphrase\"\n}\n```\n\n---\n\n## 🧭 Setup & Discovery\n\n### Setup Script (Public)\n`GET /api/setup-script`\nReturns the Proxmox/PBS setup script as a shell-script download. Accepts an\noptional legacy `setup_token` query for compatibility. Current downloads\ncontain no token: paste the separately revealed token only at the silent\nterminal prompt, or supply `PULSE_SETUP_TOKEN_FILE` pointing to a mode-0600\nregular file in a mode-0700 directory owned by the script's user. Never put a\ntoken in a copied command or URL. Canonical callers must send a supported `type` of `pve` or\n`pbs` plus non-empty `host` and `pulse_url`; the route no longer generates\nplaceholder-host scripts for later repair or reconstructs Pulse identity from\nthe request origin. The route now shares the same canonical type boundary as\n`/api/setup-script-url`, rejecting unsupported node types instead of treating\nunknown values as PBS, and it normalizes the supplied `host` before script\ngeneration so downloaded artifacts and rerun URLs preserve the same canonical\nnode identity as the bootstrap response. The optional `backup_perms=true`\nquery is supported only for `type=pve`.\n\n### Setup Script URL\n`POST /api/setup-script-url` (admin, `settings:write`)\nGenerates a one-time setup token and URL for `/api/setup-script`.\nCanonical callers must send a supported `type` of `pve` or `pbs` plus a\nnon-empty `host`; the backend normalizes that host before minting the setup\ntoken and now returns the canonical bootstrap identity back in the response as\n`type`, `host`, `url`, `downloadURL`, `scriptFileName`, `command`, `commandWithEnv`,\n`commandWithoutEnv`, `setupToken`, `tokenHint`, and `expires`. The returned\ncommands are canonical root-or-sudo `curl -fsSL` bootstrap commands for the\ngenerated setup script, while `url`, `downloadURL`, and `scriptFileName` are\nthe runtime-owned artifact metadata used by copy and manual download surfaces.\nThe request body is a single canonical JSON object only; unknown fields and trailing JSON are\nrejected as invalid request shape, and `backupPerms:true` is supported only for\n`type:\"pve\"`. This route stays on the normal\nauthenticated bootstrap boundary: when Pulse auth is already configured it\nrequires a real authenticated session or API token, and setup tokens do not\nauthorize the request itself. Pulse-managed Proxmox monitor-token names on the\nsetup/bootstrap path derive from the canonical Pulse endpoint, not request-local\nhost fallbacks, so setup-script and turnkey node-add flows stay on one\ndeterministic `pulse-<canonical-scope-slug>` identity per Pulse instance.\nThe `command`, `commandWithEnv`, and `commandWithoutEnv` fields now contain\nidentical credential-free commands. They download the complete script before\nrunning it and prompt silently in the root-or-sudo process. The token crosses\nthe installer boundary through a private file, not process arguments or an\nexported secret. `downloadURL` equals the tokenless `url`, so a manual download\nalso needs the separately revealed token at runtime. `setupToken` is used for\n`/api/auto-register`; `tokenHint` remains masked on the setup page. Settings\nreveals the token in a separate dialog: run the command first, then copy and\npaste the token only at its prompt. The artifact is reused only for the same\nhost and options while its five-minute expiry is live, and discarded when the\nsetup modal closes.\nNon-frontend consumers must validate the complete artifact, including the\ncanonical host, type, URLs, filename, masked hint and live expiry. Current\nUnified Agents and the shell installer also accept the coherent older-server\nartifact during upgrades, but never execute its command text. For new\nProxmox agent enrolment against a newer server, use its current installer;\nalready enrolled agents keep reporting normally.\n\n### Auto-Register (Public)\n`POST /api/auto-register`\nRegisters a node through the canonical `/api/auto-register` contract using a temporary\nsetup token carried in the JSON `authToken` field. Canonical callers must send\na supported `type` of `pve` or `pbs`, an explicit `source` marker of `agent` or\n`script`, a canonical Pulse-managed `tokenId` in the form\n`pulse-monitor@{pve|pbs}!pulse-<canonical-scope-slug>` matching the requested\ntype, an explicit `serverName`, and missing-token requests now fail with\n`Pulse setup token required`. Incomplete token completion requests now fail\nwith `tokenId and tokenValue must be provided together`, and other missing\ncanonical request fields fail with explicit `Missing required canonical\nauto-register fields: ...` guidance.\nSuccess responses now carry the canonical stored identity and caller boundary\nback to the installer or runtime-side Unified Agent:\n`{\"status\":\"success\",\"action\":\"use_token\",\"type\":\"pve|pbs\",\"source\":\"agent|script\",\"host\":\"https://...\",\"nodeId\":\"<stored-name>\",\"nodeName\":\"<stored-name>\",...}`.\n\n### Agent Install Command\n`POST /api/agent-install-command` (admin, `settings:write`)\nGenerates an API token and install command for agent-based Proxmox setup.\n\n### Discovery\n`GET /api/discover` (admin, `settings:write`)\nReturns the last cached network discovery result from the background discovery service. It does not scan.\n\n`POST /api/discover` (admin, `settings:write`)\nRuns a manual network scan. JSON body: `{\"subnet\": \"...\", \"use_cache\": false}`. `subnet` defaults to `auto`; `use_cache: true` returns the cached result instead of scanning.\n\n### Test Notification\n`POST /api/test-notification` (admin, `settings:write`)\nBroadcasts a WebSocket test event.\n\n---\n\n## 📊 Metrics & Charts\n\n### Chart Data\n`GET /api/charts?range=1h`\nReturns time-series data for CPU, Memory, and Storage.\n**Ranges**: `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, `24h`, `7d`\n\n### Storage Charts\n`GET /api/storage-charts`\nReturns storage chart data.\n\n### Storage Stats\n`GET /api/storage/{storageId}` (`monitoring:read`)\nReturns one storage resource by its `id`, wrapped as `{\"data\": {...}, \"timestamp\": 1700000000}`. The `data` object includes `id`, `name`, `node`, `instance`, `type`, `status`, `total`, `used`, `free`, `usage`, `content`, `shared`, `enabled` and `active`. Returns HTTP 404 with code `storage_not_found` for an unknown ID, and HTTP 405 for methods other than GET. A request without an ID (`GET /api/storage/`) returns HTTP 400 with code `missing_storage_id`.\n\n### Recovery (formerly Backups / Snapshots)\nPulse v6 uses the recovery API to provide a platform-agnostic view of backup and snapshot artifacts.\nThe endpoints below carry the provider-neutral contract covering subjects,\npoints, rollups, posture, and filter semantics.\n\n- `GET /api/recovery/points`\n  - Query params:\n    - Core filters: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`\n    - Time window: `from` (RFC3339), `to` (RFC3339)\n    - Paging: `page`, `limit`\n    - Normalized filters: `q`, `cluster`, `node`, `namespace`, `scope=workload`, `verification` (`verified` | `unverified` | `unknown`)\n- `GET /api/recovery/rollups`\n  - Query params: `provider`, `kind`, `mode`, `outcome`, `subjectResourceId`, `rollupId`, `from` (RFC3339), `to` (RFC3339), `page`, `limit`\n- `GET /api/recovery/postures`\n  - Returns server-derived per-resource protection posture and provider evidence quality.\n  - Query params: repeated `resourceId` values (maximum 200), `state` (`protected` | `attention` | `unprotected` | `unknown`), `page`, `limit` (maximum 200)\n  - Batch clients must make one bounded request per 200 resource ids, never one request per table row.\n  - Unknown identity, permission, history, or collection completeness remains `unknown`; clients must not infer a healthier state from raw backup or snapshot artifacts.\n- `GET /api/recovery/series`\n  - Returns per-day counts for the activity chart.\n  - Query params: same filters as `/api/recovery/points` (except paging), plus `tzOffsetMinutes` (integer; UTC offset minutes for day bucketing)\n- `GET /api/recovery/facets`\n  - Returns distinct filter values (clusters/nodes/namespaces) and capability flags (size/verification/entity id present).\n  - Query params: same filters as `/api/recovery/points` (except paging)\n\n---\n\n## 🔔 Notifications\n\n### Send Test Notification\n`POST /api/notifications/test` (admin, `settings:write`)\nSends one test notification through a single channel per request, not all configured channels. The JSON body `method` (or `type`) selects `email`, `webhook`, or `apprise` and uses the saved configuration. `webhookId` targets one specific saved webhook. `config` tests unsaved email or Apprise settings. If alert delivery is paused, the response includes `deliveryPaused: true`.\n\n### Email, Apprise, and Webhooks\n- `GET /api/notifications/email` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/email` (admin, `settings:write`)\n- `GET /api/notifications/apprise` (admin, `settings:read`; `settings:write` also accepted)\n- `PUT /api/notifications/apprise` (admin, `settings:write`)\n- `GET /api/notifications/webhooks` (admin, `settings:read`; `settings:write` also accepted)\n- `POST /api/notifications/webhooks` (admin, `settings:write`)\n- `PUT /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `DELETE /api/notifications/webhooks/<id>` (admin, `settings:write`)\n- `POST /api/notifications/webhooks/test` (admin, `settings:write`)\n- `GET /api/notifications/webhook-templates` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/webhook-history` (admin, `settings:read`; `settings:write` also accepted)\n- `GET /api/notifications/email-providers` (admin, `settings:read`; `settings:write` also accepted)\n\nEmail and webhook configurations accept optional `tagFilter` (an array of\nresource-tag strings) and `tagFilterMode` (`\"all\"` or `\"any\"`). An omitted\nfield is preserved on update; an empty `tagFilter` clears routing and restores\ndelivery for all resources.\n\n- `GET /api/notifications/health` (admin, `settings:read`; `settings:write` also accepted)\n  - Queue health is `degraded` whenever any retained `failed` or `dlq`\n    delivery exists, and `unavailable` when queue state cannot be read.\n    Recoverable failed attempts that returned to `pending` for retry do not\n    degrade health.\n  - `queue.attention_required` is the retained terminal-failure count.\n    `reason_codes` identifies failed and/or dead-letter state without exposing\n    notification content. Counts are retention-bounded: sent, failed, and\n    cancelled rows are retained for 7 days; dead-letter rows for 30 days.\n\n### Audit Webhooks (Pro)\n- `GET /api/admin/webhooks/audit` (admin, `settings:read`)\n- `POST /api/admin/webhooks/audit` (admin, `settings:write`)\n  - Body: `{ \"urls\": [\"https://...\"] }`\n  - Strict JSON contract: unknown fields and trailing payload are rejected.\n  - Maximum `20` webhook URLs per update request.\n  - URLs are normalized (trimmed) and duplicate entries are ignored.\n  - Endpoint fails closed if URL validation runtime is unavailable.\n\n### Advanced Reporting (Pro)\n\nReport generation uses `settings:read`. For single-resource and fleet PDF\nnarration, an API token also needs `ai:execute` (or full access). Without it,\nthe report still succeeds with a deterministic summary and no AI provider call;\na single-resource PDF also explains the missing scope. Browser sessions and\nserver-run schedules keep their existing narration behaviour. AI budgets still\napply. GET and CSV never use AI narration.\n\n- `GET /api/admin/reports/catalog` (admin, `settings:read`)\n  - Returns the canonical reporting catalog for the settings surface, including locked-state teaser copy, enabled-surface guidance copy, performance-report options, canonical single-report filename subject, canonical fallback filename date style, and the nested VM inventory export definition.\n  - Metadata route: readable without the `advanced_reporting` feature so locked admin surfaces can render the same reporting definition before upsell.\n- `POST /api/admin/reports/generate` (admin, `settings:read`)\n  - Body fields: `format` (pdf/csv, default `pdf`), `resourceType`, `resourceId`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default title for that resource report.\n  - When narration is permitted and Pulse Assistant is configured, the configured AI provider narrates a PDF's executive summary; each narration counts against the AI budget and appears in AI usage. If the call fails, times out or the budget is spent, the PDF falls back to the deterministic summary. CSV output is never narrated. The settings UI uses this method.\n  - API-token callers also need the `ai:execute` scope for AI narration. With only `settings:read` the report is still produced, with the deterministic summary and a note saying why.\n- `GET /api/admin/reports/generate` (admin, `settings:read`)\n  - Takes the same fields as query params and returns the same report, except that it never calls an AI provider: a PDF carries the deterministic summary and a note saying AI narration needs `POST`. Other methods return `405` with `Allow: GET, POST`.\n- `POST /api/admin/reports/generate-multi` (admin, `settings:read`)\n  - Body fields: `resources` (1-50 entries of `{resourceType,resourceId}`), `format`, `metricType` (optional), `start`/`end` (RFC3339, optional; defaults to last 24h), `title` (optional)\n  - If `title` is omitted, the backend applies the canonical default fleet report title.\n  - AI narration of the fleet summary follows the same rule as single reports: API-token callers need `ai:execute`, otherwise the deterministic summary is used. CSV output is never narrated.\n- `GET /api/admin/reports/inventory/vms/export` (admin, `settings:read`)\n  - Query params: `format` (`csv` only; optional and defaults to `csv`)\n  - Exports the current fleet-wide VM inventory as spreadsheet-friendly CSV using the canonical runtime model.\n\nValidation and limits:\n- `start` and `end` must be RFC3339 when provided.\n- Malformed `start`/`end` values, `end` not strictly after `start`, or report windows over 366 days return `400 invalid_time_range`.\n- `metricType` must match `[a-zA-Z0-9._:-]+` and be <= 64 chars, otherwise `400 invalid_metric_type`.\n- `title` must be <= 256 chars, otherwise `400 invalid_title`.\n- Report body max size is 1MB; oversized payloads return `400 body_too_large`.\n- Report bodies reject trailing payload and unknown JSON fields with `400 invalid_body`.\n- VM inventory export only accepts `csv`.\n\nCommon reporting error codes:\n- `invalid_format`, `missing_params`, `invalid_resource_type`, `invalid_resource_id`\n- `invalid_metric_type`, `invalid_title`, `invalid_time_range`\n- `no_resources`, `too_many_resources`, `body_too_large`, `invalid_body`\n\n### Queue and Dead-Letter Tools\n- `GET /api/notifications/delivery-log?limit=200` (admin, `settings:read`)\n  - Returns recent per-attempt delivery evidence with destination identifiers,\n    alert identifiers, outcomes, timestamps, failure classes, and redacted error\n    text. Completed attempts are retained for 7 days; dead-letter attempts are\n    retained for 30 days, so the response reports both windows explicitly.\n  - `limit` defaults to 50 and is capped at 200.\n- `GET /api/notifications/queue/stats` (admin, `settings:read` and `monitoring:read`)\n  - Returns counts for all rows still retained by the queue. Status counts have\n    different retention windows and are not a delivery rate or lifetime total.\n- `GET /api/notifications/dlq` (admin)\n- `POST /api/notifications/dlq/retry` (admin)\n- `POST /api/notifications/dlq/delete` (admin, `settings:write`; `DELETE` also accepted)\n  - Body: `{ \"id\": \"<notification id>\" }`. Marks that queue row `cancelled`\n    with the note \"Manually deleted from DLQ\"; the row is not removed and is\n    retained for 7 days like other cancelled rows. The endpoint does not check\n    that the row is in the dead-letter queue.\n  - Returns `{ \"success\": true, \"message\": \"Notification deleted from DLQ\",\n    \"id\": \"<notification id>\" }`. An empty or missing `id`, or an invalid JSON\n    body, returns `400`. An unknown `id` returns `500`.\n- `POST /api/notifications/terminal-failures/retry` (admin, `settings:write`)\n  - Returns every retained `failed` or\n\n<!-- File: tests/integration/evals/scenarios.json (Tokens: ~1499 | Category: anthropic-skill) -->\n{\n  \"scenarios\": [\n    {\n      \"id\": \"multi-tenant\",\n      \"name\": \"Multi-tenant Isolation\",\n      \"description\": \"Validates org lifecycle, switching, and isolation boundaries.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/03-multi-tenant.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_MULTI_TENANT_ENABLED\": \"true\",\n          \"PULSE_E2E_ENTITLEMENT_PROFILE\": \"multi-tenant\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/multi-tenant.md\"\n      }\n    },\n    {\n      \"id\": \"retired-trial-acquisition\",\n      \"name\": \"Retired Trial Acquisition\",\n      \"description\": \"Ensures self-hosted Pulse no longer initiates hosted Pro trial acquisition from the ordinary runtime: POST /api/license/trial/start returns 404 and preserves local entitlements.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/07-retired-trial-acquisition.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/retired-trial-acquisition.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-hosting\",\n      \"name\": \"Pulse Cloud Hosted Signup\",\n      \"description\": \"Validates public cloud signup, Stripe sandbox checkout completion, and magic-link request behavior.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/08-cloud-hosting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-hosting.md\"\n      }\n    },\n    {\n      \"id\": \"cloud-billing-lifecycle\",\n      \"name\": \"Cloud Billing Lifecycle\",\n      \"description\": \"Validates post-checkout cloud lifecycle transitions (active to canceled) through Stripe + webhook processing.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/09-cloud-billing-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ]\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/cloud-billing-lifecycle.md\"\n      }\n    },\n    {\n      \"id\": \"truenas-node-add\",\n      \"name\": \"TrueNAS Node Addition\",\n      \"description\": \"Validates TrueNAS connection add, pool/dataset discovery, and unified resource visibility.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/02-node-add-truenas.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {\n          \"PULSE_E2E_TRUENAS_INSECURE\": \"true\"\n        }\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/truenas-node-add.md\"\n      }\n    },\n    {\n      \"id\": \"relay-pairing\",\n      \"name\": \"Relay Pairing & Mobile Readiness\",\n      \"description\": \"Validates relay configuration, connection to relay server, QR code generation, and mobile pairing readiness.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/03-relay-pairing.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/relay-pairing.md\"\n      }\n    },\n    {\n      \"id\": \"agent-registration\",\n      \"name\": \"Agent Install & Registration\",\n      \"description\": \"Validates host agent registration via report API, host visibility in unified state, and infrastructure page rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/04-agent-install-registration.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/agent-registration.md\"\n      }\n    },\n    {\n      \"id\": \"saml-sso-login\",\n      \"name\": \"SAML SSO Provider Lifecycle\",\n      \"description\": \"Validates SAML SSO provider CRUD, SP metadata generation, test-connection, and SSO settings UI rendering.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/05-saml-sso-login.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/saml-sso.md\"\n      }\n    },\n    {\n      \"id\": \"audit-log-reporting\",\n      \"name\": \"Audit Log + Reporting Lifecycle\",\n      \"description\": \"Validates audit event list/export/summary, audit webhook CRUD, notification webhooks, report generation, and reporting UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/06-audit-log-reporting.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/audit-reporting.md\"\n      }\n    },\n    {\n      \"id\": \"ai-patrol-lifecycle\",\n      \"name\": \"AI Patrol Finding Lifecycle\",\n      \"description\": \"Validates AI Patrol status, findings, force run, autonomy settings, approval queue, investigation, finding acknowledge/resolve lifecycle, and AI page UI.\",\n      \"deterministic\": {\n        \"command\": [\n          \"npx\",\n          \"playwright\",\n          \"test\",\n          \"tests/journeys/07-ai-patrol-finding-lifecycle.spec.ts\",\n          \"--project=chromium\",\n          \"--reporter=list\",\n          \"--retries=1\"\n        ],\n        \"env\": {}\n      },\n      \"agentic\": {\n        \"task_file\": \"evals/tasks/ai-patrol-lifecycle.md\"\n      }\n    }\n  ]\n}\n\n<!-- File: tests/integration/evals/tasks/agent-registration.md (Tokens: ~348 | Category: anthropic-skill) -->\n# Agent Registration Journey\n\n## Goal\n\nValidate the agent registration flow end-to-end: an agent sends its first\nreport, Pulse registers the agent, the agent appears in the unified state API, and\nthe agent is visible in the infrastructure UI.\n\n## Steps\n\n1. **Login** — Authenticate as admin via the Pulse UI or API.\n2. **Register agent** — Send a POST to `/api/agents/agent/report` with a\n   synthetic unified agent report payload containing `agent.type = \"unified\"`\n   plus hostname, OS info, CPU, memory, disks, and network interfaces.\n3. **Verify API state** — GET `/api/state` and confirm the new agent appears in\n   the canonical `resources[]` collection as an `agent` resource with correct\n   hostname, platform, and OS metadata.\n4. **Verify metrics** — Confirm the agent's CPU usage and memory metrics are\n   populated on that unified resource entry.\n5. **Send follow-up report** — POST a second report with different CPU/memory\n   values and verify `lastSeen` updates.\n6. **Verify UI** — Navigate to `/infrastructure` and confirm the agent's\n   hostname or display name is visible on the page.\n7. **Delete agent** — DELETE `/api/agents/agent/{agentId}` and verify the agent is\n   removed from `/api/state` `resources[]`.\n\n## Success Criteria\n\n- Agent report returns `{ success: true, agentId: \"...\" }`.\n- Agent appears in `/api/state` `resources[]` with correct metadata.\n- Agent is visible on the infrastructure page in the browser.\n- After deletion, agent no longer appears in state.\n\n<!-- File: tests/integration/evals/tasks/ai-patrol-lifecycle.md (Tokens: ~547 | Category: anthropic-skill) -->\n# AI Patrol Finding Lifecycle\n\n## Objective\nValidate the full AI Patrol closed-loop lifecycle: patrol status, findings,\nforce run, run history, autonomy settings, approval queue, investigation,\nfinding acknowledge/resolve, suppression/dismissed endpoints, and AI page UI.\n\n## Steps\n\n### Patrol Status & Configuration\n1. Log in as admin.\n2. GET `/api/ai/patrol/status` — verify response includes `enabled`, `running`, `healthy`, `summary` (with severity counts), `findings_count`, `interval_ms`.\n3. GET `/api/ai/patrol/autonomy` — verify `autonomy_level` is one of `monitor|approval|assisted|full`.\n\n### Patrol Execution\n4. POST `/api/ai/patrol/run` — trigger patrol run (accept 200, 429 rate-limited, or 503 unavailable).\n5. GET `/api/ai/patrol/runs` — verify run history array with `id`, `started_at`, `status`, `resources_checked`.\n\n### Findings & Investigation\n6. GET `/api/ai/patrol/findings` — verify array of findings with `id`, `severity`, `category`, `resource_id`, `title`.\n7. For any finding: GET `/api/ai/findings/{id}/investigation` — verify 200 (investigation exists) or 404 (no investigation yet).\n\n### Finding Lifecycle (Closed-Loop)\n8. POST `/api/ai/patrol/acknowledge` with `{ finding_id }` — verify success.\n9. GET `/api/ai/patrol/findings` — verify finding still present (acknowledged ≠ removed).\n10. POST `/api/ai/patrol/resolve` with `{ finding_id }` — verify success.\n11. GET `/api/ai/patrol/findings` — verify finding is removed from active list.\n\n### Approval Queue\n12. GET `/api/ai/approvals` — verify 200 with `approvals` + `stats`, or 402 paywall.\n\n### Supplementary Endpoints\n13. GET `/api/ai/patrol/suppressions` — verify 200 response.\n14. GET `/api/ai/patrol/dismissed` — verify 200 response.\n\n### UI Verification\n15. Navigate to `/ai` — verify AI page renders with patrol/intelligence content.\n\n## Expected Outcomes\n- Patrol status reflects actual service state.\n- Force patrol triggers or rate-limits correctly.\n- Finding lifecycle completes: acknowledge → resolve → removed.\n- Approval queue responds (200 or 402 paywall).\n- AI page renders meaningful content.\n\n<!-- File: tests/integration/evals/tasks/audit-reporting.md (Tokens: ~361 | Category: anthropic-skill) -->\n# Audit Log + Reporting Lifecycle\n\n## Objective\nValidate audit logging and reporting enterprise features: audit event\nlisting, export, summary, webhook configuration, notification webhooks,\nsingle/multi-resource report generation, and UI rendering.\n\n## Steps\n\n### Audit Logging\n1. Log in as admin.\n2. GET `/api/audit?limit=10` — verify events list or 402 paywall.\n3. GET `/api/audit/export?format=json` — verify file download (or 501 for no logger).\n4. GET `/api/audit/summary` — verify summary statistics.\n5. GET/POST `/api/admin/webhooks/audit` — CRUD audit webhook URLs.\n6. Navigate to `/settings/security-audit` — verify audit log viewer renders.\n\n### Notification Webhooks (not license-gated)\n7. GET `/api/notifications/webhooks` — list webhooks.\n8. GET `/api/notifications/webhook-templates` — verify templates available.\n9. GET `/api/notifications/webhook-history` — verify history available.\n10. GET `/api/notifications/health` — verify notification system health.\n\n### Reporting\n11. GET `/api/state` — find a resource to report on.\n12. GET `/api/admin/reports/generate?resourceType=node&resourceId=...&format=csv` — generate report.\n13. POST `/api/admin/reports/generate-multi` — generate fleet report.\n14. Navigate to `/operations/reporting` — verify reporting page renders.\n\n## Expected Outcomes\n- Audit events and exports work when `audit_logging` is licensed.\n- Reports generate when `advanced_reporting` is licensed.\n- 402 paywall responses have correct format when not licensed.\n- UI pages render relevant content or upgrade messaging.\n\n<!-- File: tests/integration/evals/tasks/cloud-billing-lifecycle.md (Tokens: ~201 | Category: anthropic-skill) -->\n# Scenario: Cloud Billing Lifecycle\n\n## Goal\nProve post-checkout billing lifecycle behavior works end-to-end for Pulse Cloud by validating tenant provisioning and cancellation transitions.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Create a real Stripe sandbox checkout from `/cloud/signup`.\n2. Complete checkout with Stripe test card details.\n3. Verify tenant reaches active state in control-plane admin tenant listing.\n4. Trigger subscription cancellation and deliver corresponding webhook event.\n5. Verify tenant transitions to canceled state after webhook processing.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n\n<!-- File: tests/integration/evals/tasks/cloud-hosting.md (Tokens: ~205 | Category: anthropic-skill) -->\n# Scenario: Pulse Cloud Hosted Signup\n\n## Goal\nProve hosted cloud signup and magic-link request flows work end-to-end using real Stripe sandbox checkout from the public signup page.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Open `/cloud/signup` and verify form renders.\n2. Submit valid workspace creation details.\n3. Complete Stripe sandbox checkout and verify return to hosted signup completion page.\n4. Request a magic-link email from the same page.\n5. Validate the magic-link request endpoint responds with success contract.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n\n<!-- File: tests/integration/evals/tasks/multi-tenant.md (Tokens: ~201 | Category: anthropic-skill) -->\n# Scenario: Multi-tenant Isolation\n\n## Goal\nProve that multi-tenant controls and isolation behave correctly from the user surface.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Sign in and open organization management.\n2. Create two organizations.\n3. Switch between orgs and confirm org-scoped data does not leak across tenants.\n4. Verify organization CRUD behavior (create/update/delete) works for the active org.\n5. Record any failures, flaky transitions, or unexpected UI states.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n\n<!-- File: tests/integration/evals/tasks/relay-pairing.md (Tokens: ~351 | Category: anthropic-skill) -->\n# Scenario: Relay Pairing & Mobile Readiness\n\n## Goal\n\nProve that relay pairing works end-to-end: configure relay settings, verify connection to the relay server, generate a QR code for mobile pairing, and confirm the onboarding deep link is available.\n\n## Environment\n\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n- Relay Host: provided via `PULSE_E2E_RELAY_HOST`\n\n## Required checks\n\n1. Relay feature is available in license entitlements (`GET /api/license/entitlements` includes `relay` capability).\n2. Relay settings can be configured with the test relay server (`PUT /api/settings/relay` with `enabled: true, server_url: \"...\"` returns 200).\n3. Relay status becomes connected within 60s (`GET /api/settings/relay/status` → `connected: true`).\n4. Onboarding QR code returns required fields (`GET /api/onboarding/qr` → `schema`, `instance_url`, `relay`, `auth_token`, `deep_link`).\n5. Onboarding deep link is available (`GET /api/onboarding/deep-link` → `url` field non-empty).\n6. Relay settings page in the UI shows relay-related content (Relay, Connected, Enabled, etc.).\n\n## Output contract\n\nWrite JSON to `{{result_json}}` with this shape:\n\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n\n<!-- File: tests/integration/evals/tasks/retired-trial-acquisition.md (Tokens: ~220 | Category: anthropic-skill) -->\n# Scenario: Retired Trial Acquisition\n\n## Goal\nProve the self-hosted Pulse instance no longer initiates a hosted Pro trial from the ordinary in-app runtime and does not mutate local entitlements when the retired route is probed.\n\n## Environment\n- Base URL: `{{base_url}}`\n- Username: `{{username}}`\n- Password: `{{password}}`\n\n## Required checks\n1. Capture current entitlements.\n2. Probe `POST /api/license/trial/start`.\n3. Confirm the response is HTTP `404`.\n4. Confirm the response does not contain legacy hosted-signup or trial-rate-limit acquisition payloads.\n5. Verify post-probe entitlements match the pre-probe entitlement summary.\n\n## Output contract\nWrite JSON to `{{result_json}}` with this shape:\n```json\n{\n  \"status\": \"pass\",\n  \"summary\": \"short outcome summary\",\n  \"evidence\": [\"bullet point 1\", \"bullet point 2\"],\n  \"issues\": []\n}\n```\n\nUse `\"status\": \"fail\"` when any required check fails.\n\n<!-- File: tests/integration/evals/tasks/saml-sso.md (Tokens: ~294 | Category: anthropic-skill) -->\n# SAML SSO Provider Lifecycle\n\n## Objective\nValidate the full SAML SSO provider lifecycle: create a SAML provider,\nverify it appears in the provider list, check SP metadata generation,\ntest-connection validation, enable/disable toggling, and cleanup via delete.\n\n## Steps\n\n1. Log in as admin.\n2. GET `/api/security/sso/providers` — verify the endpoint responds with 200.\n3. POST `/api/security/sso/providers` — create a SAML provider with stub IdP metadata.\n4. GET `/api/security/sso/providers` — verify the new provider appears.\n5. GET `/api/saml/{providerID}/metadata` — verify SP metadata XML is returned.\n6. POST `/api/security/sso/providers/test` — validate the IdP metadata.\n7. Navigate to `/settings/security-sso` — verify the SSO settings page renders.\n8. PUT `/api/security/sso/providers/{id}` — disable the provider, verify disabled.\n9. PUT `/api/security/sso/providers/{id}` — re-enable, verify enabled.\n10. DELETE `/api/security/sso/providers/{id}` — delete and verify removal.\n\n## Expected Outcomes\n- Provider CRUD works end-to-end on the Community SSO capability.\n- SSO settings page renders SSO-related content.\n\n## Environment Variables\n- `PULSE_E2E_SAML_IDP_METADATA_URL` — optional live IdP metadata URL.\n\n<!-- File: tests/integration/evals/tasks/truenas-node-add.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Tasks Skill\n\nPath: `tests/integration/evals/tasks/truenas-node-add.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/rcourtman/Pulse/HEAD/tests/integration/evals/tasks/truenas-node-add.md)\n</yakaai_skills>"}