diff --git a/API.md b/API.md index aacf5e8..3e5f53e 100644 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ # Behavision API — for the web console, a mobile app, and platform administration -Base URL: `https://platform.loyaly.ai` (locally `http://127.0.0.1:8088`). +Base URL: `https://mcp.loyaly.ai` — the API host. (`platform.loyaly.ai` serves the head-office web console, not the API.) Everything is JSON unless stated. All times are RFC 3339 UTC unless a field says otherwise. Every shape below is taken from the server's own types, not written from memory — if the two ever disagree, the server is right and this file has a diff --git a/server/Dockerfile.runtime b/server/Dockerfile.runtime new file mode 100644 index 0000000..7311423 --- /dev/null +++ b/server/Dockerfile.runtime @@ -0,0 +1,14 @@ +# Runtime-only image, for a host that must not compile. +# +# The production box has 3.6 GB of RAM shared with other tenants' services; +# `Dockerfile` pulls a Go toolchain and builds there, which is how deploys +# became something nobody wanted to run. deploy.sh builds the static binary +# on the developer's machine and ships only that. Same runtime layer as +# Dockerfile, on purpose - the two must not drift. +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata && \ + adduser -D -u 10001 behavision +COPY behavision-server /usr/local/bin/behavision-server +USER behavision +EXPOSE 8080 +ENTRYPOINT ["/usr/local/bin/behavision-server"] diff --git a/server/deploy.sh b/server/deploy.sh new file mode 100755 index 0000000..4612c3b --- /dev/null +++ b/server/deploy.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Deploy the server to the production host, from this checkout. +# +# Until this existed the deployment was manual to a box nobody had written +# down, and production ran code months behind main - three of the desktop +# app's screens talked to routes that were not there. A deploy that is a +# script gets run; one that is a memory does not. +# +# server/deploy.sh build, back up the database, migrate, switch +# BASELINE=3 server/deploy.sh first run against a database that predates +# migration tracking: adopt 1..3 unrun +# DRY_RUN=1 server/deploy.sh build and ship, touch nothing running +# +# What it does, in order, and why the order matters: +# 1. builds the head-office web app INTO the Go module, then a static +# linux/amd64 binary here - the host has 3.6 GB of RAM and must not compile +# 2. pg_dumps the database to backups/ on the host BEFORE anything changes +# 3. builds the runtime-only image on the host from the shipped binary +# 4. runs `migrate` with the NEW binary while the OLD server still serves; +# a failing migration therefore stops here with production untouched +# 5. switches the container, then proves the routes answer over the public URL +set -euo pipefail +cd "$(dirname "$0")" + +HOST=${HOST:-root@66.116.226.161} +KEY=${KEY:-$HOME/.ssh/behavision_deploy} +REMOTE_DIR=/root/behavision +PUBLIC=https://mcp.loyaly.ai +SSH=(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=10 "$HOST") + +VERSION=$(git describe --tags --always --dirty) +case "$VERSION" in *-dirty) echo "refusing to deploy uncommitted changes ($VERSION)" >&2; exit 1;; esac + +step() { printf '\n\033[1m%s\033[0m\n' "$*"; } + +step "1. Build $VERSION" +(cd ../web && npm run build >/dev/null) +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \ + -ldflags "-s -w -X main.version=${VERSION}" -o /tmp/behavision-server ./cmd/behavision-server +ls -la /tmp/behavision-server | awk '{print " " $5 " bytes"}' + +step "2. Ship" +"${SSH[@]}" "mkdir -p $REMOTE_DIR/release/$VERSION $REMOTE_DIR/backups" +scp -q -i "$KEY" /tmp/behavision-server Dockerfile.runtime "$HOST:$REMOTE_DIR/release/$VERSION/" +git rev-parse HEAD | "${SSH[@]}" "cat > $REMOTE_DIR/release/$VERSION/GIT_SHA" + +if [ "${DRY_RUN:-}" != "" ]; then echo "DRY_RUN: shipped to $REMOTE_DIR/release/$VERSION, nothing changed"; exit 0; fi + +step "3. Back up the database" +"${SSH[@]}" "docker exec behavision-db sh -c 'PGPASSWORD=\$POSTGRES_PASSWORD pg_dump -U behavision -d behavision' | gzip > $REMOTE_DIR/backups/pre-$VERSION-\$(date +%Y%m%d-%H%M%S).sql.gz && ls -la $REMOTE_DIR/backups | tail -1" + +step "4. Image" +"${SSH[@]}" "cd $REMOTE_DIR/release/$VERSION && docker build -q -t behavision-backend:$VERSION -f Dockerfile.runtime . && docker tag behavision-backend:$VERSION behavision-backend:latest" + +step "5. Migrate (old server still serving)" +# `run` uses the compose service's environment and network, so the new binary +# reaches postgres exactly as the server will. --no-deps: do not restart the +# broker or the database to run a migration. +if [ -n "${BASELINE:-}" ]; then + "${SSH[@]}" "cd $REMOTE_DIR && docker compose run --rm --no-deps -T backend migrate -baseline $BASELINE" +fi +"${SSH[@]}" "cd $REMOTE_DIR && docker compose run --rm --no-deps -T backend migrate && docker compose run --rm --no-deps -T backend migrate -status" + +step "6. Switch" +"${SSH[@]}" "cd $REMOTE_DIR && docker compose up -d --no-build --no-deps backend && sleep 4 && docker logs --tail 15 behavision-backend" + +step "7. Verify over $PUBLIC" +for p in /healthz /api/admin/clients /api/team /api/visits /api/cameras; do + printf ' %-20s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' -m 15 "$PUBLIC$p")" +done +curl -s -m 15 "$PUBLIC/healthz" | head -c 300; echo