Migrations were run by hand and nothing recorded which had run, so re-running the setup script against an existing database failed on the first CREATE TABLE, and shipping a new migration gave an operator no way to know whether an estate had it. A missed migration is not a startup error - it is a query referencing a column that is not there, surfacing later on whichever endpoint touches it first. server/internal/migrate applies pending migrations at boot and refuses to start against a schema it does not match. One transaction per file holding both the DDL and the row that records it; an advisory lock so two servers starting at once cannot both apply 008; checksums so an edited migration is refused by name rather than silently skipped; numeric ordering so 010 does not run before 009. `migrate -baseline N` adopts a database built before any of this existed, because "the clients table exists" does not say whether 007's index does. Verified on the live database: adopted 001-007, applied 008. 008 adds two indexes on `purchases`, found by asking the database which foreign keys had nothing behind them and then checking what queries the table. The conversion report filters client_id + occurred_at, which is exactly the estate-wide case with no site to narrow it. run-local.sh had two bugs, both found by running it rather than reading it: it reused a broker container whose bind mount pointed at a directory that no longer existed, and it discarded stderr on the mosquitto_passwd call, so under `set -e` it exited at step 5 with no output at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
279 lines
11 KiB
Markdown
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 16 GB 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.
|