Files
OpenMontage/backlot/ui/library.js
calesthio 4b4d7b1e99 backlot phase 2: board UI (library, live board, filmstrip, script modal, drawers)
- vanilla ESM frontend on the mockup design system (no build step):
  library contact-sheet with mini rails + live badges; project board with
  slate header, cost meter, clickable stage rail, stage drawers (artifact
  view, review findings, gate-skipped chip, versions), screenplay card +
  full-script modal, decisions + activity rails, storyboard filmstrip
  (duration-width cards, shimmer/spec/missing/text-card states, takes,
  narration + waveform playback), render player with versions, degraded
  found-media view
- /thumb endpoint: cached downscaled JPEGs (Pillow) + ffmpeg poster-frame
  extraction for videos — the library was loading 73 full-res PNGs
- library summaries cached, invalidated by the watcher
- asset path resolution tolerates project-relative, repo-relative and
  absolute manifest paths (real-world variance found in why-do-we-dream)
- ?static=1 disables SSE (screenshots/static export)
- verified in browser against signal-from-tomorrow, why-do-we-dream,
  done-beats-perfect and the 73-project library
2026-07-01 23:47:45 -07:00

64 lines
2.4 KiB
JavaScript

import { el, fmtAgo, getJSON, subscribe, thumbURL } from "/ui/lib.js";
const grid = document.getElementById("grid");
function miniRail(states) {
const rail = el("div", { class: "mini-rail" });
for (const s of states) {
const cls = s.status === "completed" ? "d"
: s.status === "in_progress" ? "a"
: s.status === "awaiting_human" ? "w" : "";
rail.append(el("i", { class: cls, title: `${s.name}: ${s.status}` }));
}
return rail;
}
function card(p) {
const poster = el("div", { class: "lib-poster" });
if (p.poster) {
poster.append(el("img", { src: thumbURL(p.project_id, p.poster, 640), loading: "lazy", alt: "" }));
} else {
poster.append(el("span", { class: "lp-txt" }, "NO MEDIA YET"));
}
if (p.live && p.active_stage) {
poster.append(el("span", { class: "lp-live" },
el("span", { class: "dot" }),
p.awaiting_human ? "◈ AWAITING YOU" : `LIVE · ${p.active_stage.toUpperCase()}`));
} else if (p.awaiting_human) {
poster.append(el("span", { class: "lp-live" }, "◈ AWAITING YOU"));
}
const meta = el("div", { class: "lb-meta" },
el("span", { class: "chip" }, p.pipeline_type || "unknown"),
p.scene_count ? el("span", { class: "chip" }, `${p.scene_count} scenes`) : null,
p.render_count ? el("span", { class: "chip" }, `${p.render_count} renders`) : null,
el("span", { class: "when" }, fmtAgo(p.last_activity)),
);
return el("a", { class: `lib-card${p.live ? " live-card" : ""}`, href: `/p/${p.project_id}`, style: "text-decoration:none;color:inherit" },
poster,
el("div", { class: "lib-body" },
el("h3", {}, (p.title || p.project_id).toUpperCase()),
meta,
p.stage_states.length ? miniRail(p.stage_states) : null,
),
);
}
async function render() {
const projects = await getJSON("/api/projects");
document.getElementById("count").textContent = `${projects.length} projects`;
const liveCount = projects.filter((p) => p.live).length;
const badge = document.getElementById("liveBadge");
badge.classList.toggle("idle", liveCount === 0);
document.getElementById("liveText").textContent = liveCount ? `${liveCount} LIVE` : "IDLE";
grid.innerHTML = "";
document.getElementById("empty").style.display = projects.length ? "none" : "block";
for (const p of projects) grid.append(card(p));
}
render().catch(console.error);
if (!new URLSearchParams(location.search).has("static")) {
subscribe("/api/library/events", () => render().catch(console.error));
}