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
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:
parent
e7df32eba8
commit
218a01180a
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fieldlens/print",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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<u8> {
|
||||
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]
|
||||
fn run_capture_self_test() -> Result<String, String> {
|
||||
#[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,
|
||||
|
||||
@ -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<String>,
|
||||
pub checks: Vec<DebugCheck>,
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
134
src/main.ts
134
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 = `
|
||||
<div class="shell">
|
||||
@ -260,6 +289,21 @@ app.innerHTML = `
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
@ -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<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 {
|
||||
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<Status>('get_status')
|
||||
jobs = await invoke<Job[]>('recent_jobs')
|
||||
render()
|
||||
if (debugEnabled) void refreshDebugSnapshot()
|
||||
}
|
||||
|
||||
async function ensureCaptureStarted() {
|
||||
@ -549,6 +654,7 @@ el('forwardPrinter').addEventListener('change', () => {
|
||||
el<HTMLInputElement>('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'))
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user