← back to Hub
components/CommandCenterTab.tsx
479 lines
'use client';
import { useState, useEffect, useCallback } from 'react';
import { SkeletonList, SkeletonStats } from './Skeleton';
import {
LayoutDashboard,
Mail,
Award,
Megaphone,
DollarSign,
Activity,
Database,
Server,
ExternalLink,
RefreshCw,
Clock,
FileText,
Handshake,
Users,
Zap,
} from 'lucide-react';
import { useToast } from './ToastProvider';
interface AppStatus {
name: string;
port: number;
color: string;
status: 'online' | 'offline';
responseTime: number;
}
interface DashboardData {
sdcc: { drafts: number; news: number; petitions: number; collaborations: number; partners: number; leads: number; library: number };
grant: { grants: number; proposals: number; news: number; collaborations: number; total_funding: number };
patty: { petitions: number; signatures: number; campaigns: number; subscribers: number; templates: number };
freddy: { causes: number; organizations: number; granters: number; matches: number; contacts: number; revenue_total: number };
system: { apps_online: number; total_records: number };
}
interface ActivityItem {
id: string;
app: string;
type: string;
title: string;
description: string;
created_at: string;
}
const APP_ICONS: Record<string, React.ElementType> = {
SDCC: Mail,
Grant: Award,
Patty: Megaphone,
Freddy: DollarSign,
};
const TYPE_ICONS: Record<string, React.ElementType> = {
draft: FileText,
collaboration: Handshake,
grant: Award,
proposal: FileText,
petition: Megaphone,
campaign: Mail,
match: Zap,
contact: Users,
};
function formatTime(ts: string) {
const d = new Date(ts);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +
' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/Los_Angeles' });
}
export default function CommandCenterTab() {
const { addToast } = useToast();
const [statuses, setStatuses] = useState<AppStatus[]>([]);
const [dashboard, setDashboard] = useState<DashboardData | null>(null);
const [activities, setActivities] = useState<ActivityItem[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchAll = useCallback(async (isRefresh = false) => {
if (isRefresh) setRefreshing(true);
try {
const [statusRes, dashRes, actRes] = await Promise.all([
fetch('/api/apps/status', { credentials: 'include' }),
fetch('/api/dashboard', { credentials: 'include' }),
fetch('/api/activity', { credentials: 'include' }),
]);
if (statusRes.ok) {
const s = await statusRes.json();
setStatuses(s.apps || []);
}
if (dashRes.ok) {
const d = await dashRes.json();
setDashboard(d);
}
if (actRes.ok) {
const a = await actRes.json();
setActivities(a.items || []);
}
if (isRefresh) addToast('Dashboard refreshed', 'success');
} catch (err) {
console.error('Fetch error:', err);
if (isRefresh) addToast('Refresh failed', 'error');
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => { fetchAll(); }, [fetchAll]);
if (loading) {
return (
<div className="p-5 space-y-6">
<SkeletonStats count={4} />
<SkeletonList count={3} />
</div>
);
}
const onlineCount = statuses.filter(s => s.status === 'online').length;
return (
<div className="p-5 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>
Command Center
</h2>
<p className="text-sm mt-0.5" style={{ color: 'var(--color-text-muted)' }}>
SDCC Ecosystem Overview
</p>
</div>
<button
onClick={() => fetchAll(true)}
disabled={refreshing}
className="btn btn-secondary btn-sm"
>
<RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
Refresh
</button>
</div>
{/* App Status Grid */}
<div className="grid grid-cols-4 gap-4">
{statuses.map((app) => (
<div
key={app.name}
className="card"
style={{ borderLeft: `3px solid ${app.color}` }}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
{(() => {
const Icon = APP_ICONS[app.name] || Server;
return <Icon size={16} style={{ color: app.color }} />;
})()}
<span className="font-semibold text-sm" style={{ color: 'var(--color-text)' }}>
{app.name}
</span>
</div>
<div className="flex items-center gap-1.5">
<div
className={app.status === 'online' ? 'pulse-online' : ''}
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: app.status === 'online' ? '#22c55e' : '#ef4444',
}}
/>
<span
className="text-xs font-medium"
style={{ color: app.status === 'online' ? '#22c55e' : '#ef4444' }}
>
{app.status === 'online' ? 'Online' : 'Offline'}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
Port {app.port}
</span>
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{app.responseTime}ms
</span>
</div>
</div>
))}
</div>
{/* Aggregate Stats Row */}
<div className="grid grid-cols-4 gap-4">
<div className="stat-card">
<div className="flex items-center gap-2 mb-1">
<Database size={14} style={{ color: 'var(--color-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Total Records</span>
</div>
<div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
{dashboard?.system.total_records?.toLocaleString() || '0'}
</div>
</div>
<div className="stat-card">
<div className="flex items-center gap-2 mb-1">
<Server size={14} style={{ color: 'var(--color-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Apps Online</span>
</div>
<div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
{onlineCount}<span className="text-sm font-normal" style={{ color: 'var(--color-text-muted)' }}> / 4</span>
</div>
</div>
<div className="stat-card">
<div className="flex items-center gap-2 mb-1">
<LayoutDashboard size={14} style={{ color: 'var(--color-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Databases</span>
</div>
<div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>4</div>
</div>
<div className="stat-card">
<div className="flex items-center gap-2 mb-1">
<Activity size={14} style={{ color: 'var(--color-accent)' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-muted)' }}>Recent Activity</span>
</div>
<div className="text-2xl font-bold" style={{ color: 'var(--color-text)' }}>
{activities.length}
</div>
</div>
</div>
{/* App Cards — 2x2 grid */}
<div className="grid grid-cols-2 gap-4">
{/* SDCC Card */}
<AppCard
name="SDCC"
subtitle="Email Draft System"
color="#3b82f6"
port={7400}
Icon={Mail}
status={statuses.find(s => s.name === 'SDCC')?.status || 'offline'}
stats={dashboard ? [
{ label: 'Drafts', value: dashboard.sdcc.drafts },
{ label: 'News Items', value: dashboard.sdcc.news },
{ label: 'Petitions', value: dashboard.sdcc.petitions },
{ label: 'Collaborations', value: dashboard.sdcc.collaborations },
] : []}
/>
{/* Grant Card */}
<AppCard
name="Grant"
subtitle="Grant Discovery"
color="#059669"
port={7450}
Icon={Award}
status={statuses.find(s => s.name === 'Grant')?.status || 'offline'}
stats={dashboard ? [
{ label: 'Grants', value: dashboard.grant.grants },
{ label: 'Proposals', value: dashboard.grant.proposals },
{ label: 'News', value: dashboard.grant.news },
{ label: 'Collabs', value: dashboard.grant.collaborations },
] : []}
/>
{/* Patty Card */}
<AppCard
name="Patty"
subtitle="Petition Platform"
color="#7c3aed"
port={7460}
Icon={Megaphone}
status={statuses.find(s => s.name === 'Patty')?.status || 'offline'}
stats={dashboard ? [
{ label: 'Petitions', value: dashboard.patty.petitions },
{ label: 'Signatures', value: dashboard.patty.signatures },
{ label: 'Campaigns', value: dashboard.patty.campaigns },
{ label: 'Subscribers', value: dashboard.patty.subscribers },
] : []}
/>
{/* Freddy Card */}
<AppCard
name="Freddy"
subtitle="Fundraising Engine"
color="#d97706"
port={7470}
Icon={DollarSign}
status={statuses.find(s => s.name === 'Freddy')?.status || 'offline'}
stats={dashboard ? [
{ label: 'Causes', value: dashboard.freddy.causes },
{ label: 'Organizations', value: dashboard.freddy.organizations },
{ label: 'Matches', value: dashboard.freddy.matches },
{ label: 'Contacts', value: dashboard.freddy.contacts },
] : []}
/>
</div>
{/* Activity Feed */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Activity size={16} style={{ color: 'var(--color-accent)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
Recent Activity
</h3>
</div>
<div className="space-y-2">
{activities.slice(0, 10).map((item) => {
const TypeIcon = TYPE_ICONS[item.type] || Activity;
const appColor = item.app === 'sdcc' ? '#3b82f6' : item.app === 'grant' ? '#059669' : item.app === 'patty' ? '#7c3aed' : '#d97706';
return (
<div
key={item.id}
className="flex items-center gap-3 p-2 rounded-lg"
style={{ backgroundColor: 'var(--color-surface-el)' }}
>
<div
className="flex items-center justify-center shrink-0"
style={{
width: 28,
height: 28,
borderRadius: 6,
backgroundColor: `${appColor}18`,
}}
>
<TypeIcon size={14} style={{ color: appColor }} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span
className="badge"
style={{
backgroundColor: `${appColor}18`,
color: appColor,
border: `1px solid ${appColor}40`,
fontSize: 10,
padding: '1px 6px',
}}
>
{item.app.toUpperCase()}
</span>
<span className="text-sm font-medium truncate" style={{ color: 'var(--color-text)' }}>
{item.title}
</span>
</div>
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{item.description}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<Clock size={11} style={{ color: 'var(--color-text-muted)' }} />
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{formatTime(item.created_at)}
</span>
</div>
</div>
);
})}
{activities.length === 0 && (
<p className="text-sm text-center py-4" style={{ color: 'var(--color-text-muted)' }}>
No recent activity
</p>
)}
</div>
</div>
{/* Quick Actions */}
<div className="card">
<h3 className="text-sm font-semibold mb-3" style={{ color: 'var(--color-text)' }}>
Quick Actions
</h3>
<div className="flex gap-3">
{[
{ name: 'SDCC', port: 7400, color: '#3b82f6', Icon: Mail },
{ name: 'Grant', port: 7450, color: '#059669', Icon: Award },
{ name: 'Patty', port: 7460, color: '#7c3aed', Icon: Megaphone },
{ name: 'Freddy', port: 7470, color: '#d97706', Icon: DollarSign },
].map((app) => (
<a
key={app.name}
href={`http://45.61.58.125:${app.port}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-secondary flex-1"
style={{ borderColor: `${app.color}40` }}
>
<app.Icon size={14} style={{ color: app.color }} />
Open {app.name}
<ExternalLink size={12} style={{ color: 'var(--color-text-muted)' }} />
</a>
))}
</div>
</div>
</div>
);
}
/* ---- AppCard component --------------------------------------------------- */
function AppCard({
name,
subtitle,
color,
port,
Icon,
status,
stats,
}: {
name: string;
subtitle: string;
color: string;
port: number;
Icon: React.ElementType;
status: 'online' | 'offline';
stats: { label: string; value: number }[];
}) {
return (
<div
className="card"
style={{ borderTop: `3px solid ${color}` }}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div
style={{
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: `${color}18`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon size={18} style={{ color }} />
</div>
<div>
<div className="font-semibold text-sm" style={{ color: 'var(--color-text)' }}>{name}</div>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{subtitle}</div>
</div>
</div>
<div className="flex items-center gap-2">
<div
className={status === 'online' ? 'pulse-online' : ''}
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: status === 'online' ? '#22c55e' : '#ef4444',
}}
/>
<a
href={`http://45.61.58.125:${port}`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost btn-sm"
style={{ color }}
>
Open <ExternalLink size={12} />
</a>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-lg p-2.5"
style={{ backgroundColor: 'var(--color-surface-el)' }}
>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{stat.label}</div>
<div className="text-lg font-bold mt-0.5" style={{ color: 'var(--color-text)' }}>
{stat.value.toLocaleString()}
</div>
</div>
))}
</div>
</div>
);
}