rustio-admin 0.24.0

Django Admin, but for Rust. A small, focused admin framework.
Documentation
{% extends "admin/_base.html" %}
{% block content %}

{# v0.19 — page header now uses the unified `.rio-page-header`
 # primitive (cards.css) shared by every list / detail / settings
 # page. Pre-0.19 the dashboard had a bespoke `.rio-dashboard-
 # greeting` element with its own padding ladder; cross-page
 # inconsistency was a real source of "feels random" feedback. #}
<header class="rio-page-header">
  <nav class="rio-breadcrumbs">{{ app_name }}</nav>
  <div class="rio-page-actions">
    <h1>{{ index_title }}</h1>
    <div class="rio-page-header__chips">
      <span class="rio-env-chip rio-env-chip--{{ environment_kind }}" title="Runtime environment">{{ environment_label }}</span>
      <span class="rio-env-chip rio-env-chip--ver" title="Framework version">v{{ framework_version }}</span>
    </div>
  </div>
</header>

{# ---- KPI strip ---------------------------------------------------
 # v0.19 — same four data points, but the tile treatment is now
 # the unified `.rio-stat` (single white surface + 3-px top
 # accent rail). The festive cycling background fills are gone;
 # color is reserved for semantic meaning, not per-tile
 # decoration. See pages/dashboard.css. #}
<section class="rio-dashboard-stats" aria-label="At a glance">
  <article class="rio-stat rio-stat--info">
    <p class="rio-stat__label">Registered models</p>
    <p class="rio-stat__value">{{ total_models }}</p>
    <p class="rio-stat__meta">Across {{ apps|length }} app{% if apps|length != 1 %}s{% endif %}</p>
  </article>
  <article class="rio-stat rio-stat--success">
    <p class="rio-stat__label">Total rows</p>
    <p class="rio-stat__value">{{ total_rows }}</p>
    <p class="rio-stat__meta">Approx. — pg_class.reltuples</p>
  </article>
  <article class="rio-stat rio-stat--warning">
    <p class="rio-stat__label">Recent activity</p>
    <p class="rio-stat__value">{{ recent_actions_count }}</p>
    <p class="rio-stat__meta">Audit events in window</p>
  </article>
  <article class="rio-stat rio-stat--accent">
    <p class="rio-stat__label">Environment</p>
    <p class="rio-stat__value rio-stat__value--text">{{ environment_label }}</p>
    <p class="rio-stat__meta">RustIO v{{ framework_version }}</p>
  </article>
</section>

{# ---- Browse data section ----------------------------------------
 # v0.19 — wrapped in the shared `.rio-section` primitive so the
 # eyebrow label, title, and right-aligned meta all share one
 # rhythm with every other section on the page. #}
<section class="rio-section" aria-label="Browse data">
  <header class="rio-section__header">
    <div class="rio-section__heading">
      <span class="rio-section__label">Browse data</span>
      <h2 class="rio-section__title">Models</h2>
    </div>
    <span class="rio-section__meta">{{ total_models }} model{% if total_models != 1 %}s{% endif %} · {{ total_rows }} row{% if total_rows != 1 %}s{% endif %} total</span>
  </header>
  {% if apps %}
  <div class="rio-model-grid">
    {% for app in apps %}
      {% for model in app.models %}
      <article class="rio-model-tile">
        {% if app.label != model.display_name %}
        <span class="rio-model-tile__app" title="App group">{{ app.label }}</span>
        {% endif %}
        <a class="rio-model-tile__title-link" href="/admin/{{ model.admin_name }}">
          <h3 class="rio-model-tile__title">{{ model.display_name }}</h3>
        </a>
        <p class="rio-model-tile__stat">
          <span class="rio-model-tile__stat-num">{{ model.row_estimate }}</span>
          <span class="rio-model-tile__stat-suffix">{% if model.row_estimate == 1 %}row{% else %}rows{% endif %}</span>
        </p>
        {% if model.new_this_week is not none %}
        <p class="rio-model-tile__stat-secondary" title="Exact count — rows with created_at within the last 7 days">
          <span class="rio-model-tile__stat-num">{{ model.new_this_week }}</span>
          new this week
        </p>
        {% endif %}
        {% if model.weekly_series %}
        <svg class="rio-model-tile__sparkline" viewBox="0 0 140 20"
             preserveAspectRatio="none" role="img"
             aria-label="7-day creation history for {{ model.display_name }}">
          {% for v in model.weekly_series %}
          {% set bar_w = 16 %}
          {% set gap = 4 %}
          {% set x = loop.index0 * (bar_w + gap) %}
          {% set max_h = 16 %}
          {% set h = (v * max_h) // (model.weekly_series_max or 1) %}
          <rect class="rio-model-tile__sparkline-bar"
                x="{{ x }}" y="{{ max_h - h }}" width="{{ bar_w }}" height="{{ h }}" rx="1"></rect>
          {% endfor %}
        </svg>
        {% endif %}
        <p class="rio-model-tile__meta" title="Approximate — from pg_class.reltuples, refreshed by ANALYZE">
          {{ model.field_count }} field{% if model.field_count != 1 %}s{% endif %} · approx. count
        </p>
        <div class="rio-model-tile__actions">
          <a class="rio-button rio-button--ghost rio-button--sm" href="/admin/{{ model.admin_name }}">
            {{ icon("table", class="rio-icon") }} Browse
          </a>
          {% if not read_only %}
          <a class="rio-button rio-button--ghost rio-button--sm" href="/admin/{{ model.admin_name }}/new">
            {{ icon("plus", class="rio-icon") }} Add
          </a>
          {% endif %}
        </div>
      </article>
      {% endfor %}
    {% endfor %}
  </div>
  {% else %}
  {# v0.19 — proper empty state (icon + headline + meta + CTA-style
   # explanation) instead of the pre-0.19 one-paragraph .rio-empty. #}
  <div class="rio-card rio-card--quiet">
    <div class="rio-empty-state">
      <div class="rio-empty-state__icon">{{ icon("database", class="rio-icon") }}</div>
      <h3 class="rio-empty-state__title">No models registered yet</h3>
      <p class="rio-empty-state__lead">
        Run <code>rustio-admin startapp &lt;name&gt;</code> to scaffold a model + migration,
        then chain <code>.model::&lt;YourModel&gt;()</code> onto
        <code>Admin::new()</code> in <code>src/main.rs</code>. It will appear here on
        the next boot.
      </p>
    </div>
  </div>
  {% endif %}
</section>

{# ---- Activity + Tools split ------------------------------------
 # v0.19 — both halves use `.rio-section` headers + `.rio-card`
 # bodies so they share visual weight with the Browse data section
 # above. Wide ≥1024 px: split 2/3 + 1/3. Narrow: stacks. #}
<div class="rio-dashboard-split">

  <section class="rio-section" aria-label="Recent activity">
    <header class="rio-section__header">
      <div class="rio-section__heading">
        <span class="rio-section__label">Activity</span>
        <h2 class="rio-section__title">Recent</h2>
      </div>
      {% if recent_actions and identity.is_admin %}
      <a class="rio-section__link" href="/admin/history">View full history →</a>
      {% endif %}
    </header>
    {% if activity_sparkline %}
    <figure class="rio-sparkline" aria-label="Admin actions over the last 7 days">
      <figcaption class="rio-sparkline__caption">
        {{ activity_sparkline_total }} action{% if activity_sparkline_total != 1 %}s{% endif %} in the last 7 days
      </figcaption>
      {# v0.18.6 area chart unchanged in v0.19 — math + markup
       # already match the analytics-tool convention. #}
      {% set max_h = 48 %}
      {% set spark_max = activity_sparkline_max or 1 %}
      {% set ns = namespace(line="") %}
      {% for p in activity_sparkline %}
        {% set x = loop.index0 * 40 + 16 %}
        {% set y = max_h - ((p.count * max_h) // spark_max) %}
        {% set ns.line = ns.line ~ x ~ "," ~ y ~ " " %}
      {% endfor %}
      {% set first_x = 16 %}
      {% set last_x = (activity_sparkline|length - 1) * 40 + 16 %}
      <svg class="rio-sparkline__svg" viewBox="0 0 280 64" preserveAspectRatio="none" role="img" aria-hidden="true">
        <defs>
          <linearGradient id="rio-sparkline-grad" x1="0" y1="0" x2="0" y2="1">
            <stop class="rio-sparkline__grad-stop-top" offset="0%"/>
            <stop class="rio-sparkline__grad-stop-bot" offset="100%"/>
          </linearGradient>
        </defs>
        <line class="rio-sparkline__baseline" x1="{{ first_x }}" x2="{{ last_x }}" y1="{{ max_h }}" y2="{{ max_h }}"/>
        <polygon class="rio-sparkline__area" points="{{ ns.line|trim }} {{ last_x }},{{ max_h }} {{ first_x }},{{ max_h }}"/>
        <polyline class="rio-sparkline__line" points="{{ ns.line|trim }}"/>
        {% for p in activity_sparkline %}
        {% set x = loop.index0 * 40 + 16 %}
        {% set y = max_h - ((p.count * max_h) // spark_max) %}
        <circle class="rio-sparkline__dot" cx="{{ x }}" cy="{{ y }}" r="2.5"/>
        <text class="rio-sparkline__tick" x="{{ x }}" y="62" text-anchor="middle">{{ p.label }}</text>
        {% endfor %}
      </svg>
      <ul class="rio-sparkline__data" hidden>
        {% for p in activity_sparkline %}
        <li>{{ p.date_iso }} ({{ p.label }}): {{ p.count }}</li>
        {% endfor %}
      </ul>
    </figure>
    {% endif %}
    {% if recent_actions %}
    <ul class="rio-activity-feed">
      {% for a in recent_actions %}
      <li class="rio-activity-feed__item">
        <span class="rio-pill rio-pill--{{ a.pill_class }}">{{ a.label }}</span>
        <span class="rio-activity-feed__target">
          <a href="/admin/{{ a.model_name }}">{{ a.model_name }}</a>
          <span class="rio-activity-feed__sep" aria-hidden="true">·</span>
          #<a href="/admin/{{ a.model_name }}/{{ a.object_id }}/edit">{{ a.object_id }}</a>
        </span>
        {% if a.summary %}<span class="rio-activity-feed__summary">{{ a.summary }}</span>{% endif %}
        <span class="rio-activity-feed__meta">{{ a.user_email }} · {{ a.when_relative }}</span>
      </li>
      {% endfor %}
    </ul>
    {% else %}
    <div class="rio-card rio-card--quiet">
      <div class="rio-empty-state">
        <div class="rio-empty-state__icon">{{ icon("clock", class="rio-icon") }}</div>
        <h3 class="rio-empty-state__title">No actions yet</h3>
        <p class="rio-empty-state__lead">
          Once operators start creating, editing, or deleting rows,
          the audit feed lights up here.
        </p>
      </div>
    </div>
    {% endif %}
  </section>

  <aside class="rio-section" aria-label="Framework tools">
    <header class="rio-section__header">
      <div class="rio-section__heading">
        <span class="rio-section__label">Quick links</span>
        <h2 class="rio-section__title">Framework tools</h2>
      </div>
    </header>
    <ul class="rio-tool-list">
      {% if identity.is_admin %}
      <li><a class="rio-tool-link" href="/admin/history">
        {{ icon("clock", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Audit log</strong>
          <span>Full history of every authority mutation.</span>
        </span>
      </a></li>
      <li><a class="rio-tool-link" href="/admin/users">
        {{ icon("users", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Users</strong>
          <span>Create, role-shift, lock, or recover operator accounts.</span>
        </span>
      </a></li>
      <li><a class="rio-tool-link" href="/admin/groups">
        {{ icon("key", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Groups &amp; permissions</strong>
          <span>Bundle perms into reusable groups; assign by role.</span>
        </span>
      </a></li>
      <li><a class="rio-tool-link" href="/admin/feature_flags">
        {{ icon("flag", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Feature flags</strong>
          <span>Project-side boolean flags read by your code.</span>
        </span>
      </a></li>
      {% endif %}
      {% if identity.is_developer %}
      <li><a class="rio-tool-link" href="/admin/db">
        {{ icon("database", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Database explorer</strong>
          <span>Read-only Postgres schema view — tables, columns, indexes.</span>
        </span>
      </a></li>
      {% endif %}
      <li><a class="rio-tool-link" href="/admin/account/sessions">
        {{ icon("log-out", class="rio-tool-link__icon") }}
        <span class="rio-tool-link__text">
          <strong>Your sessions</strong>
          <span>Active sign-ins on this account; revoke from anywhere.</span>
        </span>
      </a></li>
    </ul>
  </aside>

</div>
{% endblock %}