Compare commits

...

7 Commits

Author SHA1 Message Date
191a368258 fix: prevent XSS in addChatMessage by using textContent for user input
- Escape sender name via escapeHtml in innerHTML template
- Set message content via textContent instead of innerHTML injection
- Prevents HTML/script injection from user input or LLM responses
2026-02-28 23:32:28 +02:00
7a10206617 feat: modal UX - close on Escape key and backdrop click, add ARIA attributes
- Escape key closes any open memory modal
- Clicking the dark backdrop behind a modal closes it
- Add role=dialog, aria-modal, aria-label for accessibility
2026-02-28 23:31:28 +02:00
8b96f4dc8a cleanup: remove duplicate escapeHtml function, add null check to remaining one 2026-02-28 23:30:05 +02:00
4666986f78 cleanup: remove ~70 lines of duplicate CSS for conversation view styles
First block of conversation-view, conversations-list, conversation-message,
message-header, sender, timestamp, message-content, message-attachments was
silently overridden by identical selectors defined later. Kept the unique
reaction/delete-button styles.
2026-02-28 23:29:15 +02:00
5e002004cc fix: notification system - timer race condition, success color, z-index above modals
- Cancel previous timer before starting new one (prevents early dismissal)
- Add green background for type='success' notifications
- Bump z-index from 1000 to 3000 so notifications show above modals
- Add fade-out transition for smoother dismissal
2026-02-28 23:28:30 +02:00
d3fb0eacb6 fix: updateBedtimeRange variable scoping - originalText accessible in finally block 2026-02-28 23:26:02 +02:00
7bcb670b96 perf: pause polling intervals when browser tab is hidden
- Replace raw setInterval with startPolling/stopPolling functions
- Add visibilitychange listener to pause when tab is hidden
- Immediately refresh data when tab becomes visible again
- Saves bandwidth and CPU when the dashboard is in background
2026-02-28 23:25:07 +02:00

View File

@@ -89,8 +89,9 @@
border-radius: 8px; border-radius: 8px;
opacity: 0.95; opacity: 0.95;
display: none; display: none;
z-index: 1000; z-index: 3000;
font-size: 0.9rem; font-size: 0.9rem;
transition: opacity 0.3s ease;
} }
.server-card { .server-card {
@@ -241,69 +242,6 @@
} }
/* Conversation View Styles */ /* Conversation View Styles */
.conversation-view {
max-width: 800px;
margin: 0 auto;
}
.conversations-list {
max-height: 600px;
overflow-y: auto;
border: 1px solid #444;
border-radius: 8px;
padding: 1rem;
background: #222;
}
.conversation-message {
margin-bottom: 1rem;
padding: 0.75rem;
border-radius: 8px;
border-left: 4px solid;
}
.conversation-message.user-message {
background: #2a2a3a;
border-left-color: #4CAF50;
}
.conversation-message.bot-message {
background: #3a2a2a;
border-left-color: #2196F3;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.sender {
font-weight: bold;
color: #fff;
}
.timestamp {
color: #999;
font-size: 0.8rem;
}
.message-content {
color: #ddd;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-attachments {
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255,255,255,0.05);
border-radius: 4px;
font-size: 0.9rem;
}
.message-reactions { .message-reactions {
margin-top: 0.5rem; margin-top: 0.5rem;
display: flex; display: flex;
@@ -1908,21 +1846,59 @@ document.addEventListener('DOMContentLoaded', function() {
}, { passive: false }); }, { passive: false });
} }
// Set up periodic updates // Set up periodic updates with visibility-aware polling
setInterval(loadStatus, 10000); let statusInterval, logsInterval, argsInterval;
setInterval(loadLogs, 5000);
setInterval(loadActiveArguments, 5000); // Refresh active arguments function startPolling() {
if (!statusInterval) statusInterval = setInterval(loadStatus, 10000);
if (!logsInterval) logsInterval = setInterval(loadLogs, 5000);
if (!argsInterval) argsInterval = setInterval(loadActiveArguments, 5000);
}
function stopPolling() {
clearInterval(statusInterval); statusInterval = null;
clearInterval(logsInterval); logsInterval = null;
clearInterval(argsInterval); argsInterval = null;
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
console.log('⏸ Tab hidden — polling paused');
} else {
loadStatus(); loadLogs(); loadActiveArguments();
startPolling();
console.log('▶️ Tab visible — polling resumed');
}
});
startPolling();
}); });
// Utility functions // Utility functions
let notificationTimer = null;
function showNotification(message, type = 'info') { function showNotification(message, type = 'info') {
const notification = document.getElementById('notification'); const notification = document.getElementById('notification');
notification.textContent = message; notification.textContent = message;
notification.style.display = 'block'; notification.style.display = 'block';
notification.style.backgroundColor = type === 'error' ? '#d32f2f' : '#222'; notification.style.opacity = '0.95';
if (type === 'error') {
notification.style.backgroundColor = '#d32f2f';
} else if (type === 'success') {
notification.style.backgroundColor = '#2e7d32';
} else {
notification.style.backgroundColor = '#222';
}
if (notificationTimer) clearTimeout(notificationTimer);
notificationTimer = setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => { setTimeout(() => {
notification.style.display = 'none'; notification.style.display = 'none';
notificationTimer = null;
}, 300);
}, 3000); }, 3000);
} }
@@ -2521,12 +2497,15 @@ async function updateBedtimeRange(guildId) {
const [startHour, startMinute] = startTime.split(':').map(Number); const [startHour, startMinute] = startTime.split(':').map(Number);
const [endHour, endMinute] = endTime.split(':').map(Number); const [endHour, endMinute] = endTime.split(':').map(Number);
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
const originalText = button ? button.textContent : 'Update Bedtime Range';
try { try {
// Show loading state // Show loading state
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`); if (button) {
const originalText = button.textContent;
button.textContent = 'Updating...'; button.textContent = 'Updating...';
button.disabled = true; button.disabled = true;
}
// Send the update request // Send the update request
const response = await fetch(`/servers/${guildIdStr}/bedtime-range`, { const response = await fetch(`/servers/${guildIdStr}/bedtime-range`, {
@@ -2556,9 +2535,8 @@ async function updateBedtimeRange(guildId) {
} catch (error) { } catch (error) {
console.error('Failed to update bedtime range:', error); console.error('Failed to update bedtime range:', error);
showNotification(error.message || 'Failed to update bedtime range', 'error'); showNotification(error.message || 'Failed to update bedtime range', 'error');
} finally {
// Restore button state // Restore button state
const button = document.querySelector(`button[onclick="updateBedtimeRange('${guildIdStr}')"]`);
if (button) { if (button) {
button.textContent = originalText; button.textContent = originalText;
button.disabled = false; button.disabled = false;
@@ -4064,6 +4042,7 @@ function scrollLogsToBottom() {
} }
function escapeHtml(text) { function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
@@ -5045,12 +5024,15 @@ function addChatMessage(sender, content, isError = false) {
messageDiv.innerHTML = ` messageDiv.innerHTML = `
<div class="chat-message-header"> <div class="chat-message-header">
<span class="chat-message-sender">${sender}</span> <span class="chat-message-sender">${escapeHtml(sender)}</span>
<span class="chat-message-time">${timestamp}</span> <span class="chat-message-time">${timestamp}</span>
</div> </div>
<div class="chat-message-content">${content}</div> <div class="chat-message-content"></div>
`; `;
// Set content via textContent to prevent XSS
messageDiv.querySelector('.chat-message-content').textContent = content;
chatMessages.appendChild(messageDiv); chatMessages.appendChild(messageDiv);
// Scroll to bottom // Scroll to bottom
@@ -5863,6 +5845,37 @@ function closeCreateMemoryModal() {
document.getElementById('create-memory-modal').style.display = 'none'; document.getElementById('create-memory-modal').style.display = 'none';
} }
// Modal keyboard and backdrop close handlers
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const editModal = document.getElementById('edit-memory-modal');
const createModal = document.getElementById('create-memory-modal');
if (editModal && editModal.style.display !== 'none') closeEditMemoryModal();
if (createModal && createModal.style.display !== 'none') closeCreateMemoryModal();
}
});
document.addEventListener('DOMContentLoaded', function() {
const editModal = document.getElementById('edit-memory-modal');
const createModal = document.getElementById('create-memory-modal');
if (editModal) {
editModal.setAttribute('role', 'dialog');
editModal.setAttribute('aria-modal', 'true');
editModal.setAttribute('aria-label', 'Edit Memory');
editModal.addEventListener('click', function(e) {
if (e.target === this) closeEditMemoryModal();
});
}
if (createModal) {
createModal.setAttribute('role', 'dialog');
createModal.setAttribute('aria-modal', 'true');
createModal.setAttribute('aria-label', 'Create Memory');
createModal.addEventListener('click', function(e) {
if (e.target === this) closeCreateMemoryModal();
});
}
});
async function saveNewMemory() { async function saveNewMemory() {
const collection = document.getElementById('create-memory-collection').value; const collection = document.getElementById('create-memory-collection').value;
const content = document.getElementById('create-memory-content').value.trim(); const content = document.getElementById('create-memory-content').value.trim();
@@ -5930,13 +5943,6 @@ function filterMemories(listId, searchTerm) {
}); });
} }
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeJsonForAttribute(obj) { function escapeJsonForAttribute(obj) {
// Properly escape JSON for use in HTML attributes // Properly escape JSON for use in HTML attributes
return JSON.stringify(obj) return JSON.stringify(obj)