06 — Screen Specifications (Biometric Module)
- 1. Biometric Hub / Sync Status (
/biometric) - 2. Device List (
/biometric/devices) - 3. Device Register (
/biometric/devices/new) - 4. Device Detail (
/biometric/devices/:id) - 5. Enrollment (
/biometric/devices/:id/enroll) - 6. Biometric Log List (
/biometric/logs) - 7. Biometric Log Detail (bottom sheet)
- 8. Attendance-Verification Check-in (
/biometric/verify) - 9. Motion & state cross-cutting (all screens)
- 10. Implementation status matrix
Production-grade specs for each Biometric screen: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens:
Sp= 00-shared/02 §3,Ty= typography §2,El= elevation §5,R= radius §4,Mo= motion §6. All components reference 00-shared/03. Screens are(planned)unless noted — no backend endpoint exists for them yet (see 12_API_Mapping.md).
1. Biometric Hub / Sync Status (/biometric)
Chrome: standard AppBar "Biometric", subtitle chip of tenant context; body scrollable; padding 16 (phone) / 24 (tablet+).
Layout (phone portrait):
[ AppBar: Biometric ] [Settings gear? no — module hub, back to settings]
Status summary card (AppCard)
"3 of 4 devices active" headlineSmall + icon check/alert
chip row: Active 3 · Inactive 0 · Offline 1 (DeviceStatus, biometric-device.schema.ts:7-11)
Last sync card (SyncStatusTile)
"Last sync 07:45 · 38 punches" + "Sync now" TextButton
Queue card (Dev persona)
"biometric-sync queue: 0 waiting · 2 failed" → deep link Bull-Board
Failed-sync banner (AppBanner error, when >0 failed jobs)
Empty state (AppEmptyState) "No devices — register one" + CTA
States:
- loading →
AppSkeleton(2 cards). - loaded → cards as above; failed-sync banner only when queue shows failures.
- queue-error → queue card shows "queue unavailable" (error chip), banner; no crash.
- offline →
AppOfflineBanner; hub shows cached last-good status (00-shared/06 §5). - permission → hub hidden entirely (route guard), not an error screen.
Data source (today): none of these endpoints exist — the screen consumes the
planned GET /biometric/devices + queue health (Bull-Board API). Until the
biometric-sync worker exists (queue.constants.ts:7, no @Processor), the queue card
will always read "0 processed" — ship the screen only after OQ-4 lands.
Motion: card entrance Mo.m-base stagger 40 ms; status chip color transition 200 ms.
A11y: summary is a live region on refresh; queue failure announced; targets ≥ 48.
Dark/light: token-only; offline badge errorContainer, active primaryContainer.
Tablet/desktop: three-column tile row ≥ 1200 px (summary, last sync, queue); hover
states; Tab order: summary → sync → queue → nav.
2. Device List (/biometric/devices)
Chrome: AppBar "Devices" + AppFAB "Register device"; list with RefreshIndicator.
Row (AppListTile):
leading: AppAvatar (device icon)
title: device name
subtitle: "{model} · {location}" (biometric-device.schema.ts:21-22, 27-28)
trailing: DeviceStatusBadge (active/inactive/offline) + AppMenu (Detail, Disable)
States:
- loading →
AppSkeleton(list). - empty →
AppEmptyStateiconsensors, "No devices registered", CTA inline. - error →
AppErrorStatecodesUNAUTHENTICATED(401 → session),INTERNAL(5xx + requestId). - row
offline→ trailing badgeoffline(error color) + subtle red-tinted tile. - disable →
AppDialogconfirm (destructive) → row removed on server 200 (no optimistic — irreversible operation, per00-shared/08).
Motion: row fade-in stagger 30 ms; badge swap Mo.m-fast.
A11y: each row one semantics node; badge text included in row label.
Tablet/desktop: master-detail — list left, detail pane right (screen 4); Enter on row opens detail.
3. Device Register (/biometric/devices/new)
Chrome: AppBar "Register device"; single AppCard form; CTA bottom AppButton
fullWidth "Register device" (h 48).
Fields (order): name*, deviceId* (monospace hint — printed on hardware), model?,
location?, status default active segmented, config? (collapsible raw JSON editor —
Dev persona only, revealed behind "Advanced" disclosure).
States:
- idle → CTA disabled until name + deviceId valid.
- submitting → CTA
loading; inputs disabled; no double-submit. - 409 duplicate → inline banner "A device with this ID already exists" (unique index
biometric-device.schema.ts:36). - success → navigate to device detail with snackbar "Device registered".
Motion: field focus Mo.m-fast; success → detail slide Mo.m-base.
A11y: labels linked to fields; first invalid focused on submit; advanced disclosure announced.
Keyboard: .next through fields, .done on deviceId submits.
4. Device Detail (/biometric/devices/:id)
Chrome: AppBar device name; status badge inline.
Content (top → bottom):
Header card: name · DeviceStatusBadge · model · location · deviceId (copyable)
Action row: [Sync now] [Enroll] [Edit] [Disable (destructive)]
Config card (collapsible): raw config JSON (biometric-device.schema.ts:30-31)
Sync card: SyncStatusTile — last sync, result, retry count
Punch preview: last 5 logs for this device (index tenantId+deviceId+timestamp, biometric-log.schema.ts:26)
Sync states (POST /biometric/devices/:id/sync):
- idle → "Sync now" enabled.
- running → button spinner + "Syncing…" (disabled), row pulse.
- success → "Sync complete · 38 punches" snackbar + preview refresh.
- failed → inline
AppBanner error"Sync failed — will retry (4×, exponential)" (blueprintRETRIES.md:38); exhausted → "moved to dead-letter" + link to Bull-Board.
Punch preview states: empty ("No punches synced yet") / list (PunchRow x5) / tap →
Log Detail sheet.
A11y: sync result announced via live region; copy button labeled.
5. Enrollment (/biometric/devices/:id/enroll)
Chrome: AppBar "Enroll student"; EnrollStepBar (1 Student → 2 Capture → 3 Confirm);
steps advance only on server/device confirmation.
Step 1 — Student: search field (name/roll) + result list (reuse student search); select → chip "Enrolled: {name}" if already enrolled (warning, confirm overwrite).
Step 2 — Capture: device-driven — "Place finger on scanner" + live status text (capture_ok / capture_fail). Failure hint: "dry hands / reposition" (persona Arjun), retry button; no client-side template processing ever (server/hardware owns the template; OQ-3).
Step 3 — Confirm: summary card (student, device, template fingerprint-hash preview — format undecided) + CTA "Confirm enrollment".
States:
- capture timeout → error step state + retry; device offline → blocked banner.
- duplicate → confirm-overwrite dialog (destructive style).
- success → device detail with "Enrolled: {name}" snackbar.
A11y: step bar announced; capture status live region; all targets ≥ 48.
Keyboard: step 1 search only; steps 2-3 are touch/pointer-first.
Note: this screen is fully blocked today — no template schema, no enrollment
endpoint, vendor SDK pending (FEATURE_ROADMAP.md:56, OQ-3). Design is frozen for review,
not for build.
6. Biometric Log List (/biometric/logs)
Chrome: AppBar "Biometric logs"; filter bar (student search chip, device dropdown,
date range picker, mode chips); RefreshIndicator; results as PunchRows.
Row: student avatar+name, deviceId · mode, timestamp (relative + absolute), status
dot unprocessed (grey) / processed (green — field planned, COLLECTIONS.md:1888).
States:
- loading skeleton; empty "No punches found" (or "No punches for this filter").
- error
AppErrorState; offline → cached last-good + banner. - tap row → Log Detail sheet (screen 7).
Filter persistence: filters survive navigation (client state only, no server query
params contract yet — planned GET /biometric/logs has no defined filter surface,
blueprint Biometric.md:30).
Tablet/desktop: two-pane list/detail; date-range quick presets (Today / This week).
A11y: each row announces timestamp; filter chip toggles announced.
7. Biometric Log Detail (bottom sheet)
Content:
student name + avatar headlineSmall
deviceId · mode bodyMedium
timestamp (absolute + tz) bodySmall
rawData JSON (collapsible) monospace, wrap — forensic payload (biometric-log.schema.ts:21-22)
"Immutable record — never updated" note (blueprint COLLECTIONS.md:1866)
[Open attendance record] → GET /attendance/student/:studentId (attendance.controller.ts:37-43)
States: loading (sheet opens with skeleton), loaded, not-found (row deleted — treat as removed, snackbar). Sheet is non-modal on tablet (pane).
8. Attendance-Verification Check-in (/biometric/verify)
Chrome: AppBar "Verify check-in"; search student → date selector (default today).
Content:
Student card: name, roll, class
Punch timeline (PunchTimeline): logs sorted desc — timestamp, device, mode
Derived status card:
matched → "Present · source: biometric" (AttendanceSource.BIOMETRIC, attendance.schema.ts:18)
mismatched → "Punch found, no attendance record — derivation gap" (warning banner)
no punch → "No punches for this day" (AppEmptyState compact)
[Edit attendance] (when matched/mismatched) → PATCH /attendance/:id (attendance.controller.ts:54-59)
States: loading; student-not-found → inline error; offline → verification blocked (evidence must be live — no cached verdicts).
A11y: verdict announced; timeline semantics grouping per punch.
9. Motion & state cross-cutting (all screens)
- Pull-to-refresh everywhere lists live; no cache for verdict screens.
- No optimistic mutation for disable/sync/enroll (server-confirm,
00-shared/08 §4). - Reduced-motion: only opacity cross-fades (
00-shared/08 §2).
10. Implementation status matrix
| Screen | Backend today | Client buildable |
|---|---|---|
| Hub/Sync Status | ✗ (OQ-4 blocks) | no |
| Device List/Register/Detail | ✗ (endpoints planned) | no |
| Enrollment | ✗ (OQ-3 blocks) | no |
| Log List/Detail | ✗ (GET /biometric/logs planned) | no |
| Verify Check-in | ✗ (derive + read planned) | no |
| QR/Push check-in | ✗ | (forward-looking), dormant |