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>
This commit is contained in:
2026-03-20 20:29:58 -04:00
parent 11f75fd823
commit 7695c6134c
21 changed files with 929 additions and 610 deletions

View File

@@ -13,10 +13,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<title>API Keys - 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">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.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; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js?v=20260320"></script>
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
@@ -25,7 +25,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="user-header">
<div class="user-header-left">
<a href="/" class="back-link">[ ← DASHBOARD ]</a>
<span style="margin-left: 1rem; color: var(--terminal-amber);">Admin: API Keys</span>
<span class="admin-page-title">Admin: API Keys</span>
</div>
<div class="user-header-right">
<?php if (isset($GLOBALS['currentUser'])): ?>
@@ -35,24 +35,23 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
</div>
<div class="ascii-frame-outer" style="max-width: 1200px; margin: 2rem auto;">
<div class="ascii-frame-outer admin-container">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">API Key Management</div>
<div class="ascii-content">
<!-- Generate New Key Form -->
<div class="ascii-frame-inner" style="margin-bottom: 1.5rem;">
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">Generate New API Key</h3>
<form id="generateKeyForm" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber); margin-bottom: 0.25rem;">Key Name *</label>
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline"
style="width: 100%; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
<div class="ascii-frame-inner">
<h3 class="admin-section-title">Generate New API Key</h3>
<form id="generateKeyForm" class="admin-form-row">
<div class="admin-form-field">
<label class="admin-label" for="keyName">Key Name *</label>
<input type="text" id="keyName" required placeholder="e.g., CI/CD Pipeline" class="admin-input">
</div>
<div style="min-width: 150px;">
<label style="display: block; font-size: 0.8rem; color: var(--terminal-amber); margin-bottom: 0.25rem;">Expires In</label>
<select id="expiresIn" style="width: 100%; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
<div class="admin-form-field">
<label class="admin-label" for="expiresIn">Expires In</label>
<select id="expiresIn" class="admin-input">
<option value="">Never</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
@@ -67,22 +66,22 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<!-- New Key Display (hidden by default) -->
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert" style="display: none; margin-bottom: 1.5rem;">
<h3 style="color: var(--terminal-amber); margin-bottom: 0.5rem;">New API Key Generated</h3>
<p style="color: var(--priority-1); margin-bottom: 1rem; font-size: 0.9rem;">
<div id="newKeyDisplay" class="ascii-frame-inner key-generated-alert" style="display: none;">
<h3 class="admin-section-title">New API Key Generated</h3>
<p class="text-danger text-sm mb-1">
Copy this key now. You won't be able to see it again!
</p>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<input type="text" id="newKeyValue" readonly
style="flex: 1; padding: 0.75rem; font-family: var(--font-mono); font-size: 0.85rem; background: var(--bg-primary); border: 2px solid var(--terminal-green); color: var(--terminal-green);">
<div class="admin-form-row">
<input type="text" id="newKeyValue" readonly class="admin-input">
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">COPY</button>
</div>
</div>
<!-- Existing Keys Table -->
<div class="ascii-frame-inner">
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">Existing API Keys</h3>
<table style="width: 100%; font-size: 0.9rem;">
<h3 class="admin-section-title">Existing API Keys</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
@@ -98,50 +97,45 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<tbody>
<?php if (empty($apiKeys)): ?>
<tr>
<td colspan="8" style="text-align: center; padding: 2rem; color: var(--terminal-green-dim);">
No API keys found. Generate one above.
</td>
<td colspan="8" class="empty-state">No API keys found. Generate one above.</td>
</tr>
<?php else: ?>
<?php foreach ($apiKeys as $key): ?>
<tr id="key-row-<?php echo $key['api_key_id']; ?>">
<td><?php echo htmlspecialchars($key['key_name']); ?></td>
<td style="font-family: var(--font-mono);">
<td class="mono">
<code><?php echo htmlspecialchars($key['key_prefix']); ?>...</code>
</td>
<td><?php echo htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown'); ?></td>
<td style="white-space: nowrap;"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
<td style="white-space: nowrap;">
<td class="nowrap"><?php echo date('Y-m-d H:i', strtotime($key['created_at'])); ?></td>
<td class="nowrap">
<?php if ($key['expires_at']): ?>
<?php
$expired = strtotime($key['expires_at']) < time();
$color = $expired ? 'var(--priority-1)' : 'var(--terminal-green)';
?>
<span style="color: <?php echo $color; ?>;">
<?php $expired = strtotime($key['expires_at']) < time(); ?>
<span class="<?php echo $expired ? 'text-danger' : ''; ?>">
<?php echo date('Y-m-d', strtotime($key['expires_at'])); ?>
<?php if ($expired): ?> (Expired)<?php endif; ?>
</span>
<?php else: ?>
<span style="color: var(--terminal-cyan);">Never</span>
<span class="text-cyan">Never</span>
<?php endif; ?>
</td>
<td style="white-space: nowrap;">
<td class="nowrap">
<?php echo $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never'; ?>
</td>
<td>
<?php if ($key['is_active']): ?>
<span style="color: var(--status-open);">Active</span>
<span class="text-open">Active</span>
<?php else: ?>
<span style="color: var(--status-closed);">Revoked</span>
<span class="text-closed">Revoked</span>
<?php endif; ?>
</td>
<td>
<?php if ($key['is_active']): ?>
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">
Revoke
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary btn-small">
REVOKE
</button>
<?php else: ?>
<span style="color: var(--text-muted);">-</span>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
</tr>
@@ -149,14 +143,15 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- API Usage Info -->
<div class="ascii-frame-inner" style="margin-top: 1.5rem;">
<h3 style="color: var(--terminal-amber); margin-bottom: 1rem;">API Usage</h3>
<p style="margin-bottom: 0.5rem;">Include the API key in your requests using the Authorization header:</p>
<pre style="background: var(--bg-primary); padding: 1rem; border: 1px solid var(--terminal-green); overflow-x: auto;"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
<p style="margin-top: 1rem; font-size: 0.9rem; color: var(--text-muted);">
<div class="ascii-frame-inner">
<h3 class="admin-section-title">API Usage</h3>
<p>Include the API key in your requests using the Authorization header:</p>
<pre class="admin-code-block"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
<p class="text-muted text-sm">
API keys provide programmatic access to create and manage tickets. Keep your keys secure and rotate them regularly.
</p>
</div>
@@ -192,20 +187,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
}
try {
const response = await fetch('/api/generate_api_key.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
key_name: keyName,
expires_in_days: expiresIn || null
})
const data = await lt.api.post('/api/generate_api_key.php', {
key_name: keyName,
expires_in_days: expiresIn || null
});
const data = await response.json();
if (data.success) {
// Show the new key
document.getElementById('newKeyValue').value = data.api_key;
@@ -231,32 +217,21 @@ $nonce = SecurityHeadersMiddleware::getNonce();
lt.toast.success('API key copied to clipboard');
}
async function revokeKey(keyId) {
if (!confirm('Are you sure you want to revoke this API key? This action cannot be undone.')) {
return;
}
try {
const response = await fetch('/api/revoke_api_key.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({ key_id: keyId })
function revokeKey(keyId) {
showConfirmModal('Revoke API Key', 'Are you sure you want to revoke this API key? This action cannot be undone.', 'error', function() {
lt.api.post('/api/revoke_api_key.php', { key_id: keyId })
.then(data => {
if (data.success) {
lt.toast.success('API key revoked successfully');
location.reload();
} else {
lt.toast.error(data.error || 'Failed to revoke API key');
}
})
.catch(error => {
lt.toast.error('Error revoking API key: ' + error.message);
});
const data = await response.json();
if (data.success) {
lt.toast.success('API key revoked successfully');
location.reload();
} else {
lt.toast.error(data.error || 'Failed to revoke API key');
}
} catch (error) {
lt.toast.error('Error revoking API key: ' + error.message);
}
});
}
</script>
</body>