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:
@@ -77,7 +77,7 @@ function saveTicket() {
|
||||
statusDisplay.className = `status-${data.status}`;
|
||||
statusDisplay.textContent = data.status;
|
||||
}
|
||||
toast.success('Ticket updated successfully');
|
||||
lt.toast.success('Ticket updated successfully');
|
||||
} else {
|
||||
}
|
||||
})
|
||||
@@ -317,11 +317,11 @@ function handleAssignmentChange() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
toast.error('Error updating assignment');
|
||||
lt.toast.error('Error updating assignment');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error updating assignment: ' + error.message);
|
||||
lt.toast.error('Error updating assignment: ' + error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -353,7 +353,7 @@ function handleMetadataChanges() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
toast.error(`Error updating ${fieldName}`);
|
||||
lt.toast.error(`Error updating ${fieldName}`);
|
||||
} else {
|
||||
// Update window.ticketData
|
||||
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
|
||||
@@ -375,7 +375,7 @@ function handleMetadataChanges() {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error(`Error updating ${fieldName}: ` + error.message);
|
||||
lt.toast.error(`Error updating ${fieldName}: ` + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -492,13 +492,13 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} else {
|
||||
toast.error('Error updating status: ' + (data.error || 'Unknown error'));
|
||||
lt.toast.error('Error updating status: ' + (data.error || 'Unknown error'));
|
||||
// Reset to current status
|
||||
statusSelect.selectedIndex = 0;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error updating status: ' + error.message);
|
||||
lt.toast.error('Error updating status: ' + error.message);
|
||||
// Reset to current status
|
||||
statusSelect.selectedIndex = 0;
|
||||
});
|
||||
@@ -587,10 +587,10 @@ function showDependencyError(message) {
|
||||
const dependentsList = document.getElementById('dependentsList');
|
||||
|
||||
if (dependenciesList) {
|
||||
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
|
||||
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||
}
|
||||
if (dependentsList) {
|
||||
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
|
||||
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,11 +618,11 @@ function renderDependencies(dependencies) {
|
||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
||||
<div>
|
||||
<a href="/ticket/${escapeHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
|
||||
#${escapeHtml(dep.depends_on_id)}
|
||||
<a href="/ticket/${lt.escHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
|
||||
#${lt.escHtml(dep.depends_on_id)}
|
||||
</a>
|
||||
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
|
||||
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
||||
</div>
|
||||
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
|
||||
</div>`;
|
||||
@@ -653,12 +653,12 @@ function renderDependents(dependents) {
|
||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
||||
<div>
|
||||
<a href="/ticket/${escapeHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
||||
#${escapeHtml(dep.ticket_id)}
|
||||
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
||||
#${lt.escHtml(dep.ticket_id)}
|
||||
</a>
|
||||
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
|
||||
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${escapeHtml(dep.dependency_type)})</span>
|
||||
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
||||
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${lt.escHtml(dep.dependency_type)})</span>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
@@ -672,7 +672,7 @@ function addDependency() {
|
||||
const dependencyType = document.getElementById('dependencyType').value;
|
||||
|
||||
if (!dependsOnId) {
|
||||
toast.warning('Please enter a ticket ID', 3000);
|
||||
lt.toast.warning('Please enter a ticket ID', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -692,15 +692,15 @@ function addDependency() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
toast.success('Dependency added', 3000);
|
||||
lt.toast.success('Dependency added', 3000);
|
||||
document.getElementById('dependencyTicketId').value = '';
|
||||
loadDependencies();
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error adding dependency', 4000);
|
||||
lt.toast.error('Error adding dependency', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -723,14 +723,14 @@ function removeDependency(dependencyId) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
toast.success('Dependency removed', 3000);
|
||||
lt.toast.success('Dependency removed', 3000);
|
||||
loadDependencies();
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error removing dependency', 4000);
|
||||
lt.toast.error('Error removing dependency', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -828,18 +828,18 @@ function handleFileUpload(files) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
if (uploadedCount === totalFiles) {
|
||||
toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
|
||||
lt.toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
|
||||
loadAttachments();
|
||||
resetUploadUI();
|
||||
}
|
||||
} else {
|
||||
toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
|
||||
lt.toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`Error parsing response for ${file.name}`, 4000);
|
||||
lt.toast.error(`Error parsing response for ${file.name}`, 4000);
|
||||
}
|
||||
} else {
|
||||
toast.error(`Error uploading ${file.name}: Server error`, 4000);
|
||||
lt.toast.error(`Error uploading ${file.name}: Server error`, 4000);
|
||||
}
|
||||
|
||||
if (uploadedCount === totalFiles) {
|
||||
@@ -849,7 +849,7 @@ function handleFileUpload(files) {
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
uploadedCount++;
|
||||
toast.error(`Error uploading ${file.name}: Network error`, 4000);
|
||||
lt.toast.error(`Error uploading ${file.name}: Network error`, 4000);
|
||||
if (uploadedCount === totalFiles) {
|
||||
setTimeout(resetUploadUI, 2000);
|
||||
}
|
||||
@@ -914,15 +914,15 @@ function renderAttachments(attachments) {
|
||||
});
|
||||
|
||||
html += `<div class="attachment-item" data-id="${att.attachment_id}">
|
||||
<div class="attachment-icon">${escapeHtml(att.icon || '📎')}</div>
|
||||
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-name" title="${escapeHtml(att.original_filename)}">
|
||||
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
|
||||
${escapeHtml(att.original_filename)}
|
||||
${lt.escHtml(att.original_filename)}
|
||||
</a>
|
||||
</div>
|
||||
<div class="attachment-meta">
|
||||
${escapeHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${escapeHtml(uploaderName)} • ${escapeHtml(uploadDate)}
|
||||
${lt.escHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${lt.escHtml(uploaderName)} • ${lt.escHtml(uploadDate)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-actions">
|
||||
@@ -968,14 +968,14 @@ function deleteAttachment(attachmentId) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
toast.success('Attachment deleted', 3000);
|
||||
lt.toast.success('Attachment deleted', 3000);
|
||||
loadAttachments();
|
||||
} else {
|
||||
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
toast.error('Error deleting attachment', 4000);
|
||||
lt.toast.error('Error deleting attachment', 4000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1118,9 +1118,9 @@ function showMentionSuggestions(query, textarea) {
|
||||
let html = '';
|
||||
filtered.forEach((user, index) => {
|
||||
const isSelected = index === 0 ? 'selected' : '';
|
||||
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" data-action="select-mention">
|
||||
<span class="mention-username">@${escapeHtml(user.username)}</span>
|
||||
${user.display_name ? `<span class="mention-displayname">${escapeHtml(user.display_name)}</span>` : ''}
|
||||
html += `<div class="mention-option ${isSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
||||
<span class="mention-username">@${lt.escHtml(user.username)}</span>
|
||||
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
@@ -1252,7 +1252,7 @@ function editComment(commentId) {
|
||||
editForm.className = 'comment-edit-form';
|
||||
editForm.id = `comment-edit-form-${commentId}`;
|
||||
editForm.innerHTML = `
|
||||
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${escapeHtml(originalText)}</textarea>
|
||||
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${lt.escHtml(originalText)}</textarea>
|
||||
<div class="comment-edit-controls">
|
||||
<label class="markdown-toggle-small">
|
||||
<input type="checkbox" id="comment-edit-markdown-${commentId}" ${markdownEnabled ? 'checked' : ''}>
|
||||
@@ -1331,7 +1331,7 @@ function saveEditComment(commentId) {
|
||||
} else {
|
||||
textDiv.removeAttribute('data-markdown');
|
||||
// Convert newlines to <br> and highlight mentions
|
||||
let displayText = escapeHtml(newText).replace(/\n/g, '<br>');
|
||||
let displayText = lt.escHtml(newText).replace(/\n/g, '<br>');
|
||||
displayText = highlightMentions(displayText);
|
||||
// Auto-link URLs
|
||||
if (typeof autoLinkUrls === 'function') {
|
||||
@@ -1543,8 +1543,8 @@ function submitReply(parentCommentId) {
|
||||
<span class="comment-date">${data.created_at}</span>
|
||||
<div class="comment-actions">
|
||||
${newDepth < 3 ? `<button type="button" class="comment-action-btn reply-btn" data-action="reply-comment" data-comment-id="${data.comment_id}" data-user="${data.user_name}" title="Reply">↩</button>` : ''}
|
||||
<button type="button" class="comment-action-btn edit-btn" data-action="edit-comment" data-comment-id="${data.comment_id}" title="Edit">✏️</button>
|
||||
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">🗑️</button>
|
||||
<button type="button" class="comment-action-btn edit-btn" data-action="edit-comment" data-comment-id="${data.comment_id}" title="Edit">[ EDIT ]</button>
|
||||
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">[ DEL ]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-text" id="comment-text-${data.comment_id}" ${isMarkdownEnabled ? 'data-markdown' : ''}>
|
||||
|
||||
Reference in New Issue
Block a user