Files
Behavision/run-local.sh
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
Five components that ship as one product:

- behavision/  the recognition engine. RTSP ingest, YuNet detection, IoU
               tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
               FastAPI dashboard. Identity is decided once per TRACK from an
               average of at least three embeddings, never per frame.
- agent/       the Go edge agent: supervises the engine, holds a durable
               spool, and drains it to MQTT. Nothing is acked before the
               broker confirms.
- desktop/     the shop PC application (Wails + React + tray).
- server/      the cloud API, MQTT consumer, reports and assistant.
- web/         platform.loyaly.ai, the head-office app, embedded in the
               server binary.

The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.

CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.

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

115 lines
4.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bring the whole platform up locally, from nothing, in one command.
#
# Everything runs on this machine and touches no production system: Postgres and
# Mosquitto in containers, the server as a local binary with the web app built
# into it. Re-running it is safe - it reuses the containers and the database.
#
# Ports are deliberately odd. Docker Desktop itself listens on 127.0.0.1:8080,
# which is how an earlier run of this ended up talking to something that was not
# the server at all.
set -euo pipefail
cd "$(dirname "$0")"
PORT=${PORT:-8088}
PG_PORT=${PG_PORT:-55432}
MQTT_PORT=${MQTT_PORT:-51883}
STATE=${STATE:-.local}
export DATABASE_URL="postgres://postgres:test@127.0.0.1:${PG_PORT}/behavision"
mkdir -p "$STATE/mosquitto"
step() { printf '\n\033[1m%s\033[0m\n' "$*"; }
step "1. Postgres (pgvector - migration 001 needs the extension)"
docker inspect bv-pg >/dev/null 2>&1 || docker run -d --name bv-pg \
-p "${PG_PORT}:5432" -e POSTGRES_PASSWORD=test -e POSTGRES_DB=behavision \
pgvector/pgvector:pg16 >/dev/null
docker start bv-pg >/dev/null 2>&1 || true
until docker exec bv-pg pg_isready -U postgres >/dev/null 2>&1; do sleep 1; done
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
echo " migrations applied"
step "2. Build (the web app builds INTO the Go module, so it goes first)"
(cd web && npm install --silent && npm run build >/dev/null)
(cd server && go build -o "../$STATE/bv-server" ./cmd/behavision-server)
echo " built $STATE/bv-server"
step "3. Encryption key (camera and broker passwords are sealed with it)"
if [ ! -f "$STATE/env.sh" ]; then
KEY=$("./$STATE/bv-server" provision key -raw 2>/dev/null)
cat > "$STATE/env.sh" <<EOF
export DATABASE_URL='${DATABASE_URL}'
export BEHAVISION_SECRET_KEY='${KEY}'
export LISTEN_ADDR=127.0.0.1:${PORT}
export MQTT_URL='tcp://127.0.0.1:${MQTT_PORT}'
export MQTT_USERNAME='behavision-server'
export MQTT_PASSWORD='server-broker-2026'
export AGENT_MQTT_URL='tcp://127.0.0.1:${MQTT_PORT}'
export BEHAVISION_ALLOW_PLAINTEXT_MQTT=1
EOF
chmod 600 "$STATE/env.sh"
echo " new key written to $STATE/env.sh (keep it: without it, sealed passwords are lost)"
else
echo " reusing $STATE/env.sh"
fi
# shellcheck disable=SC1090
. "$STATE/env.sh"
step "4. Mosquitto"
if [ ! -f "$STATE/mosquitto/mosquitto.conf" ]; then
cat > "$STATE/mosquitto/mosquitto.conf" <<EOF
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/acl
EOF
printf 'user behavision-server\ntopic read bv/#\n' > "$STATE/mosquitto/acl"
: > "$STATE/mosquitto/passwd"
fi
docker inspect bv-mqtt >/dev/null 2>&1 || docker run -d --name bv-mqtt \
-p "${MQTT_PORT}:1883" -v "$PWD/$STATE/mosquitto:/mosquitto/config" \
eclipse-mosquitto:2 >/dev/null
docker start bv-mqtt >/dev/null 2>&1 || true
sleep 1
docker exec bv-mqtt mosquitto_passwd -b /mosquitto/config/passwd \
behavision-server "$MQTT_PASSWORD" >/dev/null 2>&1 || true
docker restart bv-mqtt >/dev/null
echo " broker on ${MQTT_PORT}"
step "5. First accounts"
# Idempotent throughout: every provision subcommand upserts, so re-running this
# resets these passwords rather than failing.
"./$STATE/bv-server" provision user -email admin@loyaly.ai -role admin \
-name "Loyaly Platform" -password 'loyaly-platform-2026' >/dev/null
# A tenant to sign in as. In the real flow a platform admin creates this from
# Companies -> New company; it is seeded here so a fresh database has a working
# login without seven steps first. Creating another one through the UI still
# exercises the real path.
"./$STATE/bv-server" provision client -slug tenext-retail -name "TeNext Retail" >/dev/null
"./$STATE/bv-server" provision user -client tenext-retail -email suriya@tenext.in \
-role owner -name "Suriya" -password 'tenext-2026' >/dev/null
# The shop. Its broker password is re-rolled on every run - it is sealed and
# never readable again - so it is pushed into Mosquitto here in the same breath.
# A shop PC enrolled on an earlier run therefore has to be claimed again, which
# is the right trade locally and is why this is not how production works.
SITE_OUT=$("./$STATE/bv-server" provision site -client tenext-retail -slug chennai \
-name "TeNext Chennai" -tz Asia/Kolkata)
BUSER=$(printf '%s' "$SITE_OUT" | sed -n "s/.*passwd \([^ ]*\) .*/\1/p")
BPASS=$(printf '%s' "$SITE_OUT" | sed -n "s/.*passwd [^ ]* '\(.*\)'.*/\1/p")
docker exec bv-mqtt mosquitto_passwd -b /mosquitto/config/passwd "$BUSER" "$BPASS" >/dev/null 2>&1
grep -q "^user $BUSER$" "$STATE/mosquitto/acl" || \
printf '\nuser %s\ntopic write bv/%s/#\n' "$BUSER" "$BUSER" >> "$STATE/mosquitto/acl"
docker restart bv-mqtt >/dev/null
printf ' platform admin admin@loyaly.ai / loyaly-platform-2026 (Companies only)\n'
printf ' TeNext owner suriya@tenext.in / tenext-2026 (Shops, Live, Cameras, Customers, Reports)\n'
step "6. Run"
echo " http://127.0.0.1:${PORT}"
exec "./$STATE/bv-server"