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
166 lines
6 KiB
Bash
Executable file
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
|