Files
pxe-server/webapp/templates/audit.html
cproudlock fd00d5ff89 Webapp redesign phase 6: the remaining pages
Restyled to the same row pattern (one line, details in an expandable
body, one action plus a menu, tokens for light and dark):
- hardware coverage, audit log (family chips, destructive actions in
  red, client-side filters, first 300 rows), Clonezilla backups,
  enrollment packages (active package marked and undeletable, MCL flags
  with a visible legend)
- image Overview, OS and packages, Unattend (sticky save bar), Import
- Dell, iPXE, Blancco and BPRT update pages; Dell history links to the
  rollback review instead of rolling back on one click
- boot menu editor (tabs, rows with a menu, lint chips that jump to the
  line, tokens instead of hard-coded dark colours) and the imaging
  session detail page

Fixes found on the way:
- BPRT batch Activate never asked to confirm: a hidden input named
  'confirm' shadowed window.confirm in the inline handler, so the form
  submitted without a prompt. It now calls window.confirm.
- Backup upload refused when a backup with that name exists, unless
  'Replace the existing backup' is ticked (it overwrote silently).
- app.js: em dashes in comments replaced (plain ASCII).
2026-10-14 14:29:03 -04:00

177 lines
8.7 KiB
HTML

{% extends "base.html" %}
{% import '_ui.html' as ui %}
{% block title %}Audit log - PXE Manager{% endblock %}
{# Audit log, newest first. One line per entry: time, source, action chip and
the details truncated with a tip; the full details are in the row body.
Chips are coloured by action family; destructive actions use c-err. The
whole log is rendered; the script shows the first 300 matches and a
"Show more" control. #}
{% set families = [
('images', 'Images', 'w11'),
('drivers', 'Drivers and BIOS', 'off'),
('enrollment', 'Enrollment', 'ok'),
('blancco', 'Blancco', 'neutral'),
('imaging', 'Imaging', 'w10'),
('boot', 'Boot files', 'neutral'),
('backups', 'Backups', 'w10'),
('other', 'Other', 'neutral'),
] %}
{% set fam_kind = {} %}{% for k, l, c in families %}{% set _ = fam_kind.update({k: c}) %}{% endfor %}
{% macro family(a) -%}
{%- if a.startswith('IMAGING_') -%}imaging
{%- elif a.startswith(('IMAGE_', 'UNATTEND_', 'SHOPFLOOR_MENU', 'STARTNET', 'PACKAGE_')) -%}images
{%- elif a.startswith(('DRIVER_', 'DELL_', 'HARDWARE_', 'BIOS_')) -%}drivers
{%- elif a.startswith(('ENROLLMENT_', 'PPKG_', 'MCL_', 'BPRT_')) -%}enrollment
{%- elif a.startswith(('BLANCCO_', 'REPORT_')) -%}blancco
{%- elif a.startswith('IPXE_') -%}boot
{%- elif a.startswith('BACKUP_') -%}backups
{%- else -%}other{%- endif -%}
{%- endmacro %}
{% block extra_head %}
<style>
.agrid { display: grid; grid-template-columns: 156px 116px 210px minmax(0, 1fr) 14px; align-items: center;
gap: 0 14px; padding: 9px 24px; font-size: 14px; min-height: 24px; }
.ahead { font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding-top: 6px; padding-bottom: 6px; }
.a-when, .a-src { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.a-when { color: var(--muted); }
.a-act { min-width: 0; }
.a-act .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
.abody { padding: 2px 24px 14px 310px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
.abody .full { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.abody .raw { margin: 0; font-size: 12px; color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; }
.al-empty { padding: 28px 24px; text-align: center; color: var(--muted); border-top: 1px solid var(--line); }
.afoot { justify-content: space-between; }
@container (max-width: 820px) {
.agrid { grid-template-columns: 148px minmax(0, 1fr) 14px; }
.agrid .a-src { display: none; }
.agrid .a-det { grid-column: 1 / -1; grid-row: 2; }
.agrid .chev { grid-column: 3; grid-row: 1; }
.ahead { display: none; }
.abody { padding-left: 24px; }
}
@container (max-width: 520px) {
.agrid { grid-template-columns: auto minmax(0, 1fr) 14px; padding: 10px 16px; gap: 4px 10px; }
.agrid .a-when { overflow: visible; }
.agrid .a-when { grid-column: 1; grid-row: 1; }
.agrid .a-act { grid-column: 2; grid-row: 1; text-align: right; }
.abody { padding: 2px 16px 12px; }
}
</style>
{% endblock %}
{% block content %}
{{ ui.head('Audit log', 'Tools', (entries | length) ~ ' entr' ~ ('ies' if entries | length != 1 else 'y') ~ ', newest first.') }}
<section class="panel ui-list" aria-labelledby="al-h">
<h2 id="al-h" class="vh">Activity history</h2>
{% if entries %}
{# Parse once, so the filter pills can show counts. #}
{% set rows = [] %}
{% for entry in entries %}
{% set parts = entry.split(' ', 1) %}
{% set r = {'raw': entry, 'when': '', 'src': '', 'action': '', 'detail': entry} %}
{% if parts | length == 2 %}
{% set _ = r.update({'when': parts[0].split(',')[0], 'detail': parts[1]}) %}
{% set meta = parts[1].split('] ', 1) %}
{% if meta | length == 2 %}
{% set _ = r.update({'src': meta[0].lstrip('['), 'detail': meta[1]}) %}
{% set ad = meta[1].split(': ', 1) %}
{% if ad | length == 2 %}{% set _ = r.update({'action': ad[0], 'detail': ad[1]}) %}{% endif %}
{% endif %}
{% endif %}
{% set fam = family(r.action) if r.action else 'other' %}
{% set _ = r.update({'fam': fam, 'bad': r.action and ('DELETE' in r.action or 'ROLLBACK' in r.action or 'REMOVE' in r.action)}) %}
{% set _ = rows.append(r) %}
{% endfor %}
{% set bad_count = rows | selectattr('bad') | list | length %}
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 16px 24px">
<div role="search" style="flex: 1 1 220px; min-width: 160px; display: flex">
<label for="aq" class="vh">Filter by time, source, action or details</label>
<input id="aq" type="search" class="field" style="flex: 1; min-width: 0" placeholder="Time, source, action or details">
</div>
<div role="group" aria-label="Action family" id="afilters" style="display: flex; gap: 6px; flex-wrap: wrap">
<button type="button" class="fbtn" data-f="" aria-pressed="true">All</button>
{% if bad_count %}<button type="button" class="fbtn" data-f="destructive" aria-pressed="false">Destructive {{ bad_count }}</button>{% endif %}
{% for k, label, c in families %}
{% set n = rows | selectattr('fam', 'equalto', k) | list | length %}
{% if n %}<button type="button" class="fbtn" data-f="{{ k }}" aria-pressed="false">{{ label }} {{ n }}</button>{% endif %}
{% endfor %}
</div>
</div>
<div class="agrid ahead" aria-hidden="true"><span>Time</span><span class="a-src">Source</span><span>Action</span><span class="a-det">Details</span><span></span></div>
{% for r in rows %}
<div class="ui-row arow" data-f="{{ r.fam }}{{ ' destructive' if r.bad }}" data-q="{{ r.raw | lower }}">
<details>
<summary class="agrid">
<span class="a-when mono">{{ r.when or '-' }}</span>
<span class="a-src mono">{{ r.src or '-' }}</span>
<span class="a-act">{% if r.action %}<span class="tip inl" data-tip="{{ r.action }}" style="max-width: 100%">{{ ui.chip(r.action, 'err' if r.bad else fam_kind[r.fam]) }}</span>{% else %}<span class="sub">-</span>{% endif %}</span>
<span class="a-det tip" data-tip="{{ r.detail }}"><span class="trunc">{{ r.detail or '-' }}</span></span>
<span class="chev" aria-hidden="true">&#9662;</span>
</summary>
<div class="abody">
<p class="full">{{ r.detail or '(no details)' }}</p>
<p class="raw mono">{% if r.src %}From <span>{{ r.src }}</span> | {% endif %}{{ r.raw }}</p>
</div>
</details>
</div>
{% endfor %}
<div class="hidden-bar" id="anomatch" hidden><span>No entry matches.</span></div>
<div class="hidden-bar afoot" id="amore" hidden>
<span id="amoreText" aria-live="polite"></span>
<span style="display: flex; gap: 8px">
<button type="button" class="ui-btn sm" id="amoreBtn">Show 300 more</button>
<button type="button" class="ui-btn sm ghost" id="aallBtn">Show all</button>
</span>
</div>
{% else %}
<div class="al-empty">
No audit log entries yet. Image imports, unattend edits, backup operations and other changes are logged here.
</div>
{% endif %}
</section>
{% endblock %}
{% block extra_scripts %}
<script>
(function () {
var q = document.getElementById('aq');
if (!q) return;
var STEP = 300, limit = STEP, f = '';
var rows = Array.prototype.slice.call(document.querySelectorAll('.arow'));
var more = document.getElementById('amore'), moreText = document.getElementById('amoreText');
function apply() {
var needle = q.value.trim().toLowerCase(), matched = 0;
rows.forEach(function (r) {
var ok = (!needle || r.getAttribute('data-q').indexOf(needle) >= 0) &&
(!f || (' ' + r.getAttribute('data-f') + ' ').indexOf(' ' + f + ' ') >= 0);
if (ok) matched++;
r.hidden = !ok || matched > limit;
});
document.getElementById('anomatch').hidden = matched > 0;
var shown = Math.min(matched, limit);
more.hidden = matched <= limit;
moreText.textContent = 'Showing ' + shown + ' of ' + matched + (needle || f ? ' matching' : '') + ' entries';
}
function reset() { limit = STEP; apply(); }
q.addEventListener('input', reset);
document.getElementById('amoreBtn').addEventListener('click', function () { limit += STEP; apply(); });
document.getElementById('aallBtn').addEventListener('click', function () { limit = rows.length; apply(); });
document.querySelectorAll('#afilters .fbtn').forEach(function (b) {
b.addEventListener('click', function () {
f = b.getAttribute('data-f') || '';
document.querySelectorAll('#afilters .fbtn').forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
reset();
});
});
apply();
})();
</script>
{% endblock %}