import React, { useState, useEffect, useRef, useContext, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; // import { Card, CardContent } from "./components/ui/card"; import { ChevronDown, X, Server, Terminal, User, Settings, Plus, Trash2, Play, Square, RefreshCw, Cpu, HardDrive, MemoryStick, Copy, Maximize2, Minimize2, AlertCircle, ShoppingCart, Coins, Globe, Database, Activity, Menu, LogOut, Wifi, WifiOff, Home, BookOpen, BarChart3, HardDriveIcon, NetworkIcon, Clock, Info, CheckCircle, Gamepad2, Folder, Edit, Check, Package, Shield, MapPin, ExternalLink, AlertTriangle, Tag, Layers, Coffee, MessageCircle, Mail, Lock, Eye, EyeOff, ArrowRight, } from "lucide-react"; import ServerCreationForm from '../features/servers/ServerCreationForm.jsx' import ServerCard from '../features/servers/ServerCard.jsx' import StoreSection from '../features/store/StoreSection.jsx' import TicketSystem from '../features/support/TicketSystem.jsx' import useWebSocket from '../hooks/useWebSocket.js'; import { AuthContext } from '../App.jsx'; import GameConsole from '../features/servers/GameConsole.jsx'; import { colors, parseServerDate, formatCPU, formatMemory, getServerStatusInfo, UnifiedTooltip, Dropdown, InlineDropdown, Slider, SectionCard, ResourceGraph, minecraftVersions, MinecraftSettings, DashboardViewShell, PageHeaderBar, StatTile, DomainManager, ActionButton, DashboardEmptyState, DashboardLoadingSpinner, SettingsPanel, } from '../components/DashboardComponents.jsx'; import { dashLayout } from '../theme/dashboardTokens.js'; import FileManager from '../features/servers/FileManager.jsx'; import BackupSection from '../features/servers/BackupSection.jsx'; import PluginStoreEmbedded from '../features/plugins/PluginStoreEmbedded.jsx'; import { usePanelCurrency } from '../hooks/usePanelCurrency.js'; import { API_BASE_URL, WS_BASE_URL as WS_URL } from '../config/api'; import branding from '../branding'; const NODE_IP = '85.237.179.121'; const currencyFlags = { 'USD': 'πŸ‡ΊπŸ‡Έ', // US flag 'EUR': 'πŸ‡ͺπŸ‡Ί', // European Union flag 'GBP': 'πŸ‡¬πŸ‡§', // UK flag 'PLN': 'πŸ‡΅πŸ‡±', // Poland flag 'PHP': 'πŸ‡΅πŸ‡­', // Philippines flag 'CZK': 'πŸ‡¨πŸ‡Ώ', // Czech Republic flag // Add more as needed }; // Country list with codes and dialing codes (from your list) const countries = [ { code: 'AF', dialCode: '+93', name: 'Afghanistan' }, { code: 'AL', dialCode: '+355', name: 'Albania' }, { code: 'DZ', dialCode: '+213', name: 'Algeria' }, { code: 'AD', dialCode: '+376', name: 'Andorra' }, { code: 'AO', dialCode: '+244', name: 'Angola' }, { code: 'AG', dialCode: '+1-268', name: 'Antigua and Barbuda' }, { code: 'AR', dialCode: '+54', name: 'Argentina' }, { code: 'AM', dialCode: '+374', name: 'Armenia' }, { code: 'AU', dialCode: '+61', name: 'Australia' }, { code: 'AT', dialCode: '+43', name: 'Austria' }, { code: 'AZ', dialCode: '+994', name: 'Azerbaijan' }, { code: 'BS', dialCode: '+1-242', name: 'Bahamas' }, { code: 'BH', dialCode: '+973', name: 'Bahrain' }, { code: 'BD', dialCode: '+880', name: 'Bangladesh' }, { code: 'BB', dialCode: '+1-246', name: 'Barbados' }, { code: 'BY', dialCode: '+375', name: 'Belarus' }, { code: 'BE', dialCode: '+32', name: 'Belgium' }, { code: 'BZ', dialCode: '+501', name: 'Belize' }, { code: 'BJ', dialCode: '+229', name: 'Benin' }, { code: 'BT', dialCode: '+975', name: 'Bhutan' }, { code: 'BO', dialCode: '+591', name: 'Bolivia' }, { code: 'BA', dialCode: '+387', name: 'Bosnia and Herzegovina' }, { code: 'BW', dialCode: '+267', name: 'Botswana' }, { code: 'BR', dialCode: '+55', name: 'Brazil' }, { code: 'BN', dialCode: '+673', name: 'Brunei' }, { code: 'BG', dialCode: '+359', name: 'Bulgaria' }, { code: 'BF', dialCode: '+226', name: 'Burkina Faso' }, { code: 'BI', dialCode: '+257', name: 'Burundi' }, { code: 'KH', dialCode: '+855', name: 'Cambodia' }, { code: 'CM', dialCode: '+237', name: 'Cameroon' }, { code: 'CA', dialCode: '+1', name: 'Canada' }, { code: 'CV', dialCode: '+238', name: 'Cape Verde' }, { code: 'CF', dialCode: '+236', name: 'Central African Republic' }, { code: 'TD', dialCode: '+235', name: 'Chad' }, { code: 'CL', dialCode: '+56', name: 'Chile' }, { code: 'CN', dialCode: '+86', name: 'China' }, { code: 'CO', dialCode: '+57', name: 'Colombia' }, { code: 'KM', dialCode: '+269', name: 'Comoros' }, { code: 'CG', dialCode: '+242', name: 'Congo' }, { code: 'CR', dialCode: '+506', name: 'Costa Rica' }, { code: 'HR', dialCode: '+385', name: 'Croatia' }, { code: 'CU', dialCode: '+53', name: 'Cuba' }, { code: 'CY', dialCode: '+357', name: 'Cyprus' }, { code: 'CZ', dialCode: '+420', name: 'Czech Republic' }, { code: 'DK', dialCode: '+45', name: 'Denmark' }, { code: 'DJ', dialCode: '+253', name: 'Djibouti' }, { code: 'DM', dialCode: '+1-767', name: 'Dominica' }, { code: 'DO', dialCode: '+1-809', name: 'Dominican Republic' }, { code: 'EC', dialCode: '+593', name: 'Ecuador' }, { code: 'EG', dialCode: '+20', name: 'Egypt' }, { code: 'SV', dialCode: '+503', name: 'El Salvador' }, { code: 'GQ', dialCode: '+240', name: 'Equatorial Guinea' }, { code: 'ER', dialCode: '+291', name: 'Eritrea' }, { code: 'EE', dialCode: '+372', name: 'Estonia' }, { code: 'ET', dialCode: '+251', name: 'Ethiopia' }, { code: 'FJ', dialCode: '+679', name: 'Fiji' }, { code: 'FI', dialCode: '+358', name: 'Finland' }, { code: 'FR', dialCode: '+33', name: 'France' }, { code: 'GA', dialCode: '+241', name: 'Gabon' }, { code: 'GM', dialCode: '+220', name: 'Gambia' }, { code: 'GE', dialCode: '+995', name: 'Georgia' }, { code: 'DE', dialCode: '+49', name: 'Germany' }, { code: 'GH', dialCode: '+233', name: 'Ghana' }, { code: 'GR', dialCode: '+30', name: 'Greece' }, { code: 'GD', dialCode: '+1-473', name: 'Grenada' }, { code: 'GT', dialCode: '+502', name: 'Guatemala' }, { code: 'GN', dialCode: '+224', name: 'Guinea' }, { code: 'GW', dialCode: '+245', name: 'Guinea-Bissau' }, { code: 'GY', dialCode: '+592', name: 'Guyana' }, { code: 'HT', dialCode: '+509', name: 'Haiti' }, { code: 'HN', dialCode: '+504', name: 'Honduras' }, { code: 'HU', dialCode: '+36', name: 'Hungary' }, { code: 'IS', dialCode: '+354', name: 'Iceland' }, { code: 'IN', dialCode: '+91', name: 'India' }, { code: 'ID', dialCode: '+62', name: 'Indonesia' }, { code: 'IR', dialCode: '+98', name: 'Iran' }, { code: 'IQ', dialCode: '+964', name: 'Iraq' }, { code: 'IE', dialCode: '+353', name: 'Ireland' }, { code: 'IL', dialCode: '+972', name: 'Israel' }, { code: 'IT', dialCode: '+39', name: 'Italy' }, { code: 'JM', dialCode: '+1-876', name: 'Jamaica' }, { code: 'JP', dialCode: '+81', name: 'Japan' }, { code: 'JO', dialCode: '+962', name: 'Jordan' }, { code: 'KZ', dialCode: '+7', name: 'Kazakhstan' }, { code: 'KE', dialCode: '+254', name: 'Kenya' }, { code: 'KI', dialCode: '+686', name: 'Kiribati' }, { code: 'KP', dialCode: '+850', name: 'North Korea' }, { code: 'KR', dialCode: '+82', name: 'South Korea' }, { code: 'KW', dialCode: '+965', name: 'Kuwait' }, { code: 'KG', dialCode: '+996', name: 'Kyrgyzstan' }, { code: 'LA', dialCode: '+856', name: 'Laos' }, { code: 'LV', dialCode: '+371', name: 'Latvia' }, { code: 'LB', dialCode: '+961', name: 'Lebanon' }, { code: 'LS', dialCode: '+266', name: 'Lesotho' }, { code: 'LR', dialCode: '+231', name: 'Liberia' }, { code: 'LY', dialCode: '+218', name: 'Libya' }, { code: 'LI', dialCode: '+423', name: 'Liechtenstein' }, { code: 'LT', dialCode: '+370', name: 'Lithuania' }, { code: 'LU', dialCode: '+352', name: 'Luxembourg' }, { code: 'MG', dialCode: '+261', name: 'Madagascar' }, { code: 'MW', dialCode: '+265', name: 'Malawi' }, { code: 'MY', dialCode: '+60', name: 'Malaysia' }, { code: 'MV', dialCode: '+960', name: 'Maldives' }, { code: 'ML', dialCode: '+223', name: 'Mali' }, { code: 'MT', dialCode: '+356', name: 'Malta' }, { code: 'MH', dialCode: '+692', name: 'Marshall Islands' }, { code: 'MR', dialCode: '+222', name: 'Mauritania' }, { code: 'MU', dialCode: '+230', name: 'Maurice' }, { code: 'MX', dialCode: '+52', name: 'Mexico' }, { code: 'FM', dialCode: '+691', name: 'Micronesia' }, { code: 'MD', dialCode: '+373', name: 'Moldova' }, { code: 'MC', dialCode: '+377', name: 'Monaco' }, { code: 'MN', dialCode: '+976', name: 'Mongolia' }, { code: 'ME', dialCode: '+382', name: 'Montenegro' }, { code: 'MA', dialCode: '+212', name: 'Morocco' }, { code: 'MZ', dialCode: '+258', name: 'Mozambique' }, { code: 'MM', dialCode: '+95', name: 'Myanmar' }, { code: 'NA', dialCode: '+264', name: 'Namibia' }, { code: 'NR', dialCode: '+674', name: 'Nauru' }, { code: 'NP', dialCode: '+977', name: 'Nepal' }, { code: 'NL', dialCode: '+31', name: 'Netherlands' }, { code: 'NZ', dialCode: '+64', name: 'New Zealand' }, { code: 'NI', dialCode: '+505', name: 'Nicaragua' }, { code: 'NE', dialCode: '+227', name: 'Niger' }, { code: 'NG', dialCode: '+234', name: 'Nigeria' }, { code: 'MK', dialCode: '+389', name: 'North Macedonia' }, { code: 'NO', dialCode: '+47', name: 'Norway' }, { code: 'OM', dialCode: '+968', name: 'Oman' }, { code: 'PK', dialCode: '+92', name: 'Pakistan' }, { code: 'PW', dialCode: '+680', name: 'Palau' }, { code: 'PA', dialCode: '+507', name: 'Panama' }, { code: 'PG', dialCode: '+675', name: 'Papua New Guinea' }, { code: 'PY', dialCode: '+595', name: 'Paraguay' }, { code: 'PE', dialCode: '+51', name: 'Peru' }, { code: 'PH', dialCode: '+63', name: 'Philippines' }, { code: 'PL', dialCode: '+48', name: 'Poland' }, { code: 'PT', dialCode: '+351', name: 'Portugal' }, { code: 'QA', dialCode: '+974', name: 'Qatar' }, { code: 'RO', dialCode: '+40', name: 'Romania' }, { code: 'RU', dialCode: '+7', name: 'Russia' }, { code: 'RW', dialCode: '+250', name: 'Rwanda' }, { code: 'KN', dialCode: '+1-869', name: 'Saint Kitts and Nevis' }, { code: 'LC', dialCode: '+1-758', name: 'Saint Lucia' }, { code: 'VC', dialCode: '+1-784', name: 'Saint Vincent and the Grenadines' }, { code: 'WS', dialCode: '+685', name: 'Samoa' }, { code: 'SM', dialCode: '+378', name: 'San Marino' }, { code: 'ST', dialCode: '+239', name: 'Sao Tome and Principe' }, { code: 'SA', dialCode: '+966', name: 'Saudi Arabia' }, { code: 'SN', dialCode: '+221', name: 'Senegal' }, { code: 'RS', dialCode: '+381', name: 'Serbia' }, { code: 'SC', dialCode: '+248', name: 'Seychelles' }, { code: 'SL', dialCode: '+232', name: 'Sierra Leone' }, { code: 'SG', dialCode: '+65', name: 'Singapore' }, { code: 'SK', dialCode: '+421', name: 'Slovakia' }, { code: 'SI', dialCode: '+386', name: 'Slovenia' }, { code: 'SB', dialCode: '+677', name: 'Solomon Islands' }, { code: 'SO', dialCode: '+252', name: 'Somalia' }, { code: 'ZA', dialCode: '+27', name: 'South Africa' }, { code: 'ES', dialCode: '+34', name: 'Spain' }, { code: 'LK', dialCode: '+94', name: 'Sri Lanka' }, { code: 'SD', dialCode: '+249', name: 'Sudan' }, { code: 'SR', dialCode: '+597', name: 'Suriname' }, { code: 'SE', dialCode: '+46', name: 'Sweden' }, { code: 'CH', dialCode: '+41', name: 'Switzerland' }, { code: 'SY', dialCode: '+963', name: 'Syria' }, { code: 'TW', dialCode: '+886', name: 'Taiwan' }, { code: 'TJ', dialCode: '+992', name: 'Tajikistan' }, { code: 'TZ', dialCode: '+255', name: 'Tanzania' }, { code: 'TH', dialCode: '+66', name: 'Thailand' }, { code: 'TL', dialCode: '+670', name: 'Timor-Leste' }, { code: 'TG', dialCode: '+228', name: 'Togo' }, { code: 'TO', dialCode: '+676', name: 'Tonga' }, { code: 'TT', dialCode: '+1-868', name: 'Trinidad and Tobago' }, { code: 'TN', dialCode: '+216', name: 'Tunisia' }, { code: 'TR', dialCode: '+90', name: 'Turkey' }, { code: 'TM', dialCode: '+993', name: 'Turkmenistan' }, { code: 'TV', dialCode: '+688', name: 'Tuvalu' }, { code: 'UG', dialCode: '+256', name: 'Uganda' }, { code: 'UA', dialCode: '+380', name: 'Ukraine' }, { code: 'AE', dialCode: '+971', name: 'United Arab Emirates' }, { code: 'GB', dialCode: '+44', name: 'United Kingdom' }, { code: 'US', dialCode: '+1', name: 'United States' }, { code: 'UY', dialCode: '+598', name: 'Uruguay' }, { code: 'UZ', dialCode: '+998', name: 'Uzbekistan' }, { code: 'VU', dialCode: '+678', name: 'Vanuatu' }, { code: 'VA', dialCode: '+379', name: 'Vatican City' }, { code: 'VE', dialCode: '+58', name: 'Venezuela' }, { code: 'VN', dialCode: '+84', name: 'Vietnam' }, { code: 'YE', dialCode: '+967', name: 'Yemen' }, { code: 'ZM', dialCode: '+260', name: 'Zambia' }, { code: 'ZW', dialCode: '+263', name: 'Zimbabwe' } ]; // Helper function to parse server creation date // const parseServerDate = (dateString) => { ... } - MOVED TO SHARED // Utility functions for metrics formatting // const formatCPU = (cpuStr) => { ... } - MOVED TO SHARED // const formatMemory = (memoryStr, totalMemoryGB = 1) => { ... } - MOVED TO SHARED // Shared Dropdown Component // const Dropdown = ({ value, options, onChange, placeholder, icon: Icon }) => { ... } - MOVED TO SHARED // Unified Tooltip Component // const UnifiedTooltip = ({ children, content, show = true, placement = 'top' }) => { ... } - MOVED TO SHARED // File Manager Component with forwardRef const fileManagerRef = React.createRef(); // ResourceGraph - MOVED TO SHARED // Slider - MOVED TO SHARED // SectionCard - MOVED TO SHARED // Embedded Plugin Store Component for Dashboard function DashboardPage({ setIsLoginOpen, setIsRegisterOpen, onCurrencyChange, pricing = { price_cpu: 35.0, price_ram: 35.0, price_storage: 5.0, multiplier_vps: 1.0, multiplier_game: 0.8, multiplier_website: 1.5, multiplier_database: 1.3 } }) { const navigate = useNavigate(); const { user, logout } = useContext(AuthContext); const { selectedCurrency, setSelectedCurrency: setSelectedCurrencyBase, currencies, calculateCurrencyValue, getCurrencySymbol, } = usePanelCurrency(); const setSelectedCurrency = useCallback((v) => { setSelectedCurrencyBase(v); if (onCurrencyChange && typeof onCurrencyChange === 'function') { onCurrencyChange(v); } }, [setSelectedCurrencyBase, onCurrencyChange]); const [userData, setUserData] = useState({ ...user, email_verified: user?.email_verified || false }); const [activeTab, setActiveTab] = useState('servers'); const [newPortProtocol, setNewPortProtocol] = useState('TCP'); const [newPortInternal, setNewPortInternal] = useState(''); const [showPortMappingGuide, setShowPortMappingGuide] = useState(false); const [servers, setServers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedServer, setSelectedServer] = useState(null); const [isEditingVersion, setIsEditingVersion] = useState(false); const [isEditingLoader, setIsEditingLoader] = useState(false); const [isEditingJava, setIsEditingJava] = useState(false); const [newVersion, setNewVersion] = useState(''); const [newLoader, setNewLoader] = useState(''); const [newJavaVersion, setNewJavaVersion] = useState(''); const [isEditingResources, setIsEditingResources] = useState(false); const [resourceValues, setResourceValues] = useState({ cpu: 1, ram: 1, storage: 10 }); const [toggleSFTPPassword, setToggleSFTPPassword] = useState(false); // Initialize editing states when server is selected useEffect(() => { if (selectedServer) { if (!isEditingVersion) setNewVersion(selectedServer.version || ''); if (!isEditingLoader) setNewLoader(selectedServer.loader || 'Paper'); if (!isEditingJava) setNewJavaVersion(selectedServer.java_version || 'Java 21'); setNewPortInternal(''); setShowPortMappingGuide(false); } }, [selectedServer, isEditingVersion, isEditingLoader, isEditingJava]); // Compatibility logic for DashboardPage useEffect(() => { if (isEditingVersion && selectedServer?.image === 'minecraft' && newVersion) { const parts = newVersion.split('.'); if (parts.length >= 2) { const major = parseInt(parts[0]); const minor = parseInt(parts[1]); const patch = parts.length >= 3 ? parseInt(parts[2]) : 0; // Auto-select Java version based on MC version ONLY if not manually editing Java if (!isEditingJava) { if (major >= 26) { setNewJavaVersion('Java 25'); } else if (major === 1) { if (minor >= 21 || (minor === 20 && patch >= 5)) { setNewJavaVersion('Java 21'); } else if (minor >= 17) { setNewJavaVersion('Java 17'); } else if (minor >= 16) { setNewJavaVersion('Java 11'); } else if (minor >= 12) { setNewJavaVersion('Java 8'); } else if (minor === 8) { setNewJavaVersion('Java 7'); } else { setNewJavaVersion('Java 8'); } } } // Auto-correct incompatible loader (Fabric not on < 1.14) if (newLoader === 'Fabric' && minor < 14) { setNewLoader('Forge'); } } } }, [newVersion, isEditingVersion, selectedServer?.image, isEditingJava, newLoader]); const handleUpdateConfig = (type, value) => { if (!selectedServer) return; // We'll use the existing sendDashboardMessage logic if (sendMessageRef.current) { sendMessageRef.current({ type: 'change_server_version', serverId: selectedServer.id, version: type === 'version' ? value : newVersion, loader: type === 'loader' ? value : newLoader, java_version: type === 'java' ? value : newJavaVersion }); } // Close editing states setIsEditingVersion(false); setIsEditingLoader(false); setIsEditingJava(false); }; const [serverLogs, setServerLogs] = useState(''); const [isCreatingServer, setIsCreatingServer] = useState(false); const [terminalOutput, setTerminalOutput] = useState(''); const [terminalInput, setTerminalInput] = useState(''); const [isTerminalConnected, setIsTerminalConnected] = useState(false); const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false); const [serverMetrics, setServerMetrics] = useState({ cpu: '0m', memory: '0Mi', cpu_percentage: 0, memory_percentage: 0 }); const [metricsHistory, setMetricsHistory] = useState({ cpu: [], memory: [] }); const [mcSettings, setMcSettings] = useState(null); const [isMcSettingsLoading, setIsMcSettingsLoading] = useState(false); const [userCredits, setUserCredits] = useState(user?.credits || 0); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [activeServerDetailTab, setActiveServerDetailTab] = useState('overview'); // Ref for sendMessage to break circular dependency const sendMessageRef = useRef(null); const [displayNameModal, setDisplayNameModal] = useState({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' }); const [emailModal, setEmailModal] = useState({ open: false, currentEmail: '', title: '', label: '', placeholder: '' }); const [passwordModal, setPasswordModal] = useState({ open: false, title: '', requireCurrent: true }); const [alertModal, setAlertModal] = useState({ open: false, title: '', message: '', type: 'info' // 'info', 'success', 'error', 'warning' }); const [confirmModal, setConfirmModal] = useState({ open: false, title: '', message: '', onConfirm: () => {}, type: 'danger', confirmLabel: 'Confirm', cancelLabel: 'Cancel', }); const showAlert = useCallback((title, message, type = 'info') => { setAlertModal({ open: true, title, message, type }); }, []); const showConfirm = useCallback((title, message, onConfirm, type = 'danger', labels = {}) => { setConfirmModal({ open: true, title, message, onConfirm, type, confirmLabel: labels.confirmLabel || 'Confirm', cancelLabel: labels.cancelLabel || 'Cancel', }); }, []); const [show2FASetup, setShow2FASetup] = useState(false); const [twoFactorData, setTwoFactorData] = useState(null); const [deleteAccountModal, setDeleteAccountModal] = useState(false); const [deleteServerModal, setDeleteServerModal] = useState({ open: false, serverId: null }); const [convertDemoModal, setConvertDemoModal] = useState({ open: false, server: null, cost: 0 }); const [serverCreationProgress, setServerCreationProgress] = useState({ visible: false, phase: '', progress: 0, message: '' }); const [personalInfo, setPersonalInfo] = useState({ firstName: user?.first_name || '', lastName: user?.last_name || '', address: user?.address || '', city: user?.city || '', country: user?.country || '', postalCode: user?.postal_code || '', phone: user?.phone || '' }); const [countryOptions, setCountryOptions] = useState([]); const [selectedCountry, setSelectedCountry] = useState(''); const [phoneNumber, setPhoneNumber] = useState(''); const [detectingLocation, setDetectingLocation] = useState(false); const token = localStorage.getItem('anchorhost_token'); // Format countries for dropdown useEffect(() => { const formattedCountries = countries.map(country => ({ value: country.code, label: `${country.name} (${country.dialCode})`, dialCode: country.dialCode, name: country.name })); // Sort alphabetically by country name formattedCountries.sort((a, b) => a.name.localeCompare(b.name)); setCountryOptions(formattedCountries); // Set initial country from user data if available if (user?.country) { const userCountry = formattedCountries.find(c => c.value === user.country || c.name === user.country); if (userCountry) { setSelectedCountry(userCountry.value); } } // Set initial phone number if (user?.phone) { setPhoneNumber(user.phone); } }, [user]); // Function to detect user location const detectUserLocation = useCallback(async () => { setDetectingLocation(true); try { const response = await fetch('https://geoapi.info/api/geo'); if (!response.ok) { throw new Error('Failed to fetch location'); } const data = await response.json(); console.log('Location data:', data); if (data.location?.country) { const countryCode = data.location.country; const foundCountry = countryOptions.find(c => c.value === countryCode); if (foundCountry) { setSelectedCountry(countryCode); // Update personal info with detected country setPersonalInfo(prev => ({ ...prev, country: foundCountry.name })); } } } catch (error) { console.error('Error detecting location:', error); setAlertModal({ open: true, title: "Location Detection Failed", message: "We couldn't automatically detect your location. Please select your country manually.", type: "warning" }); } finally { setDetectingLocation(false); } }, [countryOptions]); // Auto-detect location when country is not set useEffect(() => { if (!selectedCountry && countryOptions.length > 0) { detectUserLocation(); } }, [selectedCountry, countryOptions, detectUserLocation]); // Get selected country info const getSelectedCountry = () => { return countryOptions.find(c => c.value === selectedCountry); }; // Function to update user profile with phone and country const updateUserProfile = async (profileData) => { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/profile`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(profileData) }); const data = await response.json(); if (data.success) { return { success: true, message: data.message || 'Profile updated successfully!' }; } else { return { success: false, message: data.error || 'Failed to update profile' }; } } catch (error) { console.error('Error updating profile:', error); return { success: false, message: 'Error updating profile' }; } }; const changeUserEmail = async (newEmail) => { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/email`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: newEmail }) }); const data = await response.json(); if (data.success) { return { success: true, message: data.message || 'Email changed successfully! Please check your new email for verification.' }; } else { return { success: false, message: data.error || 'Failed to change email' }; } } catch (error) { console.error('Error changing email:', error); return { success: false, message: 'Error changing email' }; } }; const changeUserPassword = async (currentPassword, newPassword) => { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/password`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, newPassword }) }); const data = await response.json(); if (data.success) { return { success: true, message: data.message || 'Password changed successfully!' }; } else { return { success: false, message: data.error || 'Failed to change password' }; } } catch (error) { console.error('Error changing password:', error); return { success: false, message: 'Error changing password' }; } }; const deleteUserAccount = async (confirmationText) => { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/account`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ confirmation: confirmationText }) }); const data = await response.json(); if (data.success) { localStorage.removeItem('anchorhost_token'); window.location.href = '/'; return { success: true, message: 'Account deleted successfully!' }; } else { return { success: false, message: data.error || 'Failed to delete account' }; } } catch (error) { console.error('Error deleting account:', error); return { success: false, message: 'Error deleting account' }; } }; const exportUserData = async () => { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/export-data`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); if (data.success) { const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `anchorhost-data-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { showAlert('Export Failed', data.error || 'Unknown error', 'error'); } } catch (error) { showAlert('Export Error', 'An unexpected error occurred while exporting your data.', 'error'); } }; const loadUserProfile = useCallback(async () => { try { const token = localStorage.getItem('anchorhost_token'); if (!token) { setAlertModal({ open: true, title: "Authentication Error", message: "No authentication token found. Please log in again.", type: "error" }); return; } console.log('Loading user profile...'); const response = await fetch(`${API_BASE_URL}/api/user/profile`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.status === 401 || response.status === 403) { logout(); return; } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.success && data.user) { // IMPORTANT: Update userData with email_verified setUserData(prev => ({ ...prev, ...data.user, email_verified: data.user.email_verified || false })); setPersonalInfo({ firstName: data.user.first_name || '', lastName: data.user.last_name || '', address: data.user.address || '', city: data.user.city || '', country: data.user.country || '', postalCode: data.user.postal_code || '', phone: data.user.phone || '' }); // Set country dropdown if (data.user.country && countryOptions.length > 0) { const userCountry = countryOptions.find(c => c.value === data.user.country || c.name === data.user.country ); if (userCountry) { setSelectedCountry(userCountry.value); } } // Set phone number if (data.user.phone) { setPhoneNumber(data.user.phone); } console.log('βœ… Profile loaded successfully:', data.user); console.log('πŸ“§ Email verified status:', data.user.email_verified); } else { console.error('Failed to load user profile:', data.error); setAlertModal({ open: true, title: "Failed to Load Profile", message: data.error || 'Unknown error', type: "error" }); } } catch (error) { console.error('Error loading user profile:', error); } }, [countryOptions, logout]); // Process metrics for human-readable display const processMetrics = useCallback((metrics) => { if (!metrics) return { cpu: formatCPU('0m'), memory: formatMemory('0Mi') }; const serverRAM = selectedServer ? parseInt(selectedServer.ram) || 1 : 1; // Calculate CPU max value for percentage calculation const cpuStr = selectedServer?.cpu || '1'; let cpuCores = 1; if (cpuStr.includes('vCPU')) { cpuCores = parseInt(cpuStr.split()[0]) || 1; } else if (typeof cpuStr === 'string' && /^\d+$/.test(cpuStr.trim())) { cpuCores = parseInt(cpuStr.trim()) || 1; } else { const match = cpuStr.match(/(\d+)/); if (match) cpuCores = parseInt(match[1]) || 1; } const maxCpuMillicores = cpuCores * 1000; // Format CPU with correct percentage const cpuFormatted = formatCPU(metrics.cpu); // Recalculate percentage based on server's actual CPU allocation const cpuPercentage = metrics.cpu_percentage !== undefined ? metrics.cpu_percentage : (cpuFormatted.value / maxCpuMillicores) * 100; return { cpu: { ...cpuFormatted, percentage: Math.min(cpuPercentage, 100) }, memory: formatMemory(metrics.memory, serverRAM), cpu_percentage: Math.min(cpuPercentage, 100), memory_percentage: metrics.memory_percentage || 0, timestamp: metrics.timestamp || new Date().toISOString() }; }, [selectedServer]); // WebSocket message handler const handleWebSocketMessage = useCallback((data) => { console.log('Dashboard received message:', data.type, data); // Forward game console messages const gameConsoleMessages = [ 'game_console_result', 'game_console_output' ]; if (gameConsoleMessages.includes(data.type) && window.__gameConsoleHandler) { window.__gameConsoleHandler(data); return; } // First, let the file manager handle file-related messages // For upload_error, also forward if it's a file-related message (even if serverId doesn't match exactly) const shouldForwardToFileManager = fileManagerRef.current && ( String(data.serverId) === String(selectedServer?.id) || (data.type === 'upload_error' && data.filePath) // Forward upload_error even if serverId mismatch ); if (shouldForwardToFileManager) { const fileManagerHandlers = [ 'files_list', 'file_content', 'file_written', 'file_uploaded', 'file_downloaded', 'directory_created', 'path_deleted', 'upload_started', 'upload_chunk_received', 'upload_error', 'path_copied', 'path_moved', 'zip_packed', 'zip_unpacked', 'zip_error' ]; if (fileManagerHandlers.includes(data.type)) { fileManagerRef.current.handleWebSocketMessage(data); return; } } // Handle command results for file listing if (data.type === 'command_result' && data.serverId === selectedServer?.id) { // Let file manager handle ls command results when in files tab if (fileManagerRef.current && activeServerDetailTab === 'files') { fileManagerRef.current.handleWebSocketMessage(data); } // Handle terminal output if (isTerminalConnected) { setTerminalOutput(prev => prev + `$ ${data.command}\n${data.result}\n`); } // Return early to prevent falling through to default case return; } // Handle domain-related messages - forward to DomainManager const domainMessageTypes = [ 'domains_list', 'custom_domains_list', 'domain_created', 'domain_deleted', 'domain_linked', 'domain_unlinked', 'custom_domain_verified', 'custom_domain_created', 'custom_domain_deleted' ]; if (domainMessageTypes.includes(data.type) && window.__domainManagerHandler) { window.__domainManagerHandler(data); return; } // Handle other message types switch (data.type) { case 'email_verified': // Update user state after verification setUserData(prev => ({ ...prev, email_verified: true })); // Show success message setAlertModal({ open: true, title: "Email Verified! πŸŽ‰", message: data.message || "Your email has been verified successfully!", type: "success" }); break; case 'authenticated': setUserCredits(data.user?.credits || 0); // IMPORTANT: Update email_verified during authentication setUserData(prev => ({ ...prev, ...data.user, email_verified: data.user?.email_verified || false })); sendMessageRef.current?.({ type: 'get_servers' }); // Request server creation progress if we have an active creation if (serverCreationProgress.visible) { sendMessageRef.current?.({ type: 'get_server_creation_progress' }); } break; case 'servers_list': setServers(data.servers || []); // Update selectedServer if it's currently selected to reflect port changes if (selectedServer && data.servers) { const updatedServer = data.servers.find(s => s.id === selectedServer.id); if (updatedServer) { setSelectedServer(updatedServer); } } setIsLoading(false); break; case 'server_created': // Hide progress notification when server creation completes setServerCreationProgress(prev => ({ ...prev, visible: false })); if (data.server && data.server.id) { setServers(prev => [...prev, data.server]); setIsCreatingServer(false); } else if (data.serverId) { sendMessageRef.current?.({ type: 'get_servers' }); setIsCreatingServer(false); } if (data.credits !== undefined) { setUserCredits(data.credits); } // Redirect to My Servers tab after successful server creation setActiveTab('servers'); break; case 'server_updated': if (data.server && data.server.id) { setServers(prev => prev.map(s => s.id === data.server.id ? { ...s, ...data.server } : s )); if (selectedServer && selectedServer.id === data.server.id) { setSelectedServer(prev => ({ ...prev, ...data.server })); } } break; case 'sftp_generated': if (data.serverId === selectedServer?.id) { setAlertModal({ open: true, title: "SFTP Provisioned! πŸš€", message: "SFTP credentials have been generated. You can now connect via any SFTP client.", type: "success" }); // Refresh servers list to get new credentials sendMessageRef.current?.({ type: 'get_servers' }); } break; case 'server_version_changed': if (data.serverId && data.version) { setServers(prev => prev.map(s => s.id === data.serverId ? { ...s, version: data.version } : s )); if (selectedServer && selectedServer.id === data.serverId) { setSelectedServer(prev => ({ ...prev, version: data.version })); } setIsEditingVersion(false); setNewVersion(''); // Show success message setAlertModal({ open: true, title: "Version Changed! βœ…", message: `Server version has been updated to ${data.version}. Billing date remains unchanged.`, type: "success" }); } break; case 'minecraft_settings': if (data.serverId === selectedServer?.id) { setMcSettings(data.settings); setIsMcSettingsLoading(false); } break; case 'minecraft_settings_updated': if (data.serverId === selectedServer?.id) { setAlertModal({ open: true, title: "Settings Updated!", message: "Quick settings updated. Restart server to apply.", type: "success" }); setIsMcSettingsLoading(false); } break; case 'server_deleted': if (data.serverId) { setServers(prev => prev.filter(s => s.id !== data.serverId)); if (selectedServer && selectedServer.id === data.serverId) { setSelectedServer(null); setServerLogs(''); disconnectTerminal(); } } break; case 'server_logs': setServerLogs(data.logs || ''); break; case 'server_metrics': if (data.serverId === selectedServer?.id && data.metrics) { const processedMetrics = processMetrics(data.metrics); setServerMetrics(processedMetrics); const now = new Date(); const timeLabel = now.toLocaleTimeString(); setMetricsHistory(prev => { const newCpuHistory = [...prev.cpu.slice(-59), { value: processedMetrics.cpu.value, time: timeLabel, percentage: processedMetrics.cpu_percentage || processedMetrics.cpu.percentage }]; const newMemoryHistory = [...prev.memory.slice(-59), { value: processedMetrics.memory.value, time: timeLabel, percentage: processedMetrics.memory_percentage || processedMetrics.memory.percentage }]; return { cpu: newCpuHistory, memory: newMemoryHistory }; }); } break; case 'credits_balance': setUserCredits(data.credits || 0); break; case 'credits_added': setUserCredits(data.new_balance || userCredits); break; case 'demo_eligibility': // Demo eligibility is handled in ServerCreationForm break; case 'demo_converted': setUserCredits(data.credits || userCredits); // Refresh servers list sendMessageRef.current?.({ type: 'get_servers' }); // Show success message in custom themed popup setAlertModal({ open: true, title: 'Conversion Successful', message: `Demo server converted successfully! Charged ${data.credits_charged} credits. New balance: ${data.credits} credits.`, type: 'success' }); break; case 'server_creation_progress': // Hide progress if no active creation found (after reconnection check) if (data.phase === 'Unknown' && data.progress === 0 && data.message === 'No active server creation found') { setServerCreationProgress(prev => ({ ...prev, visible: false })); break; } setServerCreationProgress({ visible: true, phase: data.phase || '', progress: data.progress || 0, message: data.message || 'Creating server...' }); // Hide notification when progress reaches 100% if (data.progress >= 100) { setTimeout(() => { setServerCreationProgress(prev => ({ ...prev, visible: false })); }, 2000); } break; case 'port_exposed': // Refresh servers list to get updated ports sendMessageRef.current?.({ type: 'get_servers' }); // Show success message if available if (data.message) { setAlertModal({ open: true, title: "Port Exposed! βœ…", message: data.message, type: "success" }); } break; case 'port_removed': // Refresh servers list to get updated ports sendMessageRef.current?.({ type: 'get_servers' }); // Show success message if available if (data.message) { setAlertModal({ open: true, title: "Port Removed! βœ…", message: data.message, type: "success" }); } break; case 'auth_error': console.error('Authentication error:', data.error); logout(); break; case 'error': console.error('WebSocket error:', data.error); if (data.error === 'Not authenticated') { sendMessageRef.current?.({ type: 'authenticate', token: token }); } else if (data.error && (data.error.toLowerCase().includes('convert demo') || data.error.toLowerCase().includes('convert_demo') || (data.error.toLowerCase().includes('demo') && data.error.toLowerCase().includes('paid')))) { // Show custom themed popup for demo conversion errors setAlertModal({ open: true, title: 'Conversion Failed', message: data.error || 'Failed to convert demo server to paid', type: 'error' }); } else if (data.error && data.error !== "0" && data.error !== 0) { // Show popup for other errors setAlertModal({ open: true, title: 'Error', message: data.error || 'An error occurred', type: 'error' }); } break; case '2fa_setup_details': setTwoFactorData(data); setShow2FASetup(true); break; case '2fa_enabled': setShow2FASetup(false); setAlertModal({ open: true, title: 'Success', message: '2FA enabled successfully!', type: 'success' }); loadUserProfile(); break; case '2fa_disabled': setAlertModal({ open: true, title: 'Success', message: '2FA disabled successfully!', type: 'success' }); loadUserProfile(); break; default: console.log('Unhandled message type:', data.type); } }, [selectedServer, activeServerDetailTab, isTerminalConnected, processMetrics, token, loadUserProfile, serverCreationProgress.visible, userCredits, logout]); const { ws: dashboardWs, sendMessage: sendDashboardMessage, connectionStatus: dashboardStatus, lastJsonMessage: lastMessage } = useWebSocket( `${WS_URL}?token=${token}`, handleWebSocketMessage ); // Update ref when sendDashboardMessage changes to avoid circular dependency useEffect(() => { sendMessageRef.current = sendDashboardMessage; }, [sendDashboardMessage]); // Request server creation progress after reconnection if creation is in progress useEffect(() => { if (dashboardStatus === 'connected' && serverCreationProgress.visible && sendDashboardMessage) { // Small delay to ensure authentication is complete const timeoutId = setTimeout(() => { sendDashboardMessage({ type: 'get_server_creation_progress' }); }, 500); return () => clearTimeout(timeoutId); } }, [dashboardStatus, serverCreationProgress.visible, sendDashboardMessage]); // Request metrics when server is selected and periodically update useEffect(() => { if (!selectedServer || !sendDashboardMessage || dashboardStatus !== 'connected') { // Reset metrics when no server selected setServerMetrics({ cpu: '0m', memory: '0Mi', cpu_percentage: 0, memory_percentage: 0 }); setMetricsHistory({ cpu: [], memory: [] }); return; } // Request metrics immediately when server is selected const requestMetrics = () => { sendDashboardMessage({ type: 'get_server_metrics', serverId: selectedServer.id }); }; // Request metrics immediately requestMetrics(); // Set up interval to request metrics every 2 seconds const metricsInterval = setInterval(requestMetrics, 2000); return () => { clearInterval(metricsInterval); }; }, [selectedServer, sendDashboardMessage, dashboardStatus]); // Request Quick settings when tab is active useEffect(() => { if (activeServerDetailTab === 'minecraft-settings' && selectedServer && dashboardStatus === 'connected' && !mcSettings) { setIsMcSettingsLoading(true); sendDashboardMessage({ type: 'get_minecraft_settings', serverId: selectedServer.id }); } }, [activeServerDetailTab, selectedServer, dashboardStatus, sendDashboardMessage, mcSettings]); // Clear Quick settings when server changes useEffect(() => { setMcSettings(null); }, [selectedServer]); const terminalRef = useRef(null); const connectTerminal = () => { if (selectedServer) { setIsTerminalConnected(true); setTerminalOutput("Connected to server terminal. Type commands below."); } }; const sendTerminalCommand = (command) => { if (selectedServer && dashboardStatus === 'connected') { sendDashboardMessage({ type: 'exec_command', serverId: selectedServer.id, command: command }); setTerminalInput(''); } }; const handleTerminalKeyDown = (e) => { if (e.key === 'Enter') { e.preventDefault(); sendTerminalCommand(terminalInput); } }; const disconnectTerminal = () => { setIsTerminalConnected(false); setTerminalOutput(''); }; const handleCreateServer = async (serverConfig) => { setIsCreatingServer(true); sendDashboardMessage({ type: 'create_server', name: serverConfig.name, serverType: serverConfig.type, image: serverConfig.image, version: serverConfig.version, cpu: serverConfig.cpu, ram: serverConfig.ram, storage: serverConfig.storage, ports: serverConfig.ports, is_demo: serverConfig.is_demo || false }); }; const handleDeleteServer = async (serverId) => { setDeleteServerModal({ open: true, serverId: serverId }); }; const confirmDeleteServer = async () => { if (deleteServerModal.serverId) { sendDashboardMessage({ type: 'delete_server', serverId: deleteServerModal.serverId }); setDeleteServerModal({ open: false, serverId: null }); } }; // Calculate server cost based on specs (matching backend logic) const calculateServerCost = (server) => { // Get CPU cores let cpuCores = 1; if (server.cpu) { if (typeof server.cpu === 'string' && server.cpu.includes('vCPU')) { cpuCores = parseInt(server.cpu.split(' ')[0]); } else { cpuCores = parseInt(server.cpu); } } // Get RAM GB let ramGb = 1; if (server.ram) { if (typeof server.ram === 'string' && server.ram.includes('GB')) { ramGb = parseInt(server.ram.replace('GB', '')); } else { ramGb = parseInt(server.ram); } } // Get Storage GB let storageGb = 10; if (server.storage) { if (typeof server.storage === 'string' && server.storage.includes('GB')) { storageGb = parseInt(server.storage.replace('GB', '')); } else { storageGb = parseInt(server.storage); } } // Type multipliers const typeMultiplier = { 'vps': pricing.multiplier_vps || 1.0, 'game': pricing.multiplier_game || 0.8, 'website': pricing.multiplier_website || 1.5, 'database': pricing.multiplier_database || 1.3 }[server.type] || 1.0; // Calculate costs (matching backend: 1 vCPU = 35 credits, 1GB RAM = 35 credits, 1GB Storage = 5 credits) const cpuCost = cpuCores * (pricing.price_cpu || 35); const ramCost = ramGb * (pricing.price_ram || 35); const storageCost = storageGb * (pricing.price_storage || 5); const baseCost = 0; // Total cost const totalCost = Math.round((baseCost + cpuCost + ramCost + storageCost) * typeMultiplier); return totalCost; }; const handleConvertDemoToPaid = async (serverId) => { // Find the server in the servers list const server = servers.find(s => s.id === serverId); if (!server) { setAlertModal({ open: true, title: 'Error', message: 'Server not found', type: 'error' }); return; } // Calculate cost const cost = calculateServerCost(server); // Open the custom modal setConvertDemoModal({ open: true, server: server, cost: cost }); }; const confirmConvertDemoToPaid = () => { if (!convertDemoModal.server) return; sendDashboardMessage({ type: 'convert_demo_to_paid', serverId: convertDemoModal.server.id }); // Close modal setConvertDemoModal({ open: false, server: null, cost: 0 }); }; const handleServerAction = async (serverId, action) => { if (action === 'restart') { sendDashboardMessage({ type: 'restart_server', serverId: serverId }); } else { sendDashboardMessage({ type: action + '_server', serverId: serverId }); } }; const fetchServerLogs = async (serverId) => { sendDashboardMessage({ type: 'get_server_logs', serverId: serverId }); }; const handleExposePort = async (serverId, internalPort, protocol = 'TCP') => { sendDashboardMessage({ type: 'expose_port', serverId: serverId, internalPort: parseInt(internalPort), protocol: protocol }); }; const handleRemovePort = async (serverId, internalPort, protocol = 'TCP') => { sendDashboardMessage({ type: 'remove_port', serverId: serverId, internalPort: parseInt(internalPort), protocol: protocol }); }; // Helper function to parse CPU value (e.g., "2 vCPU" -> 2) const parseCpuValue = (cpuStr) => { if (!cpuStr) return 1; const match = cpuStr.match(/(\d+)/); return match ? parseInt(match[1]) : 1; }; // Helper function to parse RAM value (e.g., "4GB" -> 4) const parseRamValue = (ramStr) => { if (!ramStr) return 1; const match = ramStr.match(/(\d+)\s*GB/i); return match ? parseInt(match[1]) : 1; }; // Helper function to parse Storage value (e.g., "50GB" -> 50) const parseStorageValue = (storageStr) => { if (!storageStr) return 10; const match = storageStr.match(/(\d+)\s*GB/i); return match ? parseInt(match[1]) : 10; }; // Initialize resource values when server is selected useEffect(() => { if (selectedServer) { setResourceValues({ cpu: parseCpuValue(selectedServer.cpu), ram: parseRamValue(selectedServer.ram), storage: parseStorageValue(selectedServer.storage) }); } }, [selectedServer]); // Function to handle adjusting server parameters const handleAdjustServerParameters = async () => { if (!selectedServer) return; try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/servers/${selectedServer.id}/adjust-parameters`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ cpu: `${resourceValues.cpu} vCPU`, ram: `${resourceValues.ram}GB`, storage: `${resourceValues.storage}GB` }) }); const data = await response.json(); if (response.ok && data.success) { let message = 'Server parameters adjusted successfully.'; if (data.is_upgrade && data.credits_charged > 0) { message = `Server upgraded successfully. Charged ${data.credits_charged} credits (difference between new plan: ${data.new_monthly_cost} credits/month and current plan: ${data.current_monthly_cost} credits/month).`; } else if (!data.is_upgrade) { message = `Server downgraded successfully. No credits were charged. Your new monthly cost is ${data.new_monthly_cost} credits (reduced from ${data.current_monthly_cost} credits).`; } else { message = `Server parameters adjusted successfully. No additional charge.`; } setAlertModal({ open: true, title: 'Success', message: message, type: 'success' }); setIsEditingResources(false); // Refresh servers to get updated values refreshServers(); // Refresh credits refreshCredits(); } else { setAlertModal({ open: true, title: 'Error', message: data.error || 'Failed to adjust server parameters', type: 'error' }); } } catch (error) { setAlertModal({ open: true, title: 'Error', message: `Failed to adjust server parameters: ${error.message}`, type: 'error' }); } }; const handleUpdateMcSettings = (newSettings) => { if (!selectedServer) return; setIsMcSettingsLoading(true); sendDashboardMessage({ type: 'update_minecraft_settings', serverId: selectedServer.id, settings: newSettings }); }; const refreshCredits = () => { sendDashboardMessage({ type: 'get_credits' }); }; const handleAddCredits = (amount) => { sendDashboardMessage({ type: 'add_credits', amount: amount }); }; const refreshServers = () => { setIsLoading(true); sendDashboardMessage({ type: 'get_servers' }); }; // Auto-refresh servers when My Servers tab is active useEffect(() => { if (activeTab === 'servers' && sendDashboardMessage) { // Refresh immediately when tab becomes active sendDashboardMessage({ type: 'get_servers' }); // Set up interval to refresh every 10 seconds const interval = setInterval(() => { sendDashboardMessage({ type: 'get_servers' }); }, 10000); // Cleanup interval when tab changes or component unmounts return () => clearInterval(interval); } }, [activeTab, sendDashboardMessage]); const handleTabChange = (tab) => { if (tab === 'admin') { navigate('/admin'); return; } setActiveTab(tab); setIsMobileSidebarOpen(false); }; const settingsInputClass = 'w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors'; // Calculate total resources used const totalResources = servers.reduce((acc, server) => { const cpu = parseInt(server.cpu) || 0; const ram = parseInt(server.ram) || 0; const storage = parseInt(server.storage) || 0; return { cpu: acc.cpu + cpu, ram: acc.ram + ram, storage: acc.storage + storage }; }, { cpu: 0, ram: 0, storage: 0 }); // Check if port is default (cannot be removed) const isDefaultPort = (server, port) => { if (!server || !port) return false; const defaultPorts = { 'website': [{ internal: 80, protocol: 'TCP' }], 'game': { 'minecraft': [{ internal: 25565, protocol: 'TCP' }], 'fivem': [{ internal: 30120, protocol: 'TCP' }], 'cs2': [{ internal: 27015, protocol: 'TCP' }], 'rust': [{ internal: 28015, protocol: 'TCP' }], 'ark': [{ internal: 7777, protocol: 'TCP' }], 'valheim': [{ internal: 2456, protocol: 'TCP' }] }, 'vps': { 'windows': [{ internal: 3389, protocol: 'TCP' }], 'default': [{ internal: 22, protocol: 'TCP' }] }, 'database': { 'mysql': [{ internal: 3306, protocol: 'TCP' }], 'postgresql': [{ internal: 5432, protocol: 'TCP' }], 'mongodb': [{ internal: 27017, protocol: 'TCP' }], 'redis': [{ internal: 6379, protocol: 'TCP' }] } }; if (server.type === 'website') { return defaultPorts.website.some(p => p.internal === port.internal && p.protocol === port.protocol); } else if (server.type === 'game') { const gamePorts = defaultPorts.game[server.image] || []; return gamePorts.some(p => p.internal === port.internal && p.protocol === port.protocol); } else if (server.type === 'vps') { const vpsPorts = defaultPorts.vps[server.image] || defaultPorts.vps.default; return vpsPorts.some(p => p.internal === port.internal && p.protocol === port.protocol); } else if (server.type === 'database') { const dbPorts = defaultPorts.database[server.image] || []; return dbPorts.some(p => p.internal === port.internal && p.protocol === port.protocol); } return false; }; // Get port tooltip information for common ports const getPortTooltip = (internalPort, externalPort) => { const portInfo = { 22: { name: 'SSH (Secure Shell)', description: 'Used for secure remote access to servers', connection: `Connect using: ssh root@${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort}` }, 80: { name: 'HTTP', description: 'Web server port for unencrypted HTTP traffic', connection: `Access at: http://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 443: { name: 'HTTPS', description: 'Web server port for encrypted HTTPS traffic', connection: `Access at: https://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 3306: { name: 'MySQL', description: 'MySQL database server port', connection: `Connect using: mysql -h ${selectedServer?.ip || 'SERVER_IP'} -P ${externalPort} -u USERNAME -p` }, 5432: { name: 'PostgreSQL', description: 'PostgreSQL database server port', connection: `Connect using: psql -h ${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort} -U USERNAME` }, 27017: { name: 'MongoDB', description: 'MongoDB database server port', connection: `Connect using: mongosh mongodb://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 6379: { name: 'Redis', description: 'Redis in-memory data store port', connection: `Connect using: redis-cli -h ${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort}` }, 3389: { name: 'RDP (Remote Desktop)', description: 'Windows Remote Desktop Protocol port', connection: `Connect using Remote Desktop Client to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 25565: { name: 'Minecraft', description: 'Minecraft server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 30120: { name: 'FiveM', description: 'FiveM game server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 27015: { name: 'CS2/Steam', description: 'Counter-Strike 2 / Steam game server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 28015: { name: 'Rust', description: 'Rust game server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 7777: { name: 'ARK', description: 'ARK: Survival Evolved game server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` }, 2456: { name: 'Valheim', description: 'Valheim game server port', connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}` } }; return portInfo[internalPort] || null; }; const getPortServiceName = (server, port, isDefault) => { if (port.name) return port.name; const tooltip = getPortTooltip(parseInt(port.internal, 10), port.external); if (tooltip) return tooltip.name; if (isDefault) return 'Main Service'; return 'Custom Service'; }; const getNetworkConnectionHint = (server) => { switch (server?.type) { case 'game': return 'Paste the public address into your game client\'s Direct Connect or Add Server field.'; case 'vps': return 'Use SSH, RDP, or your remote tool with the public IP and external port below.'; case 'database': return 'Point your database client at the public IP and external port β€” not the internal one.'; case 'website': return 'Open the public address in your browser. HTTP services are mapped automatically.'; default: return 'Copy the full IP:port address and use it in your application or client.'; } }; const submitNetworkExposePort = () => { if (!selectedServer || !newPortInternal.trim()) return; const val = parseInt(newPortInternal, 10); if (val >= 1 && val <= 65535) { handleExposePort(selectedServer.id, newPortInternal, newPortProtocol); setNewPortInternal(''); } else { setAlertModal({ open: true, title: 'Invalid Port', message: 'Port must be between 1 and 65535', type: 'error' }); } }; // Port Tooltip Component with fixed positioning to avoid overflow clipping const PortTooltip = ({ portTooltip }) => { const [isVisible, setIsVisible] = useState(false); const [position, setPosition] = useState({ top: 0, left: 0 }); const iconRef = useRef(null); const hideTimeoutRef = useRef(null); const handleMouseEnter = () => { // Clear any pending hide timeout if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } if (iconRef.current) { const rect = iconRef.current.getBoundingClientRect(); // Position tooltip above the icon, centered horizontally // Position at the top of the icon, then translate up by 100% of tooltip height plus 8px gap setPosition({ top: rect.top, left: rect.left + rect.width / 2 // Center horizontally on icon }); setIsVisible(true); } }; const handleMouseLeave = () => { // Add a small delay before hiding to allow smooth transition to tooltip hideTimeoutRef.current = setTimeout(() => { setIsVisible(false); hideTimeoutRef.current = null; }, 100); }; const handleTooltipMouseEnter = () => { // Clear any pending hide timeout when mouse enters tooltip if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } setIsVisible(true); }; const handleTooltipMouseLeave = () => { setIsVisible(false); }; // Cleanup timeout on unmount useEffect(() => { return () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } }; }, []); const tooltipContent = isVisible ? (
{portTooltip.name}
{portTooltip.description}
{portTooltip.connection}
) : null; return ( <>
{tooltipContent && createPortal(tooltipContent, document.body)} ); }; const dashboardSidebarItems = useMemo( () => [ { id: 'servers', label: 'My Servers', icon: Server, color: colors.blue }, { id: 'create', label: 'Create Server', icon: Plus, color: colors.green }, { id: 'domains', label: 'Domains', icon: Globe, color: colors.cyan }, { id: 'store', label: 'Store', icon: ShoppingCart, color: colors.yellow }, { id: 'tickets', label: 'Support', icon: MessageCircle, color: colors.magenta }, { id: 'settings', label: 'Settings', icon: Settings, color: colors.magenta }, ...(user?.role === 'admin' ? [{ id: 'admin', label: 'Admin Panel', icon: Shield, color: colors.red }] : []), ], [user?.role] ); return (
Dashboard
{activeTab === 'servers' && (
Refresh } />
{servers.length > 0 && (
)} {isLoading ? ( ) : servers.length === 0 ? ( setActiveTab('create')} className="!px-4 !py-2 !min-h-0 sm:!min-h-0" > Create your first server } /> ) : (
{servers.map((server) => ( { setSelectedServer(targetServer); setActiveServerDetailTab('overview'); fetchServerLogs(targetServer.id); }} onServerAction={handleServerAction} /> ))}
)}
)} {activeTab === 'create' && ( )} {activeTab === 'store' && (
)} {activeTab === 'domains' && (
{ sendDashboardMessage({ type: 'list_domains', includeServerless: true }); sendDashboardMessage({ type: 'list_custom_domains' }); }} aria-label="Refresh domain list" > Refresh } />
)} {activeTab === 'tickets' && ( )} {activeTab === 'settings' && (
{userData?.email_verified ? ( ) : ( )}
{userData?.email_verified ? 'Verified' : 'Pending'}
{!userData?.email_verified && ( { try { const token = localStorage.getItem('anchorhost_token'); const response = await fetch(`${API_BASE_URL}/api/user/resend-verification`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); const data = await response.json(); if (data.success) { setAlertModal({ open: true, title: 'Verification email sent', message: 'A new verification email has been sent. Please check your inbox and spam folder.', type: 'success', }); } else { setAlertModal({ open: true, title: 'Failed to send verification', message: `Failed to send verification email: ${data.error || 'Unknown error'}`, type: 'error', }); } } catch (error) { console.error('Error resending verification:', error); setAlertModal({ open: true, title: 'Error', message: 'An error occurred while sending the verification email. Please try again.', type: 'error', }); } }} > Resend email )}

{userData.two_factor_enabled ? '2FA is on for this account.' : '2FA is off.'}

{userData.two_factor_enabled ? ( { const otp = window.prompt('Enter your authentication code to disable 2FA:'); if (otp) { sendDashboardMessage({ type: '2fa_disable', otp_code: otp, }); } }} > Disable 2FA ) : ( sendDashboardMessage({ type: '2fa_setup' })} > Enable 2FA )}
Current {user?.email || 'Not set'} {user?.email_verified && ( Verified )}
{ if (user?.google_id) return; setEmailModal({ open: true, currentEmail: user?.email || '', title: 'Change Email Address', label: 'New Email Address', placeholder: 'Enter new email address', }); }} > Change email
{user?.google_id ? (

No {branding.name} password β€” you sign in with Google.

) : ( <> Current password β€’β€’β€’β€’β€’β€’β€’β€’ Hidden for security )}
{ if (user?.google_id) return; setPasswordModal({ open: true, title: 'Change Password', requireCurrent: true, }); }} > Change password
{ e.preventDefault(); try { const selectedCountryInfo = getSelectedCountry(); const profileData = { first_name: personalInfo.firstName || '', last_name: personalInfo.lastName || '', address: personalInfo.address || '', city: personalInfo.city || '', country: selectedCountryInfo?.name || '', postal_code: personalInfo.postalCode || '', phone: phoneNumber || '', }; const result = await updateUserProfile(profileData); setAlertModal({ open: true, title: result.success ? 'Profile updated' : 'Update failed', message: result.message, type: result.success ? 'success' : 'error', }); if (result.success) { loadUserProfile(); } } catch (error) { console.error('Error updating profile:', error); setAlertModal({ open: true, title: 'Error', message: 'An error occurred while updating your profile. Please try again.', type: 'error', }); } }} >

Used for invoices, fraud checks, and support. Keep this information accurate.

{detectingLocation ? ( ) : ( )} {detectingLocation ? 'Detecting…' : 'Auto-detect country'}
setPersonalInfo((prev) => ({ ...prev, firstName: e.target.value }))} className={settingsInputClass} placeholder="First name" autoComplete="given-name" />
setPersonalInfo((prev) => ({ ...prev, lastName: e.target.value }))} className={settingsInputClass} placeholder="Last name" autoComplete="family-name" />
{ setSelectedCountry(value); const country = countryOptions.find((c) => c.value === value); if (country) { setPersonalInfo((prev) => ({ ...prev, country: country.name })); } }} placeholder="Select country" icon={Globe} />
{getSelectedCountry()?.dialCode || 'β€”'}
{ const value = e.target.value.replace(/[^\d+]/g, ''); setPhoneNumber(value); }} className={`${settingsInputClass} flex-1 min-w-0`} placeholder="National number" autoComplete="tel-national" />
setPersonalInfo((prev) => ({ ...prev, address: e.target.value }))} className={settingsInputClass} placeholder="Street, building, unit" autoComplete="street-address" />
setPersonalInfo((prev) => ({ ...prev, city: e.target.value }))} className={settingsInputClass} placeholder="City" autoComplete="address-level2" />
setPersonalInfo((prev) => ({ ...prev, postalCode: e.target.value }))} className={settingsInputClass} placeholder="Postal code" autoComplete="postal-code" />
Save contact details
({ value: currency.code, label: (
{currencyFlags[currency.code] || 'πŸ’±'}
{currency.code} {currency.name}
), }))} onChange={(value) => { setSelectedCurrency(value); if (onCurrencyChange && typeof onCurrencyChange === 'function') { onCurrencyChange(value); } }} />
{currencyFlags[selectedCurrency] || 'πŸ’±'}
{currencies.find((c) => c.code === selectedCurrency)?.symbol || '$'} {selectedCurrency}
{/* Support & Contact Information */} {/*

Need Help?

Our support team is here to help you with any questions or issues you might have.

Typical response time: Within 24 hours

Support available: Monday - Friday, 9:00 - 17:00 CET

Documentation Community Forum
*/}
Download JSON export setDeleteAccountModal(true)} > Delete account…
)}
{/* Enhanced Server Details Modal */} {selectedServer && createPortal(
{getServerStatusInfo(selectedServer.status).label} β€’ {selectedServer.type} node
} actions={
{selectedServer.status === 'Running' ? ( <> { handleServerAction(selectedServer.id, 'stop'); setSelectedServer({ ...selectedServer, status: 'Stopping' }); }} className="!px-3 !min-h-[40px]" > { handleServerAction(selectedServer.id, 'restart'); setSelectedServer({ ...selectedServer, status: 'Restarting' }); }} className="!px-3 !min-h-[40px]" > ) : ( { handleServerAction(selectedServer.id, 'start'); setSelectedServer({ ...selectedServer, status: 'Starting' }); }} className="!px-3 !min-h-[40px]" > )} handleDeleteServer(selectedServer.id)} className="!px-3 !min-h-[40px]" >
setIsTerminalFullscreen(!isTerminalFullscreen)} className="!px-3 !min-h-[40px]" > {isTerminalFullscreen ? : } { setSelectedServer(null); setServerLogs(''); disconnectTerminal(); setServerMetrics({ cpu: '0m', memory: '0Mi' }); setMetricsHistory({ cpu: [], memory: [] }); setActiveServerDetailTab('overview'); }} className="!px-3 !min-h-[40px]" >
} />
{[ { id: 'overview', label: 'Overview', icon: Info }, ...(selectedServer.type === 'game' ? [ { id: 'game-console', label: 'Game Console', icon: Gamepad2 }, ...(selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft') ? [ { id: 'plugin-store', label: 'Plugin Store', icon: Package }, { id: 'minecraft-settings', label: 'Quick Settings', icon: Settings }, ] : []), ] : []), ...(selectedServer.type !== 'database' && selectedServer.type !== 'vps' ? [{ id: 'files', label: 'Files & SFTP', icon: Folder }] : []), { id: 'network', label: 'Network', icon: NetworkIcon }, { id: 'backups', label: 'Backups', icon: Database }, ].map((tab) => { const Icon = tab.icon; const active = activeServerDetailTab === tab.id; return ( ); })}
{activeServerDetailTab === 'overview' && (
{/* Top section: Overview and Resources */}
{[ { label: 'Status', value: getServerStatusInfo(selectedServer.status).label, color: getServerStatusInfo(selectedServer.status).color }, { label: 'Type', value: selectedServer.type.charAt(0).toUpperCase() + selectedServer.type.slice(1), color: colors.brightWhite }, { label: 'Created', value: parseServerDate(selectedServer.created_at), color: colors.brightWhite }, ...(selectedServer.image ? [{ label: 'Image', value: selectedServer.image, color: colors.brightWhite }] : []), ].map((item, index) => (
{item.label} {item.value}
))}
{[ { icon: Cpu, label: 'vCPU', value: selectedServer.cpu, color: colors.blue }, { icon: MemoryStick, label: 'RAM', value: selectedServer.ram, color: colors.green }, { icon: HardDriveIcon, label: 'SSD', value: selectedServer.storage, color: colors.yellow }, ].map((item, index) => { const MetricIcon = item.icon; return (
{item.value} {item.label}
); })}
{isEditingResources && (
{[ { id: 'cpu', icon: Cpu, label: 'CPU Cores', editValue: resourceValues.cpu, unit: 'vCPU', min: 1, max: 4, step: 1, color: colors.blue }, { id: 'ram', icon: MemoryStick, label: 'RAM Capacity', editValue: resourceValues.ram, unit: 'GB', min: 1, max: 12, step: 1, color: colors.green }, { id: 'storage', icon: HardDriveIcon, label: 'SSD Storage', editValue: resourceValues.storage, unit: 'GB', min: 10, max: 100, step: 5, color: colors.yellow }, ].map((item) => ( setResourceValues({ ...resourceValues, [item.id]: value })} label={item.label} unit={item.unit} icon={item.icon} color={item.color} /> ))}
)}
{!isEditingResources ? ( ) : (
)}
{/* Minecraft Software Configuration */} {((selectedServer.image && selectedServer.image.toLowerCase() === 'minecraft') || (selectedServer.type === 'game' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft'))) && (

Minecraft stack β€” game version, server engine, and JVM runtime.

{[ { key: 'version', label: 'Version', hint: 'Game release', icon: Tag, accent: colors.cyan, display: selectedServer.version, isEditing: isEditingVersion, onEdit: () => setIsEditingVersion(true), editContent: ( v.versions).map((v) => ({ value: v, label: v }))} onChange={setNewVersion} onSave={() => handleUpdateConfig('version', newVersion)} onCancel={() => setIsEditingVersion(false)} color={colors.cyan} /> ), }, { key: 'engine', label: 'Engine', hint: 'Server software', icon: Layers, accent: colors.magenta, display: selectedServer.loader || 'Paper', isEditing: isEditingLoader, onEdit: () => setIsEditingLoader(true), editContent: ( { if (opt.value === 'Fabric') { const parts = (newVersion || selectedServer.version || '').split('.'); if (parts.length >= 2 && parseInt(parts[1], 10) < 14) return false; } return true; })} onChange={setNewLoader} onSave={() => handleUpdateConfig('loader', newLoader)} onCancel={() => setIsEditingLoader(false)} color={colors.magenta} /> ), }, { key: 'java', label: 'Java Version', hint: 'JVM runtime', icon: Coffee, accent: colors.yellow, display: selectedServer.java_version || 'Java 21', isEditing: isEditingJava, onEdit: () => setIsEditingJava(true), editContent: ( handleUpdateConfig('java', newJavaVersion)} onCancel={() => setIsEditingJava(false)} color={colors.yellow} /> ), }, ].map((tile) => { const TileIcon = tile.icon; const isDemo = selectedServer.is_demo === true || selectedServer.is_demo === 1; const canEdit = !isDemo; return (

{tile.label}

{tile.hint}

{canEdit && !tile.isEditing && ( )}
{tile.isEditing && canEdit ? ( tile.editContent ) : (

{tile.display}

)}
); })}
)} {/* Bottom section: Real-time Metrics */}
{ const cpuStr = selectedServer?.cpu || '1'; let cpuCores = 1; if (cpuStr.includes('vCPU')) cpuCores = parseInt(cpuStr.split()[0]) || 1; else if (typeof cpuStr === 'string' && /^\d+$/.test(cpuStr.trim())) cpuCores = parseInt(cpuStr.trim()) || 1; else { const match = cpuStr.match(/(\d+)/); if (match) cpuCores = parseInt(match[1]) || 1; } return cpuCores * 1000; })()} currentValue={serverMetrics.cpu?.display || '0m'} percentage={Math.round(serverMetrics.cpu_percentage || 0)} serverStatus={selectedServer?.status} />
)} {activeServerDetailTab === 'game-console' && ( )} {activeServerDetailTab === 'plugin-store' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft') && (
)} {activeServerDetailTab === 'minecraft-settings' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft') && ( )} {activeServerDetailTab === 'files' && (
{selectedServer.sftp_username ? (
{[ { label: 'SFTP Address', value: selectedServer.ip || NODE_IP }, { label: 'Port', value: selectedServer.sftp_port || '2022' }, { label: 'Username', value: selectedServer.sftp_username }, { label: 'Password', value: selectedServer.sftp_password, isPassword: true } ].map((item, idx) => (
{item.label}
{item.isPassword ? (window[`show_sftp_pass_${selectedServer.id}`] ? item.value : 'β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’') : item.value}
{item.isPassword && ( )}
))}
) : (

SFTP Not Provisioned

SFTP access allows you to manage files using external clients like FileZilla.

)}
)} {activeServerDetailTab === 'network' && (() => { const serverIp = selectedServer.ip || NODE_IP; const serverPorts = selectedServer.ports || []; const primaryPort = serverPorts[0]; const mappingCount = serverPorts.length + (selectedServer.sftp_port ? 1 : 0); const flowExampleInternal = primaryPort?.internal || '8080'; const flowExampleExternal = primaryPort?.external || '31234'; const canExposePorts = ['vps', 'game', 'database'].includes(selectedServer.type); return (
{showPortMappingGuide && (

Your server listens on an internal port inside its container. {branding.name} maps it to a unique external port on our public IP so people on the internet can reach it β€” without exposing your server directly.

Inside your server

:{flowExampleInternal}

What your app or game listens on

{branding.name} gateway

Secure NodePort + firewall

Public address

{serverIp}:{flowExampleExternal}

What you give to users & clients

How to Connect

{getNetworkConnectionHint(selectedServer)}

{[ { step: '01', title: 'Copy your address', body: 'Pick a port from the table below and copy its public IP:port address.' }, { step: '02', title: 'Open your client', body: selectedServer.type === 'game' ? 'Launch your game and open Multiplayer β†’ Direct Connect.' : 'Open SSH, RDP, a browser, or your database tool.' }, { step: '03', title: 'Connect', body: `Paste the full address, e.g. ${serverIp}:${flowExampleExternal}` }, ].map((item) => (
{item.step}

{item.title}

{item.body}

))}

Good to Know

{[ { icon: Globe, iconColor: colors.blue, title: 'External ports are assigned for you', body: 'For security, public ports are unique and randomly chosen. Your internal port stays whatever your service expects.', }, { icon: Shield, iconColor: colors.brightGreen, title: 'Firewall opens automatically', body: `When you expose a port, ${branding.name} opens it on the edge firewall in real time β€” no manual rules needed.`, }, { icon: Lock, iconColor: colors.yellow, title: 'System ports cannot be removed', body: 'Default service ports (game, SSH, SFTP) are managed by the platform and marked as System in the table.', }, ].map((item, idx) => { const TipIcon = item.icon; return (

{item.title}

{item.body}

); })}
)}

Internal ports are inside your server. External ports are on our public IP ({serverIp}) β€” always use the external address when connecting from outside.

{selectedServer.sftp_port && ( )} {serverPorts.map((port, index) => { const isDefault = isDefaultPort(selectedServer, port); const portTooltip = getPortTooltip(parseInt(port.internal, 10), port.external); const shouldShowTooltip = portTooltip && (selectedServer.type === 'vps' || isDefault); const serviceName = getPortServiceName(selectedServer, port, isDefault); return ( ); })}
Service Internal External (public) Protocol Actions
SFTP Access

File transfers via Files tab

22
{serverIp}:{selectedServer.sftp_port}
TCP
System
{serviceName} {portTooltip && (

{portTooltip.description}

)}
{port.internal}
{serverIp}:{port.external} {shouldShowTooltip && }
{port.protocol || 'TCP'}
{!isDefault ? ( ) : (
System
)}
{mappingCount === 0 && (

No port mappings yet

{selectedServer.status === 'running' ? 'Ports usually appear when the server starts. If yours is running, try refreshing or expose a custom port below.' : 'Start your server to provision default ports, or expose a custom port manually.'}

)}
{canExposePorts && (
Expose New Port

Enter the port your app listens on inside the server. {branding.name} assigns a public external port automatically.

{ e.preventDefault(); submitNetworkExposePort(); }} className="flex flex-col sm:flex-row items-end gap-4 w-full max-w-xl" >
setNewPortInternal(e.target.value)} placeholder="e.g. 8080" min="1" max="65535" className="w-full rounded-xl px-4 py-3 text-sm font-bold focus:outline-none transition-all duration-200 border bg-black/40 text-center placeholder:text-white/20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" style={{ borderColor: colors.brightBlack, color: colors.brightWhite }} />
Expose Port
)}
); })()} {activeServerDetailTab === 'backups' && ( )} {activeServerDetailTab === 'terminal' && (

Terminal

{isTerminalConnected ? 'Connected' : 'Disconnected'} {isTerminalConnected ? ( ) : ( )}
{terminalOutput || "Connect to server to use terminal"}
{isTerminalConnected && (
$ setTerminalInput(e.target.value)} onKeyDown={handleTerminalKeyDown} className="flex-1 bg-transparent outline-none font-mono py-2" style={{ color: colors.brightWhite }} placeholder="Type command and press Enter" autoFocus />
)}
)} {activeServerDetailTab === 'logs' && (

Server Logs

{serverLogs || "No logs available"}
)}
, document.body )} {/* ALL MODALS */} {displayNameModal.open && createPortal(

{displayNameModal.title}

setDisplayNameModal(prev => ({ ...prev, currentName: e.target.value }))} className="w-full rounded-xl px-4 py-3 text-sm focus:outline-none transition-all duration-200 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.brightWhite }} placeholder={displayNameModal.placeholder} autoFocus />
, document.body )} {emailModal.open && createPortal(

{emailModal.title}

setEmailModal(prev => ({ ...prev, currentEmail: e.target.value }))} className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors" placeholder={emailModal.placeholder} autoFocus />
setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' })} > Cancel { if (emailModal.currentEmail.trim()) { const result = await changeUserEmail(emailModal.currentEmail.trim()); setAlertModal({ open: true, title: result.success ? "Success" : "Error", message: result.message, type: result.success ? "success" : "error" }); if (result.success) { setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' }); } } }} > Change Email
, document.body )} {passwordModal.open && createPortal(

{passwordModal.title}

{passwordModal.requireCurrent && (
)}
setPasswordModal({ open: false, title: '', requireCurrent: true })} > Cancel { const currentPassword = document.getElementById('currentPassword')?.value; const newPassword = document.getElementById('newPassword')?.value; const confirmPassword = document.getElementById('confirmPassword')?.value; if (!newPassword || newPassword.length < 6) { setAlertModal({ open: true, title: "Invalid Password", message: "New password must be at least 6 characters long", type: "error" }); return; } if (newPassword !== confirmPassword) { setAlertModal({ open: true, title: "Passwords Don't Match", message: "New password and confirmation do not match", type: "error" }); return; } if (passwordModal.requireCurrent && !currentPassword) { setAlertModal({ open: true, title: "Current Password Required", message: "Please enter your current password", type: "error" }); return; } const result = await changeUserPassword(currentPassword, newPassword); setAlertModal({ open: true, title: result.success ? "Success" : "Error", message: result.message, type: result.success ? "success" : "error" }); if (result.success) { setPasswordModal({ open: false, title: '', requireCurrent: true }); } }} > Change Password
, document.body )} {/* Convert Demo to Paid Server Modal */} {convertDemoModal.open && convertDemoModal.server && createPortal(

Convert Demo Server to Paid

Convert {convertDemoModal.server.name} from a demo server to a paid server. Credits will be charged based on the server specifications.

{/* Server Specs Display */}

Server Specifications

CPU:

{convertDemoModal.server.cpu}

RAM:

{convertDemoModal.server.ram}

Storage:

{convertDemoModal.server.storage}

Type:

{convertDemoModal.server.type?.toUpperCase() || 'VPS'}

{/* Cost Display */}
Cost to Convert: {convertDemoModal.cost.toLocaleString()} credits
Your Balance: = convertDemoModal.cost ? colors.green : colors.red }}> {userCredits.toLocaleString()} credits
{userCredits < convertDemoModal.cost && (

Insufficient credits. You need {convertDemoModal.cost.toLocaleString()} credits but only have {userCredits.toLocaleString()} credits.

)}

Rate: 100 credits = {getCurrencySymbol()}{calculateCurrencyValue(100)}

, document.body )} {confirmModal.open && createPortal(
{confirmModal.type === 'danger' ? ( ) : ( )}

{confirmModal.title}

{confirmModal.message}

{ confirmModal.onConfirm(); setConfirmModal({ ...confirmModal, open: false }); }} className="flex-1" > {confirmModal.confirmLabel}
, document.body )} {alertModal.open && createPortal(
{alertModal.type === 'success' && ( )} {alertModal.type === 'error' && ( )} {alertModal.type === 'warning' && ( )} {alertModal.type === 'info' && ( )}

{alertModal.title}

{alertModal.message}

setAlertModal({ open: false, title: '', message: '', type: 'info' })} > OK
, document.body )} {deleteAccountModal && createPortal(

Delete Account

Warning: This action cannot be undone!

All your servers, domains, tickets, and account data will be permanently deleted.

setDeleteAccountModal(false)} > Cancel { const confirmation = document.getElementById('deleteConfirmation')?.value; if (confirmation === 'DELETE MY ACCOUNT') { await deleteUserAccount(confirmation); setDeleteAccountModal(false); } else { setAlertModal({ open: true, title: "Invalid Confirmation", message: "Please type exactly: DELETE MY ACCOUNT", type: "error" }); } }} > Delete Account
, document.body )} {/* Delete Server Confirmation Modal */} {deleteServerModal.open && createPortal(

Delete Server

Warning: This action cannot be undone!

All server data, files, and configurations will be permanently deleted.

, document.body )} {/* Server Creation Progress Notifier */} {serverCreationProgress.visible && createPortal(

Server Creation in Progress

{serverCreationProgress.message}

{/* Progress Bar */}
{/* Progress Percentage */}
{serverCreationProgress.phase} {serverCreationProgress.progress.toFixed(1)}%
, document.body )} {show2FASetup && createPortal(

Enable Two-Factor Authentication

Scan the QR code with your authenticator app.

2FA QR Code

Then enter the code from your app below.

, document.body )}
); } export default DashboardPage;