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,61 @@
import { useState } from 'react';
import { Outlet } from 'react-router';
import { AppSidebar } from '../components/AppSidebar';
import { SidebarProvider, SidebarInset, SidebarTrigger } from '../components/ui/sidebar';
import { HelpProvider, useHelp } from '../contexts/HelpContext';
import { HelpPanel } from '../components/HelpPanel';
import { Button } from '../components/ui/button';
import { HelpCircle } from 'lucide-react';
function CentralLayoutContent() {
const { helpContent, setIsHelpOpen } = useHelp();
return (
<>
<AppSidebar />
<SidebarInset className="min-w-0">
<header className="flex h-16 shrink-0 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex items-center gap-2 flex-1">
<h1 className="text-lg font-semibold">Wild Central</h1>
</div>
{helpContent && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsHelpOpen(true)}
className="ml-auto"
>
<HelpCircle className="h-5 w-5" />
<span className="sr-only">Help</span>
</Button>
)}
</header>
<div className="flex flex-1 flex-col gap-4 p-4">
<Outlet />
</div>
</SidebarInset>
<HelpPanel />
</>
);
}
export function CentralLayout() {
const [sidebarOpen, setSidebarOpen] = useState(
() => localStorage.getItem('sidebar_state') !== 'false'
);
return (
<HelpProvider>
<SidebarProvider
open={sidebarOpen}
onOpenChange={(open) => {
setSidebarOpen(open);
localStorage.setItem('sidebar_state', String(open));
}}
>
<CentralLayoutContent />
</SidebarProvider>
</HelpProvider>
);
}

12
web/src/router/index.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { createBrowserRouter } from 'react-router';
import { routes } from './routes';
export const router = createBrowserRouter(routes, {
future: {
v7_startTransition: true,
v7_relativeSplatPath: true,
},
});
export { routes };
export * from './pages/NotFoundPage';

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { CentralComponent } from '../../components/CentralComponent';
export function CentralPage() {
return (
<ErrorBoundary>
<CentralComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { CloudflareComponent } from '../../components/CloudflareComponent';
export function CloudflarePage() {
return (
<ErrorBoundary>
<CloudflareComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { CrowdSecComponent } from '../../components/CrowdSecComponent';
export function CrowdSecPage() {
return (
<ErrorBoundary>
<CrowdSecComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { DdnsComponent } from '../../components/DdnsComponent';
export function DdnsPage() {
return (
<ErrorBoundary>
<DdnsComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { DhcpComponent } from '../../components/DhcpComponent';
export function DhcpPage() {
return (
<ErrorBoundary>
<DhcpComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { DnsComponent } from '../../components/DnsComponent';
export function DnsPage() {
return (
<ErrorBoundary>
<DnsComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { FirewallComponent } from '../../components/FirewallComponent';
export function FirewallPage() {
return (
<ErrorBoundary>
<FirewallComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { IngressProxyComponent } from '../../components/IngressProxyComponent';
export function IngressProxyPage() {
return (
<ErrorBoundary>
<IngressProxyComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components/ErrorBoundary';
import { LanDnsComponent } from '../../components/LanDnsComponent';
export function LanDnsPage() {
return (
<ErrorBoundary>
<LanDnsComponent />
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,30 @@
import { Link } from 'react-router';
import { AlertCircle, Home } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
export function NotFoundPage() {
return (
<div className="flex items-center justify-center min-h-[600px]">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<AlertCircle className="h-16 w-16 text-destructive" />
</div>
<CardTitle className="text-2xl">Page Not Found</CardTitle>
<CardDescription>
The page you're looking for doesn't exist or has been moved.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Link to="/">
<Button>
<Home className="mr-2 h-4 w-4" />
Go to Home
</Button>
</Link>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { ErrorBoundary } from '../../components';
import { VpnComponent } from '../../components/VpnComponent';
export function VpnPage() {
return (
<ErrorBoundary>
<VpnComponent />
</ErrorBoundary>
);
}

41
web/src/router/routes.tsx Normal file
View File

@@ -0,0 +1,41 @@
import { Navigate } from 'react-router';
import type { RouteObject } from 'react-router';
import { CentralLayout } from './CentralLayout';
import { NotFoundPage } from './pages/NotFoundPage';
import { CentralPage } from './pages/CentralPage';
import { DhcpPage } from './pages/DhcpPage';
import { FirewallPage } from './pages/FirewallPage';
import { IngressProxyPage } from './pages/IngressProxyPage';
import { DnsPage } from './pages/DnsPage';
import { CrowdSecPage } from './pages/CrowdSecPage';
import { VpnPage } from './pages/VpnPage';
import { CloudflarePage } from './pages/CloudflarePage';
import { DdnsPage } from './pages/DdnsPage';
import { LanDnsPage } from './pages/LanDnsPage';
export const routes: RouteObject[] = [
{
path: '/',
element: <Navigate to="/central" replace />,
},
{
path: '/central',
element: <CentralLayout />,
children: [
{ index: true, element: <CentralPage /> },
{ path: 'cloudflare', element: <CloudflarePage /> },
{ path: 'firewall', element: <FirewallPage /> },
{ path: 'dhcp', element: <DhcpPage /> },
{ path: 'crowdsec', element: <CrowdSecPage /> },
{ path: 'ingress', element: <IngressProxyPage /> },
{ path: 'dns', element: <DnsPage /> },
{ path: 'ddns', element: <DdnsPage /> },
{ path: 'lan-dns', element: <LanDnsPage /> },
{ path: 'vpn', element: <VpnPage /> },
],
},
{
path: '*',
element: <NotFoundPage />,
},
];