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
+25 -24
View File
@@ -288,13 +288,11 @@ function clearAllFilters() {
}
function initTableSorting() {
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach((header, index) => {
header.addEventListener('click', () => {
const table = header.closest('table');
sortTable(table, index);
});
});
// Use the TDS lt.sortTable helper which manages aria-sort attributes correctly.
// Falls back to no-op if the table isn't present on this page.
if (window.lt && lt.sortTable && document.getElementById('tickets-table')) {
lt.sortTable.init('tickets-table');
}
}
function initSidebarFilters() {
@@ -1230,7 +1228,7 @@ function showTicketPreview(event) {
currentPreview.innerHTML = `
<div class="preview-header">
<span class="preview-id">#${lt.escHtml(ticketId)}</span>
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${lt.escHtml(status)}</span>
<span class="preview-status status-${lt.escHtml(status.replace(/\s+/g, '-'))}">${lt.escHtml(status)}</span>
</div>
<div class="preview-title">${lt.escHtml(title)}</div>
<div class="preview-meta">
@@ -1330,34 +1328,37 @@ function exportSelectedTickets(format) {
/**
* Show loading overlay on element
* Show TDS spinner overlay on an element.
* Uses lt-spinner + lt-loading-text from base.css.
*/
function showLoadingOverlay(element, message = 'Loading...') {
// Remove existing overlay
const existing = element.querySelector('.loading-overlay');
const existing = element.querySelector('.lt-loading-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner"></div>
<div class="loading-text">${message}</div>
`;
element.classList.add('has-overlay');
overlay.className = 'lt-loading-overlay';
const spinner = document.createElement('div');
spinner.className = 'lt-spinner';
const text = document.createElement('div');
text.className = 'lt-loading-text';
text.textContent = message;
overlay.appendChild(spinner);
overlay.appendChild(text);
element.classList.add('has-lt-overlay');
element.appendChild(overlay);
}
/**
* Hide loading overlay
* Hide TDS spinner overlay
*/
function hideLoadingOverlay(element) {
const overlay = element.querySelector('.loading-overlay');
const overlay = element.querySelector('.lt-loading-overlay');
if (overlay) {
overlay.classList.add('loading-overlay--hiding');
setTimeout(() => {
overlay.remove();
element.classList.remove('has-overlay');
}, 300);
overlay.remove();
element.classList.remove('has-lt-overlay');
}
}