Files
FabledSteward/steward/templates/audit/_rows.html
T
bvandeusenandClaude Opus 5.5 7275fad767
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m13s
CI / publish (push) Successful in 1m7s
feat(ui): audit log, alert history and Ansible runs load as you scroll (#5211)
#5211 "Lists load as you scroll: audit, alert history, Ansible runs"
(milestone 477), per preference 172.

- core/paging.py: keyset paging over (time, id), newest first, with an
  opaque "<iso>|<id>" cursor; a page reads one row past itself to know
  whether another follows. No OFFSET, so rows added while someone scrolls
  never shift the pages after them.
- _macros.html scroll_more / scroll_end and static/scroll-more.js, written
  once: an IntersectionObserver with a 600px margin fetches the next page
  before the reader reaches the end; "Loading more…" while in flight,
  "End of the list." at the end, and a failed page says so with a Retry
  button instead of re-firing on every scroll.
- Audit log: no "Load more" link and no 500-row cap.
- Alert history: no 200-row stop with "pick a shorter range";
  alert_svc.history takes the cursor, so the MCP tool and the page share
  one query.
- Ansible runs: no silent cap of 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:10:53 -04:00

34 lines
1.6 KiB
HTML

{# audit/_rows.html — one page of the audit log's rows, then the sentinel that
loads the next page as the reader nears it (core.paging, pref 172). #}
{% from "_macros.html" import scroll_more, scroll_end %}
{% for item in events %}
{% set e = item.event %}
{% set d = item.detail %}
<tr>
<td style="white-space:nowrap;" title="{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') }}">{{ e.timestamp|ago }}</td>
<td style="font-size:0.85rem;">{{ e.username }}</td>
<td><span class="mono">{{ e.action }}</span></td>
<td>
{% if item.entity %}
<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>
{% if item.entity.url %}<a href="{{ item.entity.url }}">{{ item.entity.label }}</a>{% else %}{{ item.entity.label }}{% endif %}
{% if item.entity.gone %}<span class="field-hint">(deleted)</span>{% endif %}
{% elif e.entity_type %}<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>{% endif %}
</td>
<td style="font-size:0.8rem;color:var(--text-muted);max-width:340px;">
{% if d %}
{% for k, v in d.items() %}
<span style="color:var(--text-dim);">{{ k }}:</span>
<span class="mono">{{ v }}</span>
{% if not loop.last %} &nbsp;·&nbsp; {% endif %}
{% endfor %}
{% endif %}
</td>
</tr>
{% endfor %}
{% if after %}
{{ scroll_more("/audit/?before=" ~ (after|urlencode) ~ (("&action=" ~ (action_filter|urlencode)) if action_filter else ""), cols=5) }}
{% else %}
{{ scroll_end(cols=5) }}
{% endif %}