463 lines
26 KiB
TypeScript
463 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { ArrowRight, BadgeCheck, BellRing, Bot, CheckCircle2, Clock3, LayoutDashboard, OctagonAlert, Rocket, ShieldAlert, Sparkles, TriangleAlert } from 'lucide-react';
|
|
import { Badge, Button } from '@/components/ui/Primitives';
|
|
import { HermesShell, MetricCard, SectionCard } from '@/components/hermes-shell';
|
|
import { HermesInstanceBadge } from '@/components/hermes-instance-switcher';
|
|
import { HermesOpsPanel } from '@/components/hermes-ops-panel';
|
|
import { useHermesInstance } from '@/lib/hermes-instance-context';
|
|
import {
|
|
getHermesAgents,
|
|
getHermesOverview,
|
|
getHermesProducts,
|
|
getHermesTasks,
|
|
hermesProducts,
|
|
hermesTasks,
|
|
HERMES_INSTANCES,
|
|
type HermesProduct,
|
|
type HermesTask,
|
|
} from '@/lib/hermes';
|
|
import {
|
|
collectBackupEntries,
|
|
collectCronEntries,
|
|
collectWatchdogAlerts,
|
|
emptyTelemetryState,
|
|
loadAllHermesTelemetry,
|
|
telemetryForFilter,
|
|
type HermesTelemetryState,
|
|
} from '@/lib/hermes-telemetry-client';
|
|
|
|
const fmtDate = new Intl.DateTimeFormat('en', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
});
|
|
|
|
const statusTone: Record<string, 'success' | 'warning' | 'error' | 'info' | 'neutral'> = {
|
|
running: 'info',
|
|
idle: 'neutral',
|
|
degraded: 'warning',
|
|
error: 'error',
|
|
queued: 'neutral',
|
|
blocked: 'warning',
|
|
failed: 'error',
|
|
completed: 'success',
|
|
};
|
|
|
|
function taskStatusLabel(task: HermesTask) {
|
|
return task.status.replace('-', ' ');
|
|
}
|
|
|
|
function getTaskTone(task: HermesTask) {
|
|
return statusTone[task.status] ?? 'neutral';
|
|
}
|
|
|
|
function ProductMiniCard({ product }: { product: HermesProduct }) {
|
|
const healthColor = product.healthScore >= 85 ? 'bg-[var(--bl-success)]' : product.healthScore >= 70 ? 'bg-[var(--bl-warning)]' : 'bg-[var(--bl-danger)]';
|
|
return (
|
|
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-card)] p-4">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<p className="font-medium text-[var(--bl-text-primary)]">{product.name}</p>
|
|
<p className="text-xs text-[var(--bl-text-secondary)]">{product.category} · {product.priority}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<HermesInstanceBadge instanceId={product.instanceId} />
|
|
<Badge variant={product.needsAttention ? 'warning' : 'success'}>{product.needsAttention ? 'Attention' : 'Healthy'}</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3">
|
|
<div className="mb-1 flex items-center justify-between text-xs text-[var(--bl-text-secondary)]">
|
|
<span>Health</span>
|
|
<span>{product.healthScore}/100</span>
|
|
</div>
|
|
<div className="h-2 rounded-full bg-[var(--bl-surface-muted)]">
|
|
<div className={`h-2 rounded-full ${healthColor}`} style={{ width: `${product.healthScore}%` }} />
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
{product.tags.slice(0, 3).map((tag) => (
|
|
<Badge key={tag} variant="neutral">{tag}</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function HermesMissionControlPage() {
|
|
const { selectedInstance } = useHermesInstance();
|
|
const [telemetry, setTelemetry] = useState<HermesTelemetryState>(emptyTelemetryState);
|
|
const [telemetryError, setTelemetryError] = useState<string | null>(null);
|
|
const overview = useMemo(() => getHermesOverview(selectedInstance), [selectedInstance]);
|
|
// Per-instance roll-up cards always show both Vijay and Bheem regardless of
|
|
// the active filter — they're the "comparison" view that sits next to the
|
|
// filtered overview metrics. This satisfies the roadmap's "per-instance
|
|
// cards AND a combined roll-up" requirement.
|
|
const perInstance = useMemo(
|
|
() => HERMES_INSTANCES.map((inst) => ({ ...inst, overview: getHermesOverview(inst.id) })),
|
|
[],
|
|
);
|
|
|
|
const activeTasks = useMemo(
|
|
() => getHermesTasks({ status: 'running', instance: selectedInstance })
|
|
.concat(getHermesTasks({ status: 'blocked', instance: selectedInstance }))
|
|
.concat(getHermesTasks({ status: 'queued', instance: selectedInstance }))
|
|
.slice(0, 8),
|
|
[selectedInstance],
|
|
);
|
|
const attentionTasks = useMemo(
|
|
() => getHermesTasks({ status: 'blocked', instance: selectedInstance })
|
|
.concat(getHermesTasks({ status: 'failed', instance: selectedInstance }))
|
|
.slice(0, 8),
|
|
[selectedInstance],
|
|
);
|
|
const filteredProducts = useMemo(
|
|
() => (selectedInstance === 'all' ? hermesProducts : hermesProducts.filter((p) => p.instanceId === selectedInstance)),
|
|
[selectedInstance],
|
|
);
|
|
const filteredTasks = useMemo(
|
|
() => (selectedInstance === 'all' ? hermesTasks : hermesTasks.filter((t) => t.instanceId === selectedInstance)),
|
|
[selectedInstance],
|
|
);
|
|
const recentProducts = filteredProducts
|
|
.filter((product) => product.lastHermesActivityAt)
|
|
.sort((a, b) => new Date(b.lastHermesActivityAt!).getTime() - new Date(a.lastHermesActivityAt!).getTime())
|
|
.slice(0, 8);
|
|
const completedToday = filteredTasks.filter((task) => task.completedAt && new Date(task.completedAt).getTime() > Date.now() - 86_400_000);
|
|
const completedThisWeek = filteredTasks.filter((task) => task.completedAt && new Date(task.completedAt).getTime() > Date.now() - 7 * 86_400_000);
|
|
const failedTasks = filteredTasks.filter((task) => task.status === 'failed');
|
|
const repeatedFailures = useMemo(
|
|
() => getHermesProducts('repeated-failures', selectedInstance).slice(0, 5),
|
|
[selectedInstance],
|
|
);
|
|
const actionableProducts = filteredProducts.filter((product) => product.needsAttention).slice(0, 6);
|
|
const agentStatuses = useMemo(() => getHermesAgents(selectedInstance), [selectedInstance]);
|
|
const liveSnapshots = useMemo(() => telemetryForFilter(telemetry, selectedInstance), [telemetry, selectedInstance]);
|
|
const liveAlerts = useMemo(() => collectWatchdogAlerts(telemetry, selectedInstance).slice(0, 8), [telemetry, selectedInstance]);
|
|
const liveBackups = useMemo(() => collectBackupEntries(telemetry, selectedInstance).slice(0, 6), [telemetry, selectedInstance]);
|
|
const liveCron = useMemo(() => collectCronEntries(telemetry, selectedInstance).slice(0, 6), [telemetry, selectedInstance]);
|
|
|
|
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();
|
|
const timer = window.setInterval(load, 60_000);
|
|
return () => {
|
|
active = false;
|
|
window.clearInterval(timer);
|
|
};
|
|
}, []);
|
|
|
|
const autoActions = [
|
|
'Continue the queued execution lane for high-priority product updates.',
|
|
'Publish a weekly digest from completed and failed work.',
|
|
'Refresh the product health snapshot and attach evidence links.',
|
|
];
|
|
const founderActions = [
|
|
overview.nextRecommendedAction,
|
|
'Approve the blocked P0 work item before the release window closes.',
|
|
'Rotate the stale notification token so background alerts can resume.',
|
|
];
|
|
|
|
return (
|
|
<HermesShell
|
|
title="Hermes Mission Control"
|
|
description="A production-style command center for tracking what Hermes is doing now, what it already shipped, what is blocked, and what needs founder attention."
|
|
actions={(
|
|
<>
|
|
<Button asChild variant="secondary"><Link href="/hermes/tasks"><LayoutDashboard className="mr-2 h-4 w-4" />Task Ledger</Link></Button>
|
|
<Button asChild variant="primary"><Link href="/hermes/products"><Rocket className="mr-2 h-4 w-4" />Product Portfolio</Link></Button>
|
|
</>
|
|
)}
|
|
>
|
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<MetricCard label="Hermes status" value={overview.status.toUpperCase()} tone={overview.status === 'error' ? 'danger' : overview.status === 'degraded' ? 'warning' : overview.status === 'running' ? 'success' : 'default'} icon={<Bot className="h-5 w-5" />} helpText={overview.lastAction} />
|
|
<MetricCard label="Active tasks" value={overview.activeTasks} tone="info" icon={<Sparkles className="h-5 w-5" />} helpText={`${overview.upcomingJobs} queued jobs waiting to run`} />
|
|
<MetricCard label="Completed today" value={overview.completedToday} tone="success" icon={<CheckCircle2 className="h-5 w-5" />} helpText={`${overview.completedThisWeek} completed this week`} />
|
|
<MetricCard label="Founder attention" value={overview.founderAttentionCount} tone="warning" icon={<ShieldAlert className="h-5 w-5" />} helpText={overview.nextRecommendedAction} />
|
|
<MetricCard label="Failed tasks" value={overview.failedTasks} tone="danger" icon={<TriangleAlert className="h-5 w-5" />} helpText="Failure clusters are being tracked in the task ledger" />
|
|
<MetricCard label="Blocked tasks" value={overview.blockedTasks} tone="warning" icon={<OctagonAlert className="h-5 w-5" />} helpText="These items need a human decision or credential fix" />
|
|
<MetricCard label="Avg task duration" value={`${Math.round(overview.averageDurationMs / 60000)}m`} tone="info" icon={<Clock3 className="h-5 w-5" />} helpText="Average across completed tasks" />
|
|
<MetricCard label="Success rate" value={`${overview.successRate}%`} tone="success" icon={<BadgeCheck className="h-5 w-5" />} helpText={`${overview.productsTouchedRecently} products touched in the last 14 days`} />
|
|
</section>
|
|
|
|
<SectionCard
|
|
title="Per-instance roll-up"
|
|
subtitle="Side-by-side view of Vijay (root) and Bheem (uma). Shown regardless of the active instance filter so you can compare load and attention at a glance."
|
|
actions={<Badge variant="info">Always cross-instance</Badge>}
|
|
>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{perInstance.map(({ id, label, description, overview: ov }) => (
|
|
<div key={id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="text-base font-semibold text-[var(--bl-text-primary)]">{label}</p>
|
|
<p className="text-xs text-[var(--bl-text-secondary)]">{description}</p>
|
|
</div>
|
|
<Badge variant={ov.status === 'error' ? 'error' : ov.status === 'degraded' ? 'warning' : ov.status === 'running' ? 'info' : 'neutral'}>{ov.status}</Badge>
|
|
</div>
|
|
<dl className="mt-3 grid grid-cols-2 gap-3 text-sm text-[var(--bl-text-secondary)] md:grid-cols-4">
|
|
<div><dt className="text-xs uppercase tracking-[0.18em] text-[var(--bl-text-tertiary)]">Active</dt><dd className="text-lg font-semibold text-[var(--bl-text-primary)]">{ov.activeTasks}</dd></div>
|
|
<div><dt className="text-xs uppercase tracking-[0.18em] text-[var(--bl-text-tertiary)]">Blocked</dt><dd className="text-lg font-semibold text-[var(--bl-warning)]">{ov.blockedTasks}</dd></div>
|
|
<div><dt className="text-xs uppercase tracking-[0.18em] text-[var(--bl-text-tertiary)]">Failed</dt><dd className="text-lg font-semibold text-[var(--bl-danger)]">{ov.failedTasks}</dd></div>
|
|
<div><dt className="text-xs uppercase tracking-[0.18em] text-[var(--bl-text-tertiary)]">Success %</dt><dd className="text-lg font-semibold text-[var(--bl-success)]">{ov.successRate}%</dd></div>
|
|
</dl>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<HermesOpsPanel />
|
|
|
|
<SectionCard
|
|
title="Unified live alerts"
|
|
subtitle="Cross-instance alert, cron, session, and backup signals from the real Hermes telemetry endpoint."
|
|
actions={<Badge variant={telemetryError ? 'error' : 'success'}>{telemetryError ? 'Telemetry unavailable' : 'Live telemetry'}</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">
|
|
{liveAlerts.length > 0 ? liveAlerts.map((alert) => (
|
|
<div key={`${alert.instanceId}-${alert.timestamp}-${alert.message}`} 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={alert.severity === 'critical' ? 'error' : alert.severity === 'warn' ? 'warning' : 'info'}>{alert.severity}</Badge>
|
|
<HermesInstanceBadge instanceId={alert.instanceId} />
|
|
<span className="text-xs text-[var(--bl-text-tertiary)]">{fmtDate.format(new Date(alert.timestamp))}</span>
|
|
</div>
|
|
<p className="mt-2 text-sm text-[var(--bl-text-primary)]">{alert.message}</p>
|
|
</div>
|
|
<BellRing className="h-4 w-4 text-[var(--bl-text-tertiary)]" />
|
|
</div>
|
|
</div>
|
|
)) : (
|
|
<div className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4 text-sm text-[var(--bl-text-secondary)]">
|
|
No watchdog alerts were returned for the selected instance filter.
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="grid gap-3">
|
|
<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)]">Sessions</p>
|
|
<div className="mt-3 grid gap-2">
|
|
{liveSnapshots.map((snapshot) => (
|
|
<div key={snapshot.instanceId} className="flex items-center justify-between gap-3 text-sm">
|
|
<HermesInstanceBadge instanceId={snapshot.instanceId} />
|
|
<span className="text-[var(--bl-text-secondary)]">{snapshot.sessions.totalSessions} sessions · {snapshot.sessions.totalMessages} messages</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</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)]">Upcoming Hermes cron</p>
|
|
<div className="mt-3 space-y-2 text-sm text-[var(--bl-text-secondary)]">
|
|
{liveCron.length > 0 ? liveCron.map((entry) => (
|
|
<div key={`${entry.instanceId}-${entry.id}`} className="flex items-center justify-between gap-3">
|
|
<span className="truncate">{entry.name}</span>
|
|
<HermesInstanceBadge instanceId={entry.instanceId} />
|
|
</div>
|
|
)) : <p>No cron entries returned.</p>}
|
|
</div>
|
|
</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)]">Recent backup commits</p>
|
|
<div className="mt-3 space-y-2 text-sm text-[var(--bl-text-secondary)]">
|
|
{liveBackups.length > 0 ? liveBackups.map((entry) => (
|
|
<div key={`${entry.instanceId}-${entry.sha}`} className="flex items-center justify-between gap-3">
|
|
<span className="truncate">{entry.subject}</span>
|
|
<HermesInstanceBadge instanceId={entry.instanceId} />
|
|
</div>
|
|
)) : <p>No backup commits returned.</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</SectionCard>
|
|
|
|
<div className="grid gap-6 xl:grid-cols-[1.5fr_1fr]">
|
|
<SectionCard title="Active Missions" subtitle="What Hermes is currently running or waiting on." actions={<Button asChild variant="ghost" size="sm"><Link href="/hermes/tasks">View all tasks <ArrowRight className="ml-2 h-4 w-4" /></Link></Button>}>
|
|
<div className="space-y-3">
|
|
{activeTasks.map((task) => {
|
|
const product = hermesProducts.find((item) => item.id === task.productId);
|
|
return (
|
|
<article key={task.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">
|
|
<Link href={`/hermes/tasks/${task.id}`} className="font-medium text-[var(--bl-text-primary)] hover:underline">{task.title}</Link>
|
|
<Badge variant={getTaskTone(task)}>{taskStatusLabel(task)}</Badge>
|
|
<Badge variant="neutral">{task.priority}</Badge>
|
|
<HermesInstanceBadge instanceId={task.instanceId} />
|
|
</div>
|
|
<p className="mt-1 text-sm text-[var(--bl-text-secondary)]">{product?.name ?? 'Unknown product'} · {task.assignedAgent} · {task.type}</p>
|
|
</div>
|
|
<div className="text-right text-xs text-[var(--bl-text-secondary)]">
|
|
<p>Started {fmtDate.format(new Date(task.startedAt ?? task.createdAt))}</p>
|
|
<p>{task.currentStep ?? task.nextAction}</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3">
|
|
<div className="mb-1 flex items-center justify-between text-xs text-[var(--bl-text-secondary)]">
|
|
<span>Progress</span>
|
|
<span>{task.progressPercent}%</span>
|
|
</div>
|
|
<div className="h-2 rounded-full bg-[var(--bl-surface-card)]">
|
|
<div className="h-2 rounded-full bg-[var(--bl-accent)]" style={{ width: `${task.progressPercent}%` }} />
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Founder Attention Queue" subtitle="Items Hermes cannot safely complete without your help." actions={<Badge variant="warning">Needs decision</Badge>}>
|
|
<div className="space-y-3">
|
|
{attentionTasks.slice(0, 5).map((task) => (
|
|
<div key={task.id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div>
|
|
<Link href={`/hermes/tasks/${task.id}`} className="font-medium text-[var(--bl-text-primary)] hover:underline">{task.title}</Link>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">{task.blockerReason ?? task.error ?? task.nextAction}</p>
|
|
</div>
|
|
<Badge variant={task.status === 'failed' ? 'error' : 'warning'}>{task.status}</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{actionableProducts.slice(0, 2).map((product) => (
|
|
<div key={product.id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div>
|
|
<p className="font-medium text-[var(--bl-text-primary)]">{product.name}</p>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">{product.description}</p>
|
|
</div>
|
|
<Badge variant="warning">Product attention</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
|
<SectionCard title="What Hermes did for me" subtitle="Operational summary of recent work." actions={<Badge variant="success">Evidence-backed</Badge>}>
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<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)]">Today</p>
|
|
<p className="mt-2 text-2xl font-semibold">{completedToday.length}</p>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">Tasks completed or closed today.</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)]">This week</p>
|
|
<p className="mt-2 text-2xl font-semibold">{completedThisWeek.length}</p>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">Shipped, repaired, or documented this week.</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)]">Last 30 days</p>
|
|
<p className="mt-2 text-2xl font-semibold">{hermesTasks.length}</p>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">Tracked execution events across the portfolio.</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 grid gap-3 md:grid-cols-2">
|
|
{[
|
|
'Fixed bugs and failure loops',
|
|
'Created PRs and commit-ready changes',
|
|
'Deployed services and validated health',
|
|
'Updated docs and audit summaries',
|
|
].map((item) => (
|
|
<div key={item} className="flex items-center gap-3 rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3 text-sm text-[var(--bl-text-secondary)]">
|
|
<CheckCircle2 className="h-4 w-4 text-[var(--bl-success)]" />
|
|
{item}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Next Best Actions" subtitle="Split between automation and founder decisions." actions={<Badge variant="info">Prioritized</Badge>}>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Hermes can do automatically</p>
|
|
<div className="space-y-2">
|
|
{autoActions.map((item) => (
|
|
<div key={item} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3 text-sm text-[var(--bl-text-secondary)]">{item}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-[var(--bl-text-tertiary)]">Needs Saravana's decision</p>
|
|
<div className="space-y-2">
|
|
{founderActions.map((item) => (
|
|
<div key={item} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3 text-sm text-[var(--bl-text-secondary)]">{item}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
|
<SectionCard title="Product Health Snapshot" subtitle="50-product portfolio view with recent activity and attention flags." actions={<Button asChild variant="ghost" size="sm"><Link href="/hermes/products">Open portfolio <ArrowRight className="ml-2 h-4 w-4" /></Link></Button>}>
|
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
{recentProducts.map((product) => (
|
|
<ProductMiniCard key={product.id} product={product} />
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Ecosystem Health" subtitle="Core agents and integrations are scored with recent status." actions={<Badge variant="neutral">Telemetry placeholder</Badge>}>
|
|
<div className="space-y-3">
|
|
{agentStatuses.map((agent) => (
|
|
<div key={agent.id} className="rounded-2xl border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="font-medium text-[var(--bl-text-primary)]">{agent.name}</p>
|
|
<p className="text-sm text-[var(--bl-text-secondary)]">{agent.type} · {agent.callsToday} calls today</p>
|
|
{agent.configIssue ? <p className="mt-1 text-sm text-[var(--bl-warning)]">{agent.configIssue}</p> : null}
|
|
</div>
|
|
<Badge variant={agent.status === 'healthy' ? 'success' : agent.status === 'degraded' ? 'warning' : 'error'}>{agent.status}</Badge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<SectionCard title="Weekly digest" subtitle="A founder-friendly summary of the current operational week.">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<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)]">Shipped this week</p>
|
|
<p className="mt-2 text-2xl font-semibold">{completedThisWeek.length}</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)]">Failed this week</p>
|
|
<p className="mt-2 text-2xl font-semibold">{failedTasks.filter((task) => task.completedAt ? new Date(task.completedAt).getTime() > Date.now() - 7 * 86_400_000 : true).length}</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)]">Repeated failure products</p>
|
|
<p className="mt-2 text-2xl font-semibold">{repeatedFailures.length}</p>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
</HermesShell>
|
|
);
|
|
}
|