feat: in-app bug report form (replaces GitHub link)

This commit is contained in:
dadaloop82
2026-05-16 13:25:51 +00:00
parent 8a596cb7d8
commit 5f4c29bd5a
7 changed files with 259 additions and 37 deletions
+84
View File
@@ -438,6 +438,10 @@ try {
reportError();
break;
case 'report_bug':
reportBugManual();
break;
case 'check_update':
checkUpdate();
break;
@@ -6897,6 +6901,86 @@ function reportError(): void {
echo json_encode(['ok' => true]);
}
/**
* POST /api/?action=report_bug
*
* Manual bug/feature/question report submitted by the user via the in-app form.
* Creates a GitHub issue directly with the provided title and description.
*
* Expected JSON body:
* type string 'bug'|'feature'|'question'
* title string Issue title (required, max 150 chars)
* description string Main description (required, max 3000 chars)
* steps string? Steps to reproduce (optional, max 2000 chars)
* lang string? UI language the user is running
* url string? Page URL
* user_agent string? Navigator UA
* version string? App version
*/
function reportBugManual(): void {
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$allowedTypes = ['bug', 'feature', 'question'];
$type = in_array($input['type'] ?? '', $allowedTypes, true) ? $input['type'] : 'bug';
$title = substr(trim($input['title'] ?? ''), 0, 150);
$desc = substr(trim($input['description'] ?? ''), 0, 3000);
$steps = substr(trim($input['steps'] ?? ''), 0, 2000);
$ua = substr(trim($input['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? '')), 0, 300);
$url = substr(trim($input['url'] ?? ''), 0, 300);
$ver = substr(trim($input['version'] ?? ''), 0, 50);
$lang = preg_replace('/[^a-z\-]/', '', strtolower($input['lang'] ?? 'it'));
if (empty($title) || empty($desc)) {
echo json_encode(['ok' => false, 'error' => 'title and description required']);
return;
}
$token = _ghToken();
if (!$token) {
// No GitHub token configured — log locally and return ok so the UX is not broken
_appendErrorLog('pwa', 'manual_report', $title, $desc, $url, $ua, ['type' => $type, 'version' => $ver, 'lang' => $lang]);
echo json_encode(['ok' => true, 'issue' => null]);
return;
}
// Labels: always 'user-report' + type-specific label
$labelMap = [
'bug' => ['bug', 'user-report'],
'feature' => ['enhancement', 'user-report'],
'question' => ['question', 'user-report'],
];
$labels = $labelMap[$type];
$typeEmoji = ['bug' => '🐛', 'feature' => '💡', 'question' => '❓'][$type];
$ts = date('Y-m-d H:i:s T');
$body = "## {$typeEmoji} User Report\n\n";
$body .= "**Description:**\n{$desc}\n\n";
if ($steps) {
$body .= "**Steps to reproduce:**\n{$steps}\n\n";
}
$body .= "---\n";
$body .= "**Version:** `{$ver}` \n";
$body .= "**Language:** `{$lang}` \n";
if ($url) $body .= "**URL:** `{$url}` \n";
if ($ua) $body .= "**User-Agent:** `{$ua}` \n";
$body .= "**Reported at:** {$ts}\n\n";
$body .= "_This issue was submitted via the in-app bug report form._";
$res = _githubRequest($token, 'POST',
'https://api.github.com/repos/' . GH_REPO . '/issues',
['title' => $title, 'body' => $body, 'labels' => $labels]
);
$issueNum = $res['body']['number'] ?? null;
$issueUrl = $res['body']['html_url'] ?? null;
if ($issueNum) {
echo json_encode(['ok' => true, 'issue' => $issueNum, 'url' => $issueUrl]);
} else {
echo json_encode(['ok' => false, 'error' => 'github_api_error']);
}
}
/**
* Append to data/error_reports.log (local safety net, max 500 KB)
*/
+30
View File
@@ -3103,6 +3103,36 @@ body.server-offline .bottom-nav {
justify-content: center;
}
/* ── Bug report form ── */
.bug-type-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bug-type-pill {
flex: 1;
min-width: 80px;
padding: 7px 10px;
border: 1.5px solid #cbd5e1;
border-radius: 20px;
background: #fff;
color: #475569;
font-size: 0.88rem;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
-webkit-tap-highlight-color: transparent;
}
.bug-type-pill.active {
border-color: var(--primary, #2d5016);
background: var(--primary, #2d5016);
color: #fff;
font-weight: 600;
}
.bug-type-pill:not(.active):hover {
border-color: var(--primary, #2d5016);
color: var(--primary, #2d5016);
}
.modal-detail {
display: flex;
flex-direction: column;
+104 -31
View File
@@ -2100,51 +2100,124 @@ async function _loadAboutSection() {
* Manually triggered bug report from the About section in Settings.
* Collects basic info and submits via the existing report_error endpoint.
*/
async function reportBugManual() {
const btn = document.getElementById('btn-report-bug');
const statusEl = document.getElementById('report-bug-status');
if (!btn || !statusEl) return;
function reportBugManual() {
const mc = document.getElementById('modal-content');
if (!mc) return;
btn.disabled = true;
mc.innerHTML = `
<div class="modal-header">
<h3>${t('about.report_bug_modal_title')}</h3>
<button class="modal-close" onclick="closeModal()"></button>
</div>
<div style="padding:16px 20px 20px">
<div style="margin-bottom:14px">
<div class="bug-type-pills">
<button type="button" class="bug-type-pill active" data-btype="bug">🐛 ${t('about.report_type_bug')}</button>
<button type="button" class="bug-type-pill" data-btype="feature">💡 ${t('about.report_type_feature')}</button>
<button type="button" class="bug-type-pill" data-btype="question"> ${t('about.report_type_question')}</button>
</div>
</div>
<div style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_title')} *</label>
<input type="text" id="bug-form-title" maxlength="150" autocomplete="off"
placeholder="${t('about.report_field_title_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;background:#fff;color:#1e293b;outline:none">
</div>
<div style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_desc')} *</label>
<textarea id="bug-form-desc" rows="4" maxlength="3000"
placeholder="${t('about.report_field_desc_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;resize:vertical;background:#fff;color:#1e293b;outline:none;font-family:inherit"></textarea>
</div>
<div id="bug-form-steps-group" style="margin-bottom:12px">
<label class="settings-label" style="display:block;margin-bottom:4px">${t('about.report_field_steps')}</label>
<textarea id="bug-form-steps" rows="3" maxlength="2000"
placeholder="${t('about.report_field_steps_ph')}"
style="width:100%;box-sizing:border-box;padding:9px 11px;border:1.5px solid #cbd5e1;border-radius:8px;font-size:0.95rem;resize:vertical;background:#fff;color:#1e293b;outline:none;font-family:inherit"></textarea>
</div>
<p class="settings-hint" style="margin:0 0 14px;font-size:0.78rem">
${t('about.report_auto_info').replace('{version}', _loadedVersion || '—').replace('{lang}', _currentLang || '—')}
</p>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button type="button" class="btn btn-secondary" onclick="closeModal()">${t('common.cancel') || 'Annulla'}</button>
<button type="button" class="btn btn-primary" id="bug-form-submit" onclick="_submitBugReport()">
${t('about.report_send_btn')}
</button>
</div>
<div id="bug-form-status" style="display:none;margin-top:10px;text-align:center;font-size:0.88rem;padding:8px;border-radius:6px"></div>
</div>
`;
document.getElementById('modal-overlay').style.display = 'flex';
// Pill click: switch type, show/hide steps field
mc.querySelectorAll('.bug-type-pill').forEach(pill => {
pill.addEventListener('click', () => {
mc.querySelectorAll('.bug-type-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
const stepsGroup = document.getElementById('bug-form-steps-group');
if (stepsGroup) stepsGroup.style.display = (pill.dataset.btype === 'bug') ? '' : 'none';
});
});
}
async function _submitBugReport() {
const submitBtn = document.getElementById('bug-form-submit');
const statusEl = document.getElementById('bug-form-status');
const titleEl = document.getElementById('bug-form-title');
const descEl = document.getElementById('bug-form-desc');
const stepsEl = document.getElementById('bug-form-steps');
const activePill = document.querySelector('#modal-content .bug-type-pill.active');
const title = titleEl?.value.trim() || '';
const desc = descEl?.value.trim() || '';
const steps = stepsEl?.value.trim() || '';
const type = activePill?.dataset.btype || 'bug';
// Inline validation
if (!title) {
titleEl.style.borderColor = '#dc2626';
titleEl.focus();
return;
}
if (!desc) {
descEl.style.borderColor = '#dc2626';
descEl.focus();
return;
}
submitBtn.disabled = true;
statusEl.style.display = '';
statusEl.style.background = '#f1f5f9';
statusEl.style.color = '#64748b';
statusEl.textContent = t('about.report_bug_sending');
const manifest = await fetch('manifest.json?_=' + Date.now()).then(r => r.json()).catch(() => ({}));
try {
const res = await fetch(API_BASE + '?action=report_error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: 'pwa',
type: 'manual_report',
message: 'Manual bug report submitted from Settings → About',
stack: '',
url: location.href,
const res = await api('report_bug', null, 'POST', {
type,
title,
description: desc,
steps,
user_agent: navigator.userAgent,
version: manifest.version || '',
context: {
lang: _currentLang,
online: navigator.onLine,
version_guard_bypass: true,
}
})
url: location.href,
version: _loadedVersion || '',
lang: _currentLang || 'it',
});
const json = await res.json();
if (json.ok) {
if (res.ok) {
statusEl.style.background = '#dcfce7';
statusEl.style.color = '#15803d';
statusEl.textContent = t('about.report_bug_sent');
// Open GitHub issues so user can add details
setTimeout(() => window.open('https://github.com/dadaloop82/EverShelf/issues', '_blank', 'noopener'), 800);
const issueRef = res.issue ? ` (#${res.issue})` : '';
statusEl.textContent = t('about.report_bug_sent') + issueRef;
submitBtn.style.display = 'none';
setTimeout(() => closeModal(), 3500);
} else {
throw new Error(json.error || 'error');
throw new Error(res.error || 'error');
}
} catch(e) {
statusEl.style.background = '#fee2e2';
statusEl.style.color = '#dc2626';
statusEl.textContent = t('about.report_bug_error');
} finally {
btn.disabled = false;
submitBtn.disabled = false;
}
}
+1 -2
View File
@@ -1344,7 +1344,7 @@
<button class="btn btn-outline full-width" onclick="reportBugManual()" id="btn-report-bug">
🐛 <span data-i18n="about.report_bug">Segnala un problema</span>
</button>
<p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Apri una segnalazione su GitHub.</p>
<p class="settings-hint" style="text-align:center;margin:0" data-i18n="about.report_bug_hint">Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.</p>
<div style="display:flex;gap:8px">
<a class="btn btn-outline full-width" style="text-decoration:none;text-align:center"
href="https://github.com/dadaloop82/EverShelf/blob/main/CHANGELOG.md"
@@ -1354,7 +1354,6 @@
target="_blank" rel="noopener" data-i18n="about.github">GitHub</a>
</div>
</div>
<div id="report-bug-status" style="display:none;margin-top:8px;text-align:center;font-size:0.85rem"></div>
</div>
</section>
+13 -1
View File
@@ -1097,7 +1097,19 @@
"title": "Über",
"version": "Version",
"report_bug": "Fehler melden",
"report_bug_hint": "Etwas funktioniert nicht? Öffne ein Issue auf GitHub.",
"report_bug_hint": "Etwas funktioniert nicht? Sende uns direkt aus der App eine Meldung.",
"report_bug_modal_title": "Fehler melden",
"report_type_bug": "Fehler",
"report_type_feature": "Funktion",
"report_type_question": "Frage",
"report_field_title": "Titel",
"report_field_title_ph": "Kurze Beschreibung des Problems",
"report_field_desc": "Beschreibung",
"report_field_desc_ph": "Problem detailliert beschreiben…",
"report_field_steps": "Schritte zum Reproduzieren (optional)",
"report_field_steps_ph": "1. Gehe zu…\n2. Tippe auf…\n3. Fehler erscheint…",
"report_auto_info": "Automatisch beigefügt: Version {version}, Sprache {lang}.",
"report_send_btn": "Bericht senden",
"report_bug_sending": "Wird gesendet…",
"report_bug_sent": "Bericht gesendet — danke!",
"report_bug_error": "Bericht konnte nicht gesendet werden. Verbindung prüfen.",
+13 -1
View File
@@ -1097,7 +1097,19 @@
"title": "About",
"version": "Version",
"report_bug": "Report a Bug",
"report_bug_hint": "Something not working? Open an issue on GitHub.",
"report_bug_hint": "Something not working? Send us a report directly from the app.",
"report_bug_modal_title": "Report a Bug",
"report_type_bug": "Bug",
"report_type_feature": "Feature",
"report_type_question": "Question",
"report_field_title": "Title",
"report_field_title_ph": "Brief description of the issue",
"report_field_desc": "Description",
"report_field_desc_ph": "Describe the issue in detail…",
"report_field_steps": "Steps to reproduce (optional)",
"report_field_steps_ph": "1. Go to…\n2. Tap…\n3. See the error…",
"report_auto_info": "Automatically attached: version {version}, language {lang}.",
"report_send_btn": "Send report",
"report_bug_sending": "Sending…",
"report_bug_sent": "Report sent — thank you!",
"report_bug_error": "Could not send the report. Check your connection.",
+13 -1
View File
@@ -1097,7 +1097,19 @@
"title": "Informazioni",
"version": "Versione",
"report_bug": "Segnala un problema",
"report_bug_hint": "Qualcosa non funziona? Apri una segnalazione su GitHub.",
"report_bug_hint": "Qualcosa non funziona? Inviaci una segnalazione direttamente dall'app.",
"report_bug_modal_title": "Segnala un problema",
"report_type_bug": "Bug",
"report_type_feature": "Funzionalità",
"report_type_question": "Domanda",
"report_field_title": "Titolo",
"report_field_title_ph": "Breve descrizione del problema",
"report_field_desc": "Descrizione",
"report_field_desc_ph": "Descrivi il problema in dettaglio…",
"report_field_steps": "Passi per riprodurlo (opzionale)",
"report_field_steps_ph": "1. Vai su…\n2. Tocca…\n3. Vedi l'errore…",
"report_auto_info": "Saranno allegati automaticamente: versione {version}, lingua {lang}.",
"report_send_btn": "Invia segnalazione",
"report_bug_sending": "Invio in corso…",
"report_bug_sent": "Segnalazione inviata — grazie!",
"report_bug_error": "Impossibile inviare la segnalazione. Controlla la connessione.",