Files
Behavision/agent
Suriyakumarvijayanayagam e0ceb14589 Camera pictures without an object-storage bucket
Head office shows a camera's latest frame rather than live video, for a
reason that has not changed: the engine serves MJPEG on 127.0.0.1 on a PC
behind a shop's router with no inbound route, and relaying it needs
WebRTC/TURN. Pointing a browser straight at the shop PC is not the escape
either - the engine's API is Basic-authenticated with a credential it
generates locally and never sends anywhere, and shipping that to the
cloud so a web page could use it would put the key to the biometric API
and the live face feed in the server's database.

But that picture only worked if you had an S3 bucket. Without one,
attachSnapshots reported "This system is not storing images" for every
camera forever - on the two screens whose whole job is to show the
camera. Making them picture-led turned a missing feature into a wall of
empty tiles, on every local install and any self-hosted customer who does
not want a bucket.

migrations/009 adds camera_snapshots and the agent falls back to
PUT /api/agent/cameras/{camera}/snapshot when the presigned route answers
images_disabled - chosen by sentinel, never by matching the message, since
it picks between two routes. One row per camera is what makes this safe in
the database when face images are not: the key IS the camera, so storage
is (cameras x ~100 KB) and does not grow with footfall.

The read is session-authenticated rather than a signed link, which an
<img> cannot use - hence Shot.jsx and useAuthedImage, keyed on the URL
string rather than the snapshot object so a poll does not re-fetch 90 KB
per camera every few seconds, and revoking the object URL on cleanup.

Verified against the real office camera with no bucket configured: 90,587
bytes stored in Postgres, served as image/jpeg to a signed-in user, 401
without a session, rendered on both the Cameras and Shops cards.

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

Behavision agent (Go)

The half of the edge install that touches the network. The Python engine keeps the cameras and the models; this keeps the tray icon, the UI shell, the MQTT connection and the offline queue.

┌─ agent (Go) ───────────────────┐        ┌─ engine (Python) ────────┐
│ tray icon + WebView2 window    │        │ RTSP capture             │
│ supervises the engine process  │───────▶│ YuNet / ArcFace / FAISS  │
│ MQTT publish + offline spool   │◀───────│ SQLite (biometric)       │
│ S3 handoff, tenant config      │  local │ localhost API + events   │
└────────────────────────────────┘  HTTP  └──────────────────────────┘

Why the split

Go cannot run ONNX, OpenCV or FAISS, so the engine stays Python and ships frozen. Go is here for what it is actually better at: a durable queue that survives a store's internet dropping, a supervised child process, and one language shared with the server so the MQTT contract has a single definition.

Why 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. Since the product is "user starts and stops it from the tray", the agent is a normal user-session process that spawns the engine as a child. That also means it never needs elevation at runtime: starting a child process does not, controlling a service does.

internal/engine is written so a service wrapper can be added later without touching the supervision logic.

Layout

main.go              entry point, mode dispatch
internal/engine      start/stop/supervise the Python engine, health polling
internal/spool       durable event queue (survives restart and outage)
internal/mqtt        broker client, publishes from the spool
internal/config      tenant identity, broker settings, credentials
frontend/            React UI served into the WebView

Build

go build ./...            # agent alone
wails build               # agent + frontend, once the UI is added