diff --git a/apps/web/src/pages/room-detail.tsx b/apps/web/src/pages/room-detail.tsx index 28c7eb4f..f3e3ddc2 100644 --- a/apps/web/src/pages/room-detail.tsx +++ b/apps/web/src/pages/room-detail.tsx @@ -80,7 +80,8 @@ export function RoomDetailPage({ roomId }: { roomId: string }) { onError: (error) => { if (apiErrorStatus(error) === 409) { toast.error("Room in use", { - description: "This room still has nodes assigned and cannot be deleted.", + description: + "This room is still referenced by one or more schedules and cannot be deleted.", }); return; } @@ -188,7 +189,7 @@ export function RoomDetailPage({ roomId }: { roomId: string }) { deleteRoomMutation.mutate()} title={`Delete room "${room.name}"?`} diff --git a/docs/architecture/controller-api.md b/docs/architecture/controller-api.md index 951b3867..eaa614f0 100644 --- a/docs/architecture/controller-api.md +++ b/docs/architecture/controller-api.md @@ -58,7 +58,9 @@ issued at enrollment / rotation and stored only as hashes. ## The RBAC + audit route pattern Every user-facing route follows the same shape, enforced by the -`requirePermission(permission, action, targetFn)` middleware in `index.ts`: +`requirePermission(permission, action, targetFn)` middleware — created by +`createAuthorization(...)` in `apps/api/src/index-authorization.ts` and wired into +the `index.ts` composition root: ```text requirePermission → authenticate → resolve audit target @@ -136,8 +138,11 @@ Tuning knobs (intervals, batch sizes, leases, enable flags) are in the ## Error shapes -There is no global error middleware; routes return JSON inline with conventional -status codes: +A global `app.onError` handler (`apps/api/src/index.ts`) catches uncaught errors, +mapping `DatabaseUnavailableError` → 503 +`{ error: "Service temporarily unavailable", reason: "database_unavailable" }` and +everything else → 500 `{ error: "Internal server error" }`. Routes still return +most status codes inline with conventional shapes: | Status | Shape | When | | ------ | --------------------------------------- | ---------------------------------------- | diff --git a/docs/architecture/data-model.md b/docs/architecture/data-model.md index b8795bf6..15913ceb 100644 --- a/docs/architecture/data-model.md +++ b/docs/architecture/data-model.md @@ -35,7 +35,8 @@ The Drizzle client (`packages/db/src/client.ts`) opens a small `postgres.js` poo ## Tables -The schema (`packages/db/src/schema.ts`) defines 37 tables plus Postgres enums +The schema — assembled in `packages/db/src/schema.ts` from per-subsystem modules +under `packages/db/src/schema/` — defines 37 tables plus Postgres enums (`node_status`, `health_severity`, `recording_status`, `recording_job_status`, `recording_chunk_status`, `recording_source`, `audit_outcome`, `access_policy_effect`, `access_policy_subject_type`, `room_roster_subject_type`, @@ -116,8 +117,8 @@ The schema (`packages/db/src/schema.ts`) defines 37 tables plus Postgres enums ## Migrations Migration SQL lives in `packages/db/drizzle/*.sql` with snapshots under -`drizzle/meta/`; **migrations are committed alongside schema changes** (~40 to -date, highest `0039`). The workflow: +`drizzle/meta/`; **migrations are committed alongside schema changes** (~47 to +date, highest `0046`). The workflow: ```powershell mise run db:generate # drizzle-kit generate — emit SQL from schema.ts @@ -131,9 +132,9 @@ is part of the full `mise run check` gate and requires a working Postgres. ## Shared contracts -`@rakkr/shared` (`packages/shared/src/index.ts`) is a single Zod-based module -that both API and console import, keeping entity and request/response shapes in -sync. It exports: +`@rakkr/shared` is a set of Zod-based domain modules re-exported from +`packages/shared/src/index.ts`, which both API and console import, keeping entity +and request/response shapes in sync. It exports: - **Domain schemas + inferred types** for nearly every model (nodes, interfaces, meter frames, recordings, jobs, profiles, schedules, health/audit events, diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 46339c48..7882999b 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -21,7 +21,7 @@ flowchart TB subgraph controller["Controller"] api["Controller API (Hono)"] db[("Postgres + Drizzle\n(JSON fallback)")] - runners["Background runners\nschedule · watchdog · upload · retention · job-lease"] + runners["Background runners\nschedule · watchdog · upload · retention · job-lease · switcher-routing"] api <--> db api --- runners end diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md index 514f3908..cb62ff91 100644 --- a/docs/getting-started/concepts.md +++ b/docs/getting-started/concepts.md @@ -24,7 +24,9 @@ source of truth for room identity. **Node** — a Linux machine running the recorder agent. Identified by a stable ID and described by alias, site/building/floor/room, hostname and IPs, agent version, OS/kernel, audio backends, tags, notes, and a live status -(`online` / `offline` / `recording` / `degraded` / `alerting`). Nodes derive +(`provisioning` / `online` / `offline` / `recording` / `degraded` / `alerting`). +`provisioning` is the initial pre-contact status of an enrolled node that has +never made contact, and is excluded from offline/liveness derivation. Nodes derive `offline` automatically after a missed-heartbeat threshold. **Audio interface** — a capture device on a node (e.g. an ALSA card). Carries a @@ -61,8 +63,9 @@ recurrence (`manual`, `once`, `daily`, `weekly`, `monthly`, `always_on`), an explicit timezone, start-early/stop-late buffers, and exceptions (skip a date or pause a range). Schedules _own_ the metadata of the recordings they create (name, folder, tags, -profile, watchdog policy, retention, upload policy). No cron syntax is ever -exposed. +profile, watchdog policy, retention, upload policies). A schedule carries a list +`uploadPolicyIds`, fanning one recording out to several destinations. No cron +syntax is ever exposed. ## Quality and metering @@ -129,8 +132,13 @@ correlation IDs, and before/after snapshots where relevant. **Cache** — the local copy of a recording on the recorder node and/or the controller. Cache retention only runs _after_ a confirmed upload. -**Upload provider / upload queue** — the destinations (SMB, S3) and the -retry queue that moves cached recordings to them. +**Upload destination / upload policy / upload queue** — an **upload destination** +is a named SMB or S3 target; an **upload policy** selects one destination plus an +optional subfolder (and its trigger/retry/delete-after-upload behavior). Recordings +and schedules reference a list of policies, and the retry **upload queue** moves +each cached recording to every selected destination (one item per policy). The +legacy `upload_providers` (one row per kind) is a backfill concept superseded by +destinations. **Node lifecycle action** — an allowlisted remote operation run against a node's host over SSH via the Ansible runner: `install_dependencies`, `update_binary`, diff --git a/docs/guides/audio-enhancement.md b/docs/guides/audio-enhancement.md index 58fd2f2c..4a91be54 100644 --- a/docs/guides/audio-enhancement.md +++ b/docs/guides/audio-enhancement.md @@ -37,13 +37,14 @@ The enhancement chain lives on the **recording profile** (the preset/template), it is RBAC-gated and audited like any other settings change. Every stage is independently toggleable with configurable parameters, applied in this order: -1. **High-pass** — remove rumble / HVAC / handling (default on, 80 Hz). -2. **Denoise** — DeepFilterNet3 or RNNoise (default on, DeepFilterNet3). -3. **De-esser** — tame sibilance (default off). -4. **Compressor** — even out speakers at different mic distances (default off). -5. **Loudness normalization** — EBU R128, so every recording sits at a consistent +1. **Denoise** — DeepFilterNet3 or RNNoise, applied in-process before any ffmpeg + filter (default on, DeepFilterNet3). +2. **High-pass** — remove rumble / HVAC / handling (default on, 80 Hz). +3. **Low-pass** — optional high-frequency roll-off (default off). +4. **De-esser** — tame sibilance (default off). +5. **Compressor** — even out speakers at different mic distances (default off). +6. **Loudness normalization** — EBU R128, so every recording sits at a consistent level (default on, −16 LUFS / −1.5 dBTP / 11 LRA). -6. **Low-pass** — optional high-frequency roll-off (default off). 7. **Noise gate** — optional, threshold in dB (default off). `keepRaw` (default on) controls whether the raw master is uploaded alongside the diff --git a/docs/guides/authentication-and-rbac.md b/docs/guides/authentication-and-rbac.md index 1814333c..6e36a1f3 100644 --- a/docs/guides/authentication-and-rbac.md +++ b/docs/guides/authentication-and-rbac.md @@ -61,7 +61,9 @@ access policies ─┘ 1. **Roles → permissions.** Each role maps to a fixed permission set (defined in `@rakkr/shared`). `owner` has everything; `admin` has everything except - `system:admin`; `operator`, `viewer`, and `auditor` are progressively narrower. + `system:admin`; `operator`, `viewer`, and `auditor` are each narrower in scope + (note `auditor` is audit-focused — it holds `audit:read`, which `viewer` lacks — + rather than a strict subset of `viewer`). See the [permissions reference](../reference/permissions.md). 2. **Resource scope.** Having a permission isn't enough — the actor must be in scope for the target. `owner`/`admin` bypass scope; everyone else needs a diff --git a/docs/how-to/configure-channel-maps.md b/docs/how-to/configure-channel-maps.md index ee7d78f6..6f492f2c 100644 --- a/docs/how-to/configure-channel-maps.md +++ b/docs/how-to/configure-channel-maps.md @@ -8,8 +8,8 @@ sidebar: # Configure channel maps A **channel map** is a reusable template that decides how a device's capture -channels become the outputs of a recording — mono, stereo, grouped, or a -mono-to-stereo mix. Maps let you apply the same routing to many nodes at once +channels become the outputs of a recording — Mono, Stereo, Mono To Stereo Mix, +or Multichannel. Maps let you apply the same routing to many nodes at once instead of configuring each recording by hand. > **Who can do this:** viewing needs `settings:read`; creating and editing need @@ -22,17 +22,23 @@ routing."*). ## Create or edit a channel map -1. Click **New** (or the pencil on an existing map). -2. Choose the **node and interface** it targets. -3. Define how each capture channel maps to an output (the output mode — mono, - stereo pair, grouped, or mono-to-stereo). -4. Save. +1. Click **New** (or the pencil on an existing map). A channel map is a + **target-agnostic template** — it defines routing, not which hardware it runs + on. +2. Set the template's **Name**, **Mode**, and **Tags**, then define each + **per-channel entry** — how each capture channel maps to an output. The + **Mode** is one of **Mono**, **Stereo**, **Mono To Stereo Mix**, or + **Multichannel**. +3. **Promote** the revision (**Promote Rev N**) to save it; use **Reset** to + discard unsaved changes. ## Assign, stage, and roll back Channel maps are built for fleet management: -- **Bulk-assign** a map to many node/interface targets at once. +- Bind a map to hardware with **Assign Target** (a single node or interface) or + **Bulk Targets** (many at once) — a target can be a whole node or a specific + interface. - Changes are **staged behind an explicit apply step**, so nothing changes on the hardware until you apply it. - Maps are **versioned** and can be **rolled back** if an apply causes trouble. diff --git a/docs/how-to/configure-recording-profiles.md b/docs/how-to/configure-recording-profiles.md index 86c10180..d1dc7d33 100644 --- a/docs/how-to/configure-recording-profiles.md +++ b/docs/how-to/configure-recording-profiles.md @@ -27,10 +27,9 @@ Open **Settings** in the left nav and scroll to **Recording Profiles** - **Bitrate** and **VBR** (for MP3). - **Channel mode**. - Optional **silence handling**. - - **Maximum track length** — used to auto-split long scheduled captures into + - **Chunk Length (seconds)** — used to auto-split long scheduled captures into chunks. -3. Toggle **Enabled** so it's available to operators. -4. Save. +3. Save. The built-in default is a voice MP3-VBR profile (~128 kbps). Defaults are configuration, never hard-coded — so change them freely. @@ -42,15 +41,16 @@ is one default per type, so setting a new one clears the previous. ## Voice enhancement Each profile also carries a **voice-enhancement chain** that produces an -**enhanced** rendition alongside the always-preserved **raw** master. The stages, -applied in order, are each independently toggleable: - -1. **High-pass** (default on, 80 Hz) — remove rumble/HVAC/handling. -2. **Denoise** (default on) — DeepFilterNet3 or RNNoise. -3. **De-esser** (default off) — tame sibilance. -4. **Compressor** (default off) — even out speakers at different distances. -5. **Loudness normalization** (default on, EBU R128) — consistent levels. -6. **Low-pass** (default off) — high-frequency roll-off. +**enhanced** rendition alongside the always-preserved **raw** master. The chain +applies **denoise first**, then the ffmpeg voice filters; every stage is +independently toggleable: + +1. **Denoise** (default on) — DeepFilterNet3 or RNNoise. +2. **High-pass** (default on, 80 Hz) — remove rumble/HVAC/handling. +3. **Low-pass** (default off) — high-frequency roll-off. +4. **De-esser** (default off) — tame sibilance. +5. **Compressor** (default off) — even out speakers at different distances. +6. **Loudness normalization** (default on, EBU R128) — consistent levels. 7. **Noise gate** (default off). `keepRaw` (default on) controls whether the raw master is uploaded alongside the diff --git a/docs/how-to/enroll-and-configure-nodes.md b/docs/how-to/enroll-and-configure-nodes.md index 2759602b..718a5244 100644 --- a/docs/how-to/enroll-and-configure-nodes.md +++ b/docs/how-to/enroll-and-configure-nodes.md @@ -23,7 +23,8 @@ and shows the copy-paste installer one-liner: ```bash curl -fsSL https://rakkr.org/agent.sh | sudo sh -s -- \ --controller-url https://controller.example:8787 \ - --bootstrap-token rakkr_bs_… --node-id node_… + --bootstrap-token rakkr_bs_… --node-id node_… \ + --site … --room … ``` Run it on the fresh Linux host: it installs the latest agent, generates the diff --git a/docs/how-to/manage-groups-and-access.md b/docs/how-to/manage-groups-and-access.md index ff24f459..696480a3 100644 --- a/docs/how-to/manage-groups-and-access.md +++ b/docs/how-to/manage-groups-and-access.md @@ -19,7 +19,7 @@ An **access group** is a named set of users you can assign in one shot to schedules, room rosters, and access policies. 1. Open **Access** and find the **Groups** section. -2. Click **New group** and give it a **name** and optional **description**. +2. Click **Add group** and give it a **name** and optional **description**. 3. Use **Members** to add or remove users. 4. **Delete** a group to remove it everywhere it was used. diff --git a/docs/how-to/manage-rooms.md b/docs/how-to/manage-rooms.md index f580a2ab..7b401bc9 100644 --- a/docs/how-to/manage-rooms.md +++ b/docs/how-to/manage-rooms.md @@ -53,7 +53,7 @@ point at — so getting rooms right is the foundation for who can reach what. ## What's on the room detail page - The editable **identity** (name, site, building, floor, description, notes). -- The room's **node/channel inventory**. +- The room's **node inventory**. - Its **upcoming scheduled occurrences** — with who booked each. - Its **recent recordings**. - Its **access roster** — edited here; see diff --git a/docs/how-to/manage-users.md b/docs/how-to/manage-users.md index 6f69b5fc..034a8172 100644 --- a/docs/how-to/manage-users.md +++ b/docs/how-to/manage-users.md @@ -15,7 +15,7 @@ hold. ## Add a user 1. Open **Access** in the left nav. -2. In the **Users** section, click **New user**. +2. In the **Users** section, click **Add user**. 3. Set the **name**, **email**, an initial **password**, and one or more **roles**. 4. Click **Create**. @@ -39,7 +39,7 @@ bypass scope. See the [permissions reference](../reference/permissions.md). For any user row: -- **Edit** — change name, roles, and enabled state. +- **Edit access** — change roles, group memberships, and resource scopes. - **Reset password** — set a new password. - **Enable / disable** — the toggle. - **Delete** — remove the account. diff --git a/docs/how-to/navigate-the-console.md b/docs/how-to/navigate-the-console.md index 7b49ed71..1b597388 100644 --- a/docs/how-to/navigate-the-console.md +++ b/docs/how-to/navigate-the-console.md @@ -69,9 +69,10 @@ The left nav only shows pages you're allowed to see, always in this order: Rakkr colour-codes status everywhere. The recurring vocabularies: -- **Node status** — `online` / `recording` (green, healthy), `degraded` / - `alerting` (amber, needs attention), `offline` (grey/red). A node goes - **offline** automatically after it misses heartbeats. +- **Node status** — `provisioning` (awaiting first contact), `online` / + `recording` (green, healthy), `degraded` / `alerting` (amber, needs attention), + `offline` (grey/red). A node goes **offline** automatically after it misses + heartbeats. - **Recording / job status** — `queued`, `running`, `stop_requested`, `completed`, `failed`, `cancelled`. - **Health severity** — `info` (blue), `warning` (amber), `critical` (red). diff --git a/docs/how-to/record-a-session.md b/docs/how-to/record-a-session.md index 446f8a8f..05b0e8cc 100644 --- a/docs/how-to/record-a-session.md +++ b/docs/how-to/record-a-session.md @@ -47,8 +47,8 @@ record the whole interface. Because you can pick channels, **several recordings can run on the same interface at once**, each on its own channels — for example sixteen independent stereo recordings on a 32-channel interface. If you pick channels another -recording is already using, Rakkr refuses with **"channels busy"**; recordings on -non-overlapping channels run simultaneously. +recording is already using, Rakkr refuses with **"Requested channels are already +in use"**; recordings on non-overlapping channels run simultaneously. ## Stop a recording diff --git a/docs/how-to/respond-to-health-alerts.md b/docs/how-to/respond-to-health-alerts.md index 92f5c06a..bb63ac64 100644 --- a/docs/how-to/respond-to-health-alerts.md +++ b/docs/how-to/respond-to-health-alerts.md @@ -35,14 +35,15 @@ caught while you can still fix it. it's attached to. 2. **Filter** by status, severity, type, node, schedule, recording, and opened/resolved date ranges. -3. Expand an event for its detail and timeline, then act: - - | Action | Use it when… | - | --------------- | --------------------------------------------------------------- | - | **Acknowledge** | You've seen it and are working on it. | - | **Suppress** | It's expected (e.g. known maintenance) — mute it for a while. | - | **Resolve** | It's handled / recovered. | - | **Reopen** | It came back or was resolved prematurely. | +3. Act on an event with the always-visible inline buttons in its **Actions** + column: + + | Action | Use it when… | + | ----------- | --------------------------------------------------------------------- | + | **Ack** | You've seen it and are working on it. | + | **Mute 1h** | It's expected (e.g. known maintenance) — a fixed one-hour suppression. | + | **Resolve** | It's handled / recovered. | + | **Reopen** | It came back or was resolved prematurely. | 4. You can act on many events at once (bulk), and **export** the filtered or selected events as CSV. diff --git a/docs/how-to/schedule-recordings.md b/docs/how-to/schedule-recordings.md index 1fcd119a..ae242548 100644 --- a/docs/how-to/schedule-recordings.md +++ b/docs/how-to/schedule-recordings.md @@ -16,7 +16,7 @@ there is no cron syntax anywhere.** ## Create a schedule -1. Open **Schedules** in the left nav and click **New**. +1. Open **Schedules** in the left nav and click **Add schedule**. 2. Choose a **recurrence** mode: | Mode | Meaning | diff --git a/docs/how-to/track-recording-jobs.md b/docs/how-to/track-recording-jobs.md index cbab4011..f324ce73 100644 --- a/docs/how-to/track-recording-jobs.md +++ b/docs/how-to/track-recording-jobs.md @@ -20,8 +20,9 @@ or has failed. 2. The **status tiles** summarize active / queued / completed / failed jobs. 3. **Filter** by status, capture backend, node, interface, and created date to focus on what matters. -4. Each job row shows its capture settings, lease, heartbeats, and — if it - failed — the **failure reason**. +4. Each job row shows its capture settings, a claimed-by/lease badge, a + created / started / completed timeline, and — if it failed — the **failure + reason**. ## Retry or stop a job @@ -36,13 +37,17 @@ or has failed. A job moves through these states: 1. **queued** — created, waiting for a node. -2. **claimed** — a node has leased it. -3. **running** — capturing, heartbeating to the controller. +2. **running** — a node has leased it (its **claimed-by** badge shows which) and + is capturing, heartbeating to the controller. +3. **stop_requested** — a stop was requested while it was running; the node is + wrapping up. 4. **completed** / **failed** / **cancelled** — terminal. -A controller safety net automatically fails orphaned "running" jobs whose lease -expired, so a crashed agent never leaves a recording stranded. The full sequence -is in the [Recording guide](../guides/recording.md#the-job-lifecycle). +**claimed** is not a status — it's a lease phase, tracked via the job's +`claimedBy` field as a job starts running. A controller safety net automatically +fails orphaned "running" jobs whose lease expired, so a crashed agent never leaves +a recording stranded. The full sequence is in the +[Recording guide](../guides/recording.md#the-job-lifecycle). ## See also diff --git a/docs/how-to/tune-watchdog-policies.md b/docs/how-to/tune-watchdog-policies.md index c4edc752..bcc8de61 100644 --- a/docs/how-to/tune-watchdog-policies.md +++ b/docs/how-to/tune-watchdog-policies.md @@ -27,8 +27,7 @@ thresholds."*). - **clipping**, - **digital flatline** (stuck samples), - **high channel correlation** (a sign of a mis-wired/duplicated channel), - - **high broadband-noise / noise / hum / static likelihood**, - - and **loud non-speech audio** (for speech-required policies). + - and **high broadband-noise / noise / hum / static likelihood**. 3. Save. Use **Set default** on a policy to make it the one **pre-selected** for new diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md index dd8b1a6b..c613fce8 100644 --- a/docs/operations/deployment.md +++ b/docs/operations/deployment.md @@ -172,9 +172,14 @@ helm upgrade --install rakkr deploy/helm/rakkr-controller ` ``` Recorder-agent cache-file uploads reach the API through this ingress -> web -(nginx) path, so the chart ships a `nginx.ingress.kubernetes.io/proxy-body-size: -"0"` annotation (and the web image's nginx sets `client_max_body_size 0` on -`/api/`) to lift the 1 MB default that would otherwise `413` every upload. The +(nginx) path, so the chart ships four upload-related annotations — +`nginx.ingress.kubernetes.io/proxy-body-size: "0"`, +`nginx.ingress.kubernetes.io/proxy-request-buffering: "off"`, +`nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"`, and +`nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"` — (and the web image's +nginx sets `client_max_body_size 0` on `/api/`) to lift the 1 MB default that would +otherwise `413` every upload, stream rather than buffer the body, and give slow +multi-GB uploads room not to time out. The controller enforces the authoritative cap via `RAKKR_RECORDING_CACHE_MAX_BYTES` (4 GiB default). Override `ingress.annotations` if you run a non-nginx ingress controller. diff --git a/docs/reference/api.md b/docs/reference/api.md index bf0f09d2..17ec7583 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -68,8 +68,10 @@ Conventions: | `GET /nodes` · `GET\|POST /nodes/export` | `node:read` | List / export inventory (scoped). | | `GET /nodes/:id` · `/:id/actions` | `node:read` | Detail / action summaries. | | `GET /nodes/:id/meters` · `GET /meter-events` | `node:read` | Meter snapshot / SSE stream. | +| `GET /nodes/agent-release` | `node:read` | Latest agent release (update check). | | `POST /nodes/enroll` | `node:manage` | Enroll node (returns credential). | | `PATCH /nodes/:id` · `/:id/interfaces/:iid` | `node:manage` | Update node / interface. | +| `PUT /nodes/:id/channel-rooms` | `node:manage` | Assign node channels to rooms. | | `POST /nodes/:id/credentials/rotate` | `node:manage` | Rotate node controller token. | | `GET /nodes/:id/ssh-credential` · `POST /:id/ssh-credential/rotate` | `node:manage` | Read public SSH key / rotate keypair (private key never returned). | | `POST /nodes/:id/bootstrap-token` | `node:manage` | Mint a single-use, short-TTL day-0 bootstrap token. | @@ -85,11 +87,12 @@ Conventions: | `GET /nodes/:id/channel-map-assignments` | `node:control` | Assigned channel maps. | | `POST /nodes/:id/heartbeat` | `node:control` | Node heartbeat. | | `POST /nodes/:id/inventory` | `node:control` | Reconcile interfaces from discovered inventory (startup). | -| `POST /nodes/:id/meter-frame` | — | Push a meter frame. | +| `POST /nodes/:id/meter-frame` | `node:control` | Push a meter frame. | | `POST /nodes/:id/listen/chunk` | `node:control` | Ingest live-listen audio (`?rendition`). | | `POST /nodes/:id/health-events` | `health:acknowledge` | Sync a health event. | | `GET /nodes/:id/recording-jobs/next` | `recording:control` | Poll the next queued job (peek, no lease). | | `POST /nodes/:id/recording-jobs/claim-next` | `recording:control` | Atomically claim the next queued job. | +| `POST /nodes/:id/recording-jobs/claim-next-group` | `recording:control` | Claim the next queued job + its capture-group siblings (one shared capture). | | `POST /recording-jobs/:jid/claim` | `recording:control` | Claim a specific queued job by id. | | `POST /recording-jobs/:jid/heartbeat` | `recording:control` | Job heartbeat. | | `GET /recording-jobs/:jid` | `recording:control`/`recording:read` | Read job (dual-mode auth). | @@ -163,11 +166,12 @@ reads under `settings:read`. Mutations require `settings:manage`. | Family | Read | Manage | | ------------------------------------------- | --------------- | -------------------------------------------------------------------------------- | -| Recording profiles | `settings:read` | `PATCH /settings/recording-profiles/:id` | -| Watchdog policies | `settings:read` | `PATCH /settings/watchdog-policies/:id` (+ calibrate) | +| Recording profiles | `settings:read` | `POST` / `PATCH /settings/recording-profiles/:id` | +| Watchdog policies | `settings:read` | `POST` / `PATCH /settings/watchdog-policies/:id` (+ calibrate) | | Upload destinations / policies | `settings:read` | `PATCH`/`POST` destinations & policies | | Channel-map templates / assignments / plans | `settings:read` | create/update templates; `PUT` assignments (+ bulk, rollback); stage/apply plans | | Retention policies | `settings:read` | `POST` / `PATCH /settings/retention-policies/:id` | +| Controller | `settings:read` | `PATCH /settings/controller` (weekStartsOn + scheduling defaults) | ## Switchers — `/api/v1/settings/switchers` @@ -199,3 +203,4 @@ reads under `settings:read`. Mutations require `settings:manage`. | `GET /api/v1/status` | `node:read` | Aggregated scoped status (nodes, recordings, health, uptime). | | `GET /metrics` | `metrics:read` | Prometheus exposition (root path). | | `GET /healthz` | — | Liveness. | +| `GET /readyz` | — | Readiness (`503` while the database is unreachable). | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 4191fe55..b30cc794 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -23,9 +23,11 @@ separately in the [recorder agent CLI reference](recorder-agent.md). | `DATABASE_URL` | — | Postgres connection string. Unset → fallback stores. | | `RAKKR_WEB_ORIGIN` | `http://localhost:5173` | Allowed CORS / web origin. | | `RAKKR_RECORDING_CACHE_DIR` | `data/recordings` | Root directory for cached recording files. | +| `RAKKR_RECORDING_CACHE_MAX_BYTES` | `4294967296` | Max size of a single agent cache-file upload (4 GiB). | | `RAKKR_API_VERSION` | `0.0.0-dev` | Controller version reported by `/healthz` and status routes (stamped at image build). | | `RAKKR_API_NO_LISTEN` | — | `1` skips binding a port (used by tests). | | `RAKKR_LISTEN_SESSION_TTL_SECONDS` | `300` | Live-listen session TTL before eviction. | +| `RAKKR_AUTH_FALLBACK_GRACE_MS` | `900000` | How long the auth memory-fallback keeps honoring login-time permissions after the DB becomes unavailable (15 min). | | `RAKKR_SEED_DEMO_DATA` | enabled | Set `0` to disable demo data seeding. | | `RAKKR_DEMO_METERS` | disabled | `1` lets meter endpoints emit synthetic frames when no agent frame is stored (demonstration / screenshots / tests only). Off by default — real usage never fabricates meters; an absent feed reads as empty. | | `RAKKR_DEMO_METER_DBFS` | — | dBFS value for the synthetic demo meter data; only applies when `RAKKR_DEMO_METERS=1`. | @@ -56,6 +58,7 @@ All disabled unless `RAKKR_OIDC_ENABLED` is truthy (`1`/`on`/`true`/`yes`). | `RAKKR_OIDC_CLIENT_SECRET` | — | OIDC client secret. | | `RAKKR_OIDC_REDIRECT_URI` | — | Callback URI (must match the IdP app registration). | | `RAKKR_OIDC_SCOPES` | `openid profile email` | Requested scopes. | +| `RAKKR_OIDC_ALLOW_INSECURE_ISSUER` | — | Dev/test only: permit HTTP OIDC discovery for a **loopback** issuer. | ## TLS / transport @@ -100,6 +103,19 @@ The runner side of the fetch (set on the **runner**, not the controller): (self-signed CA bundle), `RAKKR_RUNNER_ALLOW_INSECURE` (`1` skips TLS verify; dev only), `RAKKR_RUNNER_CONTROLLER_TIMEOUT_SECONDS` (default `20`). +## Recorder-agent update check + +The controller polls GitHub releases so the nodes page can flag out-of-date +recorder agents. See [Node lifecycle](../guides/node-lifecycle.md). + +| Variable | Default | Purpose | +| -------------------------------- | ------------------------ | ---------------------------------------------------- | +| `RAKKR_AGENT_RELEASE_REPO` | `yashau/Rakkr` | GitHub repo polled for the latest `agent-v…` release. | +| `RAKKR_GITHUB_TOKEN` | — | Optional token to raise the GitHub API rate limit. | +| `RAKKR_GITHUB_API_URL` | `https://api.github.com` | GitHub API base URL. | +| `RAKKR_AGENT_RELEASE_TTL_MS` | `1800000` | Cache TTL for the release lookup (30 min). | +| `RAKKR_AGENT_RELEASE_TIMEOUT_MS` | `10000` | Per-request timeout for the release lookup. | + ## JSON fallback store paths Used when `DATABASE_URL` is unset; resolved relative to the working directory. diff --git a/docs/reference/metrics.md b/docs/reference/metrics.md index bc99bb74..836a193a 100644 --- a/docs/reference/metrics.md +++ b/docs/reference/metrics.md @@ -49,10 +49,12 @@ Per-channel audio quality, derived from the latest meter frame (labelled by | `rakkr_input_static_score` | Static-likelihood score. | | `rakkr_input_estimated_snr_db` | Estimated signal-to-noise ratio (dB). | | `rakkr_input_intelligibility_score` | First-pass voice intelligibility score. | -| `rakkr_input_channel_correlation_score` | Strongest same-interface channel correlation score. | +| `rakkr_input_channel_correlation_score` | Strongest same-interface channel correlation score (also carries `peer_channel` + `phase` labels). | ## Live listen monitor +Labelled by `node_id`/`interface_id`/`channel` plus a `source` label. + | Metric | Meaning | | --------------------------------------------- | -------------------------------------- | | `rakkr_listen_monitor_chunk_age_seconds` | Age of the latest monitor audio chunk. | diff --git a/docs/reference/recorder-agent.md b/docs/reference/recorder-agent.md index ab6be097..89342648 100644 --- a/docs/reference/recorder-agent.md +++ b/docs/reference/recorder-agent.md @@ -28,6 +28,7 @@ Without a mode flag, the agent runs as a long-lived daemon. | `--capture-recording-id` | `RAKKR_CAPTURE_RECORDING_ID` | One-shot capture → render → upload for a recording ID. | | `--attach-cache-file` | `RAKKR_ATTACH_CACHE_FILE` | Upload an existing local file as a recording's cache. | | `--bootstrap` | `RAKKR_BOOTSTRAP` | Day-0: generate SSH keypair, hand the private key to the controller (bootstrap token), write the returned controller token, then exit. | +| `--version` | _(CLI only)_ | Print the calendar agent version and exit. | ### Bootstrap mode @@ -72,6 +73,7 @@ Used at first boot (usually by `deploy/bootstrap/agent.sh`); see | `RAKKR_CAPTURE_SAMPLE_RATE` | `48000` | Sample rate (Hz). | | `RAKKR_CAPTURE_CHANNELS` | `2` | Channel count. | | `RAKKR_CAPTURE_SECONDS` | `60` | Duration for one-shot/local capture mode. | +| `RAKKR_CAPTURE_CHUNK_SECONDS` | — (unset / `0`) | Default chunk length (`--capture-chunk-seconds`) for chunked recordings when a job carries no `chunkSeconds`. Unset/`0` keeps the single-file capture path. | | `RAKKR_CAPTURE_ARGS_TEMPLATE` | — | Override capture args (placeholders; see below). | | `RAKKR_CHANNEL_RENDER_COMMAND` | `ffmpeg` | Tool used to render channel maps / re-encode. | | `RAKKR_CAPTURE_MIN_OUTPUT_BYTES` | `128` | Minimum acceptable output size (smaller = "too small" failure). | diff --git a/docs/reference/tasks.md b/docs/reference/tasks.md index a187659d..31ed4331 100644 --- a/docs/reference/tasks.md +++ b/docs/reference/tasks.md @@ -29,11 +29,17 @@ commands. Run a task with `mise run `. | `mise run check` | The full repository gate (see below). | | `mise run build` | Build TS packages/apps and the Rust agent. | | `mise run check:loc` | Enforce the 1000-LOC-per-file budget. | +| `mise run helm:check` | Render the controller Helm chart across secret backends and assert its invariants. | -`mise run check` is intentionally broad. It runs the baseline doc verifiers, -Drizzle migration replay, TypeScript checks, Node tests, oxlint, oxfmt check, the -fake-controller agent smoke, and the Rust suite (cargo check, rustfmt, clippy, -Miri). It needs a working Docker/Postgres for the DB verifier. +`mise run check` is intentionally broad. It runs the baseline doc verifiers, the +Helm chart render, Drizzle migration replay, TypeScript checks, Node tests +(including the DB-backed suite), oxlint, oxfmt check, the fake-controller agent +smoke, and the Rust suite (cargo check, rustfmt, clippy, Miri). It needs a +working Docker/Postgres for the DB verifier and DB-backed tests. + +Cut a release with `mise run release `, which pushes a +calendar-versioned tag that triggers that component's release workflow — see +[Releases](../operations/releases.md). ## Targeted Node / TypeScript @@ -41,6 +47,7 @@ Miri). It needs a working Docker/Postgres for the DB verifier. | -------------------------------------------- | --------------------------- | | `mise run node:check` | TypeScript type-check. | | `mise run node:test` | Node test suites. | +| `mise run node:test-db` | DB-backed Node tests against a throwaway Postgres. | | `mise run node:lint` | oxlint. | | `mise run node:format` / `node:format-check` | oxfmt write / check. | | `mise run node:build` | Build TS packages and apps. | @@ -81,6 +88,7 @@ Each checks a [baseline doc](../contributing/baselines.md) against the source: | `mise run health:check-watchdog` | Health watchdog | | `mise run storage:check` | Storage upload | | `mise run operations:check` | Operations | +| `mise run nodes:check-lifecycle` | Node lifecycle | | `mise run time:check` | Date / time | | `mise run ops:check-alerts` / `ops:check-prometheus` / `ops:check-grafana` / `ops:check-observability-docs` | Observability artifacts | diff --git a/packages/shared/src/enhancement.ts b/packages/shared/src/enhancement.ts index 1fc5cd21..f9efab74 100644 --- a/packages/shared/src/enhancement.ts +++ b/packages/shared/src/enhancement.ts @@ -6,9 +6,10 @@ export const enhancementDenoiseEngineSchema = z.enum(["rnnoise", "deepfilternet3 // Voice-enhancement chain stored on a recording profile (the preset/template). // Every stage is independently toggleable with configurable parameters; the agent -// applies enabled stages in a fixed order (highpass -> denoise -> deesser -> -// compressor -> loudnorm -> gate) to produce the enhanced rendition, always -// alongside the untouched raw audio when keepRaw is set. +// applies the enabled stages in a fixed order — denoise runs first in-process, +// then the ffmpeg voice chain (highpass -> lowpass -> deesser -> compressor -> +// loudnorm -> gate) — to produce the enhanced rendition, always alongside the +// untouched raw audio when keepRaw is set. export const recordingEnhancementSchema = z.object({ keepRaw: z.boolean().default(true), denoise: z