feat: produce worker deployment + web producer/collective/branding UI + mobile CSS (wave-2c)

Deployment (t4g2): media-worker gains font-dejavu (drawtext lower-thirds)
and a dedicated produce-worker compose service running src/produce.ts —
claims video_processing_jobs compose|mix by lane, renders assembled/
overlaid/voiced cuts, registers derived library assets. Deliberately its
own process so a remote-lane box needs only Postgres + S3 + ffmpeg.

Web: compose media attachments UI (t2), Producer panel in the video
drawer (t5), collective creation + branding settings + shell branding
(brandweb), mobile CSS (mcss: touch targets, icon sizing, safe areas).
This commit is contained in:
glass beadian 2026-10-14 20:40:49 +00:00
parent 1183d69849
commit d9e8cdf789
18 changed files with 2384 additions and 20 deletions

View file

@ -0,0 +1,245 @@
/**
* Postgres produce job store: the poll/claim/complete half of the produce worker.
*
* - Claim: `UPDATE ... SET state='transcoding' WHERE id IN (SELECT id ... FOR UPDATE
* SKIP LOCKED) RETURNING` — one row, one statement, lane-filtered, so concurrent
* workers never race and a claimed row is invisible to everyone else until its
* owner finishes it. The `queued → transcoding` bump inside SKIP LOCKED is the
* lease: there is no separate lease column to renew, because a claimed row is
* owned until its owner writes complete/failed.
* - Every write happens inside the job's tenant transaction (`app.workspace_id`),
* because both tables are FORCE ROW LEVEL SECURITY with a `tenant_isolation`
* policy (009). The claim reads the workspace off the row first, then re-claims
* inside that tenant's scope.
* - A row whose `input_json` or source asset fails re-validation is FAILED by THE
* CLAIM ITSELF (never thrown out of `claim`), so an undecodable row can never sit
* in `transcoding` forever holding its lease.
*/
import type { Pool, PoolClient } from "pg";
import type { Lane, ProduceJobKind } from "./command-builder";
import { parseProduceRequest } from "./command-builder";
import type { ClaimedProduceJob, ProduceJobStore, ProduceOutput, ProduceSourceAsset } from "./store";
const KINDS: readonly ProduceJobKind[] = ["compose", "mix"];
/**
* Lane → which rows this worker may claim (039 lanes: default worker = local or NULL,
* i.e. anything but an explicit `lane:'remote'`; a remote worker takes only remote).
*/
export function lanePredicate(lane: Lane): { laneSql: string; params: [boolean] } {
// $1 binds the boolean flag: a local/default worker skips remote rows, a remote
// worker claims ONLY remote rows. The predicate never matches a local row to a
// remote worker, so nothing is locked and then released.
return {
laneSql: lane === "remote"
? "(j.input_json->>'lane' = 'remote')"
: "(j.input_json->>'lane' IS DISTINCT FROM 'remote')",
params: [lane === "remote"],
};
}
function uuid(value: unknown, field: string): string {
const text = typeof value === "string" ? value : undefined;
if (text === undefined || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(text)) {
throw new Error(`produce job store: row has an invalid ${field}`);
}
return text;
}
function nullableNumber(value: unknown): number | null {
if (value === null || value === undefined) return null;
const n = typeof value === "number" ? value : Number(value);
return Number.isFinite(n) ? n : null;
}
function sourceFromRow(row: Record<string, unknown>): ProduceSourceAsset {
return {
id: uuid(row.id, "video_asset id"),
storageKey: String(row.storage_key ?? ""),
mimeType: String(row.mime_type ?? ""),
sizeBytes: Number(row.size_bytes ?? 0),
width: nullableNumber(row.width),
height: nullableNumber(row.height),
durationMs: row.duration_ms === null || row.duration_ms === undefined ? null : Math.round(Number(row.duration_ms)),
frameRate: nullableNumber(row.frame_rate),
// Probed "no audio track" (009 stores an empty codec) → a silent bed, so the
// output always has an audio stream.
hasAudio: row.audio_codec === null || row.audio_codec === undefined ? null : String(row.audio_codec) !== "",
};
}
/** The store's own reads are RLS-scoped to the job's workspace (009 tenant_isolation). */
export class PostgresProduceJobStore implements ProduceJobStore {
constructor(private readonly pool: Pool, private readonly now: () => Date = () => new Date()) {}
private async tenant<T>(workspaceId: string, fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
await client.query("SELECT set_config('app.workspace_id', $1, true)", [workspaceId]);
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK").catch(() => undefined);
throw error;
} finally {
client.release();
}
}
async claim(lane: Lane): Promise<ClaimedProduceJob | null> {
// Lane filter in the sub-select, so a worker never locks a row it would have to
// release: a remote worker only ever locks remote rows, the default worker only
// ever locks non-remote rows.
const { laneSql, params } = lanePredicate(lane);
const claimed = await this.pool.query<{ id: string; workspace_id: string }>(
`UPDATE video_processing_jobs j
SET state = 'transcoding', progress_percent = 5, error = NULL, updated_at = now()
WHERE j.id IN (
SELECT id FROM video_processing_jobs
WHERE kind IN ('compose', 'mix')
AND state = 'queued'
AND ${laneSql}
ORDER BY created_at, id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING j.id, j.workspace_id`,
params,
);
if (claimed.rowCount === 0) return null;
const row = claimed.rows[0]!;
const workspaceId = uuid(row.workspace_id, "workspace id");
return this.tenant<ClaimedProduceJob | null>(workspaceId, async (client) => {
// Re-claim inside the tenant's RLS scope (the pool-level row above is outside
// it; claiming again is the cheap way to stay correct under FORCE RLS).
const mine = await client.query(
`UPDATE video_processing_jobs
SET state = 'transcoding', progress_percent = 5, error = NULL, updated_at = now()
WHERE id = $1 AND state IN ('queued', 'transcoding')
RETURNING id, kind, input_json, video_asset_id`,
[row.id],
);
if (mine.rowCount === 0) return null;
const job = mine.rows[0]! as Record<string, unknown>;
const kind = job.kind === "mix" ? "mix" : job.kind === "compose" ? "compose" : null;
let request = null;
if (kind !== null) {
try {
request = parseProduceRequest(job.input_json, kind);
} catch {
request = null;
}
}
if (kind === null || request === null) {
// A row the worker cannot honour must FAIL here — never strand in `transcoding`.
// Written inside this transaction and COMMITTED by returning null (not thrown:
// a throw would roll the failure back and re-strand the row).
await this.failInvalid(client, job, kind === null ? "produce job store: unknown kind" : "produce invalid request");
return null;
}
const assets = await client.query(
`SELECT id, storage_key, mime_type, size_bytes, width, height, duration_ms, frame_rate, audio_codec
FROM video_assets WHERE id = $1`,
[job.video_asset_id],
);
if (assets.rowCount === 0) {
await this.failInvalid(client, job, "produce asset unavailable");
return null;
}
return {
jobId: uuid(job.id, "job id"),
workspaceId,
kind,
request,
source: sourceFromRow(assets.rows[0] as Record<string, unknown>),
claimedAt: this.now().toISOString(),
};
});
}
/**
* Fail a just-claimed row whose payload cannot be honoured, inside the claim's
* own transaction so the failure commits with it. Callers then return `null`:
* the row is done (failed), there is simply nothing for this tick to encode.
*/
private async failInvalid(client: PoolClient, job: Record<string, unknown>, reason: string): Promise<void> {
await client.query(
`UPDATE video_processing_jobs
SET state = 'failed', progress_percent = 100, error = $2, updated_at = now()
WHERE id = $1 AND state = 'transcoding'`,
[job.id, reason],
);
}
async progress(jobId: string, percent: number): Promise<void> {
await this.tenant(await this.workspaceOf(jobId), async (client) => {
await client.query(
`UPDATE video_processing_jobs SET progress_percent = $2, updated_at = now()
WHERE id = $1 AND state = 'transcoding'`,
[jobId, percent],
);
});
}
async complete(jobId: string, output: ProduceOutput, derivedFrom: string): Promise<string> {
const workspaceId = await this.workspaceOf(jobId);
return this.tenant(workspaceId, async (client) => {
const inserted = await client.query<{ id: string }>(
`INSERT INTO video_assets
(workspace_id, storage_key, mime_type, size_bytes, width, height, duration_ms, frame_rate, codec, audio_codec, derived_from)
VALUES ($1, $2, 'video/mp4', $3, $4, $5, $6, $7, 'h264', 'aac', $8)
RETURNING id`,
[workspaceId, output.storageKey, output.sizeBytes, output.width, output.height, output.durationMs, output.frameRate, derivedFrom],
);
const producedAssetId = inserted.rows[0]!.id;
const updated = await client.query(
`UPDATE video_processing_jobs
SET state = 'complete', progress_percent = 100, output_key = $2,
produced_asset_id = $3, error = NULL, updated_at = now()
WHERE id = $1 AND state = 'transcoding'
RETURNING id`,
[jobId, output.storageKey, producedAssetId],
);
if (updated.rowCount === 0) throw new Error(`produce job store: job ${jobId} lost its claim`);
return producedAssetId;
});
}
async fail(jobId: string, reason: string): Promise<void> {
await this.tenant(await this.workspaceOf(jobId), async (client) => {
await client.query(
`UPDATE video_processing_jobs
SET state = 'failed', error = $2, updated_at = now()
WHERE id = $1 AND state IN ('queued', 'transcoding')`,
[jobId, reason],
);
});
}
async asset(workspaceId: string, assetId: string): Promise<{ storageKey: string; mimeType: string } | null> {
return this.tenant(workspaceId, async (client) => {
const rows = await client.query<{ storage_key: string; mime_type: string }>(
`SELECT storage_key, mime_type FROM video_assets WHERE id = $1`,
[assetId],
);
if (rows.rowCount === 0) return null;
return { storageKey: rows.rows[0]!.storage_key, mimeType: rows.rows[0]!.mime_type };
});
}
/** A claimed job's workspace, for the tenant-scoped follow-up writes. */
private async workspaceOf(jobId: string): Promise<string> {
const rows = await this.pool.query<{ workspace_id: string }>(
`SELECT workspace_id FROM video_processing_jobs WHERE id = $1`,
[jobId],
);
if (rows.rowCount === 0) throw new Error(`produce job store: job ${jobId} not found`);
return uuid(rows.rows[0]!.workspace_id, "workspace id");
}
}
export const PRODUCE_JOB_KINDS = KINDS;