linnet/manage.sh
pyr0ball 321abe0646 feat: test/demo/cloud profiles — middleware, compose files, nginx, manage.sh
app/config.py: centralized Settings (DEMO_MODE, CLOUD_MODE, ports, etc.)
app/middleware/demo.py: DemoModeMiddleware — caps sessions (429), blocks export (403), adds X-Linnet-Mode header
app/middleware/cloud.py: CloudAuthMiddleware — requires X-CF-Session on /session/* routes, 401 without it
app/services/session_store.py: active_session_count() for demo cap
app/main.py: wires middleware conditionally, extends CORS for cloud origins

compose.test.yml: hermetic pytest runner in Docker (CF_VOICE_MOCK=1)
compose.demo.yml: DEMO_MODE=true, ports 8523/8524, demo.circuitforge.tech/linnet
compose.cloud.yml: CLOUD_MODE=true, ports 8522/8527, menagerie.circuitforge.tech/linnet

docker/web/Dockerfile: two-stage build (node:20 → nginx:alpine), VITE_BASE_URL/VITE_API_BASE ARGs
docker/web/nginx.conf: SSE + WS proxy, SPA routing (dev/demo)
docker/web/nginx.cloud.conf: adds X-CF-Session forwarding, /linnet/ alias for path-strip Caddy routing

manage.sh: profile arg (dev|demo|cloud|test), start/stop/restart/status/test/logs/build/open per profile
tests/test_profiles.py: 8 tests — demo export block, session cap, cloud auth gate, mode headers
2026-04-06 18:39:07 -07:00

166 lines
6 KiB
Bash
Executable file

#!/usr/bin/env bash
# manage.sh — Linnet dev/demo/cloud management script
set -euo pipefail
CMD=${1:-help}
PROFILE=${2:-dev} # dev | demo | cloud
API_PORT=${LINNET_PORT:-8522}
FE_PORT=${LINNET_FRONTEND_PORT:-8521}
# ── Helpers ───────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
info() { echo -e "${GREEN}[linnet]${NC} $*"; }
warn() { echo -e "${YELLOW}[linnet]${NC} $*"; }
error() { echo -e "${RED}[linnet]${NC} $*" >&2; exit 1; }
_check_env() {
if [[ ! -f .env ]]; then
warn "No .env found — copying .env.example"
cp .env.example .env
fi
}
_compose_cmd() {
# Returns the correct compose file flags for a given profile
case "$1" in
demo) echo "-f compose.demo.yml -p linnet-demo" ;;
cloud) echo "-f compose.cloud.yml -p linnet-cloud" ;;
test) echo "-f compose.test.yml" ;;
dev) echo "-f compose.yml" ;;
*) error "Unknown profile: $1. Use dev|demo|cloud|test." ;;
esac
}
# ── Commands ──────────────────────────────────────────────────────────────────
case "$CMD" in
start)
_check_env
if [[ "$PROFILE" == "dev" ]]; then
info "Starting Linnet API on :${API_PORT} (mock mode, dev)…"
CF_VOICE_MOCK=1 conda run -n cf uvicorn app.main:app \
--host 0.0.0.0 --port "$API_PORT" --reload &
info "Starting Linnet frontend on :${FE_PORT}"
cd frontend && npm install --silent && npm run dev &
info "API: http://localhost:${API_PORT}"
info "UI: http://localhost:${FE_PORT}"
wait
else
FLAGS=$(_compose_cmd "$PROFILE")
info "Starting Linnet ($PROFILE profile)…"
# shellcheck disable=SC2086
docker compose $FLAGS up -d --build
case "$PROFILE" in
demo) info "Frontend: http://localhost:8524" ;;
cloud) info "Frontend: http://localhost:8527 → menagerie.circuitforge.tech/linnet" ;;
esac
fi
;;
stop)
if [[ "$PROFILE" == "dev" ]]; then
info "Stopping Linnet dev processes…"
pkill -f "uvicorn app.main:app" 2>/dev/null || true
pkill -f "vite" 2>/dev/null || true
else
FLAGS=$(_compose_cmd "$PROFILE")
# shellcheck disable=SC2086
docker compose $FLAGS down
fi
info "Stopped ($PROFILE)."
;;
restart)
"$0" stop "$PROFILE"
"$0" start "$PROFILE"
;;
status)
case "$PROFILE" in
dev)
echo -n "API: "
curl -sf "http://localhost:${API_PORT}/health" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'], '|', d.get('mode','dev'))" \
|| echo "not running"
echo -n "Frontend: "
curl -sf "http://localhost:${FE_PORT}" -o /dev/null && echo "running" || echo "not running"
;;
demo)
echo -n "Demo API: "; curl -sf "http://localhost:8523/health" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])" || echo "not running"
echo -n "Demo Web: "; curl -sf "http://localhost:8524" -o /dev/null && echo "running" || echo "not running"
;;
cloud)
echo -n "Cloud API: "; curl -sf "http://localhost:8522/health" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])" || echo "not running"
echo -n "Cloud Web: "; curl -sf "http://localhost:8527" -o /dev/null && echo "running" || echo "not running"
;;
esac
;;
test)
_check_env
if [[ "$PROFILE" == "dev" ]]; then
info "Running test suite (local)…"
CF_VOICE_MOCK=1 conda run -n cf python -m pytest tests/ -v "${@:3}"
else
info "Running test suite (Docker)…"
docker compose -f compose.test.yml run --rm linnet-test "${@:3}"
fi
;;
logs)
if [[ "$PROFILE" == "dev" ]]; then
warn "Dev mode: check terminal output."
else
FLAGS=$(_compose_cmd "$PROFILE")
SERVICE=${3:-}
# shellcheck disable=SC2086
docker compose $FLAGS logs -f $SERVICE
fi
;;
build)
info "Building Docker images ($PROFILE)…"
FLAGS=$(_compose_cmd "$PROFILE")
# shellcheck disable=SC2086
docker compose $FLAGS build
info "Build complete."
;;
open)
case "$PROFILE" in
demo) URL="http://localhost:8524" ;;
cloud) URL="http://localhost:8527" ;;
*) URL="http://localhost:${FE_PORT}" ;;
esac
xdg-open "$URL" 2>/dev/null || open "$URL" 2>/dev/null || info "Open $URL in your browser"
;;
help|*)
echo ""
echo " Usage: $0 <command> [profile]"
echo ""
echo " Commands:"
echo -e " ${GREEN}start${NC} [profile] Start API + frontend"
echo -e " ${GREEN}stop${NC} [profile] Stop services"
echo -e " ${GREEN}restart${NC} [profile] Stop then start"
echo -e " ${GREEN}status${NC} [profile] Check running services"
echo -e " ${GREEN}test${NC} [profile] Run pytest suite"
echo -e " ${GREEN}logs${NC} [profile] Tail logs (Docker profiles only)"
echo -e " ${GREEN}build${NC} [profile] Build Docker images"
echo -e " ${GREEN}open${NC} [profile] Open UI in browser"
echo ""
echo " Profiles:"
echo " dev (default) Local uvicorn + Vite dev server, mock mode"
echo " demo Docker: DEMO_MODE=true, port 8524"
echo " cloud Docker: CLOUD_MODE=true, port 8527 → menagerie.circuitforge.tech/linnet"
echo " test Docker: pytest suite, hermetic"
echo ""
echo " Examples:"
echo " $0 start # dev mode"
echo " $0 start demo # demo stack"
echo " $0 test # local pytest"
echo " $0 test docker # pytest in Docker"
echo " $0 logs demo # demo logs"
echo ""
;;
esac