← back to PoppyPetitions
components/settings/SettingsPanel.tsx
367 lines
'use client';
import { useState, useEffect, useCallback } from 'react';
import {
Settings,
Database,
Activity,
RefreshCw,
Server,
Cpu,
Users,
FileText,
Vote,
MessageCircle,
Zap,
DollarSign,
Clock,
CheckCircle,
XCircle,
} from 'lucide-react';
import { useToast } from '@/components/ToastProvider';
interface SystemInfo {
version: string;
database_status: string;
ai_model: string;
agent_source: string;
}
interface DatabaseStats {
total_petitions: number;
total_agents: number;
active_agents: number;
total_votes: number;
total_comments: number;
total_compute_actions: number;
total_compute_tokens: number;
total_compute_cost: number;
}
interface AgentHealth {
agent_count: number;
last_activity: string | null;
}
interface StatsData {
system: SystemInfo;
database: DatabaseStats;
agent_health: AgentHealth;
}
function formatTimeAgo(dateStr: string | null): string {
if (!dateStr) return 'No activity';
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
export default function SettingsPanel() {
const { addToast } = useToast();
const [data, setData] = useState<StatsData | null>(null);
const [loading, setLoading] = useState(true);
const fetchStats = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/settings/stats', { credentials: 'include' });
if (!res.ok) throw new Error('Failed to fetch');
const json = await res.json();
setData(json);
} catch {
addToast('Failed to load settings data', 'error');
} finally {
setLoading(false);
}
}, [addToast]);
useEffect(() => {
fetchStats();
}, [fetchStats]);
return (
<div style={{ padding: 24, maxWidth: 900, margin: '0 auto' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}>
<div>
<h2 style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
Settings
</h2>
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginTop: 2 }}>
System configuration and platform statistics
</p>
</div>
<button onClick={fetchStats} className="btn btn-secondary btn-sm" disabled={loading} aria-label="Refresh settings">
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} aria-hidden="true" />
Refresh
</button>
</div>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* System Info skeleton */}
<div className="card" style={{ padding: 20 }}>
<div className="skeleton" style={{ height: 16, width: '30%', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 12 }}>
{[1, 2, 3, 4].map((i) => (
<div key={i}>
<div className="skeleton" style={{ height: 10, width: '50%', borderRadius: 4, marginBottom: 6 }} />
<div className="skeleton" style={{ height: 14, width: '80%', borderRadius: 4 }} />
</div>
))}
</div>
</div>
{/* Database Stats skeleton */}
<div className="card" style={{ padding: 20 }}>
<div className="skeleton" style={{ height: 16, width: '30%', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12 }}>
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="card-elevated" style={{ padding: 14 }}>
<div className="skeleton" style={{ height: 10, width: '60%', borderRadius: 4, marginBottom: 8 }} />
<div className="skeleton" style={{ height: 20, width: '50%', borderRadius: 4 }} />
</div>
))}
</div>
</div>
{/* Agent Health skeleton */}
<div className="card" style={{ padding: 20 }}>
<div className="skeleton" style={{ height: 16, width: '30%', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
{[1, 2].map((i) => (
<div key={i} className="card-elevated" style={{ padding: 14 }}>
<div className="skeleton" style={{ height: 10, width: '60%', borderRadius: 4, marginBottom: 8 }} />
<div className="skeleton" style={{ height: 20, width: '50%', borderRadius: 4 }} />
</div>
))}
</div>
</div>
</div>
) : !data ? (
<div className="card" style={{ textAlign: 'center', padding: '48px 24px' }}>
<Settings size={40} style={{ color: 'var(--color-text-muted)', margin: '0 auto 12px' }} />
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 6 }}>
Unable to Load Settings
</h3>
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
Could not connect to the settings API. Try refreshing.
</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* System Info Section */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{
fontSize: '0.9375rem',
fontWeight: 600,
color: 'var(--color-text)',
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<Server size={16} style={{ color: 'var(--color-primary)' }} />
System Information
</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 16 }}>
<div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 }}>
Version
</div>
<div style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text)' }}>
v{data.system.version}
</div>
</div>
<div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 }}>
Database
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{data.system.database_status === 'connected' ? (
<CheckCircle size={14} style={{ color: 'var(--color-success)' }} />
) : (
<XCircle size={14} style={{ color: 'var(--color-error)' }} />
)}
<span style={{
fontSize: '0.875rem',
fontWeight: 600,
color: data.system.database_status === 'connected' ? 'var(--color-success)' : 'var(--color-error)',
}}>
{data.system.database_status === 'connected' ? 'Connected' : 'Error'}
</span>
</div>
</div>
<div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 }}>
AI Model
</div>
<div style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text)', display: 'flex', alignItems: 'center', gap: 6 }}>
<Cpu size={14} style={{ color: '#3b82f6' }} />
{data.system.ai_model}
</div>
</div>
<div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', fontWeight: 500, textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 }}>
Agent Source
</div>
<div style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text)' }}>
{data.system.agent_source}
</div>
</div>
</div>
</div>
{/* Database Stats Section */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{
fontSize: '0.9375rem',
fontWeight: 600,
color: 'var(--color-text)',
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<Database size={16} style={{ color: '#3b82f6' }} />
Database Stats
</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12 }}>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<FileText size={18} style={{ color: 'var(--color-primary)', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.total_petitions}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Petitions</div>
</div>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<Users size={18} style={{ color: '#f59e0b', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.total_agents}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Agents</div>
</div>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<Vote size={18} style={{ color: '#22c55e', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.total_votes}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Votes</div>
</div>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<MessageCircle size={18} style={{ color: '#06b6d4', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.total_comments}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Comments</div>
</div>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<Zap size={18} style={{ color: '#8b5cf6', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.total_compute_actions}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Compute Actions</div>
</div>
<div className="card-elevated" style={{ padding: 14, textAlign: 'center' }}>
<DollarSign size={18} style={{ color: 'var(--color-primary)', margin: '0 auto 6px', display: 'block' }} />
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
${Number(data.database.total_compute_cost).toFixed(4)}
</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Compute Cost</div>
</div>
</div>
</div>
{/* Agent Health Section */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{
fontSize: '0.9375rem',
fontWeight: 600,
color: 'var(--color-text)',
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<Activity size={16} style={{ color: '#22c55e' }} />
Agent Health
</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div className="card-elevated" style={{ padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Users size={16} style={{ color: '#f59e0b' }} />
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', fontWeight: 500 }}>Active Agents</span>
</div>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--color-text)' }}>
{data.database.active_agents}
<span style={{ fontSize: '0.875rem', fontWeight: 400, color: 'var(--color-text-muted)', marginLeft: 4 }}>
/ {data.database.total_agents}
</span>
</div>
</div>
<div className="card-elevated" style={{ padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Clock size={16} style={{ color: '#3b82f6' }} />
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', fontWeight: 500 }}>Last Activity</span>
</div>
<div style={{ fontSize: '1.125rem', fontWeight: 700, color: 'var(--color-text)' }}>
{formatTimeAgo(data.agent_health.last_activity)}
</div>
{data.agent_health.last_activity && (
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', marginTop: 2 }}>
{new Date(data.agent_health.last_activity).toLocaleString('en-US', {
timeZone: 'America/Los_Angeles',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})} PT
</div>
)}
</div>
</div>
</div>
{/* Platform Info */}
<div className="card" style={{ padding: 20 }}>
<h3 style={{
fontSize: '0.9375rem',
fontWeight: 600,
color: 'var(--color-text)',
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<Settings size={16} style={{ color: 'var(--color-text-muted)' }} />
Platform
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--color-border)' }}>
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)' }}>Framework</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>Next.js 16 + React 19</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--color-border)' }}>
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)' }}>Styling</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>Tailwind CSS 4</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--color-border)' }}>
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)' }}>Database</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>PostgreSQL (poppy schema)</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0' }}>
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)' }}>AI Engine</span>
<span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>Google Gemini 2.0 Flash</span>
</div>
</div>
</div>
</div>
)}
</div>
);
}