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 (
{dashboardStatus === 'connected' ? (
) : (
)}
{dashboardStatus === 'connected' ? 'Live' : 'Offline'}
{calculateCurrencyValue(userCredits)} {getCurrencySymbol(userCredits)}
{dashboardSidebarItems.map((tab) => {
const Icon = tab.icon;
const active = activeTab === tab.id;
return (
handleTabChange(tab.id)}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border ${
active
? 'bg-primary/10 border-primary/25 text-dash-text shadow-lg shadow-primary/5'
: 'border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10'
}`}
>
{tab.label}
);
})}
Resources
navigate('/')} className="w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10">
Home
navigate('/documentation')} className="w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10">
Documentation
{user?.username ? user.username.substring(0, 2).toUpperCase() : '??'}
{user?.username}
{user?.role || 'Account'}
setIsMobileSidebarOpen(!isMobileSidebarOpen)} className="p-2.5 rounded-xl text-dash-textMuted hover:bg-white/10 active:scale-95 transition-all">
{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
({
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
setActiveTab('tickets')}
className="p-4 rounded-xl border transition-all duration-200 hover:scale-105 text-center"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
Open Support Ticket
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 (
{
setActiveServerDetailTab(tab.id);
if (tab.id !== 'network') setShowPortMappingGuide(false);
}}
className={`flex items-center space-x-2 px-5 py-4 text-xs font-bold uppercase tracking-widest border-b-2 transition-all duration-200 ${
active
? 'border-primary text-primary bg-primary/5'
: 'border-transparent text-dash-textMuted hover:text-dash-text hover:bg-white/5'
}`}
>
{tab.label}
);
})}
{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 ? (
{
setIsEditingResources(true);
setResourceValues({
cpu: parseCpuValue(selectedServer.cpu),
ram: parseRamValue(selectedServer.ram),
storage: parseStorageValue(selectedServer.storage),
});
}}
disabled={selectedServer.is_demo === true || selectedServer.is_demo === 1}
className="w-full flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl border font-bold text-sm transition-all duration-200 hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed group"
style={{ backgroundColor: `${colors.green}12`, borderColor: `${colors.green}35`, color: colors.brightGreen }}
>
Adjust Resources
) : (
Save Changes
setIsEditingResources(false)}
className="flex-1 py-3.5 rounded-xl font-bold text-sm transition-all duration-200 hover:bg-white/5 active:scale-[0.98] flex items-center justify-center gap-2 border border-white/10"
style={{ color: colors.foreground }}
>
Cancel
)}
{/* 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 (
{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 && (
{
window[`show_sftp_pass_${selectedServer.id}`] = !window[`show_sftp_pass_${selectedServer.id}`];
// Force a re-render by updating a dummy state or just rely on the next update
// Since we don't have a local state here easily without refactoring,
// we use a trick to trigger re-render if needed, but standard React pattern is better.
// For now, let's use a simple approach.
const btn = document.activeElement;
if (btn) btn.blur();
// We will add a small state to DashboardPage to handle this properly
setToggleSFTPPassword(!toggleSFTPPassword);
}}
className="p-1.5 rounded-lg hover:bg-white/10 transition-colors"
title={window[`show_sftp_pass_${selectedServer.id}`] ? "Hide password" : "Show password"}
>
{window[`show_sftp_pass_${selectedServer.id}`] ? : }
)}
{
navigator.clipboard.writeText(item.value);
showAlert('Success', `${item.label} copied to clipboard`, 'success');
}}
className="p-1.5 rounded-lg hover:bg-white/10 transition-colors"
>
))}
{
const sftpUrl = `sftp://${selectedServer.sftp_username}:${selectedServer.sftp_password}@${selectedServer.ip || NODE_IP}:${selectedServer.sftp_port || '2022'}`;
window.open(sftpUrl, '_blank');
}}
className="flex-1 lg:w-full flex items-center justify-center space-x-2 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 border"
style={{ backgroundColor: colors.blue, borderColor: colors.blue, color: colors.brightWhite }} >
Launch SFTP
{
const cmd = `sftp -P ${selectedServer.sftp_port || '2022'} ${selectedServer.sftp_username}@${selectedServer.ip || NODE_IP}`;
navigator.clipboard.writeText(cmd);
showAlert('Success', 'SFTP command copied to clipboard', 'success');
}}
className="flex-1 lg:w-full flex items-center justify-center space-x-2 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 border bg-white/5"
style={{ borderColor: 'rgba(255,255,255,0.1)', color: colors.brightWhite }}
>
Copy SSH Cmd
) : (
SFTP Not Provisioned
SFTP access allows you to manage files using external clients like FileZilla.
sendDashboardMessage({ type: 'generate_sftp', serverId: selectedServer.id })}
className="px-8 py-3 rounded-xl font-bold transition-all duration-200 hover:scale-105 border flex items-center space-x-2"
style={{ backgroundColor: colors.yellow, borderColor: colors.yellow, color: 'black' }}
>
Provision SFTP Access
)}
)}
{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 (
setShowPortMappingGuide((open) => !open)}
className="w-full flex items-center justify-between gap-4 p-5 sm:p-6 text-left transition-colors hover:bg-white/[0.02]"
>
How Port Mapping Works
{showPortMappingGuide ? 'Tap to collapse' : 'Port mapping, how to connect, and tips'}
{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
{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) => (
))}
{[
{
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 (
);
})}
)}
Internal ports are inside your server. External ports are on our public IP ({serverIp}) β always use the external address when connecting from outside.
Service
Internal
External (public)
Protocol
Actions
{selectedServer.sftp_port && (
SFTP Access
File transfers via Files tab
22
{serverIp}:{selectedServer.sftp_port}
TCP
{
navigator.clipboard.writeText(`${serverIp}:${selectedServer.sftp_port}`);
setAlertModal({ open: true, title: 'Copied', message: 'SFTP address copied to clipboard', type: 'success' });
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-white/10"
style={{ color: colors.primary }}
>
System
)}
{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 (
{serviceName}
{portTooltip && (
{portTooltip.description}
)}
{port.internal}
{serverIp}:{port.external}
{shouldShowTooltip &&
}
{port.protocol || 'TCP'}
{
navigator.clipboard.writeText(`${serverIp}:${port.external}`);
setAlertModal({ open: true, title: 'Success', message: 'Address copied to clipboard', type: 'success' });
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-white/10"
style={{ color: colors.primary }}
>
{!isDefault ? (
{
showConfirm(
'Remove port mapping?',
`Remove the public mapping for internal port ${port.internal} (${port.protocol || 'TCP'})? Players and clients will no longer reach this service on the external address.`,
() => handleRemovePort(selectedServer.id, port.internal, port.protocol),
'danger',
{ confirmLabel: 'Yes', cancelLabel: 'No' }
);
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-red-500/20"
style={{ color: colors.red }}
>
) : (
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.
)}
);
})()}
{activeServerDetailTab === 'backups' && (
)}
{activeServerDetailTab === 'terminal' && (
Terminal
{isTerminalConnected ? 'Connected' : 'Disconnected'}
{isTerminalConnected ? (
Disconnect
) : (
Connect
)}
{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
fetchServerLogs(selectedServer.id)}
className="px-5 py-3 text-sm rounded-xl flex items-center space-x-2 transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.blue,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
Refresh
{serverLogs || "No logs available"}
)}
,
document.body
)}
{/* ALL MODALS */}
{displayNameModal.open && createPortal(
{displayNameModal.title}
setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
{displayNameModal.label}
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
/>
setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
{
if (displayNameModal.currentName.trim()) {
const result = await updateUserProfile({ [displayNameModal.field]: displayNameModal.currentName.trim() });
setAlertModal({
open: true,
title: result.success ? "Success" : "Error",
message: result.message,
type: result.success ? "success" : "error"
});
if (result.success) {
setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' });
}
}
}}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.blue,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
Save Changes
,
document.body
)}
{emailModal.open && createPortal(
{emailModal.title}
setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
{emailModal.label}
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}
setPasswordModal({ open: false, title: '', requireCurrent: true })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
{passwordModal.requireCurrent && (
Current Password
)}
New Password
Confirm New Password
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
setConvertDemoModal({ open: false, server: null, cost: 0 })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
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)}
setConvertDemoModal({ open: false, server: null, cost: 0 })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
= convertDemoModal.cost ? colors.brightYellow : colors.brightBlack,
borderColor: userCredits >= convertDemoModal.cost ? colors.brightYellow : colors.brightBlack,
color: userCredits >= convertDemoModal.cost ? colors.black : colors.foreground
}}
>
Convert to Paid
,
document.body
)}
{confirmModal.open && createPortal(
{confirmModal.type === 'danger' ? (
) : (
)}
{confirmModal.title}
setConfirmModal({ ...confirmModal, open: false })}
className="p-2 rounded-lg text-dash-textMuted hover:bg-white/5 transition-colors"
>
{confirmModal.message}
setConfirmModal({ ...confirmModal, open: false })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:bg-white/5 border border-dash-border text-dash-text"
>
{confirmModal.cancelLabel}
{
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}
setAlertModal({ open: false, title: '', message: '', type: 'info' })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
{alertModal.message}
setAlertModal({ open: false, title: '', message: '', type: 'info' })}
>
OK
,
document.body
)}
{deleteAccountModal && createPortal(
setDeleteAccountModal(false)}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
Warning: This action cannot be undone!
All your servers, domains, tickets, and account data will be permanently deleted.
Type DELETE MY ACCOUNT to confirm:
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(
setDeleteServerModal({ open: false, serverId: null })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
Warning: This action cannot be undone!
All server data, files, and configurations will be permanently deleted.
setDeleteServerModal({ open: false, serverId: null })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
Delete Server
,
document.body
)}
{/* Server Creation Progress Notifier */}
{serverCreationProgress.visible && createPortal(
Server Creation in Progress
{serverCreationProgress.message}
setServerCreationProgress(prev => ({ ...prev, visible: false }))}
className="p-1 rounded-lg transition-all duration-200 hover:scale-105"
style={{
backgroundColor: colors.brightBlack,
color: colors.foreground
}}
>
{/* Progress Bar */}
{/* Progress Percentage */}
{serverCreationProgress.phase}
{serverCreationProgress.progress.toFixed(1)}%
,
document.body
)}
{show2FASetup && createPortal(
Enable Two-Factor Authentication
setShow2FASetup(false)} className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.foreground }}>
Scan the QR code with your authenticator app.
Then enter the code from your app below.
setShow2FASetup(false)} className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.brightWhite }}>
Cancel
{
const otpCode = document.getElementById('otp-code').value;
if (otpCode) {
sendDashboardMessage({ type: '2fa_enable', otp_code: otpCode });
}
}} className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.green, borderColor: colors.green, color: colors.brightWhite }}>
Verify & Enable
,
document.body
)}
);
}
export default DashboardPage;