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:
2026-06-14 22:11:07 +03:00
commit 6185b9b85a
126 changed files with 14565 additions and 0 deletions

199
frontend/app/mcp/page.tsx Normal file
View File

@@ -0,0 +1,199 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import AppShell from "@/components/AppShell";
import { EmptyState, LoadError, Modal, PageHeader, SkeletonRows, StatusBadge } from "@/components/ui";
import { useToast } from "@/components/toast";
import { api, fetcher } from "@/lib/api";
import { useAuth, roleAtLeast } from "@/lib/auth";
type Mcp = {
name: string;
status: string;
error: string | null;
source?: string;
tools: { name: string; description: string | null }[];
};
export default function McpPage() {
const router = useRouter();
const { user } = useAuth();
const isAdmin = roleAtLeast(user?.role, "admin");
const toast = useToast();
const {
data: mcps,
error: mcpError,
isLoading,
mutate,
} = useSWR<Mcp[]>("/api/mcp/servers", fetcher, { refreshInterval: 15000 });
const [expanded, setExpanded] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const [developOpen, setDevelopOpen] = useState<string | null>(null);
const [developIssue, setDevelopIssue] = useState("");
const run = async (key: string, label: string, fn: () => Promise<void>) => {
setBusy(key);
try {
await fn();
toast.success(`${label} — status will refresh shortly.`);
mutate();
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : "Action failed");
} finally {
setBusy(null);
}
};
return (
<AppShell>
<PageHeader
title="MCP Servers"
subtitle="Device integrations — reload or upgrade when tools fail or repos change"
action={
isAdmin && (
<button
className="btn-ghost text-sm"
disabled={!!busy}
onClick={() =>
run("all", "Reload all queued", async () => {
await api("/api/mcp/reload-all", { method: "POST" });
})
}
>
{busy === "all" ? "Reloading…" : "Reload all"}
</button>
)
}
/>
{isLoading && !mcps ? (
<SkeletonRows rows={4} />
) : mcpError ? (
<LoadError message="Couldnt load MCP servers." onRetry={() => mutate()} />
) : (mcps || []).length === 0 ? (
<EmptyState
title="No MCP servers reported yet"
description="The worker clones and launches them on startup; check worker logs if this persists."
/>
) : (
<div className="space-y-4">
{(mcps || []).map((m) => (
<div key={m.name} className="card">
<div className="flex w-full flex-wrap items-center justify-between gap-3">
<button
className="flex flex-1 items-center gap-3 text-left"
onClick={() => setExpanded(expanded === m.name ? null : m.name)}
>
<span className="font-medium">{m.name}</span>
<span className="text-xs text-muted">
{m.tools?.length || 0} tools · {m.source || "git"}
</span>
</button>
<div className="flex items-center gap-2">
{isAdmin && (
<>
<button
className="btn-ghost btn-sm"
disabled={!!busy}
onClick={() =>
run(`reload-${m.name}`, "Reload queued", async () => {
await api(`/api/mcp/servers/${m.name}/reload`, { method: "POST" });
})
}
>
{busy === `reload-${m.name}` ? "…" : "Reload"}
</button>
<button
className="btn-ghost btn-sm text-accent2"
disabled={!!busy}
onClick={() =>
run(`upgrade-${m.name}`, "Upgrade queued", async () => {
await api(`/api/mcp/servers/${m.name}/upgrade`, { method: "POST" });
})
}
>
{busy === `upgrade-${m.name}` ? "…" : "Upgrade"}
</button>
<button
className="btn-ghost btn-sm text-accent"
disabled={!!busy}
onClick={() => {
setDevelopIssue("");
setDevelopOpen(m.name);
}}
>
Develop
</button>
</>
)}
<StatusBadge status={m.status} />
</div>
</div>
{m.error && <div className="mt-2 text-sm text-bad">{m.error}</div>}
{expanded === m.name && m.tools?.length > 0 && (
<div className="mt-4 grid grid-cols-1 gap-2 md:grid-cols-2">
{m.tools.map((t) => (
<div key={t.name} className="rounded-lg bg-panel2 px-3 py-2">
<div className="font-mono text-xs text-accent2">{t.name}</div>
{t.description && (
<div className="mt-1 line-clamp-2 text-xs text-muted">{t.description}</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
<Modal
open={!!developOpen}
onClose={() => setDevelopOpen(null)}
title={`Develop ${developOpen || ""}`}
>
<p className="text-sm text-muted">
Describe the missing tool or bug. A monitor task opens automatically so you can watch
analyze patch reload git push in the task timeline. Push to Gitea needs approval
unless auto-push is enabled.
</p>
<textarea
className="input mt-4 min-h-[120px]"
value={developIssue}
onChange={(e) => setDevelopIssue(e.target.value)}
placeholder="e.g. add pfsense_list_firewall_rules_by_interface or fix NameError in get_firewall_log"
/>
<div className="mt-4 flex justify-end gap-2">
<button className="btn-ghost" onClick={() => setDevelopOpen(null)}>
Cancel
</button>
<button
className="btn-primary"
disabled={!developIssue.trim() || !!busy}
onClick={() => {
const server = developOpen;
if (!server) return;
run(`develop-${server}`, "Develop started", async () => {
const res = await api<{ task_id?: number; summary?: string }>(
`/api/mcp/servers/${server}/develop`,
{
method: "POST",
body: JSON.stringify({ issue: developIssue.trim() }),
}
);
setDevelopOpen(null);
if (res.task_id) {
router.push(`/tasks/${res.task_id}`);
}
});
}}
>
{busy === `develop-${developOpen}` ? "Starting…" : "Run & monitor"}
</button>
</div>
</Modal>
</AppShell>
);
}