# Verify: staff attendance face matching client · spec 0005 · 2026-10-27
_Steps derived from spec 0005 acceptance criteria. `/check verify` runs these; `/test` locks the durable ones._

## Before a live POST
- [x] In `private/includes/ask_zap.php`, set `face_api_url` to the https origin of the Node Face API (no path). Optional `face_api_timeout_seconds` in `5` to `60`. Optional `face_api_key` only if that host expects Bearer. Do not commit a new secret in a spec or a chat paste. → AC-2
- [x] Confirm `GET {origin}/health` from this host returns JSON `{status: ok}` (ops check, not an engine function). If it fails, detect and verify will be `unavailable` and the live steps below cannot pass.

## Commands
- [x] `php admin/includes/staff_attendance_face_engine_test.php` passes. It covers origin parse (including userinfo reject, `http://`, IP host allowed), timeout default and `"20.5"` reject, xor sides (`array_key_exists`, empty bytes plus url is both set), JPEG magic, url rules (https, userinfo, length, `data:` scheme), `saf_config` omitting the key, and parse fixtures: detect `count` 0 and 1, verify `{match:true, similarity:0.82}`, similarity `1.2` is `error`, `match` `1` is `error`. It does not need Node. → AC-2, AC-5, AC-7, AC-8
- [x] Engine functions take `$ctx` only, return arrays, never echo, never read `$_POST` or `$_GET`. No `staff_attendance_face_ajax.php`. Paths hardcoded `/api/multiple-faces` and `/api/compare-face`. → AC-1, AC-11
- [x] Incomplete config: empty `face_api_url`, `http://` origin, origin with a path, origin with a query, timeout `4` or `61` each return `not_configured` and make no HTTP call. Missing timeout uses `20`. → AC-2
- [x] `saf_config` success includes `url`, `timeout_seconds`, `has_key`, and does not include `face_api_key` or `Authorization`. Empty key → `has_key` false. Non empty key → `has_key` true. → AC-2, AC-8

## Live detect and verify
- [x] `saf_detect` with JPEG bytes of one face: HTTP POST JSON `{image}` as a data URI, `status` `ok`, `count` `1`, `exactly_one` true, `raw` is an array without image keys. → AC-3, AC-4 encode rule, AC-11
- [x] `saf_detect` with a JPEG that has no face: Node HTTP 200, PHP `status` `ok`, `count` `0`, `exactly_one` false. If Node returns HTTP 400 on this route, stop and reopen spec 0005. → AC-3, AC-6
- [x] `saf_verify` with two JPEGs of the same person: `status` `ok`, `match` is a boolean, `similarity` has at most four decimal places, PHP did not read `face_match_threshold`. → AC-4, AC-7, AC-9
- [x] `saf_verify` when Node returns HTTP 400: `status` `no_face`, `msg` `No face was detected.` → AC-6
- [x] Bytes that start as PNG, bytes over `5242880`, both `image_bytes` and `image_url`, neither set, and a `http://` image url each return `bad_image` with no POST (prove with a packet capture or a debug counter / by pointing url at a host that must not receive traffic). → AC-5
- [x] Estimated JSON body over `9000000` bytes returns `bad_image` before POST. → AC-5
- [x] cURL timeout (timeout `5` plus a stalled origin, or a test double) returns `timeout`. A refused connect, HTTP 500, HTTP 401, or a TLS certificate mismatch returns `unavailable` with `http_status` or `curl_errno` set. HTTP 200 `{}` on verify returns `error`. HTTP 200 `similarity` `1.2` returns `error`. No retry. → AC-6, AC-7
- [ ] Empty `face_api_key` omits `Authorization` on the live POST. When a key is set, the request has `Authorization: Bearer …` and TLS verify stays on, redirects off. → AC-2, AC-8
- [x] After detect and verify, `staff_attendance_templates`, `staff_attendance_attempts`, and `staff_attendance_days` row counts are unchanged. → AC-9

## Settings
- [x] Sign in as admin, director, or backend. Open Settings. There is no Face group and no percent input. Attendance and Location groups remain. No new matching tile. → AC-10
- [x] Note `face_match_threshold` in MySQL for the current school. Save GPS or late time. The threshold column is unchanged. Reset other knobs. The threshold column is still unchanged. JSON still includes `face_match_percent` and `face_match_threshold`. → AC-10
- [x] Extra school copy (when the switch list has two schools) also leaves each school’s `face_match_threshold` as it was. → AC-10
- [ ] Teacher hitting Settings still goes home (spec 0003). Dead session POST still HTTP 401 JSON. → AC-10 (roles unchanged)

## Logging
- [x] Trigger `unavailable` or `bad_image`. PHP error log on this request has no data URI, no Bearer token, and no full image url. → AC-8

## Value sourcing
- [x] `saf_config` origin is `ask_zap.php` `face_api_url` after normalize (Photos `DOCUMENT_ROOT` path), not a hardcoded host in the engine. → AC-2
- [x] Missing, null, or `''` timeout becomes `20`; a present `"20.5"` or float is `not_configured`. → AC-2
- [x] `has_key` is `strlen(trim(face_api_key)) > 0` when the key is a string; the returned array never includes the key. → AC-2, AC-8
- [x] Detect POST `image` is `data:image/jpeg;base64,` plus `base64_encode(image_bytes)`, or the `image_url` string. → AC-3
- [x] Detect `count` is Node JSON top level `count`; `exactly_one` is PHP `count === 1`. → AC-3, AC-7
- [x] Detect/verify `raw` is Node JSON minus `image`, `referenceImage`, `capturedImage`, `referenceImageUrl`. → AC-3
- [x] `http_status` is cURL `CURLINFO_HTTP_CODE` or `0`; `curl_errno` is `curl_errno` or `0`. → AC-6
- [x] Verify `match` is the Node JSON boolean; `similarity` is Node `similarity` rounded to 4 places in `0` to `1`. PHP does not read `distance` or `face_match_threshold`. → AC-4, AC-7
- [x] Verify `no_face` is Node HTTP 400 on compare. → AC-6
- [x] JSON body `strlen(json_encode($payload)) > 9000000` is `bad_image` before POST. → AC-5
- [x] Settings GET has no Face group because the markup was removed; save/reset `UPDATE` omits `face_match_threshold`; JSON percent still comes from `SELECT` `id = 1` as `(int) round(threshold * 100)`. → AC-10

## Develop live prove · 2026-10-27
_Recorded during `/develop`. `/check verify` still runs the boxes above._
- [x] `GET https://node-face-api.abiahighcourt.org/health` returned `{status: ok}` from this host.
- [x] `php admin/includes/staff_attendance_face_engine_test.php` passed (parse, xor, JPEG, config, no live Node).
- [x] Live `saf_detect` of a one face JPEG: `status` `ok`, `count` `1`, `exactly_one` true, HTTP 200, `raw` keys `count,detections`.
- [x] Live `saf_detect` of a blank JPEG: `status` `ok`, `count` `0`, HTTP 200 (not 400).
- [x] Live `saf_verify` of the same face twice: `status` `ok`, `match` true, `similarity` `1`.
- [x] Live `saf_verify` against a blank JPEG: `status` `no_face`, HTTP 400, msg `No face was detected.`
- [x] `saf_config` returned `url`, `timeout_seconds` `20`, `has_key` false, and no key string.
- [x] Settings page in the signed in app: headings Attendance, Location, Also save in other schools. No Face group. Save returned `Settings saved.` and refreshed the updated time.
