The first launch was a code box with a link under it, then an empty
Live screen with 'No cameras' in amber in a far corner, then a form
asking for an IP address, and for the first few minutes of all of it
the engine silently downloading 275 MB with nothing on screen but a
stopped-looking status. Walked in a browser with the new mock; nobody
who was not an installer would have got through it.
Now: a welcome that asks the one question a shop owner can answer -
managed from a head office, or on this PC only - with each path in a
sentence; a Getting Started checklist on Live that reads its three steps
from the engine and ticks them itself (recognition ready, camera added
and connected, camera proven by a walk-past), with the one button for
the next step, and that disappears the moment somebody is recognised;
and the model download reported as a percentage in the tray, the
sidebar and the checklist, parsed by the supervisor from the engine's
own progress lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
The Live screen led with a camera tile beside the arrivals. Nobody at a
counter is watching CCTV; they are looking up at a customer and need the
name. The tile also cost CPU the recognition pipeline needs and pulled a
stream relay into the app for a picture that was decoration. Arrivals now
take the whole screen. The camera picture stays on the Cameras screen,
where it is a setup tool and not a feed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
The window a shop assistant stares at all day was the weakest surface in
this system, and it looked improvised because it was: navigation drawn
with text characters (◉ ☺ ▢) that sit on the text baseline and cannot
take a stroke weight, margins set inline per screen, and four large stat
boxes dominating the page while the product's entire reason for existing
- WHO JUST WALKED IN - was a list of "person.seen" rows in the corner.
Rebuilt around the person in front of it: a counter, a cheap monitor,
somebody mid-conversation with a customer.
- ui/icons.jsx: one drawn icon set, 24-unit grid, 1.6 stroke,
currentColor, so one icon works on every surface and in every state.
- styles.css: a real system. Four-step ground→raised palette biased
blue-green (this product lives in the world of lenses), one spacing
scale, one type scale, tabular figures wherever digits are compared
or refreshed in place, and the scrollbars restyled - the default
light scrollbar on a dark panel is the loudest "web page in a frame"
tell there is.
- Live: a status strip that answers "is this working" in one line,
cameras as pictures with the caption over the image, and arrivals as
cards big enough to match against the person standing there. The
four stat boxes became a slim strip at the foot, where numbers that
nobody acts on belong.
- State is carried by shape AND colour everywhere - a pill, a dot and
an edge stripe - because this gets read from two metres away and
some operators do not see red and green apart.
- Motion only where it means something: a live camera pulses, a fresh
arrival slides in once. Nothing loops for decoration; this process
shares a CPU with recognition.
Two things fixed because the screen showed them, not because a test did:
- The sidebar read "Stopped" beside a live camera feed and a counter
ticking up, whenever the engine was running but not started BY the
app. That is the two-surfaces-disagreeing bug the tray exists to
avoid. It now reads "Running outside the app" in amber, and Start is
disabled rather than offering to launch a second engine onto one
SQLite WAL.
- The arrivals panel shrank to fit its content and left a hole beside
a tall camera tile - so the layout looked broken exactly when the
shop was quiet, which is most of the time. Both panels stretch and
scroll their own content now.
Every existing class name still resolves, so the screens not rewritten
here pick the system up unchanged. Windows and darwin build; tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
Five components that ship as one product:
- behavision/ the recognition engine. RTSP ingest, YuNet detection, IoU
tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
FastAPI dashboard. Identity is decided once per TRACK from an
average of at least three embeddings, never per frame.
- agent/ the Go edge agent: supervises the engine, holds a durable
spool, and drains it to MQTT. Nothing is acked before the
broker confirms.
- desktop/ the shop PC application (Wails + React + tray).
- server/ the cloud API, MQTT consumer, reports and assistant.
- web/ platform.loyaly.ai, the head-office app, embedded in the
server binary.
The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.
CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn