Compare commits
5 Commits
fix/deskto
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 267279b2a1 | |||
| d79fffed5e | |||
| 133e0f2d81 | |||
| caa6a5d30d | |||
| 03b9077c12 |
@ -1,4 +1,4 @@
|
|||||||
FROM node:24-bookworm AS build
|
FROM node:24-trixie AS build
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
@ -19,6 +19,9 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
|
|||||||
wget \
|
wget \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Tauri's NSIS template requires Win/RestartManager.nsh (NSIS 3.11+).
|
||||||
|
RUN test -f /usr/share/nsis/Include/Win/RestartManager.nsh
|
||||||
|
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
||||||
ENV PATH="/root/.cargo/bin:$PATH"
|
ENV PATH="/root/.cargo/bin:$PATH"
|
||||||
ENV CARGO_BUILD_JOBS=1
|
ENV CARGO_BUILD_JOBS=1
|
||||||
@ -31,7 +34,7 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN test -n "$TAURI_SIGNING_PRIVATE_KEY"
|
RUN node scripts/check-signing.mjs
|
||||||
RUN nice -n 10 npm run bundle:windows
|
RUN nice -n 10 npm run bundle:windows
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
|
|||||||
@ -14,6 +14,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
|
|||||||
python3 \
|
python3 \
|
||||||
librsvg2-dev \
|
librsvg2-dev \
|
||||||
wget \
|
wget \
|
||||||
|
xdg-utils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
|
||||||
@ -26,8 +27,8 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run icons
|
RUN npm run icons
|
||||||
|
RUN node scripts/check-signing.mjs
|
||||||
RUN cargo test --manifest-path src-tauri/Cargo.toml --lib
|
RUN cargo test --manifest-path src-tauri/Cargo.toml --lib
|
||||||
RUN test -n "$TAURI_SIGNING_PRIVATE_KEY"
|
|
||||||
RUN nice -n 10 npm run bundle:linux
|
RUN nice -n 10 npm run bundle:linux
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fieldlens/print",
|
"name": "@fieldlens/print",
|
||||||
"version": "0.1.28",
|
"version": "0.1.30",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
40
scripts/check-signing.mjs
Normal file
40
scripts/check-signing.mjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { createHash, createPublicKey, verify } from 'node:crypto';
|
||||||
|
|
||||||
|
const temp = mkdtempSync(join(tmpdir(), 'fieldlens-signing-'));
|
||||||
|
try {
|
||||||
|
if (!process.env.TAURI_SIGNING_PRIVATE_KEY) throw new Error('missing key');
|
||||||
|
const probe = join(temp, 'probe.txt');
|
||||||
|
writeFileSync(probe, 'FieldLens updater signing preflight\n', { mode: 0o600 });
|
||||||
|
const result = spawnSync(process.execPath, [resolve('node_modules/@tauri-apps/cli/tauri.js'), 'signer', 'sign', probe], {
|
||||||
|
env: process.env, stdio: 'pipe', timeout: 30000,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) throw new Error('signing failed');
|
||||||
|
const config = JSON.parse(readFileSync('src-tauri/tauri.conf.json', 'utf8'));
|
||||||
|
const publicLines = Buffer.from(config.plugins.updater.pubkey, 'base64').toString().trim().split(/\r?\n/);
|
||||||
|
const signatureLines = Buffer.from(readFileSync(probe + '.sig', 'utf8').trim(), 'base64').toString().trim().split(/\r?\n/);
|
||||||
|
const pub = Buffer.from(publicLines[1], 'base64');
|
||||||
|
const sig = Buffer.from(signatureLines[1], 'base64');
|
||||||
|
if (pub.length !== 42 || sig.length !== 74 || !pub.subarray(2, 10).equals(sig.subarray(2, 10))) throw new Error('key mismatch');
|
||||||
|
const key = createPublicKey({
|
||||||
|
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), pub.subarray(10)]),
|
||||||
|
format: 'der', type: 'spki',
|
||||||
|
});
|
||||||
|
const algorithm = sig.subarray(0, 2).toString();
|
||||||
|
if (!['ED', 'Ed'].includes(algorithm)) throw new Error('unsupported signature');
|
||||||
|
const message = readFileSync(probe);
|
||||||
|
const signed = algorithm === 'ED' ? createHash('blake2b512').update(message).digest() : message;
|
||||||
|
if (!verify(null, signed, key, sig.subarray(10))) throw new Error('invalid signature');
|
||||||
|
if (!signatureLines[2].startsWith('trusted comment: ')) throw new Error('invalid comment');
|
||||||
|
const comment = Buffer.from(signatureLines[2].slice('trusted comment: '.length));
|
||||||
|
if (!verify(null, Buffer.concat([sig.subarray(10), comment]), key, Buffer.from(signatureLines[3], 'base64'))) throw new Error('invalid trusted comment');
|
||||||
|
console.log('Updater signing preflight passed: key, password and public key match.');
|
||||||
|
} catch {
|
||||||
|
console.error('Updater signing preflight failed: check the signing key/password and configured public key.');
|
||||||
|
process.exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
rmSync(temp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fieldlens-print"
|
name = "fieldlens-print"
|
||||||
version = "0.1.28"
|
version = "0.1.30"
|
||||||
description = "FieldLens print-stream barcode capture companion"
|
description = "FieldLens print-stream barcode capture companion"
|
||||||
authors = ["Arrowstack"]
|
authors = ["Arrowstack"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@ -457,9 +457,7 @@ fn process_captured_pdf(
|
|||||||
|
|
||||||
let target = unique_processed_path(processed_dir, label);
|
let target = unique_processed_path(processed_dir, label);
|
||||||
if let Err(error) = fs::rename(path, &target) {
|
if let Err(error) = fs::rename(path, &target) {
|
||||||
state.push_job(
|
let job = JobSummary {
|
||||||
app,
|
|
||||||
JobSummary {
|
|
||||||
id,
|
id,
|
||||||
source: source.into(),
|
source: source.into(),
|
||||||
label: label.to_string(),
|
label: label.to_string(),
|
||||||
@ -469,8 +467,9 @@ fn process_captured_pdf(
|
|||||||
captured_at: chrono::Utc::now().to_rfc3339(),
|
captured_at: chrono::Utc::now().to_rfc3339(),
|
||||||
forwarded,
|
forwarded,
|
||||||
error: Some(format!("archive captured PDF failed: {error}")),
|
error: Some(format!("archive captured PDF failed: {error}")),
|
||||||
},
|
};
|
||||||
);
|
queue_capture_diagnostic(state, &job, "pdf");
|
||||||
|
state.push_job(app, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,6 +494,7 @@ fn process_captured_pdf(
|
|||||||
Ok(count) => count,
|
Ok(count) => count,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
job.error = Some(error);
|
job.error = Some(error);
|
||||||
|
queue_capture_diagnostic(state, &job, "pdf");
|
||||||
state.push_job(app, job);
|
state.push_job(app, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -507,6 +507,7 @@ fn process_captured_pdf(
|
|||||||
page.saturating_sub(1),
|
page.saturating_sub(1),
|
||||||
page_count
|
page_count
|
||||||
));
|
));
|
||||||
|
queue_capture_diagnostic(state, &job, "pdf");
|
||||||
state.push_job(app, job);
|
state.push_job(app, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -558,6 +559,7 @@ fn process_captured_pdf(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
queue_capture_diagnostic(state, &job, "pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -961,6 +963,7 @@ fn process_payload(
|
|||||||
};
|
};
|
||||||
let error = error.or(parse_error);
|
let error = error.or(parse_error);
|
||||||
let captured_at = chrono::Utc::now().to_rfc3339();
|
let captured_at = chrono::Utc::now().to_rfc3339();
|
||||||
|
let stream_format = classify_print_stream(&bytes);
|
||||||
let pallet_print = barcodes.first().map(|barcode| {
|
let pallet_print = barcodes.first().map(|barcode| {
|
||||||
let text = String::from_utf8_lossy(&bytes);
|
let text = String::from_utf8_lossy(&bytes);
|
||||||
let snapshot = parse_pallet_snapshot(&text, &barcode.value);
|
let snapshot = parse_pallet_snapshot(&text, &barcode.value);
|
||||||
@ -980,9 +983,7 @@ fn process_payload(
|
|||||||
comparison
|
comparison
|
||||||
});
|
});
|
||||||
|
|
||||||
state.push_job(
|
let job = JobSummary {
|
||||||
app,
|
|
||||||
JobSummary {
|
|
||||||
id,
|
id,
|
||||||
source: source.into(),
|
source: source.into(),
|
||||||
label,
|
label,
|
||||||
@ -992,6 +993,53 @@ fn process_payload(
|
|||||||
captured_at,
|
captured_at,
|
||||||
forwarded,
|
forwarded,
|
||||||
error,
|
error,
|
||||||
},
|
};
|
||||||
);
|
queue_capture_diagnostic(state, &job, stream_format);
|
||||||
|
state.push_job(app, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_capture_diagnostic(state: &SharedState, job: &JobSummary, stream_format: &str) {
|
||||||
|
state.queue_print_telemetry(crate::model::PrintTelemetry {
|
||||||
|
id: job.id.clone(),
|
||||||
|
source: job.source.clone(),
|
||||||
|
stream_format: stream_format.into(),
|
||||||
|
byte_count: job.bytes,
|
||||||
|
barcode_count: job.barcodes.len(),
|
||||||
|
forwarded: job.forwarded,
|
||||||
|
failure_kind: job.error.as_ref().map(|message| {
|
||||||
|
if message.contains("parser") || message.contains("decode") || message.contains("pdfinfo") {
|
||||||
|
"parser"
|
||||||
|
} else if message.contains("forward") {
|
||||||
|
"forward"
|
||||||
|
} else {
|
||||||
|
"capture"
|
||||||
|
}.into()
|
||||||
|
}),
|
||||||
|
app_version: env!("CARGO_PKG_VERSION").into(),
|
||||||
|
captured_at: job.captured_at.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
||||||
let pending = state.pending.lock().clone();
|
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 {
|
return Ok(SyncResult {
|
||||||
synced: 0,
|
synced: 0,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
@ -294,8 +295,17 @@ pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
|||||||
let client = client()?;
|
let client = client()?;
|
||||||
let device_id = ensure_device(&client, state, &access_token)?;
|
let device_id = ensure_device(&client, state, &access_token)?;
|
||||||
|
|
||||||
let batch: Vec<ScanEvent> = pending.into_iter().take(100).collect();
|
let telemetry_batch: Vec<_> = telemetry.into_iter().take(100).collect();
|
||||||
let events: Vec<Value> = batch
|
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()
|
.iter()
|
||||||
.map(|scan| {
|
.map(|scan| {
|
||||||
json!({
|
json!({
|
||||||
@ -316,7 +326,7 @@ pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Vec<Value>>());
|
||||||
|
|
||||||
let config = state.config.lock().clone();
|
let config = state.config.lock().clone();
|
||||||
let response = client
|
let response = client
|
||||||
@ -355,6 +365,7 @@ pub fn sync_pending(state: &SharedState) -> Result<SyncResult> {
|
|||||||
queue.retain(|scan| !accepted.contains(&scan.id));
|
queue.retain(|scan| !accepted.contains(&scan.id));
|
||||||
}
|
}
|
||||||
state.persist_pending()?;
|
state.persist_pending()?;
|
||||||
|
state.acknowledge_print_telemetry(&accepted)?;
|
||||||
|
|
||||||
Ok(SyncResult {
|
Ok(SyncResult {
|
||||||
synced,
|
synced,
|
||||||
|
|||||||
@ -134,6 +134,20 @@ pub struct JobSummary {
|
|||||||
pub error: Option<String>,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScanEvent {
|
pub struct ScanEvent {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use tauri::{AppHandle, Emitter};
|
|||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
AppConfig, AuthState, JobSummary, PalletPrintComparison, PalletPrintRecord,
|
AppConfig, AuthState, JobSummary, PalletPrintComparison, PalletPrintRecord,
|
||||||
PalletPrintStatus, PalletSnapshot, ScanEvent,
|
PalletPrintStatus, PalletSnapshot, PrintTelemetry, ScanEvent,
|
||||||
},
|
},
|
||||||
pallet::{comparable_field_count, diff_snapshots},
|
pallet::{comparable_field_count, diff_snapshots},
|
||||||
};
|
};
|
||||||
@ -27,6 +27,8 @@ pub struct SharedState {
|
|||||||
pub config: Arc<Mutex<AppConfig>>,
|
pub config: Arc<Mutex<AppConfig>>,
|
||||||
pub jobs: Arc<Mutex<VecDeque<JobSummary>>>,
|
pub jobs: Arc<Mutex<VecDeque<JobSummary>>>,
|
||||||
pub pending: Arc<Mutex<Vec<ScanEvent>>>,
|
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 print_history: Arc<Mutex<Vec<PalletPrintRecord>>>,
|
||||||
pub auth: Arc<Mutex<AuthState>>,
|
pub auth: Arc<Mutex<AuthState>>,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
@ -51,6 +53,7 @@ impl SharedState {
|
|||||||
fs::create_dir_all(&config.spool_dir).ok();
|
fs::create_dir_all(&config.spool_dir).ok();
|
||||||
|
|
||||||
let pending = read_json::<Vec<ScanEvent>>(&data_dir.join("pending.json")).unwrap_or_default();
|
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 =
|
let print_history =
|
||||||
read_json::<Vec<PalletPrintRecord>>(&data_dir.join("print-history.json"))
|
read_json::<Vec<PalletPrintRecord>>(&data_dir.join("print-history.json"))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@ -65,6 +68,8 @@ impl SharedState {
|
|||||||
config: Arc::new(Mutex::new(config)),
|
config: Arc::new(Mutex::new(config)),
|
||||||
jobs: Arc::new(Mutex::new(VecDeque::new())),
|
jobs: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
pending: Arc::new(Mutex::new(pending)),
|
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)),
|
print_history: Arc::new(Mutex::new(print_history)),
|
||||||
auth: Arc::new(Mutex::new(auth)),
|
auth: Arc::new(Mutex::new(auth)),
|
||||||
data_dir,
|
data_dir,
|
||||||
@ -81,6 +86,22 @@ impl SharedState {
|
|||||||
write_json_atomic(&self.data_dir.join("pending.json"), &*self.pending.lock())
|
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<()> {
|
pub fn persist_print_history(&self) -> Result<()> {
|
||||||
write_json_atomic(
|
write_json_atomic(
|
||||||
&self.data_dir.join("print-history.json"),
|
&self.data_dir.join("print-history.json"),
|
||||||
@ -224,7 +245,7 @@ impl SharedState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_count(&self) -> usize {
|
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",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "FieldLens Print",
|
"productName": "FieldLens Print",
|
||||||
"version": "0.1.28",
|
"version": "0.1.30",
|
||||||
"identifier": "dev.arrowstack.fieldlens.print",
|
"identifier": "dev.arrowstack.fieldlens.print",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@ -47,11 +47,18 @@
|
|||||||
"policykit-1"
|
"policykit-1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDAwQURCMzYxRTNGQTJBODgKUldTSUt2cmpZYk90QU1KNEtLVmgzZ1FFd1FmMnY0SU1ETVZMMVVQSE9TM2VsS1RJdDlTZHFFS3UK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYwNzYwMEYwN0YwNjI4QTEKUldTaEtBWi84QUIyWUxYUVg3V0RqSDlJbVc4Ymh2TEh5dDFHZnNDUURlTjVuc09WZzdmajM1VzEK",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://fieldlens.arrowstack.dev/downloads/latest.json"
|
"https://fieldlens.arrowstack.dev/downloads/latest.json"
|
||||||
],
|
],
|
||||||
|
|||||||
20
src/main.ts
20
src/main.ts
@ -142,7 +142,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.28'
|
let currentVersion = '0.1.30'
|
||||||
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
|
||||||
@ -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 () => {
|
el('login').addEventListener('click', async () => {
|
||||||
const login = el<HTMLButtonElement>('login')
|
const login = el<HTMLButtonElement>('login')
|
||||||
const state = el('login-state')
|
const state = el('login-state')
|
||||||
@ -942,6 +956,7 @@ el('login').addEventListener('click', async () => {
|
|||||||
role: result.role ?? null,
|
role: result.role ?? null,
|
||||||
}
|
}
|
||||||
renderAuthStatus()
|
renderAuthStatus()
|
||||||
|
void syncAfterLogin()
|
||||||
} else if (result.status === 'expired' || result.status === 'denied') {
|
} else if (result.status === 'expired' || result.status === 'denied') {
|
||||||
window.clearInterval(loginTimer)
|
window.clearInterval(loginTimer)
|
||||||
loginTimer = undefined
|
loginTimer = undefined
|
||||||
@ -1149,6 +1164,7 @@ el('install-update').addEventListener('click', () => { void installAvailableUpda
|
|||||||
await listen<Job>('capture-job', (event) => {
|
await listen<Job>('capture-job', (event) => {
|
||||||
jobs = [event.payload, ...jobs.filter((job) => job.id !== event.payload.id)].slice(0, 200)
|
jobs = [event.payload, ...jobs.filter((job) => job.id !== event.payload.id)].slice(0, 200)
|
||||||
void refreshStatus()
|
void refreshStatus()
|
||||||
|
void syncAfterLogin()
|
||||||
})
|
})
|
||||||
|
|
||||||
initOcrRefinement(el('ocr-refinement-root'))
|
initOcrRefinement(el('ocr-refinement-root'))
|
||||||
@ -1158,6 +1174,8 @@ writeForm(config)
|
|||||||
await refreshPrinters()
|
await refreshPrinters()
|
||||||
await ensureCaptureStarted()
|
await ensureCaptureStarted()
|
||||||
void refreshAuthStatus()
|
void refreshAuthStatus()
|
||||||
|
window.setTimeout(() => { void syncAfterLogin() }, 5000)
|
||||||
|
window.setInterval(() => { void syncAfterLogin() }, 30000)
|
||||||
|
|
||||||
const readyForRunning = Boolean(
|
const readyForRunning = Boolean(
|
||||||
virtualPrinterStatus?.installed
|
virtualPrinterStatus?.installed
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user