Replace state blob with resource-oriented API endpoints
Split /api/v1/state into dedicated resource endpoints: - /api/v1/operator, /api/v1/central-domain - /api/v1/ddns/config, /api/v1/dns/settings - /api/v1/dhcp/config (moved from /dnsmasq/dhcp/) - /api/v1/nftables/config, /api/v1/haproxy/routes Each page now talks to its own resource instead of deep-merging a blob. Removes useConfig hook, CentralState type, and legacy config methods.
This commit is contained in:
@@ -12,9 +12,8 @@ import {
|
||||
import { useCloudflare } from '../hooks/useCloudflare';
|
||||
import { useDdns } from '../hooks/useDdns';
|
||||
import { useCentralStatus } from '../hooks/useCentralStatus';
|
||||
import { useConfig } from '../hooks';
|
||||
import { useVpn } from '../hooks/useVpn';
|
||||
import { secretsApi } from '../services/api';
|
||||
import { secretsApi, dnsSettingsApi, nftablesConfigApi } from '../services/api';
|
||||
import { usePageHelp } from '../hooks/usePageHelp';
|
||||
import type { DdnsRecordStatus } from '../services/api/networking';
|
||||
|
||||
@@ -22,8 +21,9 @@ 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 { config: globalConfig } = useConfig();
|
||||
const { config: vpnConfig } = useVpn();
|
||||
const { data: dnsSettings } = useQuery({ queryKey: ['dns', 'settings'], queryFn: () => dnsSettingsApi.get() });
|
||||
const { data: nftConfig } = useQuery({ queryKey: ['nftables', 'config'], queryFn: () => nftablesConfigApi.get() });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: globalSecrets } = useQuery({
|
||||
@@ -379,9 +379,9 @@ export function DashboardComponent() {
|
||||
<ul className="list-disc list-inside space-y-1 text-xs">
|
||||
<li>
|
||||
Set the router's DNS server to Wild Central
|
||||
{globalConfig?.cloud?.dnsmasq?.ip && (
|
||||
{dnsSettings?.ip && (
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded ml-1">
|
||||
{globalConfig.cloud.dnsmasq.ip}
|
||||
{dnsSettings.ip}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
@@ -391,7 +391,7 @@ export function DashboardComponent() {
|
||||
{ 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 }[])
|
||||
...((nftConfig?.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}>
|
||||
|
||||
@@ -5,13 +5,20 @@ import { Input, Label } from './ui';
|
||||
import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Wifi, Loader2, Edit2, Check, X, Trash2, Plus, RotateCw, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import { Badge } from './ui/badge';
|
||||
import { useConfig } from '../hooks';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useDhcp } from '../hooks/useDhcp';
|
||||
import { useDnsmasq } from '../hooks/useDnsmasq';
|
||||
import { dhcpConfigApi, type DHCPConfig } from '../services/api/settings';
|
||||
import type { DhcpStaticLease } from '../services/api/networking';
|
||||
|
||||
export function DhcpComponent() {
|
||||
const { config: globalConfig, updateConfig, isUpdating } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: dhcpConfig } = useQuery({ queryKey: ['dhcp', 'config'], queryFn: () => dhcpConfigApi.get() });
|
||||
const dhcpMutation = useMutation({
|
||||
mutationFn: (data: Partial<DHCPConfig>) => dhcpConfigApi.update(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dhcp', 'config'] }),
|
||||
});
|
||||
const isUpdating = dhcpMutation.isPending;
|
||||
const { leases, isLoadingLeases, refetchLeases, addStatic, isAddingStatic, deleteStatic } = useDhcp();
|
||||
const { generateConfig: generateDnsConfig } = useDnsmasq();
|
||||
|
||||
@@ -36,33 +43,23 @@ export function DhcpComponent() {
|
||||
|
||||
const handleDhcpConfigEdit = () => {
|
||||
setDhcpConfigForm({
|
||||
enabled: globalConfig?.cloud?.dnsmasq?.dhcp?.enabled ?? false,
|
||||
rangeStart: globalConfig?.cloud?.dnsmasq?.dhcp?.rangeStart ?? '',
|
||||
rangeEnd: globalConfig?.cloud?.dnsmasq?.dhcp?.rangeEnd ?? '',
|
||||
leaseTime: globalConfig?.cloud?.dnsmasq?.dhcp?.leaseTime ?? '24h',
|
||||
gateway: globalConfig?.cloud?.dnsmasq?.dhcp?.gateway ?? '',
|
||||
enabled: dhcpConfig?.enabled ?? false,
|
||||
rangeStart: dhcpConfig?.rangeStart ?? '',
|
||||
rangeEnd: dhcpConfig?.rangeEnd ?? '',
|
||||
leaseTime: dhcpConfig?.leaseTime ?? '24h',
|
||||
gateway: dhcpConfig?.gateway ?? '',
|
||||
});
|
||||
setEditingDhcpConfig(true);
|
||||
};
|
||||
|
||||
const handleDhcpConfigSave = async () => {
|
||||
if (!globalConfig) return;
|
||||
try {
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: {
|
||||
...globalConfig.cloud,
|
||||
dnsmasq: {
|
||||
...globalConfig.cloud?.dnsmasq,
|
||||
dhcp: {
|
||||
enabled: dhcpConfigForm.enabled,
|
||||
rangeStart: dhcpConfigForm.rangeStart,
|
||||
rangeEnd: dhcpConfigForm.rangeEnd,
|
||||
leaseTime: dhcpConfigForm.leaseTime,
|
||||
gateway: dhcpConfigForm.gateway || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
await dhcpMutation.mutateAsync({
|
||||
enabled: dhcpConfigForm.enabled,
|
||||
rangeStart: dhcpConfigForm.rangeStart,
|
||||
rangeEnd: dhcpConfigForm.rangeEnd,
|
||||
leaseTime: dhcpConfigForm.leaseTime,
|
||||
gateway: dhcpConfigForm.gateway || undefined,
|
||||
});
|
||||
setEditingDhcpConfig(false);
|
||||
if (dhcpConfigForm.enabled) {
|
||||
@@ -104,9 +101,9 @@ export function DhcpComponent() {
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Configuration</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={globalConfig?.cloud?.dnsmasq?.dhcp?.enabled ? 'success' : 'secondary'} className="gap-1">
|
||||
{globalConfig?.cloud?.dnsmasq?.dhcp?.enabled && <CheckCircle className="h-3 w-3" />}
|
||||
{globalConfig?.cloud?.dnsmasq?.dhcp?.enabled ? 'Enabled' : 'Disabled'}
|
||||
<Badge variant={dhcpConfig?.enabled ? 'success' : 'secondary'} className="gap-1">
|
||||
{dhcpConfig?.enabled && <CheckCircle className="h-3 w-3" />}
|
||||
{dhcpConfig?.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
{!editingDhcpConfig && (
|
||||
<Button variant="outline" size="sm" onClick={handleDhcpConfigEdit} className="gap-1 h-7 text-xs">
|
||||
@@ -189,24 +186,24 @@ export function DhcpComponent() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 text-sm">
|
||||
{globalConfig?.cloud?.dnsmasq?.dhcp?.enabled ? (
|
||||
{dhcpConfig?.enabled ? (
|
||||
<>
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Range: </span>
|
||||
<span className="font-mono">
|
||||
{globalConfig.cloud.dnsmasq.dhcp.rangeStart} – {globalConfig.cloud.dnsmasq.dhcp.rangeEnd}
|
||||
{dhcpConfig!.rangeStart} – {dhcpConfig!.rangeEnd}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Lease: </span>
|
||||
<span className="font-mono">{globalConfig.cloud.dnsmasq.dhcp.leaseTime || '24h'}</span>
|
||||
<span className="font-mono">{dhcpConfig!.leaseTime || '24h'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{globalConfig.cloud.dnsmasq.dhcp.gateway && (
|
||||
{dhcpConfig!.gateway && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Gateway: </span>
|
||||
<span className="font-mono">{globalConfig.cloud.dnsmasq.dhcp.gateway}</span>
|
||||
<span className="font-mono">{dhcpConfig!.gateway}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,9 +7,10 @@ import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Shield, Loader2, AlertCircle, CheckCircle, Check, X, Plus, Trash2, BookOpen, ChevronDown, ChevronUp, Lock } from 'lucide-react';
|
||||
import { Badge } from './ui/badge';
|
||||
import { useConfig } from '../hooks';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNftables } from '../hooks/useNftables';
|
||||
import { useVpn } from '../hooks/useVpn';
|
||||
import { nftablesConfigApi, haproxyRoutesApi } from '../services/api/settings';
|
||||
|
||||
type ExtraPort = { port: number; protocol?: string; label?: string };
|
||||
|
||||
@@ -38,15 +39,20 @@ interface PortEntry {
|
||||
}
|
||||
|
||||
export function FirewallComponent() {
|
||||
const { config: globalConfig, updateConfig } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: nftCfg } = useQuery({ queryKey: ['nftables', 'config'], queryFn: () => nftablesConfigApi.get() });
|
||||
const { data: haproxyRoutes } = useQuery({ queryKey: ['haproxy', 'routes'], queryFn: () => haproxyRoutesApi.get() });
|
||||
const nftMutation = useMutation({
|
||||
mutationFn: (data: Parameters<typeof nftablesConfigApi.update>[0]) => nftablesConfigApi.update(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['nftables', 'config'] }),
|
||||
});
|
||||
const { status: nftablesStatus, isLoadingStatus: isNftablesLoading, interfaces, refetchStatus } = useNftables();
|
||||
const { config: vpnConfig } = useVpn();
|
||||
|
||||
const nftCfg = globalConfig?.cloud?.nftables;
|
||||
const isEnabled = nftCfg?.enabled !== false;
|
||||
const currentWan = nftCfg?.wanInterface ?? '';
|
||||
const currentExtraPorts: ExtraPort[] = nftCfg?.extraPorts ?? [];
|
||||
const customRoutes = globalConfig?.cloud?.haproxy?.customRoutes ?? [];
|
||||
const customRoutes = haproxyRoutes?.customRoutes ?? [];
|
||||
|
||||
const [editingWan, setEditingWan] = useState(false);
|
||||
const [wanValue, setWanValue] = useState('');
|
||||
@@ -121,15 +127,8 @@ export function FirewallComponent() {
|
||||
const tcpPorts = allPorts.filter(p => p.protocol === 'tcp');
|
||||
const udpPorts = allPorts.filter(p => p.protocol === 'udp');
|
||||
|
||||
async function saveNftConfig(patch: NonNullable<NonNullable<typeof globalConfig>['cloud']>['nftables']) {
|
||||
if (!globalConfig) return;
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: {
|
||||
...globalConfig.cloud,
|
||||
nftables: { ...nftCfg, ...patch },
|
||||
},
|
||||
});
|
||||
async function saveNftConfig(patch: Partial<NonNullable<typeof nftCfg>>) {
|
||||
await nftMutation.mutateAsync(patch);
|
||||
refetchStatus();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user