Improve deployment safety and chat reliability
Deploy FluentGerman.ai / deploy (push) Successful in 1m25s
Deploy FluentGerman.ai / deploy (push) Successful in 1m25s
This commit is contained in:
+53
-16
@@ -6,6 +6,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const user = getUser();
|
||||
document.getElementById('admin-name').textContent = user?.username || 'Admin';
|
||||
|
||||
/** Escape text destined for innerHTML or an HTML attribute. */
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, c => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]));
|
||||
}
|
||||
|
||||
// id → username, so instructions can show who they belong to
|
||||
let usersById = new Map();
|
||||
|
||||
// Tab switching
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const panels = document.querySelectorAll('.tab-panel');
|
||||
@@ -16,6 +26,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
panels.forEach(p => p.classList.add('hidden'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById(tab.dataset.panel).classList.remove('hidden');
|
||||
|
||||
// Clicking the tab itself clears any per-client filter set via 📝
|
||||
if (tab.dataset.panel === 'instructions-panel' && currentFilterUserId !== null) {
|
||||
clearInstructionFilter();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +43,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const users = await apiJSON('/users/');
|
||||
usersById = new Map(users.map(u => [u.id, u.username]));
|
||||
usersBody.innerHTML = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
@@ -38,13 +54,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
users.forEach(u => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${u.username}</td>
|
||||
<td class="hide-mobile">${u.email}</td>
|
||||
<td>${escapeHtml(u.username)}</td>
|
||||
<td class="hide-mobile">${escapeHtml(u.email)}</td>
|
||||
<td><span class="badge ${u.is_active ? 'badge-personal' : 'badge-homework'}">${u.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td class="hide-mobile">${new Date(u.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick="editUser(${u.id})">Edit</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="manageInstructions(${u.id}, '${u.username}')">📝</button>
|
||||
<button class="btn btn-sm btn-secondary" data-username="${escapeHtml(u.username)}"
|
||||
onclick="manageInstructions(${u.id}, this.dataset.username)">📝</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser(${u.id})">✕</button>
|
||||
</td>
|
||||
`;
|
||||
@@ -120,7 +137,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.deleteUser = async (id) => {
|
||||
if (!confirm('Delete this client? This will also remove all their instructions.')) return;
|
||||
try {
|
||||
await api(`/users/${id}`, { method: 'DELETE' });
|
||||
await apiVoid(`/users/${id}`, { method: 'DELETE' });
|
||||
showToast('Client deleted');
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
@@ -150,10 +167,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
instructions.forEach(inst => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${inst.title}</td>
|
||||
<td>${escapeHtml(inst.title)}</td>
|
||||
<td><span class="badge badge-${inst.type}">${inst.type}</span></td>
|
||||
<td class="hide-mobile">${inst.user_id || 'Global'}</td>
|
||||
<td class="hide-mobile">${inst.content.substring(0, 60)}${inst.content.length > 60 ? '...' : ''}</td>
|
||||
<td class="hide-mobile">${inst.user_id ? escapeHtml(usersById.get(inst.user_id) || `#${inst.user_id}`) : 'Global'}</td>
|
||||
<td class="hide-mobile">${escapeHtml(inst.content.substring(0, 60))}${inst.content.length > 60 ? '...' : ''}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick="editInstruction(${inst.id})">Edit</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteInstruction(${inst.id})">✕</button>
|
||||
@@ -171,21 +188,39 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const users = await apiJSON('/users/');
|
||||
instrUserSelect.innerHTML = '<option value="">Global (all clients)</option>';
|
||||
users.forEach(u => {
|
||||
instrUserSelect.innerHTML += `<option value="${u.id}">${u.username}</option>`;
|
||||
instrUserSelect.innerHTML += `<option value="${u.id}">${escapeHtml(u.username)}</option>`;
|
||||
});
|
||||
} catch (e) {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
window.manageInstructions = (userId, username) => {
|
||||
function applyInstructionFilter(userId, username) {
|
||||
currentFilterUserId = userId;
|
||||
|
||||
const heading = document.getElementById('instructions-heading');
|
||||
const clearBtn = document.getElementById('instr-filter-clear');
|
||||
|
||||
if (userId === null) {
|
||||
heading.textContent = 'Instructions';
|
||||
clearBtn.classList.add('hidden');
|
||||
} else {
|
||||
heading.textContent = `Instructions — ${username}`;
|
||||
clearBtn.classList.remove('hidden');
|
||||
}
|
||||
|
||||
loadInstructions(userId);
|
||||
}
|
||||
|
||||
window.clearInstructionFilter = () => applyInstructionFilter(null, null);
|
||||
|
||||
window.manageInstructions = (userId, username) => {
|
||||
// Switch to instructions tab
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
panels.forEach(p => p.classList.add('hidden'));
|
||||
document.querySelector('[data-panel="instructions-panel"]').classList.add('active');
|
||||
document.getElementById('instructions-panel').classList.remove('hidden');
|
||||
loadInstructions(userId);
|
||||
applyInstructionFilter(userId, username);
|
||||
};
|
||||
|
||||
window.showInstructionModal = () => {
|
||||
@@ -253,7 +288,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.deleteInstruction = async (id) => {
|
||||
if (!confirm('Delete this instruction?')) return;
|
||||
try {
|
||||
await api(`/instructions/${id}`, { method: 'DELETE' });
|
||||
await apiVoid(`/instructions/${id}`, { method: 'DELETE' });
|
||||
showToast('Instruction deleted');
|
||||
loadInstructions(currentFilterUserId);
|
||||
} catch (e) {
|
||||
@@ -271,9 +306,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!file) return;
|
||||
|
||||
const text = await file.text();
|
||||
// Open the modal first: showInstructionModal() calls form.reset(),
|
||||
// which would wipe anything prefilled before it.
|
||||
showInstructionModal();
|
||||
document.getElementById('instr-title').value = file.name.replace(/\.[^.]+$/, '');
|
||||
document.getElementById('instr-content').value = text;
|
||||
showInstructionModal();
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
@@ -328,14 +365,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const content = voiceGenText.value.trim();
|
||||
if (!content) return;
|
||||
|
||||
loadUserOptions();
|
||||
// Open the modal first: showInstructionModal() calls form.reset() and
|
||||
// loadUserOptions() itself, and would wipe anything prefilled before it.
|
||||
showInstructionModal();
|
||||
document.getElementById('instr-content').value = content;
|
||||
document.getElementById('instr-title').value = 'Voice Generated Instruction';
|
||||
showInstructionModal();
|
||||
};
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────
|
||||
loadUsers();
|
||||
loadInstructions();
|
||||
loadUsers().then(() => loadInstructions());
|
||||
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user