services -> domains
This commit is contained in:
@@ -15,10 +15,12 @@ import {
|
||||
useSidebar,
|
||||
} from './ui/sidebar';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useCentralStatus } from '../hooks/useCentralStatus';
|
||||
|
||||
export function AppSidebar() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { state } = useSidebar();
|
||||
const { data: centralStatus } = useCentralStatus();
|
||||
|
||||
const cycleTheme = () => {
|
||||
if (theme === 'light') {
|
||||
@@ -52,16 +54,16 @@ export function AppSidebar() {
|
||||
}
|
||||
};
|
||||
|
||||
const servicesItems = [
|
||||
const domainsItems = [
|
||||
{ to: '/central', icon: LayoutDashboard, label: 'Dashboard', end: true },
|
||||
{ to: '/central/services', icon: Globe, label: 'Services' },
|
||||
{ to: '/central/domains', icon: Globe, label: 'Domains' },
|
||||
];
|
||||
|
||||
const centralItems = [
|
||||
{ to: '/central/vpn', icon: Lock, label: 'VPN' },
|
||||
{ to: '/central/firewall', icon: Shield, label: 'Firewall' },
|
||||
{ to: '/central/crowdsec', icon: ShieldAlert, label: 'CrowdSec' },
|
||||
{ to: '/central/dhcp', icon: Wifi, label: 'DHCP' },
|
||||
{ to: '/central/vpn', icon: Lock, label: 'VPN', daemon: 'wireguard' as const },
|
||||
{ to: '/central/firewall', icon: Shield, label: 'Firewall', daemon: 'nftables' as const },
|
||||
{ to: '/central/crowdsec', icon: ShieldAlert, label: 'CrowdSec', daemon: 'crowdsec' as const },
|
||||
{ to: '/central/dhcp', icon: Wifi, label: 'DHCP', daemon: 'dnsmasq' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -80,26 +82,33 @@ export function AppSidebar() {
|
||||
<SidebarContent>
|
||||
{state === 'collapsed' ? (
|
||||
<SidebarMenu>
|
||||
{[...servicesItems, ...centralItems].map((item) => (
|
||||
<SidebarMenuItem key={item.to}>
|
||||
<NavLink to={item.to} end={'end' in item ? (item as any).end : undefined}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton isActive={isActive} tooltip={item.label}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{[...domainsItems, ...centralItems].map((item) => {
|
||||
const daemon = 'daemon' in item ? (item as any).daemon : undefined;
|
||||
const active = daemon ? centralStatus?.daemons?.[daemon]?.active : undefined;
|
||||
return (
|
||||
<SidebarMenuItem key={item.to}>
|
||||
<NavLink to={item.to} end={'end' in item ? (item as any).end : undefined}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton isActive={isActive} tooltip={item.label}>
|
||||
<item.icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
{active !== undefined && (
|
||||
<span className={`ml-auto h-1.5 w-1.5 rounded-full ${active ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
) : (
|
||||
<>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Services</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>Domains</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{servicesItems.map(({ to, icon: Icon, label, end }) => (
|
||||
{domainsItems.map(({ to, icon: Icon, label, end }) => (
|
||||
<SidebarMenuItem key={to}>
|
||||
<NavLink to={to} end={end}>
|
||||
{({ isActive }) => (
|
||||
@@ -119,18 +128,24 @@ export function AppSidebar() {
|
||||
<SidebarGroupLabel>Central</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{centralItems.map(({ to, icon: Icon, label }) => (
|
||||
<SidebarMenuItem key={to}>
|
||||
<NavLink to={to}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton isActive={isActive}>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="truncate">{label}</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{centralItems.map(({ to, icon: Icon, label, daemon }) => {
|
||||
const active = centralStatus?.daemons?.[daemon]?.active;
|
||||
return (
|
||||
<SidebarMenuItem key={to}>
|
||||
<NavLink to={to}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton isActive={isActive}>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="truncate">{label}</span>
|
||||
{active !== undefined && (
|
||||
<span className={`ml-auto h-1.5 w-1.5 rounded-full ${active ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Badge } from './ui/badge';
|
||||
import {
|
||||
LayoutDashboard, Cloud, CheckCircle, XCircle, AlertCircle, Loader2,
|
||||
RefreshCw, BookOpen, Globe, Router, Server, RotateCw, Key, Plus, X,
|
||||
RefreshCw, BookOpen, Globe, Router, Server, RotateCw, Key,
|
||||
} from 'lucide-react';
|
||||
import { useCloudflare } from '../hooks/useCloudflare';
|
||||
import { useDdns } from '../hooks/useDdns';
|
||||
@@ -22,7 +22,7 @@ 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, updateConfig } = useConfig();
|
||||
const { config: globalConfig } = useConfig();
|
||||
const { config: vpnConfig } = useVpn();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
@@ -36,8 +36,6 @@ export function DashboardComponent() {
|
||||
|
||||
const [editingToken, setEditingToken] = useState(false);
|
||||
const [tokenValue, setTokenValue] = useState('');
|
||||
const [addingDdnsRecord, setAddingDdnsRecord] = useState(false);
|
||||
const [newDdnsRecord, setNewDdnsRecord] = useState('');
|
||||
|
||||
const updateSecretsMutation = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) => secretsApi.update(values),
|
||||
@@ -260,76 +258,33 @@ export function DashboardComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Record list with remove buttons */}
|
||||
{globalConfig?.cloud?.ddns?.records?.length ? (
|
||||
{/* Record list (read-only — derived from public domains) */}
|
||||
{ddnsStatus.records?.length ? (
|
||||
<div className="space-y-1">
|
||||
{globalConfig.cloud.ddns.records.map((r: string) => {
|
||||
const rs: DdnsRecordStatus | undefined = ddnsStatus.records?.find((s: DdnsRecordStatus) => s.name === r);
|
||||
return (
|
||||
<div key={r} className="flex items-center gap-2 text-xs group">
|
||||
{rs?.ok ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
) : rs && !rs.ok ? (
|
||||
<XCircle className="h-3.5 w-3.5 text-red-500 shrink-0" />
|
||||
) : (
|
||||
<div className="h-3.5 w-3.5 shrink-0" />
|
||||
)}
|
||||
<span className="font-mono bg-muted px-2 py-0.5 rounded">{r}</span>
|
||||
{rs?.ok && rs.ip && (
|
||||
<span className="text-muted-foreground font-mono">{rs.ip}</span>
|
||||
)}
|
||||
{rs && !rs.ok && rs.error && (
|
||||
<span className="text-red-500 truncate" title={rs.error}>{rs.error}</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 ml-auto"
|
||||
onClick={async () => {
|
||||
if (!globalConfig) return;
|
||||
const records = (globalConfig.cloud?.ddns?.records || []).filter((rec: string) => rec !== r);
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{ddnsStatus.records.map((rs: DdnsRecordStatus) => (
|
||||
<div key={rs.name} className="flex items-center gap-2 text-xs">
|
||||
{rs.ok ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-3.5 w-3.5 text-red-500 shrink-0" />
|
||||
)}
|
||||
<span className="font-mono bg-muted px-2 py-0.5 rounded">{rs.name}</span>
|
||||
{rs.ok && rs.ip && (
|
||||
<span className="text-muted-foreground font-mono">{rs.ip}</span>
|
||||
)}
|
||||
{!rs.ok && rs.error && (
|
||||
<span className="text-red-500 truncate" title={rs.error}>{rs.error}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Records are derived from public domains. Toggle Public on the Domains page to manage.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Add DDNS record */}
|
||||
{!addingDdnsRecord ? (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAddingDdnsRecord(true)} className="gap-1 text-muted-foreground">
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Record
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={newDdnsRecord}
|
||||
onChange={(e) => setNewDdnsRecord(e.target.value)}
|
||||
placeholder="dev.payne.io"
|
||||
className="h-8 text-xs font-mono flex-1"
|
||||
/>
|
||||
<Button size="sm" className="h-8" disabled={!newDdnsRecord} onClick={async () => {
|
||||
if (!globalConfig || !newDdnsRecord) return;
|
||||
const records = [...(globalConfig.cloud?.ddns?.records || []), newDdnsRecord];
|
||||
await updateConfig({
|
||||
...globalConfig,
|
||||
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
|
||||
});
|
||||
setNewDdnsRecord('');
|
||||
setAddingDdnsRecord(false);
|
||||
}}>Add</Button>
|
||||
<Button size="sm" variant="ghost" className="h-8" onClick={() => { setAddingDdnsRecord(false); setNewDdnsRecord(''); }}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No public domains registered. Set a domain to Public on the Domains page to enable DDNS.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -345,7 +300,7 @@ export function DashboardComponent() {
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground ml-7">
|
||||
DDNS is not enabled. Configure it in the DDNS settings to keep DNS records in sync with your public IP.
|
||||
DDNS is not enabled. Enable it in the global config to keep public domain DNS records in sync with your public IP.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
@@ -378,10 +333,11 @@ export function DashboardComponent() {
|
||||
<div className="space-y-3 ml-7">
|
||||
{/* Service badges */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<ServiceBadge name="dnsmasq" />
|
||||
<ServiceBadge name="haproxy" />
|
||||
<ServiceBadge name="wireguard" />
|
||||
<ServiceBadge name="crowdsec" />
|
||||
<ServiceBadge name="dnsmasq" active={centralStatus?.daemons?.dnsmasq?.active} />
|
||||
<ServiceBadge name="haproxy" active={centralStatus?.daemons?.haproxy?.active} />
|
||||
<ServiceBadge name="nftables" active={centralStatus?.daemons?.nftables?.active} />
|
||||
<ServiceBadge name="wireguard" active={centralStatus?.daemons?.wireguard?.active} />
|
||||
<ServiceBadge name="crowdsec" active={centralStatus?.daemons?.crowdsec?.active} />
|
||||
</div>
|
||||
|
||||
{/* Version and uptime */}
|
||||
@@ -452,13 +408,12 @@ export function DashboardComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline badge showing a service name with a colored dot. Status is informational only. */
|
||||
function ServiceBadge({ name }: { name: string }) {
|
||||
// These are presentational — the daemon is running if we can reach the API,
|
||||
// and individual service health comes from their own endpoints.
|
||||
/** Inline badge showing a service name with a colored dot reflecting actual daemon status. */
|
||||
function ServiceBadge({ name, active }: { name: string; active?: boolean }) {
|
||||
const dotColor = active === undefined ? 'bg-gray-400' : active ? 'bg-green-500' : 'bg-red-500';
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs font-medium">
|
||||
<span className="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className={`h-2 w-2 rounded-full ${dotColor}`} />
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
Plus, Trash2, X, ChevronDown, ChevronUp, Route, Shield, FileText,
|
||||
} from 'lucide-react';
|
||||
import { useHaproxy } from '../hooks/useHaproxy';
|
||||
import { useServices } from '../hooks/useServices';
|
||||
import { useDomains } from '../hooks/useDomains';
|
||||
import { useDdns } from '../hooks/useDdns';
|
||||
import { useCert } from '../hooks/useCert';
|
||||
import { usePageHelp } from '../hooks/usePageHelp';
|
||||
import type { RegisteredService } from '../services/api/networking';
|
||||
import type { RegisteredDomain } from '../services/api/networking';
|
||||
import type { CertEntry } from '../services/api/cert';
|
||||
import { servicesApi, haproxyApi } from '../services/api/networking';
|
||||
import { domainsApi, haproxyApi } from '../services/api/networking';
|
||||
|
||||
function StatusDot({ status, label }: { status: 'ok' | 'error' | 'na'; label: string }) {
|
||||
return (
|
||||
@@ -30,9 +31,9 @@ function StatusDot({ status, label }: { status: 'ok' | 'error' | 'na'; label: st
|
||||
);
|
||||
}
|
||||
|
||||
function getTlsStatus(service: RegisteredService, certDomains: Set<string>): 'ok' | 'error' | 'na' {
|
||||
function getTlsStatus(service: RegisteredDomain, certDomains: Set<string>): 'ok' | 'error' | 'na' {
|
||||
const backendType = service.routes?.length ? service.routes[0].backend.type : service.backend.type;
|
||||
if (service.tls === 'passthrough' || backendType === 'tcp-passthrough') return 'na';
|
||||
if (service.tls === 'passthrough' || service.tls === 'none' || backendType === 'tcp-passthrough' || backendType === 'dns-only') return 'na';
|
||||
// Check exact match or wildcard coverage
|
||||
if (certDomains.has(service.domain)) return 'ok';
|
||||
const dotIdx = service.domain.indexOf('.');
|
||||
@@ -56,21 +57,17 @@ function findCert(domain: string, certs: CertEntry[]): CertEntry | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasRoutes(service: RegisteredService): boolean {
|
||||
function hasRoutes(service: RegisteredDomain): boolean {
|
||||
return (service.routes?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
function getBackendAddress(service: RegisteredService): string {
|
||||
if (hasRoutes(service)) return service.routes![0].backend.address;
|
||||
return service.backend.address;
|
||||
}
|
||||
|
||||
function getBackendType(service: RegisteredService): string {
|
||||
function getBackendType(service: RegisteredDomain): string {
|
||||
if (hasRoutes(service)) return service.routes![0].backend.type;
|
||||
return service.backend.type;
|
||||
}
|
||||
|
||||
export function ServicesComponent() {
|
||||
export function DomainsComponent() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
@@ -78,8 +75,9 @@ export function ServicesComponent() {
|
||||
generateData: haproxyGenerateData, generateError: haproxyGenerateError,
|
||||
restart: restartHaproxy, isRestarting: isHaproxyRestarting,
|
||||
} = useHaproxy();
|
||||
const { services, isLoading: isServicesLoading } = useServices();
|
||||
const { status: certStatus, provision: provisionCert, isProvisioning, renew: renewCert, isRenewing } = useCert();
|
||||
const { domains, isLoading: isDomainsLoading } = useDomains();
|
||||
const { status: ddnsStatus, trigger: triggerDdns, isTriggering: isDdnsTriggering } = useDdns();
|
||||
const { status: certStatus, provision: provisionCert, renew: renewCert } = useCert();
|
||||
|
||||
const provisionMutation = useMutation({
|
||||
mutationFn: (domain: string) => provisionCert(domain),
|
||||
@@ -103,38 +101,38 @@ export function ServicesComponent() {
|
||||
const [newBackendAddr, setNewBackendAddr] = useState('');
|
||||
const [newPublic, setNewPublic] = useState(true);
|
||||
const [newSubdomains, setNewSubdomains] = useState(false);
|
||||
const [newTls, setNewTls] = useState<string>('passthrough');
|
||||
const [gatewayMode, setGatewayMode] = useState<'none' | 'reverse-proxy' | 'passthrough'>('reverse-proxy');
|
||||
|
||||
const certDomains = new Set<string>(
|
||||
(certStatus?.certs ?? []).filter(c => c.cert.exists).map(c => c.domain)
|
||||
);
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: (svc: Record<string, unknown>) => servicesApi.register(svc as any),
|
||||
mutationFn: (svc: Record<string, unknown>) => domainsApi.register(svc as any),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['services'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['domains'] });
|
||||
setShowAddForm(false);
|
||||
setNewDomain(''); setNewBackendAddr(''); setNewPublic(true); setNewSubdomains(false); setNewTls('passthrough');
|
||||
setNewDomain(''); setNewBackendAddr(''); setNewPublic(true); setNewSubdomains(false); setGatewayMode('reverse-proxy');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ domain, updates }: { domain: string; updates: Record<string, unknown> }) =>
|
||||
servicesApi.register({ ...(services.find(s => s.domain === domain) ?? {}), ...updates } as any),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['services'] }),
|
||||
domainsApi.register({ ...(domains.find(d => d.domain === domain) ?? {}), ...updates } as any),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['domains'] }),
|
||||
});
|
||||
|
||||
const deregisterMutation = useMutation({
|
||||
mutationFn: (domain: string) => servicesApi.deregister(domain),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['services'] }),
|
||||
mutationFn: (domain: string) => domainsApi.deregister(domain),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['domains'] }),
|
||||
});
|
||||
|
||||
usePageHelp({
|
||||
title: 'Services',
|
||||
title: 'Domains',
|
||||
description: (
|
||||
<p className="leading-relaxed">
|
||||
Each service is a domain that Wild Central manages. Central provides DNS resolution,
|
||||
proxy routing, TLS certificates, and public DNS records based on how you configure each service.
|
||||
Each domain is managed by Wild Central. For each, configure whether it just resolves to an IP
|
||||
or routes through Central's gateway for DNS, proxy routing, TLS, and public exposure.
|
||||
</p>
|
||||
),
|
||||
});
|
||||
@@ -168,12 +166,12 @@ export function ServicesComponent() {
|
||||
<Globe className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Services</h2>
|
||||
<h2 className="text-2xl font-semibold">Domains</h2>
|
||||
<p className="text-muted-foreground">Registered domains and their networking status</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setShowAddForm(!showAddForm)} className="gap-1">
|
||||
<Plus className="h-4 w-4" />Add Service
|
||||
<Plus className="h-4 w-4" />Add Domain
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -181,16 +179,32 @@ export function ServicesComponent() {
|
||||
{showAddForm && (
|
||||
<Card className="border-dashed border-primary/50">
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">Domain</Label>
|
||||
<Input value={newDomain} onChange={e => setNewDomain(e.target.value)} placeholder="cloud.payne.io" className="mt-1 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">Backend (host:port)</Label>
|
||||
<Input value={newBackendAddr} onChange={e => setNewBackendAddr(e.target.value)} placeholder="192.168.8.240:443" className="mt-1 font-mono" />
|
||||
<div>
|
||||
<Label className="text-xs">Domain</Label>
|
||||
<Input value={newDomain} onChange={e => setNewDomain(e.target.value)} placeholder="dev.payne.io" className="mt-1 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs mb-2 block">Gateway</Label>
|
||||
<div className="flex gap-1 bg-muted rounded-md p-1">
|
||||
{([
|
||||
{ value: 'none' as const, label: 'None' },
|
||||
{ value: 'reverse-proxy' as const, label: 'Reverse proxy' },
|
||||
{ value: 'passthrough' as const, label: 'Passthrough' },
|
||||
] as const).map(opt => (
|
||||
<button key={opt.value} type="button"
|
||||
className={`flex-1 text-xs py-1.5 px-2 rounded transition-colors ${gatewayMode === opt.value ? 'bg-background shadow-sm font-medium' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => setGatewayMode(opt.value)}>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">{gatewayMode === 'none' ? 'Resolves to (IP)' : 'Backend (host:port)'}</Label>
|
||||
<Input value={newBackendAddr} onChange={e => setNewBackendAddr(e.target.value)}
|
||||
placeholder={gatewayMode === 'none' ? '192.168.8.222' : '192.168.8.240:443'}
|
||||
className="mt-1 font-mono" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Switch checked={newPublic} onCheckedChange={setNewPublic} />
|
||||
@@ -200,19 +214,19 @@ export function ServicesComponent() {
|
||||
<Switch checked={newSubdomains} onCheckedChange={setNewSubdomains} />
|
||||
Include subdomains
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Switch checked={newTls === 'terminate'} onCheckedChange={v => setNewTls(v ? 'terminate' : 'passthrough')} />
|
||||
Central handles TLS
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" size="sm" onClick={() => setShowAddForm(false)}><X className="h-3 w-3 mr-1" />Cancel</Button>
|
||||
<Button size="sm" disabled={!newDomain || !newBackendAddr || registerMutation.isPending}
|
||||
onClick={() => registerMutation.mutate({
|
||||
domain: newDomain,
|
||||
backend: { address: newBackendAddr, type: newTls === 'passthrough' ? 'tcp-passthrough' : 'http' },
|
||||
public: newPublic, subdomains: newSubdomains, tls: newTls, source: 'manual',
|
||||
})}>
|
||||
onClick={() => {
|
||||
const backendType = gatewayMode === 'none' ? 'dns-only' : gatewayMode === 'passthrough' ? 'tcp-passthrough' : 'http';
|
||||
const tls = gatewayMode === 'none' ? 'none' : gatewayMode === 'passthrough' ? 'passthrough' : 'terminate';
|
||||
registerMutation.mutate({
|
||||
domain: newDomain,
|
||||
backend: { address: newBackendAddr, type: backendType },
|
||||
public: newPublic, subdomains: newSubdomains, tls, source: 'manual',
|
||||
});
|
||||
}}>
|
||||
{registerMutation.isPending ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Plus className="h-3 w-3 mr-1" />}
|
||||
Register
|
||||
</Button>
|
||||
@@ -222,28 +236,32 @@ export function ServicesComponent() {
|
||||
)}
|
||||
|
||||
{/* Loading */}
|
||||
{isServicesLoading && (
|
||||
{isDomainsLoading && (
|
||||
<Card className="p-8 text-center">
|
||||
<Loader2 className="h-8 w-8 text-primary mx-auto mb-2 animate-spin" />
|
||||
<p className="text-sm text-muted-foreground">Loading services...</p>
|
||||
<p className="text-sm text-muted-foreground">Loading domains...</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Empty */}
|
||||
{!isServicesLoading && services.length === 0 && (
|
||||
{!isDomainsLoading && domains.length === 0 && (
|
||||
<Card className="p-8 text-center">
|
||||
<Globe className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No Services</h3>
|
||||
<p className="text-muted-foreground">Services appear when Wild Cloud or Wild Works registers domains, or add one manually.</p>
|
||||
<h3 className="text-lg font-medium mb-2">No Domains</h3>
|
||||
<p className="text-muted-foreground">Domains appear when Wild Cloud or Wild Works registers them, or add one manually.</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Service Cards */}
|
||||
{services.map(service => {
|
||||
{/* Domain Cards */}
|
||||
{domains.map(service => {
|
||||
const isExpanded = expandedDomains.has(service.domain);
|
||||
const backendType = getBackendType(service);
|
||||
const isDnsOnly = backendType === 'dns-only';
|
||||
const isPassthrough = service.tls === 'passthrough' || backendType === 'tcp-passthrough';
|
||||
const hasGateway = !isDnsOnly;
|
||||
const tlsStatus = getTlsStatus(service, certDomains);
|
||||
const ddnsStatus2 = service.public ? 'ok' as const : 'na' as const;
|
||||
const isPassthrough = service.tls === 'passthrough' || getBackendType(service) === 'tcp-passthrough';
|
||||
const ddnsRecord = ddnsStatus?.records?.find(r => r.name === service.domain);
|
||||
const ddnsStatus2: 'ok' | 'error' | 'na' = !service.public ? 'na' : ddnsRecord?.ok ? 'ok' : ddnsRecord ? 'error' : 'na';
|
||||
const routeCount = service.routes?.length ?? 0;
|
||||
|
||||
return (
|
||||
@@ -261,8 +279,8 @@ export function ServicesComponent() {
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<StatusDot status="ok" label="DNS" />
|
||||
<StatusDot status="ok" label="Proxy" />
|
||||
<StatusDot status={tlsStatus} label="TLS" />
|
||||
{hasGateway && <StatusDot status="ok" label="Gateway" />}
|
||||
{hasGateway && !isPassthrough && <StatusDot status={tlsStatus} label="TLS" />}
|
||||
<StatusDot status={ddnsStatus2} label="DDNS" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,7 +293,7 @@ export function ServicesComponent() {
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 py-2">
|
||||
{!hasRoutes(service) && (
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Backend</Label>
|
||||
<Label className="text-xs text-muted-foreground">{isDnsOnly ? 'Resolves to' : 'Backend'}</Label>
|
||||
<Input
|
||||
defaultValue={service.backend.address}
|
||||
className="mt-0.5 h-8 font-mono text-sm w-48"
|
||||
@@ -310,29 +328,31 @@ export function ServicesComponent() {
|
||||
<Label className="text-sm">Subdomains</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!isPassthrough}
|
||||
onCheckedChange={v => {
|
||||
if (hasRoutes(service)) {
|
||||
updateMutation.mutate({
|
||||
domain: service.domain,
|
||||
updates: { tls: v ? 'terminate' : 'passthrough' },
|
||||
});
|
||||
} else {
|
||||
updateMutation.mutate({
|
||||
domain: service.domain,
|
||||
updates: {
|
||||
tls: v ? 'terminate' : 'passthrough',
|
||||
backend: { ...service.backend, type: v ? 'http' : 'tcp-passthrough' },
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={updateMutation.isPending}
|
||||
/>
|
||||
<Label className="text-sm">{isPassthrough ? 'TLS Passthrough' : 'Central TLS'}</Label>
|
||||
</div>
|
||||
{hasGateway && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!isPassthrough}
|
||||
onCheckedChange={v => {
|
||||
if (hasRoutes(service)) {
|
||||
updateMutation.mutate({
|
||||
domain: service.domain,
|
||||
updates: { tls: v ? 'terminate' : 'passthrough' },
|
||||
});
|
||||
} else {
|
||||
updateMutation.mutate({
|
||||
domain: service.domain,
|
||||
updates: {
|
||||
tls: v ? 'terminate' : 'passthrough',
|
||||
backend: { ...service.backend, type: v ? 'http' : 'tcp-passthrough' },
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={updateMutation.isPending}
|
||||
/>
|
||||
<Label className="text-sm">{isPassthrough ? 'TLS Passthrough' : 'Central TLS'}</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Routes detail */}
|
||||
@@ -355,8 +375,8 @@ export function ServicesComponent() {
|
||||
if (val && val !== route.backend.address) {
|
||||
const updatedRoutes = [...service.routes!];
|
||||
updatedRoutes[i] = { ...route, backend: { ...route.backend, address: val } };
|
||||
servicesApi.register({ ...service, backend: undefined as any, routes: updatedRoutes } as any)
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['services'] }));
|
||||
domainsApi.register({ ...service, backend: undefined as any, routes: updatedRoutes } as any)
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['domains'] }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -401,7 +421,7 @@ export function ServicesComponent() {
|
||||
)}
|
||||
|
||||
{/* TLS cert info */}
|
||||
{!isPassthrough && (() => {
|
||||
{hasGateway && !isPassthrough && (() => {
|
||||
const cert = findCert(service.domain, certStatus?.certs ?? []);
|
||||
if (!cert) {
|
||||
const provisionDomain = service.subdomains ? `*.${service.domain}` : service.domain;
|
||||
@@ -449,6 +469,25 @@ export function ServicesComponent() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* DDNS error info */}
|
||||
{ddnsStatus2 === 'error' && ddnsRecord && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-red-500">
|
||||
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>
|
||||
DDNS sync failed{ddnsRecord.error ? `: ${ddnsRecord.error}` : ''}.
|
||||
{' '}Check that your Cloudflare API token has access to this domain's zone.
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs gap-1 shrink-0 ml-2"
|
||||
disabled={isDdnsTriggering}
|
||||
onClick={() => triggerDdns()}>
|
||||
{isDdnsTriggering ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCw className="h-3 w-3" />}
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Source + deregister */}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Source: {service.source}</span>
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../services/api/client';
|
||||
|
||||
interface DaemonStatus {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface CentralStatus {
|
||||
status: string;
|
||||
version: string;
|
||||
@@ -10,6 +14,7 @@ interface CentralStatus {
|
||||
appsDir: string;
|
||||
setupFiles: string;
|
||||
pendingRestart: boolean;
|
||||
daemons?: Record<string, DaemonStatus>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,62 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiService } from '../services/api-legacy';
|
||||
import type { GlobalConfig, GlobalConfigResponse } from '../types';
|
||||
import type { CentralState, CentralStateResponse } from '../types';
|
||||
|
||||
interface CreateConfigResponse {
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing Wild Central global configuration
|
||||
* Endpoint: /api/v1/config
|
||||
* Hook for managing Wild Central runtime state
|
||||
* Endpoint: /api/v1/state
|
||||
* File: {dataDir}/state.yaml
|
||||
*/
|
||||
export const useConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [showConfigSetup, setShowConfigSetup] = useState(false);
|
||||
|
||||
const configQuery = useQuery<GlobalConfigResponse>({
|
||||
queryKey: ['globalConfig'],
|
||||
const stateQuery = useQuery<CentralStateResponse>({
|
||||
queryKey: ['state'],
|
||||
queryFn: () => apiService.getConfig(),
|
||||
});
|
||||
|
||||
// Update showConfigSetup based on query data
|
||||
useEffect(() => {
|
||||
if (configQuery.data) {
|
||||
setShowConfigSetup(configQuery.data.configured === false);
|
||||
if (stateQuery.data) {
|
||||
setShowConfigSetup(stateQuery.data.configured === false);
|
||||
}
|
||||
}, [configQuery.data]);
|
||||
}, [stateQuery.data]);
|
||||
|
||||
const createConfigMutation = useMutation<CreateConfigResponse, Error, GlobalConfig>({
|
||||
mutationFn: (config) => apiService.createConfig(config),
|
||||
const createStateMutation = useMutation<CreateConfigResponse, Error, CentralState>({
|
||||
mutationFn: (state) => apiService.createConfig(state),
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch config after successful creation
|
||||
queryClient.invalidateQueries({ queryKey: ['globalConfig'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['state'] });
|
||||
setShowConfigSetup(false);
|
||||
},
|
||||
});
|
||||
|
||||
const updateConfigMutation = useMutation<CreateConfigResponse, Error, GlobalConfig>({
|
||||
mutationFn: (config) => apiService.updateConfig(config),
|
||||
const updateStateMutation = useMutation<CreateConfigResponse, Error, CentralState>({
|
||||
mutationFn: (state) => apiService.updateConfig(state),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['globalConfig'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['state'] });
|
||||
// Immediately re-check daemon status so the pending-restart banner appears
|
||||
queryClient.invalidateQueries({ queryKey: ['central', 'status'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
config: configQuery.data?.config || null,
|
||||
isConfigured: configQuery.data?.configured || false,
|
||||
config: stateQuery.data?.state || null,
|
||||
isConfigured: stateQuery.data?.configured || false,
|
||||
showConfigSetup,
|
||||
setShowConfigSetup,
|
||||
isLoading: configQuery.isLoading,
|
||||
isCreating: createConfigMutation.isPending,
|
||||
isUpdating: updateConfigMutation.isPending,
|
||||
error: configQuery.error || createConfigMutation.error || updateConfigMutation.error,
|
||||
createConfig: createConfigMutation.mutate,
|
||||
updateConfig: updateConfigMutation.mutateAsync,
|
||||
refetch: configQuery.refetch,
|
||||
isLoading: stateQuery.isLoading,
|
||||
isCreating: createStateMutation.isPending,
|
||||
isUpdating: updateStateMutation.isPending,
|
||||
error: stateQuery.error || createStateMutation.error || updateStateMutation.error,
|
||||
createConfig: createStateMutation.mutate,
|
||||
updateConfig: updateStateMutation.mutateAsync,
|
||||
refetch: stateQuery.refetch,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,10 +14,12 @@ export const useDdns = () => {
|
||||
const triggerMutation = useMutation({
|
||||
mutationFn: () => ddnsApi.trigger(),
|
||||
onSuccess: () => {
|
||||
// Refresh status shortly after trigger
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ddns', 'status'] });
|
||||
}, 2000);
|
||||
// Refresh status after trigger — stagger to catch async completion
|
||||
for (const delay of [1000, 3000, 6000]) {
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ddns', 'status'] });
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { servicesApi } from '../services/api/networking';
|
||||
import { domainsApi } from '../services/api/networking';
|
||||
|
||||
export const useServices = () => {
|
||||
export const useDomains = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['services'],
|
||||
queryFn: () => servicesApi.list(),
|
||||
queryKey: ['domains'],
|
||||
queryFn: () => domainsApi.list(),
|
||||
refetchInterval: 30000,
|
||||
staleTime: 10000,
|
||||
});
|
||||
|
||||
return {
|
||||
services: query.data?.services ?? [],
|
||||
domains: query.data?.domains ?? [],
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
};
|
||||
10
web/src/router/pages/DomainsPage.tsx
Normal file
10
web/src/router/pages/DomainsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { DomainsComponent } from '../../components/DomainsComponent';
|
||||
|
||||
export function DomainsPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<DomainsComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { ServicesComponent } from '../../components/ServicesComponent';
|
||||
|
||||
export function ServicesPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ServicesComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { RouteObject } from 'react-router';
|
||||
import { CentralLayout } from './CentralLayout';
|
||||
import { NotFoundPage } from './pages/NotFoundPage';
|
||||
import { DashboardPage } from './pages/DashboardPage';
|
||||
import { ServicesPage } from './pages/ServicesPage';
|
||||
import { DomainsPage } from './pages/DomainsPage';
|
||||
import { FirewallPage } from './pages/FirewallPage';
|
||||
import { VpnPage } from './pages/VpnPage';
|
||||
import { CrowdSecPage } from './pages/CrowdSecPage';
|
||||
@@ -19,7 +19,7 @@ export const routes: RouteObject[] = [
|
||||
element: <CentralLayout />,
|
||||
children: [
|
||||
{ index: true, element: <DashboardPage /> },
|
||||
{ path: 'services', element: <ServicesPage /> },
|
||||
{ path: 'domains', element: <DomainsPage /> },
|
||||
{ path: 'firewall', element: <FirewallPage /> },
|
||||
{ path: 'vpn', element: <VpnPage /> },
|
||||
{ path: 'crowdsec', element: <CrowdSecPage /> },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
Status,
|
||||
GlobalConfig,
|
||||
GlobalConfigResponse,
|
||||
CentralState,
|
||||
CentralStateResponse,
|
||||
HealthResponse,
|
||||
StatusResponse,
|
||||
NetworkInfo,
|
||||
@@ -51,35 +51,35 @@ class ApiService {
|
||||
|
||||
// ========================================
|
||||
// Global Config APIs (Wild Central level)
|
||||
// Endpoint: /api/v1/config
|
||||
// Endpoint: /api/v1/state
|
||||
// ========================================
|
||||
|
||||
async getConfig(): Promise<GlobalConfigResponse> {
|
||||
return this.request<GlobalConfigResponse>('/api/v1/config');
|
||||
async getConfig(): Promise<CentralStateResponse> {
|
||||
return this.request<CentralStateResponse>('/api/v1/state');
|
||||
}
|
||||
|
||||
async getConfigYaml(): Promise<string> {
|
||||
return this.requestText('/api/v1/config/yaml');
|
||||
return this.requestText('/api/v1/state/yaml');
|
||||
}
|
||||
|
||||
async updateConfigYaml(yamlContent: string): Promise<StatusResponse> {
|
||||
return this.request<StatusResponse>('/api/v1/config/yaml', {
|
||||
return this.request<StatusResponse>('/api/v1/state/yaml', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: yamlContent
|
||||
});
|
||||
}
|
||||
|
||||
async createConfig(config: GlobalConfig): Promise<StatusResponse> {
|
||||
return this.request<StatusResponse>('/api/v1/config', {
|
||||
async createConfig(config: CentralState): Promise<StatusResponse> {
|
||||
return this.request<StatusResponse>('/api/v1/state', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
}
|
||||
|
||||
async updateConfig(config: GlobalConfig): Promise<StatusResponse> {
|
||||
return this.request<StatusResponse>('/api/v1/config', {
|
||||
async updateConfig(config: CentralState): Promise<StatusResponse> {
|
||||
return this.request<StatusResponse>('/api/v1/state', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export { apiClient, ApiError } from './client';
|
||||
export * from './types';
|
||||
export { dnsmasqApi } from './dnsmasq';
|
||||
export { servicesApi, haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
||||
export type { RegisteredService, HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
|
||||
export { domainsApi, haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
||||
export type { RegisteredDomain, HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision } from './networking';
|
||||
export { vpnApi } from './vpn';
|
||||
export type { VpnStatus, VpnConfig, VpnPeer, VpnPeerConfig } from './vpn';
|
||||
export { certApi } from './cert';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
// Services
|
||||
// Domains
|
||||
|
||||
export interface HeaderConfig {
|
||||
request?: Record<string, string>;
|
||||
@@ -18,7 +18,7 @@ export interface Route {
|
||||
ipAllow?: string[];
|
||||
}
|
||||
|
||||
export interface RegisteredService {
|
||||
export interface RegisteredDomain {
|
||||
domain: string;
|
||||
source: string;
|
||||
subdomains: boolean;
|
||||
@@ -32,15 +32,15 @@ export interface RegisteredService {
|
||||
routes?: Route[];
|
||||
}
|
||||
|
||||
export const servicesApi = {
|
||||
async list(): Promise<{ services: RegisteredService[] }> {
|
||||
return apiClient.get('/api/v1/services');
|
||||
export const domainsApi = {
|
||||
async list(): Promise<{ domains: RegisteredDomain[] }> {
|
||||
return apiClient.get('/api/v1/domains');
|
||||
},
|
||||
async register(svc: { domain: string; backend: { address: string; type: string; health?: string }; public?: boolean; subdomains?: boolean; source?: string }): Promise<{ message: string; service: RegisteredService }> {
|
||||
return apiClient.post('/api/v1/services', svc);
|
||||
async register(dom: { domain: string; backend: { address: string; type: string; health?: string }; public?: boolean; subdomains?: boolean; source?: string; tls?: string }): Promise<{ message: string; domain: RegisteredDomain }> {
|
||||
return apiClient.post('/api/v1/domains', dom);
|
||||
},
|
||||
async deregister(domain: string): Promise<{ message: string }> {
|
||||
return apiClient.delete(`/api/v1/services/${domain}`);
|
||||
return apiClient.delete(`/api/v1/domains/${domain}`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface Status {
|
||||
|
||||
// ========================================
|
||||
// Global Config Types (Wild Central level)
|
||||
// Endpoint: /api/v1/config
|
||||
// Endpoint: /api/v1/state
|
||||
// File: {dataDir}/state.yaml
|
||||
// ========================================
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface HAProxyCustomRoute {
|
||||
backend: string;
|
||||
}
|
||||
|
||||
export interface GlobalConfig {
|
||||
export interface CentralState {
|
||||
operator?: {
|
||||
email?: string;
|
||||
};
|
||||
@@ -48,15 +48,14 @@ export interface GlobalConfig {
|
||||
ddns?: {
|
||||
enabled?: boolean;
|
||||
provider?: string;
|
||||
records?: string[];
|
||||
intervalMinutes?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface GlobalConfigResponse {
|
||||
export interface CentralStateResponse {
|
||||
configured: boolean;
|
||||
config?: GlobalConfig;
|
||||
state?: CentralState;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user