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.
This commit is contained in:
Douglas Teles
2026-07-08 13:19:37 -03:00
parent 971d91b3f2
commit 9dc5a8d8b4
9 changed files with 155 additions and 14 deletions
+20
View File
@@ -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<DiskInfo> {
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,
+5 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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 上报告"
}
+14
View File
@@ -237,6 +237,20 @@
</div>
</div>
<!-- Flash error report dialog -->
<div class="dialog-overlay hidden" id="error-report-dialog">
<div class="dialog dialog-wide">
<h3 data-i18n="error_report_title">Something went wrong</h3>
<p id="error-report-message" class="warning"></p>
<p data-i18n="error_report_hint">The diagnostic log below describes what happened. You can send it to the developers as a GitHub issue.</p>
<pre id="error-report-log" class="error-log"></pre>
<div class="dialog-buttons">
<button class="btn" id="btn-error-close" data-i18n="error_report_close">Close</button>
<button class="btn btn-primary" id="btn-error-report" data-i18n="error_report_github">Report on GitHub</button>
</div>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
+75 -9
View File
@@ -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` +
`<!-- Please describe what you were doing. -->\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);
}
+21
View File
@@ -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;
}