import React, { useState, useEffect, useRef, useId, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { CheckCircle, Search, Info, RefreshCw, X, Globe, Shield, BarChart3, Settings, Plus, Trash2, Link, ExternalLink, Unlink } from "lucide-react"; import { colors as themeColors } from '../theme/colors'; import { StatTile, ActionButton, DashboardEmptyState, DashboardLoadingSpinner, SettingsPanel, SectionCard, NumberSpinner, Dropdown, } from './dashboardPrimitives'; export const colors = themeColors; // --- Helper Functions --- export const parseServerDate = (dateString) => { if (!dateString) return 'N/A'; try { const datePart = dateString.split(' ')[0].split('T')[0]; if (datePart && /^\d{4}-\d{2}-\d{2}$/.test(datePart)) { return datePart; } return 'Invalid Date'; } catch (error) { console.error('Error parsing date:', dateString, error); return 'Invalid Date'; } }; export const formatCPU = (cpuStr) => { if (!cpuStr) return { value: 0, unit: 'm', percentage: 0 }; if (cpuStr.endsWith('m')) { const millicores = parseInt(cpuStr) || 0; return { value: millicores, unit: 'm', percentage: Math.min((millicores / 1000) * 100, 100), display: `${millicores}m` }; } const cores = parseFloat(cpuStr) || 0; return { value: cores, unit: 'cores', percentage: Math.min(cores * 100, 100), display: `${cores} core${cores !== 1 ? 's' : ''}` }; }; export const formatMemory = (memoryStr, totalMemoryGB = 1) => { if (!memoryStr) return { value: 0, unit: 'Mi', percentage: 0 }; if (memoryStr.endsWith('Mi')) { const mebibytes = parseInt(memoryStr) || 0; const totalMebibytes = totalMemoryGB * 1024; return { value: mebibytes, unit: 'Mi', percentage: Math.min((mebibytes / totalMebibytes) * 100, 100), display: `${(mebibytes / 1024).toFixed(1)} GB` }; } if (memoryStr.endsWith('Gi') || memoryStr.toLowerCase().includes('gb')) { const gibibytes = parseFloat(memoryStr) || 0; return { value: gibibytes * 1024, unit: 'Gi', percentage: Math.min((gibibytes / totalMemoryGB) * 100, 100), display: `${gibibytes.toFixed(1)} GB` }; } if (memoryStr.toLowerCase().includes('mb')) { const mebibytes = parseFloat(memoryStr) || 0; const totalMebibytes = totalMemoryGB * 1024; return { value: mebibytes, unit: 'Mi', percentage: Math.min((mebibytes / totalMebibytes) * 100, 100), display: `${(mebibytes / 1024).toFixed(1)} GB` }; } return { value: 0, unit: 'Mi', percentage: 0, display: '0 GB' }; }; export const formatFileSize = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; export const getServerStatusInfo = (status) => { switch (status) { case 'Running': return { label: 'Running', color: colors.green, bgColor: `${colors.green}20` }; case 'Stopped': return { label: 'Stopped', color: colors.red, bgColor: `${colors.red}20` }; case 'Starting': return { label: 'Starting', color: colors.yellow, bgColor: `${colors.yellow}20` }; case 'Stopping': return { label: 'Stopping', color: colors.yellow, bgColor: `${colors.yellow}20` }; case 'Restarting': return { label: 'Restarting', color: colors.yellow, bgColor: `${colors.yellow}20` }; case 'Error': return { label: 'Error', color: colors.red, bgColor: `${colors.red}20` }; default: return { label: status && String(status).length ? String(status) : 'Unknown', color: colors.foreground, bgColor: colors.brightBlack, }; } }; export const minecraftVersions = [ { series: '26 Series', versions: ['26.2', '26.1.2', '26.1.1', '26.1'] }, { series: '1.21 Series (Tricky Trials / Mounts of Mayhem)', versions: ['1.21.11', '1.21.10', '1.21.9', '1.21.8', '1.21.7', '1.21.6', '1.21.5', '1.21.4', '1.21.3', '1.21.2', '1.21.1', '1.21'] }, { series: '1.20 Series (Trails & Tales)', versions: ['1.20.6', '1.20.5', '1.20.4', '1.20.3', '1.20.2', '1.20.1', '1.20'] }, { series: '1.19 Series (The Wild Update)', versions: ['1.19.4', '1.19.3', '1.19.2', '1.19.1', '1.19'] }, { series: '1.18 Series (Caves & Cliffs Part II)', versions: ['1.18.2', '1.18.1', '1.18'] }, { series: '1.17 Series (Caves & Cliffs Part I)', versions: ['1.17.1', '1.17'] }, { series: '1.16 Series (Nether Update)', versions: ['1.16.5', '1.16.4', '1.16.3', '1.16.2', '1.16.1', '1.16'] }, { series: '1.15 Series (Buzzy Bees)', versions: ['1.15.2', '1.15.1', '1.15'] }, { series: '1.14 Series (Village & Pillage)', versions: ['1.14.4', '1.14.3', '1.14.2', '1.14.1', '1.14'] }, { series: '1.13 Series (Update Aquatic)', versions: ['1.13.2', '1.13.1', '1.13'] }, { series: '1.12 Series (World of Color Update)', versions: ['1.12.2', '1.12.1', '1.12'] }, { series: '1.11 Series (Exploration Update)', versions: ['1.11.2', '1.11.1', '1.11'] }, { series: '1.10 Series (Frostburn Update)', versions: ['1.10.2', '1.10.1', '1.10'] }, { series: '1.9 Series (Combat Update)', versions: ['1.9.4', '1.9.3', '1.9.2', '1.9.1', '1.9'] }, { series: '1.8 Series (Bountiful Update)', versions: ['1.8.9', '1.8.8', '1.8.7', '1.8.6', '1.8.5', '1.8.4', '1.8.3', '1.8.2', '1.8.1', '1.8'] }, { series: '1.7 Series (The Update that Changed the World)', versions: ['1.7.10', '1.7.9', '1.7.8', '1.7.7', '1.7.6', '1.7.5', '1.7.4', '1.7.2'] }, { series: '1.6 Series (Horse Update)', versions: ['1.6.4', '1.6.2', '1.6.1'] }, { series: '1.5 Series (Redstone Update)', versions: ['1.5.2', '1.5.1', '1.5'] }, { series: '1.4 Series (Pretty Scary Update)', versions: ['1.4.7', '1.4.6', '1.4.5', '1.4.4', '1.4.2'] }, { series: '1.3 Series (Villager Trading Update)', versions: ['1.3.2', '1.3.1'] }, { series: '1.2 Series (Faithful Update)', versions: ['1.2.5', '1.2.4', '1.2.3', '1.2.2', '1.2.1'] }, { series: '1.1 Series (Spawn Egg Update)', versions: ['1.1'] }, { series: '1.0 Series (Adventure Update)', versions: ['1.0.1', '1.0.0'] } ]; export const processMetrics = (metrics) => { if (!metrics) return { cpu: { display: '0m', value: 0 }, memory: { display: '0 Mi', value: 0 }, cpu_percentage: 0, memory_percentage: 0 }; return { cpu: formatCPU(metrics.cpu), memory: formatMemory(metrics.memory), cpu_percentage: metrics.cpu_percentage || 0, memory_percentage: metrics.memory_percentage || 0 }; }; export const parseCpuValue = (cpuStr) => { if (!cpuStr) return 1; const match = cpuStr.match(/(\d+(\.\d+)?)/); return match ? parseFloat(match[1]) : 1; }; export const parseRamValue = (ramStr) => { if (!ramStr) return 1; const match = ramStr.match(/(\d+(\.\d+)?)/); return match ? parseFloat(match[1]) : 1; }; export const parseStorageValue = (storageStr) => { if (!storageStr) return 10; const match = storageStr.match(/(\d+(\.\d+)?)/); return match ? parseFloat(match[1]) : 10; }; // --- Components --- export const PortTooltip = ({ portTooltip }) => { return (
{portTooltip.name}
{portTooltip.description}
{portTooltip.connection}
); }; export const UnifiedTooltip = ({ children, content, show = true, placement = 'top', id: idProp, multiline = true, }) => { const [isVisible, setIsVisible] = useState(false); const [position, setPosition] = useState({ top: 0, left: 0 }); const elementRef = useRef(null); const hideTimeoutRef = useRef(null); const reactId = useId(); const tooltipId = idProp || `dash-tip-${reactId.replace(/:/g, '')}`; useEffect(() => { return () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } }; }, []); if (!show || !content) return children; const updatePosition = () => { if (elementRef.current) { const rect = elementRef.current.getBoundingClientRect(); setPosition({ top: rect.top, left: rect.left + rect.width / 2, bottom: rect.bottom, }); } }; const showTip = () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } updatePosition(); setIsVisible(true); }; const hideTip = () => { hideTimeoutRef.current = setTimeout(() => { setIsVisible(false); hideTimeoutRef.current = null; }, 120); }; const handleMouseEnter = () => showTip(); const handleMouseLeave = () => hideTip(); const handleFocus = (e) => { if (elementRef.current?.contains(e.target)) showTip(); }; const handleBlur = (e) => { if (!elementRef.current?.contains(e.relatedTarget)) hideTip(); }; const getTransform = () => placement === 'bottom' ? 'translate(-50%, 10px)' : 'translate(-50%, calc(-100% - 10px))'; const getTopPosition = () => placement === 'bottom' ? `${position.bottom}px` : `${position.top}px`; const wrapClass = multiline ? 'max-w-xs text-left leading-relaxed whitespace-normal break-words' : 'whitespace-nowrap'; const tooltipContent = isVisible ? ( ) : null; return ( <>
{children}
{tooltipContent && createPortal(tooltipContent, document.body)} ); }; export const CustomTooltip = ({ children, content, show, ...rest }) => ( {children} ); export const ResourceGraph = ({ data, color, title, unit, maxValue, currentValue, percentage, serverStatus }) => { // Ensure data is valid const validData = Array.isArray(data) ? data : []; // Fill missing data points to ensure graph spans full width if less than 60 points const filledData = [...validData]; const maxPoints = 60; if (filledData.length < maxPoints) { const missing = maxPoints - filledData.length; const filler = new Array(missing).fill({ value: 0, time: '', percentage: 0 }); filledData.unshift(...filler); } // Calculate points for SVG path const pointsList = filledData.map((d, i) => { const x = (i / (maxPoints - 1)) * 100; const normalizedValue = Math.min(d.value, maxValue); const y = 100 - ((normalizedValue / (maxValue || 1)) * 100); return { x, y }; }); const pointsStr = pointsList.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' '); // Ensure the path starts at (0,100), goes to the points, and ends at (100,100) then closes const areaPath = `M 0,100 L ${pointsList.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' L ')} L 100,100 Z`; return (
{serverStatus === 'Running' && (
)}
{title}
{currentValue}
{percentage}% of {unit === 'Mi' ? formatFileSize(maxValue * 1024 * 1024) : `${maxValue / 1000} cores`}
{serverStatus === 'Running' ? ( {/* Glow effect line */} `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' L ')}`} fill="none" stroke={color} strokeWidth="6" strokeOpacity="0.2" strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" filter="url(#glow-graph)" /> {/* Main line */} ) : (
Offline
)}
); }; export const MinecraftSettings = ({ server, settings, onSave, isLoading }) => { const [localSettings, setLocalSettings] = useState(settings || {}); const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { if (settings) { setLocalSettings(settings); } }, [settings]); const handleChange = (key, value) => { setLocalSettings(prev => ({ ...prev, [key]: value })); }; const handleToggle = (key) => { const currentValue = localSettings[key] === 'true'; handleChange(key, (!currentValue).toString()); }; const handleSave = () => { onSave(localSettings); }; const settingGroups = [ { title: 'General & World', icon: Globe, settings: [ { key: 'level-name', label: 'World Name', type: 'text', description: 'The name of your world folder' }, { key: 'level-seed', label: 'Level Seed', type: 'text', description: 'Seed for world generation' }, { key: 'gamemode', label: 'Default Gamemode', type: 'select', options: [ { label: 'Survival', value: 'survival' }, { label: 'Creative', value: 'creative' }, { label: 'Adventure', value: 'adventure' }, { label: 'Spectator', value: 'spectator' } ]}, { key: 'difficulty', label: 'Difficulty', type: 'select', options: [ { label: 'Peaceful', value: 'peaceful' }, { label: 'Easy', value: 'easy' }, { label: 'Normal', value: 'normal' }, { label: 'Hard', value: 'hard' } ]}, { key: 'hardcore', label: 'Hardcore Mode', type: 'toggle', description: 'Players are banned on death' }, { key: 'pvp', label: 'PVP Enabled', type: 'toggle' }, { key: 'allow-flight', label: 'Allow Flight', type: 'toggle', description: 'Allow players to fly (requires fly mod or creative)' }, { key: 'allow-nether', label: 'Allow Nether', type: 'toggle' }, ] }, { title: 'Security & Access', icon: Shield, settings: [ { key: 'online-mode', label: 'Online Mode (Premium)', type: 'toggle', description: 'Disable to allow non-premium/cracked players' }, { key: 'white-list', label: 'Whitelist', type: 'toggle', description: 'Only allowed players can join' }, { key: 'enforce-whitelist', label: 'Enforce Whitelist', type: 'toggle' }, { key: 'spawn-protection', label: 'Spawn Protection', type: 'number', description: 'Radius of spawn protection in blocks' }, { key: 'op-permission-level', label: 'OP Permission Level', type: 'select', options: [ { label: 'Level 1 - Bypass Spawn Protection', value: '1' }, { label: 'Level 2 - Basic Commands', value: '2' }, { label: 'Level 3 - Management Commands', value: '3' }, { label: 'Level 4 - All Commands', value: '4' } ]}, { key: 'enable-command-block', label: 'Command Blocks', type: 'toggle' }, ] }, { title: 'Server Information', icon: Info, settings: [ { key: 'motd', label: 'MOTD', type: 'text', description: 'Message of the Day (shown in server list)' }, { key: 'max-players', label: 'Max Players', type: 'number' }, { key: 'view-distance', label: 'View Distance', type: 'number', description: 'Chunks sent to players (3-32)' }, { key: 'simulation-distance', label: 'Simulation Distance', type: 'number', description: 'Chunks to tick (3-32)' }, ] }, { title: 'Technical & Performance', icon: BarChart3, settings: [ { key: 'sync-chunk-writes', label: 'Sync Chunk Writes', type: 'toggle' }, { key: 'max-tick-time', label: 'Max Tick Time', type: 'number', description: 'Max ms before watchdog kills server (-1 to disable)' }, { key: 'network-compression-threshold', label: 'Network Compression', type: 'number' }, { key: 'entity-broadcast-range-percentage', label: 'Entity Range %', type: 'number' }, { key: 'enable-query', label: 'Enable Query', type: 'toggle' }, { key: 'enable-rcon', label: 'Enable RCON', type: 'toggle' }, ] } ]; // Map all current settings not in specific groups to an "Other" category if they aren't empty const knownKeys = new Set(settingGroups.flatMap(g => g.settings.map(s => s.key))); const otherSettings = Object.entries(localSettings) .filter(([key]) => !knownKeys.has(key) && key.trim() !== '') .map(([key, value]) => ({ key, label: key.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), type: (value === 'true' || value === 'false') ? 'toggle' : (isNaN(value) || value === '' ? 'text' : 'number'), description: 'Advanced property from server.properties' })); if (otherSettings.length > 0) { settingGroups.push({ title: 'Other Settings', icon: Settings, settings: otherSettings }); } const filteredGroups = settingGroups.map(group => ({ ...group, settings: group.settings.filter(s => s.label.toLowerCase().includes(searchTerm.toLowerCase()) || s.key.toLowerCase().includes(searchTerm.toLowerCase()) || (s.description && s.description.toLowerCase().includes(searchTerm.toLowerCase())) ) })).filter(group => group.settings.length > 0); return (
{/* Header with search and save */}

Direct control over server.properties

setSearchTerm(e.target.value)} className="w-full bg-black/40 border border-dash-border rounded-xl px-4 py-3 pl-11 text-sm outline-none focus:border-primary/50 focus:ring-4 focus:ring-primary/10 transition-all font-bold" style={{ color: colors.brightWhite }} />
{isLoading ? ( ) : ( )} Save Configuration
{filteredGroups.map((group, gIdx) => (
{group.settings.map((s) => (
{s.key}
{s.type === 'toggle' && ( )} {s.type === 'select' && (
handleChange(s.key, val)} placeholder="Select..." />
)}
{s.type === 'text' && ( handleChange(s.key, e.target.value)} className="bg-black/40 border border-dash-border rounded-xl px-4 py-3 text-sm outline-none focus:border-primary/50 focus:bg-black/60 w-full transition-all font-bold shadow-inner" style={{ color: colors.brightWhite }} /> )} {s.type === 'number' && ( handleChange(s.key, val)} className="w-full h-12" /> )} {s.description && (

{s.description}

)}
))}
))}
{filteredGroups.length === 0 && ( setSearchTerm('')}> Clear Search } /> )}
Restart Required

Most changes to server.properties require a server restart to take effect. Use the Restart action in the header after saving your changes.

); }; export const DomainManager = ({ userId, username, servers, sendMessage, showAlert, showConfirm }) => { const [domains, setDomains] = useState([]); const [customDomains, setCustomDomains] = useState([]); const [isLoading, setIsLoading] = useState(true); const [newDomainName, setNewDomainName] = useState(''); const [selectedServerForLink, setSelectedServerForLink] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [showLinkModal, setShowLinkModal] = useState(false); const [selectedDomainForLink, setSelectedDomainForLink] = useState(null); const [errorMessage, setErrorMessage] = useState(''); const [successMessage, setSuccessMessage] = useState(''); const [actionInProgress, setActionInProgress] = useState(false); // Custom domains state const [showCustomDomainModal, setShowCustomDomainModal] = useState(false); const [customDomainName, setCustomDomainName] = useState(''); const [verifyingCustomDomain, setVerifyingCustomDomain] = useState(false); const [customDomainVerificationResult, setCustomDomainVerificationResult] = useState(null); const [verificationStep, setVerificationStep] = useState('idle'); const linkableServers = useMemo( () => servers.filter((s) => ['website', 'vps'].includes(s.type)), [servers] ); const serverLinkOptions = useMemo( () => [ { value: '', label: 'No server (link later)' }, ...linkableServers.map((s) => ({ value: s.id, label: s.name })), ], [linkableServers] ); // Clear messages after timeout useEffect(() => { if (errorMessage || successMessage) { const timer = setTimeout(() => { setErrorMessage(''); setSuccessMessage(''); }, 5000); return () => clearTimeout(timer); } }, [errorMessage, successMessage]); // Load domains function const loadDomains = useCallback(() => { setIsLoading(true); setErrorMessage(''); sendMessage({ type: 'list_domains', includeServerless: true }); }, [sendMessage]); const loadCustomDomains = useCallback(() => { sendMessage({ type: 'list_custom_domains' }); }, [sendMessage]); // Initial load and periodic refresh useEffect(() => { loadDomains(); loadCustomDomains(); const interval = setInterval(() => { loadDomains(); loadCustomDomains(); }, 30000); return () => clearInterval(interval); }, [loadDomains, loadCustomDomains]); // WebSocket message handler useEffect(() => { const handleMessage = (data) => { switch (data.type) { case 'domains_list': setDomains(data.domains || []); setIsLoading(false); break; case 'custom_domains_list': setCustomDomains(data.domains || []); break; case 'domain_created': setSuccessMessage(data.message || 'Domain created successfully!'); setShowCreateModal(false); setNewDomainName(''); setActionInProgress(false); loadDomains(); break; case 'custom_domain_created': setSuccessMessage(data.message || 'Custom domain added successfully!'); setShowCustomDomainModal(false); setCustomDomainName(''); setCustomDomainVerificationResult(null); setVerificationStep('idle'); setActionInProgress(false); loadCustomDomains(); break; case 'custom_domain_verified': if (data.verified) { setCustomDomainVerificationResult({ success: true, message: 'Domain verified successfully!', ipAddress: data.ip_address }); setVerificationStep('success'); } else { setCustomDomainVerificationResult({ success: false, message: `Verification failed. Please update DNS records.`, currentIp: data.current_ip }); setVerificationStep('error'); } setVerifyingCustomDomain(false); break; case 'domain_deleted': setSuccessMessage(data.message || 'Domain deleted successfully!'); setActionInProgress(false); loadDomains(); break; case 'custom_domain_deleted': setSuccessMessage(data.message || 'Custom domain deleted successfully!'); setActionInProgress(false); loadCustomDomains(); break; case 'domain_linked': setSuccessMessage(data.message || 'Domain linked successfully!'); setShowLinkModal(false); setSelectedDomainForLink(null); setActionInProgress(false); loadDomains(); loadCustomDomains(); break; case 'domain_unlinked': setSuccessMessage(data.message || 'Domain unlinked successfully!'); setActionInProgress(false); loadDomains(); loadCustomDomains(); break; case 'error': if (data.error.includes('domain') || data.error.includes('Domain')) { setErrorMessage(data.error || 'An error occurred'); setIsLoading(false); setActionInProgress(false); } break; default: break; } }; window.__domainManagerHandler = handleMessage; return () => { delete window.__domainManagerHandler; }; }, [loadDomains, loadCustomDomains]); const createDomain = () => { if (!newDomainName.trim()) { setErrorMessage('Please enter a domain name'); return; } // Validate domain name format const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/; if (!domainRegex.test(newDomainName)) { setErrorMessage('Domain name can only contain letters, numbers, and hyphens. Cannot start or end with hyphen.'); return; } if (newDomainName.length > 63) { setErrorMessage('Domain name must be 63 characters or less'); return; } setActionInProgress(true); setErrorMessage(''); setSuccessMessage(''); sendMessage({ type: 'create_domain', subdomain: newDomainName.trim().toLowerCase(), serverId: selectedServerForLink || null }); }; const deleteDomain = (domainId, isCustom = false) => { showConfirm('Delete Domain?', 'Are you sure? This cannot be undone.', () => { setActionInProgress(true); sendMessage({ type: isCustom ? 'delete_custom_domain' : 'delete_domain', domainId: domainId }); }); }; const openLinkModal = (domain, isCustom = false) => { setSelectedDomainForLink({ ...domain, isCustom }); setSelectedServerForLink(domain.server_id || ''); setShowLinkModal(true); }; const confirmLinkDomain = () => { if (!selectedDomainForLink) return; if (!selectedServerForLink) { setErrorMessage('Please select a server to link'); return; } setActionInProgress(true); setErrorMessage(''); sendMessage({ type: 'link_domain', domainId: selectedDomainForLink.id, serverId: selectedServerForLink, }); }; const unlinkDomain = (domainId) => { showConfirm('Unlink Domain?', 'Remove the server association for this domain?', () => { setActionInProgress(true); sendMessage({ type: 'unlink_domain', domainId }); }); }; const getDomainStatus = (domain) => { if (!domain.server_id) return { label: 'Unlinked', color: colors.yellow, bgColor: `${colors.yellow}20` }; const linkedServer = servers.find(s => s.id === domain.server_id); if (!linkedServer) return { label: 'Server Not Found', color: colors.red, bgColor: `${colors.red}20` }; return { label: `Linked: ${linkedServer.name}`, color: colors.green, bgColor: `${colors.green}20` }; }; const NODE_IP = '85.237.179.121'; return (
{/* Summary Stats */} {!isLoading && (domains.length > 0 || customDomains.length > 0) && (
d.server_id).length} accentColor={colors.green} />
)} {/* Delegated Domains */} 0 && ( setShowCreateModal(true)} variant="primary" className="!px-4 !py-2 !min-h-0 sm:!min-h-0"> Create Domain ) } > {isLoading ? ( ) : domains.length === 0 ? ( setShowCreateModal(true)} /> ) : (
{domains.map(domain => { const status = getDomainStatus(domain); return (

{domain.full_domain}

{domain.server_id ? ( unlinkDomain(domain.id)} variant="secondary" className="!px-3 !py-2 !min-h-0" > ) : ( openLinkModal(domain)} variant="primary" className="!px-3 !py-2 !min-h-0" > )} deleteDomain(domain.id)} variant="danger" className="!px-3 !py-2 !min-h-0">
{status.label}
Protected
); })}
)}
{/* Custom Domains Section */} 0 && ( setShowCustomDomainModal(true)} variant="primary" className="!px-4 !py-2 !min-h-0 sm:!min-h-0"> Add Custom ) } > {customDomains.length === 0 ? ( setShowCustomDomainModal(true)} /> ) : (
{customDomains.map(domain => { const status = getDomainStatus(domain); return (

{domain.domain_name}

{domain.server_id ? ( unlinkDomain(domain.id)} variant="secondary" className="!px-3 !py-2 !min-h-0" > ) : ( openLinkModal(domain, true)} variant="primary" className="!px-3 !py-2 !min-h-0" > )} deleteDomain(domain.id, true)} variant="danger" className="!px-3 !py-2 !min-h-0">
{status.label}
Verified
); })}
)}
{/* Create Delegated Domain Modal */} {showCreateModal && createPortal(

New Subdomain

setNewDomainName(e.target.value)} placeholder="myserver" className="w-full bg-black/40 border border-dash-border rounded-xl px-4 py-3 text-sm font-bold focus:outline-none focus:border-primary/50 transition-colors pr-32" style={{ color: colors.brightWhite }} />
.anchorhost.eu
setShowCreateModal(false)} variant="secondary" className="flex-1">Cancel {actionInProgress ? : 'Create Domain'}
, document.body )} {/* Add Custom Domain Modal */} {showCustomDomainModal && createPortal(

Custom Domain

setCustomDomainName(e.target.value)} placeholder="play.example.com" className="w-full bg-black/40 border border-dash-border rounded-xl px-4 py-3 text-sm font-bold focus:outline-none focus:border-cyan-500/50 transition-colors" style={{ color: colors.brightWhite }} />

Instructions

To link your domain, you must point its A record to {NODE_IP} before adding it here.

{customDomainVerificationResult && (
{customDomainVerificationResult.message}
)} {verificationStep !== 'idle' && verifyingCustomDomain && (

Verifying DNS records...

)}
setShowCustomDomainModal(false)} variant="secondary" className="flex-1">Cancel { setActionInProgress(true); sendMessage({ type: 'create_custom_domain', domainName: customDomainName.trim().toLowerCase() }); }} disabled={actionInProgress || !customDomainName.trim()} variant="primary" className="flex-1" > {actionInProgress ? : 'Add Domain'}
, document.body )} {/* Link Domain to Server Modal */} {showLinkModal && selectedDomainForLink && createPortal(

Link Domain

{selectedDomainForLink.full_domain || selectedDomainForLink.domain_name}

0 ? linkableServers.map((s) => ({ value: s.id, label: s.name })) : [{ value: '', label: 'No eligible servers' }]} onChange={setSelectedServerForLink} placeholder="Select server..." />
{ setShowLinkModal(false); setSelectedDomainForLink(null); }} variant="secondary" className="flex-1"> Cancel {actionInProgress ? : 'Link Domain'}
, document.body )}
); }; export { DashboardViewShell, PageHeaderBar, StatTile, ActionButton, DashboardEmptyState, DashboardLoadingSpinner, SettingsPanel, SectionCard, Slider, NumberSpinner, Dropdown, InlineDropdown } from './dashboardPrimitives';