Record what the live system actually does, measured not assumed

Production had 1,211 events accepted and six recognised customers from
the office cameras - the first time the whole chain has carried a real
person, and the project had never been able to claim it. Repeat
sightings score 0.44-0.72, a distribution the match threshold sits
clearly below, on the head-height camera this file has recommended since
August. fraction_below_gate is still 0.59, so the visit count is a floor
and the report says so beside it.

The face-image chain was exercised on production as a shop PC does it -
upload URL, PUT to object storage, anonymous read refused 403. Every
server link holds; the only reason a customer has no photo is
app.store_faces being false by default, which is a data-protection
decision rather than a gap.

Sixteen mobile-API checks pass as a staff account. Three apparent bugs
were test errors and are written down so nobody re-files them, along
with the one field name a caller could guess wrong (site_token).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-24 13:14:14 +05:30
parent 81e2c605b9
commit 177584e812
2 changed files with 79 additions and 0 deletions

4
API.md
View File

@@ -1126,3 +1126,7 @@ a user session is refused.
A web or mobile client never calls them. They are listed here so nobody wonders
what they are.
One field name, because it is the only one in the product that is easy to guess
wrong: `POST /api/agent/enrol` takes **`site_token`** (the installation code),
not `code`, plus an optional `device`. Verified against production.

View File

@@ -2151,6 +2151,81 @@ correctly calls them one person. Tests that need several different people use
`distinctFace(i)`, which is orthogonal per index.
## It recognises people, and that is now measured rather than assumed
Verified against **production** on 2026-09-24, reading what the live system had
already recorded from the office cameras on 15-19 September. Until this, every
claim about recognition rested on the August engine tests; the whole chain -
camera to engine to agent to broker to server to a screen - had never once
carried a real person.
```
6 customers, 36 visits, 1,211 events accepted, 0 dropped, 0 duplicates
Visitor 1 15 visits Visitor 3 8 visits Visitor 5 7 visits
same-person similarity on returning matches: 0.437 - 0.716
attributes travelling with each arrival: age 37 (spread 2), gender Male
```
Those similarities are the point. The August measurement on this site said
`match_threshold: 0.42` sat *inside* the same-person distribution for the
overhead camera and no threshold could separate a person from themselves. On
`cam2` the same code now returns 0.54, 0.64, 0.68, 0.72 for repeat sightings of
the same people - a distribution the threshold sits clearly below. The camera
that produced them is the open-office one at roughly head height, which is
precisely the fix this file has recommended since August.
What is still true and unflattering: `fraction_below_gate` on that site is
**0.59**, reported with `worst_site` beside it. Well over half the faces those
two cameras see are still too poor to enrol. The footfall figure of 36 visits
is therefore a floor, not a count, and the report says so in the same response -
which is the entire reason that number travels with the one it qualifies.
### The image chain works; the engine is simply not capturing
Exercised on production exactly as a shop PC does it, end to end:
```
enrolment code -> agent token 200
POST /api/agent/upload-url 200 behavision/v2/<client>/<site>/2026/09/24/<random>.jpg
PUT <presigned url> 200 JPEG in DigitalOcean Spaces
anonymous GET of that same object 403 private, as the signature demands
staff GET /api/visitors/{id}/image 404 "No photo was captured for this visit."
```
Every server-side link holds: the key is minted by the server and scoped to one
site, the ACL is inside the signature, and an unauthenticated read is refused.
The 404 at the end is not a fault - it is `app.store_faces: false`, the shipped
default, so the engine never wrote a crop for the agent to upload. Turning
images on is one setting on the shop PC and a deliberate change to what the
system is under GDPR and India's DPDP, which is why it is off until somebody
decides otherwise rather than on until somebody notices.
### The mobile API, walked as a phone would
Sixteen checks against production as a **staff** account: login, arrivals feed
with cursor, arrivals carrying `visitor_id` and `site_slug`, customer search,
customer history, photo endpoint, profile write, purchase, device list, token
rotation (the retired access token correctly 401s), team read allowed, invite
refused 403, admin surface 404, SSE stream delivering rows, and Loya answering.
All pass.
Three things that looked like product bugs and were not, recorded so the next
person does not re-file them:
- `POST /api/purchases` takes `items` as a **list of strings**, not a count.
API.md had it right; the test was wrong.
- `new` and `returning` live **inside each bucket** of a footfall report, not at
the top level. `total`, `visits`, `fraction_below_gate` and `worst_site` are
the top-level fields.
- `?range=7d` is not a parameter. Reports take `from` / `to` / `bucket` / `site`,
and an unknown query parameter is silently ignored - so an invented one
returns the default 30-day window rather than an error. That hazard is
already recorded above for `site` vs `site_id`; it applies here too.
`POST /api/agent/enrol` takes **`site_token`**, not `code` - the only field name
in the product a caller could reasonably guess wrong, and it is a route no
client app should ever call.
## Setting up on a new machine
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds