186 lines
8.3 KiB
HTML
186 lines
8.3 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Activity Logs{% endblock %}
|
||
{% block page_title %}Activity Logs{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
<!-- ── Stats + Cleanup panel ───────────────────────────────────────────────── -->
|
||
<div class="row g-3 mb-3">
|
||
|
||
<!-- Summary -->
|
||
<div class="col-lg-4">
|
||
<div class="card h-100">
|
||
<div class="card-header"><i class="bi bi-bar-chart me-2"></i>Log Summary</div>
|
||
<div class="card-body">
|
||
<div style="font-size:28px;font-weight:700;color:var(--text);">{{ '{:,}'.format(total) }}</div>
|
||
<div style="font-size:13px;color:var(--muted);margin-bottom:12px;">total log entries</div>
|
||
{% if oldest %}
|
||
<div style="font-size:12px;color:var(--muted);">
|
||
<i class="bi bi-calendar3 me-1"></i>Oldest entry:
|
||
<span style="color:var(--text);">{{ oldest.strftime('%b %d, %Y') }}</span>
|
||
</div>
|
||
{% endif %}
|
||
<hr style="border-color:var(--border);margin:12px 0;">
|
||
<div style="font-size:12px;color:var(--muted);">Entries eligible for cleanup:</div>
|
||
<div class="mt-2" style="font-size:13px;">
|
||
{% for days, count in counts_by_retention.items() %}
|
||
<div class="d-flex justify-content-between align-items-center py-1"
|
||
style="border-bottom:1px solid var(--border);">
|
||
<span style="color:var(--muted);">Older than {{ days }} days</span>
|
||
<span style="font-weight:600;color:{% if count > 0 %}var(--warning){% else %}var(--success){% endif %};">
|
||
{{ '{:,}'.format(count) }}
|
||
</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cleanup -->
|
||
<div class="col-lg-8">
|
||
<div class="card h-100">
|
||
<div class="card-header d-flex align-items-center gap-2">
|
||
<i class="bi bi-trash3 me-1"></i>Clean Up Old Logs
|
||
<span style="font-size:11px;color:var(--muted);font-weight:400;">— permanently deletes entries older than the selected threshold</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<p style="font-size:13px;color:var(--muted);margin-bottom:16px;">
|
||
Select a retention period. All activity log entries older than that threshold will be
|
||
<strong style="color:var(--danger);">permanently deleted</strong> and cannot be recovered.
|
||
This action is itself logged before the deletion runs.
|
||
</p>
|
||
<div class="row g-3">
|
||
{% set options = [
|
||
(7, 'danger', 'bi-lightning-charge', 'Last 7 days', 'Aggressive — keeps only the past week'),
|
||
(30, 'warning', 'bi-calendar-week', 'Last 30 days', 'Recommended for busy systems'),
|
||
(60, 'info', 'bi-calendar-month', 'Last 60 days', 'Balanced retention'),
|
||
(90, 'success', 'bi-calendar3', 'Last 90 days', 'Conservative — keeps 3 months'),
|
||
] %}
|
||
{% for days, colour, icon, label, hint in options %}
|
||
<div class="col-6 col-lg-3">
|
||
<form method="POST" action="{{ url_for('admin.activity_logs') }}"
|
||
onsubmit="return confirmCleanup(this, {{ days }}, {{ counts_by_retention[days] }})">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||
<input type="hidden" name="days" value="{{ days }}"/>
|
||
<button type="submit" class="btn btn-sm w-100 d-flex flex-column align-items-center py-3 gap-1"
|
||
style="background:rgba(var(--{{ colour }}-rgb, 100,100,100),.08);
|
||
border:1px solid rgba(var(--{{ colour }}-rgb, 100,100,100),.25);
|
||
color:var(--{{ colour }});"
|
||
{% if counts_by_retention[days] == 0 %}disabled title="No entries to delete"{% endif %}>
|
||
<i class="bi {{ icon }}" style="font-size:20px;"></i>
|
||
<span style="font-size:13px;font-weight:600;">{{ label }}</span>
|
||
<span style="font-size:11px;opacity:.75;">
|
||
{{ '{:,}'.format(counts_by_retention[days]) }} to delete
|
||
</span>
|
||
<span style="font-size:10px;opacity:.6;text-align:center;line-height:1.3;">{{ hint }}</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Log table ────────────────────────────────────────────────────────────── -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i class="bi bi-list-ul me-2"></i>System Activity Log
|
||
<span style="font-size:12px;color:var(--muted);">
|
||
— page {{ logs.page }} of {{ logs.pages }} ({{ '{:,}'.format(logs.total) }} entries)
|
||
</span>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<table class="table mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Timestamp</th>
|
||
<th>Action</th>
|
||
<th>User</th>
|
||
<th>Entity</th>
|
||
<th>Details</th>
|
||
<th>IP Address</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for log in logs.items %}
|
||
<tr>
|
||
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;white-space:nowrap;">
|
||
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||
</td>
|
||
<td>
|
||
<span class="mono" style="font-size:11px;
|
||
color:{% if 'create' in log.action %}var(--success)
|
||
{% elif 'delete' in log.action or 'deactivate' in log.action %}var(--danger)
|
||
{% elif 'update' in log.action or 'edit' in log.action or 'change' in log.action %}var(--warning)
|
||
{% elif 'login' in log.action %}var(--accent3)
|
||
{% elif 'cleanup' in log.action %}var(--danger)
|
||
{% else %}var(--muted){% endif %};">
|
||
{{ log.action }}
|
||
</span>
|
||
</td>
|
||
<td style="font-size:13px;">
|
||
{% if log.user %}
|
||
{{ log.user.full_name }}<br>
|
||
<span style="font-size:11px;color:var(--muted);">{{ log.user.email }}</span>
|
||
{% else %}
|
||
<span style="color:var(--muted);">System</span>
|
||
{% endif %}
|
||
</td>
|
||
<td style="font-size:12px;color:var(--muted);">
|
||
{{ log.entity_type or '—' }}{% if log.entity_id %} <span class="mono">#{{ log.entity_id }}</span>{% endif %}
|
||
</td>
|
||
<td style="font-size:12px;color:var(--muted);max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
|
||
title="{{ log.details or '' }}">
|
||
{{ log.details or '—' }}
|
||
</td>
|
||
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;">
|
||
{{ log.ip_address or '—' }}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if logs.pages > 1 %}
|
||
<div class="d-flex justify-content-center py-3">
|
||
<nav><ul class="pagination mb-0">
|
||
{% if logs.has_prev %}
|
||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.activity_logs', page=logs.prev_num) }}">‹ Prev</a></li>
|
||
{% endif %}
|
||
{% for p in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||
{% if p %}
|
||
<li class="page-item {% if p==logs.page %}active{% endif %}">
|
||
<a class="page-link" href="{{ url_for('admin.activity_logs', page=p) }}">{{ p }}</a>
|
||
</li>
|
||
{% else %}
|
||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% if logs.has_next %}
|
||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.activity_logs', page=logs.next_num) }}">Next ›</a></li>
|
||
{% endif %}
|
||
</ul></nav>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function confirmCleanup(form, days, count) {
|
||
if (count === 0) return false;
|
||
confirmModal(
|
||
'Clean Up Activity Logs',
|
||
'Delete ' + count.toLocaleString() + ' log entr' + (count === 1 ? 'y' : 'ies') +
|
||
' older than ' + days + ' days? This action cannot be undone.',
|
||
'Delete Logs',
|
||
'btn-danger',
|
||
() => { form._confirmed = true; form.submit(); }
|
||
);
|
||
return false;
|
||
}
|
||
</script>
|
||
{% endblock %} |