196 lines
9.2 KiB
HTML
196 lines
9.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Audit Trail{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-3 align-items-center">
|
|
<div class="col">
|
|
<h2><i class="bi bi-shield-check"></i> Audit Trail</h2>
|
|
<p class="text-muted mb-0">Complete, immutable log of all system actions.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Filter Bar ──────────────────────────────────────────────────────────── -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-body py-3">
|
|
<form method="GET" action="{{ url_for('audit.index') }}" class="row g-2 align-items-end">
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">User</label>
|
|
<select name="user_id" class="form-select form-select-sm">
|
|
<option value="">All Users</option>
|
|
{% for u in users %}
|
|
<option value="{{ u.id }}" {% if filter_user == u.id|string %}selected{% endif %}>
|
|
{{ u.username }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">Action</label>
|
|
<select name="action" class="form-select form-select-sm">
|
|
<option value="">All Actions</option>
|
|
{% for a in distinct_actions %}
|
|
<option value="{{ a }}" {% if filter_action == a %}selected{% endif %}>{{ a }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">Entity Type</label>
|
|
<select name="entity_type" class="form-select form-select-sm">
|
|
<option value="">All Types</option>
|
|
{% for t in distinct_entity_types %}
|
|
<option value="{{ t }}" {% if filter_entity_type == t %}selected{% endif %}>{{ t }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">From Date</label>
|
|
<input type="date" name="date_from" class="form-control form-control-sm"
|
|
value="{{ filter_date_from }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label form-label-sm fw-semibold mb-1">To Date</label>
|
|
<input type="date" name="date_to" class="form-control form-control-sm"
|
|
value="{{ filter_date_to }}">
|
|
</div>
|
|
<div class="col-md-2 d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-fill">
|
|
<i class="bi bi-funnel me-1"></i>Filter
|
|
</button>
|
|
<a href="{{ url_for('audit.index') }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-x-circle"></i>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Results ─────────────────────────────────────────────────────────────── -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<span class="fw-semibold">
|
|
<i class="bi bi-list-ul me-1"></i>
|
|
{{ logs.total }} record{{ 's' if logs.total != 1 else '' }}
|
|
{% if filter_user or filter_action or filter_entity_type or filter_date_from or filter_date_to %}
|
|
<span class="badge bg-info ms-1">Filtered</span>
|
|
{% endif %}
|
|
</span>
|
|
<small class="text-muted">Page {{ logs.page }} of {{ logs.pages }}</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if logs.items %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:160px">Timestamp</th>
|
|
<th>User</th>
|
|
<th>Role</th>
|
|
<th>Action</th>
|
|
<th>Entity</th>
|
|
<th>Label</th>
|
|
<th>IP Address</th>
|
|
<th style="width:60px"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in logs.items %}
|
|
<tr>
|
|
<td class="text-nowrap text-muted small">
|
|
{{ entry.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
|
</td>
|
|
<td>
|
|
<strong>{{ entry.username }}</strong>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if entry.user_role == 'admin' %}danger{% elif entry.user_role == 'supervisor' %}warning{% else %}info{% endif %} bg-opacity-75">
|
|
{{ entry.user_role | title }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge
|
|
{% if entry.action == 'CREATE' %}bg-success
|
|
{% elif entry.action == 'UPDATE' %}bg-primary
|
|
{% elif entry.action == 'DELETE' %}bg-danger
|
|
{% elif entry.action == 'LOGIN' %}bg-secondary
|
|
{% elif entry.action == 'LOGOUT' %}bg-secondary
|
|
{% elif entry.action == 'EXPORT' %}bg-warning text-dark
|
|
{% else %}bg-light text-dark{% endif %}">
|
|
{{ entry.action }}
|
|
</span>
|
|
</td>
|
|
<td class="text-muted small">{{ entry.entity_type }}</td>
|
|
<td class="small">
|
|
{{ entry.entity_label or '—' }}
|
|
{% if entry.entity_id %}
|
|
<span class="text-muted">#{{ entry.entity_id }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-muted small font-monospace">
|
|
{{ entry.ip_address or '—' }}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('audit.view', log_id=entry.id) }}"
|
|
class="btn btn-sm btn-outline-secondary py-0 px-2">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5 text-muted">
|
|
<i class="bi bi-shield-check fs-2 d-block mb-2"></i>
|
|
No audit records match the current filters.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if logs.pages > 1 %}
|
|
<div class="card-footer bg-light d-flex justify-content-center">
|
|
<nav>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if logs.has_prev %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('audit.index', page=logs.prev_num,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
« 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('audit.index', page=p,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
{{ 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('audit.index', page=logs.next_num,
|
|
user_id=filter_user, action=filter_action,
|
|
entity_type=filter_entity_type,
|
|
date_from=filter_date_from, date_to=filter_date_to) }}">
|
|
Next »
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|