{#- dashboard_admin.html — the admin role's dashboard. Inherits layout_app. Backend: GET /dashboard -> render_template("dashboard_admin.html", **_admin_context()) when current_role() == 'admin' (app.py). Same shell as the client dashboard; only the topbar + content differ. CONTEXT (all real, no placeholders): users : list[dict] from db.list_users(limit=10) — id, email, role, status, created_at, updated_at. Never password_hash (db.py selects columns explicitly so a hash cannot reach a template). counts : db.user_counts() -> {total, status{...}, role{...}}, zero-filled. queue : worker.queue_stats() -> {counts{...}, pending, stale, healthy}. The routes behind the buttons are POST /admin/invite and GET /admin/users, both gated by @auth.require_role("admin") and called through fabbro.request() (which sends CSRF + `Accept: application/json`, so a lapsed session returns a 401 instead of the login page's HTML). -#} {% extends "layout_app.html" %} {% block title %}Admin{% endblock %} {#- One definition of a user row, used by the initial server render. The JS mirror (renderRows) builds the same structure from JSON. Status drives the badge colour: active=success, pending=warning, disabled=neutral. -#} {% macro user_row(u) %} {{ u.email }} {{ u.role }} {%- if u.status == 'active' %}active {%- elif u.status == 'pending' %}pending {%- else %}{{ u.status }}{% endif %} {{ u.created_at[:10] }} {% endmacro %} {#- Admin-specific nav appended to the sidebar. These use the navitem() macro from layout_app; point them at real routes as you add them. The 'users' example is commented until the route exists (no dead links by default). -#} {% block sidebar_nav %} {# {{ navitem('users', 'users', 'Users') }} — enable when /users exists #} {% endblock %} {% block app_topbar %}
Administration

Control panel

{% endblock %} {% block content %} {#- System stat cards. Every number is queried, never illustrative. -#}
Users

{{ counts.total }}

{{ counts.status.active }} active · {{ counts.status.pending }} pending

Queue

{{ queue.pending }}

{{ queue.counts.queued }} queued · {{ queue.counts.running }} running {%- if queue.counts.failed %} · {{ queue.counts.failed }} failed{% endif %}

System {#- healthy is False exactly when a human is needed: a dead-letter exists, or a worker died mid-job (stale heartbeat). Say which. -#}

{% if queue.healthy %} Operational {% else %} Attention {% endif %}

{% if queue.healthy %}Workers healthy {% elif queue.stale %}{{ queue.stale }} stale job(s) — worker may have died {% else %}{{ queue.counts.failed }} job(s) in dead-letter{% endif %}

{#- User table — the archetypal admin surface. -#}

Recent users

{#- Rows are server-rendered on load and re-rendered client-side by renderRows() after a search/refresh. Both produce the same shape; the macro below is the single definition of a row's markup. -#} {% for u in users %} {{ user_row(u) }} {% else %} {% endfor %}
EmailRoleStatusJoined
No users yet.
{% endblock %} {% block scripts %} {#- Handlers via addEventListener under the nonce (never inline onclick — CSP). All network calls go through fabbro.request(): it attaches the CSRF header and `Accept: application/json`, so an expired session comes back as a 401 we can report, not as the login page's HTML masquerading as a 200. -#} {% endblock %}