From 218a01180a3ce68ec514776322cdfbcee6fb6697 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 25 Sep 2026 20:36:08 +0000 Subject: [PATCH] feat(debug): add hidden cross-platform diagnostics mode --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 103 ++++++++++++++++++++++++++++- src-tauri/src/model.rs | 32 +++++++++ src-tauri/tauri.conf.json | 2 +- src/main.ts | 134 +++++++++++++++++++++++++++++++++++++- src/styles.css | 49 ++++++++++++++ 7 files changed, 319 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d1ca9c7..081d1b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fieldlens/print", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b04674d..ae70a4a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fieldlens-print" -version = "0.1.18" +version = "0.1.19" description = "FieldLens print-stream barcode capture companion" authors = ["Arrowstack"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 85c4eef..4c6c3bc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ use parking_lot::Mutex; use tauri::{AppHandle, Manager, State}; use capture::CaptureRuntime; -use model::{AppConfig, AuthStatus, CaptureStatus, JobSummary, LoginPoll, LoginStart, SyncResult}; +use model::{AppConfig, AuthStatus, CaptureStatus, DebugCheck, DebugSnapshot, JobSummary, LoginPoll, LoginStart, SyncResult}; use printers::{SystemPrinter, VirtualPrinterStatus}; use state::SharedState; @@ -152,6 +152,106 @@ fn build_self_test_pdf() -> Vec { pdf } +fn debug_command_check(name: &str) -> DebugCheck { + #[cfg(target_os = "windows")] + let result = std::process::Command::new("where").arg(name).output(); + + #[cfg(not(target_os = "windows"))] + let result = std::process::Command::new("sh") + .args(["-lc", &format!("command -v {name}")]) + .output(); + + match result { + Ok(output) if output.status.success() => DebugCheck { + name: name.into(), + ok: true, + detail: String::from_utf8_lossy(&output.stdout) + .lines() + .next() + .unwrap_or("available") + .trim() + .to_string(), + }, + Ok(_) => DebugCheck { + name: name.into(), + ok: false, + detail: "not found".into(), + }, + Err(error) => DebugCheck { + name: name.into(), + ok: false, + detail: error.to_string(), + }, + } +} + +#[tauri::command] +fn get_debug_snapshot(state: State<'_, AppState>) -> Result { + let config = state.shared.config.lock().clone(); + let jobs = state.shared.jobs.lock(); + let installed_printers = printers::list_system_printers() + .map(|items| items.len()) + .unwrap_or(0); + let virtual_printer = printers::virtual_printer_status() + .map(|value| value.detail) + .unwrap_or_else(|error| format!("status error: {error}")); + + #[cfg(target_os = "linux")] + let capture_storage = "/var/lib/fieldlens-print/capture".to_string(); + + #[cfg(not(target_os = "linux"))] + let capture_storage = config.spool_dir.clone(); + + #[cfg(target_os = "linux")] + let checks = vec![ + debug_command_check("lp"), + debug_command_check("pdfinfo"), + debug_command_check("pdftoppm"), + debug_command_check("zbarimg"), + debug_command_check("pkexec"), + ]; + + #[cfg(target_os = "windows")] + let checks = vec![ + debug_command_check("powershell.exe"), + debug_command_check("rundll32.exe"), + ]; + + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + let checks = Vec::new(); + + let recent_errors = jobs + .iter() + .filter_map(|job| { + job.error + .as_ref() + .map(|error| format!("{}: {}", job.label, error)) + }) + .take(5) + .collect(); + + Ok(DebugSnapshot { + platform: std::env::consts::OS.into(), + arch: std::env::consts::ARCH.into(), + app_version: env!("CARGO_PKG_VERSION").into(), + data_dir: state.shared.data_dir.to_string_lossy().into_owned(), + capture_storage, + capture_running: state.runtime.lock().is_some(), + capture_mode: config.mode.clone(), + pending: state.shared.pending_count(), + jobs: jobs.len(), + signed_in: state.shared.auth.lock().refresh_token.is_some(), + forward_printer: config.forward_printer, + listen_addr: config.listen_addr, + forward_addr: config.forward_addr, + client_id: config.client_id, + virtual_printer, + installed_printers, + recent_errors, + checks, + }) +} + #[tauri::command] fn run_capture_self_test() -> Result { #[cfg(target_os = "linux")] @@ -222,6 +322,7 @@ pub fn run() { get_virtual_printer_status, install_virtual_printer, remove_virtual_printer, + get_debug_snapshot, run_capture_self_test, begin_login, poll_login, diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index cfc4ba6..8a107b4 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -133,3 +133,35 @@ pub struct SyncResult { pub synced: usize, pub pending: usize, } + + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DebugCheck { + pub name: String, + pub ok: bool, + pub detail: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DebugSnapshot { + pub platform: String, + pub arch: String, + pub app_version: String, + pub data_dir: String, + pub capture_storage: String, + pub capture_running: bool, + pub capture_mode: CaptureMode, + pub pending: usize, + pub jobs: usize, + pub signed_in: bool, + pub forward_printer: String, + pub listen_addr: String, + pub forward_addr: String, + pub client_id: String, + pub virtual_printer: String, + pub installed_printers: usize, + pub recent_errors: Vec, + pub checks: Vec, +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 37981fd..7555760 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "FieldLens Print", - "version": "0.1.18", + "version": "0.1.19", "identifier": "dev.arrowstack.fieldlens.print", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/main.ts b/src/main.ts index 52f131b..70332d5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,6 +76,33 @@ type AuthStatus = { role?: string | null } +type DebugCheck = { + name: string + ok: boolean + detail: string +} + +type DebugSnapshot = { + platform: string + arch: string + appVersion: string + dataDir: string + captureStorage: string + captureRunning: boolean + captureMode: CaptureMode + pending: number + jobs: number + signedIn: boolean + forwardPrinter: string + listenAddr: string + forwardAddr: string + clientId: string + virtualPrinter: string + installedPrinters: number + recentErrors: string[] + checks: DebugCheck[] +} + type ManualReleaseDownload = { url: string sha256?: string @@ -97,7 +124,7 @@ let config: AppConfig let jobs: Job[] = [] let status: Status = { running: false, mode: 'raw-proxy', pending: 0, jobs: 0 } let loginTimer: number | undefined -let currentVersion = '0.1.18' +let currentVersion = '0.1.19' let bundleType: BundleType | null = null let availableUpdate: Update | null = null let availableManualUpdate: ManualUpdate | null = null @@ -110,6 +137,8 @@ let virtualPrinterStatus: VirtualPrinterStatus = { detail: 'Checking FieldLens queues…', } let authStatus: AuthStatus = { signedIn: false, accessStatus: 'signed-out', role: null } +let debugEnabled = false +let debugSnapshot: DebugSnapshot | null = null app.innerHTML = `
@@ -260,6 +289,21 @@ app.innerHTML = `
+ + @@ -326,6 +370,65 @@ function setView(view: 'setup' | 'running') { el('running-tab').classList.toggle('active', view === 'running') } +function formatDebugReport(snapshot: DebugSnapshot) { + const checks = snapshot.checks.length + ? snapshot.checks.map((check) => ` ${check.ok ? 'OK ' : 'ERR'} ${check.name}: ${check.detail}`).join('\n') + : ' (no platform command checks)' + + const errors = snapshot.recentErrors.length + ? snapshot.recentErrors.map((error) => ` - ${error}`).join('\n') + : ' none' + + return [ + `FieldLens Print ${snapshot.appVersion}`, + `Platform: ${snapshot.platform} / ${snapshot.arch}`, + `Bundle: ${bundleType ?? 'unknown'}`, + '', + `Capture: ${snapshot.captureRunning ? 'running' : 'stopped'} (${snapshot.captureMode})`, + `Pending scans: ${snapshot.pending}`, + `Recent jobs: ${snapshot.jobs}`, + `Capture storage: ${snapshot.captureStorage}`, + `Data dir: ${snapshot.dataDir}`, + '', + `Physical printer: ${snapshot.forwardPrinter || '(not selected)'}`, + `Installed printers: ${snapshot.installedPrinters}`, + `Virtual printer: ${snapshot.virtualPrinter}`, + `Listen: ${snapshot.listenAddr || '(unset)'}`, + `Forward: ${snapshot.forwardAddr || '(unset)'}`, + '', + `Auth session: ${snapshot.signedIn ? 'present' : 'none'}`, + `Access: ${authStatus.accessStatus}${authStatus.role ? ` (${authStatus.role})` : ''}`, + `OIDC client: ${snapshot.clientId}`, + '', + 'Platform checks:', + checks, + '', + 'Recent job errors:', + errors, + ].join('\n') +} + +async function refreshDebugSnapshot() { + if (!debugEnabled) return + const output = el('debug-output') + output.textContent = 'Loading diagnostics…' + try { + debugSnapshot = await invoke('get_debug_snapshot') + output.textContent = formatDebugReport(debugSnapshot) + } catch (error) { + output.textContent = `Debug snapshot failed: ${String(error)}` + } +} + +function setDebugEnabled(enabled: boolean) { + debugEnabled = enabled + el('debug-panel').hidden = !enabled + if (enabled) { + setView('setup') + void refreshDebugSnapshot() + } +} + function readForm(): AppConfig { return { ...config, @@ -443,6 +546,7 @@ async function refreshAuthStatus() { return } renderAuthStatus() + if (debugEnabled) void refreshDebugSnapshot() } function formatBytes(bytes: number) { @@ -523,6 +627,7 @@ async function refreshStatus() { status = await invoke('get_status') jobs = await invoke('recent_jobs') render() + if (debugEnabled) void refreshDebugSnapshot() } async function ensureCaptureStarted() { @@ -549,6 +654,7 @@ el('forwardPrinter').addEventListener('change', () => { el('forwardAddr').value = '' } renderPrinters() + if (debugEnabled) void refreshDebugSnapshot() }) el('refresh-printers').addEventListener('click', () => { void refreshPrinters() }) @@ -656,6 +762,32 @@ el('stop').addEventListener('click', async () => { await refreshStatus() }) +window.addEventListener('keydown', (event) => { + if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'd') { + event.preventDefault() + setDebugEnabled(!debugEnabled) + } +}) + +el('debug-refresh').addEventListener('click', () => { + void refreshDebugSnapshot() +}) + +el('debug-copy').addEventListener('click', async () => { + const state = el('debug-action-state') + if (!debugSnapshot) await refreshDebugSnapshot() + if (!debugSnapshot) { + state.textContent = 'No debug report available.' + return + } + try { + await navigator.clipboard.writeText(formatDebugReport(debugSnapshot)) + state.textContent = 'Diagnostic report copied. Secrets and tokens are excluded.' + } catch (error) { + state.textContent = `Could not copy report: ${String(error)}` + } +}) + el('setup-tab').addEventListener('click', () => setView('setup')) el('running-tab').addEventListener('click', () => setView('running')) diff --git a/src/styles.css b/src/styles.css index 08df4fd..2c78d66 100644 --- a/src/styles.css +++ b/src/styles.css @@ -641,3 +641,52 @@ select:focus { flex-direction: column; } } + + +.debug-panel { + margin-top: 12px; + border: 1px solid #5b4720; + border-radius: 11px; + background: #151109; + overflow: hidden; +} + +.debug-panel[hidden] { + display: none; +} + +.debug-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 12px; + border-bottom: 1px solid #3a2d16; +} + +.debug-head > div:first-child { + display: grid; + gap: 3px; +} + +.debug-head .eyebrow { + color: #e3ad4f; +} + +.debug-output { + margin: 0; + padding: 12px; + max-height: 340px; + overflow: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: #d8dee7; + background: #0b0e12; + font: 11px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +#debug-action-state { + display: block; + min-height: 14px; + padding: 0 12px 10px; +}