{# P1-T06/T07/T08/T09/T16 — unified Step Trace as the new debug surface. Renders the per-scan Step Trace as the primary view at ``GET /debug/scans/{scan_id}``. Inputs (shape documented in ``routes/debug.py:get_debug_scan``): * ``trace_view`` — dict assembled by the route handler: - ``scan_id``, ``scan_id_short``, ``scan_id_accent`` (first 8 chars) - ``target_url``, ``owner_email`` - ``overall_status`` + ``overall_pill_class`` - ``error_banner`` (None or {step, name, message}) - ``total_duration``, ``total_cost`` - ``llm_call_count``, ``validation_endpoint_count`` - ``deleted_at_display`` - ``steps`` — list of per-row dicts (see ``_step_view_dict``) - ``llm_cost_rollup`` — list of {step, cost_str, pct} sorted desc + total row - ``duration_rollup`` — list of {step, duration_str, pct} sorted desc * ``is_admin``, ``is_super_admin`` — gate banners + sub-page links * ``trace_json_url`` — the P1-T05 raw-payload endpoint The page is a single scrollable document. House style comes from ``admin_layout.html`` (the T55 design tokens); the only local CSS rules add the trace-specific bits (indented sub-step rows, rollup panel layout, filter pill toggles). No emojis, no Tailwind, vanilla JS only. #} {% extends "admin_layout.html" %} {% from 'partials/help_tooltip.html' import tip, tip_styles %} {% block page_title %}admin :: scan :: {{ trace_view.scan_id_short }}{% endblock %} {% block breadcrumb %} admin/ scans/ {{ trace_view.scan_id_short }} {% endblock %} {% block head_extra %} {{ tip_styles() }} {% if trace_view.in_progress_banner %}{% endif %} {% endblock %} {% block content %} {# ============== Eyebrow + headline ============== #}
02.1 — scan debug · trace

Scan {{ trace_view.scan_id_accent }}

Unified Step Trace — every pipeline step (capture · deterministic · LLM · gate · validation · I/O) joined from scan_events, llm_calls, and validation_runs. Filter client-side; the JSON payload is one link below the rollups.

{# ============== Error banner (§4·g) ============== #} {% if trace_view.error_banner %} {% endif %} {# ============== In-progress banner (P3-T23) ============== #} {% if trace_view.in_progress_banner %}
// in progress Scan in progress at step {{ trace_view.in_progress_banner.step }} ({{ trace_view.in_progress_banner.name }}) — refresh to update.
{% endif %} {# ============== Cancelled banner (P3-T23) ============== #} {% if trace_view.cancelled_banner %}
[cancelled] Scan cancelled at step {{ trace_view.cancelled_banner.step }} ({{ trace_view.cancelled_banner.name }}). Subsequent steps marked not reached.
{% endif %} {# ============== Header card ============== #}
target
{{ trace_view.overall_status }} {% if is_admin %}viewing as admin{% endif %} {% if trace_view.deleted_at_display %} (deleted {{ trace_view.deleted_at_display }}) {% endif %}
scan_id{{ trace_view.scan_id }}
target{{ trace_view.target_url }}
{% if is_admin and trace_view.owner_email %}
owner{{ trace_view.owner_email }}
{% endif %}
{# ============== Totals strip ============== #}
total duration
{{ trace_view.total_duration }}
total cost
{{ trace_view.total_cost }}
llm calls
{{ trace_view.llm_call_count }}
validated endpoints
{{ trace_view.validation_endpoint_count }}
{# ============== Filter bar (P1-T16) ============== #}
type {{ tip("Filter rows by step type. Click pills to toggle; all resets the set.") }} status {{ tip("Filter by step status (complete, errored, partial, in-progress, not-reached, skipped).") }} {{ tip("Case-insensitive match across step name, model, prompt_name, and S3 path.") }} {{ tip("Show only steps whose cost exceeds $0.01.") }} {{ trace_view.steps|length }} of {{ trace_view.steps|length }} steps shown
{# ============== Trace table ============== #}
{% if not trace_view.steps %}
// No pipeline events recorded for this scan.
{% else %} {% for step in trace_view.steps %} {% endfor %}
# step type started {{ tip("Wall-clock start. Hover shows the absolute UTC timestamp.", position="bottom") }} duration model cost status s3 {{ tip("Opens the in-app viewer for this step's artifact. Hover for the full S3 key.", position="bottom") }}
{{ step.index_padded }} {% if step.is_substep %}{% endif %} {{ step.step_name }} {% if step.error_class %} {{ step.error_class }} {% if step.error_excerpt %} · {{ step.error_excerpt }}{% endif %} {% endif %} {{ step.step_type }} {{ step.started_rel }} {{ step.duration }} {{ step.model or '—' }} {{ step.cost }} {{ step.status_label }} {% if step.s3_view_url %} view → {% else %} {% endif %} explore →
{% endif %}
{# ============== Rollups (P1-T07 + P1-T08) ============== #}
{# LLM cost by step #}
llm cost by step {{ tip("Per-step LLM cost as a fraction of total LLM cost on this scan. Sub-prompts (notes / difficulty_drivers) appear as their own rows.") }}
{% if not trace_view.llm_cost_rollup %}
// no LLM cost on this scan
{% else %} {% for r in trace_view.llm_cost_rollup %}
{{ r.step }} {{ r.cost_str }}{% if r.pct is not none %} · {{ r.pct }}%{% endif %}
{% endfor %} {% endif %}
{# Duration breakdown #}
duration breakdown {{ tip("Top-level step durations as a fraction of total wall-clock. Sub-steps run inside their parent's window so they're rolled up into the parent row.") }}
{% if not trace_view.duration_rollup %}
// no step durations recorded
{% else %} {% for r in trace_view.duration_rollup %}
{{ r.step }} {{ r.duration_str }}{% if r.pct is not none %} · {{ r.pct }}%{% endif %}
{% endfor %} {% endif %}
{# ============== Sub-pages strip (P1-T09) ============== #}

sub-pages

{% if trace_view.report_id %} full report →{{ tip("The user-facing scan report — verdict, recommendation, starter code.") }} {% endif %} {% if trace_view.buckets_step_index is not none %} buckets →{{ tip("The intent_filter step's Bucket A/B/C breakdown — A = first-party data endpoints you scrape, B = session/auth prerequisites, C = noise/trackers ignored.") }} {% endif %} raw json (debug payload) →{{ tip("The trace.json endpoint that backs this page — ETag-cached, returns the same data as the table above.") }}
{% if is_super_admin %}
[super_admin] force hard-delete

Skips the 30-day grace and wipes the scan + LLM-call rows + S3 objects immediately. Audit log entries for this scan are anonymized so the compliance trail survives. This cannot be undone.

{% endif %} {% endblock %} {% block scripts %} {% endblock %}