Files
Behavision/RUN.md
Suriyakumarvijayanayagam 2cd7a78ddc A headless PC can be claimed, and a refused broker says so
Both found by operating the stack rather than writing it: the local
processes were OOM-killed and bringing them back hit two gaps.

The headless agent had no way to be claimed at all. Bootstrap lived only
in desktop/internal/cloud, so the one configuration the agent binary
exists for - a back-office PC with no window - could only be onboarded by
hand-editing agent.json, which is the state the desktop's Setup screen was
built to end. `behavision-agent claim <code>` closes it; the CLI joins its
arguments because the code is printed in groups for reading aloud and an
operator pasting it will paste the spaces too.

Second: after the site's broker password was re-rolled, mosquitto logged
"not authorised" while the agent logged "timed out". Those need opposite
actions - re-link this PC, or go and look at the network - and paho's
SetConnectRetry collapses them, because it retries internally and the
connect token never completes. describeStall asks whether a TCP socket
opens at all, and says what is known rather than guessing at a reason the
broker never gives.

Verified end to end: minted a code from the platform as the owner,
claimed with the new command, broker connected, and the shop went to
online: true with 1/1 cameras on w600k_r50.

Also corrects this machine's memory in CLAUDE.md from 16 GB to 8 GB. It
feeds the model-fallback reasoning, and the local gallery already holds
17 embeddings tagged w600k_mbf beside 19 tagged w600k_r50 - the fallback
has silently fired before.

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

279 lines
11 KiB
Markdown

# Running the platform locally
Three processes and two containers. Nothing here touches production.
## The short way
```bash
./run-local.sh # Postgres + Mosquitto + build + accounts + run
```
Idempotent — run it again after a reset and it rebuilds the same state. It
prints the two logins and serves <http://127.0.0.1:8088>.
**Port 8088, not 8080.** Docker Desktop listens on 127.0.0.1:8080 itself, and
an earlier run of this ended up talking to Docker's own listener and reading
its 401 as a Behavision one.
Everything it writes lives in `.local/` — the binary, the encryption key, the
broker's password file. Keep `.local/env.sh`: without that key every sealed
camera and broker password is unrecoverable.
The rest of this file is the same thing done by hand.
## 1. Database
```bash
docker run -d --name bv-pg -p 55432:5432 \
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=behavision \
pgvector/pgvector:pg16 # pgvector, NOT plain postgres — 001 needs it
for f in server/migrations/*.sql; do
docker exec -i bv-pg psql -U postgres -d behavision -v ON_ERROR_STOP=1 -q < "$f"
done
```
## 2. Build
The web app builds INTO the Go module, so the server must be built after it.
```bash
cd web && npm install && npm run build # -> server/internal/web/dist
cd ../server && go build -o bv-server ./cmd/behavision-server
```
## 3. First accounts
The first platform admin comes from the CLI, because creating it cannot require
being signed in as one.
```bash
export DATABASE_URL='postgres://postgres:test@127.0.0.1:55432/behavision'
# -raw prints the key alone. Without it the command prints JSON, for pasting
# into an env file - and `$(... | tail -1)` then captures the whole object and
# hands the server a key it cannot parse.
export BEHAVISION_SECRET_KEY="$(./bv-server provision key -raw)"
./bv-server provision user -email root@loyaly.ai -role admin \
-name "Loyaly Platform" -password 'loyaly-root-2026'
```
Every company after that is created from the web UI: sign in as the admin,
**Companies → New company**. That is the only place tenants are made — there is
no public registration.
## 4. Run
```bash
LISTEN_ADDR=127.0.0.1:8080 ./bv-server
```
Open <http://127.0.0.1:8080>. The API and the web app are the same binary on the
same port; in production Traefik puts `platform.loyaly.ai` in front of it.
## Accounts in the local demo
| who | sign in | sees |
|---|---|---|
| Platform admin | `admin@loyaly.ai` / `loyaly-platform-2026` | Companies only |
| TeNext owner | `suriya@tenext.in` / `tenext-2026` | Shops, Live, Cameras, Customers, Reports |
One address is one account across the whole platform, so the same email cannot
be both a platform admin and a tenant user. See CLAUDE.md.
## Optional: the broker, for live arrivals
Only needed to watch visits arrive in real time. Reports and Customers work
without it.
```bash
docker run -d --name bv-mqtt -p 51883:1883 \
-v "$PWD/mosquitto:/mosquitto/config" eclipse-mosquitto:2
```
The config needs a `passwd` file containing the server's own broker user and one
user per site, and an `acl` granting the server `read bv/#` and each site
`write bv/<client>.<site>/#`. `provision site` prints the site's password once.
Then run the server with `MQTT_URL`, `MQTT_USERNAME` and `MQTT_PASSWORD` set.
## Onboarding a shop, the way a customer is onboarded
1. **Company** — sign in as the platform admin, **Companies → New company**.
The owner's password is shown once.
2. **Shop** — `provision site -client <slug> -site <slug> ...`, then add the
broker user it prints to Mosquitto. Still a command; see the note in
CLAUDE.md about what would have to change for this to be self-service.
3. **Installation code** — sign in as the owner, **Shops → the shop →
Set up a shop PC → Create an installation code**. Shown once, works once.
4. **The shop PC** — open Behavision on it and type the code into **Set up this
PC**. It comes back with the broker credentials, its own API token and its
topic prefix, and starts publishing immediately.
5. **Cameras** — **Cameras → Set up a camera** at head office. The shop PC picks
it up within about two minutes.
6. **Prove it** — **Shops → the shop → Run the check**.
## Running a real shop PC on this machine
The engine and the agent both honour `BEHAVISION_DATA_DIR`, so a checkout can
act as a shop PC:
```bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cd agent && CGO_ENABLED=0 go build -o ../.local/behavision-agent . && cd ..
# agent.json: written by the desktop app's "Set up this PC" screen in real life.
# Locally, redeem a code by hand and write client_slug / site_slug / broker
# credentials / agent_token into $BEHAVISION_DATA_DIR/agent.json.
BEHAVISION_DATA_DIR=$PWD ./.local/behavision-agent run
```
The agent starts the engine itself — do not also run `python -m behavision run`,
or two processes fight over one camera and one SQLite WAL. It passes the
bridge's URL to the engine through `BEHAVISION_WEBHOOK_URL`, and reads the
engine's generated Basic credential from `data/api_credentials.txt`.
`./.local/behavision-agent status` prints what the agent can see of the engine —
the fastest way to tell "the engine is down" from "the agent cannot talk to it".
## Cameras
Onboard one from **Cameras → Set up a camera** on the platform. The shop PC picks it
up within about two minutes and it turns from *Waiting for the shop PC* to
*Connected* once the agent has actually reached it.
Cameras already configured on a shop PC appear here on their own — the agent
offers them up on its first sync, so switching this on does not disturb a site
that is already running.
The picture on each card is the engine's **latest frame**, uploaded about once a
minute, not live video: the camera stream lives on the shop PC's loopback behind
a router, and putting live video in a browser at head office needs a relay this
deployment does not have.
Camera passwords are encrypted with `BEHAVISION_SECRET_KEY`. Without that key
set, a camera can be saved but not with a password, and the API says so rather
than storing it blank.
## Proving a camera works
**Cameras → Set up a camera** walks it in four steps: name and make, connection
details, a connection test, then a walk-past check. Picking the make fills in
the RTSP path, which is the field nobody can find.
A camera is only "verified" once someone has walked past it. Until then the card
says *Not checked yet* even when the stream is connected — those are different
claims, and a camera can stream perfectly while producing views nothing can
recognise.
**Cameras → Is this shop working?** runs the end-to-end check: PC online,
recognition running, cameras connected, faces recognisable, visits arriving. It
stops at the first failure rather than guessing past it.
Checks are jobs the shop PC picks up on its next sync, so allow a couple of
minutes — or restart the agent to make it sync now.
## The assistant
Set `ANTHROPIC_API_KEY` before starting the server and the **Ask** button
appears in the top bar. Without it the server logs that the assistant is off,
the panel says so, and nothing else changes.
It answers from the same business questions the screens ask — shops, cameras,
footfall, sales, customers — and can request a camera check. Every tool runs as
the signed-in user, so it can only see what that person could already see, and
staff are refused camera checks the same way the UI refuses them.
Uses `claude-sonnet-5`; set `BEHAVISION_ASSISTANT_MODEL` to change it without a
rebuild. A conversation is capped at 8 tool round trips.
If your key is **identity-linked** (issued against a user rather than an org),
it also needs `ANTHROPIC_WORKSPACE_ID` — a `wrkspc_...` id from
console.anthropic.com → Settings → Workspaces. Without it every Anthropic
endpoint returns 400, and the server says so by name rather than reporting a
generic fault. A classic API key needs nothing extra.
## Tests
```bash
cd server && go test ./... # no database needed
cd server && TEST_DATABASE_URL="$DATABASE_URL" go test ./... # adds the SQL tests
cd agent && go test ./...
```
The live database tests skip themselves when `TEST_DATABASE_URL` is unset, so
the suite stays runnable with no services — the same rule the bucket tests and
the Python tests follow.
## Windows binaries, from a Mac
```bash
cd desktop/frontend && npm run build # go:embed needs frontend/dist
cd .. && GOOS=windows CGO_ENABLED=0 go build -o behavision-desktop.exe .
cd ../agent && GOOS=windows CGO_ENABLED=0 go build -o behavision-agent.exe .
```
These compile. They have never been RUN on Windows — no `wails build`, no
installer, no code signing.
---
## Building the Windows package
`installer/build.ps1` produces `dist\Behavision-Setup-<version>.exe`.
**It has to run on Windows.** The Go binaries and the front ends cross-compile
from a Mac (verified), but PyInstaller freezes the interpreter and native
wheels of the machine it runs on — there is no cross-target flag. The engine
.exe is built on Windows or not at all.
On the Windows box, with Python 3.11+, Go 1.21+, Node LTS and
[Inno Setup 6](https://jrsoftware.org/isdl.php) installed and on PATH:
```powershell
git clone <this repo> C:\src\Behavision
cd C:\src\Behavision
powershell -ExecutionPolicy Bypass -File installer\build.ps1 -Version 0.1.0
```
It creates the venv, runs the engine test suite (a package is not worth
building if the engine is broken), freezes the engine, builds the app and the
agent, downloads the WebView2 bootstrapper, stages `dist\Behavision\`, starts
the frozen engine once to prove it runs, and compiles the installer.
`-SkipInstaller` stops after staging, for testing without Inno Setup.
### What to check on the Windows box
1. Install as an administrator. Accept the model download.
2. `C:\Program Files\Behavision\engine\behavision.exe paths` — the state root
must be `C:\ProgramData\Behavision`, not anywhere under Program Files.
3. Launch from the Start menu. **Set this PC up on its own** — no code needed.
4. Cameras → Add camera → pick the make → Test connection → Save. The feed must
appear with no restart.
5. Check `/api/health` reports `recognition_model`. On a small machine the
166 MB r50 can lose the fallback chain to the 13 MB mbf, and embeddings are
model-tagged, so which one wins decides whether a gallery carries over.
6. Sign out of the tray (Quit) — recognition must stop with it. Reboot; the app
must come back on its own.
7. Only then link it to head office, from the sidebar.
Nothing is code-signed yet, so SmartScreen will warn on first launch.
## Schema
The server applies pending migrations when it starts and refuses to run against
a schema it does not match. Three ways to look at it by hand:
```bash
export DATABASE_URL=...
./bv-server migrate -status # what is applied, adopted, pending or CHANGED
./bv-server migrate # apply everything pending
./bv-server migrate -baseline 7 # adopt a database built before tracking existed
```
The migrations are compiled into the binary, so **rebuild before migrating** —
a stale binary honestly reports "schema up to date" about files it has never
seen. Never edit an applied migration: the checksum check will refuse it by
name, and the fix is a new file.