Signing in on a second Mac showed "engine not reachable at http://127.0.0.1:8010" and 0 of 0 cameras, on an account whose shops were running and recognising people the whole time. Nothing was broken: Live() and Cameras() read only the engine on loopback, so the app answered as though the person had never signed in - and camera sync goes through the engine, which is why the count was zero rather than stale. Having no engine is a normal state. A shop PC watches cameras; an owner's laptop, a manager's machine and a second till being set up do not, and all three are signed in to the same estate. Both methods now fall back to head office when loopback fails and somebody is signed in. Loopback is still tried first: a real shop PC must never be shown a minute-old summary when the engine two milliseconds away has the live one. Decisions worth keeping: - Viewing is on the snapshot, not inferred per screen. Three surfaces read it, and a screen that computed it separately is how the shops screen once came out labelled Working, in green, above "2 of 3 cameras not connecting". - fraction_below_gate takes the WORST shop, never an average. 0.10 against 0.73 averages to 0.42 and hides the only shop anyone needs to visit. - A remote camera is flagged, and Edit, Remove and Check placement are withheld. They talk to a camera on a LAN this computer cannot reach, and a button that cannot work is worse than one that is absent. - connected is three states. null is "no shop computer has reported yet" and reads as waiting; false is "Not connecting". A bare false sends somebody to check cabling on a camera nobody has tried to reach. - Snapshots are fetched in Go as data: URIs and cached by snapshot_at. A webview <img> resolves a relative src against wails:// and cannot send the bearer - the problem VisitorImage already solved - and this screen polls every 8 seconds at ~90 KB a camera. - With no engine AND no session, the engine error is still the answer. The person is most likely setting this PC up. The picture is the last snapshot and the banner says so: there is no live video from here, because the engine's MJPEG stream is on the shop PC's loopback behind a router with no inbound route. The LiveHub relay head office uses is the answer to that and is a further step for this client. Verified against production: five arrivals and two cameras parsed from the real API. viewing_test.go covers the fallback, the worst-shop rule, the withheld credentials and that an unchanged snapshot is fetched once across two polls. 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.