feat: Show Registered Services on Ingress Proxy page

Replace "Instance Routes" with "Registered Services" on the Central
Ingress page. Services are fetched from /api/v1/services and display
name, domain, backend, source, reach level, and backend type.

Central no longer manages instances directly — it shows services
registered by Wild Cloud, Wild Works, and itself.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 05:43:32 +00:00
parent 57a597fe5a
commit 75a82becb9
4 changed files with 76 additions and 34 deletions

View File

@@ -4,12 +4,12 @@ import { Button } from './ui/button';
import { Input, Label } from './ui';
import { Alert, AlertDescription } from './ui/alert';
import { Textarea } from './ui/textarea';
import { Network, Loader2, AlertCircle, CheckCircle, Play, RotateCw, Plus, Trash2, X, Check, Activity, ChevronDown, ChevronUp } from 'lucide-react';
import { Network, Loader2, AlertCircle, CheckCircle, Play, RotateCw, Plus, Trash2, X, Check, Activity, ChevronDown, ChevronUp, Globe, Server } from 'lucide-react';
import { Badge } from './ui/badge';
import { useConfig } from '../hooks';
import { useHaproxy } from '../hooks/useHaproxy';
import { useServices } from '../hooks/useServices';
import type { HAProxyCustomRoute } from '../types';
import type { HaproxyInstanceRoute } from '../services/api/networking';
import { haproxyApi } from '../services/api/networking';
export function IngressProxyComponent() {
@@ -27,6 +27,8 @@ export function IngressProxyComponent() {
isRestarting: isHaproxyRestarting,
} = useHaproxy();
const { services, isLoading: isServicesLoading } = useServices();
const [showAddCustomRoute, setShowAddCustomRoute] = useState(false);
const [newCustomRoute, setNewCustomRoute] = useState<HAProxyCustomRoute>({ name: '', port: 0, backend: '' });
const [successMessage, setSuccessMessage] = useState<string | null>(null);
@@ -133,7 +135,7 @@ export function IngressProxyComponent() {
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Instance Routes</CardTitle>
<CardTitle>Registered Services</CardTitle>
{isHaproxyLoading ? (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
) : (
@@ -145,37 +147,39 @@ export function IngressProxyComponent() {
</div>
</CardHeader>
<CardContent className="space-y-4">
{(() => {
const routes: HaproxyInstanceRoute[] = haproxyStatus?.routes ?? [];
return routes.length > 0 ? (
{isServicesLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />Loading services...
</div>
) : services.length > 0 ? (
<div className="border rounded-lg divide-y text-sm">
{routes.map(r => (
<div key={r.name} className="px-3 py-2">
{services.map(s => (
<div key={s.name} className="px-3 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
<span className="font-mono text-xs">*.{r.domain}</span>
</div>
<span className="font-mono text-xs text-muted-foreground">{r.backendIP}</span>
</div>
{r.extraDomains && r.extraDomains.length > 0 && (
<div className="mt-1 ml-5 flex flex-wrap gap-1">
{r.extraDomains.map(d => (
<span key={d} className="font-mono text-xs text-muted-foreground bg-muted rounded px-1.5 py-0.5">
{d}
</span>
))}
</div>
{s.reach === 'public' ? (
<Globe className="h-3.5 w-3.5 text-blue-500 shrink-0" />
) : (
<Server className="h-3.5 w-3.5 text-green-500 shrink-0" />
)}
<span className="font-medium">{s.name}</span>
<span className="font-mono text-xs text-muted-foreground">{s.domain}</span>
</div>
<span className="font-mono text-xs text-muted-foreground">{s.backend.address}</span>
</div>
<div className="mt-1 ml-5 flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>source: <span className="text-foreground">{s.source}</span></span>
<span>reach: <span className="text-foreground">{s.reach}</span></span>
<span>type: <span className="text-foreground">{s.backend.type}</span></span>
</div>
</div>
))}
</div>
) : (
<p className="text-xs text-muted-foreground">
No instances registered click Generate &amp; Apply to configure.
No services registered click Generate &amp; Apply to configure.
</p>
);
})()}
)}
{haproxyGenerateData && (
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">

View File

@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { servicesApi } from '../services/api/networking';
export const useServices = () => {
const query = useQuery({
queryKey: ['services'],
queryFn: () => servicesApi.list(),
refetchInterval: 30000,
staleTime: 10000,
});
return {
services: query.data?.services ?? [],
isLoading: query.isLoading,
error: query.error,
};
};

View File

@@ -1,8 +1,8 @@
export { apiClient, ApiError } from './client';
export * from './types';
export { dnsmasqApi } from './dnsmasq';
export { haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
export type { HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
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 { vpnApi } from './vpn';
export type { VpnStatus, VpnConfig, VpnPeer, VpnPeerConfig } from './vpn';
export { certApi } from './cert';

View File

@@ -1,5 +1,26 @@
import { apiClient } from './client';
// Services
export interface RegisteredService {
name: string;
source: string;
domain: string;
backend: {
address: string;
type: string;
health?: string;
};
reach: string;
tls?: string;
}
export const servicesApi = {
async list(): Promise<{ services: RegisteredService[] }> {
return apiClient.get('/api/v1/services');
},
};
// HAProxy
export interface HaproxyInstanceRoute {