feat: Add Wild Central web app (Central-only UI)

Extract Central pages from the wild-cloud web app into a standalone
React app for Wild Central. Includes:

- Central overview, DNS, DHCP, Firewall, VPN, Ingress, CrowdSec pages
- Simplified sidebar with Central-only navigation
- Branding updated to "Wild Central"
- All Cloud-specific pages, components, hooks, and API services removed
- TypeScript type-check and production build pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 04:10:43 +00:00
parent 5defc27f63
commit 735f3fcb05
121 changed files with 18347 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
import { useState } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from './ui/card';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import { Alert, AlertDescription } from './ui/alert';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Router, CheckCircle, ExternalLink, Loader2, AlertCircle, Play, RotateCw, Copy, ChevronDown, ChevronUp, Edit } from 'lucide-react';
import { Badge } from './ui/badge';
import { useConfig } from '../hooks';
import { useDnsmasq } from '../hooks/useDnsmasq';
import { apiService } from '../services/api-legacy';
export function LanDnsComponent() {
const { config: globalConfig } = useConfig();
const {
status: dnsStatus,
isLoadingStatus: isDnsStatusLoading,
config: dnsConfig,
fetchConfig: fetchDnsConfig,
generateConfig: generateDnsConfig,
isGenerating: isDnsGenerating,
generateData: dnsGenerateData,
restart: restartDns,
isRestarting: isDnsRestarting,
restartData: dnsRestartData,
generateError: dnsGenerateError,
restartError: dnsRestartError,
} = useDnsmasq();
const [showDnsAdvanced, setShowDnsAdvanced] = useState(false);
const [showDnsEditDialog, setShowDnsEditDialog] = useState(false);
const [editedDnsConfig, setEditedDnsConfig] = useState('');
const [copiedDnsIp, setCopiedDnsIp] = useState(false);
const isDnsRunning = dnsStatus?.status === 'active';
const dnsIp = dnsStatus?.ip;
const handleCopyDnsIp = () => {
if (dnsIp) {
navigator.clipboard.writeText(dnsIp);
setCopiedDnsIp(true);
setTimeout(() => setCopiedDnsIp(false), 2000);
}
};
const handleShowDnsAdvanced = () => {
if (!showDnsAdvanced && !dnsConfig) {
fetchDnsConfig();
}
setShowDnsAdvanced(!showDnsAdvanced);
};
const handleEditDnsConfig = () => {
if (dnsConfig?.content) {
setEditedDnsConfig(dnsConfig.content);
} else if (dnsGenerateData?.config || dnsGenerateData?.content) {
setEditedDnsConfig(dnsGenerateData.config || dnsGenerateData.content || '');
} else {
setEditedDnsConfig('');
}
setShowDnsEditDialog(true);
};
const handleSaveDnsConfig = async () => {
try {
await apiService.writeDnsmasqConfig(editedDnsConfig);
setShowDnsEditDialog(false);
fetchDnsConfig();
} catch (error) {
console.error('Failed to save config:', error);
}
};
const handleSaveAndRestartDns = async () => {
try {
await apiService.writeDnsmasqConfig(editedDnsConfig);
await apiService.restartDnsmasq();
setShowDnsEditDialog(false);
fetchDnsConfig();
} catch (error) {
console.error('Failed to save config and restart:', error);
}
};
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="p-2 bg-primary/10 rounded-lg">
<Router className="h-6 w-6 text-primary" />
</div>
<div>
<h2 className="text-2xl font-semibold">LAN DNS</h2>
<p className="text-muted-foreground">Resolves Wild Cloud domains on your local network via dnsmasq</p>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>DNS Service</CardTitle>
<Badge variant={isDnsRunning ? 'success' : 'warning'} className="gap-1">
{isDnsStatusLoading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : isDnsRunning ? (
<CheckCircle className="h-3 w-3" />
) : null}
{isDnsStatusLoading ? 'Checking...' : isDnsRunning ? 'Running' : 'Stopped'}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<div>
<div className="text-sm text-muted-foreground">
Set this as the primary DNS server in your router
</div>
<code className="text-xl font-mono font-bold mt-1 inline-block">
{dnsIp || 'Auto-detecting...'}
</code>
</div>
<Button
variant="outline"
size="sm"
onClick={handleCopyDnsIp}
disabled={!dnsIp}
className="gap-2"
>
<Copy className="h-4 w-4" />
{copiedDnsIp ? 'Copied!' : 'Copy'}
</Button>
</div>
{globalConfig?.cloud?.router?.ip && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Router: <span className="font-mono">{globalConfig.cloud.router.ip}</span>
</span>
<Button
variant="ghost"
size="sm"
onClick={() => window.open(`http://${globalConfig?.cloud?.router?.ip}`, '_blank')}
className="gap-1 text-xs"
>
<ExternalLink className="h-3 w-3" />
Open Admin
</Button>
</div>
)}
<div className="flex gap-2">
{!isDnsRunning ? (
<Button onClick={() => generateDnsConfig(true)} disabled={isDnsGenerating} className="gap-2">
{isDnsGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
Start DNS
</Button>
) : (
<Button onClick={() => restartDns()} disabled={isDnsRestarting} variant="outline" className="gap-2">
{isDnsRestarting ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCw className="h-4 w-4" />}
Restart
</Button>
)}
<Button onClick={handleShowDnsAdvanced} variant="outline" className="gap-2">
{showDnsAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
Advanced
</Button>
</div>
{(dnsGenerateData || dnsRestartData) && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
{dnsGenerateData?.message || dnsRestartData?.message}
</AlertDescription>
</Alert>
)}
{(dnsGenerateError || dnsRestartError) && (
<Alert variant="error">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{(dnsGenerateError || dnsRestartError)?.toString()}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{showDnsAdvanced && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Advanced DNS Configuration</CardTitle>
<Button variant="outline" size="sm" onClick={handleEditDnsConfig} className="gap-2">
<Edit className="h-4 w-4" />
Edit Config
</Button>
</div>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-lg overflow-x-auto text-xs font-mono">
{isDnsGenerating && !dnsConfig && !dnsGenerateData && (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
)}
{(dnsConfig?.content || dnsGenerateData?.config || dnsGenerateData?.content) && (
dnsConfig?.content || dnsGenerateData?.config || dnsGenerateData?.content
)}
{!isDnsGenerating && !dnsGenerateData && !dnsConfig && (
<div className="text-center p-8 text-sm text-muted-foreground">
<p>Configuration preview will appear here</p>
</div>
)}
</pre>
</CardContent>
</Card>
)}
<Dialog open={showDnsEditDialog} onOpenChange={setShowDnsEditDialog}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Edit DNS Configuration</DialogTitle>
<DialogDescription>
Modify the dnsmasq configuration. Changes will take effect after saving and restarting.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
value={editedDnsConfig}
onChange={(e) => setEditedDnsConfig(e.target.value)}
className="font-mono text-xs min-h-[400px]"
placeholder="# dnsmasq configuration"
/>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={() => setShowDnsEditDialog(false)}>
Cancel
</Button>
<Button variant="outline" onClick={handleSaveDnsConfig}>
Save
</Button>
<Button onClick={handleSaveAndRestartDns}>
Save & Restart
</Button>
</DialogFooter>
<p className="text-xs text-muted-foreground text-center">
Save: Write config without restarting | Save & Restart: Write config and restart service
</p>
</DialogContent>
</Dialog>
</div>
);
}