#!/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' "$*"; } # Checked up front rather than discovered in the middle of step 2, where the # failure is a bare "go: command not found" after two minutes of npm install. for tool in docker go npm; do command -v "$tool" >/dev/null 2>&1 || { printf 'need %s on PATH.\n' "$tool" >&2 [ "$tool" = go ] && printf 'go is often installed outside the default PATH; try: export PATH="$HOME/go/bin:$PATH"\n' >&2 exit 1 } done 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 echo " ready (the server applies the schema itself on start)" 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" </dev/null; then rm -f "$STATE/mosquitto/passwd" "$STATE/mosquitto/acl" docker rm -f bv-mqtt >/dev/null 2>&1 || true cat > "$STATE/mosquitto/mosquitto.conf" < "$STATE/mosquitto/passwd.seed" docker run --rm -v "$PWD/$STATE/mosquitto:/m" eclipse-mosquitto:2 \ mosquitto_passwd -b /m/passwd.seed behavision-server "$MQTT_PASSWORD" 2>/dev/null "./$STATE/bv-server" broker-init -passwd "$STATE/mosquitto/passwd.seed" \ -out "$STATE/mosquitto/data/dynamic-security.json" -backend-user behavision-server >/dev/null rm -f "$STATE/mosquitto/passwd.seed" # The plugin rewrites this file, so the broker's user (1883) must own it. chmod 666 "$STATE/mosquitto/data/dynamic-security.json" fi # A container is reused only if its config mount still points HERE. The bind # source is baked in when the container is created, so one made while the # checkout lived somewhere else - or by a run from another directory - comes # back up with an empty /mosquitto/config and dies with "Unable to open config # file", which the old `|| true` below then hid completely. MQTT_CONF="$PWD/$STATE/mosquitto" if docker inspect bv-mqtt >/dev/null 2>&1; then MOUNTED=$(docker inspect bv-mqtt \ --format '{{range .Mounts}}{{if eq .Destination "/mosquitto/config"}}{{.Source}}{{end}}{{end}}') if [ "$MOUNTED" != "$MQTT_CONF" ]; then echo " recreating bv-mqtt (its config was mounted from ${MOUNTED:-nowhere})" docker rm -f bv-mqtt >/dev/null fi fi docker inspect bv-mqtt >/dev/null 2>&1 || docker run -d --name bv-mqtt \ -p "${MQTT_PORT}:1883" -v "$MQTT_CONF:/mosquitto/config" \ -v "$MQTT_CONF/data:/mosquitto/data" \ eclipse-mosquitto:2 >/dev/null docker start bv-mqtt >/dev/null 2>&1 || true # Wait for it, and say so if it never arrives. `docker start` returning 0 only # means the container was launched; mosquitto exits a moment later if it cannot # read its config, and every `docker exec` after that fails for a reason that # has nothing to do with what it was asked to do. for _ in $(seq 1 20); do docker exec bv-mqtt sh -c 'exit 0' >/dev/null 2>&1 && break sleep 1 done if ! docker exec bv-mqtt sh -c 'exit 0' >/dev/null 2>&1; then echo " broker will not stay up:" >&2 docker logs --tail 5 bv-mqtt >&2 exit 1 fi echo " broker on ${MQTT_PORT} (dynamic security)" 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. MQTT_URL="tcp://127.0.0.1:${MQTT_PORT}" MQTT_USERNAME=behavision-server MQTT_PASSWORD="$MQTT_PASSWORD" \ "./$STATE/bv-server" provision site -client tenext-retail -slug chennai \ -name "TeNext Chennai" -tz Asia/Kolkata | sed 's/^/ /' 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"