feat(fpagent): show scanner availability and gate capture

Consume fpagent's fprint-device-status {available, device, captureAgentId}
in the /fingerprint wizard. Availability is tracked tri-state so an agent
that does not report it leaves capture working as before; the header tag
shows offline / no-scanner / ready, the capture panel warns, and capture is
disabled until a supported reader is attached.

Adds a vm-sandbox test for the wizard logic and the OpenSpec change
fpagent-device-availability, archived with the fingerprint-wizard spec.
This commit is contained in:
2026-10-02 15:12:16 +01:00
parent 35d895574f
commit 548e33390c
11 changed files with 458 additions and 9 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-02
@@ -0,0 +1,72 @@
# Design
## Context
See `proposal.md` — Why. fpagent pushes `fprint-device-status`
`{available, device, captureAgentId}` to each connection on connect and to all
connections when availability changes (2 s watcher; probes skipped while an
enrollment runs). `available` means "a supported reader is attached", not that a
capture will succeed. The wizard currently learns only `agent-hello` and the
enrollment events, and its header tag reflects only the socket connection.
## Goals / Non-Goals
**Goals:** surface scanner availability in the wizard; stop a capture from
starting when no reader is attached; keep working against agents that do not
emit the event.
**Non-Goals:** changing the agent or the event contract; blocking capture when
the agent is merely unreachable (pre-existing behavior); a proactive device
picker or wiring guidance.
## Decisions
1. **The client is a pure listener.** The agent already emits on connect and on
change, so the wizard adds one handler and no polling or request event.
2. **Availability is tri-state, not boolean.** `agent.available` starts `null`
(unknown) and becomes `true`/`false` on the first status. An agent that never
emits the event leaves it `null`, so captures are not blocked — the feature
degrades to today's behavior instead of deadlocking older field laptops.
3. **The status tag has four labels.** Offline (socket down), "Scanner not
connected" (`available: false`), "Agent ready" (`available: true`), and
"Agent connected" (unknown). The `title` carries the version and the device
name when known.
4. **Capture is gated at both layers.** The buttons are disabled when
`available === false`, and `capture()` returns early on the same condition so
a race (availability changing between click and handler) cannot start an
enrollment. The agent remains authoritative and still rejects a capture with
no reader.
5. **The gating logic gets a small vm-sandbox test.** `app.js` is a browser
IIFE, so `test/fp-wizard.test.js` loads it in a `node:vm` sandbox with
stubbed `Vue`/`io`/`document` and exercises the availability state, the tag
labels, and the capture refusal directly. No new dependency and no browser
runtime; the remaining end-to-end pass against a real agent is manual, since
`/fingerprint` requires a session.
## Risks / Trade-offs
- [Old agent never emits the event] → availability stays `null`, captures are
allowed, behavior matches today. The status tag shows "Agent connected".
- [Brief wrong tag between socket connect and first status] → the agent emits
the status in the same connection handler as `agent-hello`, so the window is
negligible; the `null` state shows "Agent connected" rather than a false
"Scanner not connected".
- [`available` read as "ready to capture"] → the agent's contract says it means
"attached"; enrollment remains authoritative and a capture can still fail.
- [Watcher is skipped during a capture] → availability is not updated mid-scan,
which is correct: the reader is in use by that capture.
## Migration Plan
Ship the wizard; it is backward compatible with agents that lack the event.
No agent change is required to deploy the wizard, and the full benefit appears
once the field laptops run the agent that emits `fprint-device-status`. Rollback
= revert the wizard.
## Open Questions
None.
@@ -0,0 +1,43 @@
# Proposal: fpagent-device-availability
## Why
fpagent now reports whether a supported fingerprint reader is attached, as
`fprint-device-status {available, device, captureAgentId}`, sent to each portal
client on connect and whenever availability changes. The wizard does not consume
it: the header tag reflects only the socket connection, so a connected agent
with no scanner still reads "Agent connected" and capture is allowed, failing
only when the enrollment reaches the reader. Operators need to see that the
scanner is missing before they try.
## What Changes
- The wizard consumes `fprint-device-status` and tracks availability and the
device name alongside the connected state.
- The header status tag distinguishes agent offline, connected with no scanner,
and ready.
- While no supported reader is attached, the capture panel shows a warning and
the capture/recapture controls are disabled; the capture handler refuses a
racing attempt.
## Capabilities
### New Capabilities
(none)
### Modified Capabilities
- `fingerprint-wizard`: shows scanner availability from `fprint-device-status`
and gates capture on a supported reader being attached.
## Impact
- `tools/public/assets/fp/app.js`: handle the event, a multi-state status tag,
and a gated `capture()`.
- `tools/public/assets/fp/index.html`: disabled controls and an inline warning
when no reader is attached.
- `README.md`, `tools/public/assets/help.html`: document the behavior.
- Depends on `fingerprint-agent`'s `fprint-device-status` event (change
`scanner-hotplug-recovery`). Agents that do not emit it are tolerated:
availability stays unknown and captures are not blocked.
@@ -0,0 +1,44 @@
# Spec Delta: fingerprint-wizard
## ADDED Requirements
### Requirement: Scanner availability is shown and gates capture
The wizard SHALL consume fpagent's `fprint-device-status`
`{available, device, captureAgentId}` event and SHALL distinguish the agent
being offline (socket not connected), connected with no supported reader
attached (`available` false), and ready (`available` true). Until a status is
received, availability SHALL be treated as unknown. The wizard SHALL NOT start a
capture while no supported reader is attached, SHALL re-enable capture when a
reader becomes available, and SHALL clear availability when the agent
disconnects. Availability means "a supported reader is attached"; it does not
guarantee a capture will succeed.
#### Scenario: Reader attached on connect
- **WHEN** the page connects and fpagent reports `fprint-device-status` with
`available: true`
- **THEN** the wizard shows the agent ready and leaves capture enabled
#### Scenario: No reader attached on connect
- **WHEN** the page connects and fpagent reports `fprint-device-status` with
`available: false`
- **THEN** the wizard shows that the scanner is not connected, explains captures
are disabled, and disables the capture controls
#### Scenario: Unplug blocks capture
- **WHEN** fpagent reports `available: false` while the operator is capturing or
between captures
- **THEN** the wizard disables the capture controls and a capture attempt is
refused
#### Scenario: Replug re-enables capture
- **WHEN** fpagent reports `available: true` after the reader is reconnected
- **THEN** the wizard re-enables the capture controls
#### Scenario: Status absent or old agent
- **WHEN** the connected agent never reports `fprint-device-status`
- **THEN** availability stays unknown and the wizard does not block capture
#### Scenario: Disconnect clears availability
- **WHEN** the agent socket disconnects
- **THEN** the wizard shows the agent offline and does not present the reader as
available
@@ -0,0 +1,40 @@
# Tasks
## 1. Consume the availability event
- [x] 1.1 In `tools/public/assets/fp/app.js`, add `agent.available` (default
`null`) and `agent.device` (default `''`); handle `fprint-device-status` to set
both and re-render the tag. Verify the handler is present.
- [x] 1.2 Clear `available` back to `null` and `device` to `''` on `disconnect`
and `connect_error`.
## 2. Show availability
- [x] 2.1 Extend `renderAgentTag` to the four states (offline, scanner not
connected, agent ready, agent connected/unknown) with the device name in the
tag `title`.
- [x] 2.2 Add a computed notice (title/subtitle) for agent-offline and
no-scanner, and render it as a warning `cds-inline-notification` in the
capture panel.
- [x] 2.3 Disable the capture and recapture buttons while
`agent.available === false`, and make `capture()` refuse an attempt in the
same state.
## 3. Docs
- [x] 3.1 Add a paragraph to `README.md` under "The Fingerprint Agent".
- [x] 3.2 Add a line to `tools/public/assets/help.html`.
## 4. Verification
- [x] 4.1 `cd tools && npm test` passes (65 tests, 6 new in
`test/fp-wizard.test.js`).
- [x] 4.2 `openspec validate 2026-10-02-fpagent-device-availability --strict`
passes.
- [x] 4.3 `test/fp-wizard.test.js` loads `app.js` in a vm sandbox and asserts the
availability states, tag labels, capture refusal, and disconnect reset.
- [x] 4.4 End-to-end browser pass against a running agent (tag transitions and
disabled controls) — done with Playwright driving the real page against a
stubbed agent socket; screenshots in the harness run.
- [x] 4.5 Archive the change (`openspec archive`), which syncs the
`fingerprint-wizard` spec.