This commit introduces a complete transparency infrastructure including: Core Transparency Modules: - AuditLog: Immutable, cryptographically-linked audit trail for all actions - EventStream: Real-time SSE streaming and webhook support - TransparencyDashboard: Aggregated metrics and system health monitoring - DigitalSignatures: Cryptographic verification for handoffs and certificates API Endpoints: - /api/transparency/dashboard - Full platform metrics - /api/transparency/audit - Query and log audit entries - /api/transparency/events - SSE stream and event history - /api/transparency/webhooks - Webhook management - /api/transparency/signatures - Digital signature operations - /api/transparency/certificate/[plantId] - Plant authenticity certificates - /api/transparency/export - Multi-format data export - /api/transparency/report - Compliance reporting - /api/transparency/health - System health checks Features: - Immutable audit logging with chain integrity verification - Real-time event streaming via Server-Sent Events - Webhook support with HMAC signature verification - Digital signatures for transport handoffs and ownership transfers - Certificate of Authenticity generation for plants - Multi-format data export (JSON, CSV, summary) - Public transparency portal at /transparency - System health monitoring for all components Documentation: - Comprehensive TRANSPARENCY.md guide with API examples
525 lines
19 KiB
TypeScript
525 lines
19 KiB
TypeScript
/**
|
|
* Public Transparency Portal
|
|
*
|
|
* A public-facing dashboard showing all transparency metrics
|
|
* for LocalGreenChain platform operations.
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Head from 'next/head';
|
|
|
|
interface SystemHealth {
|
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
uptime: number;
|
|
lastCheck: string;
|
|
components: Record<string, { status: string; errorCount24h: number }>;
|
|
}
|
|
|
|
interface DashboardData {
|
|
generatedAt: string;
|
|
systemHealth: SystemHealth;
|
|
audit: {
|
|
totalEntries: number;
|
|
entriesLast24h: number;
|
|
entriesLast7d: number;
|
|
errorRate24h: number;
|
|
};
|
|
events: {
|
|
totalEvents: number;
|
|
eventsLast24h: number;
|
|
activeSubscriptions: number;
|
|
activeWebhooks: number;
|
|
};
|
|
plants: {
|
|
totalPlantsRegistered: number;
|
|
plantsRegisteredToday: number;
|
|
plantsRegisteredThisWeek: number;
|
|
totalClones: number;
|
|
averageLineageDepth: number;
|
|
topVarieties: Array<{ variety: string; count: number }>;
|
|
};
|
|
blockchain: {
|
|
totalBlocks: number;
|
|
chainValid: boolean;
|
|
difficulty: number;
|
|
lastBlockTime: string | null;
|
|
};
|
|
environmental: {
|
|
totalCarbonSavedKg: number;
|
|
waterSavedLiters: number;
|
|
foodMilesReduced: number;
|
|
sustainabilityScore: number;
|
|
};
|
|
agents: {
|
|
totalAgents: number;
|
|
activeAgents: number;
|
|
totalTasksCompleted: number;
|
|
};
|
|
alerts: Array<{
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
timestamp: string;
|
|
}>;
|
|
}
|
|
|
|
const StatusBadge = ({ status }: { status: string }) => {
|
|
const colors: Record<string, string> = {
|
|
healthy: 'bg-green-500',
|
|
up: 'bg-green-500',
|
|
degraded: 'bg-yellow-500',
|
|
unhealthy: 'bg-red-500',
|
|
down: 'bg-red-500'
|
|
};
|
|
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white ${colors[status] || 'bg-gray-500'}`}>
|
|
{status.toUpperCase()}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const MetricCard = ({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
icon
|
|
}: {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle?: string;
|
|
icon?: string;
|
|
}) => (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-500">{title}</p>
|
|
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
|
{subtitle && <p className="text-sm text-gray-400">{subtitle}</p>}
|
|
</div>
|
|
{icon && <span className="text-3xl">{icon}</span>}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const SectionHeader = ({ title, subtitle }: { title: string; subtitle?: string }) => (
|
|
<div className="mb-4">
|
|
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
|
|
{subtitle && <p className="text-sm text-gray-500">{subtitle}</p>}
|
|
</div>
|
|
);
|
|
|
|
export default function TransparencyPortal() {
|
|
const [data, setData] = useState<DashboardData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
|
|
const fetchDashboard = async () => {
|
|
try {
|
|
const response = await fetch('/api/transparency/dashboard');
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
setData(result.data);
|
|
setLastUpdated(new Date());
|
|
setError(null);
|
|
} else {
|
|
setError(result.error || 'Failed to load dashboard');
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to connect to transparency API');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchDashboard();
|
|
|
|
// Auto-refresh every 30 seconds if enabled
|
|
let interval: NodeJS.Timeout;
|
|
if (autoRefresh) {
|
|
interval = setInterval(fetchDashboard, 30000);
|
|
}
|
|
|
|
return () => {
|
|
if (interval) clearInterval(interval);
|
|
};
|
|
}, [autoRefresh]);
|
|
|
|
const formatUptime = (ms: number) => {
|
|
const hours = Math.floor(ms / 3600000);
|
|
const minutes = Math.floor((ms % 3600000) / 60000);
|
|
return `${hours}h ${minutes}m`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Loading transparency data...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="text-red-500 text-4xl mb-4">!</div>
|
|
<p className="text-gray-600">{error}</p>
|
|
<button
|
|
onClick={fetchDashboard}
|
|
className="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>Transparency Portal | LocalGreenChain</title>
|
|
<meta name="description" content="Public transparency dashboard for LocalGreenChain operations" />
|
|
</Head>
|
|
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Header */}
|
|
<header className="bg-white shadow">
|
|
<div className="max-w-7xl mx-auto px-4 py-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900">
|
|
Transparency Portal
|
|
</h1>
|
|
<p className="text-gray-500">
|
|
Real-time visibility into LocalGreenChain operations
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
{data?.systemHealth && (
|
|
<StatusBadge status={data.systemHealth.status} />
|
|
)}
|
|
<label className="flex items-center space-x-2 text-sm text-gray-600">
|
|
<input
|
|
type="checkbox"
|
|
checked={autoRefresh}
|
|
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
className="rounded text-green-500"
|
|
/>
|
|
<span>Auto-refresh</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{lastUpdated && (
|
|
<p className="text-xs text-gray-400 mt-2">
|
|
Last updated: {lastUpdated.toLocaleTimeString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 py-8">
|
|
{/* System Health Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="System Health"
|
|
subtitle="Current status of all platform components"
|
|
/>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{data?.systemHealth.components && Object.entries(data.systemHealth.components).map(([name, component]) => (
|
|
<div key={name} className="bg-white rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium capitalize">{name}</span>
|
|
<StatusBadge status={component.status} />
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-2">
|
|
Errors (24h): {component.errorCount24h}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{data?.systemHealth && (
|
|
<p className="text-sm text-gray-500 mt-2">
|
|
Uptime: {formatUptime(data.systemHealth.uptime)}
|
|
</p>
|
|
)}
|
|
</section>
|
|
|
|
{/* Key Metrics Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Platform Metrics"
|
|
subtitle="Key operational statistics"
|
|
/>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<MetricCard
|
|
title="Total Plants Registered"
|
|
value={data?.plants.totalPlantsRegistered || 0}
|
|
subtitle={`${data?.plants.plantsRegisteredToday || 0} today`}
|
|
icon="🌱"
|
|
/>
|
|
<MetricCard
|
|
title="Blockchain Blocks"
|
|
value={data?.blockchain.totalBlocks || 0}
|
|
subtitle={data?.blockchain.chainValid ? 'Chain Valid' : 'Chain Invalid'}
|
|
icon="⛓️"
|
|
/>
|
|
<MetricCard
|
|
title="Audit Entries"
|
|
value={data?.audit.totalEntries || 0}
|
|
subtitle={`${data?.audit.entriesLast24h || 0} last 24h`}
|
|
icon="📋"
|
|
/>
|
|
<MetricCard
|
|
title="Active Events"
|
|
value={data?.events.totalEvents || 0}
|
|
subtitle={`${data?.events.activeWebhooks || 0} webhooks`}
|
|
icon="📡"
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Environmental Impact Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Environmental Impact"
|
|
subtitle="Sustainability metrics and environmental benefits"
|
|
/>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<MetricCard
|
|
title="Carbon Saved"
|
|
value={`${data?.environmental.totalCarbonSavedKg || 0} kg`}
|
|
subtitle="CO2 emissions prevented"
|
|
icon="🌍"
|
|
/>
|
|
<MetricCard
|
|
title="Water Saved"
|
|
value={`${data?.environmental.waterSavedLiters || 0} L`}
|
|
subtitle="Through efficient farming"
|
|
icon="💧"
|
|
/>
|
|
<MetricCard
|
|
title="Food Miles Reduced"
|
|
value={data?.environmental.foodMilesReduced || 0}
|
|
subtitle="Miles per delivery"
|
|
icon="🚗"
|
|
/>
|
|
<MetricCard
|
|
title="Sustainability Score"
|
|
value={`${data?.environmental.sustainabilityScore || 0}/100`}
|
|
subtitle="Platform impact rating"
|
|
icon="⭐"
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Plant Registry Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Plant Registry"
|
|
subtitle="Registered plants and varieties"
|
|
/>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="font-medium mb-4">Registration Stats</h3>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Total Plants</span>
|
|
<span className="font-medium">{data?.plants.totalPlantsRegistered || 0}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">This Week</span>
|
|
<span className="font-medium">{data?.plants.plantsRegisteredThisWeek || 0}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Total Clones</span>
|
|
<span className="font-medium">{data?.plants.totalClones || 0}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Avg Lineage Depth</span>
|
|
<span className="font-medium">{data?.plants.averageLineageDepth?.toFixed(1) || 0}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="font-medium mb-4">Top Varieties</h3>
|
|
{data?.plants.topVarieties && data.plants.topVarieties.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{data.plants.topVarieties.slice(0, 5).map((v, i) => (
|
|
<div key={i} className="flex justify-between items-center">
|
|
<span className="text-gray-600">{v.variety}</span>
|
|
<span className="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">
|
|
{v.count}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500">No varieties registered yet</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Blockchain Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Blockchain Status"
|
|
subtitle="Immutable ledger health and statistics"
|
|
/>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Total Blocks</p>
|
|
<p className="text-xl font-bold">{data?.blockchain.totalBlocks || 0}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500">Chain Integrity</p>
|
|
<p className="text-xl font-bold">
|
|
{data?.blockchain.chainValid ? (
|
|
<span className="text-green-600">Valid</span>
|
|
) : (
|
|
<span className="text-red-600">Invalid</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500">Difficulty</p>
|
|
<p className="text-xl font-bold">{data?.blockchain.difficulty || 4}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500">Last Block</p>
|
|
<p className="text-sm font-medium">
|
|
{data?.blockchain.lastBlockTime
|
|
? new Date(data.blockchain.lastBlockTime).toLocaleString()
|
|
: 'N/A'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Agents Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Autonomous Agents"
|
|
subtitle="AI agents monitoring and optimizing the platform"
|
|
/>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<p className="text-3xl font-bold text-green-600">{data?.agents.totalAgents || 10}</p>
|
|
<p className="text-sm text-gray-500">Total Agents</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-3xl font-bold text-blue-600">{data?.agents.activeAgents || 0}</p>
|
|
<p className="text-sm text-gray-500">Active</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-3xl font-bold text-gray-600">{data?.agents.totalTasksCompleted || 0}</p>
|
|
<p className="text-sm text-gray-500">Tasks Completed</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Alerts Section */}
|
|
{data?.alerts && data.alerts.length > 0 && (
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Active Alerts"
|
|
subtitle="Current system notifications"
|
|
/>
|
|
<div className="space-y-2">
|
|
{data.alerts.map(alert => (
|
|
<div
|
|
key={alert.id}
|
|
className={`p-4 rounded-lg ${
|
|
alert.type === 'critical' ? 'bg-red-100 border-l-4 border-red-500' :
|
|
alert.type === 'error' ? 'bg-red-50 border-l-4 border-red-400' :
|
|
alert.type === 'warning' ? 'bg-yellow-50 border-l-4 border-yellow-400' :
|
|
'bg-blue-50 border-l-4 border-blue-400'
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h4 className="font-medium">{alert.title}</h4>
|
|
<p className="text-sm text-gray-600">{alert.message}</p>
|
|
</div>
|
|
<span className="text-xs text-gray-400">
|
|
{new Date(alert.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Export Section */}
|
|
<section className="mb-8">
|
|
<SectionHeader
|
|
title="Data Export"
|
|
subtitle="Download transparency data for your records"
|
|
/>
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex flex-wrap gap-4">
|
|
<a
|
|
href="/api/transparency/export?type=dashboard&format=json"
|
|
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
|
download
|
|
>
|
|
Export Dashboard (JSON)
|
|
</a>
|
|
<a
|
|
href="/api/transparency/export?type=audit&format=csv"
|
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
download
|
|
>
|
|
Export Audit Log (CSV)
|
|
</a>
|
|
<a
|
|
href="/api/transparency/report?format=summary"
|
|
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
|
|
download
|
|
>
|
|
Generate Report
|
|
</a>
|
|
<a
|
|
href="/api/transparency/export?type=full&format=json"
|
|
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
|
|
download
|
|
>
|
|
Full Export
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Footer */}
|
|
<footer className="text-center text-gray-500 text-sm py-8">
|
|
<p>
|
|
LocalGreenChain Transparency Portal
|
|
</p>
|
|
<p className="mt-2">
|
|
Committed to open, verifiable, and trustworthy food systems
|
|
</p>
|
|
<p className="mt-2">
|
|
Data generated at: {data?.generatedAt ? new Date(data.generatedAt).toLocaleString() : 'N/A'}
|
|
</p>
|
|
</footer>
|
|
</main>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|