How it is put together¶
An Electron application: a Node main process that owns the sockets and the databases, and an Angular renderer that draws the console.
analyzer ──TCP──► main process ──► parsers ──► SQLite ──► MySQL / LIS API / result webhook
│ │
└────── IPC ──────► Angular renderer (console, settings)
The main process¶
app/main.ts. It owns everything with a handle attached to it:
- TCP sockets, in either direction.
tcpserverlistens for the analyzer;tcpclientdials it. - SQLite, through
@vscode/sqlite3, atinterface.dbin the data directory. Migrations inapp/sqlite-migrations/are applied in order and recorded, so an installation from two years ago and a fresh one converge. - MySQL, optionally, as the table an LIS on another machine reads.
- Settings, in
electron-store, with automatic backups and encrypted export.
The data directory is named, not derived¶
Electron puts userData under app.getName(), which is the product name. That
would mean renaming the product moved every laboratory's settings, database,
backups and migration history out from under it — silently, with an application
that opens looking brand new.
So app/main.ts names the directory instead, and names it what installations
have always used:
const DATA_DIRECTORY_NAME = 'vlsm-interfacing';
app.setPath('userData', path.join(app.getPath('appData'), DATA_DIRECTORY_NAME));
It runs before anything opens the store or the database, because both resolve
the path when they are constructed. scripts/test-upgrade-identity.mjs fails
the build if either of those two facts stops being true.
The renderer¶
Angular, in src/. Services under src/app/services/ do the protocol work:
| Service | Responsibility |
|---|---|
instrument-interface.service.ts |
Owns each connection; routes bytes to the right helper; persists results |
astm-helper.service.ts |
E1381 framing, checksums, ACK/NAK, E1394 record extraction |
hl7-helper.service.ts |
MLLP block assembly, HL7 parsing, per-analyzer field mapping |
raw-data-processor.service.ts |
Re-derives results from stored raw transmissions |
database.service.ts |
SQLite and MySQL persistence, sync state |
utilities.service.ts |
Control-character handling, date formatting, logging |
The renderer is privileged, and that is why the check exists¶
It runs with nodeIntegration: true and contextIsolation: false. Anything
that executes as script in it executes with full Node — the filesystem, the
network, the operator's machine.
That is safe today for exactly two reasons, and only those two:
- Every value reaching the DOM goes through Angular interpolation, which escapes it. No analyzer or LIS text can become script.
- The window only ever loads the bundled
file://build. No remote content arrives in a privileged context.
Both are invisible in the code that depends on them. A single [innerHTML]
added to render a formatted result, or one remote URL opened in the window,
converts a rendering convenience into remote code execution — and whoever writes
that line has no reason to connect it to a webPreferences setting written
years earlier.
scripts/check-electron-safety.mjs makes that connection at the moment it
matters, failing the build on unescaped HTML sinks, remote window loads, second
web contexts, weakened webPreferences, and runtime code evaluation. A finding
that is genuinely safe is waived on the line, with a reason:
A bare marker without a reason is still rejected. The check reads whole files rather than single lines, because a call the formatter wrapped across two lines is exactly the one nobody would notice.
Testing at the level that matters¶
src/app/testing/wire-harness.ts feeds bytes into the real services through a
fake socket and a fake database, so a test asserts from bytes in to rows out.
A regression anywhere in framing, parsing or persistence shows up as a failing
test rather than as a wrong result in a laboratory.
src/app/services/analyzer-captures.spec.ts replays transmissions shaped
exactly like production captures. Those tests are the contract with the field:
a change that alters what a real analyzer's message stores has to fail there
first.