Apply web_template gap analysis improvements (P1-P3)

P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
      after successful validation and stores in $GLOBALS['_new_csrf_token']; add
      apiRespond() helper to append token to responses; lt.api interceptor in
      layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
      uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
      '?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
      ?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
      base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
      use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 17:02:40 -04:00
parent f0abadfc57
commit 2e450dc01d
19 changed files with 174 additions and 145 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'API Keys';
$activeNav = 'admin-api-keys';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Audit Log';
$activeNav = 'admin-audit-log';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Custom Fields';
$activeNav = 'admin-custom-fields';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Recurring Tickets';
$activeNav = 'admin-recurring';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Templates';
$activeNav = 'admin-templates';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'User Activity';
$activeNav = 'admin-user-activity';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+1 -1
View File
@@ -5,7 +5,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = 'Workflow Designer';
$activeNav = 'admin-workflow';
$pageStyles = ['/assets/css/dashboard.css?v=20260327'];
$pageScripts = [];
$pageScripts = ['/assets/js/keyboard-shortcuts.js'];
include __DIR__ . '/../../views/layout_header.php';
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = '403 Forbidden';
$activeNav = '';
$pageStyles = [];
include __DIR__ . '/../views/layout_header.php';
?>
<div class="lt-frame" style="max-width:32rem;margin:4rem auto">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header lt-text-danger">[ 403 ] ACCESS DENIED</div>
<div class="lt-section-body" style="text-align:center">
<p class="lt-text-muted lt-mb-md">You do not have permission to access this resource.</p>
<a href="/" class="lt-btn lt-btn-primary">&larr; Dashboard</a>
</div>
</div>
<?php include __DIR__ . '/../views/layout_footer.php'; ?>
+18
View File
@@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
$pageTitle = '404 Not Found';
$activeNav = '';
$pageStyles = [];
include __DIR__ . '/../views/layout_header.php';
?>
<div class="lt-frame" style="max-width:32rem;margin:4rem auto">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
<div class="lt-section-header lt-text-amber">[ 404 ] NOT FOUND</div>
<div class="lt-section-body" style="text-align:center">
<p class="lt-text-muted lt-mb-md">The page you requested does not exist.</p>
<a href="/" class="lt-btn lt-btn-primary">&larr; Dashboard</a>
</div>
</div>
<?php include __DIR__ . '/../views/layout_footer.php'; ?>
+17 -9
View File
@@ -52,7 +52,7 @@
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="show-keyboard-help" title="Show keyboard shortcuts (?)"><span class="lt-footer-key">[ ? ]</span> HELP</button>
</nav>
<span aria-label="Application version">TINKER TICKETS &mdash; TDS v1.2</span>
<span aria-label="Application version"><?= htmlspecialchars($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', ENT_QUOTES, 'UTF-8') ?> &mdash; TDS v<?= htmlspecialchars($GLOBALS['config']['APP_VERSION'] ?? '1.2', ENT_QUOTES, 'UTF-8') ?></span>
</footer>
<!-- ================================================================
@@ -131,7 +131,7 @@
<!-- LT INIT — boot animation + global UI init (base.js handles keys/nav automatically) -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
if (window.lt) {
lt.init({ bootName: 'TINKER TICKETS' });
lt.init({ bootName: <?= json_encode($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', JSON_HEX_TAG) ?> });
// Theme toggle button
var themeBtn = document.getElementById('lt-theme-btn');
@@ -155,19 +155,27 @@
]);
}
// Patch lt.api mutating methods to auto-rotate CSRF token when server returns a new one
if (window.lt && lt.api) {
['post', 'put', 'patch', 'delete'].forEach(function(method) {
if (typeof lt.api[method] !== 'function') return;
var _orig = lt.api[method];
lt.api[method] = function(url, body) {
return _orig.call(lt.api, url, body).then(function(data) {
if (data && data.csrf_token) window.CSRF_TOKEN = data.csrf_token;
return data;
});
};
});
}
// Footer hint bar actions (keyboard help + settings — work on all pages)
document.addEventListener('click', function(e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
var action = btn.getAttribute('data-action');
if (action === 'show-keyboard-help') {
if (typeof showKeyboardHelp === 'function') {
showKeyboardHelp();
} else if (window.lt) {
var h = document.getElementById('lt-keys-help');
if (h) lt.modal.open('lt-keys-help');
else lt.toast.info('Keyboard shortcuts: ESC=close Ctrl+K=search ?=this help');
}
if (window.lt) lt.modal.open('lt-keys-help');
} else if (action === 'open-settings' || action === 'open-settings-modal') {
if (typeof openSettingsModal === 'function') {
openSettingsModal();
+13 -10
View File
@@ -17,19 +17,22 @@
$_lt_user = $GLOBALS['currentUser'] ?? [];
$_lt_isAdmin = !empty($_lt_user['is_admin']);
$_lt_navActive = $activeNav ?? 'dashboard';
$_lt_appName = $GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS';
$_lt_subtitle = $GLOBALS['config']['APP_SUBTITLE'] ?? 'LotusGuild Infrastructure';
$_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
?>
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#030508">
<title><?= htmlspecialchars($pageTitle ?? 'Dashboard', ENT_QUOTES, 'UTF-8') ?> &mdash; Tinker Tickets</title>
<title><?= htmlspecialchars($pageTitle ?? 'Dashboard', ENT_QUOTES, 'UTF-8') ?> &mdash; <?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></title>
<meta name="robots" content="noindex, nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/base.css">
<link rel="stylesheet" href="/assets/css/base.css?v=<?= $_lt_assetVer ?>">
<?php if (!empty($pageStyles)): ?>
<?php foreach ($pageStyles as $_lt_css): ?>
<link rel="stylesheet" href="<?= htmlspecialchars($_lt_css, ENT_QUOTES, 'UTF-8') ?>">
@@ -37,8 +40,8 @@ $_lt_navActive = $activeNav ?? 'dashboard';
<?php endif; ?>
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
<!-- Base JS loaded in head so lt.* is available for inline view scripts -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/base.js"></script>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js"></script>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/base.js?v=<?= $_lt_assetVer ?>"></script>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js?v=<?= $_lt_assetVer ?>"></script>
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
@@ -58,14 +61,14 @@ $_lt_navActive = $activeNav ?? 'dashboard';
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
<!-- BOOT OVERLAY — controlled by lt.boot() in base.js; shown once per session -->
<div id="lt-boot" class="lt-boot-overlay" data-app-name="TINKER TICKETS" style="display:none" aria-hidden="true">
<div id="lt-boot" class="lt-boot-overlay" data-app-name="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?>" style="display:none" aria-hidden="true">
<pre id="lt-boot-text" class="lt-boot-text"></pre>
</div>
<!-- MOBILE NAV DRAWER — matches web_template structure exactly -->
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
<div class="lt-nav-drawer-header">
<span class="lt-brand-title">TINKER TICKETS</span>
<span class="lt-brand-title"><?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></span>
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">&#x2715;</button>
</div>
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
@@ -108,10 +111,10 @@ $_lt_navActive = $activeNav ?? 'dashboard';
<div class="lt-brand">
<a href="/"
class="lt-brand-title lt-glitch"
data-text="TINKER TICKETS"
data-text="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?>"
style="text-decoration:none"
aria-label="Tinker Tickets home">TINKER TICKETS</a>
<span class="lt-brand-subtitle">LotusGuild Infrastructure</span>
aria-label="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?> home"><?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></a>
<span class="lt-brand-subtitle"><?= htmlspecialchars($_lt_subtitle, ENT_QUOTES, 'UTF-8') ?></span>
</div>
<!-- Desktop navigation -->