audit UI update

main
oscar 2 months ago
parent 903341f901
commit 6e9aa44dad

@ -291,23 +291,76 @@ def export_csv():
@app.get("/audit") @app.get("/audit")
def audit_html(): def audit_html():
db = get_db() db = get_db()
rows = db.execute("""
# --- filters from query params ---
entry_id = request.args.get("entry_id", type=int)
action = request.args.get("action", type=str) # add|edit|delete
actor = request.args.get("actor", type=str)
device = request.args.get("device", type=str)
q = request.args.get("q", type=str) # substring search in old/new JSON
page = max(1, request.args.get("page", default=1, type=int))
per_page = min(200, request.args.get("per_page", default=50, type=int))
offset = (page - 1) * per_page
# --- build WHERE clause dynamically ---
wh, params = [], []
if entry_id is not None:
wh.append("entry_id = ?"); params.append(entry_id)
if action:
wh.append("action = ?"); params.append(action)
if actor:
wh.append("actor LIKE ?"); params.append(f"%{actor}%")
if device:
wh.append("device = ?"); params.append(device)
if q:
wh.append("(COALESCE(old_row,'') LIKE ? OR COALESCE(new_row,'') LIKE ? OR COALESCE(user_agent,'') LIKE ?)")
params.extend([f"%{q}%", f"%{q}%", f"%{q}%"])
where_sql = ("WHERE " + " AND ".join(wh)) if wh else ""
# total count for pagination
total = db.execute(f"SELECT COUNT(*) FROM audits {where_sql}", params).fetchone()[0]
rows = db.execute(f"""
SELECT id, ts, action, entry_id, actor, ip, user_agent, device, old_row, new_row SELECT id, ts, action, entry_id, actor, ip, user_agent, device, old_row, new_row
FROM audits FROM audits
{where_sql}
ORDER BY datetime(ts) DESC, id DESC ORDER BY datetime(ts) DESC, id DESC
LIMIT 500 LIMIT ? OFFSET ?
""").fetchall() """, (*params, per_page, offset)).fetchall()
# simple inline HTML (you can move to a template later)
html = ["<!doctype html><meta charset='utf-8'><title>Audit</title>", # pretty JSON (server-side) to avoid giant lines
"<style>body{font-family:Arial;background:#111;color:#eee}table{width:100%;border-collapse:collapse}th,td{padding:8px;border-bottom:1px solid #333}code{white-space:pre-wrap}</style>", def pretty(s):
"<h1>Audit (latest 500)</h1><table><tr><th>ts</th><th>action</th><th>entry</th><th>actor</th><th>ip</th><th>device</th><th>old</th><th>new</th></tr>"] if not s: return ""
try:
obj = json.loads(s)
return json.dumps(obj, ensure_ascii=False, indent=2)
except Exception:
return s
# enrich rows for template
data = []
for r in rows: for r in rows:
html.append(f"<tr><td>{r['ts']}</td><td>{r['action']}</td><td>{r['entry_id']}</td>" d = dict(r)
f"<td>{r['actor']}</td><td>{r['ip']}</td><td>{r['device']}</td>" d["old_pretty"] = pretty(d.get("old_row"))
f"<td><code>{(r['old_row'] or '')[:2000]}</code></td>" d["new_pretty"] = pretty(d.get("new_row"))
f"<td><code>{(r['new_row'] or '')[:2000]}</code></td></tr>") data.append(d)
html.append("</table>")
return "".join(html) # distinct devices/actions for filter dropdowns
devices = [r[0] for r in db.execute("SELECT DISTINCT device FROM audits WHERE device IS NOT NULL ORDER BY device").fetchall()]
actions = ["add", "edit", "delete"]
return render_template(
"audit.html",
rows=data,
total=total,
page=page,
per_page=per_page,
pages=max(1, (total + per_page - 1) // per_page),
# current filters
f_entry_id=entry_id, f_action=action, f_actor=actor, f_device=device, f_q=q,
devices=devices, actions=actions
)
@app.get("/audit.csv") @app.get("/audit.csv")
def audit_csv(): def audit_csv():

@ -0,0 +1,142 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Audit Log</title>
<style>
:root { color-scheme: dark; }
body { margin:0; font-family:system-ui, Arial; background:#0e0f12; color:#e8e8ea; }
header { padding:16px 20px; border-bottom:1px solid #23242a; display:flex; align-items:center; gap:12px; }
h1 { margin:0; font-size:20px; }
.filters { display:flex; gap:10px; flex-wrap:wrap; margin-left:auto; }
.filters input, .filters select {
background:#14151a; color:#e8e8ea; border:1px solid #2a2c33; border-radius:8px; padding:8px 10px; font-size:14px;
}
.filters button {
background:#2b6efe; color:white; border:none; border-radius:8px; padding:8px 12px; cursor:pointer;
}
.filters a { color:#9aa0a6; text-decoration:none; padding-left:6px; }
.wrap { padding:16px 20px; }
table { width:100%; border-collapse:separate; border-spacing:0 10px; }
thead th { font-weight:600; font-size:12px; color:#a9acb3; text-align:left; padding:0 12px 6px; }
tbody tr { background:#14151a; border:1px solid #23242a; }
tbody td { padding:12px; vertical-align:top; }
.badge { display:inline-block; padding:2px 8px; border-radius:999px; font-size:12px; font-weight:600; }
.ad { background:#153e25; color:#67d48a; border:1px solid #1f6b3b; }
.ed { background:#332a16; color:#f0c56a; border:1px solid #6d5a24; }
.del{ background:#3a1e22; color:#f08a94; border:1px solid #6d2b33; }
.meta { color:#a9acb3; font-size:12px; }
.json { background:#0c0d10; border:1px solid #23242a; border-radius:8px; padding:10px; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12px; white-space:pre-wrap; max-height:260px; overflow:auto; }
details { margin-top:8px; }
details > summary { cursor:pointer; color:#bfc3cb; }
.pill { display:inline-block; background:#1b1d23; border:1px solid #2a2c33; border-radius:999px; padding:2px 8px; font-size:12px; margin-right:6px; color:#cfd2d8; }
.rowhead { display:flex; align-items:center; gap:10px; }
.pagination { display:flex; gap:8px; align-items:center; margin-top:10px; }
.pagination a, .pagination span {
background:#14151a; border:1px solid #23242a; color:#e8e8ea; padding:6px 10px; border-radius:8px; text-decoration:none; font-size:14px;
}
.pagination .current { background:#1b1d23; }
</style>
</head>
<body>
<header>
<h1>Audit Log</h1>
<form class="filters" method="get">
<input type="number" name="entry_id" placeholder="Entry ID" value="{{ f_entry_id or '' }}">
<select name="action">
<option value="">Any action</option>
{% for a in actions %}
<option value="{{a}}" {% if f_action==a %}selected{% endif %}>{{a}}</option>
{% endfor %}
</select>
<input type="text" name="actor" placeholder="Actor" value="{{ f_actor or '' }}">
<select name="device">
<option value="">Any device</option>
{% for d in devices %}
<option value="{{d}}" {% if f_device==d %}selected{% endif %}>{{d}}</option>
{% endfor %}
</select>
<input type="text" name="q" placeholder="Search old/new/UA" value="{{ f_q or '' }}">
<input type="number" name="per_page" min="10" max="200" value="{{ per_page }}">
<button type="submit">Filter</button>
<a href="{{ url_for('audit_html') }}">Clear</a>
</form>
</header>
<div class="wrap">
<div class="meta">{{ total }} events • page {{ page }} / {{ pages }}</div>
<table>
<thead>
<tr>
<th style="width:180px">Time (UTC)</th>
<th style="width:100px">Action</th>
<th style="width:90px">Entry</th>
<th>Who / Where</th>
<th>Old → New</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr>
<td>{{ r.ts }}</td>
<td>
{% if r.action=='add' %}
<span class="badge ad">add</span>
{% elif r.action=='edit' %}
<span class="badge ed">edit</span>
{% else %}
<span class="badge del">delete</span>
{% endif %}
</td>
<td>
{% if r.entry_id %}
<span class="pill">#{{ r.entry_id }}</span>
<a class="pill" href="{{ url_for('audit_html', entry_id=r.entry_id) }}">history</a>
{% else %}
<span class="pill"></span>
{% endif %}
</td>
<td>
<div class="rowhead">
<span class="pill">{{ r.actor or 'anon' }}</span>
<span class="pill">{{ r.device or 'Unknown' }}</span>
<span class="pill">{{ r.ip or '' }}</span>
</div>
<div class="meta" title="{{ r.user_agent }}">{{ r.user_agent[:80] }}{% if r.user_agent and r.user_agent|length>80 %}…{% endif %}</div>
</td>
<td>
<details>
<summary>Old</summary>
<pre class="json">{{ r.old_pretty }}</pre>
</details>
<details>
<summary>New</summary>
<pre class="json">{{ r.new_pretty }}</pre>
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('audit_html', entry_id=f_entry_id, action=f_action, actor=f_actor, device=f_device, q=f_q, per_page=per_page, page=page-1) }}">← Prev</a>
{% else %}
<span>← Prev</span>
{% endif %}
<span class="current">Page {{ page }}</span>
{% if page < pages %}
<a href="{{ url_for('audit_html', entry_id=f_entry_id, action=f_action, actor=f_actor, device=f_device, q=f_q, per_page=per_page, page=page+1) }}">Next →</a>
{% else %}
<span>Next →</span>
{% endif %}
</div>
</div>
</body>
</html>
Loading…
Cancel
Save