From 7587c7d2f4e8f0fe45bc705a4d9772571ec051c6 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 5 Jun 2026 17:37:17 +0000 Subject: [PATCH] chore(ui+infra): retire stale recorder modal, clean VC-3 card, fix schema drift GUI: - Retire orphaned modal-new-recorder.jsx (old New Recorder dialog). Nothing triggered it; the clean inline RecorderConfigModal in screens-ingest.jsx is the single recorder editor now. Drop dead app.jsx/index.html wiring and the unused onNew prop. - Recorder card sub-line shows VC-3/DNxHD MXF OP1a 4:2:2 in growing mode instead of the misleading hevc_nvenc native native. Infra: - Remove vestigial local db service from compose (app uses external Postgres via DATABASE_URL). Drops db depends_on refs + orphan postgres_data volume. compose config validates clean. - Regenerate schema.sql from the live migrated DB (8 -> 29 tables). Old schema.sql predated 37 migrations (missing growing_codec, device_index, node_id, audio_offset_ms) so a fresh init silently rolled the UI back to XDCAM. Fresh init now matches current code. --- docker-compose.yml | 27 +- services/mam-api/src/db/schema.sql | 1745 +++++++++++++++-- services/web-ui/public/app.jsx | 4 +- services/web-ui/public/index.html | 1 - services/web-ui/public/modal-new-recorder.jsx | 604 ------ services/web-ui/public/screens-ingest.jsx | 18 +- 6 files changed, 1626 insertions(+), 773 deletions(-) delete mode 100644 services/web-ui/public/modal-new-recorder.jsx diff --git a/docker-compose.yml b/docker-compose.yml index 66f41b7..11acc33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,4 @@ services: - db: - image: postgres:16 - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - ports: - - "${PORT_DB:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./services/mam-api/src/db/schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - wild-dragon - queue: image: redis:7-alpine ports: @@ -30,8 +11,6 @@ services: mam-api: build: ./services/mam-api depends_on: - db: - condition: service_healthy queue: condition: service_started ports: @@ -119,7 +98,6 @@ services: privileged: true depends_on: - queue - - db environment: REDIS_URL: ${REDIS_URL} DATABASE_URL: ${DATABASE_URL} @@ -152,7 +130,7 @@ services: worker-p400a: image: wild-dragon-worker-gpu:latest runtime: nvidia - depends_on: [queue, db, worker-p4] + depends_on: [queue, worker-p4] environment: REDIS_URL: ${REDIS_URL} DATABASE_URL: ${DATABASE_URL} @@ -171,7 +149,7 @@ services: worker-p400b: image: wild-dragon-worker-gpu:latest runtime: nvidia - depends_on: [queue, db, worker-p4] + depends_on: [queue, worker-p4] environment: REDIS_URL: ${REDIS_URL} DATABASE_URL: ${DATABASE_URL} @@ -210,7 +188,6 @@ services: image: wild-dragon-playout:latest volumes: - postgres_data: redis_data: networks: diff --git a/services/mam-api/src/db/schema.sql b/services/mam-api/src/db/schema.sql index dd139c9..3c688f6 100644 --- a/services/mam-api/src/db/schema.sql +++ b/services/mam-api/src/db/schema.sql @@ -1,164 +1,1637 @@ --- Wild Dragon MAM Platform - PostgreSQL Schema +-- +-- PostgreSQL database dump +-- --- Enable UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +\restrict mZfxRJrhoiQMVbssWG695aKA8Byev914TXjorYV82xHaynuUiKs6gaVmQSkekKy --- ENUM types -CREATE TYPE asset_status AS ENUM ( - 'ingesting', - 'processing', - 'ready', - 'error', - 'archived' +-- Dumped from database version 16.14 (Debian 16.14-1.pgdg13+1) +-- Dumped by pg_dump version 16.14 (Debian 16.14-1.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + + +-- +-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; + + +-- +-- Name: access_level; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.access_level AS ENUM ( + 'view', + 'edit' ); -CREATE TYPE media_type AS ENUM ( - 'video', - 'audio', - 'image', - 'document' + +-- +-- Name: asset_status; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.asset_status AS ENUM ( + 'live', + 'ingesting', + 'processing', + 'ready', + 'error', + 'archived', + 'pending_migration' ); -CREATE TYPE job_type AS ENUM ( - 'proxy_gen', - 'thumbnail', - 'conform', - 'export' + +-- +-- Name: job_status; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.job_status AS ENUM ( + 'queued', + 'processing', + 'complete', + 'failed', + 'completed' ); -CREATE TYPE job_status AS ENUM ( - 'queued', - 'processing', - 'complete', - 'failed' + +-- +-- Name: job_type; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.job_type AS ENUM ( + 'proxy_gen', + 'thumbnail', + 'conform', + 'export', + 'youtube_import', + 'trim', + 'proxy', + 'import' ); --- Projects table -CREATE TABLE projects ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name TEXT NOT NULL, - description TEXT, - s3_prefix TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() + +-- +-- Name: media_type; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.media_type AS ENUM ( + 'video', + 'audio', + 'image', + 'document' ); --- Bins table -CREATE TABLE bins ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - project_id UUID NOT NULL REFERENCES projects ON DELETE CASCADE, - parent_id UUID REFERENCES bins ON DELETE SET NULL, - name TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() + +-- +-- Name: source_type; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.source_type AS ENUM ( + 'sdi', + 'srt', + 'rtmp', + 'deltacast' ); --- Assets table -CREATE TABLE assets ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - project_id UUID NOT NULL REFERENCES projects ON DELETE CASCADE, - bin_id UUID REFERENCES bins ON DELETE SET NULL, - filename TEXT NOT NULL, - display_name TEXT, - status asset_status DEFAULT 'ingesting', - media_type media_type NOT NULL, - original_s3_key TEXT, - proxy_s3_key TEXT, - thumbnail_s3_key TEXT, - codec TEXT, - resolution TEXT, - fps NUMERIC(6,3), - duration_ms BIGINT, - start_tc TEXT, - file_size BIGINT, - tags TEXT[] DEFAULT '{}', - notes TEXT, - -- AMPP sync fields (used by upload.js to mirror folder structure) - ampp_folder_id TEXT, - ampp_synced_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: api_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.api_tokens ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + user_id uuid NOT NULL, + name text NOT NULL, + token_hash text NOT NULL, + token_prefix text NOT NULL, + last_used_at timestamp with time zone, + expires_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now(), + bound_hostname text ); --- Jobs table -CREATE TABLE jobs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - type job_type NOT NULL, - asset_id UUID REFERENCES assets ON DELETE SET NULL, - status job_status DEFAULT 'queued', - payload JSONB DEFAULT '{}', - result JSONB DEFAULT '{}', - error TEXT, - progress INTEGER DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() + +-- +-- Name: asset_comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.asset_comments ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + asset_id uuid NOT NULL, + user_id uuid, + body text NOT NULL, + frame_ms integer, + resolved boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL ); --- Capture Sessions table -CREATE TABLE capture_sessions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - project_id UUID NOT NULL REFERENCES projects, - bin_id UUID REFERENCES bins, - clip_name TEXT NOT NULL, - device TEXT NOT NULL, - started_at TIMESTAMPTZ DEFAULT NOW(), - stopped_at TIMESTAMPTZ, - asset_id UUID REFERENCES assets + +-- +-- Name: assets; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.assets ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + project_id uuid NOT NULL, + bin_id uuid, + filename text NOT NULL, + display_name text, + status public.asset_status DEFAULT 'ingesting'::public.asset_status, + media_type public.media_type NOT NULL, + original_s3_key text, + proxy_s3_key text, + thumbnail_s3_key text, + codec text, + resolution text, + fps numeric(6,3), + duration_ms bigint, + start_tc text, + file_size bigint, + tags text[] DEFAULT '{}'::text[], + notes text, + ampp_folder_id text, + ampp_synced_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + source_url text, + conform_source_sequence_id uuid, + filmstrip_s3_key text, + ampp_sync_status text DEFAULT 'pending'::text NOT NULL, + ampp_sync_attempts integer DEFAULT 0 NOT NULL, + ampp_sync_next_attempt_at timestamp with time zone, + ampp_sync_last_error text, + audio_metadata jsonb, + hls_s3_key text ); --- Users table -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - display_name TEXT, - role TEXT DEFAULT 'editor', - created_at TIMESTAMPTZ DEFAULT NOW() + +-- +-- Name: audit_log; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.audit_log ( + id bigint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + event_type text NOT NULL, + actor_id uuid, + actor_label text, + target_type text, + target_id text, + ip_address text, + user_agent text, + meta jsonb DEFAULT '{}'::jsonb NOT NULL ); --- Sessions table (for connect-pg-simple session store) -CREATE TABLE sessions ( - sid TEXT PRIMARY KEY, - sess JSONB NOT NULL, - expire TIMESTAMPTZ NOT NULL + +-- +-- Name: audit_log_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.audit_log_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: audit_log_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.audit_log_id_seq OWNED BY public.audit_log.id; + + +-- +-- Name: bins; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.bins ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + project_id uuid NOT NULL, + parent_id uuid, + name text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() ); --- Indexes -CREATE INDEX idx_assets_project_id ON assets(project_id); -CREATE INDEX idx_assets_status ON assets(status); -CREATE INDEX idx_assets_created_at_desc ON assets(created_at DESC); -CREATE INDEX idx_assets_tags ON assets USING GIN(tags); -CREATE INDEX idx_assets_fulltext ON assets USING GIN(to_tsvector('english', COALESCE(display_name, '') || ' ' || COALESCE(filename, '') || ' ' || COALESCE(notes, ''))); -CREATE INDEX idx_jobs_asset_id ON jobs(asset_id); -CREATE INDEX idx_jobs_status ON jobs(status); -CREATE INDEX idx_jobs_type ON jobs(type); +-- +-- Name: capture_sessions; Type: TABLE; Schema: public; Owner: - +-- -CREATE INDEX idx_bins_project_id ON bins(project_id); - -CREATE INDEX idx_sessions_expire ON sessions(expire); - --- Recorder source types -CREATE TYPE source_type AS ENUM ('sdi', 'srt', 'rtmp', 'deltacast'); - --- Recorder instances table --- NOTE: current_session_id is TEXT (stores a human-readable clip name, not a UUID) -CREATE TABLE recorders ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name TEXT NOT NULL, - source_type source_type NOT NULL, - source_config JSONB NOT NULL DEFAULT '{}', - recording_codec TEXT NOT NULL DEFAULT 'prores_hq', - recording_resolution TEXT DEFAULT 'native', - proxy_enabled BOOLEAN DEFAULT true, - proxy_codec TEXT DEFAULT 'libx264', - proxy_resolution TEXT DEFAULT '1920x1080', - project_id UUID REFERENCES projects, - container_id TEXT, - status TEXT DEFAULT 'stopped', - current_session_id TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() +CREATE TABLE public.capture_sessions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + project_id uuid NOT NULL, + bin_id uuid, + clip_name text NOT NULL, + device text NOT NULL, + started_at timestamp with time zone DEFAULT now(), + stopped_at timestamp with time zone, + asset_id uuid ); -CREATE INDEX idx_recorders_status ON recorders(status); + +-- +-- Name: cluster_nodes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.cluster_nodes ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + hostname text NOT NULL, + ip_address text, + role text DEFAULT 'worker'::text NOT NULL, + version text, + api_url text, + cpu_usage numeric(5,2), + mem_used_mb integer, + mem_total_mb integer, + last_seen timestamp with time zone DEFAULT now() NOT NULL, + registered_at timestamp with time zone DEFAULT now() NOT NULL, + metadata jsonb, + capabilities jsonb DEFAULT '{}'::jsonb, + metrics jsonb, + last_seen_at timestamp with time zone +); + + +-- +-- Name: groups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.groups ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name text NOT NULL, + description text, + created_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: jobs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.jobs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + type public.job_type NOT NULL, + asset_id uuid, + status public.job_status DEFAULT 'queued'::public.job_status, + payload jsonb DEFAULT '{}'::jsonb, + result jsonb DEFAULT '{}'::jsonb, + error text, + progress integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + expires_at timestamp with time zone +); + + +-- +-- Name: playout_as_run; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_as_run ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel_id uuid NOT NULL, + asset_id uuid, + item_id uuid, + clip_name text, + started_at timestamp with time zone DEFAULT now() NOT NULL, + ended_at timestamp with time zone, + duration_s numeric, + result text DEFAULT 'played'::text NOT NULL, + CONSTRAINT playout_as_run_result_check CHECK ((result = ANY (ARRAY['played'::text, 'skipped'::text, 'error'::text, 'scte'::text]))) +); + + +-- +-- Name: playout_channels; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_channels ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name text NOT NULL, + node_id uuid, + output_type text DEFAULT 'srt'::text NOT NULL, + output_config jsonb DEFAULT '{}'::jsonb NOT NULL, + video_format text DEFAULT '1080p5994'::text NOT NULL, + status text DEFAULT 'stopped'::text NOT NULL, + container_id text, + container_meta jsonb DEFAULT '{}'::jsonb NOT NULL, + error_message text, + restart_count integer DEFAULT 0 NOT NULL, + last_restart_at timestamp with time zone, + last_heartbeat_at timestamp with time zone, + project_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT playout_channels_output_type_check CHECK ((output_type = ANY (ARRAY['decklink'::text, 'ndi'::text, 'srt'::text, 'rtmp'::text]))), + CONSTRAINT playout_channels_status_check CHECK ((status = ANY (ARRAY['stopped'::text, 'starting'::text, 'running'::text, 'error'::text]))) +); + + +-- +-- Name: playout_items; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_items ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + playlist_id uuid, + asset_id uuid NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + scheduled_at timestamp with time zone, + in_point numeric, + out_point numeric, + transition text DEFAULT 'cut'::text NOT NULL, + transition_ms integer DEFAULT 0 NOT NULL, + graphics jsonb, + media_status text DEFAULT 'pending'::text NOT NULL, + media_path text, + audio_normalized boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT playout_items_media_status_check CHECK ((media_status = ANY (ARRAY['pending'::text, 'staging'::text, 'ready'::text, 'error'::text]))), + CONSTRAINT playout_items_transition_check CHECK ((transition = ANY (ARRAY['cut'::text, 'mix'::text, 'wipe'::text]))) +); + + +-- +-- Name: playout_playlists; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_playlists ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel_id uuid NOT NULL, + name text NOT NULL, + loop boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: playout_schedule; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_schedule ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel_id uuid NOT NULL, + asset_id uuid, + scheduled_at timestamp with time zone NOT NULL, + in_point numeric, + out_point numeric, + transition text DEFAULT 'cut'::text NOT NULL, + transition_ms integer DEFAULT 0 NOT NULL, + is_filler boolean DEFAULT false NOT NULL, + status text DEFAULT 'scheduled'::text NOT NULL, + media_status text DEFAULT 'pending'::text NOT NULL, + media_path text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT playout_schedule_media_status_check CHECK ((media_status = ANY (ARRAY['pending'::text, 'staging'::text, 'ready'::text, 'error'::text]))), + CONSTRAINT playout_schedule_status_check CHECK ((status = ANY (ARRAY['scheduled'::text, 'playing'::text, 'played'::text, 'skipped'::text, 'error'::text]))), + CONSTRAINT playout_schedule_transition_check CHECK ((transition = ANY (ARRAY['cut'::text, 'mix'::text, 'wipe'::text]))) +); + + +-- +-- Name: playout_scte_breaks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_scte_breaks ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel_id uuid NOT NULL, + playlist_pos integer, + scheduled_at timestamp with time zone, + duration_s integer DEFAULT 30 NOT NULL, + event_id integer DEFAULT 1 NOT NULL, + type text DEFAULT 'splice_insert'::text NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + fired_at timestamp with time zone, + created_by uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT playout_scte_breaks_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'fired'::text, 'done'::text, 'cancelled'::text]))), + CONSTRAINT playout_scte_breaks_type_check CHECK ((type = ANY (ARRAY['splice_insert'::text, 'immediate'::text, 'splice_out'::text, 'splice_in'::text]))) +); + + +-- +-- Name: playout_sidecars; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playout_sidecars ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel_id uuid NOT NULL, + node_id uuid, + container_id text NOT NULL, + sidecar_url text, + amcp_port integer, + status text DEFAULT 'running'::text NOT NULL, + last_heartbeat_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT playout_sidecars_status_check CHECK ((status = ANY (ARRAY['starting'::text, 'running'::text, 'error'::text, 'stopped'::text]))) +); + + +-- +-- Name: project_access; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.project_access ( + project_id uuid NOT NULL, + subject_type text NOT NULL, + subject_id uuid NOT NULL, + level public.access_level DEFAULT 'view'::public.access_level NOT NULL, + granted_by uuid, + granted_at timestamp with time zone DEFAULT now(), + CONSTRAINT project_access_subject_type_check CHECK ((subject_type = ANY (ARRAY['user'::text, 'group'::text]))) +); + + +-- +-- Name: projects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projects ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name text NOT NULL, + description text, + s3_prefix text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: recorder_schedules; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.recorder_schedules ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name text NOT NULL, + recorder_id uuid NOT NULL, + start_at timestamp with time zone NOT NULL, + end_at timestamp with time zone NOT NULL, + recurrence text DEFAULT 'none'::text NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + last_asset_id uuid, + error_message text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT recorder_schedules_check CHECK ((end_at > start_at)), + CONSTRAINT recorder_schedules_recurrence_check CHECK ((recurrence = ANY (ARRAY['none'::text, 'daily'::text, 'weekly'::text]))), + CONSTRAINT recorder_schedules_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'running'::text, 'completed'::text, 'failed'::text, 'cancelled'::text, 'starting'::text, 'stopping'::text]))) +); + + +-- +-- Name: recorders; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.recorders ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name text NOT NULL, + source_type public.source_type NOT NULL, + source_config jsonb DEFAULT '{}'::jsonb NOT NULL, + recording_codec text DEFAULT 'prores_hq'::text NOT NULL, + recording_resolution text DEFAULT 'native'::text, + proxy_enabled boolean DEFAULT true, + proxy_codec text DEFAULT 'libx264'::text, + proxy_resolution text DEFAULT '1920x1080'::text, + project_id uuid, + container_id text, + status text DEFAULT 'stopped'::text, + current_session_id text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + recording_video_bitrate text, + recording_framerate text, + recording_audio_codec text DEFAULT 'pcm_s24le'::text, + recording_audio_bitrate text, + recording_audio_channels integer DEFAULT 2, + recording_container text DEFAULT 'mov'::text, + proxy_video_bitrate text DEFAULT '8M'::text, + proxy_framerate text, + proxy_audio_codec text DEFAULT 'aac'::text, + proxy_audio_bitrate text DEFAULT '192k'::text, + proxy_audio_channels integer DEFAULT 2, + proxy_container text DEFAULT 'mp4'::text, + node_id uuid, + device_index integer, + growing_enabled boolean, + gpu_uuid text, + label text, + enabled boolean DEFAULT false NOT NULL, + auto_provisioned boolean DEFAULT false NOT NULL, + growing_codec text DEFAULT 'avci100'::text, + audio_offset_ms integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + filename text NOT NULL, + applied_at timestamp with time zone DEFAULT now() NOT NULL, + checksum_sha text +); + + +-- +-- Name: sequence_clips; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sequence_clips ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + sequence_id uuid NOT NULL, + asset_id uuid NOT NULL, + track integer DEFAULT 0 NOT NULL, + timeline_in_frames bigint NOT NULL, + timeline_out_frames bigint NOT NULL, + source_in_frames bigint DEFAULT 0 NOT NULL, + source_out_frames bigint NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT chk_source_range CHECK ((source_out_frames > source_in_frames)), + CONSTRAINT chk_timeline_range CHECK ((timeline_out_frames > timeline_in_frames)), + CONSTRAINT chk_track_valid CHECK ((track >= 0)), + CONSTRAINT sequence_clips_track_check CHECK ((track >= 0)) +); + + +-- +-- Name: sequences; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sequences ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + project_id uuid NOT NULL, + name text DEFAULT 'Sequence 1'::text NOT NULL, + frame_rate numeric(6,3) DEFAULT 59.94 NOT NULL, + width integer DEFAULT 1920 NOT NULL, + height integer DEFAULT 1080 NOT NULL, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: sessions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sessions ( + sid text NOT NULL, + sess jsonb NOT NULL, + expire timestamp with time zone NOT NULL +); + + +-- +-- Name: settings; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.settings ( + key text NOT NULL, + value text, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: temp_segments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.temp_segments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + job_id uuid NOT NULL, + clip_instance_id uuid NOT NULL, + asset_id uuid NOT NULL, + s3_key text NOT NULL, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: user_groups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_groups ( + user_id uuid NOT NULL, + group_id uuid NOT NULL +); + + +-- +-- Name: user_recovery_codes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_recovery_codes ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + user_id uuid NOT NULL, + code_hash text NOT NULL, + used_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + username text NOT NULL, + password_hash text, + display_name text, + role text DEFAULT 'editor'::text, + created_at timestamp with time zone DEFAULT now(), + is_client boolean DEFAULT false NOT NULL, + last_login_at timestamp with time zone, + failed_attempts integer DEFAULT 0 NOT NULL, + password_updated_at timestamp with time zone DEFAULT now(), + totp_secret text, + totp_enabled boolean DEFAULT false NOT NULL, + google_sub text, + email text, + totp_last_counter bigint DEFAULT 0 NOT NULL +); + + +-- +-- Name: audit_log id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_log ALTER COLUMN id SET DEFAULT nextval('public.audit_log_id_seq'::regclass); + + +-- +-- Name: api_tokens api_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_token_hash_key UNIQUE (token_hash); + + +-- +-- Name: asset_comments asset_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_comments + ADD CONSTRAINT asset_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: assets assets_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.assets + ADD CONSTRAINT assets_pkey PRIMARY KEY (id); + + +-- +-- Name: audit_log audit_log_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_log + ADD CONSTRAINT audit_log_pkey PRIMARY KEY (id); + + +-- +-- Name: bins bins_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.bins + ADD CONSTRAINT bins_pkey PRIMARY KEY (id); + + +-- +-- Name: capture_sessions capture_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.capture_sessions + ADD CONSTRAINT capture_sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: cluster_nodes cluster_nodes_hostname_uq; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cluster_nodes + ADD CONSTRAINT cluster_nodes_hostname_uq UNIQUE (hostname); + + +-- +-- Name: cluster_nodes cluster_nodes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cluster_nodes + ADD CONSTRAINT cluster_nodes_pkey PRIMARY KEY (id); + + +-- +-- Name: groups groups_name_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT groups_name_key UNIQUE (name); + + +-- +-- Name: groups groups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT groups_pkey PRIMARY KEY (id); + + +-- +-- Name: jobs jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_as_run playout_as_run_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_as_run + ADD CONSTRAINT playout_as_run_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_channels playout_channels_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_channels + ADD CONSTRAINT playout_channels_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_items playout_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_items + ADD CONSTRAINT playout_items_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_playlists playout_playlists_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_playlists + ADD CONSTRAINT playout_playlists_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_schedule playout_schedule_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_schedule + ADD CONSTRAINT playout_schedule_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_scte_breaks playout_scte_breaks_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_scte_breaks + ADD CONSTRAINT playout_scte_breaks_pkey PRIMARY KEY (id); + + +-- +-- Name: playout_sidecars playout_sidecars_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_sidecars + ADD CONSTRAINT playout_sidecars_pkey PRIMARY KEY (id); + + +-- +-- Name: project_access project_access_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.project_access + ADD CONSTRAINT project_access_pkey PRIMARY KEY (project_id, subject_type, subject_id); + + +-- +-- Name: projects projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects + ADD CONSTRAINT projects_pkey PRIMARY KEY (id); + + +-- +-- Name: recorder_schedules recorder_schedules_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.recorder_schedules + ADD CONSTRAINT recorder_schedules_pkey PRIMARY KEY (id); + + +-- +-- Name: recorders recorders_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.recorders + ADD CONSTRAINT recorders_pkey PRIMARY KEY (id); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (filename); + + +-- +-- Name: sequence_clips sequence_clips_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequence_clips + ADD CONSTRAINT sequence_clips_pkey PRIMARY KEY (id); + + +-- +-- Name: sequences sequences_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequences + ADD CONSTRAINT sequences_pkey PRIMARY KEY (id); + + +-- +-- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions + ADD CONSTRAINT sessions_pkey PRIMARY KEY (sid); + + +-- +-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.settings + ADD CONSTRAINT settings_pkey PRIMARY KEY (key); + + +-- +-- Name: temp_segments temp_segments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.temp_segments + ADD CONSTRAINT temp_segments_pkey PRIMARY KEY (id); + + +-- +-- Name: sequences uq_sequences_project_name; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequences + ADD CONSTRAINT uq_sequences_project_name UNIQUE (project_id, name); + + +-- +-- Name: user_groups user_groups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups + ADD CONSTRAINT user_groups_pkey PRIMARY KEY (user_id, group_id); + + +-- +-- Name: user_recovery_codes user_recovery_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_recovery_codes + ADD CONSTRAINT user_recovery_codes_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_username_key UNIQUE (username); + + +-- +-- Name: api_tokens_bound_hostname_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX api_tokens_bound_hostname_idx ON public.api_tokens USING btree (bound_hostname) WHERE (bound_hostname IS NOT NULL); + + +-- +-- Name: assets_ampp_sync_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX assets_ampp_sync_idx ON public.assets USING btree (ampp_sync_status, ampp_sync_next_attempt_at) WHERE (ampp_sync_status = ANY (ARRAY['pending'::text, 'failed'::text])); + + +-- +-- Name: audit_log_actor_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX audit_log_actor_idx ON public.audit_log USING btree (actor_id, created_at DESC); + + +-- +-- Name: audit_log_created_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX audit_log_created_idx ON public.audit_log USING btree (created_at DESC); + + +-- +-- Name: audit_log_type_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX audit_log_type_idx ON public.audit_log USING btree (event_type, created_at DESC); + + +-- +-- Name: cluster_nodes_hostname_uniq; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX cluster_nodes_hostname_uniq ON public.cluster_nodes USING btree (hostname); + + +-- +-- Name: idx_api_tokens_hash; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_api_tokens_hash ON public.api_tokens USING btree (token_hash); + + +-- +-- Name: idx_api_tokens_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_api_tokens_user_id ON public.api_tokens USING btree (user_id); + + +-- +-- Name: idx_asset_comments_asset; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_asset_comments_asset ON public.asset_comments USING btree (asset_id, created_at); + + +-- +-- Name: idx_assets_conform_source; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_conform_source ON public.assets USING btree (conform_source_sequence_id); + + +-- +-- Name: idx_assets_created_at_desc; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_created_at_desc ON public.assets USING btree (created_at DESC); + + +-- +-- Name: idx_assets_fulltext; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_fulltext ON public.assets USING gin (to_tsvector('english'::regconfig, ((((COALESCE(display_name, ''::text) || ' '::text) || COALESCE(filename, ''::text)) || ' '::text) || COALESCE(notes, ''::text)))); + + +-- +-- Name: idx_assets_live_unique; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_assets_live_unique ON public.assets USING btree (project_id, display_name) WHERE (status = 'live'::public.asset_status); + + +-- +-- Name: idx_assets_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_project_id ON public.assets USING btree (project_id); + + +-- +-- Name: idx_assets_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_status ON public.assets USING btree (status); + + +-- +-- Name: idx_assets_tags; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_assets_tags ON public.assets USING gin (tags); + + +-- +-- Name: idx_bins_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_bins_project_id ON public.bins USING btree (project_id); + + +-- +-- Name: idx_jobs_asset_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_jobs_asset_id ON public.jobs USING btree (asset_id); + + +-- +-- Name: idx_jobs_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_jobs_status ON public.jobs USING btree (status); + + +-- +-- Name: idx_jobs_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_jobs_type ON public.jobs USING btree (type); + + +-- +-- Name: idx_playout_as_run_channel; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_as_run_channel ON public.playout_as_run USING btree (channel_id, started_at DESC); + + +-- +-- Name: idx_playout_channels_node; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_channels_node ON public.playout_channels USING btree (node_id); + + +-- +-- Name: idx_playout_channels_project; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_channels_project ON public.playout_channels USING btree (project_id); + + +-- +-- Name: idx_playout_items_asset; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_items_asset ON public.playout_items USING btree (asset_id); + + +-- +-- Name: idx_playout_items_playlist; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_items_playlist ON public.playout_items USING btree (playlist_id, sort_order); + + +-- +-- Name: idx_playout_playlists_channel; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_playlists_channel ON public.playout_playlists USING btree (channel_id); + + +-- +-- Name: idx_playout_schedule_channel_time; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_schedule_channel_time ON public.playout_schedule USING btree (channel_id, scheduled_at); + + +-- +-- Name: idx_playout_schedule_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_schedule_status ON public.playout_schedule USING btree (status, scheduled_at); + + +-- +-- Name: idx_playout_scte_channel; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_scte_channel ON public.playout_scte_breaks USING btree (channel_id, created_at DESC); + + +-- +-- Name: idx_playout_scte_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_scte_status ON public.playout_scte_breaks USING btree (status); + + +-- +-- Name: idx_playout_sidecars_channel; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_playout_sidecars_channel ON public.playout_sidecars USING btree (channel_id) WHERE (status = ANY (ARRAY['starting'::text, 'running'::text])); + + +-- +-- Name: idx_playout_sidecars_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_playout_sidecars_status ON public.playout_sidecars USING btree (status); + + +-- +-- Name: idx_project_access_subject; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_project_access_subject ON public.project_access USING btree (subject_type, subject_id); + + +-- +-- Name: idx_recorder_schedules_recorder; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_recorder_schedules_recorder ON public.recorder_schedules USING btree (recorder_id); + + +-- +-- Name: idx_recorder_schedules_status_start; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_recorder_schedules_status_start ON public.recorder_schedules USING btree (status, start_at); + + +-- +-- Name: idx_recorders_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_recorders_status ON public.recorders USING btree (status); + + +-- +-- Name: idx_sequence_clips_asset_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sequence_clips_asset_id ON public.sequence_clips USING btree (asset_id); + + +-- +-- Name: idx_sequence_clips_sequence_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sequence_clips_sequence_id ON public.sequence_clips USING btree (sequence_id); + + +-- +-- Name: idx_sequence_clips_track_position; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sequence_clips_track_position ON public.sequence_clips USING btree (sequence_id, track, timeline_in_frames); + + +-- +-- Name: idx_sequences_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sequences_project_id ON public.sequences USING btree (project_id); + + +-- +-- Name: idx_sequences_updated_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sequences_updated_at ON public.sequences USING btree (updated_at DESC); + + +-- +-- Name: idx_sessions_expire; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sessions_expire ON public.sessions USING btree (expire); + + +-- +-- Name: idx_temp_segments_asset_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_temp_segments_asset_id ON public.temp_segments USING btree (asset_id); + + +-- +-- Name: idx_temp_segments_expires_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_temp_segments_expires_at ON public.temp_segments USING btree (expires_at); + + +-- +-- Name: idx_temp_segments_job_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_temp_segments_job_id ON public.temp_segments USING btree (job_id); + + +-- +-- Name: idx_user_groups_group; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_groups_group ON public.user_groups USING btree (group_id); + + +-- +-- Name: idx_user_groups_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_groups_user ON public.user_groups USING btree (user_id); + + +-- +-- Name: idx_user_recovery_codes_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_recovery_codes_user ON public.user_recovery_codes USING btree (user_id); + + +-- +-- Name: idx_users_google_sub; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_users_google_sub ON public.users USING btree (google_sub) WHERE (google_sub IS NOT NULL); + + +-- +-- Name: recorders_node_device_uniq; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX recorders_node_device_uniq ON public.recorders USING btree (node_id, device_index) WHERE ((node_id IS NOT NULL) AND (device_index IS NOT NULL)); + + +-- +-- Name: recorders_node_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX recorders_node_id_idx ON public.recorders USING btree (node_id); + + +-- +-- Name: users_is_client_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX users_is_client_idx ON public.users USING btree (is_client) WHERE (is_client = true); + + +-- +-- Name: api_tokens api_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: asset_comments asset_comments_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_comments + ADD CONSTRAINT asset_comments_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; + + +-- +-- Name: asset_comments asset_comments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.asset_comments + ADD CONSTRAINT asset_comments_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + +-- +-- Name: assets assets_bin_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.assets + ADD CONSTRAINT assets_bin_id_fkey FOREIGN KEY (bin_id) REFERENCES public.bins(id) ON DELETE SET NULL; + + +-- +-- Name: assets assets_conform_source_sequence_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.assets + ADD CONSTRAINT assets_conform_source_sequence_id_fkey FOREIGN KEY (conform_source_sequence_id) REFERENCES public.sequences(id); + + +-- +-- Name: assets assets_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.assets + ADD CONSTRAINT assets_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: audit_log audit_log_actor_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audit_log + ADD CONSTRAINT audit_log_actor_id_fkey FOREIGN KEY (actor_id) REFERENCES public.users(id) ON DELETE SET NULL; + + +-- +-- Name: bins bins_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.bins + ADD CONSTRAINT bins_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.bins(id) ON DELETE SET NULL; + + +-- +-- Name: bins bins_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.bins + ADD CONSTRAINT bins_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: capture_sessions capture_sessions_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.capture_sessions + ADD CONSTRAINT capture_sessions_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id); + + +-- +-- Name: capture_sessions capture_sessions_bin_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.capture_sessions + ADD CONSTRAINT capture_sessions_bin_id_fkey FOREIGN KEY (bin_id) REFERENCES public.bins(id); + + +-- +-- Name: capture_sessions capture_sessions_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.capture_sessions + ADD CONSTRAINT capture_sessions_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id); + + +-- +-- Name: jobs jobs_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.jobs + ADD CONSTRAINT jobs_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE SET NULL; + + +-- +-- Name: playout_as_run playout_as_run_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_as_run + ADD CONSTRAINT playout_as_run_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE SET NULL; + + +-- +-- Name: playout_as_run playout_as_run_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_as_run + ADD CONSTRAINT playout_as_run_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.playout_channels(id) ON DELETE CASCADE; + + +-- +-- Name: playout_channels playout_channels_node_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_channels + ADD CONSTRAINT playout_channels_node_id_fkey FOREIGN KEY (node_id) REFERENCES public.cluster_nodes(id) ON DELETE SET NULL; + + +-- +-- Name: playout_channels playout_channels_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_channels + ADD CONSTRAINT playout_channels_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE SET NULL; + + +-- +-- Name: playout_items playout_items_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_items + ADD CONSTRAINT playout_items_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; + + +-- +-- Name: playout_items playout_items_playlist_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_items + ADD CONSTRAINT playout_items_playlist_id_fkey FOREIGN KEY (playlist_id) REFERENCES public.playout_playlists(id) ON DELETE CASCADE; + + +-- +-- Name: playout_playlists playout_playlists_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_playlists + ADD CONSTRAINT playout_playlists_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.playout_channels(id) ON DELETE CASCADE; + + +-- +-- Name: playout_schedule playout_schedule_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_schedule + ADD CONSTRAINT playout_schedule_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE SET NULL; + + +-- +-- Name: playout_schedule playout_schedule_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_schedule + ADD CONSTRAINT playout_schedule_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.playout_channels(id) ON DELETE CASCADE; + + +-- +-- Name: playout_scte_breaks playout_scte_breaks_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_scte_breaks + ADD CONSTRAINT playout_scte_breaks_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.playout_channels(id) ON DELETE CASCADE; + + +-- +-- Name: playout_scte_breaks playout_scte_breaks_created_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_scte_breaks + ADD CONSTRAINT playout_scte_breaks_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL; + + +-- +-- Name: playout_sidecars playout_sidecars_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_sidecars + ADD CONSTRAINT playout_sidecars_channel_id_fkey FOREIGN KEY (channel_id) REFERENCES public.playout_channels(id) ON DELETE CASCADE; + + +-- +-- Name: playout_sidecars playout_sidecars_node_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playout_sidecars + ADD CONSTRAINT playout_sidecars_node_id_fkey FOREIGN KEY (node_id) REFERENCES public.cluster_nodes(id) ON DELETE SET NULL; + + +-- +-- Name: project_access project_access_granted_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.project_access + ADD CONSTRAINT project_access_granted_by_fkey FOREIGN KEY (granted_by) REFERENCES public.users(id) ON DELETE SET NULL; + + +-- +-- Name: project_access project_access_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.project_access + ADD CONSTRAINT project_access_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: recorder_schedules recorder_schedules_recorder_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.recorder_schedules + ADD CONSTRAINT recorder_schedules_recorder_id_fkey FOREIGN KEY (recorder_id) REFERENCES public.recorders(id) ON DELETE CASCADE; + + +-- +-- Name: recorders recorders_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.recorders + ADD CONSTRAINT recorders_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id); + + +-- +-- Name: sequence_clips sequence_clips_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequence_clips + ADD CONSTRAINT sequence_clips_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; + + +-- +-- Name: sequence_clips sequence_clips_sequence_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequence_clips + ADD CONSTRAINT sequence_clips_sequence_id_fkey FOREIGN KEY (sequence_id) REFERENCES public.sequences(id) ON DELETE CASCADE; + + +-- +-- Name: sequences sequences_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sequences + ADD CONSTRAINT sequences_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: temp_segments temp_segments_asset_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.temp_segments + ADD CONSTRAINT temp_segments_asset_id_fkey FOREIGN KEY (asset_id) REFERENCES public.assets(id) ON DELETE CASCADE; + + +-- +-- Name: temp_segments temp_segments_job_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.temp_segments + ADD CONSTRAINT temp_segments_job_id_fkey FOREIGN KEY (job_id) REFERENCES public.jobs(id) ON DELETE CASCADE; + + +-- +-- Name: user_groups user_groups_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups + ADD CONSTRAINT user_groups_group_id_fkey FOREIGN KEY (group_id) REFERENCES public.groups(id) ON DELETE CASCADE; + + +-- +-- Name: user_groups user_groups_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups + ADD CONSTRAINT user_groups_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: user_recovery_codes user_recovery_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_recovery_codes + ADD CONSTRAINT user_recovery_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict mZfxRJrhoiQMVbssWG695aKA8Byev914TXjorYV82xHaynuUiKs6gaVmQSkekKy + diff --git a/services/web-ui/public/app.jsx b/services/web-ui/public/app.jsx index 1510629..6709a70 100644 --- a/services/web-ui/public/app.jsx +++ b/services/web-ui/public/app.jsx @@ -6,7 +6,6 @@ function App() { const [route, setRoute] = React.useState('home'); const [openAsset, setOpenAsset] = React.useState(null); const [openProject, setOpenProject] = React.useState(null); - const [showNewRecorder, setShowNewRecorder] = React.useState(false); const [dataReady, setDataReady] = React.useState(false); const [loadError, setLoadError] = React.useState(null); const [sidebarCollapsed, setSidebarCollapsed] = React.useState(() => { @@ -126,7 +125,7 @@ function App() { case 'library': content = setOpenProject(null)} onOpenProject={openProjectFromAnywhere} />; break; case 'projects': content = ; break; case 'upload': content = ; break; - case 'recorders': content = setShowNewRecorder(true)} />; break; + case 'recorders': content = ; break; case 'schedule': content = ; break; case 'youtube': content = ; break; case 'capture': content = ; break; @@ -162,7 +161,6 @@ function App() { )} {content} - {showNewRecorder && setShowNewRecorder(false)} />} ); } diff --git a/services/web-ui/public/index.html b/services/web-ui/public/index.html index 2f29532..af814ca 100644 --- a/services/web-ui/public/index.html +++ b/services/web-ui/public/index.html @@ -49,7 +49,6 @@ - diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx deleted file mode 100644 index 57d248d..0000000 --- a/services/web-ui/public/modal-new-recorder.jsx +++ /dev/null @@ -1,604 +0,0 @@ -// modal-new-recorder.jsx - New Recorder dialog (SRT / RTMP / SDI / Deltacast) - -/** - * DevicePortPicker - groups a flat per-port API response by node_id and - * renders one button per actual port. Replaces the old code that iterated - * over entries and synthesised port counts, which caused duplicate groups. - * - * props: - * ports - flat array from /cluster/devices/blackmagic or /deltacast - * each entry: { node_id, hostname, model, index, device, present? } - * selectedIdx - currently selected device_index - * selectedNode - currently selected node_id - * onSelect(idx, nodeId) - * portLabel - e.g. "SDI" or "Port" - * showTestBadge - show TEST CARD badge when present===false - */ -function DevicePortPicker({ ports, selectedIdx, selectedNode, onSelect, portLabel = 'Port', showTestBadge = false }) { - // Group by node_id (stable - one group per physical node) - const groups = React.useMemo(() => { - const map = new Map(); - for (const p of ports) { - const key = p.node_id || p.hostname || 'unknown'; - if (!map.has(key)) map.set(key, { nodeId: p.node_id || p.hostname || '', hostname: p.hostname || key, model: p.model || '', ports: [] }); - map.get(key).ports.push(p); - } - // Sort ports within each group by index - for (const g of map.values()) g.ports.sort((a, b) => a.index - b.index); - return Array.from(map.values()); - }, [ports]); - - return ( -
- {groups.map(group => ( -
1 ? 12 : 4 }}> - {/* Node header: only show when multiple groups, or always for clarity */} -
- {group.model ? group.model.toUpperCase() + ' · ' : ''}{group.hostname} -
- {group.ports.map(port => { - const active = selectedIdx === port.index && selectedNode === group.nodeId; - return ( - - ); - })} -
- ))} -
- ); -} - -/** - * ManualDevicePicker - fallback when no devices detected. Lets the operator - * pick node + index from dropdowns. - */ -function ManualDevicePicker({ nodes, nodeId, deviceIdx, portLabel, portCount, onNodeChange, onIdxChange, emptyNote }) { - return ( -
-
- {emptyNote || `No ${portLabel} devices auto-detected. Configure manually:`} -
-
-
- - -
-
- - -
-
-
- ); -} - -function ProbeResult({ result }) { - if (!result.ok) { - return ( -
- Probe failed: {result.error} -
- ); - } - const d = result.data || {}; - const entries = Object.entries(d).filter(([, v]) => v !== null && typeof v !== 'object'); - if (entries.length === 0) { - return ( -
- ✓ Source reachable -
- ); - } - return ( -
- {entries.map(([k, v]) => ( -
- {k} - {String(v)} -
- ))} -
- ); -} - -function NewRecorderModal({ open, onClose }) { - const PROJECTS = window.ZAMPP_DATA?.PROJECTS || []; - const NODES = window.ZAMPP_DATA?.NODES || []; - const [name, setName] = React.useState(''); - const [sourceType, setSourceType] = React.useState('SRT'); - const [srtUrl, setSrtUrl] = React.useState('srt://10.0.4.18:4200'); - const [rtmpUrl, setRtmpUrl] = React.useState('rtmp://stream.local/live/cam_a'); - const [sdiDeviceIdx, setSdiDeviceIdx] = React.useState(0); - const [sdiNodeId, setSdiNodeId] = React.useState(() => { - const n = NODES[0]; - return n ? (n.id || n.hostname || '') : ''; - }); - const [sdiDevices, setSdiDevices] = React.useState(null); - const [dcDeviceIdx, setDcDeviceIdx] = React.useState(0); - const [dcNodeId, setDcNodeId] = React.useState(() => { - const n = NODES[0]; - return n ? (n.id || n.hostname || '') : ''; - }); - const [dcDevices, setDcDevices] = React.useState(null); - const [recTab, setRecTab] = React.useState('video'); - // All-Intra HEVC (NVENC) is the default master — GPU-encoded, growing-file - // capable in fragmented MOV. ProRes stays selectable for 4:2:2 mezzanine. - const [recCodec, setRecCodec] = React.useState('hevc_nvenc'); - // Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 / - // x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven). - const [recBitrate, setRecBitrate] = React.useState('25'); - // Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR - // → MOV (fragmented, growing-capable); H.264 → MP4. - const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov'; - // Codecs whose bitrate is operator-controlled (everything except ProRes). - const BITRATE_CODECS = new Set(['hevc_nvenc', 'h264_nvenc', 'libx264', 'libx265', 'dnxhd', 'dnxhr_hq']); - const codecUsesBitrate = BITRATE_CODECS.has(recCodec); - const [proxyOn, setProxyOn] = React.useState(true); - const [growingOn, setGrowingOn] = React.useState(false); - // Growing master is VC-3 / DNxHD in MXF OP1a (ffmpeg-direct, Premiere-native - // edit-while-record — matches vMix). Two bitrates: 90 Mbps (default, lighter) - // or 220 Mbps (highest quality). Both 8-bit 4:2:2 @ 1080p59.94. - const [growingCodec, setGrowingCodec] = React.useState('vc3_90'); - const showBitrate = codecUsesBitrate; - const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || ''); - const [submitting, setSubmitting] = React.useState(false); - const [submitErr, setSubmitErr] = React.useState(null); - const [probing, setProbing] = React.useState(false); - const [probeResult, setProbeResult] = React.useState(null); - - React.useEffect(() => { - if (sourceType !== 'SDI' || sdiDevices !== null) return; - window.ZAMPP_API.fetch('/cluster/devices/blackmagic') - .then(d => setSdiDevices(Array.isArray(d) ? d : [])) - .catch(() => setSdiDevices([])); - }, [sourceType]); - - React.useEffect(() => { - if (sourceType !== 'DELTACAST' || dcDevices !== null) return; - window.ZAMPP_API.fetch('/cluster/devices/deltacast') - .then(d => setDcDevices(Array.isArray(d) ? d : [])) - .catch(() => setDcDevices([])); - }, [sourceType]); - - React.useEffect(() => { setProbeResult(null); }, [sourceType, srtUrl, rtmpUrl]); - - const handleProbe = () => { - setProbing(true); - setProbeResult(null); - const body = sourceType === 'SRT' - ? { source_type: 'srt', url: srtUrl } - : { source_type: 'rtmp', url: rtmpUrl }; - window.ZAMPP_API.fetch('/recorders/probe', { method: 'POST', body: JSON.stringify(body) }) - .then(r => { setProbing(false); setProbeResult({ ok: true, data: r }); }) - .catch(e => { setProbing(false); setProbeResult({ ok: false, error: e.message }); }); - }; - - const handleCreate = () => { - if (!name.trim()) { setSubmitErr('Recorder name is required.'); return; } - if (sourceType === 'SDI' && !sdiNodeId) { setSubmitErr('Select a capture node for SDI.'); return; } - if (sourceType === 'DELTACAST' && !dcNodeId) { setSubmitErr('Select a capture node for Deltacast.'); return; } - setSubmitting(true); - setSubmitErr(null); - - const body = { - name: name.trim(), - source_type: sourceType.toLowerCase(), - project_id: projectId || undefined, - generate_proxy: proxyOn, - growing_enabled: growingOn, - growing_codec: growingOn ? growingCodec : undefined, - recording_codec: recCodec, - recording_container: recContainer, - // Framerate + resolution are auto-detected from the source signal/stream. - recording_framerate: '', // empty = match source - recording_resolution: 'native', - }; - // Custom bitrate applies to non-growing bitrate-controlled codecs only. - // VC-3 growing bitrate is selected via the growing codec (vc3_90 / vc3_220). - if (codecUsesBitrate && !growingOn && recBitrate) { - body.recording_video_bitrate = `${recBitrate}M`; - } - - if (sourceType === 'SRT') { - body.source_config = { url: srtUrl }; - } else if (sourceType === 'RTMP') { - body.source_config = { url: rtmpUrl }; - } else if (sourceType === 'DELTACAST') { - // One Deltacast board (index 0) exposes 8 channels. The picker's selected - // index IS the capture channel, so persist it as source_config.port; the - // capture sidecar maps that to the bridge's --port. device_index is kept - // for backward-compatible display/fallback. - body.source_config = { port: dcDeviceIdx }; - body.device_index = dcDeviceIdx; - body.node_id = dcNodeId || undefined; - } else { - // SDI (DeckLink): device_index and node_id are top-level fields - body.source_config = {}; - body.device_index = sdiDeviceIdx; - body.node_id = sdiNodeId || undefined; - } - - window.ZAMPP_API.fetch('/recorders', { method: 'POST', body: JSON.stringify(body) }) - .then(() => { - setSubmitting(false); - // Recorders list listens for this and re-fetches; otherwise the - // operator has to wait for the next 10s poll tick to see the new row. - window.dispatchEvent(new CustomEvent('df:recorders-changed')); - onClose(); - }) - .catch(e => { setSubmitting(false); setSubmitErr(e.message || 'Failed to create recorder'); }); - }; - - if (!open) return null; - - return ( -
-
e.stopPropagation()}> -
-
-
New recorder
-
Configure source, codec, and destination
-
- -
- -
-
- - setName(e.target.value)} /> -
- -
- -
- {[ - { id: 'SRT', label: 'SRT', desc: 'Secure Reliable Transport · pull caller', icon: 'signal' }, - { id: 'RTMP', label: 'RTMP', desc: 'Real-Time Messaging Protocol', icon: 'globe' }, - { id: 'SDI', label: 'SDI', desc: 'Blackmagic DeckLink hardware', icon: 'video' }, - { id: 'DELTACAST', label: 'Deltacast', desc: 'Deltacast VideoMaster SDI', icon: 'video' }, - ].map(t => ( - - ))} -
-
- - {sourceType === 'SRT' && ( -
- -
- setSrtUrl(e.target.value)} style={{ flex: 1 }} /> - -
-
- Recorder connects out to this URL (caller mode). -
- {probeResult && } -
- )} - - {sourceType === 'RTMP' && ( -
- -
- setRtmpUrl(e.target.value)} style={{ flex: 1 }} /> - -
-
- Recorder pulls this RTMP stream. -
- {probeResult && } -
- )} - - {sourceType === 'SDI' && ( -
- - {sdiDevices === null && ( -
Detecting DeckLink devices…
- )} - {sdiDevices !== null && sdiDevices.length > 0 && ( - { setSdiDeviceIdx(idx); setSdiNodeId(nodeId); }} - portLabel="SDI" - /> - )} - {sdiDevices !== null && sdiDevices.length === 0 && ( - - )} -
- )} - - {sourceType === 'DELTACAST' && ( -
- - {dcDevices === null && ( -
Detecting Deltacast devices…
- )} - {dcDevices !== null && dcDevices.length > 0 && ( - { setDcDeviceIdx(idx); setDcNodeId(nodeId); }} - portLabel="Port" - showTestBadge - /> - )} - {dcDevices !== null && dcDevices.length === 0 && ( - - )} -
- )} - -
-
- Master recording - -
- {['video', 'audio', 'container'].map(t => ( - - ))} -
-
-
- {recTab === 'video' && ( - <> - {/* Codec presets — one click fills codec + bitrate with a known-good - combo that passes the server-side validateRecorderConfig guard. - Container is derived from the codec (HEVC/ProRes/DNxHR → MOV, - H.264 → MP4), and master audio is always PCM (valid in MOV). */} -
- {[ - { id: 'hevc', label: 'HEVC Master (MOV)', codec: 'hevc_nvenc', bitrate: '25' }, - { id: 'h264', label: 'H.264 Proxy-friendly (MP4)', codec: 'h264_nvenc', bitrate: '25' }, - ].map(p => ( - - ))} -
-
-
- - {growingOn ? ( -
- {[ - { v: 'vc3_90', big: '90', note: 'Lighter · default' }, - { v: 'vc3_220', big: '220', note: 'Highest quality' }, - ].map(opt => { - const active = growingCodec === opt.v; - return ( - - ); - })} -
- ) : ( - - )} -
- {showBitrate ? ( -
- - setRecBitrate(e.target.value)} - /> -
- ) : ( - - )} - - - {/* #3: warn when the configured bitrate exceeds the probed source - bitrate — re-encoding above source adds storage, not quality. */} - {codecUsesBitrate && (() => { - const d = probeResult && probeResult.ok ? (probeResult.data || {}) : null; - const raw = d && (d.bitrate ?? d.bit_rate ?? d.video_bitrate ?? (d.video && d.video.bit_rate)); - const srcMbps = raw ? (Number(raw) > 100000 ? Number(raw) / 1e6 : Number(raw)) : null; - const cfg = parseFloat(recBitrate); - if (srcMbps && cfg && cfg > srcMbps * 1.05) { - return ( -
- ⚠ Target {cfg} Mbps exceeds the source stream (~{srcMbps.toFixed(1)} Mbps). Encoding above the source bitrate increases file size without adding quality. -
- ); - } - return null; - })()} -
- - )} - {recTab === 'audio' && ( -
- - - - -
- )} - {recTab === 'container' && ( -
- - -
- )} -
-
- -
- -
-
Generate proxy
-
- SDI sources record proxy in parallel. Network sources generate proxy after stop. -
-
-
- -
- -
-
Growing-files mode
-
- Write the live master to the SMB share so editors can cut while it's still recording. - Requires the SMB share to be configured in Settings → Storage. -
- {growingOn && ( -
- Records VC-3 / DNxHD (8-bit 4:2:2) in MXF OP1a — Premiere-native edit-while-record, - imports and grows live in Premiere. {growingCodec === 'vc3_220' ? '~220 Mbps (highest quality).' : '~90 Mbps (lighter storage, default).'} -
- )} -
-
- - {proxyOn && ( -
-
Proxy
-
-
- {['H.264', '2 Mbps', 'MP4', '1920×1080', 'AAC 128 kbps'].map(tag => ( - {tag} - ))} -
-
Fixed proxy profile. Not configurable.
-
-
- )} - -
-
Destination
-
-
- - -
-
-
- - {submitErr && ( -
{submitErr}
- )} -
- -
- - - -
-
-
- ); -} - -window.NewRecorderModal = NewRecorderModal; diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index 7fe1d92..ae58998 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -878,7 +878,7 @@ function _nodeMeta(nodeId) { return { hostname: n.hostname || (nodeId ? nodeId.slice(0, 8) : 'node'), online }; } -function Recorders({ navigate, onNew }) { +function Recorders({ navigate }) { const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []); // Per-recorder config editor (codec / growing / label). Null = closed. const [configRecorder, setConfigRecorder] = React.useState(null); @@ -1177,9 +1177,19 @@ function RecorderRow({ recorder: initialRecorder, onRefresh, onConfigure, nodeOn )}
- {recorder.codec}· - {recorder.res}· - {recorder.framerate} + {recorder.growing ? ( + <> + VC-3/DNxHD· + MXF OP1a· + 4:2:2 + + ) : ( + <> + {recorder.codec}· + {recorder.res}· + {recorder.framerate} + + )}
{err &&
{err}
} {liveStatus?.lastError && isRec && (