Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/web/src/pages/room-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -188,7 +189,7 @@ export function RoomDetailPage({ roomId }: { roomId: string }) {
</Button>
<ConfirmButton
confirmLabel="Delete"
description={`This permanently deletes the room "${room.name}". Rooms with assigned nodes cannot be deleted.`}
description={`This permanently deletes the room "${room.name}". Rooms still referenced by a schedule cannot be deleted.`}
disabled={deleteRoomMutation.isPending}
onConfirm={() => deleteRoomMutation.mutate()}
title={`Delete room "${room.name}"?`}
Expand Down
11 changes: 8 additions & 3 deletions docs/architecture/controller-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
| ------ | --------------------------------------- | ---------------------------------------- |
Expand Down
13 changes: 7 additions & 6 deletions docs/architecture/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions docs/getting-started/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`,
Expand Down
13 changes: 7 additions & 6 deletions docs/guides/audio-enhancement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/authentication-and-rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions docs/how-to/configure-channel-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
24 changes: 12 additions & 12 deletions docs/how-to/configure-recording-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/how-to/enroll-and-configure-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/manage-groups-and-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/manage-rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/manage-users.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand All @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions docs/how-to/navigate-the-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 2 additions & 2 deletions docs/how-to/record-a-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 9 additions & 8 deletions docs/how-to/respond-to-health-alerts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/schedule-recordings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
19 changes: 12 additions & 7 deletions docs/how-to/track-recording-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions docs/how-to/tune-watchdog-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading