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:
131
app/agents/page.tsx
Normal file
131
app/agents/page.tsx
Normal 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
114
app/intents/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
118
app/ledger/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user