Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,67 @@
# syntax=docker/dockerfile:1.7
# ---- Stage 1: build React SPA ----
FROM node:20-alpine AS web-builder
WORKDIR /web
# Build-time SPA config — pass via `--build-arg VITE_*=...`. Defaults
# point at the DIDI SSO cluster, which is what production uses.
ARG VITE_KEYCLOAK_URL=https://sso.clossers.com
ARG VITE_KEYCLOAK_REALM=didi-clients
ARG VITE_KEYCLOAK_CLIENT_ID=ai-platform-dashboard
ARG VITE_KEYCLOAK_REQUIRED_ROLE=admin
ARG VITE_STAGING_MODE=false
ENV VITE_KEYCLOAK_URL=$VITE_KEYCLOAK_URL \
VITE_KEYCLOAK_REALM=$VITE_KEYCLOAK_REALM \
VITE_KEYCLOAK_CLIENT_ID=$VITE_KEYCLOAK_CLIENT_ID \
VITE_KEYCLOAK_REQUIRED_ROLE=$VITE_KEYCLOAK_REQUIRED_ROLE \
VITE_STAGING_MODE=$VITE_STAGING_MODE
# Cache deps
COPY web/package.json web/package-lock.json* ./
RUN npm install --no-audit --no-fund
# Build
COPY web/index.html web/tsconfig.json web/vite.config.ts ./
COPY web/src ./src
RUN npm run build
# ---- Stage 2: build Python deps ----
FROM python:3.11.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.10 /uv /uvx /bin/
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PROJECT_ENVIRONMENT=/app/.venv
WORKDIR /app
COPY pyproject.toml ./
RUN uv venv /app/.venv && uv pip install --python /app/.venv/bin/python \
fastapi uvicorn[standard] pydantic pydantic-settings httpx \
"sqlalchemy[asyncio]>=2.0.36" asyncpg alembic jinja2 python-multipart
COPY src ./src
RUN uv pip install --python /app/.venv/bin/python -e .
# ---- Stage 3: runtime ----
FROM python:3.11.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app /app
COPY --from=web-builder /web/dist /app/web_dist
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONPATH=/app/src \
PYTHONUNBUFFERED=1
WORKDIR /app
EXPOSE 51300
CMD ["uvicorn", "dashboard.api.app:app", "--host", "0.0.0.0", "--port", "51300"]

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
[[ -f .env ]] && set -a && source .env && set +a
check_required_var() {
if [[ -z "${!1:-}" ]]; then
echo "ERROR: Required variable $1 not set (check .env)"
exit 1
fi
}
usage() {
cat <<'EOF'
Usage: ./deploy.sh {up|down|logs|restart}
Actions:
up Build and start dashboard + database
down Stop containers (volumes preserved)
logs Tail logs
restart Restart dashboard without rebuild
EOF
}
check_required_var DASHBOARD_DB_USER
check_required_var DASHBOARD_DB_PASSWORD
check_required_var DASHBOARD_DB_NAME
check_required_var DASHBOARD_EXTERNAL_URL
case "${1:-}" in
up)
docker compose --profile dashboard up -d --build
echo "Dashboard started at ${DASHBOARD_EXTERNAL_URL}"
;;
down)
docker compose --profile dashboard down
;;
logs)
docker compose --profile dashboard logs -f --tail=100
;;
restart)
docker compose --profile dashboard restart
;;
*)
usage
exit 1
;;
esac

View file

@ -0,0 +1,102 @@
# Dashboard Module - Docker Compose
#
# Port Allocation (Dev: 51300):
# 51300 - Dashboard API + Web UI
# 15432 - PostgreSQL (optional host exposure for debugging)
#
# Naming Convention: didiAI-{module}-{service}
networks:
didi-network:
external: true # single shared network for all DIDI + AI platform stacks
volumes:
dashboard_db_data:
driver: local
services:
# ==========================================================================
# PostgreSQL
# ==========================================================================
dashboard-db:
container_name: didiAI-dashboard-db
image: postgres:16-alpine
networks:
- didi-network
environment:
- POSTGRES_USER=${DASHBOARD_DB_USER}
- POSTGRES_PASSWORD=${DASHBOARD_DB_PASSWORD}
- POSTGRES_DB=${DASHBOARD_DB_NAME}
volumes:
- dashboard_db_data:/var/lib/postgresql/data
ports:
- "15432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DASHBOARD_DB_USER} -d ${DASHBOARD_DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
profiles:
- dashboard
# ==========================================================================
# Dashboard API + Web UI
# ==========================================================================
dashboard-api:
container_name: didiAI-dashboard
image: didiai-dashboard
build:
context: ..
dockerfile: deploy/Dockerfile
args:
# SPA build-time config — Keycloak settings get baked into the JS bundle.
# Default: local Keycloak proxied through admin nginx at /auth/.
VITE_KEYCLOAK_URL: ${VITE_KEYCLOAK_URL:-https://10.11.10.11:3001/auth}
VITE_KEYCLOAK_REALM: ${VITE_KEYCLOAK_REALM:-didi-admins}
VITE_KEYCLOAK_CLIENT_ID: ${VITE_KEYCLOAK_CLIENT_ID:-ai-platform-dashboard}
VITE_KEYCLOAK_REQUIRED_ROLE: ${VITE_KEYCLOAK_REQUIRED_ROLE:-admin}
VITE_STAGING_MODE: ${VITE_STAGING_MODE:-false}
ports:
- "51300:51300"
networks:
- didi-network
depends_on:
dashboard-db:
condition: service_healthy
env_file:
- .env
environment:
- DASHBOARD_HOST=0.0.0.0
- DASHBOARD_PORT=51300
- DASHBOARD_DATABASE_URL=postgresql+asyncpg://${DASHBOARD_DB_USER}:${DASHBOARD_DB_PASSWORD}@didiAI-dashboard-db:5432/${DASHBOARD_DB_NAME}
- DASHBOARD_BRAIN_URL=${DASHBOARD_BRAIN_URL:-http://didibrain-api:8090}
# Keycloak SSO (leave empty to disable JWT auth)
- DASHBOARD_KEYCLOAK_URL=${DASHBOARD_KEYCLOAK_URL:-}
- DASHBOARD_KEYCLOAK_REALM=${DASHBOARD_KEYCLOAK_REALM:-didi-clients}
- DASHBOARD_KEYCLOAK_CLIENT_ID=${DASHBOARD_KEYCLOAK_CLIENT_ID:-ai-platform-dashboard}
- DASHBOARD_KEYCLOAK_REQUIRED_ROLE=${DASHBOARD_KEYCLOAK_REQUIRED_ROLE:-admin}
- DASHBOARD_STAGING_MODE=${DASHBOARD_STAGING_MODE:-false}
# OTel — traces to Jaeger via OTel Collector
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-http://didi-otel-collector:4317}
- OTEL_SERVICE_NAME=didiAI-dashboard
# Module health endpoint overrides — point at real upstreams
# (raw vLLM at 10.11.10.15 instead of the unbuilt FastAPI wrapper at 10.11.10.17:14100/14200)
- DASHBOARD_EMBEDDINGS_HEALTH_URL=${DASHBOARD_EMBEDDINGS_HEALTH_URL:-http://10.11.10.15:8200/v1/models}
- DASHBOARD_RERANK_HEALTH_URL=${DASHBOARD_RERANK_HEALTH_URL:-http://10.11.10.15:8100/v1/models}
# Catalog runs locally on Docker DNS; /v1/status is the aggregated reachability probe
# (catalog does not expose /v1/info — it consumes it from other modules)
- DASHBOARD_CATALOG_HEALTH_URL=${DASHBOARD_CATALOG_HEALTH_URL:-http://didiAI-catalog-api:11000/v1/status}
# Gateway is nginx — only /health is auth-free and inline
- DASHBOARD_GATEWAY_HEALTH_URL=${DASHBOARD_GATEWAY_HEALTH_URL:-http://didiAI-gateway:11000/health}
# Brain doesn't expose /v1/info — point at /health (matches catalog rationale)
- DASHBOARD_BRAIN_HEALTH_URL=${DASHBOARD_BRAIN_HEALTH_URL:-http://didibrain-api:8090/health}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:51300/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped
profiles:
- dashboard

View file

@ -0,0 +1,209 @@
#!/usr/bin/env bash
# Set up Keycloak client `ai-platform-dashboard` on sso.clossers.com realm `didi-clients`.
# Idempotent — safe to re-run; existing client gets updated to match this config.
#
# Usage:
# KC_ADMIN_USER=admin KC_ADMIN_PASS=admin123 ./setup-keycloak.sh
#
# Optional overrides:
# KC_BASE_URL=https://sso.clossers.com
# KC_REALM=didi-clients
# KC_CLIENT_ID=ai-platform-dashboard
# DASHBOARD_HOST=10.11.10.12 # used to build redirect URI
set -euo pipefail
# Keycloak admin endpoints redirect public sso.clossers.com → internal
# sso.clossers.local. Token issuer must match the host you call, so we use the
# internal name end-to-end (this host has DNS for it).
KC_BASE_URL="${KC_BASE_URL:-https://sso.clossers.local}"
KC_REALM="${KC_REALM:-didi-clients}"
KC_CLIENT_ID="${KC_CLIENT_ID:-ai-platform-dashboard}"
KC_ADMIN_USER="${KC_ADMIN_USER:-admin}"
KC_ADMIN_PASS="${KC_ADMIN_PASS:?Set KC_ADMIN_PASS}"
DASHBOARD_HOST="${DASHBOARD_HOST:-10.11.10.12}"
DASHBOARD_PORT="${DASHBOARD_PORT:-51300}"
echo "→ Authenticating to Keycloak master realm…"
ADMIN_TOKEN=$(
curl -ksS -X POST "$KC_BASE_URL/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=admin-cli&grant_type=password&username=$KC_ADMIN_USER&password=$KC_ADMIN_PASS" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])"
)
[ -n "$ADMIN_TOKEN" ] || { echo "FAIL: could not get admin token"; exit 1; }
echo " OK (token length ${#ADMIN_TOKEN})"
# ---------- Step 1: ensure realm role 'admin' exists ----------
echo "→ Checking realm role 'admin' exists in realm '$KC_REALM'…"
RAW=$(curl -ksSL -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_BASE_URL/admin/realms/$KC_REALM/roles/admin")
if echo "$RAW" | grep -q '"name"'; then
echo " OK — role 'admin' present"
else
echo " NOT found — creating…"
curl -ksSL -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"admin","description":"Platform admin (DIDI + AI platform)"}' \
"$KC_BASE_URL/admin/realms/$KC_REALM/roles"
echo " CREATED"
fi
# ---------- Step 2: ensure client `ai-platform-dashboard` exists ----------
echo "→ Checking client '$KC_CLIENT_ID' exists…"
EXISTING=$(curl -ksSL -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients?clientId=$KC_CLIENT_ID")
CLIENT_UUID=$(echo "$EXISTING" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['id'] if d else '')")
REDIRECTS=$(python3 -c "
import json
print(json.dumps([
'http://$DASHBOARD_HOST:$DASHBOARD_PORT/admin-ai/*',
'https://$DASHBOARD_HOST:$DASHBOARD_PORT/admin-ai/*',
'http://localhost:$DASHBOARD_PORT/admin-ai/*',
'http://localhost:5173/*',
# Reverse-proxy URL (admin-dashboard nginx exposes /admin-ai/ on port 3000)
'https://$DASHBOARD_HOST:3000/admin-ai/*',
]))
")
PAYLOAD=$(python3 -c "
import json, os
print(json.dumps({
'clientId': '$KC_CLIENT_ID',
'name': 'AI Platform Admin Dashboard',
'description': 'Reskinned admin dashboard for the AI platform (web/llm/embeddings/rerank/audio/video/catalog/brain)',
'rootUrl': 'http://$DASHBOARD_HOST:$DASHBOARD_PORT/admin-ai/',
'baseUrl': '/admin-ai/',
'enabled': True,
'protocol': 'openid-connect',
'publicClient': True,
'standardFlowEnabled': True,
'directAccessGrantsEnabled': False,
'serviceAccountsEnabled': False,
'frontchannelLogout': True,
'redirectUris': $REDIRECTS,
'webOrigins': ['+'],
'attributes': {
'pkce.code.challenge.method': 'S256',
'post.logout.redirect.uris': '+',
},
}))
")
if [ -z "$CLIENT_UUID" ]; then
echo " NOT found — creating…"
curl -ksSL -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients"
CLIENT_UUID=$(
curl -ksSL -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients?clientId=$KC_CLIENT_ID" \
| python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])"
)
echo " CREATED uuid=$CLIENT_UUID"
else
echo " Found uuid=$CLIENT_UUID — updating to current spec…"
curl -ksSL -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients/$CLIENT_UUID"
echo " UPDATED"
fi
# ---------- Step 3: smoke test JWKS reachable ----------
echo "→ Smoke test: JWKS endpoint reachable…"
JWKS_KEYS=$(curl -ksS \
"$KC_BASE_URL/realms/$KC_REALM/protocol/openid-connect/certs" \
| python3 -c "import json,sys; print(len(json.load(sys.stdin).get('keys',[])))")
echo " OK — JWKS returns $JWKS_KEYS keys"
# ---------- Step 3b: ensure admin-dashboard client also exists (DIDI side) ----------
# This is a no-op when the client already exists with correct config; included
# so a fresh Keycloak install gets both apps wired in one go.
ADMIN_DASH_CLIENT="admin-dashboard"
echo "→ Checking client '$ADMIN_DASH_CLIENT' (DIDI admin) exists…"
EXISTING_AD=$(curl -ksSL -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients?clientId=$ADMIN_DASH_CLIENT")
ADMIN_DASH_UUID=$(echo "$EXISTING_AD" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['id'] if d else '')")
ADMIN_DASH_REDIRECTS=$(python3 -c "
import json
print(json.dumps([
'https://$DASHBOARD_HOST:3000/admin/*',
'https://$DASHBOARD_HOST:3000/admin-backend/*',
'http://localhost:3000/admin/*',
]))
")
ADMIN_DASH_PAYLOAD=$(python3 -c "
import json
print(json.dumps({
'clientId': '$ADMIN_DASH_CLIENT',
'name': 'DIDI Admin Dashboard',
'description': 'Backend admin (framework config, users, moderation queue, history)',
'rootUrl': 'https://$DASHBOARD_HOST:3000/admin/',
'baseUrl': '/admin/',
'enabled': True,
'protocol': 'openid-connect',
'publicClient': True,
'standardFlowEnabled': True,
'directAccessGrantsEnabled': False,
'redirectUris': $ADMIN_DASH_REDIRECTS,
'webOrigins': ['+'],
'attributes': {
'pkce.code.challenge.method': 'S256',
'post.logout.redirect.uris': '+',
},
}))
")
if [ -z "$ADMIN_DASH_UUID" ]; then
echo " NOT found — creating…"
curl -ksSL -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "$ADMIN_DASH_PAYLOAD" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients"
ADMIN_DASH_UUID=$(
curl -ksSL -H "Authorization: Bearer $ADMIN_TOKEN" \
"$KC_BASE_URL/admin/realms/$KC_REALM/clients?clientId=$ADMIN_DASH_CLIENT" \
| python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])"
)
echo " CREATED uuid=$ADMIN_DASH_UUID"
else
echo " Found uuid=$ADMIN_DASH_UUID — leaving config as-is (would overwrite custom redirect URIs)"
fi
# ---------- Step 4: print summary ----------
cat <<EOF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Keycloak setup complete.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Realm: $KC_REALM
Client: $KC_CLIENT_ID (uuid=$CLIENT_UUID)
Role: admin (realm)
Issuer: $KC_BASE_URL/realms/$KC_REALM
JWKS: $KC_BASE_URL/realms/$KC_REALM/protocol/openid-connect/certs
REDIRECTS:
$(python3 -c "import json; [print(f' - {u}') for u in $REDIRECTS]")
NEXT STEPS:
1. Verify YOUR user has the 'admin' realm role assigned in $KC_REALM.
(If you can already log into the DIDI admin-dashboard, you do.)
2. Update dashboard/.env:
DASHBOARD_KEYCLOAK_URL=$KC_BASE_URL
DASHBOARD_STAGING_MODE=false
VITE_STAGING_MODE=false
3. Rebuild + redeploy:
cd /home/admin365/didi_mono/ai_platform/modules/dashboard/deploy
docker compose --profile dashboard up -d --build dashboard-api
4. Open http://$DASHBOARD_HOST:$DASHBOARD_PORT/v2/
→ Keycloak login redirect → enter creds → SPA loads with your JWT.
EOF