Fix bracket buttons rendering below text + UI/security improvements
CSS fixes: - Fix [ ] brackets appearing below button text by replacing display:inline-flex with display:inline-block + white-space:nowrap on .btn — removes cross-browser flex pseudo-element inconsistency as root cause - Remove conflicting .btn::before ripple block (position:absolute was overriding bracket content positioning) - Remove overflow:hidden from .btn which was clipping bracket content - Fix body::after duplicate rule causing GPU layer blink (second position:fixed rule re-created compositor layer, overriding display:none suppression) - Replace all transition:all with scoped property transitions in dashboard.css, ticket.css, base.css (prevents full CSS property evaluation on every hover) - Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity animation (GPU-composited, eliminates CPU repaints at 60fps) - Fix mobile *::before/*::after blanket content:none rule — now targets only decorative frame glyphs, preserving button brackets and status indicators - Remove --terminal-green-dim override that broke .lt-btn hover backgrounds JS fixes: - Fix all lt.lt.toast.* double-prefix instances in dashboard.js - Add null guard before .appendChild() on bulkAssignUser select - Replace all remaining emoji with terminal bracket notation (dashboard.js, ticket.js, markdown.js) - Migrate all toast.*() shim calls to lt.toast.* across all JS files View fixes: - Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them) - Replace all emoji with terminal bracket notation in all views and admin views - Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags - Bump CSS version strings to ?v=20260319b for cache busting Security fixes: - update_ticket.php: add authorization check (non-admins can only edit their own or assigned tickets) - add_comment.php: validate and cast ticket_id to integer with 400 response - clone_ticket.php: fix unconditional session_start(), add ticket ID validation, add internal ticket access check - bulk_operation.php: add HTTP 401/403 status codes on auth failures - upload_attachment.php: fix missing $conn arg in AttachmentModel constructor - assign_ticket.php: add ticket existence check and permission verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,7 @@ function initMobileSidebar() {
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.id = 'mobileFilterToggle';
|
||||
toggleBtn.className = 'mobile-filter-toggle';
|
||||
toggleBtn.innerHTML = '☰ Filters & Search Options';
|
||||
toggleBtn.innerHTML = '[ = ] Filters & Search Options';
|
||||
toggleBtn.onclick = openMobileSidebar;
|
||||
dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild);
|
||||
}
|
||||
@@ -79,20 +79,20 @@ function initMobileSidebar() {
|
||||
nav.className = 'mobile-bottom-nav';
|
||||
nav.innerHTML = `
|
||||
<a href="/">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-label">Home</span>
|
||||
<span class="nav-icon">[ ~ ]</span>
|
||||
<span class="nav-label">HOME</span>
|
||||
</a>
|
||||
<button type="button" data-action="open-mobile-sidebar">
|
||||
<span class="nav-icon">🔍</span>
|
||||
<span class="nav-label">Filter</span>
|
||||
<span class="nav-icon">[ / ]</span>
|
||||
<span class="nav-label">FILTER</span>
|
||||
</button>
|
||||
<a href="/ticket/create">
|
||||
<span class="nav-icon">➕</span>
|
||||
<span class="nav-label">New</span>
|
||||
<span class="nav-icon">[ + ]</span>
|
||||
<span class="nav-label">NEW</span>
|
||||
</a>
|
||||
<button type="button" data-action="open-settings-modal">
|
||||
<span class="nav-icon">⚙</span>
|
||||
<span class="nav-label">Settings</span>
|
||||
<span class="nav-icon">[ * ]</span>
|
||||
<span class="nav-label">CFG</span>
|
||||
</button>
|
||||
`;
|
||||
document.body.appendChild(nav);
|
||||
@@ -566,11 +566,11 @@ function quickSave() {
|
||||
}
|
||||
|
||||
} else {
|
||||
toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error updating ticket: ' + error.message, 5000);
|
||||
lt.toast.error('Error updating ticket: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -671,11 +671,11 @@ function loadTemplate() {
|
||||
document.getElementById('priority').value = template.default_priority;
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error loading template: ' + error.message, 4000);
|
||||
lt.toast.error('Error loading template: ' + error.message, 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ function bulkClose() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -782,17 +782,17 @@ function performBulkCloseAction(ticketIds) {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
|
||||
lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Bulk close failed: ' + error.message, 5000);
|
||||
lt.toast.error('Bulk close failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ function showBulkAssignModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -834,12 +834,14 @@ function showBulkAssignModal() {
|
||||
.then(data => {
|
||||
if (data.success && data.users) {
|
||||
const select = document.getElementById('bulkAssignUser');
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if (select) {
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => lt.toast.error('Error loading users'));
|
||||
@@ -856,7 +858,7 @@ function performBulkAssign() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!userId) {
|
||||
toast.warning('Please select a user', 2000);
|
||||
lt.toast.warning('Please select a user', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -878,17 +880,17 @@ function performBulkAssign() {
|
||||
if (data.success) {
|
||||
closeBulkAssignModal();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
lt.toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
|
||||
lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Bulk assign failed: ' + error.message, 5000);
|
||||
lt.toast.error('Bulk assign failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -896,7 +898,7 @@ function showBulkPriorityModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -941,7 +943,7 @@ function performBulkPriority() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!priority) {
|
||||
toast.warning('Please select a priority', 2000);
|
||||
lt.toast.warning('Please select a priority', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -963,17 +965,17 @@ function performBulkPriority() {
|
||||
if (data.success) {
|
||||
closeBulkPriorityModal();
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
lt.toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
|
||||
lt.toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Bulk priority update failed: ' + error.message, 5000);
|
||||
lt.toast.error('Bulk priority update failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1013,7 +1015,7 @@ function showBulkStatusModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1057,7 +1059,7 @@ function performBulkStatusChange() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!status) {
|
||||
toast.warning('Please select a status', 2000);
|
||||
lt.toast.warning('Please select a status', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1079,17 +1081,17 @@ function performBulkStatusChange() {
|
||||
closeBulkStatusModal();
|
||||
if (data.success) {
|
||||
if (data.failed > 0) {
|
||||
toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
lt.toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
|
||||
} else {
|
||||
toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
|
||||
lt.toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Bulk status change failed: ' + error.message, 5000);
|
||||
lt.toast.error('Bulk status change failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1098,7 +1100,7 @@ function showBulkDeleteModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1106,7 +1108,7 @@ function showBulkDeleteModal() {
|
||||
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header" style="color: var(--status-closed);">
|
||||
<span class="lt-modal-title">⚠ Delete ${ticketIds.length} Ticket(s)</span>
|
||||
<span class="lt-modal-title">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
|
||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body" style="text-align:center;">
|
||||
@@ -1150,14 +1152,14 @@ function performBulkDelete() {
|
||||
.then(data => {
|
||||
closeBulkDeleteModal();
|
||||
if (data.success) {
|
||||
toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
|
||||
lt.toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Bulk delete failed: ' + error.message, 5000);
|
||||
lt.toast.error('Bulk delete failed: ' + error.message, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1186,15 +1188,15 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
|
||||
|
||||
// Icon based on type
|
||||
const icons = {
|
||||
warning: '⚠',
|
||||
error: '✗',
|
||||
info: 'ℹ'
|
||||
warning: '[ ! ]',
|
||||
error: '[ X ]',
|
||||
info: '[ i ]',
|
||||
};
|
||||
const icon = icons[type] || icons.warning;
|
||||
|
||||
// Escape user-provided content to prevent XSS
|
||||
const safeTitle = escapeHtml(title);
|
||||
const safeMessage = escapeHtml(message);
|
||||
const safeTitle = lt.escHtml(title);
|
||||
const safeMessage = lt.escHtml(message);
|
||||
|
||||
const modalHtml = `
|
||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
||||
@@ -1243,9 +1245,9 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
|
||||
const inputId = modalId + '_input';
|
||||
|
||||
// Escape user-provided content to prevent XSS
|
||||
const safeTitle = escapeHtml(title);
|
||||
const safeLabel = escapeHtml(label);
|
||||
const safePlaceholder = escapeHtml(placeholder);
|
||||
const safeTitle = lt.escHtml(title);
|
||||
const safeLabel = lt.escHtml(label);
|
||||
const safePlaceholder = lt.escHtml(placeholder);
|
||||
|
||||
const modalHtml = `
|
||||
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
|
||||
@@ -1307,8 +1309,8 @@ function quickStatusChange(ticketId, currentStatus) {
|
||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
|
||||
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${escapeHtml(currentStatus)}</p>
|
||||
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
||||
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${lt.escHtml(currentStatus)}</p>
|
||||
<label for="quickStatusSelect">New Status:</label>
|
||||
<select id="quickStatusSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
||||
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
|
||||
@@ -1351,15 +1353,15 @@ function performQuickStatusChange(ticketId) {
|
||||
.then(data => {
|
||||
closeQuickStatusModal();
|
||||
if (data.success) {
|
||||
toast.success(`Status updated to ${newStatus}`, 3000);
|
||||
lt.toast.success(`Status updated to ${newStatus}`, 3000);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
closeQuickStatusModal();
|
||||
toast.error('Error updating status', 4000);
|
||||
lt.toast.error('Error updating status', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1375,7 +1377,7 @@ function quickAssign(ticketId) {
|
||||
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
|
||||
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
|
||||
<label for="quickAssignSelect">Assign to:</label>
|
||||
<select id="quickAssignSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
|
||||
<option value="">Unassigned</option>
|
||||
@@ -1433,15 +1435,15 @@ function performQuickAssign(ticketId) {
|
||||
.then(data => {
|
||||
closeQuickAssignModal();
|
||||
if (data.success) {
|
||||
toast.success('Assignment updated', 3000);
|
||||
lt.toast.success('Assignment updated', 3000);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
closeQuickAssignModal();
|
||||
toast.error('Error updating assignment', 4000);
|
||||
lt.toast.error('Error updating assignment', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1522,13 +1524,13 @@ function populateKanbanCards() {
|
||||
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
|
||||
card.innerHTML = `
|
||||
<div class="card-header">
|
||||
<span class="card-id">#${escapeHtml(ticketId)}</span>
|
||||
<span class="card-id">#${lt.escHtml(ticketId)}</span>
|
||||
<span class="card-priority p${priority}">P${priority}</span>
|
||||
</div>
|
||||
<div class="card-title">${escapeHtml(title)}</div>
|
||||
<div class="card-title">${lt.escHtml(title)}</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-category">${escapeHtml(category)}</span>
|
||||
<span class="card-assignee" title="${escapeHtml(assignedTo)}">${escapeHtml(initials)}</span>
|
||||
<span class="card-category">${lt.escHtml(category)}</span>
|
||||
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
|
||||
</div>
|
||||
`;
|
||||
column.appendChild(card);
|
||||
@@ -1615,17 +1617,17 @@ function showTicketPreview(event) {
|
||||
// Build preview content
|
||||
currentPreview.innerHTML = `
|
||||
<div class="preview-header">
|
||||
<span class="preview-id">#${escapeHtml(ticketId)}</span>
|
||||
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${escapeHtml(status)}</span>
|
||||
<span class="preview-id">#${lt.escHtml(ticketId)}</span>
|
||||
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${lt.escHtml(status)}</span>
|
||||
</div>
|
||||
<div class="preview-title">${escapeHtml(title)}</div>
|
||||
<div class="preview-title">${lt.escHtml(title)}</div>
|
||||
<div class="preview-meta">
|
||||
<div><strong>Priority:</strong> P${escapeHtml(priority)}</div>
|
||||
<div><strong>Category:</strong> ${escapeHtml(category)}</div>
|
||||
<div><strong>Type:</strong> ${escapeHtml(type)}</div>
|
||||
<div><strong>Assigned:</strong> ${escapeHtml(assignedTo)}</div>
|
||||
<div><strong>Priority:</strong> P${lt.escHtml(priority)}</div>
|
||||
<div><strong>Category:</strong> ${lt.escHtml(category)}</div>
|
||||
<div><strong>Type:</strong> ${lt.escHtml(type)}</div>
|
||||
<div><strong>Assigned:</strong> ${lt.escHtml(assignedTo)}</div>
|
||||
</div>
|
||||
<div class="preview-footer">Created by ${escapeHtml(createdBy)}</div>
|
||||
<div class="preview-footer">Created by ${lt.escHtml(createdBy)}</div>
|
||||
`;
|
||||
|
||||
// Position the preview
|
||||
@@ -1697,7 +1699,7 @@ function exportSelectedTickets(format) {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (ticketIds.length === 0) {
|
||||
toast.warning('No tickets selected', 2000);
|
||||
lt.toast.warning('No tickets selected', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user