From 9dc5a8d8b45c8f297e48641589aa296ea16536aa Mon Sep 17 00:00:00 2001 From: Douglas Teles Date: Wed, 8 Jul 2026 13:19:37 -0300 Subject: [PATCH] flasher: crash-reporter dialog with prefilled GitHub issue on flash errors When a flash fails (anything except a user cancel), a dialog now shows the translated error, the tail of the universal diagnostic log in a scrollable monospace block, and a Report on GitHub button. The button opens the browser on a prefilled issue: stable error token in the title, version and OS in the body, and the log in a code fence. GitHub caps URL length, so the body keeps the failing end of the log and trims older lines until the whole URL fits in 6000 characters, flagging the trim. Backend gains a get_flash_log command that returns the last 16KB of the diaglog file; the external-URL opener was factored into a shared helper. Strings translated in the five supported languages. --- src-tauri/src/main.rs | 20 +++++++++++ src/i18n/en.json | 6 +++- src/i18n/es.json | 6 +++- src/i18n/pt-BR.json | 6 +++- src/i18n/ru.json | 6 +++- src/i18n/zh.json | 6 +++- src/index.html | 14 ++++++++ src/main.js | 84 ++++++++++++++++++++++++++++++++++++++----- src/style.css | 21 +++++++++++ 9 files changed, 155 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a0670cd..b07d833 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -46,6 +46,25 @@ fn get_version() -> String { env!("CARGO_PKG_VERSION").to_string() } +#[tauri::command] +fn get_flash_log() -> String { + // Tail of the universal flash log for the error-report dialog. 16KB + // keeps the dialog and the prefilled GitHub issue manageable while + // preserving the failing end of the story. + const TAIL: usize = 16 * 1024; + match std::fs::read(diaglog::log_path()) { + Ok(bytes) => { + let start = bytes.len().saturating_sub(TAIL); + let mut text = String::from_utf8_lossy(&bytes[start..]).into_owned(); + if start > 0 { + text = format!("[... {} earlier bytes omitted ...]\n{}", start, text); + } + text + } + Err(_) => String::new(), + } +} + #[tauri::command] fn list_disks() -> Vec { disk::list_removable_disks() @@ -378,6 +397,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ get_locale, get_version, + get_flash_log, list_disks, check_latest_release, download_image, diff --git a/src/i18n/en.json b/src/i18n/en.json index f68709f..ddd0098 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -93,5 +93,9 @@ "verify_label": "Verify after writing", "verify_hint": "Reads the SD card back and compares its SHA-256 against the image. Adds 1-2 minutes but catches bad cards and corrupted writes.", "error_mount_boot": "The image was written, but the boot partition did not mount to install your panel overlay. Remove and reinsert the card, then use the overlay generator to copy overlays/mipi-panel.dtbo manually.", - "configuring": "Installing panel overlay..." + "configuring": "Installing panel overlay...", + "error_report_title": "Something went wrong", + "error_report_hint": "The diagnostic log below describes what happened. You can send it to the developers as a GitHub issue.", + "error_report_close": "Close", + "error_report_github": "Report on GitHub" } \ No newline at end of file diff --git a/src/i18n/es.json b/src/i18n/es.json index 86e585c..f4b26f4 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -93,5 +93,9 @@ "verify_label": "Verificar tras escribir", "verify_hint": "Lee la SD y compara su SHA-256 con la imagen. Suma 1-2 minutos pero detecta tarjetas defectuosas y escrituras corruptas.", "error_mount_boot": "La imagen se grabó, pero la partición de arranque no se montó para instalar el overlay del panel. Retire y reinserte la tarjeta y copie overlays/mipi-panel.dtbo manualmente con el generador de overlays.", - "configuring": "Instalando overlay del panel..." + "configuring": "Instalando overlay del panel...", + "error_report_title": "Algo salió mal", + "error_report_hint": "El registro de diagnóstico de abajo describe lo que ocurrió. Puede enviarlo a los desarrolladores como una issue de GitHub.", + "error_report_close": "Cerrar", + "error_report_github": "Reportar en GitHub" } \ No newline at end of file diff --git a/src/i18n/pt-BR.json b/src/i18n/pt-BR.json index 12fbe82..8250db2 100644 --- a/src/i18n/pt-BR.json +++ b/src/i18n/pt-BR.json @@ -93,5 +93,9 @@ "verify_label": "Verificar após gravar", "verify_hint": "Le o cartão SD e compara o SHA-256 com a imagem. Acrescenta 1-2 minutos mas detecta cartões ruins e gravações corrompidas.", "error_mount_boot": "A imagem foi gravada, mas a partição de boot não montou para instalar o overlay do painel. Remova e reinsira o cartão e copie overlays/mipi-panel.dtbo manualmente com o gerador de overlays.", - "configuring": "Instalando overlay do painel..." + "configuring": "Instalando overlay do painel...", + "error_report_title": "Algo deu errado", + "error_report_hint": "O log de diagnóstico abaixo descreve o que aconteceu. Você pode enviá-lo aos desenvolvedores como uma issue no GitHub.", + "error_report_close": "Fechar", + "error_report_github": "Reportar no GitHub" } \ No newline at end of file diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 049163f..c62cf5a 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -93,5 +93,9 @@ "verify_label": "Проверить после записи", "verify_hint": "Считывает SD-карту и сравнивает её SHA-256 с образом. Добавляет 1-2 минуты, но обнаруживает плохие карты и поврежденные записи.", "error_mount_boot": "Образ записан, но загрузочный раздел не смонтировался для установки оверлея панели. Извлеките и снова вставьте карту, затем скопируйте overlays/mipi-panel.dtbo вручную через генератор оверлеев.", - "configuring": "Установка оверлея панели..." + "configuring": "Установка оверлея панели...", + "error_report_title": "Что-то пошло не так", + "error_report_hint": "Приведённый ниже журнал диагностики описывает, что произошло. Вы можете отправить его разработчикам как issue на GitHub.", + "error_report_close": "Закрыть", + "error_report_github": "Сообщить на GitHub" } \ No newline at end of file diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 46da3ea..c121d2d 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -93,5 +93,9 @@ "verify_label": "写入后校验", "verify_hint": "读回 SD 卡并将其 SHA-256 与镜像比较。会增加 1-2 分钟,可发现损坏的 SD 卡和损坏的写入。", "error_mount_boot": "镜像已写入,但引导分区未能挂载以安装面板 overlay。请拔出并重新插入存储卡,然后使用 overlay 生成器手动复制 overlays/mipi-panel.dtbo。", - "configuring": "正在安装面板 overlay..." + "configuring": "正在安装面板 overlay...", + "error_report_title": "出现问题", + "error_report_hint": "下方的诊断日志描述了发生的情况。您可以将其作为 GitHub issue 发送给开发者。", + "error_report_close": "关闭", + "error_report_github": "在 GitHub 上报告" } \ No newline at end of file diff --git a/src/index.html b/src/index.html index 785bb42..1914e5a 100644 --- a/src/index.html +++ b/src/index.html @@ -237,6 +237,20 @@ + + + diff --git a/src/main.js b/src/main.js index 1dde360..2630791 100644 --- a/src/main.js +++ b/src/main.js @@ -475,12 +475,73 @@ async function startFlash() { setFlashStatus(t('done'), 'success'); } catch (e) { const msg = typeof e === 'string' ? e : String(e); - if (msg === 'cancelled') setFlashStatus(t('flash_cancelled'), ''); - else setFlashStatus(translateError(msg), 'error'); + if (msg === 'cancelled') { + setFlashStatus(t('flash_cancelled'), ''); + } else { + setFlashStatus(translateError(msg), 'error'); + await showErrorReportDialog(msg); + } } setBusy(false); } +// --------------------------------------------------------------------------- +// Flash error report dialog: crash-reporter style popup with the diagnostic +// log and a prefilled GitHub issue. GitHub caps URLs around 8KB, so the +// prefilled body keeps the END of the log (where the failure lives) and +// trims from the start until the whole URL fits in 6000 characters. +// --------------------------------------------------------------------------- +const FLASHER_ISSUES_URL = 'https://github.com/archr-linux/archr-flasher/issues/new'; +let lastErrorReport = { raw: '', log: '' }; + +async function showErrorReportDialog(rawMsg) { + let log = ''; + try { log = await invoke('get_flash_log'); } catch (_) {} + lastErrorReport = { raw: rawMsg, log }; + $('error-report-message').textContent = translateError(rawMsg); + $('error-report-log').textContent = log || '(no log file found)'; + $('error-report-dialog').classList.remove('hidden'); +} + +function buildIssueUrl(rawMsg, log, appVersion, osName) { + // First line of the raw error doubles as the issue title; err:* tokens + // are stable and searchable. + const firstLine = (rawMsg.split('\n')[0] || 'flash error').slice(0, 120); + const title = `[flash-error] ${firstLine} (v${appVersion}, ${osName})`; + const bodyFor = (logPart, truncated) => + `**Error:** \`${firstLine}\`\n**Version:** ${appVersion}\n**OS:** ${osName}\n\n` + + `\n\n**Flash log${truncated ? ' (older lines trimmed to fit)' : ''}:**\n` + + '```\n' + logPart + '\n```\n'; + const urlFor = (body) => + `${FLASHER_ISSUES_URL}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`; + + let logPart = log || '(log file empty)'; + let truncated = false; + let url = urlFor(bodyFor(logPart, truncated)); + while (url.length > 6000 && logPart.length > 0) { + truncated = true; + // Drop from the start, a line at a time, keeping the failing tail. + const cut = logPart.indexOf('\n', Math.max(200, logPart.length - Math.floor((logPart.length * 5000) / url.length))); + logPart = cut > 0 && cut < logPart.length - 1 + ? logPart.slice(cut + 1) + : logPart.slice(Math.floor(logPart.length / 2)); + url = urlFor(bodyFor(logPart, truncated)); + } + return url; +} + +$('btn-error-close').addEventListener('click', () => + $('error-report-dialog').classList.add('hidden')); + +$('btn-error-report').addEventListener('click', async () => { + let appVersion = '?'; + try { appVersion = await invoke('get_version'); } catch (_) {} + const osName = navigator.userAgent.includes('Mac') ? 'macOS' + : navigator.userAgent.includes('Win') ? 'Windows' : 'Linux'; + const url = buildIssueUrl(lastErrorReport.raw, lastErrorReport.log, appVersion, osName); + try { await openExternalUrl(url); } catch (e) { console.error('Could not open issue URL:', e); } +}); + // Format bytes as MB or GB with 1 decimal. We intentionally use 1024 // (MiB/GiB) here because that's what dd writes and what users expect // to see lining up with the SD card capacity printed on the box. @@ -636,16 +697,21 @@ function buildOverlaySummary() { // file-picker step. const OVERLAY_GENERATOR_URL = 'https://arch-r.io/overlay-generator/'; +// Open any external URL (Tauri opener, else shell, else a browser tab). +async function openExternalUrl(url) { + if (window.__TAURI__?.opener?.openUrl) { + await window.__TAURI__.opener.openUrl(url); + } else if (window.__TAURI__?.shell?.open) { + await window.__TAURI__.shell.open(url); + } else { + window.open(url, '_blank', 'noopener'); + } +} + // Actually navigate to the generator (Tauri opener, else a new browser tab). const openGeneratorUrl = async () => { try { - if (window.__TAURI__?.opener?.openUrl) { - await window.__TAURI__.opener.openUrl(OVERLAY_GENERATOR_URL); - } else if (window.__TAURI__?.shell?.open) { - await window.__TAURI__.shell.open(OVERLAY_GENERATOR_URL); - } else { - window.open(OVERLAY_GENERATOR_URL, '_blank', 'noopener'); - } + await openExternalUrl(OVERLAY_GENERATOR_URL); } catch (e) { console.error('Could not open online generator URL:', e); } diff --git a/src/style.css b/src/style.css index 284b189..a26a088 100644 --- a/src/style.css +++ b/src/style.css @@ -806,3 +806,24 @@ body { .main-body { padding: 10px 20px; } .nav-bar { padding: 8px 20px 10px; } } + +/* Flash error report dialog */ +.dialog-wide { + max-width: 560px; + width: 90%; +} + +.error-log { + max-height: 220px; + overflow: auto; + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + padding: 10px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11px; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-all; + text-align: left; +}