import { isGateHeld } from "../lib/pauseGates"; import type { WorkerRuntimeState } from "../jobs/workerPool"; import type { WorkerDefaults } from "../jobs/workerDefaults"; import type { LiveInputs } from "./inputs"; // THE WORKERS PAYLOAD, AND WHERE ITS TYPES USED TO LIVE. // // These three types were declared inside the `"use client"` WorkersView.tsx and // the SERVER builder imported them back out of it — a payload shape owned by // the component that renders it, which meant every route and widget that reads // a worker list depended on a React file to say what a worker is. They are the // wire shape of /api/workers; they belong beside the function that builds them. // The component now imports them from here (and re-exports them until slice 1D // repoints its importers). export type WorkerTask = { id: string; label: string; fraction?: number; detail?: string; startedAt: number; channelSlug?: string; }; export type WorkerView = { id: string; name: string; kind: "local" | "remote" | "llm"; appId?: string; priority: number; busy: boolean; state: WorkerRuntimeState; degraded: boolean; enabled: boolean; // Capability tags (see lib/workers.ts workerMatches). Absent = takes anything. tags?: string[]; // True when the in-flight transcription can be stopped into a partial result. canStopPartial: boolean; tasks: WorkerTask[]; }; export type WorkersPayload = { paused: boolean; // Persisted global downloads pause (settings.json `autoQueue.download.held`; // this payload field keeps the older name, which pinned clients read). Separate // from `paused` (live transcription-worker pause). Surfaced on the same poll // so the dashboard/widget controls reflect both without a second request. downloadsPaused: boolean; workers: WorkerView[]; // Worker ids saved as the launch default ("Set as default"), or null when no // default has been saved yet. Drives the button label and per-worker marker. defaultEnabledIds: string[] | null; }; // What this view needs, and nothing else: the pool's read half, the registry's // list, the settings the download gate is stored in, and the persisted launch // default — the last one READ BY THE SHELL, because `readWorkerDefaults` is a // synchronous `fs` read and a view never touches the disk. export type WorkersInputs = Pick< LiveInputs, "registry" | "pool" | "settings" > & { workerDefaults: WorkerDefaults | null; }; // Builds the Workers screen payload: the pool's per-worker slot/state summary // joined with the registry's in-flight transcribe tasks (which carry workerId) // so each worker shows what it's currently running. Shared by the server- // rendered page (initial state), the /api/workers poll route, and the read-only // monitor widget. export function buildWorkersPayload(i: WorkersInputs): WorkersPayload { const summary = i.pool.summary(); const byWorker = new Map(); for (const job of i.registry.list()) { if (job.status !== "running" || !job.tasks) continue; for (const t of job.tasks) { if (t.kind !== "transcribe" || !t.workerId) continue; const list = byWorker.get(t.workerId) ?? []; list.push({ id: t.id, label: t.label, fraction: t.fraction, detail: t.detail, startedAt: t.startedAt, channelSlug: job.channelSlug, }); byWorker.set(t.workerId, list); } } const workers = summary.map((w) => ({ ...w, tasks: byWorker.get(w.id) ?? [], // True when the in-flight transcription can be stopped into a partial result // (parakeet). Drives the "Stop & keep partial" button on the Workers page. canStopPartial: i.pool.canStopPartial(w.id), })); // The saved launch default (Set as default), or null if none has been saved. // Drives the button label ("Set as default" vs "Update default") and the // per-worker "default" marker. const defaultEnabledIds = i.workerDefaults?.enabledWorkerIds ?? null; return { paused: i.pool.isPaused(), // Persisted global downloads pause (settings.json). Surfaced here so the // dashboard/widget controls can read both pause states from one poll. downloadsPaused: isGateHeld(i.settings, "download"), workers, defaultEnabledIds, }; }