diff --git a/server/api/review.ts b/server/api/review.ts index c015895..81dccff 100644 --- a/server/api/review.ts +++ b/server/api/review.ts @@ -182,7 +182,7 @@ app.post('/series/:seriesKey/approve-all', (c) => { for (const plan of pending) { db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id); const { item, streams, decisions } = loadItemDetail(db, plan.item_id); - if (item) db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); + if (item) db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'audio', 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); } return c.json({ ok: true, count: pending.length }); }); @@ -201,7 +201,7 @@ app.post('/season/:seriesKey/:season/approve-all', (c) => { for (const plan of pending) { db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id); const { item, streams, decisions } = loadItemDetail(db, plan.item_id); - if (item) db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); + if (item) db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'audio', 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); } return c.json({ ok: true, count: pending.length }); }); @@ -216,7 +216,7 @@ app.post('/approve-all', (c) => { for (const plan of pending) { db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id); const { item, streams, decisions } = loadItemDetail(db, plan.item_id); - if (item) db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); + if (item) db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'audio', 'pending')").run(plan.item_id, buildCommand(item, streams, decisions)); } return c.json({ ok: true, count: pending.length }); }); @@ -304,7 +304,7 @@ app.post('/:id/approve', (c) => { db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id); if (!plan.is_noop) { const { item, streams, decisions } = loadItemDetail(db, id); - if (item) db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(id, buildCommand(item, streams, decisions)); + if (item) db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'audio', 'pending')").run(id, buildCommand(item, streams, decisions)); } return c.json({ ok: true }); }); diff --git a/server/api/subtitles.ts b/server/api/subtitles.ts index d5dce95..d3be1a8 100644 --- a/server/api/subtitles.ts +++ b/server/api/subtitles.ts @@ -305,7 +305,7 @@ app.post('/extract-all', (c) => { const streams = db.prepare('SELECT * FROM media_streams WHERE item_id = ? ORDER BY stream_index').all(item.id) as MediaStream[]; const command = buildExtractOnlyCommand(item, streams); if (!command) continue; - db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(item.id, command); + db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'subtitle', 'pending')").run(item.id, command); queued++; } @@ -328,7 +328,7 @@ app.post('/:id/extract', (c) => { const command = buildExtractOnlyCommand(item, streams); if (!command) return c.json({ ok: false, error: 'No subtitles to extract' }, 400); - db.prepare("INSERT INTO jobs (item_id, command, status) VALUES (?, ?, 'pending')").run(id, command); + db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, 'subtitle', 'pending')").run(id, command); return c.json({ ok: true }); }); diff --git a/server/db/index.ts b/server/db/index.ts index 24152fd..115d6f2 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -56,6 +56,7 @@ export function getDb(): Database { try { _db.exec('ALTER TABLE review_plans ADD COLUMN subs_extracted INTEGER NOT NULL DEFAULT 0'); } catch { /* already exists */ } try { _db.exec("ALTER TABLE nodes ADD COLUMN movies_path TEXT NOT NULL DEFAULT ''"); } catch { /* already exists */ } try { _db.exec("ALTER TABLE nodes ADD COLUMN series_path TEXT NOT NULL DEFAULT ''"); } catch { /* already exists */ } + try { _db.exec("ALTER TABLE jobs ADD COLUMN job_type TEXT NOT NULL DEFAULT 'audio'"); } catch { /* already exists */ } seedDefaults(_db); return _db; } diff --git a/server/db/schema.ts b/server/db/schema.ts index 23e26ea..24c226a 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -103,6 +103,7 @@ CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, command TEXT NOT NULL, + job_type TEXT NOT NULL DEFAULT 'audio', node_id INTEGER REFERENCES nodes(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'pending', output TEXT, diff --git a/server/types.ts b/server/types.ts index 3680057..d17176e 100644 --- a/server/types.ts +++ b/server/types.ts @@ -79,6 +79,7 @@ export interface Job { id: number; item_id: number; command: string; + job_type: 'audio' | 'subtitle'; node_id: number | null; status: 'pending' | 'running' | 'done' | 'error'; output: string | null; diff --git a/src/features/execute/ExecutePage.tsx b/src/features/execute/ExecutePage.tsx index bfa1d59..51a8b91 100644 --- a/src/features/execute/ExecutePage.tsx +++ b/src/features/execute/ExecutePage.tsx @@ -3,12 +3,11 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router'; import { api } from '~/shared/lib/api'; import { Badge } from '~/shared/components/ui/badge'; import { Button } from '~/shared/components/ui/button'; -import { Select } from '~/shared/components/ui/select'; import { FilterTabs } from '~/shared/components/ui/filter-tabs'; -import type { Job, Node, MediaItem } from '~/shared/lib/types'; +import type { Job, MediaItem } from '~/shared/lib/types'; -interface JobEntry { job: Job; item: MediaItem | null; node: Node | null; } -interface ExecuteData { jobs: JobEntry[]; nodes: Node[]; filter: string; totalCounts: Record; } +interface JobEntry { job: Job; item: MediaItem | null; } +interface ExecuteData { jobs: JobEntry[]; filter: string; totalCounts: Record; } const FILTER_TABS = [ { key: 'all', label: 'All' }, @@ -26,6 +25,10 @@ function itemName(job: Job, item: MediaItem | null): string { return item.name; } +function jobTypeLabel(job: Job): string { + return job.job_type === 'subtitle' ? 'ST Extract' : 'Audio Mod'; +} + // Module-level cache for instant tab switching const cache = new Map(); @@ -36,6 +39,7 @@ export function ExecutePage() { const [loading, setLoading] = useState(!cache.has(filter)); const [logs, setLogs] = useState>(new Map()); const [logVisible, setLogVisible] = useState>(new Set()); + const [cmdVisible, setCmdVisible] = useState>(new Set()); const esRef = useRef(null); const reloadTimerRef = useRef | null>(null); @@ -108,20 +112,15 @@ export function ExecutePage() { const clearCompleted = async () => { await api.post('/api/execute/clear-completed'); cache.clear(); load(); }; const runJob = async (id: number) => { await api.post(`/api/execute/job/${id}/run`); cache.clear(); load(); }; const cancelJob = async (id: number) => { await api.post(`/api/execute/job/${id}/cancel`); cache.clear(); load(); }; - const assignNode = async (jobId: number, nodeId: number | null) => { - await api.post(`/api/execute/job/${jobId}/assign`, { node_id: nodeId }); - cache.clear(); - load(); - }; const toggleLog = (id: number) => setLogVisible((prev) => { const s = new Set(prev); s.has(id) ? s.delete(id) : s.add(id); return s; }); + const toggleCmd = (id: number) => setCmdVisible((prev) => { const s = new Set(prev); s.has(id) ? s.delete(id) : s.add(id); return s; }); const totalCounts = data?.totalCounts ?? { all: 0, pending: 0, running: 0, done: 0, error: 0 }; const pending = totalCounts.pending ?? 0; const done = totalCounts.done ?? 0; const errors = totalCounts.error ?? 0; const jobs = data?.jobs ?? []; - const nodes = data?.nodes ?? []; return (
@@ -153,51 +152,31 @@ export function ExecutePage() {
- {['#', 'Item', 'Command', 'Node', 'Status', 'Actions'].map((h) => ( + {['#', 'Item', 'Type', 'Status', 'Actions'].map((h) => ( ))} - {jobs.map(({ job, item, node }) => { + {jobs.map(({ job, item }: JobEntry) => { const name = itemName(job, item); - const cmdShort = job.command.length > 80 ? job.command.slice(0, 77) + '…' : job.command; const jobLog = logs.get(job.id) ?? job.output ?? ''; const showLog = logVisible.has(job.id) || job.status === 'running' || job.status === 'error'; + const showCmd = cmdVisible.has(job.id); return ( <> - - - {jobLog && ( + {showCmd && ( + + + + )} + {jobLog && showLog && ( - )} diff --git a/src/shared/lib/types.ts b/src/shared/lib/types.ts index 0b7714a..2f536f4 100644 --- a/src/shared/lib/types.ts +++ b/src/shared/lib/types.ts @@ -78,6 +78,7 @@ export interface Job { item_id: number; node_id: number | null; command: string; + job_type: 'audio' | 'subtitle'; status: 'pending' | 'running' | 'done' | 'error'; output: string | null; exit_code: number | null;
{h}
{job.id} -
{name}
- {item && ( -
- ← Details -
- )} - {item?.file_path && ( -
- {item.file_path.split('/').pop()} -
- )} +
+ {item ? ( + {name} + ) : name} +
- {cmdShort} - - {job.status === 'pending' ? ( - - ) : ( - {node?.name ?? 'Local'} - )} + + {jobTypeLabel(job)} {job.status} @@ -211,20 +190,28 @@ export function ExecutePage() { )} + {(job.status === 'done' || job.status === 'error') && jobLog && ( )}
+
+ {job.command} +
+
- {showLog && ( -
- {jobLog} -
- )} +
+
+ {jobLog} +