diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx
new file mode 100644
index 0000000..3eaf391
--- /dev/null
+++ b/services/web-ui/public/screens-ingest.jsx
@@ -0,0 +1,396 @@
+// screens-ingest.jsx — Upload, Recorders, Capture (SDI), Monitors
+
+const { RECORDERS, NODES, SDI_PORTS_zampp2, PROJECTS } = window.ZAMPP_DATA;
+
+function Upload({ navigate }) {
+ const [files, setFiles] = React.useState([
+ { id: 1, name: "Drone_Aerial_Lap_4.mov", size: "12.4 GB", progress: 68, status: "uploading" },
+ { id: 2, name: "Interview_Director.mxf", size: "2.1 GB", progress: 100, status: "done" },
+ { id: 3, name: "Sponsor_Logo_v4.mov", size: "120 MB", progress: 100, status: "done" },
+ { id: 4, name: "Pit_Cam_3.mp4", size: "4.8 GB", progress: 24, status: "uploading" },
+ { id: 5, name: "Backstage_Audio.wav", size: "920 MB", progress: 0, status: "queued" },
+ ]);
+
+ React.useEffect(() => {
+ const i = setInterval(() => {
+ setFiles(fs => fs.map(f => {
+ if (f.status !== "uploading") return f;
+ const next = f.progress + Math.random() * 4;
+ if (next >= 100) return { ...f, progress: 100, status: "done" };
+ return { ...f, progress: next };
+ }));
+ }, 600);
+ return () => clearInterval(i);
+ }, []);
+
+ return (
+
+
+
Upload
+ Drop video, audio, or stills — we proxy and index automatically.
+
+
+
+
+
+
Protour 2026
+
+
+
+
+
+
+
Drop files here or click to browse
+
Video, audio, and image files — up to 5 GB each
+
+ {["MOV", "MP4", "MXF", "ProRes", "DNxHR", "WAV", "AIFF"].map(f => {f})}
+
+
+
+
+
+ Queue
+ {files.length}
+
+
+
+
+
+ {files.map(f => (
+
+
+
+
+ {f.name}
+ {f.size}
+
+
+
+
+ {f.status === "done" ? "✓ done" : f.status === "queued" ? "queued" : `${Math.round(f.progress)}%`}
+
+
+ ))}
+
+
+
+
+ );
+}
+
+function Recorders({ navigate, onNew }) {
+ return (
+
+
+
Recorders
+
Live ingest from SRT, RTMP, and SDI sources
+
+
+
+ 4 recording · 1 armed · 1 error
+
+
+
+
+
+ {RECORDERS.map(r => )}
+
+
+
+ );
+}
+
+function RecorderRow({ recorder }) {
+ const isRec = recorder.status === "recording";
+ return (
+
+
+ {recorder.audio ? (
+
+ ) : isRec ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {recorder.name}
+
+ {recorder.status.toUpperCase()}
+
+ {recorder.source}
+
+
{recorder.url}
+
+ {recorder.codec}·
+ {recorder.res}·
+ node: {recorder.node}
+
+
+
+
+
Elapsed
+
{recorder.elapsed}
+
+
+
Bitrate
+
{recorder.bitrate}
+
+
+
+
+ {isRec ? (
+
+ ) : recorder.status === "error" ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+function HealthBar({ value }) {
+ const color = value > 80 ? "var(--success)" : value > 40 ? "var(--warning)" : "var(--danger)";
+ return (
+
+ );
+}
+
+function badgeForStatus(s) {
+ return { recording: "live", armed: "accent", idle: "neutral", error: "danger", offline: "neutral" }[s] || "neutral";
+}
+
+function Capture({ navigate }) {
+ const [activePort, setActivePort] = React.useState(1);
+ const ports = SDI_PORTS_zampp2;
+
+ return (
+
+
+
Capture
+
DeckLink SDI ingest — multi-port routing across cluster nodes
+
+
+
+
+
+
+ );
+}
+
+function DeckLinkVisual({ ports, activePort, onSelect }) {
+ return (
+
+
+
+
DeckLink Duo 2
+
zampp2 · 172.18.91.217
+
+
ONLINE
+
+
+
+
+ {ports.map(p => (
+
+ ))}
+
+
+
+ Ports: 4
+ Active: {ports.filter(p => p.active).length}
+ Recording: {ports.filter(p => p.recording).length}
+
+
+ );
+}
+
+function CaptureDetail({ port }) {
+ if (!port.active) {
+ return (
+
+
+
No signal on SDI {port.idx}
+
Connect a source, then click Refresh.
+
+ );
+ }
+ return (
+
+
+
+
+ {port.recording && (
+
+ REC
+
+ )}
+
+
+ {port.signal}
+
+
+
+
+
+
+
+
+
+
+
+ {!port.recording ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+
+function CaptureStat({ label, value }) {
+ return (
+
+ );
+}
+
+function Monitors({ navigate }) {
+ const [grid, setGrid] = React.useState(4);
+ const allFeeds = [
+ ...RECORDERS.filter(r => !r.audio).map(r => ({ ...r, kind: "video" })),
+ { id: "audio1", name: "FOH Mix", kind: "audio" },
+ { id: "audio2", name: "Stage Mics", kind: "audio" },
+ { id: "audio3", name: "PGM Bus", kind: "audio" },
+ ];
+ const feeds = allFeeds.slice(0, grid * grid - (grid === 2 ? 1 : 0));
+ const gridSize = grid;
+
+ return (
+
+
+
Monitors
+
Multi-cam live monitoring across all active feeds
+
+
+ {[2, 3, 4].map(n => (
+
+ ))}
+
+
+
+
+
+ {feeds.map((f, i) => (
+
+ ))}
+
+
+
+ );
+}
+
+function MonitorTile({ feed, seed }) {
+ if (feed.kind === "audio") {
+ return (
+
+
+
+
+
+
+ LIVE
+ {feed.name}
+
+
+ );
+ }
+ return (
+
+
+
+
+ {feed.status === "recording" && REC}
+ {feed.status === "armed" && ARMED}
+ {feed.status === "error" && ERR}
+ {feed.status === "idle" && IDLE}
+
+
+ {feed.name}
+ {feed.elapsed && feed.elapsed !== "00:00:00" && {feed.elapsed}}
+
+
+ );
+}
+
+Object.assign(window, { Upload, Recorders, Capture, Monitors });