From 133e0f2d812d3a8156bd36f4abcd24bbe196523c Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 26 Sep 2026 22:20:33 +0000 Subject: [PATCH] feat(print): publish 0.1.29 Windows capture diagnostics --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/capture.rs | 48 ++++++++++++++++++++++++++++++++++++++ src-tauri/src/fieldlens.rs | 19 +++++++++++---- src-tauri/src/model.rs | 14 +++++++++++ src-tauri/src/state.rs | 25 ++++++++++++++++++-- src-tauri/tauri.conf.json | 2 +- src/main.ts | 20 +++++++++++++++- 8 files changed, 122 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e72b8ce..2112fa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fieldlens/print", - "version": "0.1.28", + "version": "0.1.29", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 564f926..2b65c2b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fieldlens-print" -version = "0.1.28" +version = "0.1.29" description = "FieldLens print-stream barcode capture companion" authors = ["Arrowstack"] edition = "2021" diff --git a/src-tauri/src/capture.rs b/src-tauri/src/capture.rs index 1bc58d2..a46b022 100644 --- a/src-tauri/src/capture.rs +++ b/src-tauri/src/capture.rs @@ -961,6 +961,29 @@ fn process_payload( }; let error = error.or(parse_error); let captured_at = chrono::Utc::now().to_rfc3339(); + #[cfg(target_os = "windows")] + if source == "raw-proxy" { + state.queue_print_telemetry(crate::model::PrintTelemetry { + id: id.clone(), + source: "raw-proxy".into(), + stream_format: classify_print_stream(&bytes).into(), + byte_count: bytes.len(), + barcode_count: barcodes.len(), + forwarded, + failure_kind: error.as_ref().map(|message| { + let kind = if message.contains("parser") { + "parser" + } else if message.contains("forward") { + "forward" + } else { + "capture" + }; + kind.to_owned() + }), + app_version: env!("CARGO_PKG_VERSION").into(), + captured_at: captured_at.clone(), + }); + } let pallet_print = barcodes.first().map(|barcode| { let text = String::from_utf8_lossy(&bytes); let snapshot = parse_pallet_snapshot(&text, &barcode.value); @@ -995,3 +1018,28 @@ fn process_payload( }, ); } + +#[cfg(any(target_os = "windows", test))] +fn classify_print_stream(bytes: &[u8]) -> &'static str { + if bytes.starts_with(b"%PDF-") { "pdf" } + else if bytes.starts_with(b"%!PS") { "postscript" } + else if bytes.starts_with(b"@PJL") || bytes.starts_with(b"\x1b%-12345X@PJL") { "pjl" } + else if bytes.starts_with(b"\x1bE") { "pcl" } + else if bytes.starts_with(b"PK\x03\x04") { "zip" } + else if bytes.windows(3).take(4096).any(|part| part == b"^XA") { "zpl" } + else { "unknown" } +} + +#[cfg(test)] +mod telemetry_tests { + use super::classify_print_stream; + + #[test] + fn classifies_known_print_headers_without_retaining_bytes() { + assert_eq!(classify_print_stream(b"@PJL ENTER LANGUAGE=PCL"), "pjl"); + assert_eq!(classify_print_stream(b"\x1bEprinter"), "pcl"); + assert_eq!(classify_print_stream(b"^XA^BC"), "zpl"); + assert_eq!(classify_print_stream(b"%PDF-1.4"), "pdf"); + assert_eq!(classify_print_stream(b"binary"), "unknown"); + } +} diff --git a/src-tauri/src/fieldlens.rs b/src-tauri/src/fieldlens.rs index e52689a..bdbc0b3 100644 --- a/src-tauri/src/fieldlens.rs +++ b/src-tauri/src/fieldlens.rs @@ -283,7 +283,8 @@ fn refresh_access_token(state: &SharedState) -> Result { pub fn sync_pending(state: &SharedState) -> Result { let pending = state.pending.lock().clone(); - if pending.is_empty() { + let telemetry = state.pending_telemetry.lock().clone(); + if pending.is_empty() && telemetry.is_empty() { return Ok(SyncResult { synced: 0, pending: 0, @@ -294,8 +295,17 @@ pub fn sync_pending(state: &SharedState) -> Result { let client = client()?; let device_id = ensure_device(&client, state, &access_token)?; - let batch: Vec = pending.into_iter().take(100).collect(); - let events: Vec = batch + let telemetry_batch: Vec<_> = telemetry.into_iter().take(100).collect(); + let batch: Vec = pending.into_iter().take(100 - telemetry_batch.len()).collect(); + let mut events: Vec = telemetry_batch.iter().map(|item| json!({ + "id": item.id, + "aggregateId": item.id, + "eventType": "print.capture.diagnostic", + "schemaVersion": 1, + "createdAt": item.captured_at, + "payload": item, + })).collect(); + events.extend(batch .iter() .map(|scan| { json!({ @@ -316,7 +326,7 @@ pub fn sync_pending(state: &SharedState) -> Result { } }) }) - .collect(); + .collect::>()); let config = state.config.lock().clone(); let response = client @@ -355,6 +365,7 @@ pub fn sync_pending(state: &SharedState) -> Result { queue.retain(|scan| !accepted.contains(&scan.id)); } state.persist_pending()?; + state.acknowledge_print_telemetry(&accepted)?; Ok(SyncResult { synced, diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index b9f94c8..5bf770c 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -134,6 +134,20 @@ pub struct JobSummary { pub error: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrintTelemetry { + pub id: String, + pub source: String, + pub stream_format: String, + pub byte_count: usize, + pub barcode_count: usize, + pub forwarded: bool, + pub failure_kind: Option, + pub app_version: String, + pub captured_at: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScanEvent { diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 5271102..fa55d72 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -14,7 +14,7 @@ use tauri::{AppHandle, Emitter}; use crate::{ model::{ AppConfig, AuthState, JobSummary, PalletPrintComparison, PalletPrintRecord, - PalletPrintStatus, PalletSnapshot, ScanEvent, + PalletPrintStatus, PalletSnapshot, PrintTelemetry, ScanEvent, }, pallet::{comparable_field_count, diff_snapshots}, }; @@ -27,6 +27,8 @@ pub struct SharedState { pub config: Arc>, pub jobs: Arc>>, pub pending: Arc>>, + pub pending_telemetry: Arc>>, + telemetry_io: Arc>, pub print_history: Arc>>, pub auth: Arc>, pub data_dir: PathBuf, @@ -51,6 +53,7 @@ impl SharedState { fs::create_dir_all(&config.spool_dir).ok(); let pending = read_json::>(&data_dir.join("pending.json")).unwrap_or_default(); + let pending_telemetry = read_json::>(&data_dir.join("pending-telemetry.json")).unwrap_or_default(); let print_history = read_json::>(&data_dir.join("print-history.json")) .unwrap_or_default(); @@ -65,6 +68,8 @@ impl SharedState { config: Arc::new(Mutex::new(config)), jobs: Arc::new(Mutex::new(VecDeque::new())), pending: Arc::new(Mutex::new(pending)), + pending_telemetry: Arc::new(Mutex::new(pending_telemetry)), + telemetry_io: Arc::new(Mutex::new(())), print_history: Arc::new(Mutex::new(print_history)), auth: Arc::new(Mutex::new(auth)), data_dir, @@ -81,6 +86,22 @@ impl SharedState { write_json_atomic(&self.data_dir.join("pending.json"), &*self.pending.lock()) } + pub fn persist_pending_telemetry(&self) -> Result<()> { + write_json_atomic(&self.data_dir.join("pending-telemetry.json"), &*self.pending_telemetry.lock()) + } + + pub fn queue_print_telemetry(&self, event: PrintTelemetry) { + let _guard = self.telemetry_io.lock(); + self.pending_telemetry.lock().push(event); + let _ = self.persist_pending_telemetry(); + } + + pub fn acknowledge_print_telemetry(&self, accepted: &std::collections::HashSet) -> Result<()> { + let _guard = self.telemetry_io.lock(); + self.pending_telemetry.lock().retain(|item| !accepted.contains(&item.id)); + self.persist_pending_telemetry() + } + pub fn persist_print_history(&self) -> Result<()> { write_json_atomic( &self.data_dir.join("print-history.json"), @@ -224,7 +245,7 @@ impl SharedState { } pub fn pending_count(&self) -> usize { - self.pending.lock().len() + self.pending.lock().len() + self.pending_telemetry.lock().len() } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 296af3e..6182383 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.28", + "version": "0.1.29", "identifier": "dev.arrowstack.fieldlens.print", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/main.ts b/src/main.ts index 46ec0bf..93a5fda 100644 --- a/src/main.ts +++ b/src/main.ts @@ -142,7 +142,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.28' +let currentVersion = '0.1.29' let bundleType: BundleType | null = null let availableUpdate: Update | null = null let availableManualUpdate: ManualUpdate | null = null @@ -904,6 +904,20 @@ el('sync').addEventListener('click', async () => { } }) +let automaticSyncInFlight = false +async function syncAfterLogin() { + if (automaticSyncInFlight || !authStatus.signedIn || authStatus.accessStatus !== 'approved') return + automaticSyncInFlight = true + try { + await invoke('sync_pending') + await refreshStatus() + } catch { + // Keep local outbox entries for the next attempt or manual sync. + } finally { + automaticSyncInFlight = false + } +} + el('login').addEventListener('click', async () => { const login = el('login') const state = el('login-state') @@ -942,6 +956,7 @@ el('login').addEventListener('click', async () => { role: result.role ?? null, } renderAuthStatus() + void syncAfterLogin() } else if (result.status === 'expired' || result.status === 'denied') { window.clearInterval(loginTimer) loginTimer = undefined @@ -1149,6 +1164,7 @@ el('install-update').addEventListener('click', () => { void installAvailableUpda await listen('capture-job', (event) => { jobs = [event.payload, ...jobs.filter((job) => job.id !== event.payload.id)].slice(0, 200) void refreshStatus() + void syncAfterLogin() }) initOcrRefinement(el('ocr-refinement-root')) @@ -1158,6 +1174,8 @@ writeForm(config) await refreshPrinters() await ensureCaptureStarted() void refreshAuthStatus() +window.setTimeout(() => { void syncAfterLogin() }, 5000) +window.setInterval(() => { void syncAfterLogin() }, 30000) const readyForRunning = Boolean( virtualPrinterStatus?.installed