Backend: - tiers.py: real license validation via Heimdall (self-hosted key or cloud user_id), 30-min cache, BYOK exception for translation - session_store.py: create_session() accepts tier/user_id/window_ms/transcribe_lang/ num_speakers; tier-aware _should_reap() (Free: reap on subscriber leave, Paid: activity TTL); auto-snapshot paid sessions with history on reap; voice-ready probe on session start - services/translator.py: DeepL-backed label translation, CF-managed and BYOK key paths, per-session label cache - models/session.py, scene_event.py, service_event.py: new event types and session fields - api/snapshots.py: GET /sessions/history + POST /session/resume (Paid tier only) - api/dev.py: dev-only /dev/voice/stop|start|restart proxy to cf-orch agent - migrations/002_session_snapshots.sql: session_snapshots table Frontend: - ComposeBar.vue: pre-session settings panel (window, language, speakers, Elcor toggle) with slide transition; passes options to startSession() - stores/settings.ts: persistent user preferences (localStorage), survives page reload - stores/session.ts: voiceReady/voiceError/voiceWarning states; SceneEvent/AccentEvent/ ServiceEvent types; TTL timers for environ/queue badges; startSession() accepts opts - composables/useToneStream.ts: handlers for scene-event, accent-event, service-event SSE - NowPanel.vue: diarization speaker identity badge, acoustic scene badge, privacy risk indicator - HistoryStrip.vue: per-speaker colour chips in history strip - components/DevPanel.vue: dev-only cf-voice stop/restart controls + thread view toggle - App.vue: DevPanel in footer (DEV only), threadView flag switches v0.1.x/v0.2.x preview Tests: - tests/test_pinning.py: 20 tests covering tier-aware reaping, snapshot creation, and translator behaviour - tests/test_tiers.py: 17 tests covering key validation, cloud resolution, caching, require_paid() gate, BYOK exception - All 72 tests passing
169 lines
6.1 KiB
Bash
Executable file
169 lines
6.1 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
|
|
# Load .env into the current shell so uvicorn and Vite inherit all settings
|
|
# (CF_VOICE_MOCK, CF_ORCH_URL, VITE_BASE_URL, VITE_API_BASE, etc.)
|
|
set -a; source .env; set +a
|
|
info "Starting Linnet API on :${API_PORT}…"
|
|
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
|