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; +}