diff --git a/main.py b/main.py index 85749a3..0f42c3b 100644 --- a/main.py +++ b/main.py @@ -291,23 +291,76 @@ def export_csv(): @app.get("/audit") def audit_html(): 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 FROM audits + {where_sql} ORDER BY datetime(ts) DESC, id DESC - LIMIT 500 - """).fetchall() - # simple inline HTML (you can move to a template later) - html = ["Audit", - "", - "

Audit (latest 500)

"] + LIMIT ? OFFSET ? + """, (*params, per_page, offset)).fetchall() + + # pretty JSON (server-side) to avoid giant lines + def pretty(s): + 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: - html.append(f"" - f"" - f"" - f"") - html.append("
tsactionentryactoripdeviceoldnew
{r['ts']}{r['action']}{r['entry_id']}{r['actor']}{r['ip']}{r['device']}{(r['old_row'] or '')[:2000]}{(r['new_row'] or '')[:2000]}
") - return "".join(html) + d = dict(r) + d["old_pretty"] = pretty(d.get("old_row")) + d["new_pretty"] = pretty(d.get("new_row")) + data.append(d) + + # 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") def audit_csv(): diff --git a/templates/audit.html b/templates/audit.html new file mode 100644 index 0000000..d175873 --- /dev/null +++ b/templates/audit.html @@ -0,0 +1,142 @@ + + + + + Audit Log + + + + +
+

Audit Log

+
+ + + + + + + + Clear +
+
+ +
+
{{ total }} events • page {{ page }} / {{ pages }}
+ + + + + + + + + + + + + {% for r in rows %} + + + + + + + + {% endfor %} + +
Time (UTC)ActionEntryWho / WhereOld → New
{{ r.ts }} + {% if r.action=='add' %} + add + {% elif r.action=='edit' %} + edit + {% else %} + delete + {% endif %} + + {% if r.entry_id %} + #{{ r.entry_id }} + history + {% else %} + + {% endif %} + +
+ {{ r.actor or 'anon' }} + {{ r.device or 'Unknown' }} + {{ r.ip or '' }} +
+
{{ r.user_agent[:80] }}{% if r.user_agent and r.user_agent|length>80 %}…{% endif %}
+
+
+ Old +
{{ r.old_pretty }}
+
+
+ New +
{{ r.new_pretty }}
+
+
+ + +
+ + +