{% extends "components/_cc_shell.html" %}
{# ── Run canvas (Pass 4 · Task 5) — the STATIC visual layer ───────────────────
This template renders the DOM + Alpine bindings + the window.__RUN bootstrap.
The Alpine controller `runCanvas()` lives in a SEPARATE file (run-page.js, a
later task). This template DEFINES the template↔JS contract that run-page.js
implements against. The controller MUST expose exactly:
x-data="runCanvas()" reads window.__RUN = {runId, kind, scope,
workflowName, nodes, replay,
terminal, events, result, startedAt, finishedAt}
— the last five drive TERMINAL mode (spec §5.2): a
past run re-rendered from its persisted _run.json.
terminal:true → NO socket; replay `events` through
the live handlers; orb from `result`; elapsed =
finishedAt − startedAt. Live runs: terminal:false +
events:[] (the SSE stream is the source).
DATA (reactive):
orbState string — "idle" | "running" | "complete" | "error"
(drives the brand orb recolor via :data-state)
runState string — current run-state WORD (e.g. "running",
"complete", "cancelled") shown in the run chip
elapsed string — elapsed time "mm:ss"
stepDone number — sections/steps reached a terminal state
stepTotal number — total sections/steps
unitLabel string — "sections" (compound) | "steps"/"recipes" (else)
pct string — progress percentage label, e.g. "38%"
canStop bool — Stop button enabled (run is in flight)
canReplay bool — Replay button enabled (run is terminal + replayable)
METHODS:
nodeClass(id) -> string|object — Alpine :class for node #n-
(the state class: s-queued/s-running/s-done/
s-error/s-skipped, keyed off an internal
runStates[id] map mirroring builder-page.js)
nodeBadge(id) -> string — the per-node badge text (x-text)
nodeMsg(id) -> string — the live running message (x-text)
onNodeClick(id) — click handler; the JS decides
clickability by node kind + run kind (no-op for
source / compound children; opens the report for a
done workflow step or the deliverable)
stop() — POST cancel the run
replay() — re-launch via window.__RUN.replay
init() + internal SSE / layout — connect /api/run//events,
place nodes, draw edges into #e-dorm/#e-done/#e-live
The node id hooks (id="n-" + data-node-id) are what the JS lights
by SSE step_id — the node-id ↔ step-id invariant (spec §4.2). Keep exact. #}
{% block title %}fs-report · Run{% endblock %}
{% block head %}
{% endblock %}
{% block content %}
{% if expired %}
{# ── Expired-run panel (spec §7) — no canvas, no x-data ─────────────── #}
This run has expired
The live run {{ run_id }} is no longer in
memory. Browse the generated reports or read the captured log.
{% else %}
{# ── Lane-color macro: category → the lane CSS var (spec §4) ─────────── #}
{% macro lane_var(category) -%}
{%- if category == "Executive" -%}var(--nav-executive)
{%- elif category == "Investigation" -%}var(--nav-investigation)
{%- elif category == "Remediation" -%}var(--nav-remediation)
{%- elif category == "Compliance" -%}var(--nav-compliance)
{%- else -%}var(--ink-faint){%- endif -%}
{%- endmacro %}
{# Node-count helpers (server-rendered honest context). The source node is
always index 0; everything after it is a section/step/recipe. The
deliverable (compound only) is a terminal node, not a "section". #}
{% set work_nodes = canvas_nodes[1:] %}
{% set section_count = work_nodes | rejectattr("kind", "equalto", "deliverable") | list | length %}
{# Brand orb — the run page renders its own (the shell topbar has none).
Recolors via :data-state set by the controller (idle/running/complete/
error). SVG ported from the mockup (aura / ring / core). #}
{# Kind eyebrow — STATIC, server-rendered from `kind` (NOT a toggle). #}
{# Transport — Stop + Replay (no Pause) + a persistent Run log link.
The Run log is ALWAYS reachable from the canvas (fix ④): canvas error
rows now reopen the canvas instead of jumping straight to the raw log,
so this link keeps the log one click away. #}
{# All runs ↗ — the Run canvas is one entry in the persistent /runs index
(spec §7); this keeps the full list one click away. #}
{# Stats — N/M · pct readout. #}
/
·
{# Context line — honest, server-rendered from `kind` + node counts. #}
{% if kind == "compound" %}
Compound report — {{ section_count }} section{{ "" if section_count == 1 else "s" }} fan into one combined deliverable
{% elif kind == "workflow" %}
Workflow — {{ section_count }} report-run{{ "" if section_count == 1 else "s" }} walked in sequence
{% else %}
{{ section_count }} report{{ "" if section_count == 1 else "s" }}
{% endif %}
{# Edges layer — the JS task draws bezier paths into the three groups.
Groups are left EMPTY here; the controller populates them on layout
+ per SSE state. The gradients/markers are ported verbatim. #}
{# Nodes layer — server-rendered from canvas_nodes so every node exists
before the SSE stream arrives. The JS lights each by id. #}
{% for node in canvas_nodes %}
{# State class comes solely from :class="nodeClass(...)" (Alpine adds it
to the static class, so a static s-queued would never be cleared and
would dim a later s-done node — keep the static class state-free). #}