import { useMemo, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useProxyHosts } from '../hooks/useProxyHosts' import { useRemoteServers } from '../hooks/useRemoteServers' import { useCertificates } from '../hooks/useCertificates' import { useAccessLists } from '../hooks/useAccessLists' import { useQuery, useQueryClient } from '@tanstack/react-query' import { checkHealth } from '../api/health' import { Globe, Server, FileKey, Activity, CheckCircle2, AlertTriangle } from 'lucide-react' import { PageShell } from '../components/layout/PageShell' import { StatsCard, Skeleton } from '../components/ui' import UptimeWidget from '../components/UptimeWidget' function StatsCardSkeleton() { return (
) } export default function Dashboard() { const { t } = useTranslation() const { hosts, loading: hostsLoading } = useProxyHosts() const { servers, loading: serversLoading } = useRemoteServers() const { data: accessLists, isLoading: accessListsLoading } = useAccessLists() const queryClient = useQueryClient() // Fetch certificates (polling interval managed via effect below) const { certificates, isLoading: certificatesLoading } = useCertificates() // Build set of certified domains for pending detection // ACME certificates (Let's Encrypt) are auto-managed and don't set certificate_id, // so we match by domain name instead const hasPendingCerts = useMemo(() => { const certifiedDomains = new Set() certificates.forEach(cert => { // Handle missing or undefined domain field if (!cert.domain) return cert.domain.split(',').forEach(d => { const trimmed = d.trim().toLowerCase() if (trimmed) certifiedDomains.add(trimmed) }) }) // Check if any SSL host lacks a certificate const sslHosts = hosts.filter(h => h.ssl_forced && h.enabled) return sslHosts.some(host => { const hostDomains = host.domain_names.split(',').map(d => d.trim().toLowerCase()) return !hostDomains.some(domain => certifiedDomains.has(domain)) }) }, [hosts, certificates]) // Poll certificates every 15s when there are pending certs useEffect(() => { if (!hasPendingCerts) return const interval = setInterval(() => { queryClient.invalidateQueries({ queryKey: ['certificates'] }) }, 15000) return () => clearInterval(interval) }, [hasPendingCerts, queryClient]) // Use React Query for health check - benefits from global caching const { data: health, isLoading: healthLoading } = useQuery({ queryKey: ['health'], queryFn: checkHealth, staleTime: 1000 * 60, // 1 minute for health checks refetchInterval: 1000 * 60, // Auto-refresh every minute }) const enabledHosts = hosts.filter(h => h.enabled).length const enabledServers = servers.filter(s => s.enabled).length const enabledAccessLists = accessLists?.filter(a => a.enabled).length ?? 0 const validCertificates = certificates.filter(c => c.status === 'valid').length const isInitialLoading = hostsLoading || serversLoading || accessListsLoading || certificatesLoading return ( {/* Stats Grid */}
{isInitialLoading ? ( <> ) : ( <> } href="/proxy-hosts" change={enabledHosts > 0 ? { value: Math.round((enabledHosts / hosts.length) * 100) || 0, trend: 'neutral', label: t('common.enabledCount', { count: enabledHosts }), } : undefined} /> } href="/certificates" change={validCertificates > 0 ? { value: Math.round((validCertificates / certificates.length) * 100) || 0, trend: 'neutral', label: t('common.validCount', { count: validCertificates }), } : undefined} /> } href="/remote-servers" change={enabledServers > 0 ? { value: Math.round((enabledServers / servers.length) * 100) || 0, trend: 'neutral', label: t('common.enabledCount', { count: enabledServers }), } : undefined} /> } href="/access-lists" change={enabledAccessLists > 0 ? { value: Math.round((enabledAccessLists / (accessLists?.length ?? 1)) * 100) || 0, trend: 'neutral', label: t('common.activeCount', { count: enabledAccessLists }), } : undefined} /> ) : health?.status === 'ok' ? ( ) : ( ) } /> )}
{/* Uptime Widget */}
) }