chunker and ui

This commit is contained in:
2026-03-13 14:29:38 -03:00
parent 3eeedebb15
commit ccc478fbaa
69 changed files with 6481 additions and 282 deletions

245
ui/chunker/src/App.tsx Normal file
View File

@@ -0,0 +1,245 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import "./App.css";
import { createChunkJob, getAssets, scanMediaFolder } from "./api";
import { ChunkGrid } from "./components/ChunkGrid";
import { ConfigPanel } from "./components/ConfigPanel";
import { ErrorLog } from "./components/ErrorLog";
import { PipelineDiagram } from "./components/PipelineDiagram";
import { QueueGauge } from "./components/QueueGauge";
import { StatsPanel } from "./components/StatsPanel";
import { WorkerPanel } from "./components/WorkerPanel";
import { useEventStream } from "./hooks/useEventStream";
import type {
ChunkInfo,
ErrorEntry,
MediaAsset,
PipelineConfig,
PipelineStats,
WorkerInfo,
} from "./types";
export default function App() {
const [jobId, setJobId] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
// Asset state
const [assets, setAssets] = useState<MediaAsset[]>([]);
const [selectedAsset, setSelectedAsset] = useState<MediaAsset | null>(null);
const [scanning, setScanning] = useState(false);
const { events, connected, done } = useEventStream(jobId);
// Load assets on mount
useEffect(() => {
getAssets()
.then((data) => setAssets(data.sort((a, b) => a.filename.localeCompare(b.filename))))
.catch((e) => setError(e instanceof Error ? e.message : "Failed to load assets"));
}, []);
const handleScan = useCallback(async () => {
setScanning(true);
setError(null);
try {
await scanMediaFolder();
const data = await getAssets();
setAssets(data.sort((a, b) => a.filename.localeCompare(b.filename)));
} catch (e) {
setError(e instanceof Error ? e.message : "Scan failed");
} finally {
setScanning(false);
}
}, []);
// Derive state from events
const { chunks, workers, stats, errors, activeStage, queueSize } =
useMemo(() => {
const chunkMap = new Map<number, ChunkInfo>();
const workerMap = new Map<string, WorkerInfo>();
const errorList: ErrorEntry[] = [];
let totalChunks = 0;
let processed = 0;
let failed = 0;
let retries = 0;
let elapsed = 0;
let throughput = 0;
let queueSize = 0;
let stage = "pending";
for (const evt of events) {
if (evt.total_chunks) totalChunks = evt.total_chunks;
if (evt.processed_chunks) processed = evt.processed_chunks;
if (evt.failed_chunks) failed = evt.failed_chunks;
if (evt.elapsed) elapsed = evt.elapsed;
if (evt.throughput_mbps) throughput = evt.throughput_mbps;
if (evt.queue_size !== undefined) queueSize = evt.queue_size;
if (evt.status && evt.status !== "waiting") stage = evt.status;
// Track chunks
if (evt.sequence !== undefined) {
const existing = chunkMap.get(evt.sequence) || {
sequence: evt.sequence,
state: "pending" as const,
};
if (evt.status === "chunking" || evt.status === "pending") {
existing.state = "queued";
} else if (evt.status === "processing") {
existing.state = "processing";
if (evt.worker_id) existing.worker_id = evt.worker_id;
} else if (evt.status === "completed") {
existing.state = "done";
if (evt.processing_time)
existing.processing_time = evt.processing_time;
if (evt.retries) existing.retries = evt.retries;
} else if (evt.status === "failed") {
existing.state = "error";
if (evt.error) existing.error = evt.error;
}
if (evt.size) existing.size = evt.size;
chunkMap.set(evt.sequence, existing);
}
// Track workers
if (evt.worker_id) {
const w = workerMap.get(evt.worker_id) || {
worker_id: evt.worker_id,
state: "idle" as const,
processed: 0,
errors: 0,
retries: 0,
};
if (evt.state === "processing") {
w.state = "processing";
w.current_chunk = evt.sequence;
} else if (evt.state === "idle") {
w.state = "idle";
w.current_chunk = undefined;
} else if (evt.state === "stopped") {
w.state = "stopped";
}
if (evt.success !== undefined) {
if (evt.success) w.processed++;
else w.errors++;
}
if (evt.retries) {
retries += evt.retries;
w.retries += evt.retries;
}
workerMap.set(evt.worker_id, w);
}
// Track errors
if (evt.error) {
errorList.push({
timestamp: Date.now(),
sequence: evt.sequence,
worker_id: evt.worker_id,
error: evt.error,
retries: evt.retries,
event_type: evt.status || "error",
});
}
}
const statsObj: PipelineStats = {
total_chunks: totalChunks,
processed,
failed,
retries,
elapsed,
throughput_mbps: throughput,
queue_size: queueSize,
};
return {
chunks: Array.from(chunkMap.values()).sort(
(a, b) => a.sequence - b.sequence
),
workers: Array.from(workerMap.values()),
stats: statsObj,
errors: errorList,
activeStage: stage,
queueSize,
};
}, [events]);
const handleStart = useCallback(async (config: PipelineConfig) => {
setError(null);
setRunning(true);
try {
const result = await createChunkJob(config);
setJobId(result.id);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to start");
setRunning(false);
}
}, []);
// Reset running state when done
if (done && running) {
setRunning(false);
}
return (
<div className="app">
<header className="header">
<h1>MPR Chunker Pipeline</h1>
<div className="connection-status">
{jobId && (
<span className={`dot ${connected ? "connected" : ""}`} />
)}
<span className="status-text">
{!jobId
? "Configure and launch"
: connected
? "Streaming"
: done
? "Complete"
: "Connecting..."}
</span>
</div>
</header>
{error && <div className="error-banner">{error}</div>}
<div className="layout">
<aside className="sidebar">
<ConfigPanel
onStart={handleStart}
running={running}
assets={assets}
selectedAsset={selectedAsset}
onSelectAsset={setSelectedAsset}
onScan={handleScan}
scanning={scanning}
/>
</aside>
<main className="main">
<PipelineDiagram activeStage={activeStage} />
<div className="main-grid">
<div className="main-left">
<ChunkGrid chunks={chunks} totalChunks={stats.total_chunks} />
<QueueGauge
current={queueSize}
max={10}
buffered={0}
/>
</div>
<div className="main-right">
<WorkerPanel workers={workers} />
<StatsPanel stats={stats} />
<ErrorLog errors={errors} />
</div>
</div>
</main>
</div>
</div>
);
}