feat(ui): split setup and running views
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:
Admin 2026-09-24 17:05:32 +00:00
parent cc7c5df011
commit 481b1c181c
5 changed files with 339 additions and 141 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@fieldlens/print",
"version": "0.1.15",
"version": "0.1.16",
"private": true,
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
[package]
name = "fieldlens-print"
version = "0.1.15"
version = "0.1.16"
description = "FieldLens print-stream barcode capture companion"
authors = ["Arrowstack"]
edition = "2021"

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "FieldLens Print",
"version": "0.1.15",
"version": "0.1.16",
"identifier": "dev.arrowstack.fieldlens.print",
"build": {
"beforeDevCommand": "npm run dev",

View File

@ -85,7 +85,7 @@ let config: AppConfig
let jobs: Job[] = []
let status: Status
let loginTimer: number | undefined
let currentVersion = '0.1.15'
let currentVersion = '0.1.16'
let bundleType: BundleType | null = null
let availableUpdate: Update | null = null
let availableManualUpdate: ManualUpdate | null = null
@ -93,6 +93,7 @@ let updateBusy = false
let captureError: string | null = null
let systemPrinters: SystemPrinter[] = []
let virtualPrinterStatus: VirtualPrinterStatus | null = null
let activeView: 'setup' | 'running' = 'setup'
app.innerHTML = `
<div class="shell">
@ -104,155 +105,194 @@ app.innerHTML = `
<p class="subtitle">Capture and inspect print jobs before the printer driver renders them.</p>
</div>
</div>
<div id="status-pill" class="status"><span class="dot"></span><span>Loading</span></div>
<div class="topbar-actions">
<nav class="view-tabs" aria-label="FieldLens Print view">
<button class="view-tab active" id="setup-tab" type="button">Setup</button>
<button class="view-tab" id="running-tab" type="button">Running</button>
</nav>
<div id="status-pill" class="status"><span class="dot"></span><span>Loading</span></div>
</div>
</header>
<main class="grid">
<section class="card route-card">
<div class="card-head">
<div>
<span class="eyebrow">PRINTER ROUTING</span>
<h2>Destination</h2>
</div>
<button class="icon-btn" id="refresh-printers" title="Refresh printers" aria-label="Refresh printers">↻</button>
</div>
<div class="card-body">
<label class="field-label">Physical printer
<select id="forwardPrinter">
<option value="">Choose a printer…</option>
</select>
<span id="forward-printer-help" class="help">Choose the printer that should receive production jobs.</span>
</label>
<div class="route-tiles">
<div class="route-tile">
<div class="route-icon live-route">P</div>
<main>
<section id="setup-view" class="app-view">
<div class="grid">
<section class="card route-card">
<div class="card-head">
<div>
<strong>FieldLens Print</strong>
<span>Capture + physical print</span>
<span class="eyebrow">PRINTER ROUTING</span>
<h2>Destination</h2>
</div>
<button class="icon-btn" id="refresh-printers" title="Refresh printers" aria-label="Refresh printers">↻</button>
</div>
<div class="route-tile">
<div class="route-icon test-route">T</div>
<div>
<strong>FieldLens Test Capture</strong>
<span>Full pipeline, no paper</span>
</div>
</div>
</div>
<div class="printer-state">
<span class="state-dot"></span>
<div>
<strong>System printer</strong>
<span id="virtual-printer-state">Checking FieldLens queues…</span>
</div>
</div>
<div class="card-body">
<label class="field-label">Physical printer
<select id="forwardPrinter">
<option value="">Choose a printer…</option>
</select>
<span id="forward-printer-help" class="help">Choose the printer that should receive production jobs.</span>
</label>
<div class="actions printer-actions">
<button class="btn primary" id="install-printer">Create / repair printers</button>
<button class="btn ghost" id="self-test">Run capture self-test</button>
<button class="btn ghost danger" id="remove-printer">Remove</button>
</div>
<details class="advanced">
<summary>Advanced capture settings</summary>
<div class="advanced-body">
<div class="fields">
<label>Capture mode
<select id="mode">
<option value="raw-proxy">RAW 9100 proxy</option>
<option value="spool-folder">Spool folder</option>
<option value="both">Both routes</option>
</select>
</label>
<label>Listen address
<input id="listenAddr" placeholder="127.0.0.1:9100" />
</label>
<label>RAW/TCP destination
<input id="forwardAddr" placeholder="10.0.0.50:9100" />
</label>
<label>Spool capture folder
<input id="spoolDir" />
</label>
<div class="route-tiles">
<div class="route-tile">
<div class="route-icon live-route">P</div>
<div>
<strong>FieldLens Print</strong>
<span>Capture + physical print</span>
</div>
</div>
<div class="route-tile">
<div class="route-icon test-route">T</div>
<div>
<strong>FieldLens Test Capture</strong>
<span>Full pipeline, no paper</span>
</div>
</div>
</div>
<div class="actions compact-actions">
<button class="btn ghost" id="save">Save settings</button>
<button class="btn ghost" id="start">Start capture</button>
<button class="btn ghost danger" id="stop">Stop capture</button>
</div>
</div>
</details>
</div>
</section>
<section class="card activity-card">
<div class="card-head">
<div>
<span class="eyebrow">ACTIVITY</span>
<h2>FieldLens</h2>
</div>
<span id="job-hint" class="muted">Waiting for capture</span>
</div>
<div class="card-body">
<div class="metrics">
<div class="metric"><b id="job-count">0</b><span>Jobs</span></div>
<div class="metric"><b id="code-count">0</b><span>Codes</span></div>
<div class="metric"><b id="pending-count">0</b><span>Pending</span></div>
</div>
<div class="account-row">
<div>
<strong>FieldLens account</strong>
<span id="login-state" class="muted">Not signed in.</span>
<div id="login-code" class="login-code"></div>
</div>
<div class="inline-actions">
<button class="btn ghost" id="sync">Sync</button>
<button class="btn blue" id="login">Sign in</button>
</div>
</div>
<details class="advanced">
<summary>Connection & updates</summary>
<div class="advanced-body">
<div class="fields">
<label class="full">FieldLens API<input id="apiBaseUrl" /></label>
<label class="full">Keycloak issuer<input id="issuer" /></label>
<label>Desktop client<input id="clientId" /></label>
<label>Station ID<input id="stationId" placeholder="optional" /></label>
</div>
<div class="update-box">
<div class="printer-state">
<span class="state-dot"></span>
<div>
<strong>App updates</strong>
<span id="version-label" class="muted">Version …</span>
<small id="update-state" class="muted">Checking for updates shortly after startup.</small>
<strong>System printer</strong>
<span id="virtual-printer-state">Checking FieldLens queues…</span>
</div>
</div>
<div class="actions printer-actions">
<button class="btn primary" id="install-printer">Create / repair printers</button>
<button class="btn ghost" id="self-test">Run capture self-test</button>
<button class="btn ghost danger" id="remove-printer">Remove</button>
</div>
</div>
</section>
<section class="card settings-card">
<div class="card-head">
<div>
<span class="eyebrow">SETTINGS</span>
<h2>FieldLens Print</h2>
</div>
<span id="settings-state" class="muted">Local configuration</span>
</div>
<div class="card-body">
<div class="account-row settings-account">
<div>
<strong>FieldLens account</strong>
<span id="login-state" class="muted">Not signed in.</span>
<div id="login-code" class="login-code"></div>
</div>
<div class="inline-actions">
<button class="btn ghost" id="check-update">Check</button>
<button class="btn blue" id="install-update" hidden>Install</button>
<button class="btn ghost" id="sync">Sync</button>
<button class="btn blue" id="login">Sign in</button>
</div>
<div id="update-progress" class="update-progress" hidden><span></span></div>
</div>
<details class="advanced" open>
<summary>Capture settings</summary>
<div class="advanced-body">
<div class="fields">
<label>Capture mode
<select id="mode">
<option value="raw-proxy">RAW 9100 proxy</option>
<option value="spool-folder">Spool folder</option>
<option value="both">Both routes</option>
</select>
</label>
<label>Listen address
<input id="listenAddr" placeholder="127.0.0.1:9100" />
</label>
<label>RAW/TCP destination
<input id="forwardAddr" placeholder="10.0.0.50:9100" />
</label>
<label>Spool capture folder
<input id="spoolDir" />
</label>
</div>
<div class="actions compact-actions">
<button class="btn ghost" id="save">Save settings</button>
<button class="btn blue" id="start">Start capture</button>
<button class="btn ghost danger" id="stop">Stop capture</button>
</div>
</div>
</details>
<details class="advanced">
<summary>Connection & updates</summary>
<div class="advanced-body">
<div class="fields">
<label class="full">FieldLens API<input id="apiBaseUrl" /></label>
<label class="full">Keycloak issuer<input id="issuer" /></label>
<label>Desktop client<input id="clientId" /></label>
<label>Station ID<input id="stationId" placeholder="optional" /></label>
</div>
<div class="update-box">
<div>
<strong>App updates</strong>
<span id="version-label" class="muted">Version …</span>
<small id="update-state" class="muted">Checking for updates shortly after startup.</small>
</div>
<div class="inline-actions">
<button class="btn ghost" id="check-update">Check</button>
<button class="btn blue" id="install-update" hidden>Install</button>
</div>
<div id="update-progress" class="update-progress" hidden><span></span></div>
</div>
</div>
</details>
</div>
</details>
</section>
</div>
</section>
<section class="card jobs-card">
<div class="card-head">
<div>
<span class="eyebrow">RECENT ACTIVITY</span>
<h2>Print jobs</h2>
</div>
<span class="muted">Newest first</span>
</div>
<div class="card-body">
<div id="jobs" class="jobs"></div>
<section id="running-view" class="app-view" hidden>
<div class="running-grid">
<section class="card running-summary-card">
<div class="card-head">
<div>
<span class="eyebrow">RUNNING</span>
<h2>Capture activity</h2>
</div>
<span id="job-hint" class="muted">Waiting for capture</span>
</div>
<div class="card-body">
<div class="metrics">
<div class="metric"><b id="job-count">0</b><span>Jobs</span></div>
<div class="metric"><b id="code-count">0</b><span>Codes</span></div>
<div class="metric"><b id="pending-count">0</b><span>Pending</span></div>
</div>
<div class="running-route">
<div>
<span class="eyebrow">PHYSICAL DESTINATION</span>
<strong id="running-printer">No printer selected</strong>
</div>
<div class="inline-actions">
<button class="btn ghost" id="running-self-test">Test capture</button>
<button class="btn ghost danger" id="running-stop">Stop capture</button>
</div>
</div>
<span id="running-action-state" class="muted running-action-state"></span>
</div>
</section>
<section class="card jobs-card">
<div class="card-head">
<div>
<span class="eyebrow">RECENT ACTIVITY</span>
<h2>Print jobs</h2>
</div>
<span class="muted">Newest first</span>
</div>
<div class="card-body">
<div id="jobs" class="jobs"></div>
</div>
</section>
</div>
</section>
</main>
@ -263,6 +303,14 @@ function el<T extends HTMLElement>(id: string) {
return document.getElementById(id) as T
}
function setView(view: 'setup' | 'running') {
activeView = view
el('setup-view').hidden = view !== 'setup'
el('running-view').hidden = view !== 'running'
el('setup-tab').classList.toggle('active', view === 'setup')
el('running-tab').classList.toggle('active', view === 'running')
}
function readForm(): AppConfig {
return {
...config,
@ -304,6 +352,7 @@ function renderPrinters() {
if (options.some((printer) => printer.name === current)) select.value = current
const selected = options.find((printer) => printer.name === select.value)
el('running-printer').textContent = selected?.name || config?.forwardPrinter || 'No physical printer selected'
const help = el('forward-printer-help')
if (selected?.address) {
help.textContent = `Detected RAW network endpoint: ${selected.address}`
@ -459,22 +508,38 @@ el('install-printer').addEventListener('click', async () => {
}
button.disabled = true
state.textContent = 'Creating FieldLens Print… administrator approval may appear.'
let stoppedForRepair = false
try {
state.textContent = 'Stopping capture before printer repair…'
await invoke('stop_capture')
stoppedForRepair = true
await refreshStatus()
state.textContent = 'Repairing printers and migrating captured jobs… administrator approval may appear.'
virtualPrinterStatus = await invoke<VirtualPrinterStatus>('install_virtual_printer', { targetPrinter })
config = await invoke<AppConfig>('save_config', { config: readForm() })
await restartCapture()
state.textContent = 'Restarting capture and processing migrated backlog…'
const restarted = await ensureCaptureStarted()
if (!restarted) {
throw new Error(captureError || 'capture did not restart')
}
stoppedForRepair = false
await refreshPrinters()
setView('running')
} catch (error) {
state.textContent = `Printer setup failed: ${String(error)}`
if (stoppedForRepair) {
await ensureCaptureStarted().catch(() => undefined)
}
} finally {
button.disabled = false
}
})
el('self-test').addEventListener('click', async () => {
const button = el<HTMLButtonElement>('self-test')
const state = el('virtual-printer-state')
async function submitCaptureSelfTest(button: HTMLButtonElement, state: HTMLElement) {
button.disabled = true
state.textContent = 'Submitting a no-paper capture self-test…'
try {
@ -486,6 +551,20 @@ el('self-test').addEventListener('click', async () => {
} finally {
button.disabled = false
}
}
el('self-test').addEventListener('click', () => {
void submitCaptureSelfTest(
el<HTMLButtonElement>('self-test'),
el('virtual-printer-state'),
)
})
el('running-self-test').addEventListener('click', () => {
void submitCaptureSelfTest(
el<HTMLButtonElement>('running-self-test'),
el('running-action-state'),
)
})
el('remove-printer').addEventListener('click', async () => {
@ -512,7 +591,8 @@ el('save').addEventListener('click', async () => {
el('start').addEventListener('click', async () => {
config = await invoke<AppConfig>('save_config', { config: readForm() })
await restartCapture()
const started = await restartCapture()
if (started) setView('running')
})
el('stop').addEventListener('click', async () => {
@ -521,6 +601,18 @@ el('stop').addEventListener('click', async () => {
await refreshStatus()
})
el('setup-tab').addEventListener('click', () => setView('setup'))
el('running-tab').addEventListener('click', () => setView('running'))
el('running-stop').addEventListener('click', async () => {
const state = el('running-action-state')
state.textContent = 'Stopping capture…'
captureError = null
await invoke('stop_capture')
await refreshStatus()
state.textContent = 'Capture stopped. Open Setup to restart or change configuration.'
})
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.`
@ -710,6 +802,14 @@ writeForm(config)
await refreshPrinters()
await ensureCaptureStarted()
const readyForRunning = Boolean(
virtualPrinterStatus?.installed
&& config.forwardPrinter
&& status?.running
&& !captureError
)
setView(readyForRunning ? 'running' : 'setup')
currentVersion = await getVersion()
bundleType = await getBundleType().catch(() => null)
el('version-label').textContent = `Version ${currentVersion}${bundleType ? ` · ${bundleType}` : ''}`

View File

@ -543,3 +543,101 @@ select:focus {
color: #9fcef5;
border-color: #294865;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 12px;
}
.view-tabs {
display: inline-flex;
gap: 4px;
padding: 4px;
border: 1px solid #202a36;
border-radius: 10px;
background: #0d131a;
}
.view-tab {
border: 0;
border-radius: 7px;
padding: 7px 10px;
background: transparent;
color: #718094;
font-size: 10.5px;
font-weight: 800;
}
.view-tab:hover {
color: #dce4ed;
background: #151d27;
}
.view-tab.active {
background: #1a2b24;
color: #b9f3d5;
}
.app-view[hidden] {
display: none;
}
.settings-card .settings-account {
margin-top: 0;
}
.running-grid {
display: grid;
gap: 16px;
}
.running-summary-card .card-body {
display: grid;
gap: 14px;
}
.running-route {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
border: 1px solid #222c38;
border-radius: 11px;
padding: 12px;
background: #0d131a;
}
.running-route > div:first-child {
min-width: 0;
display: grid;
gap: 4px;
}
.running-route strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
}
.running-action-state {
min-height: 14px;
}
@media (max-width: 860px) {
.topbar-actions {
width: 100%;
justify-content: space-between;
}
.topbar {
flex-wrap: wrap;
}
.running-route {
align-items: flex-start;
flex-direction: column;
}
}