feat(audit): P3-08 — audit log UI with filters
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -311,6 +311,24 @@
|
||||
.tag-critical { color: var(--bad); border-color: color-mix(in oklch, var(--bad), transparent 60%); background: color-mix(in oklch, var(--bad), transparent 92%); }
|
||||
.tag-info { color: var(--ink-mid); }
|
||||
|
||||
/* ---------- audit rows (/audit list) ---------- */
|
||||
.audit-row {
|
||||
display: grid; align-items: center;
|
||||
grid-template-columns: 110px 80px 110px 1.4fr 1.6fr 110px;
|
||||
column-gap: 16px;
|
||||
padding: 11px 16px; font-size: 13px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
transition: background 100ms ease;
|
||||
}
|
||||
.audit-row:hover { background: var(--panel-hi); }
|
||||
.audit-row:last-child { border-bottom: 0; }
|
||||
.audit-row.head {
|
||||
cursor: default; padding-top: 9px; padding-bottom: 9px;
|
||||
font-size: 11px; color: var(--ink-fade);
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
.audit-row.head:hover { background: transparent; }
|
||||
|
||||
/* ---------- schedule rows (Schedules tab) ---------- */
|
||||
.schd-row {
|
||||
display: grid; align-items: center;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{{define "title"}}Audit · restic-manager{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{$page := .Page}}
|
||||
{{$filter := $page.Filter}}
|
||||
{{$rng := $page.Range}}
|
||||
<div class="max-w-[1280px] mx-auto px-8 pb-14">
|
||||
|
||||
{{/* crumbs */}}
|
||||
<div class="crumbs pt-6">
|
||||
<a href="/">Dashboard</a><span class="sep">/</span>
|
||||
<span class="text-ink-mid">audit</span>
|
||||
</div>
|
||||
|
||||
{{/* page header */}}
|
||||
<div class="flex items-baseline justify-between mt-3.5">
|
||||
<div>
|
||||
<h1 class="text-[22px] font-medium tracking-[-0.005em]">
|
||||
Audit log
|
||||
<span class="text-ink-fade font-normal text-[14px] ml-2">
|
||||
{{len $page.Entries}} entries · last {{if eq $rng "all"}}all-time{{else}}{{$rng}}{{end}}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-ink-mute mt-2 leading-[1.55]" style="font-size: 11.5px; max-width: 760px;">
|
||||
Append-only history of every operator action, agent message, and system-driven change.
|
||||
Read-only — entries cannot be edited or deleted.
|
||||
</div>
|
||||
|
||||
{{/* filter strip */}}
|
||||
<div class="panel mt-4 px-4 py-3 rounded-[7px]"
|
||||
style="display: grid; grid-template-columns: auto auto auto auto 1fr; gap: 14px; align-items: center;">
|
||||
|
||||
{{/* time-range pills */}}
|
||||
<div class="inline-flex gap-1 p-[3px]" style="border: 1px solid var(--line-soft); border-radius: 5px;">
|
||||
{{range list "24h" "7d" "30d" "all"}}
|
||||
{{$r := .}}
|
||||
{{$active := eq $r $rng}}
|
||||
<a href="/audit?range={{$r}}{{if $filter.UserID}}&user_id={{$filter.UserID}}{{end}}{{if $filter.Actor}}&actor={{$filter.Actor}}{{end}}{{if $filter.ActionLike}}&action={{$filter.ActionLike}}{{end}}{{if $filter.TargetKind}}&target_kind={{$filter.TargetKind}}{{end}}"
|
||||
class="btn btn-ghost"
|
||||
style="padding: 5px 10px; font-size: 11.5px;{{if $active}} background: var(--panel-hi); color: var(--ink);{{end}}">
|
||||
{{if eq $r "all"}}All{{else}}{{$r}}{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* user dropdown */}}
|
||||
<div>
|
||||
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 140px;"
|
||||
onchange="window.location='/audit?range={{$rng}}&user_id='+this.value+'{{if $filter.Actor}}&actor={{$filter.Actor}}{{end}}{{if $filter.ActionLike}}&action={{$filter.ActionLike}}{{end}}{{if $filter.TargetKind}}&target_kind={{$filter.TargetKind}}{{end}}'">
|
||||
<option value="" {{if eq $filter.UserID ""}}selected{{end}}>User · any</option>
|
||||
{{range $id, $name := $page.UserNames}}
|
||||
<option value="{{$id}}" {{if eq $filter.UserID $id}}selected{{end}}>{{$name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{/* actor dropdown — user/agent/system */}}
|
||||
<div>
|
||||
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 130px;"
|
||||
onchange="window.location='/audit?range={{$rng}}{{if $filter.UserID}}&user_id={{$filter.UserID}}{{end}}&actor='+this.value+'{{if $filter.ActionLike}}&action={{$filter.ActionLike}}{{end}}{{if $filter.TargetKind}}&target_kind={{$filter.TargetKind}}{{end}}'">
|
||||
<option value="" {{if eq $filter.Actor ""}}selected{{end}}>Actor · any</option>
|
||||
<option value="user" {{if eq $filter.Actor "user"}}selected{{end}}>user</option>
|
||||
<option value="agent" {{if eq $filter.Actor "agent"}}selected{{end}}>agent</option>
|
||||
<option value="system" {{if eq $filter.Actor "system"}}selected{{end}}>system</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{/* target kind dropdown */}}
|
||||
<div>
|
||||
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 160px;"
|
||||
onchange="window.location='/audit?range={{$rng}}{{if $filter.UserID}}&user_id={{$filter.UserID}}{{end}}{{if $filter.Actor}}&actor={{$filter.Actor}}{{end}}{{if $filter.ActionLike}}&action={{$filter.ActionLike}}{{end}}&target_kind='+this.value">
|
||||
<option value="" {{if eq $filter.TargetKind ""}}selected{{end}}>Target · any</option>
|
||||
<option value="host" {{if eq $filter.TargetKind "host"}}selected{{end}}>host</option>
|
||||
<option value="schedule" {{if eq $filter.TargetKind "schedule"}}selected{{end}}>schedule</option>
|
||||
<option value="source_group" {{if eq $filter.TargetKind "source_group"}}selected{{end}}>source_group</option>
|
||||
<option value="alert" {{if eq $filter.TargetKind "alert"}}selected{{end}}>alert</option>
|
||||
<option value="notification_channel" {{if eq $filter.TargetKind "notification_channel"}}selected{{end}}>notification_channel</option>
|
||||
<option value="job" {{if eq $filter.TargetKind "job"}}selected{{end}}>job</option>
|
||||
<option value="user" {{if eq $filter.TargetKind "user"}}selected{{end}}>user</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{/* action substring search */}}
|
||||
<form method="get" action="/audit">
|
||||
<input type="hidden" name="range" value="{{$rng}}">
|
||||
{{if $filter.UserID}}<input type="hidden" name="user_id" value="{{$filter.UserID}}">{{end}}
|
||||
{{if $filter.Actor}}<input type="hidden" name="actor" value="{{$filter.Actor}}">{{end}}
|
||||
{{if $filter.TargetKind}}<input type="hidden" name="target_kind" value="{{$filter.TargetKind}}">{{end}}
|
||||
<input type="text" name="action" value="{{$filter.ActionLike}}"
|
||||
placeholder="action contains… (e.g. alert., host.)"
|
||||
class="field mono"
|
||||
style="padding: 6px 10px; font-size: 11.5px;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{/* table */}}
|
||||
<div class="panel mt-3.5 rounded-[7px] overflow-hidden">
|
||||
|
||||
<div class="audit-row head">
|
||||
<div>When</div>
|
||||
<div>Actor</div>
|
||||
<div>User</div>
|
||||
<div>Action</div>
|
||||
<div>Target</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
{{if eq (len $page.Entries) 0}}
|
||||
<div style="padding: 40px; text-align: center;">
|
||||
<div class="text-ink text-[14px] font-medium">No matching entries.</div>
|
||||
<div class="text-ink-mute text-[12px] mt-1">
|
||||
{{if eq $rng "24h"}}Try widening the time range.{{else}}Adjust filters or pick a longer range.{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{range $page.Entries}}
|
||||
{{$e := .}}
|
||||
<div class="audit-row">
|
||||
<div class="mono text-[12px] text-ink-mute" title="{{absTime $e.TS}}">
|
||||
{{relTime $e.TS}}
|
||||
</div>
|
||||
<div>
|
||||
{{if eq $e.Actor "user"}}<span class="tag" style="background: color-mix(in oklch, var(--accent), transparent 92%); border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">user</span>
|
||||
{{else if eq $e.Actor "agent"}}<span class="tag" style="background: color-mix(in oklch, var(--ok), transparent 92%); border-color: color-mix(in oklch, var(--ok), transparent 60%); color: var(--ok);">agent</span>
|
||||
{{else}}<span class="tag" style="background: color-mix(in oklch, var(--ink-fade), transparent 92%); color: var(--ink-mute);">system</span>{{end}}
|
||||
</div>
|
||||
<div class="mono text-[12px] text-ink-mid">
|
||||
{{if $e.UserID}}{{$un := index $page.UserNames (deref $e.UserID)}}{{if $un}}{{$un}}{{else}}<span class="text-ink-fade">{{deref $e.UserID}}</span>{{end}}{{else}}<span class="text-ink-fade">—</span>{{end}}
|
||||
</div>
|
||||
<div class="mono text-[12px] text-ink">{{$e.Action}}</div>
|
||||
<div class="mono text-[12px] text-ink-mute">
|
||||
{{if $e.TargetKind}}
|
||||
<span class="text-ink-fade">{{deref $e.TargetKind}}</span>
|
||||
{{if $e.TargetID}}
|
||||
{{$tid := deref $e.TargetID}}
|
||||
{{if eq (deref $e.TargetKind) "host"}}{{$hn := index $page.HostNames $tid}}{{if $hn}} · {{$hn}}{{else}} · {{$tid}}{{end}}
|
||||
{{else}} · {{$tid}}{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<span class="text-ink-fade">—</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{if and $e.Payload (gt (len $e.Payload) 2)}}
|
||||
<details class="inline-block">
|
||||
<summary class="text-ink-fade cursor-pointer" style="font-size: 11px;">payload</summary>
|
||||
<pre class="mono text-[11px] text-ink-mute mt-2" style="white-space: pre-wrap; max-width: 400px; text-align: left; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 4px; padding: 6px 8px;">{{printf "%s" $e.Payload}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user