{% 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). #}
{% if kind == "compound" %} COMPOUND REPORT {% if workflow_name %}{{ workflow_name }}{% endif %} {% elif kind == "workflow" %} WORKFLOW · SEQUENTIAL {% if workflow_name %}{{ workflow_name }}{% endif %} {% else %} REPORT {% if workflow_name %}{{ workflow_name }}{% endif %} {% endif %}
{# Run-state chip — state word + elapsed mm:ss. #} {# Legend — Running / Complete / Queued / Cached. #}
Running Complete Queued Cached
{# 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). #}
{%- if node.kind == "source" -%}Source {%- elif node.kind == "mcp_tool" -%}Tool {%- elif node.kind == "deliverable" -%}Deliverable {%- else -%}Recipe{%- endif -%}
{{ node.title }}
{% if node.kind == "source" %} {# Source node shows the REAL scope string verbatim — never a fabricated portfolio count. #}
{{ scope }}
shared scope
{% else %} {# Done-state check (lit by the s-done class). #}
{# Format chips — one per format; "AI" → .chip.ai. #} {% if node.formats %}
{% for fmt in node.formats %} {{ fmt }} {% endfor %}
{% endif %} {# Live block — running message + progress fill. #}
{% endif %}
{% endfor %}
{% endif %} {% endblock %} {% block scripts %} {% endblock %}