d8e6dcf7fa
CSS: - ticket.css: use combined .comment.thread-depth-N selectors to resolve the margin-left conflict between .comment-reply and .thread-depth-N classes dashboard.js: - Remove legacy initStatusFilter() (superseded by TDS v1.2 sidebar filters) - Remove initTableSorting() call (client-side sort conflicts with server ?sort=) - Remove quickSave() + saveTicket() (old hamburger-menu ticket page functions) - Remove global loadTemplate() (duplicate of IIFE-scoped version in CreateTicketView) - Remove generateSkeletonRows/Comments/Stats helpers (never called, used unregistered CSS class names like .skeleton-row-tr) - Remove "force dark mode" lines that overrode the user theme preference - Fix non-TDS CSS classes in modal templates: text-center → style, text-green → lt-text-cyan, mb-half → lt-mb-xs, modal-warning-text → lt-text-danger Admin views: - RecurringTicketsView: replace innerHTML += loop with createElement/appendChild (avoids serial DOM re-parsing on each iteration) - AuditLogView: add htmlspecialchars() to action_type option values (consistency) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
7.3 KiB
PHP
152 lines
7.3 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
|
|
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
|
|
$nonce = SecurityHeadersMiddleware::getNonce();
|
|
$pageTitle = 'Audit Log';
|
|
$activeNav = 'admin-audit-log';
|
|
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
|
|
$pageScripts = [];
|
|
include __DIR__ . '/../../views/layout_header.php';
|
|
?>
|
|
|
|
<div class="lt-page-header">
|
|
<div class="lt-flex lt-flex-gap-sm lt-flex-align-center">
|
|
<a href="/" class="lt-btn lt-btn-ghost lt-btn-sm">← Dashboard</a>
|
|
<span class="lt-text-muted lt-text-xs">/</span>
|
|
<span class="lt-text-muted lt-text-xs">Admin: Audit Log</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lt-frame">
|
|
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
|
<div class="lt-section-header">Audit Log Browser</div>
|
|
<div class="lt-section-body">
|
|
|
|
<!-- Filters -->
|
|
<form method="GET" class="lt-flex lt-flex-wrap lt-flex-gap-sm lt-mb-md" role="search" aria-label="Filter audit logs">
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="action_type">Action Type</label>
|
|
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
|
<option value="">All Actions</option>
|
|
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a): ?>
|
|
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8') ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $a)), ENT_QUOTES, 'UTF-8') ?></option>
|
|
<?php endforeach ?>
|
|
</select>
|
|
</div>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="user_id">User</label>
|
|
<select name="user_id" id="user_id" class="lt-select lt-select-sm">
|
|
<option value="">All Users</option>
|
|
<?php if (isset($users)): foreach ($users as $u): ?>
|
|
<option value="<?= (int)$u['user_id'] ?>" <?= ($filters['user_id'] ?? '') == $u['user_id'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
|
</option>
|
|
<?php endforeach; endif ?>
|
|
</select>
|
|
</div>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="date_from">Date From</label>
|
|
<input type="date" name="date_from" id="date_from" class="lt-input lt-input-sm"
|
|
value="<?= htmlspecialchars($filters['date_from'] ?? '') ?>">
|
|
</div>
|
|
<div class="lt-form-group" style="margin:0">
|
|
<label class="lt-label" for="date_to">Date To</label>
|
|
<input type="date" name="date_to" id="date_to" class="lt-input lt-input-sm"
|
|
value="<?= htmlspecialchars($filters['date_to'] ?? '') ?>">
|
|
</div>
|
|
<div class="lt-form-group lt-flex lt-flex-align-center lt-flex-gap-sm" style="margin:0;align-self:flex-end">
|
|
<button type="submit" class="lt-btn lt-btn-primary lt-btn-sm">FILTER</button>
|
|
<a href="?" class="lt-btn lt-btn-ghost lt-btn-sm">RESET</a>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Log table -->
|
|
<div class="lt-table-wrap">
|
|
<table class="lt-table lt-table-responsive" aria-label="Audit log entries">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Timestamp</th>
|
|
<th scope="col">User</th>
|
|
<th scope="col">Action</th>
|
|
<th scope="col">Entity</th>
|
|
<th scope="col">Entity ID</th>
|
|
<th scope="col">Details</th>
|
|
<th scope="col">IP Address</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($auditLogs)): ?>
|
|
<tr><td colspan="7" class="lt-empty">No audit log entries found.</td></tr>
|
|
<?php else: foreach ($auditLogs as $log): ?>
|
|
<tr>
|
|
<td data-label="Timestamp" class="lt-text-xs"><?= date('Y-m-d H:i:s', strtotime($log['created_at'])) ?></td>
|
|
<td data-label="User"><?= htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System') ?></td>
|
|
<td data-label="Action"><span class="lt-text-amber"><?= htmlspecialchars($log['action_type']) ?></span></td>
|
|
<td data-label="Entity" class="lt-text-xs"><?= htmlspecialchars($log['entity_type'] ?? '-') ?></td>
|
|
<td data-label="Entity ID" class="lt-text-xs">
|
|
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
|
<a href="/ticket/<?= htmlspecialchars($log['entity_id']) ?>"><?= htmlspecialchars($log['entity_id']) ?></a>
|
|
<?php else: ?>
|
|
<?= htmlspecialchars($log['entity_id'] ?? '-') ?>
|
|
<?php endif ?>
|
|
</td>
|
|
<td data-label="Details" class="lt-text-xs lt-text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">
|
|
<?php
|
|
if ($log['details']) {
|
|
$det = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
|
echo '<code>' . htmlspecialchars(is_array($det) ? json_encode($det) : (string)$log['details']) . '</code>';
|
|
} else {
|
|
echo '-';
|
|
}
|
|
?>
|
|
</td>
|
|
<td data-label="IP" class="lt-text-xs lt-text-muted"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
|
</tr>
|
|
<?php endforeach; endif ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<?php if (($totalPages ?? 1) > 1): ?>
|
|
<div class="lt-pagination" role="navigation">
|
|
<?php
|
|
$params = $_GET;
|
|
$start = max(1, $page - 2);
|
|
$end = min($totalPages, $page + 2);
|
|
if ($page > 1) {
|
|
$params['page'] = $page - 1;
|
|
$pUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
|
echo '<a href="' . $pUrl . '" class="lt-btn lt-btn-sm" aria-label="Previous page">«</a> ';
|
|
}
|
|
if ($start > 1) {
|
|
$params['page'] = 1;
|
|
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">1</a> ';
|
|
if ($start > 2) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
|
}
|
|
for ($i = $start; $i <= $end; $i++) {
|
|
$params['page'] = $i;
|
|
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
|
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
|
$curr = ($i == $page) ? ' aria-current="page"' : '';
|
|
echo '<a href="' . $url . '" class="lt-btn lt-btn-sm' . $class . '"' . $curr . '>' . $i . '</a> ';
|
|
}
|
|
if ($end < $totalPages) {
|
|
if ($end < $totalPages - 1) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
|
$params['page'] = $totalPages;
|
|
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">' . $totalPages . '</a> ';
|
|
}
|
|
if ($page < $totalPages) {
|
|
$params['page'] = $page + 1;
|
|
$nUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
|
echo '<a href="' . $nUrl . '" class="lt-btn lt-btn-sm" aria-label="Next page">»</a>';
|
|
}
|
|
?>
|
|
</div>
|
|
<?php endif ?>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<?php include __DIR__ . '/../../views/layout_footer.php'; ?>
|