Files
Behavision/desktop
Suriyakumarvijayanayagam 18686cbceb Live view at head office, relayed through the agent's outbound connection
I got this wrong first time. "Head office cannot show live video cheaply"
conflated TRUE VIDEO with SEEING THE CAMERA NOW, and only the first needs
WebRTC and a TURN server.

The shop PC is behind a router with no inbound route, so head office
cannot pull the engine's MJPEG. It can answer the agent's outbound
requests, which is the shape of everything else here: the server holds a
poll open, the agent asks "is anyone watching?", and pushes JPEGs up for
exactly as long as somebody is.

Measured on the office camera: 98 KB full frame, 20.8 KB re-encoded at
640/q60, so one watcher costs ~83 KB/s. 47 frames arrived in 12 seconds -
4 fps, as configured. The UI says "about 4 frames a second" rather than
letting anyone conclude the camera stutters.

Nothing is uploaded when nobody is looking, which is the whole cost
argument: Publish returns false once the last viewer goes, interest lapses
on a timer each viewer refreshes as it reads (so a closed tab stops the
upload within seconds), one push is capped at five minutes, and the UI
streams one camera at a time.

LiveHub is deliberately the opposite of the arrivals Hub. There a doorbell
pushes nothing because nothing may be lost; here a dropped frame is the
correct outcome, so each viewer has a one-slot buffer that is overwritten -
the only frame worth having is the newest, and a queue would show an
ever-growing delay behind the shop instead of dropping back to live.

Ownership is proved once, before anything streams: the relay is keyed on a
camera id, a hub does not know whose camera it holds, and a camera id is
not a secret. Verified: another tenant gets 404, no session gets 401, and
an agent cannot push into another site's camera.

Also fixes a bug I introduced with it - the Live button was gated on
`connected`, which is head office's last report and up to two minutes
stale, so it hid itself during every reconnect. "Is that camera really
down?" is exactly when somebody wants to look, and a hidden control says
"you cannot" where the honest answer is "here is why".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-04 16:46:23 +05:30
..

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.