feat(print): publish 0.1.29 Windows capture diagnostics
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
caa6a5d30d
commit
133e0f2d81
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fieldlens/print",
|
||||
"version": "0.1.28",
|
||||
"version": "0.1.29",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,7 +283,8 @@ fn refresh_access_token(state: &SharedState) -> Result<String> {
|
||||
|
||||
pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
||||
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<SyncResult> {
|
||||
let client = client()?;
|
||||
let device_id = ensure_device(&client, state, &access_token)?;
|
||||
|
||||
let batch: Vec<ScanEvent> = pending.into_iter().take(100).collect();
|
||||
let events: Vec<Value> = batch
|
||||
let telemetry_batch: Vec<_> = telemetry.into_iter().take(100).collect();
|
||||
let batch: Vec<ScanEvent> = pending.into_iter().take(100 - telemetry_batch.len()).collect();
|
||||
let mut events: Vec<Value> = 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<SyncResult> {
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<Value>>());
|
||||
|
||||
let config = state.config.lock().clone();
|
||||
let response = client
|
||||
@ -355,6 +365,7 @@ pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
||||
queue.retain(|scan| !accepted.contains(&scan.id));
|
||||
}
|
||||
state.persist_pending()?;
|
||||
state.acknowledge_print_telemetry(&accepted)?;
|
||||
|
||||
Ok(SyncResult {
|
||||
synced,
|
||||
|
||||
@ -134,6 +134,20 @@ pub struct JobSummary {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub app_version: String,
|
||||
pub captured_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScanEvent {
|
||||
|
||||
@ -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<Mutex<AppConfig>>,
|
||||
pub jobs: Arc<Mutex<VecDeque<JobSummary>>>,
|
||||
pub pending: Arc<Mutex<Vec<ScanEvent>>>,
|
||||
pub pending_telemetry: Arc<Mutex<Vec<PrintTelemetry>>>,
|
||||
telemetry_io: Arc<Mutex<()>>,
|
||||
pub print_history: Arc<Mutex<Vec<PalletPrintRecord>>>,
|
||||
pub auth: Arc<Mutex<AuthState>>,
|
||||
pub data_dir: PathBuf,
|
||||
@ -51,6 +53,7 @@ impl SharedState {
|
||||
fs::create_dir_all(&config.spool_dir).ok();
|
||||
|
||||
let pending = read_json::<Vec<ScanEvent>>(&data_dir.join("pending.json")).unwrap_or_default();
|
||||
let pending_telemetry = read_json::<Vec<PrintTelemetry>>(&data_dir.join("pending-telemetry.json")).unwrap_or_default();
|
||||
let print_history =
|
||||
read_json::<Vec<PalletPrintRecord>>(&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<String>) -> 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
20
src/main.ts
20
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<HTMLButtonElement>('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<Job>('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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user