import { BundleType, getBundleType, getVersion } from '@tauri-apps/api/app' import { invoke } from '@tauri-apps/api/core' import { listen } from '@tauri-apps/api/event' import { openUrl } from '@tauri-apps/plugin-opener' import { relaunch } from '@tauri-apps/plugin-process' import { check, type Update } from '@tauri-apps/plugin-updater' import './styles.css' type CaptureMode = 'raw-proxy' | 'spool-folder' | 'both' type AppConfig = { mode: CaptureMode listenAddr: string forwardAddr: string spoolDir: string apiBaseUrl: string issuer: string clientId: string stationId: string installationId: string deviceId?: string | null } type Barcode = { format: string; value: string } type Job = { id: string source: string label: string bytes: number barcodes: Barcode[] capturedAt: string forwarded: boolean error?: string | null } type Status = { running: boolean mode: CaptureMode pending: number jobs: number } type LoginStart = { userCode: string verificationUri: string verificationUriComplete?: string | null expiresIn: number interval: number } const app = document.querySelector('#app')! let config: AppConfig let jobs: Job[] = [] let status: Status let loginTimer: number | undefined let currentVersion = '0.1.1' let bundleType: BundleType | null = null let availableUpdate: Update | null = null let updateBusy = false app.innerHTML = `
FL

FieldLens Print

Digital barcode capture before the label leaves the printer.

Loading

Capture

RAW + spool routes

FieldLens

Device login
0Jobs
0Codes
0Pending
Connection settings
App updates Version … Checking for updates shortly after startup.

Recent print jobs

Waiting for capture
` function el(id: string) { return document.getElementById(id) as T } function readForm(): AppConfig { return { ...config, mode: el('mode').value as CaptureMode, listenAddr: el('listenAddr').value.trim(), forwardAddr: el('forwardAddr').value.trim(), spoolDir: el('spoolDir').value.trim(), apiBaseUrl: el('apiBaseUrl').value.trim(), issuer: el('issuer').value.trim(), clientId: el('clientId').value.trim(), stationId: el('stationId').value.trim(), } } function writeForm(next: AppConfig) { el('mode').value = next.mode el('listenAddr').value = next.listenAddr el('forwardAddr').value = next.forwardAddr el('spoolDir').value = next.spoolDir el('apiBaseUrl').value = next.apiBaseUrl el('issuer').value = next.issuer el('clientId').value = next.clientId el('stationId').value = next.stationId } function render() { const pill = el('status-pill') pill.classList.toggle('live', status?.running ?? false) pill.querySelector('span:last-child')!.textContent = status?.running ? `Capturing · ${status.mode}` : 'Stopped' el('job-count').textContent = String(status?.jobs ?? jobs.length) el('code-count').textContent = String(jobs.reduce((sum, job) => sum + job.barcodes.length, 0)) el('pending-count').textContent = String(status?.pending ?? 0) el('job-hint').textContent = jobs.length ? `${jobs.length} retained locally` : 'Waiting for capture' el('jobs').innerHTML = jobs.length ? jobs.map((job) => `
${escapeHtml(job.source)} · ${escapeHtml(job.label)}${new Date(job.capturedAt).toLocaleTimeString()}
${job.barcodes.length ? `${job.barcodes.length} barcode${job.barcodes.length === 1 ? '' : 's'} detected` : 'No supported barcode command detected'}
${job.barcodes.map((code) => `${escapeHtml(code.format)} · ${escapeHtml(code.value)}`).join('')}
${job.bytes.toLocaleString()} bytes · ${job.forwarded ? 'forwarded' : 'not forwarded'}${job.error ? ` · ${escapeHtml(job.error)}` : ''}
`).join('') : '
No jobs captured yet.
' } function escapeHtml(value: string) { return value.replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]!)) } async function refreshStatus() { status = await invoke('get_status') jobs = await invoke('recent_jobs') render() } el('save').addEventListener('click', async () => { config = await invoke('save_config', { config: readForm() }) writeForm(config) await refreshStatus() }) el('start').addEventListener('click', async () => { config = await invoke('save_config', { config: readForm() }) await invoke('start_capture') await refreshStatus() }) el('stop').addEventListener('click', async () => { await invoke('stop_capture') await refreshStatus() }) el('sync').addEventListener('click', async () => { const result = await invoke<{ synced: number; pending: number }>('sync_pending') el('login-state').textContent = `Synced ${result.synced}. ${result.pending} pending.` await refreshStatus() }) el('login').addEventListener('click', async () => { const session = await invoke('begin_login') el('login-code').textContent = session.userCode el('login-state').textContent = 'Authorize this device in your browser…' const target = session.verificationUriComplete || session.verificationUri await openUrl(target) if (loginTimer) window.clearInterval(loginTimer) loginTimer = window.setInterval(async () => { try { const result = await invoke<{ status: string }>('poll_login') if (result.status === 'authorized') { window.clearInterval(loginTimer) loginTimer = undefined el('login-state').textContent = 'Signed in to FieldLens.' el('login-code').textContent = '' } else if (result.status === 'expired' || result.status === 'denied') { window.clearInterval(loginTimer) loginTimer = undefined el('login-state').textContent = `Login ${result.status}.` } } catch (error) { el('login-state').textContent = String(error) } }, Math.max(3, session.interval) * 1000) }) async function checkForUpdates(manual = false) { if (updateBusy) return const state = el('update-state') const installButton = el('install-update') const checkButton = el('check-update') try { checkButton.disabled = true state.textContent = manual ? 'Checking for updates…' : 'Checking quietly for updates…' const update = await check() availableUpdate = update if (!update) { installButton.hidden = true state.textContent = `FieldLens Print ${currentVersion} is up to date.` return } state.textContent = `Version ${update.version} is available.` installButton.hidden = false installButton.textContent = bundleType === BundleType.Deb ? 'Open Linux installer' : 'Download & restart' } catch (error) { if (manual) state.textContent = `Update check failed: ${String(error)}` else state.textContent = 'Automatic update check unavailable. You can retry manually.' } finally { checkButton.disabled = false } } async function installAvailableUpdate() { if (!availableUpdate || updateBusy) return const state = el('update-state') const button = el('install-update') const progress = el('update-progress') const bar = progress.querySelector('span') as HTMLSpanElement if (bundleType === BundleType.Deb) { state.textContent = 'Debian packages update through the Linux installer.' await openUrl('https://fieldlens.arrowstack.dev/downloads') return } if (status?.running) { state.textContent = 'Stop capture before installing an update.' return } updateBusy = true button.disabled = true progress.hidden = false bar.style.width = '8%' let downloaded = 0 let total = 0 try { state.textContent = `Downloading FieldLens Print ${availableUpdate.version}…` await availableUpdate.downloadAndInstall((event) => { if (event.event === 'Started') { total = event.data.contentLength ?? 0 downloaded = 0 bar.style.width = '10%' } else if (event.event === 'Progress') { downloaded += event.data.chunkLength if (total > 0) { const percent = Math.min(92, Math.max(10, Math.round((downloaded / total) * 100))) bar.style.width = `${percent}%` } } else if (event.event === 'Finished') { bar.style.width = '100%' state.textContent = 'Update installed. Restarting…' } }) await relaunch() } catch (error) { state.textContent = `Update failed: ${String(error)}` progress.hidden = true } finally { updateBusy = false button.disabled = false } } el('check-update').addEventListener('click', () => { void checkForUpdates(true) }) el('install-update').addEventListener('click', () => { void installAvailableUpdate() }) await listen('capture-job', (event) => { jobs = [event.payload, ...jobs.filter((job) => job.id !== event.payload.id)].slice(0, 200) void refreshStatus() }) config = await invoke('get_config') writeForm(config) await refreshStatus() currentVersion = await getVersion() bundleType = await getBundleType().catch(() => null) el('version-label').textContent = `Version ${currentVersion}${bundleType ? ` · ${bundleType}` : ''}` window.setTimeout(() => { void checkForUpdates(false) }, 3000) window.setInterval(() => { void refreshStatus() }, 5000)