From Hermes' agent/default/soft-delete-audit branch: - Add migration 004_audit_logging.sql (audit_logs table, trigger function, triggers on loads/shippers/vehicles/payments/portal_users, set_audit_user() helper function) - Improved: uses IF NOT EXISTS, AFTER triggers, user session context var, distinguishes SOFT_DELETE vs HARD_DELETE, notes field New: - GET /audit-logs (admin-only, filterable by table/action, paginated) - GET /audit-logs/:id (detail view with before/after JSON) - Audit Logs link in sidebar Keeps all existing OWL code: CI/CD, Pino, Prometheus, tests, cache-busting, debounced search, ESLint, Prettier
93 lines
3.9 KiB
Text
93 lines
3.9 KiB
Text
<%- include('../partials/header', { activeMenu: 'audit' }) %>
|
|
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">📜 Audit Logs</h1>
|
|
<p class="page-subtitle">Track all changes across the platform</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="GET" action="/audit-logs" class="filter-bar">
|
|
<div class="form-group">
|
|
<label class="form-label">Table</label>
|
|
<select name="table" class="form-input" onchange="this.form.submit()">
|
|
<option value="">All Tables</option>
|
|
<option value="loads" <%= filters.table === 'loads' ? 'selected' : '' %>>Loads</option>
|
|
<option value="shippers" <%= filters.table === 'shippers' ? 'selected' : '' %>>Shippers</option>
|
|
<option value="vehicles" <%= filters.table === 'vehicles' ? 'selected' : '' %>>Vehicles</option>
|
|
<option value="payments" <%= filters.table === 'payments' ? 'selected' : '' %>>Payments</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Action</label>
|
|
<select name="action" class="form-input" onchange="this.form.submit()">
|
|
<option value="">All Actions</option>
|
|
<option value="INSERT" <%= filters.action === 'INSERT' ? 'selected' : '' %>>Insert</option>
|
|
<option value="UPDATE" <%= filters.action === 'UPDATE' ? 'selected' : '' %>>Update</option>
|
|
<option value="SOFT_DELETE" <%= filters.action === 'SOFT_DELETE' ? 'selected' : '' %>>Soft Delete</option>
|
|
<option value="HARD_DELETE" <%= filters.action === 'HARD_DELETE' ? 'selected' : '' %>>Hard Delete</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label"> </label>
|
|
<a href="/audit-logs" class="btn btn-outline">Clear</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Table -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<% if (!logs || logs.length === 0) { %>
|
|
<p class="empty-state">No audit logs found matching your filters.</p>
|
|
<% } else { %>
|
|
<p class="text-muted mb-3">Showing <%= logs.length %> of <%= total %> logs (page <%= page %> of <%= totalPages %>)</p>
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Action</th>
|
|
<th>Table</th>
|
|
<th>Row ID</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<% for (const log of logs) { %>
|
|
<tr>
|
|
<td><%= new Date(log.created_at).toLocaleString('en-IN', { dateStyle: 'short', timeStyle: 'short' }) %></td>
|
|
<td>
|
|
<span class="badge badge-<%= log.action === 'INSERT' ? 'green' : log.action === 'UPDATE' ? 'blue' : log.action === 'SOFT_DELETE' ? 'orange' : 'red' %>">
|
|
<%= log.action %>
|
|
</span>
|
|
</td>
|
|
<td><code><%= log.table_name %></code></td>
|
|
<td><code><%= log.row_id ? log.row_id.slice(0,8) + '...' : '—' %></code></td>
|
|
<td><a href="/audit-logs/<%= log.id %>" class="btn btn-sm btn-outline">View</a></td>
|
|
</tr>
|
|
<% } %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- Pagination -->
|
|
<% if (totalPages > 1) { %>
|
|
<div class="pagination mt-3">
|
|
<% if (page > 1) { %>
|
|
<a href="/audit-logs?page=<%= page-1 %>&table=<%= filters.table || '' %>&action=<%= filters.action || '' %>" class="btn btn-sm btn-outline">← Prev</a>
|
|
<% } %>
|
|
<span class="text-muted mx-2">Page <%= page %> of <%= totalPages %></span>
|
|
<% if (page < totalPages) { %>
|
|
<a href="/audit-logs?page=<%= page+1 %>&table=<%= filters.table || '' %>&action=<%= filters.action || '' %>" class="btn btn-sm btn-outline">Next →</a>
|
|
<% } %>
|
|
</div>
|
|
<% } %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<%- include('../partials/footer') %>
|