Files
local-ci-cd-system/src/ci_orchestrator/web/templates/partials/audit.html
T
Simone 9d7bbee35d feat(web): dashboard redesign — Gitea palette, health panels, VM/audit enrichment (stages A–C)
Stage A: swap CSS tokens to the real Gitea dark palette (accent blue #4183c4); add Jinja formatting filters (humanbytes/duration/relative_time/localtime/shorten_path/thousands), a health status-header, and an inline SVG icon sprite; fix two UI bugs — unthemed action pages (htmx swap into #action-slot with a full-page HX-Request fallback) and polling wiping form input (hx-preserve).

Stage B: four read-only panels that degrade to notices (never 500) — host resources (/proc, two-sample CPU%), VMware module health, template inventory (.vmem warn + last backup), and a path-contained file browser; backup card becomes a validated template dropdown; expensive dir-size walk is TTL-cached. Adds operations/inventory.py plus host_resources()/vmware_module_health() in operations/host.py.

Stage C: parse_clone_name() adds a Job column (job_id + started) to the VM table; nav gains a Gitea link-out derived from the OIDC issuer origin (no fabricated run URLs); audit page gets action icons, relative time, and a client-side filter.

Gates: 989 tests pass, coverage 94.12%, ruff + mypy --strict clean. Stage D (service management) intentionally deferred.
2026-07-24 11:46:50 +02:00

82 lines
2.8 KiB
HTML

{% extends "base.html" %}
{% block title %}Audit log - ci-orchestrator{% endblock %}
{% block content %}
<section class="card" id="audit-card">
<h2><svg class="icon" aria-hidden="true"><use href="#i-check"></use></svg>Audit log</h2>
{% if entries %}
<div class="audit-filter">
<label for="audit-filter-input">Filter</label>
<input type="text" id="audit-filter-input" autocomplete="off"
placeholder="Filter by user, action, target, detail..."
aria-controls="audit-rows">
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Time</th>
<th>User</th>
<th>Action</th>
<th>Target</th>
<th>Result</th>
<th>Detail</th>
</tr>
</thead>
<tbody id="audit-rows">
{% for e in entries %}
{% set icon_id = "i-server" if e.action.startswith("vm.")
else "i-file" if e.action.startswith("cache.")
else "i-box" if e.action.startswith("host.")
else "i-gear" if e.action.startswith("service.")
else "i-alert" %}
<tr class="{{ 'row-alert' if e.result == 'error' else '' }}">
<td title="{{ e.ts | localtime }}">{{ e.ts | relative_time }}</td>
<td>{{ e.user }}</td>
<td><svg class="icon" aria-hidden="true"><use href="#{{ icon_id }}"></use></svg> {{ e.action }}</td>
<td><code>{{ e.target }}</code></td>
<td>
{% if e.result == "ok" %}
<span class="badge badge-success">ok</span>
{% elif e.result == "error" %}
<span class="badge badge-danger">error</span>
{% else %}
<span class="badge badge-warn">{{ e.result }}</span>
{% endif %}
</td>
<td>{{ e.detail }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p>No audit entries yet.</p>
{% endif %}
</section>
{% if entries %}
{#
Client-side-only filter (C3): no CSP header is set anywhere in this app
(see web/app.py / security.py), so a plain inline <script> is safe here --
no backend round-trip needed for a hide/show over rows already on the
page. Case-insensitive substring match over each row's full text.
#}
<script>
(function () {
var input = document.getElementById("audit-filter-input");
var rows = document.getElementById("audit-rows");
if (!input || !rows) { return; }
input.addEventListener("input", function () {
var q = input.value.trim().toLowerCase();
var trs = rows.getElementsByTagName("tr");
for (var i = 0; i < trs.length; i++) {
var text = trs[i].textContent.toLowerCase();
trs[i].style.display = (q === "" || text.indexOf(q) !== -1) ? "" : "none";
}
});
})();
</script>
{% endif %}
{% endblock %}