Files
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

316 lines
14 KiB
SQL

-- =====================================================================
-- RENDER SCHEMA — Part 2: Jobs, Frames, Snapshots, Exports
-- =====================================================================
SET search_path TO render, public;
CREATE TYPE render_step AS ENUM (
'Queued','Preparing','TemplateCache','JsxGen','Music',
'Rendering','Validating','Repairing','Optimisation','Video',
'Mixing','Final','Uploading','Done','Failed','Cancelled'
);
CREATE TYPE price_kind AS ENUM ('Free','Preview','Cash','Plan','Snapshot','Reseller');
CREATE TYPE render_quality AS ENUM ('Low','Medium','High','Full','Lossless');
CREATE TYPE frame_job_status AS ENUM (
'Pending','Rendering','Validated','Repairing','Converting','Done','Failed'
);
CREATE TYPE render_priority_queue AS ENUM (
'snapshot','vip','paid','preview','mockup','voiceover'
);
-- ---------------------------------------------------------------------
-- render_jobs — top-level render task
-- ---------------------------------------------------------------------
CREATE TABLE render_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
-- References (loose cross-schema)
saved_project_id UUID NOT NULL,
original_project_id UUID NOT NULL,
project_name TEXT,
-- Job identity
title TEXT,
name TEXT,
external_job_id TEXT, -- RabbitMQ message ID
priority_queue render_priority_queue NOT NULL,
priority_score INT NOT NULL DEFAULT 50, -- 0-100, higher = sooner
-- Pipeline state
step render_step NOT NULL DEFAULT 'Queued',
render_progress INT NOT NULL DEFAULT 0 CHECK (render_progress BETWEEN 0 AND 100),
convert_progress INT NOT NULL DEFAULT 0,
image_preview_b64 TEXT, -- last frame thumbnail
-- Pricing
price_type price_kind NOT NULL,
paid_price_minor BIGINT NOT NULL DEFAULT 0,
discount_code TEXT,
support_flatrender BOOLEAN NOT NULL DEFAULT FALSE,
-- Output config
mode TEXT NOT NULL, -- FIX/FLEXIBLE/MockUp/MusicVisualizer
quality render_quality NOT NULL DEFAULT 'High',
resolution TEXT NOT NULL, -- FullHD/FourK
r_height INT NOT NULL,
frame_rate INT NOT NULL DEFAULT 30,
is_60_fps BOOLEAN NOT NULL DEFAULT FALSE,
duration_sec NUMERIC(8,2) NOT NULL,
export_duration_sec NUMERIC(8,2),
-- Audio (NEW)
has_music BOOLEAN NOT NULL DEFAULT FALSE,
has_sfx BOOLEAN NOT NULL DEFAULT FALSE,
has_voiceover BOOLEAN NOT NULL DEFAULT FALSE,
music_volume NUMERIC(4,3),
sfx_volume NUMERIC(4,3),
voiceover_volume NUMERIC(4,3),
-- Resource allocation
render_node_count INT NOT NULL DEFAULT 1,
current_active_nodes INT NOT NULL DEFAULT 0,
region TEXT, -- preferred region
tell_me_when_done BOOLEAN NOT NULL DEFAULT TRUE,
-- Retry / recovery
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 3,
repair_attempts INT NOT NULL DEFAULT 0,
failed_message TEXT,
failed_at_step render_step,
-- File outputs (paths in MinIO; final URL goes in exports.path)
render_folder TEXT, -- temp working dir
output_folder TEXT, -- frames dir
physical_render_folder TEXT, -- absolute path for nodes
physical_output_folder TEXT,
target_replica_name TEXT,
-- Reference to result
export_id UUID,
-- Timing
task_start_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
start_render_date TIMESTAMPTZ,
done_task_date TIMESTAMPTZ,
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_render_jobs_user ON render_jobs(user_id, created_at DESC);
CREATE INDEX idx_render_jobs_tenant ON render_jobs(tenant_id, created_at DESC);
CREATE INDEX idx_render_jobs_step ON render_jobs(step) WHERE step NOT IN ('Done','Failed','Cancelled');
CREATE INDEX idx_render_jobs_queue ON render_jobs(priority_queue, priority_score DESC, queued_at)
WHERE step = 'Queued';
CREATE INDEX idx_render_jobs_in_flight ON render_jobs(started_at) WHERE step NOT IN ('Done','Failed','Cancelled','Queued');
CREATE TRIGGER tg_render_jobs_updated_at
BEFORE UPDATE ON render_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- frame_jobs — per-node frame-range assignments
-- ---------------------------------------------------------------------
CREATE TABLE frame_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES render_nodes(id),
-- Range
start_frame INT NOT NULL,
end_frame INT NOT NULL,
collect_frame_count INT NOT NULL,
order_value INT NOT NULL DEFAULT 0,
folder_name TEXT NOT NULL, -- "O0", "O1"...
convert_url TEXT,
-- Status
status frame_job_status NOT NULL DEFAULT 'Pending',
frames_rendered INT NOT NULL DEFAULT 0,
frames_validated INT NOT NULL DEFAULT 0,
-- Errors
attempt INT NOT NULL DEFAULT 1,
last_error TEXT,
-- Outputs
output_mp4_url TEXT, -- chunk MP4 after ffmpeg
-- Timing
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
last_progress_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_frame_jobs_render ON frame_jobs(render_job_id, order_value);
CREATE INDEX idx_frame_jobs_node ON frame_jobs(node_id, status);
CREATE INDEX idx_frame_jobs_stalled ON frame_jobs(last_progress_at) WHERE status = 'Rendering';
CREATE TRIGGER tg_frame_jobs_updated_at
BEFORE UPDATE ON frame_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- frame_repair_jobs — missing/corrupt frame repair tracking
-- ---------------------------------------------------------------------
CREATE TABLE frame_repair_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
node_id UUID REFERENCES render_nodes(id),
-- Range to repair
start_frame INT NOT NULL,
end_frame INT NOT NULL,
missing_frames INT[] NOT NULL DEFAULT '{}',
corrupt_frames INT[] NOT NULL DEFAULT '{}',
attempt INT NOT NULL DEFAULT 1,
status frame_job_status NOT NULL DEFAULT 'Pending',
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE INDEX idx_frame_repair_job ON frame_repair_jobs(render_job_id, attempt);
-- ---------------------------------------------------------------------
-- snapshots — single-frame scene previews (new feature)
-- ---------------------------------------------------------------------
CREATE TABLE snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
saved_project_id UUID NOT NULL,
scene_key TEXT NOT NULL,
frame_number INT NOT NULL,
-- Cache key — same inputs = same output
inputs_hash TEXT NOT NULL,
-- Status
status TEXT NOT NULL DEFAULT 'Pending', -- Pending/Rendering/Done/Failed
render_node_id UUID REFERENCES render_nodes(id),
-- Output
image_url TEXT,
thumbnail_url TEXT,
width INT,
height INT,
size_bytes BIGINT,
-- Timing
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
duration_ms INT,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX uq_snapshots_cache ON snapshots(saved_project_id, scene_key, frame_number, inputs_hash)
WHERE status = 'Done';
CREATE INDEX idx_snapshots_user ON snapshots(user_id, requested_at DESC);
CREATE INDEX idx_snapshots_expire ON snapshots(expires_at) WHERE status = 'Done';
-- ---------------------------------------------------------------------
-- exports — final rendered output records
-- ---------------------------------------------------------------------
CREATE TYPE export_create_type AS ENUM ('Render','Upload','Snapshot','Reupload');
CREATE TYPE export_file_type AS ENUM ('Video','Image','Audio','GIF','PDF');
CREATE TABLE exports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
saved_project_id UUID NOT NULL,
project_id UUID NOT NULL,
render_job_id UUID REFERENCES render_jobs(id) ON DELETE SET NULL,
-- Output
image TEXT, -- thumbnail
path TEXT NOT NULL, -- main file URL
file_extension TEXT NOT NULL DEFAULT 'mp4',
file_type export_file_type NOT NULL DEFAULT 'Video',
render_quality render_quality NOT NULL,
create_type export_create_type NOT NULL DEFAULT 'Render',
size_bytes BIGINT NOT NULL,
duration_sec NUMERIC(8,2),
width INT,
height INT,
-- Lifecycle
produce_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
auto_delete_date TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 days',
delete_notified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_exports_user ON exports(user_id, produce_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_exports_tenant ON exports(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_exports_saved_project ON exports(saved_project_id);
CREATE INDEX idx_exports_auto_delete ON exports(auto_delete_date) WHERE deleted_at IS NULL;
-- Now wire FK back to render_jobs
ALTER TABLE render_jobs
ADD CONSTRAINT fk_render_jobs_export
FOREIGN KEY (export_id) REFERENCES exports(id) ON DELETE SET NULL;
-- ---------------------------------------------------------------------
-- export_files — mockup mode produces multiple images per export
-- ---------------------------------------------------------------------
CREATE TABLE export_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
export_id UUID NOT NULL REFERENCES exports(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
name TEXT,
thumbnail TEXT,
path TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
file_type export_file_type NOT NULL DEFAULT 'Image',
width INT,
height INT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_export_files_export ON export_files(export_id, sort);
-- ---------------------------------------------------------------------
-- render_progress_events — WebSocket fan-out source (short-lived)
-- ---------------------------------------------------------------------
CREATE TABLE render_progress_events (
id BIGSERIAL PRIMARY KEY,
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
step render_step NOT NULL,
progress INT NOT NULL,
current_frame INT,
total_frames INT,
eta_seconds INT,
preview_b64 TEXT,
message TEXT,
emitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_progress_events_job ON render_progress_events(render_job_id, emitted_at DESC);
-- Cleanup: keep last N per job via cron, drop > 7 days