#!/bin/bash set -e # ── Secrets ─────────────────────────────────────────────────────────────────── # Set these as HF Secrets in Space Settings → Variables and secrets. # If unset, random values are generated — auth breaks on every restart! if [ -z "$JWT_SECRET" ]; then echo "[WARN] JWT_SECRET not set — generating ephemeral value. Set it as a HF Secret." export JWT_SECRET="$(openssl rand -base64 48)" fi if [ -z "$API_KEY_SECRET" ]; then echo "[WARN] API_KEY_SECRET not set — generating ephemeral value. Set it as a HF Secret." export API_KEY_SECRET="$(openssl rand -hex 32)" fi if [ -z "$OMNIROUTE_WS_BRIDGE_SECRET" ]; then echo "[WARN] OMNIROUTE_WS_BRIDGE_SECRET not set — generating ephemeral value." export OMNIROUTE_WS_BRIDGE_SECRET="$(openssl rand -hex 32)" fi # ── Core config ─────────────────────────────────────────────────────────────── export PORT="${PORT:-7860}" export HOSTNAME="0.0.0.0" export INITIAL_PASSWORD="${INITIAL_PASSWORD:-omniroute}" export DATA_DIR="${DATA_DIR:-/app/data}" export AUTH_COOKIE_SECURE="${AUTH_COOKIE_SECURE:-false}" export NODE_ENV="${NODE_ENV:-production}" export STORAGE_DRIVER="${STORAGE_DRIVER:-sqlite}" # ── Public URL ──────────────────────────────────────────────────────────────── if [ -n "$SPACE_HOST" ]; then export NEXT_PUBLIC_BASE_URL="https://${SPACE_HOST}" export BASE_URL="https://${SPACE_HOST}" else export NEXT_PUBLIC_BASE_URL="${NEXT_PUBLIC_BASE_URL:-http://0.0.0.0:${PORT}}" export BASE_URL="${BASE_URL:-${NEXT_PUBLIC_BASE_URL}}" fi # ── Storage dir must exist before restore ───────────────────────────────────── mkdir -p "$DATA_DIR" # ── HF Dataset persistence (Node, zero-dependency) ──────────────────────────── # Set HF_TOKEN + HF_STATE_DATASET as HF Secrets to persist the DB across # restarts and rebuilds. Unlike the old python3/huggingface_hub block, hf-sync # needs nothing beyond the Node runtime that already ships in this image, it # pulls a WAL-consistent snapshot via OmniRoute's own export API, and it uploads # on an interval AND on shutdown (SIGTERM) so a restart can't drop config. if [ -n "$HF_TOKEN" ] && [ -n "$HF_STATE_DATASET" ]; then echo "[sync] Restoring state from HF dataset: $HF_STATE_DATASET" node /app/hf-sync.mjs restore || echo "[sync] restore reported an error (continuing with local/fresh DB)" else echo "[sync] HF persistence disabled" echo "[sync] Set HF_TOKEN and HF_STATE_DATASET to enable persistence" fi echo "================================================" echo " OmniRoute starting on port $PORT" echo " Dashboard: ${NEXT_PUBLIC_BASE_URL}" echo " Data dir: $DATA_DIR" echo "================================================" # ── Run the app as a child so we can snapshot on shutdown ───────────────────── # HF Spaces delivers SIGTERM on restart/rebuild AND when a free Space is put to # sleep. `exec node …` would replace this shell and make a shutdown snapshot # impossible, so we background the app, trap the signal, take one final upload, # then forward the signal on. # # Ordering matters and is deliberate: we snapshot the DB via OmniRoute's own # export API *while the app is still alive*, and only THEN stop the app. The # final snapshot is bounded by `timeout` (and a low per-request timeout) so on # slow cpu-basic hardware it aborts cleanly — leaving the dataset on the last # good commit — rather than being frozen mid-upload when SIGKILL lands. The # periodic daemon is the real safety net; this is best-effort on top. APP_PID="" SHUTDOWN_BUDGET="${HF_SYNC_SHUTDOWN_SECONDS:-8}" shutdown() { echo "[sync] shutdown signal received" if [ -n "$HF_TOKEN" ] && [ -n "$HF_STATE_DATASET" ]; then echo "[sync] taking final snapshot before exit (budget ${SHUTDOWN_BUDGET}s)" HF_SYNC_HTTP_TIMEOUT_MS=$(( (SHUTDOWN_BUDGET - 1) * 1000 )) \ timeout "${SHUTDOWN_BUDGET}s" node /app/hf-sync.mjs snapshot \ || echo "[sync] final snapshot skipped/failed (dataset stays on last good commit)" fi if [ -n "$APP_PID" ]; then kill -TERM "$APP_PID" 2>/dev/null || true; wait "$APP_PID" 2>/dev/null || true; fi exit 0 } trap shutdown TERM INT # Periodic snapshotter (no-ops immediately if persistence is disabled). node /app/hf-sync.mjs daemon & SYNC_PID=$! node dev/run-standalone.mjs & APP_PID=$! # `wait` returns when the trap fires; loop so we keep waiting until the app # actually exits rather than returning on the first delivered signal. while kill -0 "$APP_PID" 2>/dev/null; do wait "$APP_PID" && break done EXIT_CODE=$? kill -TERM "$SYNC_PID" 2>/dev/null || true echo "[sync] app exited with code $EXIT_CODE" exit $EXIT_CODE