# Scope: Staff attendance

A staff check in and check out loop for the existing ZAP school app. Admins enroll faces and set school locations. Staff prove they are present with a live face and a server side location check, on a shared kiosk or a personal phone, with no staff login.

**Build approach:** Tracer Bullet (each slice is a real path through storage, server, and UI, just narrow).
**Workflow:** Alpha (after `/develop`, `/check verify` on the real app). The project default level of rigor. `/architect` is the recommended first stop for a feature with a real decision, but skippable when you already know the build. Any feature can carry its own tag (e.g. `· GA`) to do more or less.

_These are recommendations to keep your build orderly, not requirements. Skip anything that does not fit: if you already know how to build a feature, use `/develop` and skip `/architect`. You decide when a feature is `done`._

## At a glance

| # | Feature | Phase | Status |
|---|---------|-------|--------|
| A | Auth and sessions | Existing | existing |
| B | School tenant | Existing | existing |
| C | App chrome and UI | Existing | existing |
| D | Staff list | Existing | existing |
| E | Student attendance | Existing | existing |
| F | Event clock | Existing | existing |
| 1 | Attendance data model | Foundation | done |
| 2 | Attendance locations | Slice 1 | done |
| 3 | Attendance settings | Slice 1 | done |
| 4 | Private attendance photos | Slice 1 | done |
| 5 | Face matching service | Slice 1 | done |
| 6 | Face enrollment | Slice 1 | in-progress |
| 7 | Staff check in | Slice 1 | in-progress |
| 8 | Attendance report | Slice 2 | planned |
| 9 | Live check in board | Slice 3 | planned |

## Existing

### A. Auth and sessions · existing
Pre workflow sign in, PDO sessions, autologin, and login gates used by app screens. code in `private/login.php`, `app/admin/includes/init.php`, `app/admin/includes/authenticate.php`

### B. School tenant · existing
Each school has its own database selected from the session. The public check in page stays on that school's existing app URL, already tenant bound, with no extra school code. code in `app/admin/includes/init.php`, `private/includes/settings.php`

### C. App chrome and UI · existing
App pages boot, wrap in `md-page`, and use `zapapp.css`. New attendance screens follow this, not a new UI stack. code in `app/admin/header.php`, `app/admin/styles/zapapp.css`

### D. Staff list · existing
Current school staff list in the app (name, position, status). Face enrollment hangs off this list. Do not change the `staff` table. code in `app/admin/staff_list.php`

### E. Student attendance · existing
Class and subject attendance for learners. Staff attendance is a new module; do not extend these screens. code in `app/admin/attendance_class.php`, `app/admin/attendance_subject.php`

### F. Event clock · existing
Logged in QR clock for students and parents at events. Useful kiosk pattern; not the staff daily loop. code in `app/admin/event-clock.php`, `app/admin/event-parent-clock.php`

## Foundations

### 1. Attendance data model
Tables and tenant rules for settings, locations, enrollments, one use tokens, daily records, verification attempts, and face templates, including consent and a 7 day photo retention window. Leave the existing `staff` table unchanged.
spec [0001](../specs/0001-attendance-data-model/index.md) · code in `docs/specs/0001-attendance-data-model/schema.sql`
**Done when:** a school database can hold those rows, every read and write stays inside the current school, phone lookup is stored as 13 digit `CHAR`, and a second current enrollment on the same phone is refused and logged without guessing.
- [x] Design it (spec): `/architect attendance data model`
- [x] Build it: `/develop attendance data model`
  - [x] Apply the published SQL on one school and confirm seven tables, InnoDB, utf8mb4, and the settings seed (AC-1, AC-14, AC-17)
  - [x] Prove unique keys (current phone, current staff, staff plus date) and that replace frees them (AC-5, AC-6, AC-7, AC-9)
  - [x] Confirm `staff` is unchanged and no new table has `school_id` (AC-2, AC-3)
  - [x] Confirm column types for phone, token hash, templates, `tested_at`, checkout, late, and photo keys (AC-4, AC-8, AC-10, AC-11, AC-12, AC-13, AC-15, AC-16)
  - [x] Apply the same SQL on the remaining school databases (AC-1, AC-14, AC-17)
- [x] Verify it: `/check verify attendance data model`

## Slice 1: Core check in loop

### 2. Attendance locations · done
Named places for the current school, each with a center and a radius. Staff pass if they are inside any active place. First real admin write path on the new tables.
spec [0002](../specs/0002-attendance-locations/index.md) · code in `app/admin/staff_attendance_locations.php`, `app/admin/staff_attendance_location_edit.php`, `admin/includes/staff_attendance_locations_engine.php`
**Done when:** an admin, director, or backend user can add, edit, toggle, and delete locations; the map shows the radius and other school locations; save stores name, latitude, longitude, and radius (20 to 2000 meters, default 100).
- [x] Design it (spec): `/architect attendance locations`
- [x] Build it: `/develop attendance locations`
  - [x] List shell, edit map, and save into the current school (AC-1, AC-3, AC-4, AC-11, AC-12)
  - [x] Cards, toggle, delete (remove or turn off), and unique names (AC-2, AC-5, AC-6, AC-7)
  - [x] Address search, my location, map start, faded circles, fail messages (AC-8, AC-9, AC-11)
  - [x] Copy into other schools on the switch list, with a flash of partial failures (AC-10, AC-12)
- [x] Verify it: `/check verify attendance locations`

### 3. Attendance settings · done
Per school knobs the server uses for matching and attendance: face match threshold, max GPS accuracy, allow check out, minimum minutes before check out, and the time after which a check in is late.
spec [0003](../specs/0003-attendance-settings/index.md) · code in `app/admin/staff_attendance_settings.php`, `admin/includes/staff_attendance_settings_engine.php`
**Done when:** admin, director, or backend can save these for the current school; check in at any hour still records; late is a flag only; the public page reads these on the server, never from the client.
- [x] Design it (spec): `/architect attendance settings`
- [x] Build it: `/develop attendance settings`
  - [x] Nav tiles, settings GET and current school save (AC-1 to AC-6, AC-12)
  - [x] Ranges, percent, checkout switch, late after parse, missing row (AC-5, AC-7, AC-8)
  - [x] Time picker with native fallback (AC-11)
  - [x] Extra schools, reset, flash, restore session database (AC-9, AC-10, AC-12)
- [x] Verify it: `/check verify attendance settings`

### 4. Private attendance photos · done · GA
Private Backblaze storage for enrollment portraits. Admins capture on a Photos page (oval and three step liveness), the browser PUTs through a short signed URL, and they view through a five minute GET URL. Failed check in frames are not stored.
spec [0004](../specs/0004-private-attendance-photos/index.md) · code in `app/admin/staff_attendance_photos.php`, `admin/includes/staff_attendance_photos_engine.php`
**Done when:** upload, signed view, and delete work without a public object URL; credentials live in `private/includes/ask_zap.php`; the bucket stays private; fail photos are not stored.
- [x] Design it (spec): `/architect private attendance photos`
- [x] Build it: `/develop private attendance photos`
  - [x] Config keys, SigV4, mint, browser PUT, confirm, view, camera with oval and liveness (AC-4, AC-5, AC-7, AC-8, AC-9, AC-10, AC-15, AC-18)
  - [x] Pattern C list, paging, delete, nav tile, CSRF (AC-1, AC-2, AC-3, AC-6, AC-11, AC-12, AC-16)
  - [x] Prefix allowlist, missing config, camera blocked, PUT retry, name-edit purge, CORS checklist (AC-3, AC-4, AC-7, AC-13, AC-14, AC-17)
- [x] Verify it: `/check verify private attendance photos`
- [x] Review it (fresh model): `/check review private attendance photos`

### 5. Face matching service · done · GA
A small server contract to detect faces in an image and to compare a live capture with an enrollment. The browser check is UX only. The server decides match or not. Origin and optional key live in config; Node paths live in the engine.
spec [0005](../specs/0005-face-matching-service/index.md) · code in `admin/includes/staff_attendance_face_engine.php`, `app/admin/staff_attendance_settings.php`
**Done when:** detect reports face count and whether it is exactly one face; verify returns match (from the API boolean), similarity, and raw; the school Face percent is unused and hidden; missing config and transport failures are typed errors, not a fatal retry screen.
- [x] Design it (spec): `/architect face matching service`
- [x] Build it: `/develop face matching service`
  - [x] Config keys plus live detect and compare to the Node origin (AC-1, AC-2, AC-3, AC-4, AC-8, AC-11)
  - [x] Validation, typed errors, parse fixtures, and engine test (AC-5, AC-6, AC-7, AC-8, AC-9)
  - [x] Hide Settings Face group; save and reset skip the percent column (AC-10)
- [x] Verify it: `/check verify face matching service`
- [x] Review it (fresh model): `/check review face matching service`

### 6. Face enrollment · in-progress · GA
Admin staff list for the current school with enrollment status (Not enrolled / Enrolled / Enrolled + tested), search, and paging. Capture uses the device camera, a face guide, and a consent step. Enrolled cards show the portrait. Remove deletes the file. The Photos page is retired. Test uses the same server match as check in (face only, no location).
spec [0006](../specs/0006-face-enrollment/index.md) · code in `app/admin/staff_attendance_enrollments.php`, `admin/includes/staff_attendance_enrollments_engine.php`
**Done when:** admin can enroll, retake, test, replace, view the portrait, and remove; Remove deletes the row and the B2 file; the Photos page redirects; MediaPipe requires a face in the oval; PHP requires a pending JPEG; Test uses Node verify; retake edits the same row and stamps who replaced it; `consent_at` is PHP Lagos now; phone is copied from `staff.phone_number`; Universal admin actor ids come from `executives`.
- [x] Design it (spec): `/architect face enrollment`
- [x] Build it: `/develop face enrollment`
  - [x] List shell, nav tile, search, chips, paging (AC-1 to AC-5, AC-15)
  - [x] Capture pending PUT, confirm_face, consent sheet (AC-6, AC-7, AC-8, AC-14, AC-16)
  - [x] Enroll commit then copy, duplicate phone (AC-9, AC-10, AC-17)
  - [x] Retake replace and Remove (AC-11, AC-12)
  - [x] Test, config hide, engine tests (AC-13, AC-14, AC-15, AC-17)
  - [x] Portrait thumbs, view sheet, enroll without matching (AC-3, AC-4, AC-14, AC-15, AC-18)
  - [x] Remove deletes B2, lock and pending guards (AC-7, AC-9, AC-10, AC-12, AC-17)
  - [x] Drop Photos tile, redirect and 410 (AC-2, AC-19)
- [ ] Verify it: `/check verify face enrollment`
- [x] Test it: `/test face enrollment`
- [x] Review it (fresh model): `/check review face enrollment`
- [ ] Document it: `/document face enrollment`

### 7. Staff check in · in-progress · GA
Logged in page for staff plus admin, director, and backend. A shared gate stays signed in as an attendant. Flow: In or Out tab, GPS watch until accuracy is within max, typed phone, liveness, then one frame to the server. No per phone cap and no per IP cap. Several gates at one school may be busy at once.
spec [0007](../specs/0007-staff-check-in/index.md) · code in `app/admin/staff_attendance_checkin.php`, `admin/includes/staff_attendance_checkin_engine.php`
**Done when:** a matching enrolled staff member inside any active location gets a success screen with masked name, time, and location name; check out is an Out tab that follows settings and the minimum minutes rule; an open day blocks another check in until they check out; GPS worse than the school max is refused; duplicate phones error and log, never guess; some failures stay generic and never leak other staff data; every attempt is logged; live JPEGs are not stored; CSRF and prepared statements cover the POST paths; timestamps use server time in `Africa/Lagos`.
- [x] Design it (spec): `/architect staff check in`
- [x] Build it: `/develop staff check in`
  - [x] Page shell, access, nav tiles, In and Out strip, GPS then phone (AC-1, AC-2, AC-3, AC-4, AC-13, AC-16)
  - [x] Mint token and attempts: phone, GPS from the client, nearest fence, masked name, no rate limit (AC-5, AC-6, AC-12, AC-14, AC-17)
  - [x] Liveness sheet, consume check in, success card (AC-7, AC-8, AC-9, AC-11)
  - [x] Check out, already in, lock, replay, idle, engine tests (AC-10, AC-11, AC-12, AC-15, AC-16, AC-17)
  - [x] One GPS watch until accuracy is within max, reuse that point at mint (AC-4, AC-12, AC-16, AC-17)
- [ ] Verify it: `/check verify staff check in`
- [ ] Test it: `/test staff check in`
- [ ] Review it (fresh model): `/check review staff check in`
- [ ] Document it: `/document staff check in`

## Slice 2: Audit and reports

### 8. Attendance report · needs a decision · GA
Admin list of attendance by date range, staff, and location, with check in and check out times, location, face score, and late. CSV export. A second tab lists failed attempts with the captured photo while it still exists.
**Done when:** admin, director, or backend can filter and export successes; they can review failures with a short lived photo view; missing photos after 7 days show as expired, not as a broken image.
- [ ] Design it (spec): `/architect attendance report`

## Slice 3: Today

### 9. Live check in board · needs a decision
A today view of who has checked in, refreshing through the day, for admin, director, and backend. Not for other staff.
**Done when:** those roles can open today's board, see name, time, and location, and watch new check ins appear without a full page reload being the only option.
- [ ] Design it (spec): `/architect live check in board`

## Deferred
Out of scope for the current build pass, kept so the plan stays honest.
- **Payroll feed**: send check in and check out into existing salary screens · needs a decision
- **Failure notifications**: mail or push when a check in fails or looks suspicious · needs a decision
- **Desktop admin copy**: the same admin tools on desktop `admin/` · needs a decision
- **Check in time window**: block or warn outside school hours · needs a decision
- **Product analytics**: measure adoption of the gate loop · needs a decision
- **Attendance correction**: void or fix a wrong successful match on a staff date · from spec 0001
- **Biometric legal review**: NDPR or other review of face templates and fail photos · from spec 0001
- **Matching service API key**: put a real `face_api_key` on the Node host and in `ask_zap.php` before production if that origin is on the public internet · from spec 0005
- **Location change audit**: who moved a fence · from spec 0002
- **Settings change audit**: who saved attendance knobs · from spec 0003
- **Settings nav subtitle**: drop "face match" from the Settings tile when you next touch the nav · from spec 0003
- **Pending portrait cleanup**: leftover B2 objects with no current enrollment (old Photos captures, failed deletes, pending files). A one day B2 lifecycle on `enrollments/pending-` can cover the pending prefix · from spec 0006
- **Deactivated leftover enrollments**: a list of deactivated staff who still have `status = current`, so an admin can Remove them · from spec 0006
- **Check in reference key**: verify against the current row’s `photo_object_key`, which may still be a pending key if copy failed · from spec 0006
- **Public check in still detects**: that page should call `saf_detect` then `saf_verify`. Do not skip Node detect because enrollment skipped it · from spec 0006
- **Trusted hop for same origin**: if a later host terminates TLS on a proxy that leaves `HTTPS` unset, do not add `X-Forwarded-Proto` on `request_is_same_domain`; write a trusted hop spec first · from spec 0006
- **Kiosk session keep alive**: PHP session expiry stops a gate that stays open all morning · from spec 0007
- **Public logged out staff check in**: this slice requires a live session; a public URL would need its own spec · from spec 0007
- **Attempts IP index**: spec 0001 offered `(ip, created_at)` for an IP cap; unused because spec 0007 has no IP cap · from spec 0007

## Legend

**The decision box.** Every feature carries exactly one, the sub-task whose label ends with `(spec)`. Its wording varies (`Design it (spec)` normally, `Decide the stack (spec)` on Stack & architecture), so skills locate it by that `(spec)` suffix, never by an exact label. Every other box is an execution box and `/architect` never ticks one.

**Feature lifecycle**: the scope updates as a feature moves; each row is what it shows and who sets it:

| State | Set by | The feature shows |
|---|---|---|
| `planned` · needs a decision | `/scope` | one box: `Design it (spec): /architect <feature>` |
| `in-progress` (designed) | **`/architect` at spec capture** | `Design it` ticked; spec linked; `Build it: /develop <feature>` + **2 to 5 milestones**; the tier's closing boxes (`Verify it` Alpha+, `Test it` Beta+, `Review it` + `Document it` GA); any surfaced follow-up enrolled |
| `in-progress` (building) | `/develop` | milestone sub-boxes tick one by one; code pointer filled |
| `in-progress` (verified) | `/check verify` | `Build it` + milestones ticked; `Verify it` ticked |
| `done` | **you, when you decide it is** (any skill sets it when you say so); `/sync` reconciles | boxes you ran ticked, skipped ones marked skipped; the tier's last stage (`Prototype` → after `/develop`; `Alpha` → after `/check verify`; `Beta`/`GA` → after `/test`) is the suggested point to call it done; `/sync` captures conventions |

- **Next step** = the first unticked box (always a command or a tracked milestone).
- **needs a decision** = run `/architect` first; otherwise straight to `/develop` (or `/audit` for standards & tooling). The tag drops once the spec is captured.
- **Atomic build tasks live in the spec's `## Build plan`, not here**: the scope carries only the milestone rollup.
- **Status** `planned` → `in-progress` → `done`, plus `existing` (pre-workflow) and `dropped` (de-scoped, kept for history).
- **Approach tag** beside a heading (e.g. `· Facade`) overrides the project default for that feature; no tag = inherits it.
- **Workflow tier tag** beside a heading (e.g. `· GA`, `· Prototype`) sets that one feature's rigor above or below the project default; no tag inherits the default. It decides the feature's check boxes and each skill's next suggestion.
- **Workflow** (header line) is the project default, what runs after `/develop`: **Prototype** = nothing (trust develop's own build time self check); **Alpha** = `/check verify`; **Beta** = `/check verify` then `/test`; **GA** = adds a fresh model `/check review` then `/document`. A feature built on an unratified decision (an `Assumed` spec) stays flagged, but that never blocks `done`.
- **Pointer line** (`spec <n> · code in <path>`): the spec link added by `/architect`, the code path by `/develop`.
