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