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>
This commit is contained in:
2026-04-17 08:20:44 +00:00
parent faf44c18fc
commit 85367aeaa0
25 changed files with 695 additions and 222 deletions

View File

@@ -3,14 +3,21 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const NAV = [
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: 'Experiments' },
{ href: '/experiments', label: 'MLOps' },
{ href: '/simulations', label: 'Simulations' },
{ href: '/models', label: 'Models' },
{ href: '/data-quality', label: 'Data quality' },
@@ -21,6 +28,11 @@ const NAV = [
{ 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 (
@@ -33,7 +45,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
Admin
</span>
</div>
<nav className="flex-1 px-2 py-3 space-y-0.5">
<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 (
@@ -50,6 +62,20 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
</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 */}