localgreenchain/pages/transparency.tsx
Claude 0fcc2763fe
Add comprehensive transparency system for LocalGreenChain
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
2025-11-23 03:29:56 +00:00

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>
</>
);
}