Support custom domains. Fix host-record/address resolution.
This commit is contained in:
@@ -104,6 +104,7 @@ export function AppSidebar() {
|
||||
{ to: '/advanced/wireguard', icon: Lock, label: 'WireGuard', daemon: 'wireguard' as const },
|
||||
{ to: '/advanced/crowdsec', icon: ShieldAlert, label: 'CrowdSec', daemon: 'crowdsec' as const },
|
||||
{ to: '/advanced/authelia', icon: KeyRound, label: 'Authelia', daemon: 'authelia' as const },
|
||||
{ to: '/advanced/ddns', icon: Globe, label: 'DDNS', daemon: undefined },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
160
web/src/components/advanced/DdnsSubsystem.tsx
Normal file
160
web/src/components/advanced/DdnsSubsystem.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Loader2, CheckCircle, AlertCircle, RefreshCw, Globe } from 'lucide-react';
|
||||
import { useDdns } from '../../hooks/useDdns';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ddnsConfigApi } from '../../services/api/settings';
|
||||
|
||||
function formatTime(iso?: string): string {
|
||||
if (!iso) return '--';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '--';
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
export function DdnsSubsystem() {
|
||||
const { status, isLoadingStatus, trigger, isTriggering } = useDdns();
|
||||
|
||||
const configQuery = useQuery({
|
||||
queryKey: ['ddns', 'config'],
|
||||
queryFn: () => ddnsConfigApi.get(),
|
||||
});
|
||||
|
||||
const config = configQuery.data;
|
||||
const records = status?.records ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Globe className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Dynamic DNS</h2>
|
||||
<p className="text-muted-foreground">Cloudflare DNS records managed by Wild Central</p>
|
||||
</div>
|
||||
</div>
|
||||
{status && (
|
||||
<Badge variant={status.enabled ? 'success' : 'secondary'} className="gap-1">
|
||||
{status.enabled ? <CheckCircle className="h-3 w-3" /> : <AlertCircle className="h-3 w-3" />}
|
||||
{status.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Global error */}
|
||||
{status?.lastError && (
|
||||
<Alert variant="error">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{status.lastError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Status + Actions */}
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm flex-1">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Public IP</span>
|
||||
<div className="font-mono mt-0.5">{status?.currentIP ?? '--'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Provider</span>
|
||||
<div className="font-mono mt-0.5">{config?.provider ?? '--'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last Checked</span>
|
||||
<div className="font-mono mt-0.5 text-xs">{formatTime(status?.lastChecked)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last Updated</span>
|
||||
<div className="font-mono mt-0.5 text-xs">{formatTime(status?.lastUpdated)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1 ml-4 shrink-0"
|
||||
onClick={() => trigger()}
|
||||
disabled={isTriggering || !status?.enabled}
|
||||
>
|
||||
{isTriggering ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
|
||||
Sync Now
|
||||
</Button>
|
||||
</div>
|
||||
{config?.intervalMinutes && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Checks every {config.intervalMinutes} minute{config.intervalMinutes !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Records table */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-medium mb-3">Managed Records</h3>
|
||||
{isLoadingStatus ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 text-primary animate-spin" />
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Globe className="h-10 w-10 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{status?.enabled
|
||||
? 'No public domains registered. Mark a domain as public to create a DDNS record.'
|
||||
: 'DDNS is disabled. Enable it in the Domains settings to manage DNS records.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left">
|
||||
<th className="pb-2 font-medium text-muted-foreground">Record</th>
|
||||
<th className="pb-2 font-medium text-muted-foreground">IP</th>
|
||||
<th className="pb-2 font-medium text-muted-foreground text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((r) => (
|
||||
<tr key={r.name} className="border-b last:border-0">
|
||||
<td className="py-2 font-mono text-xs">{r.name}</td>
|
||||
<td className="py-2 font-mono text-xs">{r.ip ?? '--'}</td>
|
||||
<td className="py-2 text-right">
|
||||
{r.ok ? (
|
||||
<Badge variant="success" className="gap-1 text-xs">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Synced
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Badge variant="destructive" className="gap-1 text-xs">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
Error
|
||||
</Badge>
|
||||
{r.error && (
|
||||
<span className="text-xs text-muted-foreground max-w-[200px] truncate" title={r.error}>
|
||||
{r.error}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export { NftablesSubsystem } from './NftablesSubsystem';
|
||||
export { WireguardSubsystem } from './WireguardSubsystem';
|
||||
export { CrowdsecSubsystem } from './CrowdsecSubsystem';
|
||||
export { AutheliaSubsystem } from './AutheliaSubsystem';
|
||||
export { DdnsSubsystem } from './DdnsSubsystem';
|
||||
|
||||
10
web/src/router/pages/advanced/DdnsPage.tsx
Normal file
10
web/src/router/pages/advanced/DdnsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../../components';
|
||||
import { DdnsSubsystem } from '../../../components/advanced';
|
||||
|
||||
export function DdnsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DdnsSubsystem />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export { NftablesPage } from './NftablesPage';
|
||||
export { WireguardPage } from './WireguardPage';
|
||||
export { CrowdsecPage } from './CrowdsecPage';
|
||||
export { AutheliaAdvancedPage } from './AutheliaPage';
|
||||
export { DdnsPage } from './DdnsPage';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
HaproxyPage, DnsmasqPage, NftablesPage, WireguardPage,
|
||||
CrowdsecPage as CrowdsecAdvancedPage,
|
||||
AutheliaAdvancedPage,
|
||||
DdnsPage,
|
||||
} from './pages/advanced';
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
@@ -34,6 +35,7 @@ export const routes: RouteObject[] = [
|
||||
{ path: 'advanced/wireguard', element: <WireguardPage /> },
|
||||
{ path: 'advanced/crowdsec', element: <CrowdsecAdvancedPage /> },
|
||||
{ path: 'advanced/authelia', element: <AutheliaAdvancedPage /> },
|
||||
{ path: 'advanced/ddns', element: <DdnsPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user