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
Behavision desktop
The store-facing app: a tray icon, a window, and the supervisor for the Python recognition engine.
Why one process, not three
The tray, the window and the supervisor all need the same state, and a user who quits the tray expects recognition to stop. Splitting them means two things can disagree about whether the engine is running.
It is deliberately not a Windows service. A service runs in session 0 and cannot draw a tray icon — that is Windows session isolation, not a library limitation. Spawning a child process also needs no elevation, while controlling a service does, so this design never triggers UAC at runtime.
Layout
main.go wails.Run, window options
app.go the methods bound to the frontend
tray.go fyne.io/systray — wails v2 has no tray of its own
icons.go tray icons generated at run time, not embedded
internal/local client for the engine on 127.0.0.1:8010
internal/cloud client for https://mcp.loyaly.ai
frontend/ React + Vite
The supervisor, durable spool, broker client and path resolution come from
../agent/pkg/* — the same tested code the headless agent runs, imported
rather than copied.
Build
cd frontend && npm install && npm run build # then, from this directory:
wails build -platform windows/amd64
wails build needs the Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@v2.9.2
Without it, go build still type-checks everything provided
frontend/dist exists — the embed directive requires it.
What the frontend talks to
Nothing is imported from generated bindings. src/bridge.js calls
window.go.main.App.* directly, so npm run build works without running
wails generate, and there is one place that handles "the engine is not
running yet" — the state every screen has to survive on a fresh install.