Dentolize · X-Ray Integration Walkthrough
On this pageBusiness viewTechnical view

The Desktop App & Scanner Integration

This page covers the companion pull request dentolize/xolize-core#6 ("Feature/xray integration"), checked out separately from the main Dentolize monorepo. All file references on this page are in that repository, not /work/repo, unless stated otherwise.

Business view

The half of this feature that actually talks to a scanner is a real, installable Windows desktop application — internally called the Dentolize Bridge — not a mock or a placeholder. A clinic installs it on the workstation physically connected to their X-ray sensor/scanner, pastes in an Access Key generated from Dentolize's web app, and picks a branch, a room, and a TWAIN scanner device. From then on, the app sits quietly in the background: when a staff member clicks Acquire in the browser, a request pops up as a modal the operator must Accept or Reject; on accept, it drives the real scanner hardware through a native helper process, shows the operator a preview to select the right page (for multi-page captures), and uploads the result.

It supports 10 languages (English, Arabic, French, German, Russian, Italian, Spanish, Chinese, Portuguese, Turkish) with right-to-left layout for Arabic, ships auto-update via GitHub Releases, and includes an upload queue for images that couldn't be sent immediately (manual-upload mode, or a network hiccup) with per-item retry.

What's genuinely not finished yet: per the desktop repo's own XRAY_RELEASE_ACCEPTANCE.md, the "real-sensor validation matrix" — actually testing against physical scanner hardware from different vendors — is entirely "Not run / TBD" as of this PR. Everything validated so far is against TWAIN's reference sample data source and the app's own end-to-end simulation mode. This is a stated prerequisite for general availability, not something this documentation is inferring.

Technical view

Two processes, one app

  • apps/clinic-desktop — the Electron main process (Node.js). Creates the app window with contextIsolation: true, sandbox: true, nodeIntegration: false, and a strict Content-Security-Policy allowing only the configured API/WS origins plus a custom xray-image:// scheme used to show captured-image previews without exposing them on the filesystem (src/app/app.ts). Blocks in-app navigation and new-window creation outright.
  • apps/clinic-desktop-frontend — the React/antd renderer, talking to the main process exclusively through window.desktopApp / window.auth / window.desktopConfig / window.xray / window.desktopUpdates, a bridge exposed via contextBridge.exposeInMainWorld in apps/clinic-desktop/src/app/api/main.preload.ts. Every payload crossing that bridge is validated with Zod schemas on the way in, so the renderer can't hand the privileged main process malformed data.
  • apps/twain-cli — a separate native Win32 C++ executable that does the actual TWAIN scanner session (device selection, image transfer, BMP/PNG encoding) and communicates back over stdout as JSON. Running it out-of-process means a scanner driver crash can't take down the Electron app. The main process verifies the helper's SHA-256 against resources/twain-helper-sha256.json before every invocation.

Authenticating and connecting

  • The Access Key is pasted into features/AccessKey/AccessKeyForm.tsx and validated against POST /api/access-keys/validate (matching the server route documented in Access Keys & Security Model), requiring the same three scopes the "Desktop App" checkbox grants. It's stored in the OS credential vault via keytar (apps/clinic-desktop/src/utils/credentialStore.ts) — never written to a plaintext config file in production builds (an E2E-only fallback stores it in electron-store for automated testing).
  • Session state (validated, current room lease) lives only in memory (src/utils/desktopSession.ts) — the app always re-validates online on startup, it never trusts a cached "was authenticated" flag across restarts.
  • The app continuously re-validates: it polls /api/access-keys/validate every 30 seconds even before a room is selected, and force-disconnects on a 401.
  • Branch/room selection (features/GeneralSettings/GeneralSettings.tsx) calls GET /api/branches, then POST /api/xray-acquisition/connect to obtain the lease token described in Access Keys & Security Model; a 409 ROOM_OCCUPIED response (another instance already holding that room) is handled by prompting for a different room rather than failing silently.

The capture flow and state machine

apps/clinic-desktop/src/utils/acquisitionManager.ts implements: IDLE → REQUEST_RECEIVED → ACCEPTED → IN_PROGRESS → PREVIEW → CAPTURED (or REJECTED at the acceptance step), reporting every transition to the server via POST /api/xray-acquisition/:id/transitions with a per-transition idempotency eventId — matching the transition endpoint and idempotency model described in X-Ray Acquisition Workflow. Incoming requests arrive over the same xrayAcquisitionRequested GraphQL subscription documented server-side, using graphql-ws, not polling.

Capture itself goes through the native twain-cli helper in production; a simulation path exists (runE2ECapture()) but is explicitly gated to non-production, test-only builds and writes fixture files instead of talking to a real device — it cannot run in a packaged release. Once a page is captured, only the operator's selected page is encrypted and staged locally (encryptStagingArtifact); any other captured-but-unselected pages are deleted immediately, not kept around. Before every capture, the selected device identity is re-validated against a fresh device list to reject an ambiguous or since-disappeared source.

Upload

Matching the server-side upload-intent flow, apps/clinic-desktop/src/utils/uploadQueue.ts requests an upload intent (POST /api/xray-acquisition/:id/upload-intents), then performs a multipart POST directly to the presigned S3 form returned in that response, then calls POST /api/xray-acquisition/:id/complete. Failed uploads land in a visible Upload Queue screen (features/UploadQueue/) with per-item retry; apps/clinic-desktop-frontend-e2e/src/recovery-policy.spec.ts unit-tests the retry decision logic specifically: a 409 conflict after a successful upload retries only the idempotent /complete call (not a re-upload), while a 410 UPLOAD_INTENT_EXPIRED response restarts the whole upload — this lines up with the expiry/idempotency behavior documented in X-Ray Acquisition Workflow.

Crash recovery

On startup, acquisitionManager.ts's restoreLocalState/syncFromServer destroys any plaintext capture artifacts left over from an interrupted PREVIEW/IN_PROGRESS state (they're never resurrected — safer to force a retake than to trust a half-finished local file), reconciles its local upload journal against the server's own list of the room's active acquisitions, and reports FAILED with a specific error code if an expected encrypted/staged file has gone missing. A resumable WAITING/ACCEPTED request that was in flight when the app died is picked back up rather than lost.

Updates and packaging

Despite legacy Squirrel-event handling code still present in the app (squirrel.events.ts, invoked defensively at startup), the actual packaging target is electron-builder with an NSIS installer (src/app/options/maker.options.json — one-click, per-user, no elevation required), publishing to a private GitHub releases repo. The Squirrel code path is not reachable with this packaging configuration and appears to be inherited boilerplate rather than an active update mechanism — worth a cleanup pass, not a functional concern. electron-updater's autoUpdater (update.events.ts) deliberately will not force a restart while a capture is in progress or an upload is active, rechecking every 5 seconds until it's safe — so an auto-update can't interrupt a scan mid-capture. Releases are currently built and shipped unsigned, framed explicitly in the repo's own acceptance doc as a "temporary unsigned pilot."

End-to-end testing against the same sandbox used for these docs

apps/clinic-desktop-frontend-e2e/src/sandbox-live.spec.ts is a 436-line, non-mocked test that logs into the real clinic-web sandbox, creates a real Access Key with the desktop scopes, launches the packaged desktop app, pastes in the key, selects a branch/room/TWAIN device, and drives a full request through capture, preview, accept, and upload against the live sandbox API. This isn't hypothetical: the Walkthrough screenshots in this documentation were captured from that same shared sandbox, and several of them show real accumulated state (dozens of Access Keys, 52 logged acquisition requests) produced by this exact test suite running on a schedule.