← back to Hub
components/SettingsTab.tsx
179 lines
'use client';
import { useState, useEffect } from 'react';
import { SkeletonList } from './Skeleton';
import { Settings, Server, Database, RefreshCw, ExternalLink, Globe, Shield } from 'lucide-react';
interface AppStatus {
name: string;
port: number;
color: string;
status: 'online' | 'offline';
responseTime: number;
}
interface DashboardData {
sdcc: Record<string, number>;
grant: Record<string, number>;
patty: Record<string, number>;
freddy: Record<string, number>;
system: { apps_online: number; total_records: number };
}
export default function SettingsTab() {
const [statuses, setStatuses] = useState<AppStatus[]>([]);
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
async function fetchData() {
try {
const [sRes, dRes] = await Promise.all([
fetch('/api/apps/status', { credentials: 'include' }),
fetch('/api/dashboard', { credentials: 'include' }),
]);
if (sRes.ok) { const s = await sRes.json(); setStatuses(s.apps || []); }
if (dRes.ok) setData(await dRes.json());
} catch (err) {
console.error('Settings fetch error:', err);
} finally {
setLoading(false);
}
}
useEffect(() => { fetchData(); }, []);
if (loading) {
return <div className="p-5"><SkeletonList count={4} /></div>;
}
const apps = [
{ name: 'SDCC', port: 7400, db: 'sdcc', color: '#3b82f6', description: 'Email Draft System' },
{ name: 'Grant', port: 7450, db: 'grant_app', color: '#059669', description: 'Grant Discovery & Proposals' },
{ name: 'Patty', port: 7460, db: 'patty', color: '#7c3aed', description: 'Petition Platform' },
{ name: 'Freddy', port: 7470, db: 'freddy', color: '#d97706', description: 'Fundraising Engine' },
{ name: 'Hub', port: 7480, db: 'all', color: '#0891b2', description: 'Command Center (this app)' },
];
return (
<div className="p-5 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Settings size={20} style={{ color: 'var(--color-accent)' }} />
<div>
<h2 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>Settings</h2>
<p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>Hub configuration and system info</p>
</div>
</div>
<button onClick={() => { setLoading(true); fetchData(); }} className="btn btn-secondary btn-sm">
<RefreshCw size={14} /> Refresh
</button>
</div>
{/* App URLs */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Globe size={16} style={{ color: 'var(--color-accent)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>App URLs</h3>
</div>
<div className="space-y-2">
{apps.map((app) => {
const status = statuses.find(s => s.name === app.name);
return (
<div key={app.name} className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
<div className="flex items-center gap-3">
<div
style={{
width: 8, height: 8, borderRadius: '50%',
backgroundColor: status?.status === 'online' ? '#22c55e' : app.name === 'Hub' ? '#22c55e' : '#ef4444',
}}
/>
<div>
<div className="text-sm font-medium" style={{ color: app.color }}>{app.name}</div>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{app.description}</div>
</div>
</div>
<div className="flex items-center gap-3">
<code className="text-xs px-2 py-0.5 rounded" style={{ backgroundColor: 'var(--color-bg)', color: 'var(--color-text-secondary)' }}>
http://45.61.58.125:{app.port}
</code>
{app.name !== 'Hub' && (
<a href={`http://45.61.58.125:${app.port}`} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
<ExternalLink size={12} />
</a>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Database Info */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Database size={16} style={{ color: 'var(--color-accent)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Database Overview</h3>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12 }}>Database</th>
<th style={{ textAlign: 'left', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12 }}>Tables</th>
<th style={{ textAlign: 'right', padding: '8px 12px', borderBottom: '1px solid var(--color-border)', color: 'var(--color-text-muted)', fontSize: 12 }}>Record Counts</th>
</tr>
</thead>
<tbody>
{data && [
{ db: 'sdcc', color: '#3b82f6', counts: data.sdcc },
{ db: 'grant_app', color: '#059669', counts: data.grant },
{ db: 'patty', color: '#7c3aed', counts: data.patty },
{ db: 'freddy', color: '#d97706', counts: data.freddy },
].map(({ db, color, counts }) => (
<tr key={db}>
<td style={{ padding: '8px 12px', borderBottom: '1px solid var(--color-border)', fontWeight: 600, fontSize: 13, color }}>
{db}
</td>
<td style={{ padding: '8px 12px', borderBottom: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text-secondary)' }}>
{Object.keys(counts).filter(k => k !== 'total_funding' && k !== 'revenue_total').join(', ')}
</td>
<td style={{ padding: '8px 12px', borderBottom: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text)', textAlign: 'right', fontWeight: 600 }}>
{Object.entries(counts)
.filter(([k]) => k !== 'total_funding' && k !== 'revenue_total')
.reduce((sum, [, v]) => sum + (typeof v === 'number' ? v : 0), 0)
.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* System Info */}
<div className="card">
<div className="flex items-center gap-2 mb-4">
<Shield size={16} style={{ color: 'var(--color-accent)' }} />
<h3 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>System Information</h3>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Hub Port', value: '7480' },
{ label: 'Auth Cookie', value: 'hub-auth' },
{ label: 'Health Check', value: 'Every 30s (Activity)' },
{ label: 'PM2 Process', value: 'hub-command' },
{ label: 'Stack', value: 'Next.js 16 + Tailwind 4 + PostgreSQL' },
{ label: 'Theme', value: 'Cyan/Teal Dark' },
].map((item) => (
<div key={item.label} className="flex items-center justify-between p-2 rounded-lg" style={{ backgroundColor: 'var(--color-surface-el)' }}>
<span className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{item.label}</span>
<span className="text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }}>{item.value}</span>
</div>
))}
</div>
</div>
</div>
);
}