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:
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user