Add Agents, Intents, and Ledger pages to Prism Console

- Add /agents page with agent cards showing status, type, trust score
- Add /intents page to track declared intentions
- Add /ledger page with immutable event log display
- Update layout with navigation menu
- All pages fetch from api.blackroad.io

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alexa Louise
2025-12-14 19:01:07 -06:00
parent 557ff7fc14
commit 1655047a04
4 changed files with 385 additions and 1 deletions

131
app/agents/page.tsx Normal file
View File

@@ -0,0 +1,131 @@
import { Suspense } from 'react';
interface Agent {
id: string;
identity: string;
name?: string;
type: 'human' | 'ai' | 'system' | 'hybrid';
status: 'observing' | 'active' | 'sleeping' | 'suspended';
trustScore: number;
lastSeen: string;
}
async function fetchAgents(): Promise<Agent[]> {
try {
const res = await fetch('https://api.blackroad.io/agents', {
next: { revalidate: 30 },
});
if (!res.ok) return [];
const data = await res.json();
return data.agents || [];
} catch (error) {
console.error('Failed to fetch agents:', error);
return [];
}
}
function AgentCard({ agent }: { agent: Agent }) {
const statusColors = {
active: 'bg-green-500/10 text-green-400 border-green-500/20',
observing: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
sleeping: 'bg-gray-500/10 text-gray-400 border-gray-500/20',
suspended: 'bg-red-500/10 text-red-400 border-red-500/20',
};
const typeIcons = {
human: '👤',
ai: '🤖',
system: '⚙️',
hybrid: '🧬',
};
return (
<div className="card-surface p-4 hover:border-gray-600 transition-colors">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{typeIcons[agent.type]}</span>
<div>
<h3 className="font-medium text-white">
{agent.name || agent.identity}
</h3>
<p className="text-xs text-gray-500 font-mono">{agent.identity}</p>
</div>
</div>
<div
className={`px-2 py-1 rounded border text-xs font-medium ${
statusColors[agent.status]
}`}
>
{agent.status}
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-gray-400">Type</p>
<p className="text-white capitalize">{agent.type}</p>
</div>
<div>
<p className="text-gray-400">Trust Score</p>
<p className="text-white">{agent.trustScore.toFixed(1)}%</p>
</div>
</div>
<div className="mt-3 text-xs text-gray-500">
Last seen: {new Date(agent.lastSeen).toLocaleString()}
</div>
</div>
);
}
async function AgentsList() {
const agents = await fetchAgents();
if (agents.length === 0) {
return (
<div className="card-surface p-8 text-center">
<p className="text-gray-400">No agents found</p>
<p className="text-sm text-gray-500 mt-2">
Agents will appear here once they connect to the mesh
</p>
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{agents.map((agent) => (
<AgentCard key={agent.id} agent={agent} />
))}
</div>
);
}
export default function AgentsPage() {
return (
<div className="flex flex-col gap-6">
<section className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Agents</h1>
<p className="text-gray-400 mt-1">
Monitor and manage agents in the BlackRoad mesh
</p>
</div>
<button className="btn-primary">
<span>+</span>
<span>New Agent</span>
</button>
</section>
<Suspense
fallback={
<div className="card-surface p-8 text-center text-gray-400">
Loading agents...
</div>
}
>
<AgentsList />
</Suspense>
</div>
);
}

114
app/intents/page.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { Suspense } from 'react';
interface Intent {
id: string;
agentId: string;
action: string;
target?: string;
status: 'pending' | 'executing' | 'completed' | 'failed';
createdAt: string;
completedAt?: string;
reason?: string;
}
async function fetchIntents(): Promise<Intent[]> {
try {
const res = await fetch('https://api.blackroad.io/intents', {
next: { revalidate: 10 },
});
if (!res.ok) return [];
const data = await res.json();
return data.intents || [];
} catch (error) {
console.error('Failed to fetch intents:', error);
return [];
}
}
function IntentCard({ intent }: { intent: Intent }) {
const statusColors = {
pending: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',
executing: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
completed: 'bg-green-500/10 text-green-400 border-green-500/20',
failed: 'bg-red-500/10 text-red-400 border-red-500/20',
};
return (
<div className="card-surface p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-white">{intent.action}</h3>
<div
className={`px-2 py-0.5 rounded border text-xs font-medium ${
statusColors[intent.status]
}`}
>
{intent.status}
</div>
</div>
{intent.target && (
<p className="text-sm text-gray-400 mt-1"> {intent.target}</p>
)}
{intent.reason && (
<p className="text-sm text-gray-500 mt-2">{intent.reason}</p>
)}
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-gray-500">
<span className="font-mono">{intent.agentId}</span>
<span>{new Date(intent.createdAt).toLocaleString()}</span>
</div>
</div>
);
}
async function IntentsList() {
const intents = await fetchIntents();
if (intents.length === 0) {
return (
<div className="card-surface p-8 text-center">
<p className="text-gray-400">No intents declared</p>
<p className="text-sm text-gray-500 mt-2">
Intents will appear here as agents declare their intentions
</p>
</div>
);
}
return (
<div className="flex flex-col gap-3">
{intents.map((intent) => (
<IntentCard key={intent.id} intent={intent} />
))}
</div>
);
}
export default function IntentsPage() {
return (
<div className="flex flex-col gap-6">
<section>
<h1 className="text-2xl font-bold text-white">Intents</h1>
<p className="text-gray-400 mt-1">
Track declared intentions across the BlackRoad mesh
</p>
<p className="text-sm text-gray-500 mt-2">
"Opacity is violence. Transparency is trust."
</p>
</section>
<Suspense
fallback={
<div className="card-surface p-8 text-center text-gray-400">
Loading intents...
</div>
}
>
<IntentsList />
</Suspense>
</div>
);
}

View File

@@ -1,17 +1,29 @@
import './globals.css'; import './globals.css';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import Link from 'next/link';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Prism Console', title: 'Prism Console',
description: 'BlackRoad OS admin console Gen-0 scaffold' description: 'BlackRoad OS admin console Gen-0 scaffold'
}; };
function NavLink({ href, children }: { href: string; children: ReactNode }) {
return (
<Link
href={href}
className="text-gray-400 hover:text-white transition-colors text-sm"
>
{children}
</Link>
);
}
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="en" className="dark"> <html lang="en" className="dark">
<body className="min-h-screen bg-background text-gray-100"> <body className="min-h-screen bg-background text-gray-100">
<div className="mx-auto flex max-w-6xl flex-col gap-8 px-6 py-10"> <div className="mx-auto flex max-w-7xl flex-col gap-8 px-6 py-10">
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<div> <div>
<p className="text-xs uppercase tracking-[0.2em] text-muted">BlackRoad OS</p> <p className="text-xs uppercase tracking-[0.2em] text-muted">BlackRoad OS</p>
@@ -21,6 +33,15 @@ export default function RootLayout({ children }: { children: ReactNode }) {
Edge-ready shadcn/tailwind Gen-0 Edge-ready shadcn/tailwind Gen-0
</div> </div>
</header> </header>
<nav className="flex gap-6 border-b border-gray-800 pb-4">
<NavLink href="/">Home</NavLink>
<NavLink href="/agents">Agents</NavLink>
<NavLink href="/intents">Intents</NavLink>
<NavLink href="/ledger">Ledger</NavLink>
<NavLink href="/env/production">Environments</NavLink>
</nav>
<main>{children}</main> <main>{children}</main>
</div> </div>
</body> </body>

118
app/ledger/page.tsx Normal file
View File

@@ -0,0 +1,118 @@
import { Suspense } from 'react';
interface LedgerEntry {
id: string;
timestamp: string;
agentId: string;
action: string;
target?: string;
hash: string;
previousHash?: string;
data: Record<string, any>;
}
async function fetchLedger(): Promise<LedgerEntry[]> {
try {
const res = await fetch('https://api.blackroad.io/ledger', {
next: { revalidate: 5 },
});
if (!res.ok) return [];
const data = await res.json();
return data.entries || [];
} catch (error) {
console.error('Failed to fetch ledger:', error);
return [];
}
}
function LedgerEntryCard({ entry }: { entry: LedgerEntry }) {
return (
<div className="card-surface p-4 font-mono text-sm">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<span className="text-blue-400">{entry.action}</span>
{entry.target && (
<>
<span className="text-gray-600"></span>
<span className="text-gray-400">{entry.target}</span>
</>
)}
</div>
<div className="mt-2 text-xs text-gray-500">
<div>Agent: {entry.agentId}</div>
<div>Hash: {entry.hash.substring(0, 16)}...</div>
{entry.previousHash && (
<div>Prev: {entry.previousHash.substring(0, 16)}...</div>
)}
</div>
{Object.keys(entry.data).length > 0 && (
<details className="mt-3">
<summary className="text-gray-400 cursor-pointer hover:text-gray-300">
Data ({Object.keys(entry.data).length} fields)
</summary>
<pre className="mt-2 text-xs text-gray-500 bg-black/30 p-2 rounded overflow-x-auto">
{JSON.stringify(entry.data, null, 2)}
</pre>
</details>
)}
</div>
<div className="text-xs text-gray-500 text-right">
{new Date(entry.timestamp).toLocaleString()}
</div>
</div>
</div>
);
}
async function LedgerList() {
const entries = await fetchLedger();
if (entries.length === 0) {
return (
<div className="card-surface p-8 text-center">
<p className="text-gray-400">No ledger entries</p>
<p className="text-sm text-gray-500 mt-2">
The immutable record will appear here
</p>
</div>
);
}
return (
<div className="flex flex-col gap-2">
{entries.map((entry) => (
<LedgerEntryCard key={entry.id} entry={entry} />
))}
</div>
);
}
export default function LedgerPage() {
return (
<div className="flex flex-col gap-6">
<section>
<h1 className="text-2xl font-bold text-white">Ledger</h1>
<p className="text-gray-400 mt-1">
Immutable event log with PS-SHA hash chain
</p>
<p className="text-sm text-gray-500 mt-2">
"The record is sacred."
</p>
</section>
<Suspense
fallback={
<div className="card-surface p-8 text-center text-gray-400">
Loading ledger...
</div>
}
>
<LedgerList />
</Suspense>
</div>
);
}