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

226
frontend/components/ui.tsx Normal file
View File

@@ -0,0 +1,226 @@
"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
export function PageHeader({
title,
subtitle,
action,
backHref,
backLabel,
}: {
title: string;
subtitle?: string;
action?: React.ReactNode;
backHref?: string;
backLabel?: string;
}) {
return (
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0">
{backHref && (
<Link
href={backHref}
className="mb-1 inline-block text-xs text-muted hover:text-accent"
>
{backLabel || "Back"}
</Link>
)}
<h1 className="text-2xl font-semibold">{title}</h1>
{subtitle && <p className="mt-1 break-words text-sm text-muted">{subtitle}</p>}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
);
}
export function Spinner({ className = "" }: { className?: string }) {
return (
<span
className={`inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent ${className}`}
aria-hidden="true"
/>
);
}
export function SkeletonRows({ rows = 3 }: { rows?: number }) {
return (
<div className="space-y-3" aria-hidden="true">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-2xl border border-edge bg-panel2/50" />
))}
</div>
);
}
export function EmptyState({
title,
description,
action,
}: {
title: string;
description?: string;
action?: React.ReactNode;
}) {
return (
<div className="card flex flex-col items-center gap-2 py-10 text-center">
<div className="text-sm font-medium">{title}</div>
{description && <div className="max-w-sm text-sm text-muted">{description}</div>}
{action && <div className="mt-2">{action}</div>}
</div>
);
}
export function LoadError({
message = "Couldnt load data.",
onRetry,
}: {
message?: string;
onRetry?: () => void;
}) {
return (
<div className="card border-bad/40 text-center">
<div className="text-sm text-bad">{message}</div>
{onRetry && (
<button className="btn-ghost btn-sm mt-3" onClick={onRetry}>
Retry
</button>
)}
</div>
);
}
export function FormError({ message }: { message?: string | null }) {
if (!message) return null;
return <div className="text-sm text-bad">{message}</div>;
}
const STATUS_STYLES: Record<string, string> = {
queued: "bg-slate-500/20 text-slate-300",
running: "bg-accent/20 text-accent",
waiting_approval: "bg-warn/20 text-warn",
succeeded: "bg-good/20 text-good",
failed: "bg-bad/20 text-bad",
cancelled: "bg-slate-500/20 text-slate-400",
loaded: "bg-good/20 text-good",
error: "bg-bad/20 text-bad",
starting: "bg-warn/20 text-warn",
cloning: "bg-accent/20 text-accent",
stopped: "bg-slate-500/20 text-slate-400",
};
export function StatusBadge({ status }: { status: string }) {
return (
<span className={`badge ${STATUS_STYLES[status] || "bg-slate-500/20 text-slate-300"}`}>
{status.replace(/_/g, " ")}
</span>
);
}
export function Modal({
open,
onClose,
title,
children,
}: {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
// Focus the dialog only when it opens (not on every parent re-render).
useEffect(() => {
if (!open) return;
ref.current?.focus();
}, [open]);
// Escape-to-close + body scroll lock while open.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onCloseRef.current();
};
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 grid place-items-center bg-black/60 p-4"
onClick={onClose}
>
<div
ref={ref}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-label={title}
className="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-2xl border border-edge bg-panel p-6 shadow-2xl focus:outline-none"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">{title}</h2>
<button
onClick={onClose}
aria-label="Close dialog"
className="rounded-lg px-2 text-muted hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
</button>
</div>
{children}
</div>
</div>
);
}
export function ConfirmDialog({
open,
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
destructive = false,
busy = false,
onConfirm,
onCancel,
}: {
open: boolean;
title: string;
message?: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
busy?: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<Modal open={open} onClose={onCancel} title={title}>
{message && <p className="mb-5 text-sm text-muted">{message}</p>}
<div className="flex justify-end gap-2">
<button className="btn-ghost" onClick={onCancel} disabled={busy}>
{cancelLabel}
</button>
<button
className={destructive ? "btn-danger" : "btn-primary"}
onClick={onConfirm}
disabled={busy}
>
{busy ? "Working…" : confirmLabel}
</button>
</div>
</Modal>
);
}