Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

32 KiB
Raw Permalink Blame History

API contract

1. General rules

  • Base path: /api/v1.
  • JSON request/response.
  • OpenAPI is generated/validated in CI.
  • UTC RFC3339 timestamps.
  • UUID resource IDs.
  • Cursor pagination for mutable/large collections.
  • ETag or explicit version for optimistic concurrency.
  • Problem details style errors with safe message, code and correlation ID.
  • Authentication required except health and OIDC bootstrap endpoints.
  • Authorization is server-side for every resource/action.
  • All list/query inputs are bounded.

2. Error shape

{
  "type": "https://pulse.local/problems/query-limit",
  "title": "Query limit exceeded",
  "status": 422,
  "code": "QUERY_POINT_LIMIT",
  "detail": "Reduce the time range or increase the step.",
  "correlationId": "01...",
  "fields": {
    "range": "..."
  }
}

No stack trace or secret-bearing upstream response.

3. Authoritative route inventory

specs/api-routes.json is the machine-readable source for this table. python tools/check_api_contract.py verifies that every router registration is represented here, every implementation file exists, and this table has neither missing nor invented method/path combinations. Paths outside /api/v1 are limited to health, OIDC bootstrap, development-only mock login, and logout.

Route Access Implementation
GET /healthz public internal/service/health.go
GET /readyz public internal/service/health.go
GET /auth/login public-oidc internal/authapi/handler.go
GET /auth/callback public-oidc internal/authapi/handler.go
GET /auth/test-login development-only cmd/api/main.go
POST /session/logout session cmd/api/main.go
GET /api/v1/system/status view internal/systemstatusapi/handler.go
GET /api/v1/system/diagnostics operate internal/systemstatusapi/handler.go
GET /api/v1/system/metrics operate internal/observability/metrics.go
GET /api/v1/system/backups admin internal/backupapi/handler.go
POST /api/v1/system/backups admin internal/backupapi/handler.go
GET /api/v1/onboarding view internal/onboardingapi/handler.go
POST /api/v1/onboarding admin internal/onboardingapi/handler.go
GET /api/v1/widgets/catalog view internal/widgetapi/handler.go
POST /api/v1/widgets/preview edit internal/widgetapi/handler.go
GET /api/v1/metrics/catalog view internal/metricsapi/handler.go
POST /api/v1/metrics/query view internal/metricquery/handler.go
POST /api/v1/metrics/query-range view internal/metricquery/handler.go
POST /api/v1/metrics/inspect operate internal/metricquery/handler.go
GET /api/v1/live view-websocket internal/live/live.go
GET /api/v1/dashboards view internal/dashboardapi/handler.go
POST /api/v1/dashboards edit internal/dashboardapi/handler.go
GET /api/v1/dashboards/{id} view internal/dashboardapi/handler.go
PATCH /api/v1/dashboards/{id} edit internal/dashboardapi/handler.go
DELETE /api/v1/dashboards/{id} edit internal/dashboardapi/handler.go
PUT /api/v1/dashboards/{id}/document edit internal/dashboardapi/handler.go
POST /api/v1/dashboards/{id}/preview edit internal/dashboardapi/handler.go
POST /api/v1/dashboards/{id}/clone edit internal/dashboardapi/handler.go
GET /api/v1/dashboards/{id}/versions view internal/dashboardapi/handler.go
GET /api/v1/dashboards/{id}/versions/{version} view internal/dashboardapi/handler.go
POST /api/v1/dashboards/{id}/restore edit internal/dashboardapi/handler.go
POST /api/v1/dashboards/{id}/restore/{version} edit internal/dashboardapi/handler.go
GET /api/v1/entities view internal/inventoryapi/handler.go
GET /api/v1/entities/{id} view internal/inventoryapi/handler.go
GET /api/v1/entities/{id}/relations view internal/inventoryapi/handler.go
GET /api/v1/host view internal/hostapi/handler.go
GET /api/v1/processes view internal/processapi/handler.go
GET /api/v1/containers view internal/containerapi/handler.go
GET /api/v1/containers/{id} view internal/containerapi/handler.go
GET /api/v1/applications view internal/applicationapi/handler.go
GET /api/v1/events view internal/eventapi/handler.go
GET /api/v1/applications/{id} view internal/applicationapi/handler.go
GET /api/v1/array view internal/arrayapi/handler.go
GET /api/v1/disks view internal/diskapi/handler.go
GET /api/v1/disks/{id} view internal/diskapi/handler.go
GET /api/v1/pools view internal/poolapi/handler.go
GET /api/v1/pools/{id} view internal/poolapi/handler.go
GET /api/v1/shares view internal/shareapi/handler.go
GET /api/v1/shares/{id} view internal/shareapi/handler.go
GET /api/v1/forecasts view internal/forecastapi/handler.go
GET /api/v1/services view internal/serviceapi/handler.go
GET /api/v1/services/{id} view internal/serviceapi/handler.go
GET /api/v1/services/{id}/history view internal/serviceapi/handler.go
GET /api/v1/services/{id}/dependencies view internal/serviceapi/handler.go
GET /api/v1/topology view internal/serviceapi/handler.go
GET /api/v1/network view internal/networkapi/handler.go
GET /api/v1/reverse-proxy view internal/reverseproxyapi/handler.go
GET /api/v1/alert-rules view internal/alertapi/handler.go
POST /api/v1/alert-rules edit internal/alertapi/handler.go
GET /api/v1/alert-rules/{id} view internal/alertapi/handler.go
PUT /api/v1/alert-rules/{id} edit internal/alertapi/handler.go
GET /api/v1/alert-rules/{id}/versions view internal/alertapi/handler.go
POST /api/v1/alert-rules/{id}/test edit internal/alertapi/handler.go
POST /api/v1/alert-rules/{id}/enable edit internal/alertapi/handler.go
POST /api/v1/alert-rules/{id}/disable edit internal/alertapi/handler.go
GET /api/v1/alerts view internal/alertopsapi/handler.go
GET /api/v1/alerts/{id} view internal/alertopsapi/handler.go
POST /api/v1/alerts/{id}/acknowledge operate internal/alertopsapi/handler.go
POST /api/v1/alerts/{id}/unacknowledge operate internal/alertopsapi/handler.go
GET /api/v1/alert-silences view internal/alertcontrolapi/handler.go
POST /api/v1/alert-silences operate internal/alertcontrolapi/handler.go
POST /api/v1/alert-silences/preview view internal/alertcontrolapi/handler.go
POST /api/v1/alert-silences/{id}/revoke operate internal/alertcontrolapi/handler.go
GET /api/v1/maintenance-windows view internal/alertcontrolapi/handler.go
POST /api/v1/maintenance-windows operate internal/alertcontrolapi/handler.go
POST /api/v1/maintenance-windows/preview view internal/alertcontrolapi/handler.go
POST /api/v1/maintenance-windows/{id}/revoke operate internal/alertcontrolapi/handler.go
GET /api/v1/incidents view internal/incidentapi/handler.go
GET /api/v1/incidents/{id} view internal/incidentapi/handler.go
PATCH /api/v1/incidents/{id} operate internal/incidentapi/handler.go
GET /api/v1/incidents/{id}/notes view internal/incidentapi/handler.go
POST /api/v1/incidents/{id}/notes operate internal/incidentapi/handler.go
POST /api/v1/incidents/{id}/alerts/{alertId} operate internal/incidentapi/handler.go
DELETE /api/v1/incidents/{id}/alerts/{alertId} operate internal/incidentapi/handler.go

4. Endpoint behavior

Session/user

OIDC login and callback are the only public authentication routes. The browser session is server-side and intentionally has no introspection or preference endpoint in v1. Logout clears the opaque session cookie. Sessions use a bounded sliding idle lifetime: a successful authenticated request renews the HttpOnly cookie after half of the idle TTL has elapsed, while a separate absolute TTL always requires a new OIDC login. The defaults are eight hours idle and seven days absolute; production rejects an absolute TTL below 24 hours because an authenticated wallboard is a supported 24-hour workload. The mock test-login route returns 404 unless both the environment is non-production and mock authentication is configured.

Health

Liveness is served at /healthz; dependency-aware readiness is served at /readyz. Detailed authenticated state, safe diagnostics, internal metrics and backup operations live below /api/v1/system/* with the permissions in the route inventory.

Dashboards

Save accepts a complete validated dashboard document plus expected revision. Metadata update, archive, preview, clone, version reads and restore routes are listed above. Restore accepts the version in the path or request body and always creates a new current version. Import/export are UI-side validated document transfers in v1 and are not API routes. Partial widget edits may exist internally, but the user save is atomic.

Widget catalog

The catalog is built from the same validated registry used by dashboard documents. Preview is edit-protected, bounded, does not persist data and applies the same widget validation as a saved document.

Metrics

POST /api/v1/metrics/inspect accepts the same bounded semantic query request as the range endpoint, requires operate permission, and returns the resolved semantic metric, generated approved PromQL, estimated cost and applied series/point limits. It never executes the source query. The inspector field on a metric response is omitted for viewers and is safe/redacted for operators. Arbitrary label enumeration is deliberately absent in v1; callers use the allowlisted semantic catalog.

Semantic query example:

{
  "metric": "container.cpu.utilization",
  "scope": {"containerId": "uuid"},
  "range": {"from": "...", "to": "...", "stepSeconds": 15},
  "aggregation": "avg",
  "groupBy": ["container"],
  "maxSeries": 20,
  "maxPoints": 4000
}

Host

GET /host — authenticated viewer, bounded current host snapshot

The response contains host identity, uptime/boot time, CPU aggregate and bounded per-core values, load averages, memory totals/percentages, bounded filesystems/inodes, network counters/errors/drops, time synchronization and source provenance. Optional hardware contains capability states, stable temperature/fan identities and bounded GPU values. Missing optional capabilities are disabled rather than errors. source.freshness=stale or unavailable telemetry makes status.state=unknown; the endpoint never returns fabricated healthy values.

Inventory

Entity list, detail and relation reads expose the persisted inventory. Discovery scheduling and datasource configuration remain worker/runtime configuration, not public API operations in v1. No endpoint mutates host/container state.

GET /api/v1/entities accepts bounded limit (1100), opaque after, q, type, status and order=asc|desc parameters. The stable keyset is effective display name plus entity ID. Each summary exposes effective display/status values, fact/override/relation/source counts and stale-fact count; manual overrides therefore remain visible in both filtering and presentation.

GET /api/v1/entities/{id} returns aliases, all source-owned facts, overrides, effective values and incoming/outgoing relation peers. Effective values select a manual/system override before the best non-stale discovered fact while retaining every fact and its source, observation time, confidence and validity. Stale facts, tombstoned peers and absent relations are explicit states. GET /api/v1/entities/{id}/relations returns the same deterministic relation projection only. All three endpoints are authenticated, read-only and return safe problem details without database errors.

Processes

GET /processes?limit=25&sort=cpu&q=...&container=...&after=... — authenticated viewer, bounded read-only process page

The response exposes only PID, normalized process name, state, runtime, CPU/memory counters and optional known container association. Command-line arguments, environment, working directory and control actions are not part of the contract. Supported sort modes are cpu and memory; bounded name/container filters are applied before the cursor so total and pagination remain truthful.

Containers

GET /containers?limit=25&q=...&state=...&health=...&sort=name&after=... — authenticated viewer, bounded read-only container page GET /containers/{id} — authenticated viewer, bounded read-only container detail

The response preserves runtime state separately from health, includes uptime, restart/exit counters, image and digest, resource counters, ports, volumes, networks, project and bounded labels where available. Search, runtime-state and health filters are applied before cursor pagination; supported deterministic sort modes are name, CPU, memory and state. intentionalStop is explicit and must not be inferred as healthy. Source freshness and provenance are retained; unavailable sources return an Unknown snapshot. There are no start, stop, restart, delete or exec endpoints.

Applications

GET /applications — authenticated viewer, bounded application snapshot GET /applications/{id} — authenticated viewer, one application with components and contributing reasons

Applications are derived from discovered components and persisted user overrides. Aggregation keeps critical and optional component roles explicit, treats a service-down component as unhealthy even when its container is running, and returns reason codes with component IDs. No application endpoint changes infrastructure state.

Events

There is no standalone event route yet. Bounded events are projected in their owning domains (for example service history, alert occurrences and incident notes). The contract does not advertise an unimplemented aggregate event store.

Alerts

GET    /alerts
GET    /alerts/{id}
POST   /alerts/{id}/acknowledge
POST   /alerts/{id}/unacknowledge
POST   /alerts/{id}/silence
DELETE /alerts/{id}/silence

GET    /alert-rules
POST   /alert-rules
GET    /alert-rules/{id}
GET    /alert-rules/{id}/versions
PUT    /alert-rules/{id}
POST   /alert-rules/{id}/test
POST   /alert-rules/{id}/enable
POST   /alert-rules/{id}/disable

Alert-rule contract (M8-01)

Alert-rule documents conform to specs/alert-rule.schema.json. The condition references only a semantic metric name from the server-side metric catalog; arbitrary PromQL, query templates, interpolation, and unbounded scope values are rejected. Reads require view; create, update, test, enable and disable require edit.

PUT /alert-rules/{id} requires the current revision in If-Match (or the revision query parameter). A stale revision returns 409 REVISION_CONFLICT. Every accepted document change creates an immutable version; GET /alert-rules/{id}/versions returns versions newest first. Enabling and disabling increment the revision and create an audit event.

POST /alert-rules/{id}/test accepts an optional rule document and bounded sample value. It evaluates in memory and returns a preview only; it does not create versions, mutate enabled state, evaluate live sources, or write audit events. A rule condition may define a lower (for high-threshold rules) or higher (for low-threshold rules) recoveryThreshold; pending and resolve durations are evaluated at UTC observation timestamps. cooldownSeconds suppresses duplicate firing notifications after recovery without deleting occurrence history.

Incidents

Incidents are created by the correlation pipeline in v1. The API exposes bounded list/detail, ownership update, note reads/writes and explicit alert association/disassociation. There is no manual incident-creation route.

Probes/services

Service list, detail, history, dependency and topology routes are read-only. Probe configuration is owned by the worker/runtime boundary in v1: service detail may expose a bounded safe probe summary, but targets, credentials and mutation/test endpoints are deliberately absent. This prevents the observability API from becoming a general network-request primitive.

Maintenance

Silences and maintenance windows support bounded list, create, preview and revision-checked revoke operations. Records are immutable evidence after creation; revoke replaces generic update/delete semantics.

Operations

Administrative backup list/create is exposed at /api/v1/system/backups. Safe diagnostics and internal metrics use /api/v1/system/diagnostics and /api/v1/system/metrics. Restore, backup verification and general audit-log browsing remain CLI/operations procedures until their controlled workflows are implemented; they are not advertised API routes.

5. WebSocket

Endpoint:

GET /api/v1/live

Requirements:

  • authenticated before upgrade;
  • same-origin/allowed-origin check;
  • role-aware subscription authorization;
  • message size/rate limits;
  • heartbeat and idle timeout;
  • per-session subscription/series limits;
  • sequence numbers and resync;
  • explicit unsubscribe;
  • no arbitrary backend URL/query.

Message schemas are in specs/live-message.schema.json.

6. Idempotency

Use idempotency keys for:

  • acknowledgement;
  • incident note/create where retries matter;
  • notification delivery;
  • backup start;
  • manual discovery.

7. Compatibility

  • Breaking changes require /api/v2 or a documented migration.
  • Schema versions are explicit in dashboard/export/live messages.
  • Old dashboard versions are migrated through tested deterministic migrations.

Array and parity

GET /array is an authenticated viewer endpoint for one bounded, read-only array snapshot. The response contains source freshness/provenance, operational/degraded/missing/unknown state, parity presence/state/errors, bounded data/parity members, current check progress/speed/errors and bounded check history. Stale or unavailable storage source data is Unknown; there are no array start, stop, check or correct endpoints.

Disks

GET /disks?limit=... and GET /disks/{id} are authenticated viewer endpoints for bounded, read-only disk inventory and detail. Responses preserve source freshness/provenance and expose canonical stable source IDs, role, availability state, model, privacy-aware serial display, filesystem, size, used/free/utilization, separate capacitySeverity and thermalSeverity, and optional inode capacity. Missing-disk history remains visible; raw serials are never returned. Exact duplicate source observations are idempotently collapsed, while conflicting observations for one canonical ID fail closed. No format, mount, unmount, remove or repair endpoint exists.

SMART detail

Disk detail may include a bounded SMART object with capability state (available or unknown), generic overall result, freshness timestamp, explicit critical attributes/reasons and self-test result/age. Pending, reallocated, offline-uncorrectable, CRC and wear signals remain separate attributes; a generic passed result does not suppress an attention reason. SMART has no write or self-test trigger endpoint.

Disk telemetry

Disk detail may include bounded performance samples (read/write bytes per second, IOPS and latency), temperature state/value and optional spin capability. Unsupported latency or spin is explicit rather than fabricated; history is bounded and deterministically ordered. Temperature status applies warning/critical/recovery hysteresis, while live/history values remain read-only.

Pools

GET /pools?limit=... and GET /pools/{id} are authenticated viewer endpoints for bounded, read-only cache/Btrfs/ZFS pool snapshots and detail. Responses preserve source freshness/provenance and expose filesystem, device-health state, usable/used/free capacity, separate capacitySeverity, profile/redundancy, deduplicated bounded members, filesystem errors, scrub state/history and explicit capability states. Capacity pressure never rewrites device health. Stale pool data maps every pool state and severity to Unknown; no scrub, repair, balance, mount or pool-control endpoint exists.

Shares

GET /shares?limit=... and GET /shares/{id} are authenticated viewer endpoints for bounded, read-only share usage and growth. Responses expose configured storage policy, source-owned size observations, cache/pool placements, bounded growth history and scan-plan cost metadata. Size scans are rate-limited/cached through the normalized scan plan; stale or unavailable observations are Unknown. No path, filename, directory listing or file-content field is returned.

Storage map and heatmap views

The storage view composes authenticated read-only array, pool and disk snapshots in the browser. Array membership and disk telemetry with the same canonical physical ID form one disk node; a pool aggregate remains a separate meaningful role. Every node states availability/device health, capacity and temperature independently, while the strongest trustworthy severity determines its visual accent. Map nodes retain entity links and text/icon state; the temperature heatmap is bounded and always includes a table/text alternative. No new control endpoint or raw source credential is exposed.

Capacity forecasts

GET /api/v1/forecasts is an authenticated, read-only endpoint. Each bounded assessment exposes the entity identity, method, historical window, point count, confidence and reason. qualifiedCount counts only medium/high forecasts and is intentionally distinct from items.length, which may include insufficient or stale assessments. A projected date is only returned for qualified (medium or high) median-rate forecasts; disabled policy, insufficient data, bulk-import detection, irregular intervals and unknown/reached capacity remain explicit states without a false date. When no real capacity entity exists, items is empty and the top-level reason explains the Unknown state; the API never synthesizes a blank none/0-byte entity. The response contains no write or capacity-management operation.

M7 service and probe model

Service and probe configuration is versioned and archive-oriented. Services, endpoints and probes expose revisions for optimistic concurrency; active names are unique per parent while archived configurations remain available for historical result retention. Probe results and certificate observations are append-only by probe/service and UTC observation time. Service dependencies retain source and confidence, and service permissions map existing Pulse roles to view/operate/edit/admin without storing probe credentials.

M7 probe target safety

Probe target policy is evaluated server-side before every request and again for redirects. HTTP/HTTPS GET/HEAD are the default methods; request headers, response headers/body, timeout and redirect count are bounded. Loopback, link-local, metadata, multicast and unspecified addresses are blocked, and private/LAN targets require an explicit CIDR allowlist. DNS answers are revalidated at dial time so a later blocked answer cannot be used for DNS rebinding. Policy changes emit audit events with redacted bounded policy metadata.

M7 probe execution

Probe execution supports HTTP/HTTPS, TCP, DNS and TLS within the server-side network policy. HTTP status and bounded keyword/JSON assertions are represented as up, down, degraded or unknown; redirect following is opt-in and each hop is revalidated. TLS results retain bounded expiry, issuer, subject and hostname-validity facts. ICMP is capability-aware and returns Unknown with an unsupported class when the runtime cannot provide it. Probe configuration may contain only a secret reference; credentials are never returned by the API or included in result errors.

M7 service status and history

GET /api/v1/services?limit=... and GET /api/v1/services/{id} are authenticated, read-only service projections. GET /api/v1/services/{id}/history?limit=... returns bounded probe history. The projection derives service state from probe results independently of container state, preserves last success/failure and latency, calculates bounded availability, and maps absent or stale probe data to unknown. Results, histories and transition events are deterministically ordered and capped. Service detail may include a bounded, read-only probe configuration summary (id, name, type, interval, timeout, enabled state and safe TLS/redirect flags); target payloads, content assertions, secret references and credentials are never returned.

Service snapshots expose separate capabilityState (available, unavailable, unsupported) and configurationState (configured, not_configured, unknown). A catalog without services is available/not_configured; an unreadable repository is unavailable/unknown. Per-service reasons distinguish no configured probe, disabled probes, pending results and stale results. These states are also propagated into topology and each network-health scope so an empty configuration is never presented as a generic source failure.

GET /api/v1/services/{id}/dependencies?limit=... is an authenticated, read-only bounded relation projection. Dependencies are sorted deterministically and expose source, confidence and confirmed/inferred state; dependency writes remain server-side and audit-backed.

GET /api/v1/topology?limit=... is an authenticated, read-only graph projection bounded to at most 100 nodes and 200 edges by default. Nodes expose service status and whether they are present in the current snapshot; missing nodes remain explicit unknown. Edges combine active service dependencies with active depends_on, backs and exposes inventory relations when both inventory entities map to active services. Every edge preserves relation type, source, confidence and confirmed/inferred state, is sorted deterministically, and never asserts causality. The endpoint has no write operation.

GET /api/v1/network is an authenticated, read-only network health projection. It separates internal interface health, gateway, DNS and internet scopes; stale/unavailable host telemetry maps interfaces and internal health to unknown, while independent internet or DNS failures remain independently visible. Interface RX/TX bytes, errors and drops are bounded and certificates are exposed as safe observed facts without probe credentials.

GET /api/v1/reverse-proxy is an authenticated, read-only projection of optional reverse-proxy host mappings. The connector reports a versioned capability and source ownership, returns only bounded host/URL-to-service mappings, and exposes a disabled or unavailable state when no connector is configured. Connector credentials remain external references and are never serialized. Duplicate host mappings with different targets are rejected unless a user-confirmed override explicitly selects the effective service; no proxy configuration mutation endpoint exists. Topology may represent enabled mappings as reverse_proxy nodes with exposes edges that retain source and confirmed/inferred provenance.

Alert silences and maintenance windows (M8-06)

GET    /alert-silences
POST   /alert-silences
POST   /alert-silences/preview
POST   /alert-silences/{id}/revoke
GET    /maintenance-windows
POST   /maintenance-windows
POST   /maintenance-windows/preview
POST   /maintenance-windows/{id}/revoke

Reads and matcher previews require view; create and revoke require operate. Silences and maintenance windows require a non-empty reason, bounded matcher and selector fields, and an expiry or end time. Revocation uses If-Match or the revision query parameter. Controls never delete alert history. Every create and revoke is audited; expiry is idempotently marked by the API expiry job.

Alert operations (M8-07)

GET /alerts returns a bounded deterministic list of non-inactive alert instances; the state query filter is allow-listed. GET /alerts/{id} returns rule/entity metadata, current state, revision and bounded occurrence history.

POST /alerts/{id}/acknowledge and POST /alerts/{id}/unacknowledge require operate permission, If-Match or a positive revision query parameter, and a bounded Idempotency-Key or evaluationKey. A replay with the same key is idempotent even when the original revision is stale. Acknowledge changes firing or pending to acknowledged; unacknowledge changes acknowledged back to firing. Resolved state remains a persisted evaluator result and is not rewritten by a read or duplicate operation. Accepted and idempotent operations are audited.

Notification delivery contract

Notification delivery is an internal worker contract in M8-08. Public alert operations create no direct channel side effect: an event is first represented by a bounded outbox record with an idempotency key. Workers claim and complete records transactionally, and delivery history remains queryable for audit. Channel test actions are explicitly bounded and rate limited; no secret value is returned by the channel or delivery model.

M8-09 incident contract

GET /incidents and GET /incidents/{id} are bounded viewer operations. POST /incidents/{id}/alerts/{alertId} and DELETE /incidents/{id}/alerts/{alertId} require operate permission and create audit events. Manual association requests require bounded rationale and confidence; the response exposes whether the association was idempotent. Incident operations remain observational and do not remediate alerts, services, or infrastructure.

M8-10 incident detail, ownership and notes

GET /incidents/{id} returns bounded incident detail, associations and ordered notes. PATCH /incidents/{id} requires operate permission and updates only the owner user ID with the current positive revision and returns a conflict for stale writes. GET /incidents/{id}/notes is viewer-readable; POST /incidents/{id}/notes requires operate permission and stores the authenticated actor as author. Note bodies are bounded plain text: tag-shaped markup is removed, whitespace is normalized, and empty or oversized values are rejected. Incident UI communicates correlation confidence and rationale as uncertainty-aware evidence; Pulse provides no remediation action.

Onboarding

GET /api/v1/onboarding is an authenticated viewer endpoint. It returns only bounded readiness states for the database, Prometheus, read-only Unraid source, authentication, alert defaults and the default dashboard. It never returns datasource URLs, API tokens, OIDC client secrets or other secret-bearing configuration.

POST /api/v1/onboarding requires the administrator permission and accepts only dashboard and rules choices with values default or skip. Completion is additive and resumable: it records progress in system_settings, installs the validated default dashboard only when the stable slug is absent, and preserves existing dashboards and alert rules. It exposes no host, Docker, storage, database-control or remediation operation.

Authentication bootstrap

GET /auth/login and GET /auth/callback are the only unauthenticated non-health endpoints. They are browser redirect endpoints outside /api/v1; they return 302 rather than JSON, except for a problem-details 405 on a non-GET method.

GET /auth/login starts the Authentik OIDC authorization code flow with state, nonce and PKCE S256. State, nonce, PKCE verifier and the requested post-login path are stored server-side in a bounded, TTL-expired flow store; the browser receives only an opaque flow identifier in a short-lived HttpOnly, SameSite=Lax cookie that is Secure in production. The optional redirect query parameter is accepted only as an in-app path with a single leading slash: protocol-relative, absolute, backslash-prefixed, control-character and oversized values fall back to /. The response redirects to the provider authorization URL.

GET /auth/callback consumes the flow identifier exactly once, deleting the stored flow before validating anything. It compares state in constant time, exchanges the code with the PKCE verifier, verifies the ID token issuer, audience, signature and nonce, maps the configured role claim to a Pulse role and only then issues the session cookie and redirects to the validated post-login path. An unknown, replayed or expired flow, a state or nonce mismatch, a provider error response, a failed exchange, a missing ID token, an unmapped role and a failed audit or session write all issue no session and redirect to a fixed in-app error route with one of a closed set of reason codes (invalid_request, expired, denied, provider_unavailable, not_authorized, unavailable). Provider-supplied error text, authorization codes, tokens and PKCE verifiers are never reflected into a response or logged.