Show routes, TLS certs, and port-forwarding on services page

- Add Routes model to services UI (paths, headers, IP whitelisting per route)
- Show TLS cert info per service with inline provision/renew actions
- Remove TLS Certificates section from dashboard (now on services page)
- Make gateway router port list dynamic from config + VPN state
- Add TODO for header validation in HAProxy config generation
This commit is contained in:
2026-07-10 06:10:40 +00:00
parent e78a1d548a
commit 68d6fde80d
9 changed files with 570 additions and 1072 deletions

View File

@@ -7,13 +7,13 @@ import { Alert, AlertDescription } from './ui/alert';
import { Badge } from './ui/badge';
import {
LayoutDashboard, Cloud, CheckCircle, XCircle, AlertCircle, Loader2,
RefreshCw, BookOpen, Globe, Shield, Router, Server, RotateCw, Key, Plus, X,
RefreshCw, BookOpen, Globe, Router, Server, RotateCw, Key, Plus, X,
} from 'lucide-react';
import { useCloudflare } from '../hooks/useCloudflare';
import { useDdns } from '../hooks/useDdns';
import { useCentralStatus } from '../hooks/useCentralStatus';
import { useCert } from '../hooks/useCert';
import { useConfig } from '../hooks';
import { useVpn } from '../hooks/useVpn';
import { secretsApi } from '../services/api';
import { usePageHelp } from '../hooks/usePageHelp';
import type { DdnsRecordStatus } from '../services/api/networking';
@@ -22,8 +22,8 @@ export function DashboardComponent() {
const { data: centralStatus, isLoading: statusLoading, restart: restartDaemon, isRestarting } = useCentralStatus();
const { verification, isLoading: cfLoading } = useCloudflare();
const { status: ddnsStatus, isLoadingStatus: ddnsLoading, trigger: triggerDdns, isTriggering } = useDdns();
const { status: certStatus } = useCert();
const { config: globalConfig, updateConfig } = useConfig();
const { config: vpnConfig } = useVpn();
const queryClient = useQueryClient();
const { data: globalSecrets } = useQuery({
@@ -52,10 +52,8 @@ export function DashboardComponent() {
// Warning conditions
const cfTokenMissing = !cfLoading && !verification?.tokenConfigured;
const cfTokenInvalid = !cfLoading && verification?.tokenConfigured && !verification?.tokenValid;
const certExpiring = certStatus?.certs?.some(c => c.cert.exists && (c.cert.daysLeft ?? 999) < 14);
const certMissing = certStatus?.certs?.some(c => !c.cert.exists);
const ddnsFailed = ddnsStatus?.enabled && ddnsStatus?.lastError;
const hasWarnings = cfTokenMissing || cfTokenInvalid || certExpiring || certMissing || ddnsFailed;
const hasWarnings = cfTokenMissing || cfTokenInvalid || ddnsFailed;
usePageHelp({
title: 'What is the Dashboard?',
@@ -103,18 +101,6 @@ export function DashboardComponent() {
<AlertDescription>Cloudflare API token is invalid. Check your token in Cloudflare settings.</AlertDescription>
</Alert>
)}
{certMissing && (
<Alert variant="warning">
<AlertCircle className="h-4 w-4" />
<AlertDescription>One or more TLS certificates are missing. Services may not be reachable over HTTPS.</AlertDescription>
</Alert>
)}
{certExpiring && !certMissing && (
<Alert variant="warning">
<AlertCircle className="h-4 w-4" />
<AlertDescription>A TLS certificate expires within 14 days. Consider renewing soon.</AlertDescription>
</Alert>
)}
{ddnsFailed && (
<Alert variant="error">
<AlertCircle className="h-4 w-4" />
@@ -426,62 +412,39 @@ export function DashboardComponent() {
)}
</Card>
{/* 4. TLS Certificates */}
{certStatus?.certs && certStatus.certs.length > 0 && (
<Card className="p-4 border-l-4 border-l-blue-500">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
<div className="font-medium">TLS Certificates</div>
</div>
{(() => {
const allExist = certStatus.certs!.every(c => c.cert.exists);
const someExist = certStatus.certs!.some(c => c.cert.exists);
if (allExist) {
return <Badge variant="success" className="gap-1"><CheckCircle className="h-3 w-3" />All valid</Badge>;
} else if (someExist) {
return <Badge variant="warning" className="gap-1"><AlertCircle className="h-3 w-3" />Partial</Badge>;
} else {
return <Badge variant="destructive" className="gap-1"><XCircle className="h-3 w-3" />Missing</Badge>;
}
})()}
</div>
<div className="space-y-2 ml-7">
{certStatus.certs!.map((entry) => (
<div key={entry.domain} className="flex items-center justify-between text-sm">
<span className="font-mono">{entry.domain}</span>
{entry.cert.exists ? (
<span className="text-xs text-green-600 dark:text-green-400">
Valid ({entry.cert.daysLeft}d)
</span>
) : (
<span className="text-xs text-red-500">Missing</span>
)}
</div>
))}
</div>
</Card>
)}
{/* 5. Gateway Router */}
{/* 4. Gateway Router */}
<Card className="p-4 border-l-4 border-l-amber-500 bg-gradient-to-r from-amber-50/50 to-orange-50/50 dark:from-amber-950/10 dark:to-orange-950/10">
<div className="flex items-center gap-2 mb-3">
<Router className="h-5 w-5 text-amber-500" />
<div className="font-medium">Gateway Router</div>
</div>
<div className="space-y-2 ml-7 text-sm text-muted-foreground">
<p>Ensure your router is configured for Wild Central to work correctly:</p>
<p>Your LAN router should be configured to work with Wild Central:</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>
Forward DNS to Wild Central
Set the router's DNS server to Wild Central
{globalConfig?.cloud?.dnsmasq?.ip && (
<span className="font-mono bg-muted px-1.5 py-0.5 rounded ml-1">
{globalConfig.cloud.dnsmasq.ip}
</span>
)}
</li>
<li>Forward ports <span className="font-mono">443</span>, <span className="font-mono">80</span>, <span className="font-mono">51820</span> to Wild Central</li>
<li>Set Wild Central as the primary DNS server for your LAN</li>
<li>
Port-forward to Wild Central:{' '}
{[
{ port: 443, proto: 'TCP', label: 'HTTPS' },
{ port: 80, proto: 'TCP', label: 'HTTP' },
...(vpnConfig?.enabled ? [{ port: vpnConfig.listenPort || 51820, proto: 'UDP', label: 'VPN' }] : []),
...((globalConfig?.cloud?.nftables?.extraPorts ?? []) as { port: number; protocol?: string; label?: string }[])
.map(ep => ({ port: ep.port, proto: (ep.protocol || 'tcp').toUpperCase(), label: ep.label || '' })),
].map((p, i) => (
<span key={i}>
{i > 0 && ', '}
<span className="font-mono">{p.port}</span>/{p.proto}
{p.label && <span className="text-muted-foreground/60"> ({p.label})</span>}
</span>
))}
</li>
</ul>
</div>
</Card>