{# Project detail panel partial. Rendered inline by dashboard.html and swapped in by GET /projects/{key}. Carries data-selected-key on its root so the sidebar can re-sync the active highlight after a swap. Live refresh: - Explicit "Refresh" button (hx-get this same partial). - hx-trigger="every 10s" poll on the root, as the live-feed fallback. TODO(SSE): once the GET /events SSE bridge lands, replace the every-10s poll here with an EventSource subscription (push roster/handoff deltas) and keep the manual Refresh button as a fallback. #} {% if selected %} {# Sort handoffs: priority-major (urgent → high → medium → low → unknown), newest-first within each priority. Jinja's `sort` only orders by raw attribute value (priority strings would sort alphabetically), so instead we sort newest-first once, then partition into explicit priority tiers and concatenate them in rank order. The date sort is stable, so newest-first is preserved within each tier. #} {% set prio_order = ["urgent", "high", "medium", "low"] %} {% set handoffs_by_date = handoffs | sort(attribute="created_at", reverse=True) | list %} {% set ns = namespace(sorted=[]) %} {% for tier in prio_order %} {% set ns.sorted = ns.sorted + (handoffs_by_date | selectattr("priority", "equalto", tier) | list) %} {% endfor %} {# any handoff with an unexpected/missing priority falls to the end #} {% set ns.sorted = ns.sorted + (handoffs_by_date | rejectattr("priority", "in", prio_order) | list) %} {% set handoffs_sorted = ns.sorted %} {# Tasks: in-progress / active first (so live work sits at the top), other statuses after. Partition into the two groups, preserving API order within each, then concatenate (active group first). #} {% set active_first = ["in_progress", "active"] %} {% set tasks_active = tasks | selectattr("status", "in", active_first) | list %} {% set tasks_rest = tasks | rejectattr("status", "in", active_first) | list %} {% set tasks_sorted = tasks_active + tasks_rest %} {# Test/pollution-pattern hint substrings. A name carrying one of these is flagged with a dim `test?` tag — a hint to the polluted/synthetic agents, NOT authority. Trailing-suffix checks (-test) handled separately below. #} {% set test_marks = ["-ddl-", "-state-", "-poc-", "-smoke", "-init-"] %}

{{ selected.display_name or selected.project_key }}

{{ selected.repo_root }}
{# Manual refresh. Targets the outer #project-panel (not panel-inner) so the whole partial — including this button — is replaced cleanly. #}
{# ---------- Vitals strip ---------- #} {% set summary = state.get("summary", {}) %}
{{ summary.active_tasks if summary.active_tasks is not none else "—" }} Active tasks
{{ summary.pending_handoffs if summary.pending_handoffs is not none else "—" }} Pending handoffs
{{ summary.events_24h if summary.events_24h is not none else "—" }} Events · 24h
{{ roster | length }} Agents
{# ---------- Roster ---------- #}

Roster

{{ roster | length }}
{% if roster %}
    {% for a in roster %} {% set nm = a.name or "" %} {% set is_test = (nm.endswith("-test")) or (test_marks | select("in", nm) | list | length > 0) %}
  • {{ a.name }}{% if is_test %}test?{% endif %} {{ a.role }} {{ a.model or "—" }} {{ a.writer_scope or "—" }}
  • {% endfor %}
{% else %}

No agents in this project's roster.

{% endif %}
{# ---------- Tasks ---------- #}

Tasks

{{ tasks | length }}
{% if tasks_sorted %}
    {% for t in tasks_sorted %}
  • {{ t.title }} {{ t.assigned_agent or "—" }} {{ t.status }}
  • {% endfor %}
{% else %}

No active tasks.

{% endif %}
{# ---------- Handoffs ---------- #}

Handoffs

{{ handoffs | length }}
{% if handoffs_sorted %}
    {% for h in handoffs_sorted %}
  • {{ h.priority }} {{ h.status }} {{ h.summary }}
    from {{ h.from_agent }} {% if h.to_agent %} to {{ h.to_agent }} {% elif h.to_role %} to {{ h.to_role }} {% endif %} {{ h.created_at }}
  • {% endfor %}
{% else %}

No handoffs for this project.

{% endif %}
{% else %}

Select a project to view its vitals, tasks, roster, and handoffs.

{% endif %}