"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("/api/balance", fetcher, { refreshInterval: 60000 }); const { data: mcps } = useSWR("/api/mcp/servers", fetcher, { refreshInterval: 30000 }); const { data: tasks } = useSWR("/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 ( New / view tasks } />

Active Tasks

queued, running, approvals
{active.map((t) => (
{t.title}
{t.vessel_name || "No vessel"} · {t.devices_checked?.join(", ") || "scope pending"}

{t.summary || t.issue}

))} {active.length === 0 && (
No active tasks right now.
)}

Attention Needed

View all
{attention.map((t) => (
{t.title}

{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}

))} {lowBalance.map((b) => (
{b.provider} budget
{b.error || `Low balance: $${b.remaining?.toFixed(2)}`}
))} {attention.length === 0 && lowBalance.length === 0 && (
Nothing needs attention.
)}

Recent Findings

All reports
{recentReports.map((t) => (
{t.title} {t.vessel_name && {t.vessel_name}}

{t.key_finding || t.summary}

{t.devices_checked?.length > 0 && checked: {t.devices_checked.join(", ")}} {t.memory_id && memory saved} {t.obsidian_path && obsidian saved}
))} {recentReports.length === 0 &&
No completed findings yet.
}

Budgets

API balances
{(balances || []).map((b) => (
{b.provider} {b.currency}
{b.error ? (
{b.error}
) : (
{b.remaining != null ? `$${b.remaining.toFixed(2)}` : "—"}
)} {b.total_usage != null &&
used ${b.total_usage.toFixed(2)}
}
))} {(!balances || balances.length === 0) &&
No balance data yet.
}

Platform Readiness

MCP details
{(mcps || []).map((m) => (
{m.name}
{m.tools?.length || 0} tools
))} {(!mcps || mcps.length === 0) &&
Worker is starting MCP servers.
}
); } 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 (
{label}
{value}
); }