# 0007 rationale: Staff check in on the school app

## Context

> Premise note: The scope row said a logged out page with no staff login. You overrode that. This page requires a live session. A shared gate must stay signed in as an attendant. When that session expires, the queue stops. A keep alive is follow-up, not this slice.

> Premise note: The scope seed said 5 tries per phone per 10 minutes and 30 per IP per 10 minutes. You dropped both, including the IP cap, so a busy kiosk on one school IP is not queued. A stolen staff session can call the matching host until Apache or Node stops it.

> Premise note: Mint returns a masked name before the face matches. Anyone who can open this page can tell which phones are enrolled. You accepted that so an attendant can see who they are about to capture.

> Premise note: The first GPS decision stored a second `getCurrentPosition` at mint so a walk from the parking lot was not frozen. You then said one GPS check before the phone is enough. Idle 60 s still drops a stale point. A person who walks after a good fix can still mint that earlier point until idle or tab switch. You accepted that bound.

Staff already have enrollments, places, settings, private portraits, and a matching client. What was missing was the gate loop: prove this phone, this GPS point, and this live face belong together, then write today.

The original product line was a public URL on the school host, because the school app URL already selects the tenant database. You then said this is not a page for everyone. Logged in staff, plus admin, director, and backend, may open it. A kiosk is one of those sessions left on a table. People in the queue type a phone. That keeps students and parents off the biometric POST, and it keeps CSRF and same origin on a real session.

Identity cannot be the attendant. One signed in person clocks many. Spec 0001 already stores `phone_normalized` on tokens and looks up the current enrollment by that phone. The typed field stays. GPS must be the device at the gate. A single `getCurrentPosition` often returns the first fused wifi or cell fix, whose accuracy (meters of possible error) is worse than the school max (settings default 50 m), so the page shows `Location is too coarse` and stops. A second read at mint can fail even after the first read passed. The phone needs time to lock GPS.

Two gates at one school can POST at the same moment. Unique `(staff_id, attend_date)` plus a GET_LOCK are the cheap way to make one check in per Lagos date true. Check out is a tab, not a guess: you asked for In and Out like the timeline sort strip, always on screen, with GPS, phone, and camera inside each tab. Spec 0001 still enforces open day, wait minutes, and checkout off.

Live JPEGs are biometric. Spec 0004 already said fail frames are not stored. Multipart bytes in PHP memory, then discard, matches Test on Enrollments and avoids a B2 object for a stranger’s face.

If this is not decided, `/develop` will invent a public page, a phone cap that stalls a retry, or a consume that skips Node detect (spec 0006 already warned against that).

## Options considered

### Option 1: Logged in attendant page, mint then consume, no rate limits

Session required. Phone typed. Token binds GPS and enrollment before the face POST. No phone cap and no IP cap. In and Out tabs. Multipart consume. Reuse spec 0001 tables with no ALTER.

**Pros**:
- Fits a shared device without a public biometric URL.
- Token expiry and consume replay match spec 0001.
- A morning rush on one NAT IP is not queued.

**Cons**:
- The gate depends on session lifetime.
- Enrolled phones are enumerable by mint name.
- Node can be hammered by one stolen login.

### Option 2: Public logged out page on the school URL

No login. School host selects the database. Phone plus GPS plus face, with rate limits as originally scoped.

**Pros**:
- A kiosk needs no attendant account.
- Matches the old scope sentence.

**Cons**:
- You said this is not for everyone. Parents, students, and the open internet would share the matching POST.
- An IP cap of 30 per 10 minutes would stall a busy gate.

### Option 3: Clock only the session staff member, no phone field

Personal phones only. `staff_id` from the session. Skip mint phone lookup.

**Pros**:
- No typed number, no mint name leak.
- Simpler POST.

**Cons**:
- A shared kiosk cannot clock the next person without a logout.
- You asked to keep a typed phone for an attendant session.

### Option 4: One POST with phone, GPS, intent, and image, no token

Skip `staff_attendance_tokens`. Verify and write in one request.

**Pros**:
- Fewer round trips. No leftover token rows.

**Cons**:
- Spec 0001 already shipped tokens so GPS cannot change between place check and face check.
- A lost success JSON has no replay key.
- Two gates would both run Node before the unique key could stop the loser.

### Option 5: Watch until accuracy is within max, mint that one point

`watchPosition` with high accuracy, a 15 s page timer, `clearWatch` on the first fix whose accuracy is within `max_gps_accuracy_m`. Phone submit sends that stored point. No second geolocation read.

**Pros**:
- Uses the later GPS lock instead of the first coarse fused fix.
- One wait before the phone, which matches how a kiosk actually stands still.
- Stops watching as soon as the fix is good enough.

**Cons**:
- A walk after the good fix can still mint that earlier point until idle 60 s.
- 15 s of watching can feel slow outdoors when the first fix was already fine.

### Option 6: Keep two one shot `getCurrentPosition` reads

First read gates the phone field. Second read at mint is the token point.

**Pros**:
- The token stores the point at submit, not the point at the door.

**Cons**:
- Each call can return a coarse first fix. The second call can fail after the first passed.
- This is the behaviour that showed `Location is too coarse` on real phones.

### Option 7: Retry `getCurrentPosition` a few times, still no watch

Loop one shot reads until accuracy is within max or the 15 s budget ends.

**Pros**:
- Avoids a long lived watch id.

**Cons**:
- Each retry can still return the same coarse fused fix. Watching is what lets the device keep improving the lock.

## Rationale

Option 1 is the only shape that honours a logged in attendant, a typed phone, and the token table you already applied on every school. Option 2 fights “not for everyone.” Option 3 fights the kiosk. Option 4 fights spec 0001 and makes retry of a successful consume guesswork.

No rate limit is a conscious cost: a shared kiosk IP must not look like abuse. The matching host and Apache remain the backstop. Mint name before match is also a conscious cost: the attendant needs to see `A. Okonkwo` before they hold the camera up.

In and Out as a timeline sort strip keeps intent visible without a second page. The server still refuses a check in while any open day exists, so the unique staff plus date key and the “one open day” rule do not fight. GPS uses Option 5: a phone’s first fused fix is often coarse, so watching until accuracy is within max is the fix that matches the device. Minting that stored point (and not a second one shot read) is what you asked for. Option 6 is the old path that failed in the field. Option 7 still fights the fused first fix. The Geolocation `timeout` option is omitted so a slow lock is not turned into a denied error. A page timer of 15 s is the wait budget. `clearWatch` on leave keeps GPS off during phone and camera. Idle 60 s is the bound on a stale point.

Multipart bytes reuse Enrollment Test. Putting the live frame in B2 would reopen spec 0004 and leave portraits of failed strangers in the bucket. Detect then verify on consume closes the spec 0006 follow-up that this page must not skip Node detect.

Haversine lives in this engine because locations admin never needed a point in polygon helper. Nearest center with a `location_id` tie break makes two kiosks pick the same place when fences overlap. GPS accuracy is a separate gate; it does not grow the radius.

GET_LOCK waits 40 seconds and stays held through Node so two gates cannot both verify. Idle 60 seconds pauses while the camera sheet is open. Mint success is logged as `mint_ok` / `ok_mint` because spec 0001 outcomes `success_in` and `success_out` are reserved for a finished clock.

The build is Tracer Bullet: shell and GPS, then mint for real, then a successful check in through Node, then check out and the refuse paths. No new tables, no new secrets.
