Add user bug reporting with admin triage (open/read/resolved)

Turns the /report-bug placeholder into a real form (POST /bug-reports,
optionally attributed to the logged-in user) and adds a "Segnalazioni bug"
section to /admin to view and triage them. A logged-in reporter can also
check their own report's status via GET /bug-reports/mine, since anonymous
submissions have no user to show a history to.

Status is a three-state lifecycle (open -> read -> resolved) rather than a
plain boolean, so an admin can acknowledge a report distinctly from actually
fixing it. The schema went through two migrations because the first one
(add bug_reports table) had already been applied against the running
instance with a `resolved` boolean before the three-state design was
decided, so a follow-up migration backfills it into `status` instead of
rewriting already-applied history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:50:40 +02:00
co-authored by Claude Sonnet 5
parent 977bb762c7
commit ee4e845c89
12 changed files with 628 additions and 8 deletions
+46 -3
View File
@@ -89,8 +89,8 @@ function stopChainStatusPolling() {
chainStatusInterval = null;
}
const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit'];
const VIEW_LOADERS = { utenti: loadUsers, round: loadRounds, pending: loadPending, audit: loadAuditLog };
const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit', 'bugreports'];
const VIEW_LOADERS = { utenti: loadUsers, round: loadRounds, pending: loadPending, audit: loadAuditLog, bugreports: loadBugReports };
let currentAdminView = 'parametri';
function switchView(name) {
@@ -109,7 +109,7 @@ function showDashboard() {
}
async function loadDashboard() {
await Promise.all([adminLoadConfig(), loadUsers(), loadRounds(), loadPending(), loadAuditLog()]);
await Promise.all([adminLoadConfig(), loadUsers(), loadRounds(), loadPending(), loadAuditLog(), loadBugReports()]);
}
async function adminLogin() {
@@ -346,6 +346,49 @@ async function loadAuditLog() {
}
}
const BUG_REPORT_STATUS_LABELS = { open: 'Da leggere', read: 'Letta', resolved: 'Risolta' };
function bugReportBadge(status) {
return `<span class="badge bug-status-${escapeHtml(status)}">${escapeHtml(BUG_REPORT_STATUS_LABELS[status] || status)}</span>`;
}
async function loadBugReports() {
try {
const reports = await callAdmin('GET', '/admin/bug-reports');
const tbody = document.getElementById('bugreports-tbody');
tbody.innerHTML = reports.map((r) => `
<tr>
<td>${r.id}</td>
<td class="payload-cell">${escapeHtml(r.description)}</td>
<td>${r.contact ? escapeHtml(r.contact) : '—'}</td>
<td>${r.username ? escapeHtml(r.username) : '—'}</td>
<td>${fmtDate(r.created_at)}</td>
<td>
${bugReportBadge(r.status)}
<div class="bug-actions">
${r.status === 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'read', this)">Segna come letta</button>` : ''}
${r.status !== 'resolved' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'resolved', this)">Segna come risolta</button>` : ''}
${r.status !== 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'open', this)">Riapri</button>` : ''}
</div>
</td>
</tr>
`).join('') || '<tr><td colspan="6" class="hint">Nessuna segnalazione ricevuta.</td></tr>';
} catch (e) {
toast('Errore nel caricamento segnalazioni: ' + e.message, 'error');
}
}
async function setBugReportStatus(reportId, newStatus, button) {
await withLoading(button, '…', async () => {
try {
await callAdmin('POST', '/admin/bug-reports/' + reportId + '/status', { status: newStatus });
await loadBugReports();
} catch (e) {
toast('Errore: ' + e.message, 'error');
}
});
}
document.getElementById('admin-token').addEventListener('keydown', (e) => {
if (e.key === 'Enter') adminLogin();
});