Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 — Screen Specifications (Biometric Module)

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 → AppEmptyState icon sensors, "No devices registered", CTA inline.
  • error → AppErrorState codes UNAUTHENTICATED (401 → session), INTERNAL (5xx + requestId).
  • row offline → trailing badge offline (error color) + subtle red-tinted tile.
  • disable → AppDialog confirm (destructive) → row removed on server 200 (no optimistic — irreversible operation, per 00-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)" (blueprint RETRIES.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

ScreenBackend todayClient 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