'use client' import { use, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { useEnterpriseClient, useUpdateEnterpriseClient, useDeleteEnterpriseClient, useClientServers, useServerAction, useErrorRules, useCreateErrorRule, useDetectedErrors, useAcknowledgeError, useAddServerToClient, } from '@/hooks/use-enterprise-clients' import { useNetcupServers } from '@/hooks/use-netcup' import { ArrowLeft, Building2, Server, AlertTriangle, Mail, Phone, FileText, Edit, Trash2, Power, RotateCcw, Plus, Check, X, Loader2, AlertCircle, RefreshCw, Activity, HardDrive, Cpu, MemoryStick, Clock, ShieldAlert, Eye, } from 'lucide-react' import type { EnterpriseServerWithStatus, ErrorDetectionRule, DetectedError, ErrorSeverity } from '@/types/api' // Overview stat card function OverviewCard({ title, value, icon: Icon, className = '' }: { title: string value: string | number icon: typeof Cpu className?: string }) { return (

{title}

{value}

) } // Server card component function ServerCard({ server, clientId, onPowerAction }: { server: EnterpriseServerWithStatus clientId: string onPowerAction: (serverId: string, command: 'ON' | 'OFF' | 'POWERCYCLE') => void }) { const statusColors: Record = { running: { bg: 'bg-emerald-50 dark:bg-emerald-950/30', text: 'text-emerald-700 dark:text-emerald-400', dot: 'bg-emerald-500 animate-pulse' }, stopped: { bg: 'bg-slate-50 dark:bg-slate-950/30', text: 'text-slate-600 dark:text-slate-400', dot: 'bg-slate-400' }, error: { bg: 'bg-red-50 dark:bg-red-950/30', text: 'text-red-700 dark:text-red-400', dot: 'bg-red-500' }, unknown: { bg: 'bg-amber-50 dark:bg-amber-950/30', text: 'text-amber-700 dark:text-amber-400', dot: 'bg-amber-500' } } const status = server.netcupStatus?.toLowerCase() || 'unknown' const colors = statusColors[status] || statusColors.unknown return (
{server.nickname || server.netcupServerId} {server.purpose && (

{server.purpose}

)}
{status.charAt(0).toUpperCase() + status.slice(1)}
{server.netcupIps?.length > 0 && (

{server.netcupIps[0]}

)}
{status === 'running' ? ( <> ) : ( )}
) } // Error rule row function ErrorRuleRow({ rule }: { rule: ErrorDetectionRule }) { const severityColors: Record = { INFO: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', WARNING: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', ERROR: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', CRITICAL: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' } return (
{rule.isActive ? ( ) : ( )}

{rule.name}

{rule.pattern}

{rule.severity} {rule._count?.detectedErrors || 0} matches
) } // Detected error row function DetectedErrorRow({ error, onAcknowledge }: { error: DetectedError onAcknowledge: () => void }) { const severityColors: Record = { INFO: 'border-l-blue-500', WARNING: 'border-l-amber-500', ERROR: 'border-l-red-500', CRITICAL: 'border-l-purple-500' } const isAcknowledged = !!error.acknowledgedAt return (
{error.rule?.name} {error.containerName && ( in {error.containerName} )}

{error.logLine}

{new Date(error.timestamp).toLocaleString()} {error.server && ` • ${error.server.nickname || error.server.netcupServerId}`}

{!isAcknowledged && ( )}
) } // Add rule dialog function AddRuleDialog({ open, onOpenChange, clientId }: { open: boolean onOpenChange: (open: boolean) => void clientId: string }) { const [formData, setFormData] = useState({ name: '', pattern: '', severity: 'WARNING' as ErrorSeverity, description: '' }) const createRule = useCreateErrorRule() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() try { await createRule.mutateAsync({ clientId, data: { name: formData.name, pattern: formData.pattern, severity: formData.severity, description: formData.description || undefined } }) setFormData({ name: '', pattern: '', severity: 'WARNING', description: '' }) onOpenChange(false) } catch { // Error handled by mutation } } return ( Add Error Detection Rule Create a regex pattern to detect errors in container logs.
setFormData({ ...formData, name: e.target.value })} placeholder="Database Connection Failed" required />
setFormData({ ...formData, pattern: e.target.value })} placeholder="error|ERROR|failed|FAILED" className="font-mono text-sm" required />
setFormData({ ...formData, description: e.target.value })} placeholder="Detects database connection errors" />
) } // Add server dialog function AddServerDialog({ open, onOpenChange, clientId, existingServerIds }: { open: boolean onOpenChange: (open: boolean) => void clientId: string existingServerIds: string[] }) { const [formData, setFormData] = useState({ netcupServerId: '', nickname: '', purpose: '', portainerUrl: '', portainerUsername: '', portainerPassword: '' }) const { data: netcupServers, isLoading: loadingServers } = useNetcupServers(false) const addServer = useAddServerToClient() // Filter out servers that are already linked const availableServers = netcupServers?.servers?.filter( (s: { id: string }) => !existingServerIds.includes(s.id) ) || [] const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!formData.netcupServerId) return try { await addServer.mutateAsync({ clientId, data: { netcupServerId: formData.netcupServerId, nickname: formData.nickname || undefined, purpose: formData.purpose || undefined, portainerUrl: formData.portainerUrl || undefined, portainerUsername: formData.portainerUsername || undefined, portainerPassword: formData.portainerPassword || undefined } }) setFormData({ netcupServerId: '', nickname: '', purpose: '', portainerUrl: '', portainerUsername: '', portainerPassword: '' }) onOpenChange(false) } catch { // Error handled by mutation } } return ( Add Server to Client Link a Netcup server to this enterprise client.
{loadingServers ? (
Loading servers...
) : availableServers.length === 0 ? (
No available servers. All Netcup servers are already linked.
) : ( )}
setFormData({ ...formData, nickname: e.target.value })} placeholder="Production Server" />
setFormData({ ...formData, purpose: e.target.value })} placeholder="Web hosting" />

Portainer Credentials (Optional)

setFormData({ ...formData, portainerUrl: e.target.value })} placeholder="https://portainer.example.com" />
setFormData({ ...formData, portainerUsername: e.target.value })} placeholder="admin" />
setFormData({ ...formData, portainerPassword: e.target.value })} placeholder="••••••••" />
) } export default function EnterpriseClientDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id: clientId } = use(params) const router = useRouter() const [showAddRuleDialog, setShowAddRuleDialog] = useState(false) const [showAddServerDialog, setShowAddServerDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const { data: client, isLoading, isError, error, refetch } = useEnterpriseClient(clientId) const { data: servers } = useClientServers(clientId) const { data: errorRules } = useErrorRules(clientId) const { data: detectedErrors } = useDetectedErrors(clientId, { acknowledged: false, limit: 50 }) const deleteClient = useDeleteEnterpriseClient() const serverAction = useServerAction() const acknowledgeError = useAcknowledgeError() const handlePowerAction = async (serverId: string, command: 'ON' | 'OFF' | 'POWERCYCLE') => { try { await serverAction.mutateAsync({ clientId, serverId, action: { action: 'power', command } }) } catch { // Error handled by mutation } } const handleAcknowledgeError = async (errorId: string) => { try { await acknowledgeError.mutateAsync({ clientId, errorId }) } catch { // Error handled by mutation } } const handleDeleteClient = async () => { try { await deleteClient.mutateAsync(clientId) router.push('/admin/enterprise-clients') } catch { // Error handled by mutation } } if (isLoading) { return (

Loading client details...

) } if (isError || !client) { return (

Failed to load client

{error instanceof Error ? error.message : 'Client not found'}

) } return (
{/* Header */}

{client.name}

{client.companyName && (

{client.companyName}

)}
{/* Client info cards */}

Contact Email

{client.contactEmail}

{client.contactPhone && (

Contact Phone

{client.contactPhone}

)} {client.notes && (

Notes

{client.notes}

)}
{/* Overview stats */}
{/* Tabs */} Servers ({servers?.length || 0}) Errors ({detectedErrors?.length || 0}) Rules ({errorRules?.length || 0})

{servers?.length || 0} server{servers?.length !== 1 ? 's' : ''} linked

{servers && servers.length > 0 ? (
{servers.map((server) => ( ))}
) : (

No servers linked to this client yet.

)}
{detectedErrors && detectedErrors.length > 0 ? (
{detectedErrors.map((err) => ( handleAcknowledgeError(err.id)} /> ))}
) : (

No unacknowledged errors.

)}

Define regex patterns to detect errors in container logs.

{errorRules && errorRules.length > 0 ? (
{errorRules.map((rule) => ( ))}
) : (

No error detection rules configured.

)}
{/* Add Rule Dialog */} {/* Add Server Dialog */} s.netcupServerId) || []} /> {/* Delete Confirmation Dialog */} Delete Enterprise Client Are you sure you want to delete {client.name}? This will remove all associated servers, error rules, and detected errors. This action cannot be undone.
) }