bytelyst-devops-tools/dashboard/web/src/app/hermes/tasks/[id]/page.tsx

252 lines
16 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { ArrowLeft, CircleDashed, Clock3, ShieldAlert, Sparkles } from 'lucide-react';
import { Badge, Button } from '@/components/ui/Primitives';
import { HermesShell, MetricCard, SectionCard } from '@/components/hermes-shell';
import { getHermesProductById, getHermesTaskById, getHermesTaskEvents } from '@/lib/hermes';
import {
collectSessionEvents,
collectSessionEntries,
emptyTelemetryState,
loadAllHermesTelemetry,
type HermesTelemetryState,
} from '@/lib/hermes-telemetry-client';
const fmt = new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
function levelTone(level: 'debug' | 'info' | 'warn' | 'error' | 'success'): 'success' | 'warning' | 'error' | 'neutral' | 'info' {
switch (level) {
case 'success': return 'success';
case 'warn': return 'warning';
case 'error': return 'error';
case 'debug': return 'neutral';
default: return 'info';
}
}
export default function HermesTaskDetailPage({ params }: { params: { id: string } }) {
const routeParams = useParams<{ id: string }>();
const taskId = routeParams?.id ?? params.id;
const task = getHermesTaskById(taskId);
const events = getHermesTaskEvents(taskId);
const [telemetry, setTelemetry] = useState<HermesTelemetryState>(emptyTelemetryState);
const [telemetryError, setTelemetryError] = useState<string | null>(null);
const liveSessions = useMemo(() => collectSessionEntries(telemetry, 'all').slice(0, 8), [telemetry]);
const liveEvents = useMemo(() => collectSessionEvents(telemetry, 'all').slice(0, 12), [telemetry]);
useEffect(() => {
let active = true;
const load = async () => {
try {
const next = await loadAllHermesTelemetry();
if (!active) return;
setTelemetry(next);
setTelemetryError(null);
} catch (err) {
if (!active) return;
setTelemetryError(err instanceof Error ? err.message : String(err));
}
};
void load();
return () => {
active = false;
};
}, []);
if (!task) {
return (
<HermesShell
title="Task not found"
description={`No Hermes task matched the id ${taskId}.`}
actions={<Button asChild variant="secondary"><Link href="/hermes/tasks"><ArrowLeft className="mr-2 h-4 w-4" />Back to task ledger</Link></Button>}
>
<SectionCard title="Missing task" subtitle="The mock service did not contain a matching record.">
<p className="text-sm text-[var(--bl-text-secondary)]">Check the task id or return to the ledger and select another item.</p>
</SectionCard>
</HermesShell>
);
}
const product = getHermesProductById(task.productId);
const timeline = events.slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return (
<HermesShell
title={task.title}
description={task.description}
actions={<Button asChild variant="secondary"><Link href="/hermes/tasks"><ArrowLeft className="mr-2 h-4 w-4" />Back to task ledger</Link></Button>}
>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<MetricCard label="Status" value={task.status.toUpperCase()} tone={task.status === 'completed' ? 'success' : task.status === 'failed' ? 'danger' : task.status === 'blocked' ? 'warning' : 'info'} icon={<CircleDashed className="h-5 w-5" />} helpText={task.currentStep ?? 'Awaiting next step'} />
<MetricCard label="Priority" value={task.priority} tone={task.priority === 'P0' ? 'danger' : task.priority === 'P1' ? 'warning' : 'default'} icon={<ShieldAlert className="h-5 w-5" />} helpText={task.type} />
<MetricCard label="Duration" value={task.durationMs ? `${Math.round(task.durationMs / 60000)}m` : '—'} tone="info" icon={<Clock3 className="h-5 w-5" />} helpText={task.retryCount ? `${task.retryCount} retries` : 'No retries recorded'} />
<MetricCard label="Product" value={product?.name ?? 'Unknown'} tone="default" icon={<Sparkles className="h-5 w-5" />} helpText={product?.category ?? 'No product metadata'} />
</section>
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
<SectionCard title="Summary" subtitle="Everything Hermes knows about this task in one place.">
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 space-y-3">
<div className="flex items-center gap-2">
<Badge variant={task.status === 'completed' ? 'success' : task.status === 'failed' ? 'error' : task.status === 'blocked' ? 'warning' : 'neutral'}>{task.status}</Badge>
<Badge variant="neutral">{task.source}</Badge>
</div>
<div className="space-y-2 text-sm text-[var(--bl-text-secondary)]">
<p><span className="text-[var(--bl-text-tertiary)]">Product:</span> {product?.name ?? 'Unknown'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Assigned agent:</span> {task.assignedAgent}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Current step:</span> {task.currentStep ?? 'n/a'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Result:</span> {task.result ?? 'n/a'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Blocker:</span> {task.blockerReason ?? 'n/a'}</p>
</div>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 space-y-3">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Execution details</p>
<div className="space-y-2 text-sm text-[var(--bl-text-secondary)]">
<p><span className="text-[var(--bl-text-tertiary)]">Created:</span> {fmt.format(new Date(task.createdAt))}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Started:</span> {task.startedAt ? fmt.format(new Date(task.startedAt)) : '—'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Completed:</span> {task.completedAt ? fmt.format(new Date(task.completedAt)) : '—'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Last action:</span> {task.lastAction ?? 'n/a'}</p>
<p><span className="text-[var(--bl-text-tertiary)]">Next action:</span> {task.nextAction ?? 'n/a'}</p>
</div>
<div className="flex flex-wrap gap-2">
{task.tags.map((tag) => <Badge key={tag} variant="neutral">{tag}</Badge>)}
</div>
</div>
</div>
</SectionCard>
<SectionCard title="Hermes learning" subtitle="A place to capture the memory and prevention pattern for next time.">
<div className="space-y-3 text-sm text-[var(--bl-text-secondary)]">
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Lesson learned</p>
<p className="mt-2">{task.status === 'failed' ? 'Capture the failing command, dependency, and the exact resolution before retrying the lane.' : 'Preserve the successful execution path as a repeatable pattern.'}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Suggested memory update</p>
<p className="mt-2">{task.status === 'blocked' ? 'Remember that this workflow requires founder approval or a credential refresh before execution can continue.' : 'Document the command sequence and verification checks for future reuse.'}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Prevention for next time</p>
<p className="mt-2">{task.nextAction ?? 'Keep telemetry wired into the dashboard for follow-up visibility.'}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Recurring issue detection</p>
<p className="mt-2">{task.retryCount > 0 ? 'Multiple retries detected; this lane should be watched for recurrence.' : 'No recurring pattern detected for this task.'}</p>
</div>
</div>
</SectionCard>
</div>
<SectionCard
title="Live Hermes event timeline"
subtitle="Sanitized session JSONL events read from Hermes homes, paired with durable session index context. Message content is redacted at the backend."
actions={<Badge variant={telemetryError ? 'error' : 'success'}>{telemetryError ? 'Telemetry unavailable' : 'Live sessions'}</Badge>}
>
{telemetryError ? (
<p className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 text-sm text-[var(--bl-warning)]">
Could not load telemetry: {telemetryError}
</p>
) : (
<div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
<div className="space-y-3">
{liveEvents.map((event) => (
<div key={`${event.instanceId}-${event.id}`} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={event.eventType === 'tool-call' ? 'info' : event.eventType === 'system' ? 'neutral' : 'success'}>{event.eventType}</Badge>
<Badge variant="neutral">{event.instanceId}</Badge>
{event.status ? <Badge variant="neutral">{event.status}</Badge> : null}
</div>
<p className="mt-2 font-medium text-[var(--bl-text-primary)]">{event.summary}</p>
<p className="mt-1 truncate text-xs text-[var(--bl-text-secondary)]">{event.sessionFile}</p>
</div>
<p className="text-xs text-[var(--bl-text-tertiary)]">{event.timestamp ? fmt.format(new Date(event.timestamp)) : 'unknown'}</p>
</div>
</div>
))}
{liveEvents.length === 0 ? (
<p className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 text-sm text-[var(--bl-text-secondary)]">No live session events were returned.</p>
) : null}
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
{liveSessions.map((session) => (
<div key={`${session.instanceId}-${session.id}`} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<div className="flex items-center justify-between gap-3">
<Badge variant={session.resumePending || session.suspended ? 'warning' : 'info'}>{session.platform ?? 'session'}</Badge>
<Badge variant="neutral">{session.instanceId}</Badge>
</div>
<p className="mt-3 truncate font-medium text-[var(--bl-text-primary)]">{session.displayName ?? session.sessionKey}</p>
<p className="mt-1 text-xs text-[var(--bl-text-secondary)]">Updated {session.updatedAt ? fmt.format(new Date(session.updatedAt)) : 'unknown'}</p>
</div>
))}
{liveSessions.length === 0 ? (
<p className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 text-sm text-[var(--bl-text-secondary)]">No live session entries were returned.</p>
) : null}
</div>
</div>
)}
</SectionCard>
<SectionCard title="Timeline" subtitle="Chronological event stream for the task lifecycle.">
<ol className="space-y-4">
{timeline.map((event) => (
<li key={event.id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={levelTone(event.level)}>{event.eventType}</Badge>
<span className="text-sm font-medium text-[var(--bl-text-primary)]">{event.message}</span>
</div>
{event.command ? <p className="text-sm text-[var(--bl-text-secondary)]"><span className="text-[var(--bl-text-tertiary)]">Command:</span> {event.command}</p> : null}
{event.toolName ? <p className="text-sm text-[var(--bl-text-secondary)]"><span className="text-[var(--bl-text-tertiary)]">Tool:</span> {event.toolName}</p> : null}
{event.artifactUrl ? <p className="text-sm text-[var(--bl-text-secondary)]"><span className="text-[var(--bl-text-tertiary)]">Artifact:</span> {event.artifactUrl}</p> : null}
</div>
<p className="text-xs text-[var(--bl-text-tertiary)]">{fmt.format(new Date(event.timestamp))}</p>
</div>
</li>
))}
</ol>
</SectionCard>
<div className="grid gap-6 lg:grid-cols-2">
<SectionCard title="Commands executed" subtitle="Execution evidence captured by Hermes.">
<div className="space-y-3">
{events.filter((event) => event.command || event.toolName).map((event) => (
<div key={event.id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 text-sm text-[var(--bl-text-secondary)]">
<p className="font-medium text-[var(--bl-text-primary)]">{event.message}</p>
<p className="mt-2 font-mono text-xs">{event.command ?? event.toolName ?? 'No command captured'}</p>
</div>
))}
{events.every((event) => !event.command && !event.toolName) ? <p className="text-sm text-[var(--bl-text-secondary)]">No command logs were captured for this task.</p> : null}
</div>
</SectionCard>
<SectionCard title="File and branch context" subtitle="Code and Git artifacts associated with the run.">
<div className="grid gap-3 md:grid-cols-2">
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Git branch</p>
<p className="mt-2 font-medium">hermes/{task.id}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Commit SHA</p>
<p className="mt-2 font-medium">{task.completedAt ? task.id.replace('task', 'commit').slice(0, 16) : 'pending'}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">PR URL</p>
<p className="mt-2 text-sm text-[var(--bl-text-secondary)]">{task.status === 'completed' ? `https://github.com/bytelyst/hermes/pull/${task.id.replace('task-', '')}` : 'Not created yet'}</p>
</div>
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Deployment URL</p>
<p className="mt-2 text-sm text-[var(--bl-text-secondary)]">{product?.productionUrl ?? 'Not deployed yet'}</p>
</div>
</div>
</SectionCard>
</div>
</HermesShell>
);
}