Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
119
frontend/components/AppShell.tsx
Normal file
119
frontend/components/AppShell.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth, roleAtLeast } from "@/lib/auth";
|
||||
|
||||
const NAV = [
|
||||
{ href: "/", label: "Dashboard", icon: "▦", min: "readonly" as const },
|
||||
{ href: "/tasks", label: "Tasks", icon: "✦", min: "readonly" as const },
|
||||
{ href: "/inventory", label: "Vessels", icon: "▤", min: "readonly" as const },
|
||||
{ href: "/mcp", label: "MCP Servers", icon: "⚇", min: "readonly" as const },
|
||||
{ href: "/rules", label: "Rules", icon: "☰", min: "support" as const },
|
||||
{ href: "/models", label: "Models", icon: "◈", min: "admin" as const },
|
||||
{ href: "/users", label: "Users", icon: "♟", min: "admin" as const },
|
||||
];
|
||||
|
||||
export default function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) router.replace("/login");
|
||||
}, [loading, user, router]);
|
||||
|
||||
// Close the mobile drawer on navigation.
|
||||
useEffect(() => {
|
||||
setNavOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center gap-2 text-muted">
|
||||
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
Loading…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sidebar = (
|
||||
<>
|
||||
<div className="mb-8 flex items-center gap-2 px-2">
|
||||
<div className="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-accent to-accent2 font-bold text-white">
|
||||
A
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">Agentic OS</div>
|
||||
<div className="text-[11px] text-muted">troubleshooting</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col gap-1">
|
||||
{NAV.filter((n) => roleAtLeast(user.role, n.min)).map((n) => {
|
||||
const active = pathname === n.href || (n.href !== "/" && pathname.startsWith(n.href));
|
||||
return (
|
||||
<Link
|
||||
key={n.href}
|
||||
href={n.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`flex items-center gap-3 rounded-xl px-3 py-2 text-sm transition ${
|
||||
active ? "bg-accent/15 text-white" : "text-slate-300 hover:bg-panel2"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base opacity-80" aria-hidden="true">
|
||||
{n.icon}
|
||||
</span>
|
||||
{n.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="mt-4 border-t border-edge pt-4">
|
||||
<div className="px-2 text-sm">{user.full_name || user.email}</div>
|
||||
<div className="mb-3 px-2 text-[11px] uppercase tracking-wide text-muted">{user.role}</div>
|
||||
<button onClick={logout} className="btn-ghost w-full">
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Mobile top bar */}
|
||||
<header className="fixed inset-x-0 top-0 z-30 flex items-center gap-3 border-b border-edge bg-panel/90 px-4 py-3 backdrop-blur md:hidden">
|
||||
<button
|
||||
aria-label="Open menu"
|
||||
aria-expanded={navOpen}
|
||||
onClick={() => setNavOpen(true)}
|
||||
className="btn-ghost btn-sm"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<span className="text-sm font-semibold">Agentic OS</span>
|
||||
</header>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
{navOpen && (
|
||||
<div className="fixed inset-0 z-40 md:hidden" onClick={() => setNavOpen(false)}>
|
||||
<div className="absolute inset-0 bg-black/60" />
|
||||
<aside
|
||||
className="absolute left-0 top-0 flex h-full w-64 flex-col border-r border-edge bg-panel p-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{sidebar}
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden w-60 flex-col border-r border-edge bg-panel/60 p-4 md:flex">
|
||||
{sidebar}
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4 pt-20 md:p-8 md:pt-8">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user