Fix layout regressions, nav drawer structure, and security issues
- base.css: add width:100%+min-width:0 to .lt-main so flex column body doesn't shrink content due to margin:0 auto from .lt-container - layout_header.php: restructure mobile nav drawer to match web_template exactly (nav-drawer-links nav, direct <a> links, section div, no ul/li wrapper, overlay after drawer); fix lt-nav-overlay id mismatch with base.js; rename lt-header-username -> lt-header-user (matches CSS); add JSON_HEX_TAG to all inline json_encode calls (closes </script> XSS) - base.css: add lt-kv-row/label/value aliases (display:contents pattern used in web_template v1.2 kv-grid); add lt-badge-sm variant - Admin views: add missing .catch() on editField/editRecurring/loadUsers; add JSON_HEX_TAG to json_encode in TemplatesView/WorkflowDesignerView - TicketView: add JSON_HEX_TAG to all ticket-data json_encode calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -359,6 +359,10 @@ hr {
|
|||||||
.lt-main {
|
.lt-main {
|
||||||
padding-top: calc(var(--header-height) + var(--space-lg));
|
padding-top: calc(var(--header-height) + var(--space-lg));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
/* When body is a flex column, margin:0 auto from .lt-container would prevent
|
||||||
|
stretch. Force full width so max-width+auto-margin centering still works. */
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0; /* prevent flex overflow on very small viewports */
|
||||||
}
|
}
|
||||||
|
|
||||||
.lt-layout {
|
.lt-layout {
|
||||||
@@ -1211,6 +1215,7 @@ select option:checked {
|
|||||||
.lt-badge-green { color: var(--accent-green); }
|
.lt-badge-green { color: var(--accent-green); }
|
||||||
.lt-badge-amber { color: var(--accent-amber); }
|
.lt-badge-amber { color: var(--accent-amber); }
|
||||||
.lt-badge-red { color: var(--accent-red); }
|
.lt-badge-red { color: var(--accent-red); }
|
||||||
|
.lt-badge-sm { font-size: 0.52rem; padding: 0.05rem 0.3rem; letter-spacing: 0.08em; }
|
||||||
|
|
||||||
/* Status + priority badge variants (dark-mode base) */
|
/* Status + priority badge variants (dark-mode base) */
|
||||||
.lt-badge-open { color: var(--accent-green); background: rgba(0,255,136,0.08); border-color: rgba(0,255,136,0.35); text-shadow: var(--glow-green); }
|
.lt-badge-open { color: var(--accent-green); background: rgba(0,255,136,0.08); border-color: rgba(0,255,136,0.35); text-shadow: var(--glow-green); }
|
||||||
@@ -3192,6 +3197,11 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-kv-val--green { color: var(--accent-green); }
|
.lt-kv-val--green { color: var(--accent-green); }
|
||||||
.lt-kv-val--red { color: var(--accent-red); }
|
.lt-kv-val--red { color: var(--accent-red); }
|
||||||
|
|
||||||
|
/* v1.2 aliases: lt-kv-row wraps label+value as a transparent grid wrapper */
|
||||||
|
.lt-kv-row { display: contents; }
|
||||||
|
.lt-kv-label { padding: var(--space-xs) var(--space-md) var(--space-xs) 0; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; white-space: nowrap; border-right: 1px solid var(--border-dim); }
|
||||||
|
.lt-kv-value { padding: var(--space-xs) 0 var(--space-xs) var(--space-md); color: var(--text-primary); }
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
43. HERO / BANNER SECTION
|
43. HERO / BANNER SECTION
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ $visUserModel = new UserModel($conn);
|
|||||||
$allAvailableGroups = $visUserModel->getAllGroups();
|
$allAvailableGroups = $visUserModel->getAllGroups();
|
||||||
|
|
||||||
// JSON-encode ticket fields for the inline script
|
// JSON-encode ticket fields for the inline script
|
||||||
$json_ticket_id = json_encode($ticket['ticket_id']);
|
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
|
||||||
$json_title = json_encode($ticket['title']);
|
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
|
||||||
$json_status = json_encode($ticket['status']);
|
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
|
||||||
$json_priority = json_encode($ticket['priority']);
|
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
|
||||||
$json_category = json_encode($ticket['category']);
|
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
|
||||||
$json_type = json_encode($ticket['type']);
|
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
|
||||||
$pageInlineScript = <<<JS
|
$pageInlineScript = <<<JS
|
||||||
window.ticketData = {
|
window.ticketData = {
|
||||||
ticket_id: {$json_ticket_id},
|
ticket_id: {$json_ticket_id},
|
||||||
|
|||||||
@@ -202,8 +202,10 @@ function editField(id) {
|
|||||||
}
|
}
|
||||||
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
||||||
lt.modal.open('fieldModal');
|
lt.modal.open('fieldModal');
|
||||||
|
} else {
|
||||||
|
lt.toast.error(data.error || 'Failed to load field');
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { lt.toast.error('Failed to load field'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteField(id) {
|
function deleteField(id) {
|
||||||
|
|||||||
@@ -240,8 +240,10 @@ function editRecurring(id) {
|
|||||||
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
||||||
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
||||||
lt.modal.open('recurringModal');
|
lt.modal.open('recurringModal');
|
||||||
|
} else {
|
||||||
|
lt.toast.error(data.error || 'Failed to load schedule');
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { lt.toast.error('Failed to load schedule'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRecurring(id) {
|
function toggleRecurring(id) {
|
||||||
@@ -287,7 +289,7 @@ function loadUsers() {
|
|||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { /* non-critical: assigned_to stays as manual input */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScheduleOptions();
|
updateScheduleOptions();
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?= $nonce ?>">
|
<script nonce="<?= $nonce ?>">
|
||||||
var templates = <?= json_encode($templates ?? []) ?>;
|
var templates = <?= json_encode($templates ?? [], JSON_HEX_TAG) ?>;
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var target = e.target.closest('[data-action]');
|
var target = e.target.closest('[data-action]');
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?= $nonce ?>">
|
<script nonce="<?= $nonce ?>">
|
||||||
var workflows = <?= json_encode($workflows ?? []) ?>;
|
var workflows = <?= json_encode($workflows ?? [], JSON_HEX_TAG) ?>;
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var target = e.target.closest('[data-action]');
|
var target = e.target.closest('[data-action]');
|
||||||
|
|||||||
+25
-36
@@ -41,15 +41,15 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
<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/utils.js"></script>
|
||||||
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
|
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
|
||||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE) ?>;
|
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
||||||
window.CURRENT_USER = <?= json_encode([
|
window.CURRENT_USER = <?= json_encode([
|
||||||
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
||||||
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
||||||
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
||||||
], JSON_UNESCAPED_UNICODE) ?>;
|
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -62,39 +62,30 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MOBILE NAV DRAWER -->
|
<!-- MOBILE NAV DRAWER — matches web_template structure exactly -->
|
||||||
<div class="lt-nav-drawer" id="lt-nav-drawer" role="dialog" aria-modal="true" aria-label="Mobile navigation">
|
<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-backdrop" id="lt-nav-drawer-backdrop" data-action="close-nav-drawer" aria-hidden="true"></div>
|
<div class="lt-nav-drawer-header">
|
||||||
<nav class="lt-nav-drawer-panel" aria-label="Mobile main navigation">
|
<span class="lt-brand-title">TINKER TICKETS</span>
|
||||||
<div class="lt-nav-drawer-header">
|
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">✕</button>
|
||||||
<span class="lt-nav-drawer-title">TINKER TICKETS</span>
|
</div>
|
||||||
<button type="button"
|
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
|
||||||
class="lt-nav-drawer-close"
|
<a href="/"
|
||||||
id="lt-nav-drawer-close"
|
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
||||||
data-action="close-nav-drawer"
|
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>Dashboard</a>
|
||||||
aria-label="Close navigation">✕</button>
|
|
||||||
</div>
|
|
||||||
<ul class="lt-nav-drawer-list" role="list">
|
|
||||||
<li>
|
|
||||||
<a href="/"
|
|
||||||
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
|
||||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
|
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php if ($_lt_isAdmin): ?>
|
<?php if ($_lt_isAdmin): ?>
|
||||||
<li class="lt-nav-drawer-section-label" aria-hidden="true">Admin</li>
|
<div class="lt-nav-drawer-section">Admin</div>
|
||||||
<li><a href="/admin/templates" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a></li>
|
<a href="/admin/templates" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a>
|
||||||
<li><a href="/admin/workflow" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a></li>
|
<a href="/admin/workflow" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a>
|
||||||
<li><a href="/admin/recurring-tickets" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a></li>
|
<a href="/admin/recurring-tickets" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a>
|
||||||
<li><a href="/admin/custom-fields" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a></li>
|
<a href="/admin/custom-fields" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a>
|
||||||
<li><a href="/admin/user-activity" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a></li>
|
<a href="/admin/user-activity" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a>
|
||||||
<li><a href="/admin/audit-log" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a></li>
|
<a href="/admin/audit-log" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a>
|
||||||
<li><a href="/admin/api-keys" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a></li>
|
<a href="/admin/api-keys" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div><!-- /.lt-nav-drawer -->
|
</div><!-- /.lt-nav-drawer -->
|
||||||
|
<!-- Overlay: outside drawer, full-screen; JS toggles .open class -->
|
||||||
|
<div id="lt-nav-overlay" class="lt-nav-drawer-overlay"></div>
|
||||||
|
|
||||||
<!-- PRIMARY HEADER -->
|
<!-- PRIMARY HEADER -->
|
||||||
<header class="lt-header" role="banner">
|
<header class="lt-header" role="banner">
|
||||||
@@ -160,9 +151,7 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
|
|
||||||
<div class="lt-header-right">
|
<div class="lt-header-right">
|
||||||
<?php if (!empty($_lt_user)): ?>
|
<?php if (!empty($_lt_user)): ?>
|
||||||
<span class="lt-header-username">
|
<span class="lt-header-user"><?= htmlspecialchars($_lt_user['display_name'] ?? $_lt_user['username'] ?? '', ENT_QUOTES, 'UTF-8') ?></span>
|
||||||
<?= htmlspecialchars($_lt_user['display_name'] ?? $_lt_user['username'] ?? '', ENT_QUOTES, 'UTF-8') ?>
|
|
||||||
</span>
|
|
||||||
<?php if ($_lt_isAdmin): ?>
|
<?php if ($_lt_isAdmin): ?>
|
||||||
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user