From 177584e812642c02bf7fef6d97c32d197d9dd143 Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Thu, 24 Sep 2026 13:14:14 +0530 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- API.md | 4 +++ CLAUDE.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/API.md b/API.md index e87a206..f9e3364 100644 --- a/API.md +++ b/API.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md index 43628cc..44250d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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///2026/09/24/.jpg +PUT 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