17 KiB
Dashboard Module - INDEX
AI platform monitoring/admin dashboard. Browse archived claims, view ingest history, audit trail, runtime config overrides, costs per provider/tier, and live provider quotas. Single FastAPI service serving:
- React 19 + MUI 7 SPA at
/admin-ai/(default modern UI, added in Phase C 2026-05-02) - Jinja2 templates at
/,/history,/cost,/providers,/archive,/audit,/config(legacy, kept side-by-side until full deprecation) - JSON API at
/api/*and/admin-ai/api/*(dual-mounted) consumed byweb-api+ the SPA itself
Stack
- Python 3.10+ (FastAPI 0.115+, Uvicorn)
- React 19 + MUI 7 + Vite 7 + react-router 7 + TanStack Query + Recharts (built into
/app/web_dist/, served as static via SPAStaticFiles with index-fallback) - Jinja2 templates legacy (HTMX + Alpine.js + Tailwind CDN — kept until React reaches 100% parity)
- SQLAlchemy 2.0 async + asyncpg, Alembic
- pydantic-settings (env prefix
DASHBOARD_) - httpx for live provider quota fetching, brain proxy
- python-jose[cryptography] for Keycloak JWT validation
Coordinates
- SPA URL:
https://10.11.10.12:8443/admin-ai/(via frontend nginx) orhttp://10.11.10.12:51300/admin-ai/(direct) - Public URL:
https://didi365.eu/admin-ai/(via Cloudflare tunnel + frontend nginx) - Container:
didiAI-dashboard(alongsidedidiAI-dashboard-dbon:15432) - Compose:
deploy/docker-compose.yml, profiledashboard - Network:
didi-network(unified single network for all DIDI + AI platform stacks since 2026-05-04)
Auth (current state — 2026-05-04)
Hybrid auth in dependencies.py:verify_bearer_token:
STAGING_MODE=true→ all auth bypassed (default for dev)- JWT (3 dot-separated parts) → validated via
keycloak_auth.py(JWKS cache 10min, signature, issuer, exp, role check) - DB-backed bearer tokens (legacy, from CLI
dashboard create-user) - Static
api_tokensenv var (legacy fallback) - Otherwise → 401
Keycloak settings (config.py:DashboardSettings):
keycloak_url,keycloak_realm(defaultdidi-clients),keycloak_client_id(defaultai-platform-dashboard)keycloak_required_role(defaultadmin— same role as DIDI admin-dashboard for unified access)- Manual setup: create client + assign role via Keycloak admin or
deploy/setup-keycloak.sh
Cutover from staging to prod:
- Set
DASHBOARD_KEYCLOAK_URL=https://sso.clossers.com,DASHBOARD_STAGING_MODE=false,VITE_STAGING_MODE=false - Rebuild image with build args (Dockerfile bakes Keycloak config into JS bundle at build time)
Plan/runbook: AI_PLATFORM_RESKIN_PLAN.md (Phase C.8) + /home/admin365/didi_mono/UNIFIED_KEYCLOAK_CUTOVER.md
Ce face
Sections served as HTML pages (pages.py) + JSON-mirror endpoints in routes/:
- Overview (
/) - KPIs (totals, error rate, avg duration, total cost) + provider grid (live quotas) - Providers (
/providers) - detailed provider cards + raw table (live SerpAPI/Tavily/Brave/LinkUp/Exa/OpenRouter/internal stats) - History (
/history,/history/{request_id}) - request log with filters (tier, provider, endpoint, hours) + drill-down with full stages + raw_request/response - Cost (
/cost) - 24h/7d/30d spend, projected monthly, by-provider, by-tier, top 10 expensive requests, quota-vs-budget bars - Archive (
/archive,/archive/{claim_id}) - browse promoted claims + linked articles (permanent storage seeded viaPOST /archive/promote/{request_id}) - Audit (
/audit) - audit log entries (config.set, config.reset, config.delete, archive.promote) - Config (
/config) - runtime overrides table grouped by category (providers/routing/llm/tiers); HTMX in-place edit + reset
API endpoints
routes/health.py
GET /health- liveness + DB pingGET /ready- app.state populated check
routes/ingest.py (no auth, service-to-service)
POST /api/ingest/event- receives request events fromweb-apimiddleware; trims fields, computes cost viapricing.estimate_costif missing, inserts intorequest_history
routes/history.py (no auth, read-only)
GET /api/history- filtered list (tier, provider, endpoint, hours, limit, offset)GET /api/history/{request_id}- full record (includes stages + raw_request/response)
routes/stats.py (no auth, read-only)
GET /api/stats/providers?force=- live provider stats (cachedprovider_stats_cache_seconds, default 30s)GET /api/stats/summary?hours=- aggregated counters (totals, by_tier, by_provider, by_endpoint, error_rate, avg_duration, total_cost)GET /api/stats/timeline?hours=- hourly buckets (date_trunc('hour', ...)) for charts
routes/archive.py
GET /api/archive/claims- paginated claims list with optionalqilike searchGET /api/archive/claims/{claim_id}- single claim + linked articlesPOST /api/archive/promote/{request_id}(auth) - promote arequest_historyrow intoclaims_archive+articles_archive+claim_articles; only/v1/gatherrows can be promoted
routes/config.py
GET /api/config- all keys with overrides merged on top ofKNOWN_KEYSdefaults (consumed byweb-api, no auth)GET /api/config/{key}- single keyPUT /api/config/{key}(auth) - set override, validates against schema (bool/int/enum/csv/string), writes audit logDELETE /api/config/{key}(auth) - revert to default, writes audit log
KNOWN_KEYS in routes/config.py enumerates the runtime keys consumed by web-api: web.providers.{serpapi,tavily,brave,linkup,exa}.enabled, web.premium.strategy, web.premium.priority_order, web.openrouter.model, web.tier.{free,premium}.max_search_results.
routes/pages.py (Jinja HTML)
GET /- overviewGET /history,GET /history/{request_id}GET /providersGET /costGET /archive,GET /archive/{claim_id}GET /auditGET /config+ HTMX form handlersPOST /config/{key}andPOST /config/{key}/reset(return partial fragments)
Structura fisiere
src/dashboard/
__init__.py
cli.py # admin CLI: create-user, list-users, delete-user (python -m dashboard.cli)
auth.py # SHA-256 + hmac.compare_digest, User CRUD
config.py # DashboardSettings (pydantic-settings, DASHBOARD_ prefix), SettingsCache
logging.py # get_logger helper (JSON or text via DASHBOARD_LOG_JSON)
pricing.py # estimate_cost(endpoint, tier, provider) - per-provider USD
retention.py # 30-day rolling cleanup of request_history
api/
app.py # FastAPI factory, lifespan (init engine + provider registry), router wiring
dependencies.py # get_session, get_registry, verify_bearer_token, get_username
routes/
archive.py # claims archive CRUD + promote
config.py # KNOWN_KEYS + runtime override CRUD
health.py # /health, /ready
history.py # /api/history (read-only)
ingest.py # POST /api/ingest/event (service-to-service)
pages.py # all Jinja HTML pages + HTMX handlers
stats.py # /api/stats/{providers,summary,timeline}
db/
models.py # Base, RequestHistory, ProviderStatsHourly, ConfigOverride, User, AuditLog,
# ClaimsArchive, ArticlesArchive, ClaimArticle
session.py # init_engine, get_session, get_session_factory, close_engine
providers/
base.py # ProviderStats dataclass + ProviderClient ABC
registry.py # ProviderRegistry (cached fan-out across providers)
serpapi.py # SerpAPI quota + plan price
tavily.py # Tavily quota
brave.py # Brave Search quota
linkup.py # LinkUp quota
openrouter.py # OpenRouter spend
internal.py # Internal services (web-api, SearXNG, vLLM, llama.cpp) live health
templates/ # Jinja
base.html
overview.html
archive.html, archive_detail.html
audit.html
config.html
cost.html
history.html, history_detail.html
providers.html
partials/
config_row.html # HTMX swap target after edit/reset
provider_card.html # reusable card on overview + providers pages
quota_bar.html # quota progress bar
static/
css/, js/ # tailwind via CDN, htmx + alpine inline
deploy/
Dockerfile
docker-compose.yml # didiAI-dashboard + didiAI-dashboard-db (postgres:16-alpine)
deploy.sh # convenience wrapper around `docker compose --profile dashboard`
tests/
...
pyproject.toml # hatchling build, ruff inherited from ../../ruff.toml
uv.lock
Database
didiAI-dashboard-db (postgres:16-alpine, host :15432 -> container :5432). Tables:
request_history- 30-day rolling per-request log (BigInt id, request_id unique, JSON stages/raw_request/raw_response, cost_usd Numeric(12,6), indexes on created_at, tier, provider, endpoint)provider_stats_hourly- rollups by (provider, hour)config_overrides- runtime config k/v overrides (key PK, JSON value, updated_by)users- dashboard users (username unique, token_hash SHA-256, role, last_login)audit_log- mutation history (timestamp, username, action, target, old_value/new_value JSON)claims_archive- permanent claims storage (claim_hash unique, verdict, confidence Numeric(5,4), summary, entities JSON, tags JSON)articles_archive- permanent article full-text (url unique, url_hash unique, full_text, publisher, credibility_score)claim_articles- M2M claims <-> articles (relevance_score, snippet)
Connection string format: postgresql+asyncpg://USER:PASS@didiAI-dashboard-db:5432/DB injected via DASHBOARD_DATABASE_URL.
Pages
base.html- layout shell (Tailwind CDN, sidebar nav, HTMX + Alpine includes)overview.html- KPI tiles + provider grid (usespartials/provider_card.html+partials/quota_bar.html)providers.html- full provider cards + raw quota tablehistory.html/history_detail.html- filterable list + drill-down with stages JSON pretty-printcost.html- cost cards + by-provider/by-tier breakdown + budget barsarchive.html/archive_detail.html- claim search + linked articlesaudit.html- chronological mutation logconfig.html- runtime overrides grouped by category, HTMX inline edit ->partials/config_row.html
Authentication (current state)
- Bearer token in
Authorization: Bearer <token>header dependencies.verify_bearer_tokenreads header, hashes, scansusers, returnsUseror 401dependencies.get_username(principal)extracts username for audit log- Token issuance via CLI inside container:
docker exec -it didiAI-dashboard python -m dashboard.cli create-user <name> [--email] [--role admin|viewer] - All read endpoints +
POST /api/ingest/eventare auth-free (VPN-internal trust)
Reskin plan (FUTURE - NOT done yet)
- React 19 + MUI 7 + Keycloak SSO DONE 2026-05-02 (Phase C in
agent-v3/IMPLEMENTATION_PLAN_HIL_BRAIN.md, detail inAI_PLATFORM_RESKIN_PLAN.md) - Becomes admin-only (Keycloak realm role)
- Bearer-token table retired; existing
rolecolumn may persist for historical audit-log mapping - Brain admin UI (atom browse, force-gold, brain stats) added as a new section in this dashboard during Phase C
- Jinja templates + HTMX endpoints in
pages.pywill be replaced by JSON endpoints; existingroutes/*.pyJSON API stays as the contract
Deployment
cd /home/admin365/didi_mono/ai_platform/modules/dashboard/deploy
cp ../.env.example .env # set DASHBOARD_DB_USER/PASSWORD/NAME + provider keys
./deploy.sh up # docker compose --profile dashboard up -d --build
- Healthcheck:
python -c urllib.request.urlopen('http://localhost:51300/health')every 30s - Restart policy:
unless-stopped - Settings prefix:
DASHBOARD_*(seeconfig.pyfor full list)
Ce NU face
No SSO yetKeycloak SSO wired (DONE 2026-05-02). Bearer tokens kept as legacy fallback.- No multi-tenant - single shared
userstable, no per-tenant scoping - No public access - binds to internal
didi-networknetwork, not exposed via Kong/edge No React frontend yetReact 19 SPA at/admin-ai/(DONE 2026-05-02). Jinja kept side-by-side until parity.- No real-time push - HTMX polling, no WebSockets/SSE
Related docs
- AI platform CLAUDE.md:
/home/admin365/didi_mono/ai_platform/CLAUDE.md - Module README:
/home/admin365/didi_mono/ai_platform/modules/dashboard/README.md - Reskin + Brain admin plan (Phase C):
/home/admin365/didi_mono/backend/services/orchestration-layer/agent-v3/IMPLEMENTATION_PLAN_HIL_BRAIN.md - Web-api ingest middleware (the producer for
POST /api/ingest/event):/home/admin365/didi_mono/ai_platform/modules/web-api/
Recent Changes (2026-05-05)
- 3 pagini noi:
/admin-ai/operations/live(Live Status) — KPI 1h cu refresh 5s, Recent Activity feed (latest 20 cu chip warning daca >1h vechi), Provider Health cu cache age, throughput sparkline 6h/admin-ai/system/settings(System Settings) — health endpoint, identity card cu roluri, Configuration Surface (98 chei + breakdown per modul + count overrides), Quick Access dynamic links/admin-ai/system/schema(Schema Overrides) — CRUD UI pentruconfig_schema_override(Register/Edit/Delete via dialog cu validare type/min/max/options)
- Pagini eliminate:
/admin-ai/system/users(admin backend gestioneaza userii Keycloak) - Overview: card Phase C status chips -> inlocuit cu Quick Links chips clickable (Live Status, History, Cost, Providers, Brain Atoms, Audit Log, Schema Overrides)
- Modules ModulePage (toate 8): tab "Live State" functional cu real backend
/api/proxy/{moduleId}/health(status badge, latency, payload/v1/info, refresh 10s, env override DASHBOARD__HEALTH_URL); tab "Actions" reformat ca "Pending Restarts" cu lista overrides curestart_required=true+ comanda SSH copy-able - Backend endpoint nou:
routes/proxy.pycuGET /api/proxy/{module_id}/health— proxy catre modul real, hardcoded URLs pe 10.11.10.17 cu fallback laDASHBOARD_<MODULE>_HEALTH_URLenv override - Cost endpoint imbunatatit:
GET /api/stats/costreturneazaprojection_basis(blend 30d=40%/7d=60%, sau 7d_avg, sau 24h_only),projection_confidence(stable/moderate/rough),trend(increasing/decreasing/stable),trend_pct. UI Cost page afiseaza confidence chip + trend arrow (↗↘→). - Providers endpoint imbunatatit:
GET /api/stats/providersadaugalast_refresh(ISO),age_seconds,cache_ttl_seconds. UI Providers + Live Status afiseaza "refreshed Xs ago" chip (warning peste 120s). - ProviderRegistry: tracks
_cache_wall: datetimeseparat de_cache_time(monotonic), expune propertieslast_refresh_iso+age_seconds. - Frontend type fix:
Live.tsx+Providers.tsxfoloseau campuri inexistente (status,quota_used_pct,plan) — corectate lahealthy: bool,quota_percent_used,display_name,plan_name(matching backend response). - Schema migration: 98 chei seed la primul startup in
config_schema_override, vizibile in Schema Overrides UI cu actiuni Edit/Delete.
Brain admin pages — Phase D2 (2026-05-05)
3 rute noi sub /admin-ai/brain/ + 1 tab nou pe system/audit. Toate consumă brain prin proxy-ul /api/brain/*.
pages/brain/Facts.tsx
Browser pe brain_fact_status cu filter (entity ILIKE, predicate exact, current_truth, locked_only, topic), DataGrid paginat. Click pe rând → drawer dreapta cu Triple summary (chip volatility + lock + topics), Truth timeline Stepper cu toate brain_fact_version rows, Moderator override form (moderator_user_id + set_truth + confidence + evidence URLs + lock/unlock + notes → PATCH /v1/fact_status/{id}).
pages/brain/Invalidate.tsx
Form-driven mass invalidation cu UX 2-step: Build filter (topic_codes, entity_canonicals, claim_pattern, since, invalidate_gold cu warning) → Preview (dry run) → Confirm invalidate (button enabled doar după Preview). Side panel: ultimele 10 invalidări (auto-refresh 30s).
pages/system/AuditLog.tsx — refactor cu tabs
Wrapper cu Tabs: Dashboard tab → AuditLogDashboard.tsx (existing code extras intact); Brain tab → AuditLogBrain.tsx nou, citește /api/brain/v1/cache/audit_log cu action presets (judge_, fact_truth_, invalidate, promote_gold), action chip color-coded, payload tooltip JSON pretty.
Backend (brain_proxy.py)
Whitelist extins cu: /v1/fact_status/list, /v1/fact_status/, /v1/cache/audit_log, /v1/cache/invalidate, /v1/canonicalize. GET pass-through; POST/PATCH cer bearer.
types/brain.ts extins
Volatility, FactStatusItem(+List+VersionItem+Versions+Patch)Response, AuditLogItem(+Response), CacheInvalidate(Request|Response).
Routes + sidebar
App.tsx: brain/facts, brain/invalidate. AppShell.tsx Brain Admin section: Fact Status (FactCheckIcon), Invalidate (DeleteSweepIcon).