feat(debug): add hidden cross-platform diagnostics mode
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled

This commit is contained in:
Admin 2026-09-25 20:36:08 +00:00
parent e7df32eba8
commit 218a01180a
7 changed files with 319 additions and 5 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@fieldlens/print", "name": "@fieldlens/print",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fieldlens-print" name = "fieldlens-print"
version = "0.1.18" version = "0.1.19"
description = "FieldLens print-stream barcode capture companion" description = "FieldLens print-stream barcode capture companion"
authors = ["Arrowstack"] authors = ["Arrowstack"]
edition = "2021" edition = "2021"

View File

@ -10,7 +10,7 @@ use parking_lot::Mutex;
use tauri::{AppHandle, Manager, State}; use tauri::{AppHandle, Manager, State};
use capture::CaptureRuntime; 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 printers::{SystemPrinter, VirtualPrinterStatus};
use state::SharedState; use state::SharedState;
@ -152,6 +152,106 @@ fn build_self_test_pdf() -> Vec<u8> {
pdf 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<DebugSnapshot, String> {
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] #[tauri::command]
fn run_capture_self_test() -> Result<String, String> { fn run_capture_self_test() -> Result<String, String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -222,6 +322,7 @@ pub fn run() {
get_virtual_printer_status, get_virtual_printer_status,
install_virtual_printer, install_virtual_printer,
remove_virtual_printer, remove_virtual_printer,
get_debug_snapshot,
run_capture_self_test, run_capture_self_test,
begin_login, begin_login,
poll_login, poll_login,

View File

@ -133,3 +133,35 @@ pub struct SyncResult {
pub synced: usize, pub synced: usize,
pub pending: 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<String>,
pub checks: Vec<DebugCheck>,
}

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "FieldLens Print", "productName": "FieldLens Print",
"version": "0.1.18", "version": "0.1.19",
"identifier": "dev.arrowstack.fieldlens.print", "identifier": "dev.arrowstack.fieldlens.print",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@ -76,6 +76,33 @@ type AuthStatus = {
role?: string | null 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 = { type ManualReleaseDownload = {
url: string url: string
sha256?: string sha256?: string
@ -97,7 +124,7 @@ let config: AppConfig
let jobs: Job[] = [] let jobs: Job[] = []
let status: Status = { running: false, mode: 'raw-proxy', pending: 0, jobs: 0 } let status: Status = { running: false, mode: 'raw-proxy', pending: 0, jobs: 0 }
let loginTimer: number | undefined let loginTimer: number | undefined
let currentVersion = '0.1.18' let currentVersion = '0.1.19'
let bundleType: BundleType | null = null let bundleType: BundleType | null = null
let availableUpdate: Update | null = null let availableUpdate: Update | null = null
let availableManualUpdate: ManualUpdate | null = null let availableManualUpdate: ManualUpdate | null = null
@ -110,6 +137,8 @@ let virtualPrinterStatus: VirtualPrinterStatus = {
detail: 'Checking FieldLens queues…', detail: 'Checking FieldLens queues…',
} }
let authStatus: AuthStatus = { signedIn: false, accessStatus: 'signed-out', role: null } let authStatus: AuthStatus = { signedIn: false, accessStatus: 'signed-out', role: null }
let debugEnabled = false
let debugSnapshot: DebugSnapshot | null = null
app.innerHTML = ` app.innerHTML = `
<div class="shell"> <div class="shell">
@ -260,6 +289,21 @@ app.innerHTML = `
</div> </div>
</div> </div>
</details> </details>
<section id="debug-panel" class="debug-panel" hidden>
<div class="debug-head">
<div>
<span class="eyebrow">DEBUG MODE</span>
<strong>Diagnostics</strong>
</div>
<div class="inline-actions">
<button class="btn ghost" id="debug-refresh" type="button">Refresh</button>
<button class="btn ghost" id="debug-copy" type="button">Copy report</button>
</div>
</div>
<pre id="debug-output" class="debug-output">Loading diagnostics…</pre>
<span id="debug-action-state" class="muted"></span>
</section>
</div> </div>
</section> </section>
</div> </div>
@ -326,6 +370,65 @@ function setView(view: 'setup' | 'running') {
el('running-tab').classList.toggle('active', view === '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<DebugSnapshot>('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 { function readForm(): AppConfig {
return { return {
...config, ...config,
@ -443,6 +546,7 @@ async function refreshAuthStatus() {
return return
} }
renderAuthStatus() renderAuthStatus()
if (debugEnabled) void refreshDebugSnapshot()
} }
function formatBytes(bytes: number) { function formatBytes(bytes: number) {
@ -523,6 +627,7 @@ async function refreshStatus() {
status = await invoke<Status>('get_status') status = await invoke<Status>('get_status')
jobs = await invoke<Job[]>('recent_jobs') jobs = await invoke<Job[]>('recent_jobs')
render() render()
if (debugEnabled) void refreshDebugSnapshot()
} }
async function ensureCaptureStarted() { async function ensureCaptureStarted() {
@ -549,6 +654,7 @@ el('forwardPrinter').addEventListener('change', () => {
el<HTMLInputElement>('forwardAddr').value = '' el<HTMLInputElement>('forwardAddr').value = ''
} }
renderPrinters() renderPrinters()
if (debugEnabled) void refreshDebugSnapshot()
}) })
el('refresh-printers').addEventListener('click', () => { void refreshPrinters() }) el('refresh-printers').addEventListener('click', () => { void refreshPrinters() })
@ -656,6 +762,32 @@ el('stop').addEventListener('click', async () => {
await refreshStatus() 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('setup-tab').addEventListener('click', () => setView('setup'))
el('running-tab').addEventListener('click', () => setView('running')) el('running-tab').addEventListener('click', () => setView('running'))

View File

@ -641,3 +641,52 @@ select:focus {
flex-direction: column; 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;
}