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:
205
frontend/components/TaskLiveStatus.tsx
Normal file
205
frontend/components/TaskLiveStatus.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { humanToolName, taskPhaseLabel, TaskEvent } from "@/lib/task-types";
|
||||
|
||||
const PHASES = [
|
||||
{ id: "context", label: "Context" },
|
||||
{ id: "triage", label: "Plan" },
|
||||
{ id: "connect", label: "Connect" },
|
||||
{ id: "diagnose", label: "Diagnose" },
|
||||
{ id: "reason", label: "Analyze" },
|
||||
{ id: "report", label: "Report" },
|
||||
] as const;
|
||||
|
||||
function phaseIndex(phase: string | undefined): number {
|
||||
if (!phase) return -1;
|
||||
return PHASES.findIndex((p) => p.id === phase);
|
||||
}
|
||||
|
||||
function derivePhase(events: TaskEvent[], taskStatus?: string): string {
|
||||
if (taskStatus === "succeeded" || taskStatus === "waiting_approval") return "report";
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const p = events[i].payload?.phase as string | undefined;
|
||||
if (p) return p;
|
||||
const k = events[i].kind;
|
||||
if (k === "context" || k === "rule") return "context";
|
||||
if (k === "triage") return "triage";
|
||||
if (k === "connect") return "connect";
|
||||
if (k === "diagnose" || k === "tool_start" || k === "tool_call") return "diagnose";
|
||||
if (k === "reason") return "reason";
|
||||
if (k === "report") return "report";
|
||||
}
|
||||
return "context";
|
||||
}
|
||||
|
||||
function activeTool(events: TaskEvent[]): TaskEvent | null {
|
||||
for (let i = events.length - 1; i >= 0; i--) {
|
||||
const ev = events[i];
|
||||
if (ev.kind === "tool_start") return ev;
|
||||
if (ev.kind === "tool_call" || ev.kind === "error") return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function TaskLiveStatus({
|
||||
events,
|
||||
taskStatus,
|
||||
}: {
|
||||
events: TaskEvent[];
|
||||
taskStatus: string;
|
||||
}) {
|
||||
const running = taskStatus === "running" || taskStatus === "queued";
|
||||
const currentPhase = derivePhase(events, taskStatus);
|
||||
const currentIdx = phaseIndex(currentPhase);
|
||||
const active = activeTool(events);
|
||||
const lastProgress = [...events].reverse().find((e) => e.kind === "progress");
|
||||
const devices = Array.from(
|
||||
new Set(
|
||||
events
|
||||
.map((e) => e.payload?.device)
|
||||
.filter((device): device is string => typeof device === "string" && !!device)
|
||||
)
|
||||
);
|
||||
const nowDoing = active
|
||||
? `${active.payload?.device || "Device"}: ${humanToolName(String(active.payload?.tool || "diagnostic"))}`
|
||||
: taskPhaseLabel(taskStatus, events);
|
||||
|
||||
return (
|
||||
<div className="card mb-6 border-accent/30 bg-gradient-to-br from-panel to-panel2">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold">Task status</h2>
|
||||
<p className="mt-1 text-sm text-muted">{nowDoing}</p>
|
||||
</div>
|
||||
{running && (
|
||||
<span className="flex items-center gap-2 text-xs text-accent">
|
||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-accent" />
|
||||
In progress
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 grid grid-cols-2 gap-2 md:grid-cols-6">
|
||||
{PHASES.map((phase, idx) => {
|
||||
const done = !running && currentIdx >= 0 ? idx <= currentIdx : idx < currentIdx;
|
||||
const activePhase = idx === currentIdx && running;
|
||||
return (
|
||||
<div
|
||||
key={phase.id}
|
||||
className={`rounded-lg px-3 py-2 text-center text-xs transition-colors ${
|
||||
activePhase
|
||||
? "bg-accent/20 text-accent ring-1 ring-accent/40"
|
||||
: done
|
||||
? "bg-good/10 text-good"
|
||||
: "bg-ink/40 text-muted"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{phase.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{devices.length > 0 && (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{devices.map((device) => (
|
||||
<span key={device} className="badge bg-panel text-xs text-accent">
|
||||
{device}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(lastProgress?.message || active) && (
|
||||
<div className="rounded-lg bg-ink/50 p-3">
|
||||
{lastProgress?.message && (
|
||||
<p className="text-sm text-slate-200">{lastProgress.message}</p>
|
||||
)}
|
||||
{active && (
|
||||
<div className="mt-2 border-t border-edge/50 pt-2">
|
||||
<div className="text-xs uppercase tracking-wide text-accent2">Running now</div>
|
||||
<div className="mt-1 font-mono text-sm">
|
||||
{(active.payload?.device as string) || "?"} - {humanToolName((active.payload?.tool as string) || "tool")}
|
||||
</div>
|
||||
{typeof active.payload?.command === "string" && (
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-black/40 p-2 font-mono text-[11px] text-emerald-300/90">
|
||||
$ {active.payload.command}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function eventTime(ev: TaskEvent) {
|
||||
const raw = ev.ts || ev.created_at;
|
||||
if (!raw) return "";
|
||||
try {
|
||||
return new Date(raw).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function TimelineEvent({ ev }: { ev: TaskEvent }) {
|
||||
const p = ev.payload || {};
|
||||
const isError = ev.kind === "error" || p.status === "error" || p.ok === false;
|
||||
const isRunning = p.status === "running" || ev.kind === "tool_start";
|
||||
const isDone = p.ok === true || p.status === "done";
|
||||
|
||||
const border = isError
|
||||
? "border-l-2 border-l-bad"
|
||||
: isRunning
|
||||
? "border-l-2 border-l-accent animate-pulse"
|
||||
: isDone
|
||||
? "border-l-2 border-l-good"
|
||||
: "border-l-2 border-l-transparent";
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg bg-panel2 px-3 py-2 ${border}`}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={isError ? "text-bad" : isRunning ? "text-accent" : "text-accent2"}>
|
||||
{isError ? "!" : isRunning ? ">" : "-"}
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-wide text-muted">{ev.kind.replace("_", " ")}</span>
|
||||
{eventTime(ev) && <span className="text-[10px] text-muted">{eventTime(ev)}</span>}
|
||||
</div>
|
||||
{typeof p.phase === "string" && (
|
||||
<span className="badge bg-panel text-[10px] text-muted">{p.phase}</span>
|
||||
)}
|
||||
{p.ok === false && <span className="text-[10px] text-bad">fail</span>}
|
||||
{isRunning && <span className="text-[10px] text-accent">running…</span>}
|
||||
</div>
|
||||
{ev.message && <div className="mt-1 text-sm">{ev.message}</div>}
|
||||
{typeof p.tool === "string" && (
|
||||
<div className="mt-1 text-xs text-muted">
|
||||
{typeof p.device === "string" ? `${p.device} - ` : ""}
|
||||
{humanToolName(p.tool)}
|
||||
</div>
|
||||
)}
|
||||
{typeof p.command === "string" && (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs text-muted">Command</summary>
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-black/50 p-2 font-mono text-[11px] text-emerald-300/90">
|
||||
$ {p.command}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
{typeof p.output === "string" && p.output && (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs text-muted">Evidence output</summary>
|
||||
<pre className="mt-2 max-h-40 overflow-auto rounded bg-ink/70 p-2 text-[11px] text-slate-300">
|
||||
{p.output}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
{typeof p.error === "string" && (
|
||||
<pre className="mt-2 text-[11px] text-bad">{p.error}</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user