feat(simulate): MLflow tracking, Airflow DAG integration, health checks for mlflow/airflow

- sim_runs schema: add judge_mode, n_policies, airflow_dag_run_id, mlflow_run_id columns
- admin health endpoint: add mlflow + airflow checks (Basic auth for Airflow API)
- admin nav: add Simulations page link; rename section label
- runner.py: optional MLflow experiment tracking; multi-policy support
- sim_dag.py: Airflow DAG for offline sim pipeline
- admin simulate page + API client methods for sim runs
- shared-types tsconfig: exclude test files from build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 12:08:36 +00:00
parent e96ceb7ee1
commit bad1bb2cba
12 changed files with 818 additions and 107 deletions

View File

@@ -2,14 +2,16 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
const mlflowUrl = process.env.NEXT_PUBLIC_MLFLOW_URL ?? '/mlflow';
const mlflowUrl = process.env.NEXT_PUBLIC_MLFLOW_URL ?? '/mlflow';
const airflowUrl = process.env.NEXT_PUBLIC_AIRFLOW_URL ?? '/airflow';
type NavItem = {
href: string;
label: string;
external?: boolean;
svcName?: string; // key in the health services map
};
type NavSection = {
@@ -24,40 +26,60 @@ const NAV: NavSection[] = [
{
label: 'Signals',
items: [
{ href: '/users', label: 'Users' },
{ href: '/events', label: 'Events' },
{ href: '/features', label: 'Features' },
{ href: '/users', label: 'Users' },
{ href: '/events', label: 'Events' },
{ href: '/features', label: 'Features' },
{ href: '/data-quality', label: 'Data quality' },
],
},
{
label: 'Recommender status',
label: 'Recommender',
items: [
{ href: '/tips', label: 'Tips' },
{ href: '/tips', label: 'Tips' },
{ href: '/reward-analytics', label: 'Rewards' },
{ href: '/simulate', label: 'Simulations' },
],
},
{
label: 'Operations',
items: [
{ href: '/health', label: 'Health' },
{ href: '/ops', label: 'Ops' },
{ href: '/sql', label: 'SQL runner' },
{ href: '/audit', label: 'Audit log' },
{ href: '/ops', label: 'Ops' },
{ href: '/sql', label: 'SQL runner' },
{ href: '/audit', label: 'Audit log' },
],
},
{
label: 'Resources',
items: [
{ href: '/docs', label: 'Docs' },
{ href: mlflowUrl, label: 'MLflow ↗', external: true },
{ href: airflowUrl, label: 'Airflow ↗', external: true },
{ href: '/docs', label: 'Docs' },
{ href: mlflowUrl, label: 'MLflow ↗', external: true, svcName: 'mlflow' },
{ href: airflowUrl, label: 'Airflow ↗', external: true, svcName: 'airflow' },
],
},
];
const STATUS_DOT: Record<string, string> = {
ok: 'bg-green-500',
degraded: 'bg-yellow-400',
down: 'bg-red-500',
};
export function AdminShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const [svcStatus, setSvcStatus] = useState<Record<string, string>>({});
useEffect(() => {
fetch('/api/admin/health', { credentials: 'include' })
.then((r) => r.json())
.then((data: { services?: { name: string; status: string }[] }) => {
const map: Record<string, string> = {};
for (const s of data.services ?? []) map[s.name] = s.status;
setSvcStatus(map);
})
.catch(() => {});
}, []);
return (
<div className="flex min-h-screen">
{/* Sidebar */}
@@ -83,13 +105,19 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
const active =
!item.external &&
(item.href === '/' ? pathname === '/' : pathname.startsWith(item.href));
const className = `flex items-center px-3 py-2 rounded text-sm transition-colors ${
const className = `flex items-center gap-2 px-3 py-2 rounded text-sm transition-colors ${
active
? 'bg-gray-800 text-white font-medium'
: item.external
? 'text-gray-500 hover:text-white hover:bg-gray-900'
: 'text-gray-400 hover:text-white hover:bg-gray-900'
}`;
const dot = item.svcName
? svcStatus[item.svcName]
? <span className={`inline-block w-1.5 h-1.5 rounded-full flex-shrink-0 ${STATUS_DOT[svcStatus[item.svcName]] ?? STATUS_DOT.down}`} />
: <span className="inline-block w-1.5 h-1.5 rounded-full flex-shrink-0 bg-gray-700" />
: null;
return item.external ? (
<a
key={item.href}
@@ -98,6 +126,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
rel="noreferrer"
className={className}
>
{dot}
{item.label}
</a>
) : (