{# T29 scan debug page -- extends the T28 admin layout. #} {# Layout: scan header (always visible) + sticky tab bar + 5 panes. #} {# All panes are server-rendered; tiny inline JS toggles visibility on #} {# click and rewrites the URL via ``history.replaceState`` so a deep #} {# link to ``?tab=llm`` survives a reload AND stays shareable. #} {% extends "admin_layout.html" %} {% block page_title %}Scan {{ scan_id_short }}{% endblock %} {% block breadcrumb %} Admin / Scans / {{ scan_id_short }} {% endblock %} {% block content %} {# ------------------------------------------------------------------ #} {# Status pill helper -- keeps the JSON-shape colour rules in one spot #} {# rather than re-deriving on every badge. #} {# ------------------------------------------------------------------ #} {% set status_classes = { 'ready': 'bg-green-100 text-green-800', 'success': 'bg-green-100 text-green-800', 'errored': 'bg-red-100 text-red-800', 'error': 'bg-red-100 text-red-800', 'cancelled': 'bg-gray-200 text-gray-700', 'pending': 'bg-blue-100 text-blue-800', } %} {% set status_class = status_classes.get(status, 'bg-gray-100 text-gray-700') %} {# ================== Scan header (above tabs) ================== #}

Scan {{ scan_id }}

Target: {{ target_url }} {% if is_admin and owner_email %} · Owner: {{ owner_email }} {% endif %}
Intent: {{ intent_text or '(no intent recorded)' }}
{{ status }} {% if is_admin %} Viewing as admin {% endif %} {% if deleted_at_display %} {# Keep the legacy "(deleted YYYY-MM-DD)" wording so existing #} {# soft-delete integration tests don't have to re-snapshot. #} (deleted {{ deleted_at_display }}) {% endif %}
{# ================== Sticky tab bar ================== #} {# ``top-14`` lines up with the admin layout's h-14 header so the tab #} {# bar settles right beneath it when the content area scrolls. #} {% set tabs = [ ('overview', 'Overview'), ('capture', 'Capture'), ('buckets', 'Buckets'), ('llm', 'LLM calls'), ('evals', 'Evals'), ('audit', 'Audit'), ] %} {# ================== Tab pane: Overview ================== #}
{# --- Synthesis summary (legacy heading kept for test snapshots) --- #}

Synthesis view

{% if not synthesis_view %}

No synthesis output persisted yet.

{% else %} {% if synthesis_view.report_id %}

Full report →

{% endif %}

Verdict

{% if synthesis_view.verdict %}
{{ synthesis_view.verdict }}
{% else %}

(empty)

{% endif %}

Primary recommendation

{{ synthesis_view.primary_recommendation or '(none)' }}

View recommendation JSON
{{ synthesis_view.recommendation | tojson(indent=2) }}
View starter_code JSON
{{ synthesis_view.starter_code | tojson(indent=2) }}
{% endif %}
{# --- Bucket distribution + Replay confidence --- #}

Bucket distribution

Bucket A
{{ bucket_counts.A }}
target endpoints
Bucket B
{{ bucket_counts.B }}
prerequisites
Bucket C
{{ bucket_counts.C }}
ignored

Replay confidence

{% set replay = (synthesis_view.recommendation or {}).get('replay_confidence') if synthesis_view else None %} {% if replay is not none %}
{{ (replay * 100) | round(0, 'floor') | int }}%

model-estimated replay survivability

{% else %}

No replay-confidence signal on this scan.

{% endif %}
{% if is_super_admin %} {# T25: super_admin force-delete affordance. Kept on Overview so the #} {# action is one click away when an admin is reviewing a soft-deleted #} {# scan; the JS handler lower in the file fires the DELETE. #}

Force hard-delete (super_admin)

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 %}
{# ================== Tab pane: Capture ================== #}
{# --- Re-run controls (moved here from the old synthesis section) --- #}

Re-run

Filter + synthesis are free; full re-run mints a new scan + costs 1 credit.

{# --- Action timeline (legacy heading "Capture timeline" kept) --- #}

Capture timeline

{% if not timeline %}

No segmented actions persisted for this scan.

{% else %}
{% for action in timeline %}
{{ loop.index }}. {{ action.label or '(unlabelled click)' }} {% if action.started_at %} · t={{ '%.2f'|format(action.started_at) }}s {% endif %}
{% if action.click_target %}
{{ action.click_target.tag or '?' }} {%- if action.click_target.text %} · "{{ action.click_target.text }}"{% endif %}
{% endif %} {% if action.requests %}
{% for req in action.requests %}
{{ req.bucket }} {{ req.method }} {{ req.url }}
{% if req.response_summary %}
response summary
{{ req.response_summary }}
{% endif %}
{% endfor %}
{% else %}
(no requests captured in this action)
{% endif %}
{% endfor %}
{% endif %}
{# ================== Tab pane: Buckets ================== #}

Bucket explainer

{% if not bucket_explainer %}

No bucket assignment persisted (filter did not run, or legacy scan).

{% else %} {# Group by bucket: A first, then B, then C, then '-' #} {% set bucket_order = ['A', 'B', 'C', '-'] %} {% for bucket_label in bucket_order %} {% set group = bucket_explainer | selectattr('bucket', 'equalto', bucket_label) | list %} {% if group %}

Bucket {{ bucket_label }} ({{ group | length }})

{% for ep in group %}
{{ ep.bucket }} {{ ep.method }} {{ ep.url_template }}

Bucket signals

{% if ep.signals %}
{% for k, v in ep.signals.items() %}
{{ k }}
{{ v }}
{% endfor %}
{% else %}

No signals captured.

{% endif %} {% if ep.rationale %}

Filter rationale

{{ ep.rationale }}
{% endif %}
{% endfor %}
{% endif %} {% endfor %} {% endif %}
{# ================== Tab pane: LLM calls ================== #}

LLM call ledger

{% if not llm_ledger %}

No LLM calls recorded for this scan.

{% else %}
{% for call in llm_ledger %} {# T32 -- full-width expand row. Hidden until the user clicks #} {# the "Load prompt + response" button on the main row above. #} {# The chat view lives in a single `` {% endfor %}
When Prompt Model in / out / cache Cost Latency Status Dump
{{ call.created_at }} {{ call.prompt_name }}
v{{ call.prompt_version }}
{{ call.model }} {{ call.input_tokens }} / {{ call.output_tokens }} / {{ call.cache_read_tokens or 0 }}r+{{ call.cache_write_tokens or 0 }}w ${{ '%.6f'|format(call.cost_usd) }} {{ call.latency_ms }}ms {% if call.error_class %} {{ call.error_class }} {% if call.error_message %}
{{ call.error_message }}
{% endif %} {% else %} ok{% if call.retry_count %} (×{{ call.retry_count }}){% endif %} {% endif %}
{% if is_admin %} Sandbox {% endif %}
`` so it #} {# spans the entire ledger width rather than being squeezed #} {# into the narrow Dump column. ``data-expand-row-for`` keys #} {# the row to its parent call.id for the toggle handler. #}
{% endif %}
{# ================== Tab pane: Evals (T48) ================== #}
{% include 'partials/evals_matrix.html' %} {# T48.1: shared eval-new modal. fixed-scan mode -- scan_id is the #} {# page's scan_id, picker hidden. Included exactly once even though #} {# the debug page has multiple tabs, because the partial defines a #} {# fixed-id element (``#eval-new-modal``) and double-include would #} {# break the DOM-id uniqueness contract. #} {% with mode='fixed-scan', fixed_scan_id=scan_id %} {% include 'partials/eval_new_modal.html' %} {% endwith %}
{# ================== Tab pane: Audit ================== #}

Audit log

{% if not audit_entries %}

No audit entries recorded for this scan.

{% else %}
{% for entry in audit_entries %} {% endfor %}
Timestamp Actor Action Metadata
{{ entry.timestamp }} {{ entry.actor_email }} {{ entry.action }} {% if entry.metadata %} {{ entry.metadata | tojson }} {% else %} {% endif %}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %}