Files
Agentic-OS/frontend/app/page.tsx
nearxos 6185b9b85a 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>
2026-06-14 22:11:07 +03:00

224 lines
10 KiB
TypeScript

"use client";
import useSWR from "swr";
import Link from "next/link";
import AppShell from "@/components/AppShell";
import { PageHeader, StatusBadge } from "@/components/ui";
import { fetcher } from "@/lib/api";
import { isActiveStatus, TaskOverview } from "@/lib/task-types";
type Balance = {
provider: string;
remaining: number | null;
total_credits: number | null;
total_usage: number | null;
currency: string;
error: string | null;
};
type Mcp = { name: string; status: string; tools: { name: string }[] };
export default function Dashboard() {
const { data: balances } = useSWR<Balance[]>("/api/balance", fetcher, { refreshInterval: 60000 });
const { data: mcps } = useSWR<Mcp[]>("/api/mcp/servers", fetcher, { refreshInterval: 30000 });
const { data: tasks } = useSWR<TaskOverview[]>("/api/tasks/overview", fetcher, {
refreshInterval: (data) =>
(data || []).some((t) => isActiveStatus(t.status)) ? 8000 : 30000,
});
const taskList = tasks || [];
const active = taskList.filter((t) => isActiveStatus(t.status));
const approvals = taskList.filter((t) => t.status === "waiting_approval");
const failed = taskList.filter((t) => t.status === "failed");
const artifactWarnings = taskList.filter((t) => t.obsidian_push_error);
const lowBalance = (balances || []).filter((b) => b.error || (b.remaining != null && b.remaining < 1));
const attention = [...approvals, ...failed, ...artifactWarnings].filter(
(task, index, arr) => arr.findIndex((t) => t.id === task.id) === index
);
const recentReports = taskList.filter((t) => t.summary || t.key_finding).slice(0, 5);
const mcpReady = (mcps || []).filter((m) => m.status === "loaded").length;
const mcpTotal = (mcps || []).length;
return (
<AppShell>
<PageHeader
title="Operations Dashboard"
subtitle="Active troubleshooting, attention queue, recent findings, and platform readiness"
action={
<Link href="/tasks" className="btn-primary">
New / view tasks
</Link>
}
/>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<MetricCard label="Active" value={active.length} tone={active.length ? "accent" : "muted"} />
<MetricCard label="Needs approval" value={approvals.length} tone={approvals.length ? "warn" : "muted"} />
<MetricCard label="Failed" value={failed.length} tone={failed.length ? "bad" : "muted"} />
<MetricCard label="MCP ready" value={`${mcpReady}/${mcpTotal || "—"}`} tone={mcpReady === mcpTotal ? "good" : "warn"} />
</div>
<div className="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.3fr_0.9fr]">
<div className="card">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold">Active Tasks</h2>
<span className="text-xs text-muted">queued, running, approvals</span>
</div>
<div className="space-y-2">
{active.map((t) => (
<Link key={t.id} href={`/tasks/${t.id}`} className="block rounded-xl bg-panel2 p-3 hover:ring-1 hover:ring-accent/40">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium">{t.title}</div>
<div className="mt-1 text-xs text-muted">
{t.vessel_name || "No vessel"} · {t.devices_checked?.join(", ") || "scope pending"}
</div>
</div>
<StatusBadge status={t.status} />
</div>
<p className="mt-2 line-clamp-2 text-sm text-muted">{t.summary || t.issue}</p>
</Link>
))}
{active.length === 0 && (
<div className="rounded-xl bg-panel2 p-4 text-sm text-muted">No active tasks right now.</div>
)}
</div>
</div>
<div className="card">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold">Attention Needed</h2>
<Link href="/tasks" className="text-xs text-accent hover:underline">
View all
</Link>
</div>
<div className="space-y-2">
{attention.map((t) => (
<Link key={t.id} href={`/tasks/${t.id}`} className="block rounded-xl bg-panel2 p-3 hover:ring-1 hover:ring-warn/40">
<div className="flex items-center justify-between gap-3">
<span className="truncate text-sm font-medium">{t.title}</span>
<StatusBadge status={t.status} />
</div>
<p className="mt-1 text-xs text-muted">
{t.obsidian_push_error
? "Obsidian push failed"
: t.status === "waiting_approval"
? "Approval required before changes continue"
: t.status === "failed"
? t.key_finding || "Task failed"
: t.issue}
</p>
</Link>
))}
{lowBalance.map((b) => (
<div key={b.provider} className="rounded-xl bg-panel2 p-3">
<div className="text-sm font-medium capitalize text-warn">{b.provider} budget</div>
<div className="mt-1 text-xs text-muted">{b.error || `Low balance: $${b.remaining?.toFixed(2)}`}</div>
</div>
))}
{attention.length === 0 && lowBalance.length === 0 && (
<div className="rounded-xl bg-panel2 p-4 text-sm text-muted">Nothing needs attention.</div>
)}
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.2fr_0.8fr]">
<div className="card">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold">Recent Findings</h2>
<Link href="/tasks" className="text-xs text-accent hover:underline">
All reports
</Link>
</div>
<div className="space-y-3">
{recentReports.map((t) => (
<Link key={t.id} href={`/tasks/${t.id}`} className="block rounded-xl bg-panel2 p-3 hover:ring-1 hover:ring-accent/40">
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{t.title}</span>
{t.vessel_name && <span className="badge bg-panel text-xs text-accent">{t.vessel_name}</span>}
<StatusBadge status={t.status} />
</div>
<p className="line-clamp-2 text-sm text-muted">{t.key_finding || t.summary}</p>
<div className="mt-2 flex flex-wrap gap-2 text-[11px] text-muted">
{t.devices_checked?.length > 0 && <span>checked: {t.devices_checked.join(", ")}</span>}
{t.memory_id && <span>memory saved</span>}
{t.obsidian_path && <span>obsidian saved</span>}
</div>
</Link>
))}
{recentReports.length === 0 && <div className="text-sm text-muted">No completed findings yet.</div>}
</div>
</div>
<div className="space-y-6">
<div className="card">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold">Budgets</h2>
<span className="text-xs text-muted">API balances</span>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-1">
{(balances || []).map((b) => (
<div key={b.provider} className="rounded-xl bg-panel2 p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium capitalize">{b.provider}</span>
<span className="text-xs text-muted">{b.currency}</span>
</div>
{b.error ? (
<div className="mt-2 text-sm text-bad">{b.error}</div>
) : (
<div className="mt-2 text-2xl font-semibold">
{b.remaining != null ? `$${b.remaining.toFixed(2)}` : "—"}
</div>
)}
{b.total_usage != null && <div className="mt-1 text-xs text-muted">used ${b.total_usage.toFixed(2)}</div>}
</div>
))}
{(!balances || balances.length === 0) && <div className="text-sm text-muted">No balance data yet.</div>}
</div>
</div>
<div className="card">
<div className="mb-4 flex items-center justify-between">
<h2 className="font-semibold">Platform Readiness</h2>
<Link href="/mcp" className="text-xs text-accent hover:underline">
MCP details
</Link>
</div>
<div className="space-y-2">
{(mcps || []).map((m) => (
<div key={m.name} className="flex items-center justify-between rounded-xl bg-panel2 px-3 py-2">
<div>
<div className="text-sm font-medium">{m.name}</div>
<div className="text-xs text-muted">{m.tools?.length || 0} tools</div>
</div>
<StatusBadge status={m.status} />
</div>
))}
{(!mcps || mcps.length === 0) && <div className="text-sm text-muted">Worker is starting MCP servers.</div>}
</div>
</div>
</div>
</div>
</AppShell>
);
}
function MetricCard({ label, value, tone }: { label: string; value: number | string; tone: "accent" | "warn" | "bad" | "good" | "muted" }) {
const toneClass =
tone === "accent"
? "text-accent"
: tone === "warn"
? "text-warn"
: tone === "bad"
? "text-bad"
: tone === "good"
? "text-good"
: "text-muted";
return (
<div className="card">
<div className="text-xs uppercase tracking-wide text-muted">{label}</div>
<div className={`mt-2 text-3xl font-semibold ${toneClass}`}>{value}</div>
</div>
);
}