Files
tinker_tickets/views/admin/AuditLogView.php
Jared Vititoe 7695c6134c Accessibility pass: ARIA roles, label associations, CSS class migrations
- Add role=dialog/aria-modal/aria-labelledby to all 12 modal overlays (JS + PHP)
- Add aria-label="Close" to all 14 modal close buttons
- Add full ARIA combobox pattern to @mention autocomplete (listbox, option, aria-selected, aria-expanded)
- Add for= attributes to admin filter form labels (AuditLog, UserActivity, ApiKeys)
- Remove dead closeOnAdvancedSearchBackdropClick() from advanced-search.js

CSS/JS style cleanup:
- Move .ascii-banner static styles from JS inline to CSS class; add .ascii-banner--glow
- Add .ascii-banner-cursor, .loading-overlay--hiding, .has-overlay, tr[data-clickable]
- Add .animate-fadein/.animate-fadeout/.comment--deleting to ticket.css
- Add .lt-toast--hiding to base.css; remove opacity/transition inline JS
- Remove redundant cursor:pointer JS (already in th{} CSS rule)
- Remove trailing space in lt-select class attributes

Bug fixes:
- base.js: boot overlay opacity inline style was overriding .fade-out class opacity via
  specificity (1000 vs 20), preventing the fade-out animation — removed
- ascii-banner.js: cursor used blink-caret (border-color only) instead of blink-cursor
  (opacity-based), so the █ cursor never actually blinked — fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 20:29:58 -04:00

167 lines
9.4 KiB
PHP

<?php
// Admin view for browsing audit logs
// Receives $auditLogs, $totalPages, $page, $filters from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audit Log - Admin</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="/assets/css/base.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260320">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260320">
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
<span class="admin-page-title">Admin: Audit Log</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
<span class="user-name">[ <?php echo htmlspecialchars(strtoupper($GLOBALS['currentUser']['display_name'] ?? $GLOBALS['currentUser']['username'])); ?> ]</span>
<span class="admin-badge">[ ADMIN ]</span>
<?php endif; ?>
</div>
</div>
<div class="ascii-frame-outer admin-container-wide">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Audit Log Browser</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<!-- Filters -->
<form method="GET" class="admin-form-row">
<div class="admin-form-field">
<label class="admin-label" for="action_type">Action Type</label>
<select name="action_type" id="action_type" class="admin-input">
<option value="">All Actions</option>
<option value="create" <?php echo ($filters['action_type'] ?? '') === 'create' ? 'selected' : ''; ?>>Create</option>
<option value="update" <?php echo ($filters['action_type'] ?? '') === 'update' ? 'selected' : ''; ?>>Update</option>
<option value="delete" <?php echo ($filters['action_type'] ?? '') === 'delete' ? 'selected' : ''; ?>>Delete</option>
<option value="comment" <?php echo ($filters['action_type'] ?? '') === 'comment' ? 'selected' : ''; ?>>Comment</option>
<option value="assign" <?php echo ($filters['action_type'] ?? '') === 'assign' ? 'selected' : ''; ?>>Assign</option>
<option value="status_change" <?php echo ($filters['action_type'] ?? '') === 'status_change' ? 'selected' : ''; ?>>Status Change</option>
<option value="login" <?php echo ($filters['action_type'] ?? '') === 'login' ? 'selected' : ''; ?>>Login</option>
<option value="security" <?php echo ($filters['action_type'] ?? '') === 'security' ? 'selected' : ''; ?>>Security</option>
</select>
</div>
<div class="admin-form-field">
<label class="admin-label" for="user_id">User</label>
<select name="user_id" id="user_id" class="admin-input">
<option value="">All Users</option>
<?php if (isset($users)): foreach ($users as $user): ?>
<option value="<?php echo $user['user_id']; ?>" <?php echo ($filters['user_id'] ?? '') == $user['user_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($user['display_name'] ?? $user['username']); ?>
</option>
<?php endforeach; endif; ?>
</select>
</div>
<div class="admin-form-field">
<label class="admin-label" for="date_from">Date From</label>
<input type="date" name="date_from" id="date_from" value="<?php echo htmlspecialchars($filters['date_from'] ?? ''); ?>" class="admin-input">
</div>
<div class="admin-form-field">
<label class="admin-label" for="date_to">Date To</label>
<input type="date" name="date_to" id="date_to" value="<?php echo htmlspecialchars($filters['date_to'] ?? ''); ?>" class="admin-input">
</div>
<div class="admin-form-actions">
<button type="submit" class="btn">FILTER</button>
<a href="?" class="btn btn-secondary">RESET</a>
</div>
</form>
<!-- Log Table -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Action</th>
<th>Entity</th>
<th>Entity ID</th>
<th>Details</th>
<th>IP Address</th>
</tr>
</thead>
<tbody>
<?php if (empty($auditLogs)): ?>
<tr>
<td colspan="7" class="empty-state">No audit log entries found.</td>
</tr>
<?php else: ?>
<?php foreach ($auditLogs as $log): ?>
<tr>
<td class="nowrap"><?php echo date('Y-m-d H:i:s', strtotime($log['created_at'])); ?></td>
<td><?php echo htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System'); ?></td>
<td>
<span class="text-amber"><?php echo htmlspecialchars($log['action_type']); ?></span>
</td>
<td><?php echo htmlspecialchars($log['entity_type'] ?? '-'); ?></td>
<td>
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
<a href="/ticket/<?php echo htmlspecialchars($log['entity_id']); ?>" class="text-green">
<?php echo htmlspecialchars($log['entity_id']); ?>
</a>
<?php else: ?>
<?php echo htmlspecialchars($log['entity_id'] ?? '-'); ?>
<?php endif; ?>
</td>
<td class="td-truncate">
<?php
if ($log['details']) {
$details = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
if (is_array($details)) {
echo '<code>' . htmlspecialchars(json_encode($details)) . '</code>';
} else {
echo htmlspecialchars($log['details']);
}
} else {
echo '-';
}
?>
</td>
<td class="nowrap text-sm"><?php echo htmlspecialchars($log['ip_address'] ?? '-'); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php
$params = $_GET;
for ($i = 1; $i <= min($totalPages, 10); $i++) {
$params['page'] = $i;
$activeClass = ($i == $page) ? 'active' : '';
$url = '?' . http_build_query($params);
echo "<a href='$url' class='btn btn-small $activeClass'>$i</a> ";
}
if ($totalPages > 10) {
echo "...";
}
?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script nonce="<?php echo $nonce; ?>">if (window.lt) lt.keys.initDefaults();</script>
</body>
</html>