Files
oO/apps/admin/src/components/AdminShell.tsx
alvis 85367aeaa0 feat: MLOps external services, AI stack planning, admin MLOps hub
Infrastructure:
- Add `mlops` compose profile: MLflow (basic-auth, /mlflow path) + Airflow (LocalExecutor, /airflow path) + airflow-db
- infra/mlflow/basic_auth.ini for MLflow auth config
- Caddy routes /mlflow* and /airflow* inside existing o.alogins.net block (see agap_git)
- Dockerfile.admin: NEXT_PUBLIC_MLFLOW_URL / NEXT_PUBLIC_AIRFLOW_URL build args (default /mlflow, /airflow)

Admin panel:
- /admin/models: replace MLflow iframe with external link cards
- /admin/experiments: replace LinUCB stats with MLOps hub (links to MLflow experiments/models + Airflow DAGs/datasets)
- AdminShell: external nav links for MLflow ↗ and Airflow ↗ under MLOps section

Docs & planning:
- README: new AI stack section (Ollama/LiteLLM/OpenWebUI three-tier, tip generation pipeline, model aliases)
- README: Phase 2 expanded with AI infra issues (#86-#93) and granular pipeline breakdown
- README: Phase 4 expanded with LLM MLOps items (#94-#97)
- CLAUDE.md: AI stack section, updated current phase (M1 shipped / M2 in progress), compose profiles, updated What NOT to do
- docs/architecture/overview.md: AI stack section, updated decision flow diagram for Phase 2 LLM pipeline
- ADR-0006: updated to reflect external services (path-based, not embedded)
- Gitea issues #86-#97 created (M2: AI infra + pipeline; M4: LLM MLOps)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 08:20:44 +00:00

86 lines
3.0 KiB
TypeScript

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
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?: false }
| { href: string; label: string; external: true };
const NAV: NavItem[] = [
{ href: '/', label: 'Overview' },
{ href: '/users', label: 'Users' },
{ href: '/events', label: 'Events' },
{ href: '/features', label: 'Features' },
{ href: '/tips', label: 'Rec log' },
{ href: '/reward-analytics', label: 'Rewards' },
{ href: '/experiments', label: 'MLOps' },
{ href: '/simulations', label: 'Simulations' },
{ href: '/models', label: 'Models' },
{ href: '/data-quality', label: 'Data quality' },
{ href: '/ops', label: 'Ops' },
{ href: '/sql', label: 'SQL runner' },
{ href: '/health', label: 'Health' },
{ href: '/audit', label: 'Audit log' },
{ href: '/docs', label: 'Docs' },
];
const NAV_EXTERNAL: NavItem[] = [
{ href: mlflowUrl, label: 'MLflow ↗', external: true },
{ href: airflowUrl, label: 'Airflow ↗', external: true },
];
export function AdminShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="w-52 flex-shrink-0 border-r border-gray-800 bg-gray-950 flex flex-col">
<div className="px-5 py-4 border-b border-gray-800">
<span className="text-lg font-bold tracking-tight">oO</span>
<span className="ml-2 text-xs text-gray-500 font-medium uppercase tracking-widest">
Admin
</span>
</div>
<nav className="flex-1 px-2 py-3 space-y-0.5 overflow-y-auto">
{NAV.map(({ href, label }) => {
const active = href === '/' ? pathname === '/' : pathname.startsWith(href);
return (
<Link
key={href}
href={href}
className={`flex items-center px-3 py-2 rounded text-sm transition-colors ${
active
? 'bg-gray-800 text-white font-medium'
: 'text-gray-400 hover:text-white hover:bg-gray-900'
}`}
>
{label}
</Link>
);
})}
<div className="pt-3 pb-1 px-3">
<span className="text-xs text-gray-600 uppercase tracking-wider font-medium">MLOps</span>
</div>
{NAV_EXTERNAL.map(({ href, label }) => (
<a
key={href}
href={href}
target="_blank"
rel="noreferrer"
className="flex items-center px-3 py-2 rounded text-sm text-gray-500 hover:text-white hover:bg-gray-900 transition-colors"
>
{label}
</a>
))}
</nav>
</aside>
{/* Main content */}
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
);
}