dragonflight/services/mam-api/src/db/migrations/036-recorder-hardware-identity.sql
ZGaetano de509c66ab feat(recorders): hardware-identity model with Enable/Disable lifecycle
Recorders are now physical capture ports, not user-created rows:
- migration 036: label, enabled, auto_provisioned + UNIQUE(node_id,device_index)
  (the structural fix that makes two recorders sharing a port impossible)
- mam-api: auto-provision one recorder row per port from heartbeat capabilities
  (reconcileRecordersForNode); create-once, never overwrites operator config
- mam-api: POST /:id/enable + /:id/disable (provision/teardown standby sidecar);
  PATCH accepts label; config persists across enable/disable
- node-agent: freeCapturePort() force-removes any container on a capture port
  before standby/start — eliminates the EADDRINUSE collisions
- web-ui: recorder menu grouped by node (online/offline), Enable/Disable toggle,
  per-recorder config modal (codec/bitrate/growing/label/project), friendly
  label over hardware name, no destructive delete

Fixes the delete/recreate churn that orphaned standby sidecars and collided on
capture ports during this session's outage.
2026-06-04 03:14:43 +00:00

38 lines
2 KiB
SQL

-- Migration 036: Recorders become physical hardware, not user-created rows.
--
-- A recorder now maps 1:1 to a physical capture port: (node_id, device_index).
-- mam-api auto-provisions one row per port from each node-agent heartbeat's
-- capabilities (deltacast/blackmagic arrays). Rows are NEVER deleted by the
-- operator — they're discovered, enabled/disabled, and configured in place.
-- This removes the delete/create churn that orphaned standby sidecars and
-- caused capture-port (EADDRINUSE) collisions.
--
-- New columns:
-- label : optional friendly name overlaid on the hardware identity
-- (e.g. "Aurora" for zampp3-dc0). NULL → UI shows node+port name.
-- enabled : operator opt-in. false (default) = no standby sidecar, port idle.
-- true = persistent standby sidecar kept up (idle-preview), ready
-- to record. Toggled by the Enable/Disable button.
-- auto_provisioned : true when the row was created by heartbeat discovery
-- (vs a legacy manually-created recorder). Informational.
--
-- Identity:
-- UNIQUE(node_id, device_index) is the structural guarantee that two
-- recorders can never share a capture port — the root-cause fix for the
-- collisions. Partial unique index (WHERE both are non-null) so any legacy
-- rows without a node/device don't violate it.
ALTER TABLE recorders
ADD COLUMN IF NOT EXISTS label TEXT DEFAULT NULL,
ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS auto_provisioned BOOLEAN NOT NULL DEFAULT false;
-- One recorder per physical port. Partial so pre-existing rows lacking a
-- node_id/device_index (e.g. network sources) are unaffected.
CREATE UNIQUE INDEX IF NOT EXISTS recorders_node_device_uniq
ON recorders (node_id, device_index)
WHERE node_id IS NOT NULL AND device_index IS NOT NULL;
-- Fast lookup of a node's ports during heartbeat reconciliation.
CREATE INDEX IF NOT EXISTS recorders_node_id_idx
ON recorders (node_id);