Files
plm-lottery/app/static/report-bug.html
T
davideandClaude Sonnet 5 ee4e845c89 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>
2026-07-31 15:50:40 +02:00

119 lines
4.4 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Segnala un bug — PLM Lottery</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="app-shell">
<h1>Segnala un bug</h1>
<p>Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.</p>
<div class="card" id="report-form">
<label for="bug-description">Cosa è successo?</label>
<textarea id="bug-description" rows="6" maxlength="5000" placeholder="Descrivi il bug: cosa stavi facendo, cosa ti aspettavi e cosa è successo invece."></textarea>
<label for="bug-contact">Contatto (opzionale)</label>
<input id="bug-contact" type="text" maxlength="256" placeholder="Email o altro recapito, se vuoi essere ricontattato">
<button onclick="submitBugReport()" id="bug-submit-btn">Invia segnalazione</button>
</div>
<div class="card hidden" id="my-reports-card">
<h2>Le tue segnalazioni</h2>
<p class="hint">Solo le segnalazioni inviate da questo account, con lo stato aggiornato dall'amministrazione.</p>
<div id="my-reports-list"></div>
</div>
<p><a class="link" href="/">&larr; Torna alla home</a></p>
</div>
<div id="toast-container" aria-live="polite"></div>
<script>
function toast(message, type) {
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = message;
container.appendChild(el);
setTimeout(() => el.remove(), 4000);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
const BUG_REPORT_STATUS_LABELS = { open: 'Da leggere', read: 'Letta', resolved: 'Risolta' };
async function submitBugReport() {
const btn = document.getElementById('bug-submit-btn');
const description = document.getElementById('bug-description').value.trim();
const contact = document.getElementById('bug-contact').value.trim();
if (!description) {
toast('Descrivi il bug prima di inviare.', 'error');
return;
}
const headers = { 'Content-Type': 'application/json' };
const token = localStorage.getItem('plm_token');
if (token) headers['Authorization'] = 'Bearer ' + token;
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'Invio…';
try {
const res = await fetch('/bug-reports', {
method: 'POST',
headers,
body: JSON.stringify({ description, contact: contact || null }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail?.message || data.detail || res.statusText);
document.getElementById('bug-description').value = '';
document.getElementById('bug-contact').value = '';
toast('Grazie! Segnalazione inviata.', 'success');
loadMyBugReports();
} catch (e) {
toast('Errore nell\'invio: ' + e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = original;
}
}
async function loadMyBugReports() {
const token = localStorage.getItem('plm_token');
const card = document.getElementById('my-reports-card');
if (!token) {
card.classList.add('hidden');
return;
}
try {
const res = await fetch('/bug-reports/mine', { headers: { Authorization: 'Bearer ' + token } });
if (!res.ok) {
card.classList.add('hidden');
return;
}
const reports = await res.json();
card.classList.remove('hidden');
const list = document.getElementById('my-reports-list');
list.innerHTML = reports.map((r) => `
<div class="my-report-row">
<span class="badge bug-status-${escapeHtml(r.status)}">${escapeHtml(BUG_REPORT_STATUS_LABELS[r.status] || r.status)}</span>
<span class="my-report-desc">${escapeHtml(r.description)}</span>
<span class="my-report-date">${new Date(r.created_at).toLocaleString('it-IT')}</span>
</div>
`).join('') || '<p class="hint">Non hai ancora inviato segnalazioni.</p>';
} catch (e) {
card.classList.add('hidden');
}
}
loadMyBugReports();
</script>
</body>
</html>