backlot: fix all five dogfood findings (F-01..F-05)

- F-01: cost bar crit (red) state past 90% of budget
- F-02: normalize() hardens fetched board state against sparse payloads
- F-03: /thumb 404s for videos with no extractable poster frame instead
  of serving raw video bytes
- F-04: checkpoint artifact path refs only resolve inside the project dir
- F-05 (board half): stall detection — in_progress stage with no disk
  activity >10min renders red 'stalled?' + header badge flips to STALLED?;
  verified against the real wedged why-cities-glow project
- eval harness from dogfood session committed (visual regression +
  interaction smoke, capture watcher, server/gate test suites) +
  regression tests for each finding; 46 backlot tests green, visual eval
  green (restage-before-capture note logged)
This commit is contained in:
calesthio
2026-07-02 08:22:03 -07:00
parent 811480d39b
commit 1d60f0da14
11 changed files with 950 additions and 5 deletions

View File

@@ -32,9 +32,13 @@ function renderSlate(s) {
const awaiting = s.stages.find((x) => x.status === "awaiting_human");
const inProgress = s.stages.find((x) => x.status === "in_progress");
const stalled = s.stages.find((x) => x.stalled);
let liveEl;
if (awaiting) {
liveEl = el("span", { class: "live" }, el("span", { class: "dot" }), "◈ AWAITING YOU");
} else if (stalled) {
liveEl = el("span", { class: "live", style: "color:var(--red)" },
el("span", { class: "dot", style: "background:var(--red);animation:none" }), "⚠ STALLED?");
} else if (s.live || inProgress) {
liveEl = el("span", { class: "live" }, el("span", { class: "dot" }), "LIVE");
} else {
@@ -52,7 +56,7 @@ function renderSlate(s) {
hasBudget ? el("span", {}, ` / ${fmtMoney(budget)}`) : ""));
if (hasBudget) {
cost.append(el("div", { class: "bar" }, el("i", {
class: pct > 75 ? "warn" : "", style: `width:${pct}%`,
class: pct > 90 ? "crit" : pct > 75 ? "warn" : "", style: `width:${pct}%`,
})));
}
cost.append(el("div", { class: "label" }, "generation spend"));
@@ -77,6 +81,9 @@ function renderSlate(s) {
function stageSub(st) {
if (st.status === "awaiting_human") return "awaiting your approval\nreply in chat to continue";
if (st.status === "in_progress" && st.stalled) {
return `stalled? no activity for ${st.stalled_minutes}m\nask the agent for status`;
}
if (st.status === "in_progress" && st.partial_progress) {
const done = st.partial_progress.completed_scene_ids;
if (Array.isArray(done)) return `${done.length} scene${done.length === 1 ? "" : "s"} done`;
@@ -96,7 +103,7 @@ function renderRail(s) {
let pendingIndex = 1;
for (const st of s.stages) {
const cls = st.status === "completed" ? "done"
: st.status === "in_progress" ? "active"
: st.status === "in_progress" ? (st.stalled ? "active stalled" : "active")
: st.status === "awaiting_human" ? "await"
: st.status === "failed" ? "failed" : "";
const icon = STAGE_ICONS[st.status] || String(pendingIndex);
@@ -717,8 +724,31 @@ function render() {
if (renders) app.append(renders);
}
// Defensive normalization (F-02): the server contract guarantees these
// fields, but a sparse/legacy payload must degrade, never crash the board.
function normalize(s) {
s.pipeline = s.pipeline || { pipeline_type: "unknown", stages: [], known: false };
s.stages = Array.isArray(s.stages) ? s.stages : [];
s.artifacts = s.artifacts || {};
s.media = s.media || {};
s.media.renders = Array.isArray(s.media.renders) ? s.media.renders : [];
s.media.snapshots = Array.isArray(s.media.snapshots) ? s.media.snapshots : [];
s.media.music = Array.isArray(s.media.music) ? s.media.music : [];
s.events = Array.isArray(s.events) ? s.events : [];
if (s.storyboard && Array.isArray(s.storyboard.scenes)) {
for (const c of s.storyboard.scenes) {
c.takes = Array.isArray(c.takes) ? c.takes : [];
c.audio = Array.isArray(c.audio) ? c.audio : [];
c.required_assets = Array.isArray(c.required_assets) ? c.required_assets : [];
}
} else {
s.storyboard = null;
}
return s;
}
async function refresh() {
state = await getJSON(`/api/project/${encodeURIComponent(projectId)}/state`);
state = normalize(await getJSON(`/api/project/${encodeURIComponent(projectId)}/state`));
render();
}