Improve deployment safety and chat reliability
Deploy FluentGerman.ai / deploy (push) Successful in 1m25s

This commit is contained in:
2026-08-29 17:28:50 +02:00
parent e9f12bc2ba
commit d974aaedc8
15 changed files with 746 additions and 153 deletions
+53 -16
View File
@@ -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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[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);
});