# Rationale: Staff attendance hub and settings

## Amendment (2026-10-31)

Spec 0006 shipped the Enrollments page and owns Staff attendance nav group membership: three tiles (Locations, Settings, Enrollments) and no Photos tile. This spec still owns the Locations and Settings tile copy. The Settings subtitle still says "face match"; that leftover is the remaining Follow-up. Index AC-2, AC-3, Decision, and Consequences now point at 0006 instead of saying the group has two tiles.

## Amendment (2026-10-27)

Spec 0005 shipped the Node Face API client and removed the Settings Face group. This file keeps the original 0003 options. The index now records that save and reset skip `face_match_threshold`, that posted `face_match_percent` is ignored, and that JSON still maps the stored percent from `SELECT`. The unused column stays so this slice needs no `ALTER`. Match is the Node boolean, not a school knob.

## Context

> ⚠️ Premise note: Copying settings into another school `UPDATE`s that campus’s only policy row. Locations at least matched a gate by name. Settings have no name, so a checked extra school is a full overwrite, and Reset with extras restores seed on every checked branch. That is simple and easy to regret. We still design the picker because you asked for the locations pattern. Face percent 0 to 100 is also a guess at the matcher scale; spec 0001 already called 0.5000 a placeholder. Native `type=time` would have been enough on a phone; you asked for flatpickr, so the spec pins it and keeps the native input as fallback.

Staff check in needs school policy: how late is late, how coarse GPS may be, whether check out exists, how long to wait, and how strict face match is. Spec 0001 already put those five knobs on one row, `id = 1`, in each school database, and froze `is_late` onto the day row at insert so a later edit does not rewrite history. Locations (spec 0002) is built. The people menu still sends Staff attendance straight at the locations list.

Forces: admins already know the locations AJAX, CSRF, and extra school picker; the public kiosk must never take these numbers from the browser; late is a flag only (a blocked hour window is deferred in scope); several schools may share one admin switch list; the table cannot grow actor columns without an `ALTER` that 0001 did not allow; the app is PHP pages in a WebView, styled with `zapapp.css`, with no npm app.

If we do not decide this, `/develop` will invent ranges, percent mapping, whether the menu still opens Locations, and whether extra schools are copied, and those guesses will fight spec 0001 and the locations chrome.

Compliance: these knobs are school policy, not a staff face or a phone. Access stays on admin, director, and backend (plus staff sessions whose status is admin). This is not a finished NDPR review.

## Options considered

### Option 1: App hub plus settings form on the 0001 row, AJAX save and reset, optional extra school UPDATE, flatpickr with native fallback

PHP pages in `app/admin`, engine and AJAX in `admin/includes`, one Pattern E hub, one settings form. Save and reset `UPDATE` `id = 1`. Extra schools get the same overwrite when checked. Time uses flatpickr 4.6.13 from jsDelivr, with `input type=time` still in the form.

**Pros**:
- Fits the existing app bootstrap, CSRF, and zap-app patterns.
- No schema change. Spec 0001 stays the column contract.
- Hub fixes the 0002 follow-up that the menu should not stay on Locations forever.

**Cons**:
- Extra school overwrite has no name match safety net.
- A second CDN for one clock field.
- Hub with two tiles looks unfinished until later slices add destinations.

### Option 2: Settings page only, keep the menu on Locations

No hub. Add a settings control on the locations list. Menu tile stays as spec 0002 built it.

**Pros**:
- One fewer page. Faster slice.
- No nav rewrite.

**Cons**:
- Locations remains a fake home. Every later slice (enrollment, report) would then fight the same tile.
- You asked for a hub with Locations and Settings.

### Option 3: Owner database or a JSON file for policy

One global settings document, or a file on disk, instead of the school table.

**Pros**:
- One place to edit every school.

**Cons**:
- Spec 0001 already seeded per school `staff_attendance_settings` with no `school_id`. A second store would drift from consume.
- Breaks tenant isolation the rest of attendance uses.

### Option 4: Classic form POST, no AJAX, native time only, no extra schools

Submit, redirect, query string `msg`. No picker library. Current school only.

**Pros**:
- Fewest moving parts. No CDN, no extra school `finally` restore.

**Cons**:
- Fights the locations and finance JSON envelope the app already uses for CSRF rotation.
- You asked for optional copies and for a time library.

## Rationale

Option 1 matches the stack and the 0001 row. The job is letting the same people who draw fences also set the knobs check in will read, and giving those pages a real home so the menu can stop pretending Locations is the product.

Option 2 keeps the wrong menu destination. Option 3 invents a second policy store. Option 4 is the right size for a prototype and the wrong fit next to spec 0002.

Ranges are refuse, not clamp, because locations already taught admins that a bad radius does not silently become 20. GPS 10 to 200 sits next to the locations 20 to 2000 radius: a phone fix is often 10 to 50 m, and 0001 already fails mint when accuracy is worse than max (equal is allowed). Minutes 0 to 480 covers a school day; 0 is a real “no wait” and must not be rewritten to 30. `late_after` is `TIME`, so it has no zone. Storing the typed `HH:MM` as Lagos wall clock is the only mapping that does not shift 08:00 when a device sits in UTC.

Percent is a form view. The column stays `DECIMAL(8,4)` so the face matching spec can later say what 0.5000 means without another `ALTER`. Integer 0 to 100 keeps the control on a phone. The matching spec must revisit this if the service is not a 0 to 1 score.

Reset writes the published seed, not the form, so a dirty field cannot leak into a “defaults” action. Extra schools start unchecked, same as locations edit, so a save cannot surprise another campus. Current school always writes. Failures on extras are reported and kept, because there is no transaction across school databases.

Shared `staff_attendance_can_access` exists so hub, settings, and locations cannot drift. `sal_can_access` remains as a wrapper. The locations list already had a back control on history; pointing that href at the hub is the smallest chrome change that matches “hub is the parent.”

CSRF copies the shipped locations dispatcher: `csrf_token_is_valid` plus same origin, 401 on a missing `authenticated` or `zap_auth` flag, because `check_csrf_token()` exits HTTP 200 and would break AC-12. Numeric fields are matched as strings before `(int)` so `"50.9"` and an empty checkout cannot become a silent write. `SELECT` before `UPDATE` is the missing row test; `affected_rows` is 0 on a same value save. flatpickr `dateFormat` `H:i` is required so a loaded picker posts a clock, not a date.

flatpickr 4.6.13 is the last 4.x release the team can pin with SRI the same way Leaflet 1.9.4 is pinned. Native `type=time` stays in the DOM so a blocked CDN does not block Save. Agent Skills and MCP servers stay declined, matching root `AGENTS.md`.

Pagination does not apply: there is one row. An audit log would need columns 0001 did not create; last write wins is the accepted race, same as a school with few admins on locations names.
