commit cc60ca7103558f31c9165b219a43ddbb6e758bef Author: Yuri Date: Mon Nov 3 13:40:33 2025 +0100 password fix diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b57e8f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ArkHost + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fe5a7e --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# ArkHost Hetzner VPS Module + +WHMCS server module for Hetzner Cloud VPS management. + +## Features + +**Core** + +- VPS lifecycle: create, suspend, unsuspend, terminate +- Server control: start, stop, restart, shutdown +- Automatic provisioning on payment + +**Advanced** + +- Floating IP management with reverse DNS +- Backup creation and restoration +- Firewall rule management +- Performance graphs (CPU, network, disk I/O) +- Rescue mode with temporary passwords +- ISO mounting +- OS reinstallation + +**Interface** + +- Bootstrap 4 responsive design +- Multiple language translations included +- Real-time status updates +- VNC console access + +## Requirements + +- WHMCS 8.9+ +- PHP 7.4+ +- Hetzner Cloud API token +- cURL extension +- HTTPS connection + +## Installation + +1. Upload files to `/path/to/whmcs/modules/servers/ArkHostHetznerVPS/` +2. Create server and add it to a server group: + - Setup → Products/Services → Servers → Create New Group + - Name: `Hetzner Cloud` + - Type: `ArkHostHetznerVPS` + - Hostname: `localhost` + - Username: Hetzner Project ID (optional) + - Password: Hetzner Cloud API Token +3. Create products: + - Setup → Products/Services → Products/Services → Create New Product + - Type: `VPS/Dedicated Server` + - Module: `ArkHostHetznerVPS` + - Server Group: Select Hetzner Cloud + +## Configuration + +**Module Settings** + +- Server Type: `cx11`, `cx21`, `cx31`, `cx41`, `cx51` , etc. +- Operating System: `ubuntu-20.04`, `debian-11`, etc. +- Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil` +- Backups: On/Off +- Create Floating IP: On/Off + +**Custom Field (Required)** + +- Field Name: `ArkHostHetznerVPS|VPS ID` +- Type: Text Box +- Admin Only: Yes + +For ModulesGarden migration, keep existing `serverID|Server ID` field. + +## Configurable Options + +Create in Setup → Products/Services → Configurable Options: + +**1\. Floating IP Add-on** + +- Option Name: `Floating IP` +- Option Type: `Yes/No` +- Options: + - `No|None` + - `Yes|1 Floating IP` + +**2\. Datacenter Selection** + +- Option Name: `datacenter` +- Option Type: `Dropdown` +- Options: + - `fsn1-dc14|Falkenstein, Germany` + - `nbg1-dc3|Nuremberg, Germany` + - `hel1-dc2|Helsinki, Finland` + - `ash-dc1|Ashburn, USA` + - `hil-dc1|Hillsboro, USA` + - `sin-dc1|Singapore, Singapore` + +**3\. Server Type** + +- Option Name: `planid` +- Option Type: `Dropdown` +- Options: +- `cpx11|CPX 11 - 2 vCPU, 2 GB RAM, 40 GB Disk, shared` +- `cpx21|CPX 21 - 3 vCPU, 4 GB RAM, 80 GB Disk, shared` +- `cpx31|CPX 31 - 4 vCPU, 8 GB RAM, 160 GB Disk, shared` +- `cpx41|CPX 41 - 8 vCPU, 16 GB RAM, 240 GB Disk, shared` +- `cpx51|CPX 51 - 16 vCPU, 32 GB RAM, 360 GB Disk, shared` +- `cax11|CAX11 - 2 vCPU, 4 GB RAM, 40 GB Disk, shared` +- `cax21|CAX21 - 4 vCPU, 8 GB RAM, 80 GB Disk, shared` +- `cax31|CAX31 - 8 vCPU, 16 GB RAM, 160 GB Disk, shared` +- `cax41|CAX41 - 16 vCPU, 32 GB RAM, 320 GB Disk, shared` +- `ccx13|CCX13 Dedicated CPU - 2 vCPU, 8 GB RAM, 80 GB Disk, dedicated` +- `ccx23|CCX23 Dedicated CPU - 4 vCPU, 16 GB RAM, 160 GB Disk, dedicated` +- `ccx33|CCX33 Dedicated CPU - 8 vCPU, 32 GB RAM, 240 GB Disk, dedicated` +- `ccx43|CCX43 Dedicated CPU - 16 vCPU, 64 GB RAM, 360 GB Disk, dedicated` +- `ccx53|CCX53 Dedicated CPU - 32 vCPU, 128 GB RAM, 600 GB Disk, dedicated` +- `ccx63|CCX63 Dedicated CPU - 48 vCPU, 192 GB RAM, 960 GB Disk, dedicated` +- `cx22|CX22 - 2 vCPU, 4 GB RAM, 40 GB Disk, shared` +- `cx32|CX32 - 4 vCPU, 8 GB RAM, 80 GB Disk, shared` +- `cx42|CX42 - 8 vCPU, 16 GB RAM, 160 GB Disk, shared` +- `cx52|CX52 - 16 vCPU, 32 GB RAM, 320 GB Disk, shared` + + +**4\. Operating System** + +- Option Name: `osid` +- Option Type: `Dropdown` +- Options: +- `lamp|lamp` +-`wordpress|wordpress` +-`jitsi|jitsi` +-`nextcloud|nextcloud` +- `docker-ce|docker-ce` +- `gitlab|gitlab` +- `debian-11|Debian 11` +- `rocky-8|Rocky Linux 8` +- `centos-stream-9|CentOS Stream 9` +- `ubuntu-22.04|Ubuntu 22.04` +- `prometheus-grafana|prometheus-grafana` +- `rocky-9|Rocky Linux 9` +- `wireguard|wireguard` +- `owncast|owncast` +- `photoprism|photoprism` +- `rustdesk|rustdesk` +- `centos-stream-9|CentOS Stream 9` +- `debian-11|Debian 11` +- `rocky-8|Rocky Linux 8` +- `rocky-9|Rocky Linux 9` +- `ubuntu-22.04|Ubuntu 22.04` +- `alma-8|AlmaLinux 8` +- `alma-9|AlmaLinux 9` +- `jitsi|jitsi` +- `owncast|owncast` + + + + +## Client Interface + +**Overview**: Server status, information, control buttons **Graphs**: CPU, network, disk I/O monitoring **Backups**: Create, restore, delete backups **Settings**: Hostname, ISO, password reset, reinstall, firewall, rescue mode, floating IP + +## SSH Key Management + +**Why no SSH key feature?** + +Hetzner's API stores SSH keys at the project level, not per-server. In a multi-tenant WHMCS environment, this creates security risks: + +- Potential key exposure across customers +- No proper isolation between services +- Complex workarounds that compromise security + +**Current recommended approach:** + +1. Use password authentication for initial access +2. Add your SSH key manually: `ssh-copy-id root@your-server-ip` +3. Secure your server: + +```bash +sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_configsystemctl restart sshd +``` + +This ensures proper key isolation and security for each customer. + +**Future development:** + +We're exploring secure implementations using cloud-init or per-server user data to enable SSH key management while maintaining proper isolation. + +## Screenshots +![Screenshot 1](screenshots/1.png) +![Screenshot 2](screenshots/2.png) +![Screenshot 3](screenshots/3.png) +![Screenshot 4](screenshots/4.png) +![Screenshot 5](screenshots/5.png) +![Screenshot 6](screenshots/6.png) +![Screenshot 7](screenshots/7.png) +![Screenshot 8](screenshots/8.png) +![Screenshot 9](screenshots/9.png) +![Screenshot 10](screenshots/10.png) + +## Troubleshooting + +**API Connection Failed**: Check API token in server group settings **Server Not Found**: Verify VPS ID custom field exists **Floating IP Issues**: Enable in configurable options + +Enable module debugging: Configuration → System Logs → Module Log + +## Support Policy + +This is free, open-source software provided AS-IS. + +**NO SUPPORT** is provided except through paid channels: +- Premium Support: €100/hour (2 hour minimum) +- Email: support@arkhost.com (paid support only) + +### Before Requesting Support +- English only +- Must provide complete error logs +- Must provide steps to reproduce +- Vague reports like "doesn't work" will be ignored + +### GitHub Issues +- For documented bugs only +- Requires complete technical information +- Issues lacking proper information will be closed immediately +- This is not a support forum + +If you need help with basic installation or configuration, purchase premium support. + +## License + +MIT License - see [LICENSE](LICENSE) file for details. + +Free and open source. Commercial support available. + +## Other ArkHost Modules + +Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://arkhost.com/whmcs-modules.php "https://arkhost.com/whmcs-modules.php") + +© 2025 ArkHost diff --git a/modules/servers/ArkHostHetznerVPS/ArkHostHetznerVPS.php b/modules/servers/ArkHostHetznerVPS/ArkHostHetznerVPS.php new file mode 100644 index 0000000..08ffe48 --- /dev/null +++ b/modules/servers/ArkHostHetznerVPS/ArkHostHetznerVPS.php @@ -0,0 +1,2551 @@ + + * @link https://arkhost.com + */ + +if (!defined('WHMCS')) { + http_response_code(403); + exit('Access denied'); +} + +use WHMCS\Config\Setting; +use WHMCS\Database\Capsule; + +function ArkHostHetznerVPS_GetVPSID(array $params) { + // Check if we have model (new way) - ArkHostHetznerVPS format + if (isset($params['model']) && is_object($params['model']) && isset($params['model']->serviceProperties)) { + // Try our new format first + $vpsId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|VPS ID'); + if ($vpsId) { + return str_replace(['ArkHostHetznerVPS-', 'VPS-'], '', $vpsId); + } + + // Try ModulesGarden format for migration compatibility + $serverId = $params['model']->serviceProperties->get('serverID|Server ID'); + if ($serverId) { + return str_replace(['server-', 'Server-'], '', $serverId); + } + } + + // Check if we have customfields (alternative way) + if (isset($params['customfields']) && is_array($params['customfields'])) { + foreach ($params['customfields'] as $field => $value) { + // Check for our new format: ArkHostHetznerVPS|VPS ID + if (stripos($field, 'ArkHostHetznerVPS') !== false && stripos($field, 'VPS ID') !== false) { + return str_replace(['ArkHostHetznerVPS-', 'VPS-'], '', $value); + } + + // Check for ModulesGarden format: serverID|Server ID (MIGRATION COMPATIBILITY) + if (stripos($field, 'serverID') !== false && stripos($field, 'Server ID') !== false) { + return str_replace(['server-', 'Server-'], '', $value); + } + + // Fallback: any field containing VPS ID + if (stripos($field, 'VPS ID') !== false && !empty($value)) { + return str_replace(['ArkHostHetznerVPS-', 'VPS-', 'server-', 'Server-'], '', $value); + } + } + } + + // For ModulesGarden migration - check if server ID is in domain field + if (isset($params['domain']) && is_numeric($params['domain'])) { + return $params['domain']; + } + + // For ModulesGarden migration - check username field + if (isset($params['username']) && is_numeric($params['username'])) { + return $params['username']; + } + + // Check customfields for any numeric server ID (broader compatibility) + if (isset($params['customfields']) && is_array($params['customfields'])) { + foreach ($params['customfields'] as $field => $value) { + // ModulesGarden might store it as "Server ID" or similar + if ((stripos($field, 'server') !== false || stripos($field, 'vps') !== false) && is_numeric($value)) { + return $value; + } + } + } + + // Check if there's a dedicatedip field with server ID (some modules use this) + if (isset($params['dedicatedip']) && preg_match('/(\d{6,})/', $params['dedicatedip'], $matches)) { + return $matches[1]; + } + + return null; +} + +function ArkHostHetznerVPS_GetConfigurableOption(array $params, $optionName) { + // Check for WHMCS Configurable Options (add-ons) that customer has ordered + if (isset($params['configoptions']) && is_array($params['configoptions'])) { + foreach ($params['configoptions'] as $optName => $optValue) { + // Check if option name contains the floating IP identifier + if (stripos($optName, $optionName) !== false && !empty($optValue)) { + return $optValue; + } + } + } + + // Also check by option ID if available + if (isset($params['configoptionshash']) && is_array($params['configoptionshash'])) { + foreach ($params['configoptionshash'] as $optId => $optValue) { + if (stripos($optId, $optionName) !== false && !empty($optValue)) { + return $optValue; + } + } + } + + return null; +} + +function ArkHostHetznerVPS_API(array $params) { + $url = 'https://api.hetzner.cloud/v1/'; + $data = []; + $method = ''; + + switch ($params['action']) { + case 'Test': + $url .= 'server_types?per_page=1'; + $method = 'GET'; + break; + + case 'Packages': + $url .= 'server_types'; + $method = 'GET'; + break; + + case 'Operating Systems': + $url .= 'images?type=system'; + $method = 'GET'; + break; + + case 'Datacenters': + $url .= 'datacenters'; + $method = 'GET'; + break; + + case 'Upgrades': + // Not supported in Hetzner API + return array(); + break; + + case 'Discount': + // Hetzner doesn't have a discount endpoint, return dummy data + return array('percent' => 0); + break; + + case 'Balance': + // Hetzner doesn't have a balance endpoint, return dummy data + return array('balance' => 'N/A'); + break; + + case 'Order': + $url .= 'servers'; + $method = 'POST'; + + $data = array( + 'name' => $params['domain'] ?? 'vps-' . time(), + 'server_type' => ArkHostHetznerVPS_GetOption($params, 'planid'), + 'image' => ArkHostHetznerVPS_GetOption($params, 'osid'), + 'start_after_create' => true, + 'labels' => array( + 'whmcs_service_id' => (string)$params['serviceid'], + 'whmcs_user_id' => (string)$params['userid'] + ) + ); + + // Add datacenter if specified + $datacenter = ArkHostHetznerVPS_GetOption($params, 'datacenter'); + if ($datacenter) { + $data['datacenter'] = $datacenter; + } + + // Handle backups + if (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on') { + $data['automount'] = false; + $data['backups'] = true; + } + + // Handle IPv6 + if (ArkHostHetznerVPS_GetOption($params, 'ipv6') === 'on') { + $data['enable_ipv6'] = false; + } + + // Handle SSH keys from custom field if needed + // This would need to be implemented with a custom field + break; + + case 'Server Info': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'GET'; + break; + + case 'Label': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'PUT'; + $data = array( + 'name' => $params['label'], + ); + break; + + case 'Graphs': + // Hetzner metrics endpoint requires specific format + $serverId = ArkHostHetznerVPS_GetVPSID($params); + + // Determine time range based on period + $period = $params['time'] ?? 'day'; + $end = time(); + switch ($period) { + case 'hour': + $start = $end - 3600; // 1 hour + $step = 60; // 1 minute intervals + break; + case 'day': + $start = $end - 86400; // 24 hours + $step = 300; // 5 minute intervals + break; + case 'week': + $start = $end - 604800; // 7 days + $step = 1800; // 30 minute intervals + break; + case 'month': + $start = $end - 2592000; // 30 days + $step = 7200; // 2 hour intervals + break; + case 'year': + $start = $end - 31536000; // 365 days + $step = 86400; // 1 day intervals + break; + default: + $start = $end - 86400; + $step = 300; + } + + // Get all metric types at once + $metricType = 'cpu,disk,network'; + + // Format timestamps in ISO-8601 - Hetzner requires specific format without timezone + $startISO = gmdate('Y-m-d\TH:i:s\Z', $start); + $endISO = gmdate('Y-m-d\TH:i:s\Z', $end); + + $url .= 'servers/' . $serverId . '/metrics?type=' . $metricType . '&start=' . $startISO . '&end=' . $endISO . '&step=' . $step; + $method = 'GET'; + break; + + case 'Operating Systems - Server': + $url .= 'images?type=system'; + $method = 'GET'; + break; + + case 'Cancel': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'DELETE'; + break; + + case 'Stop Cancellation': + // Not supported in Hetzner API + return array('success' => true); + break; + + case 'VNC Console': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/request_console'; + $method = 'POST'; + // No data needed for request_console + break; + + case 'Reinstall': + // Rebuild server with new image (destroys all data) + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/rebuild'; + $method = 'POST'; + $data = array( + 'image' => $params['os'] // Can be image ID or name (e.g. "ubuntu-20.04") + ); + break; + + case 'Reboot': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/reboot'; + $method = 'POST'; + break; + + case 'Stop': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweroff'; + $method = 'POST'; + break; + + case 'Shutdown': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/shutdown'; + $method = 'POST'; + break; + + case 'Start': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweron'; + $method = 'POST'; + break; + + case 'Disable': + // Use poweroff for disable in Hetzner + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweroff'; + $method = 'POST'; + break; + + case 'Enable': + // Use poweron for enable in Hetzner + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/poweron'; + $method = 'POST'; + break; + + case 'IPv4 Addresses': + // Get server info which includes IP addresses + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'GET'; + break; + + case 'Reverse DNS': + // Hetzner handles rDNS differently + return array('success' => false, 'message' => 'Not implemented'); + break; + + case 'Addons': + // Not supported in Hetzner API + return array(); + break; + + case 'Upgrade': + // Hetzner requires creating a new server + return array('success' => false, 'message' => 'Server upgrades not supported'); + break; + + case 'Hostname rDNS': + // Update server name + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'PUT'; + $data = array( + 'name' => $params['hostname'] + ); + break; + + case 'Create backup': + // Use snapshots + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image'; + $method = 'POST'; + $data = array( + 'description' => 'Backup created on ' . date('Y-m-d H:i:s'), + 'type' => 'backup' + ); + break; + + case 'Delete backup': + // Delete image + $imageId = $params['image_id'] ?? $params['file'] ?? ''; + $url .= 'images/' . $imageId; + $method = 'DELETE'; + break; + + case 'List backups': + // List images of type backup + $url .= 'images?type=backup&bound_to=' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'GET'; + break; + + case 'Restore backup': + // Rebuild from image + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/rebuild'; + $method = 'POST'; + $data = array( + 'image' => $params['image_id'] ?? $params['file'] ?? '' + ); + break; + + case 'Get Firewall rules': + // Get server info first to find attached firewalls + $serverId = ArkHostHetznerVPS_GetVPSID($params); + $url .= 'servers/' . $serverId; + $method = 'GET'; + break; + + case 'List Firewalls': + // List all available firewalls + $url .= 'firewalls'; + $method = 'GET'; + break; + + case 'Create Firewall': + $url .= 'firewalls'; + $method = 'POST'; + $data = array( + 'name' => $params['name'] ?? 'firewall-' . time(), + 'rules' => isset($params['rules']) ? $params['rules'] : array() + ); + break; + + case 'Apply Firewall': + $url .= 'firewalls/' . $params['firewall_id'] . '/actions/apply_to_resources'; + $method = 'POST'; + $serverId = ArkHostHetznerVPS_GetVPSID($params); + $data = array( + 'apply_to' => array( + array( + 'type' => 'server', + 'server' => array( + 'id' => intval($serverId) + ) + ) + ) + ); + break; + case 'Get Firewall Details': + $url .= 'firewalls/' . $params['firewall_id']; + $method = 'GET'; + break; + case 'Update Firewall Rules': + $url .= 'firewalls/' . $params['firewall_id'] . '/actions/set_rules'; + $method = 'POST'; + $data = array( + 'rules' => $params['rules'] + ); + break; + + case 'Remove Firewall': + $url .= 'firewalls/' . $params['firewall_id'] . '/actions/remove_from_resources'; + $method = 'POST'; + $data = array( + 'remove_from' => array( + array( + 'type' => 'server', + 'server' => ArkHostHetznerVPS_GetVPSID($params) + ) + ) + ); + break; + + case 'Add Firewall rules': + // For Hetzner, we need to check if a firewall exists, create one if not, then update rules + $serverId = ArkHostHetznerVPS_GetVPSID($params); + + // First, get server info to check if firewall is attached + $serverParams = $params; + $serverParams['action'] = 'Server Info'; + $serverInfo = ArkHostHetznerVPS_API($serverParams); + + $firewallId = null; + if (isset($serverInfo['server']['public_net']['firewalls']) && !empty($serverInfo['server']['public_net']['firewalls'])) { + // Use existing firewall + $firewallId = $serverInfo['server']['public_net']['firewalls'][0]['id']; + + // Get current firewall rules + $firewallParams = $params; + $firewallParams['action'] = 'Get Firewall Details'; + $firewallParams['firewall_id'] = $firewallId; + $firewallDetails = ArkHostHetznerVPS_API($firewallParams); + + $existingRules = isset($firewallDetails['firewall']['rules']) ? $firewallDetails['firewall']['rules'] : array(); + } else { + // No firewall attached, create one + $createParams = $params; + $createParams['action'] = 'Create Firewall'; + // Simple unique name + $createParams['name'] = 'server-' . $serverId . '-' . time(); + $createParams['rules'] = array(); + + try { + $createResult = ArkHostHetznerVPS_API($createParams); + } catch (Exception $e) { + // If name is still not unique, try with random suffix + if (strpos($e->getMessage(), 'uniqueness_error') !== false || strpos($e->getMessage(), 'name is already used') !== false) { + $createParams['name'] = 'firewall-server-' . $serverId . '-' . time() . '-' . rand(1000, 9999); + $createResult = ArkHostHetznerVPS_API($createParams); + } else { + throw $e; + } + } + $firewallId = $createResult['firewall']['id']; + + // Attach firewall to server + $attachParams = $params; + $attachParams['action'] = 'Apply Firewall'; + $attachParams['firewall_id'] = $firewallId; + $attachResult = ArkHostHetznerVPS_API($attachParams); + + // Wait a moment for the firewall to be applied + sleep(2); + + // Verify the firewall was attached + $verifyParams = $params; + $verifyParams['action'] = 'Server Info'; + $verifyInfo = ArkHostHetznerVPS_API($verifyParams); + + // Check if firewall is actually attached + if (!isset($verifyInfo['server']['public_net']['firewalls']) || + empty($verifyInfo['server']['public_net']['firewalls']) || + $verifyInfo['server']['public_net']['firewalls'][0]['id'] != $firewallId) { + // Try to attach again if it failed + $attachParams = $params; + $attachParams['action'] = 'Apply Firewall'; + $attachParams['firewall_id'] = $firewallId; + ArkHostHetznerVPS_API($attachParams); + } + + $existingRules = array(); + } + + // Add new rule to existing rules + // Handle protocol - Hetzner API doesn't accept "ANY", we need to create multiple rules + $protocol = strtolower($params['protocol']); + $rulesToAdd = array(); + + if ($protocol === 'any') { + // Create rules for tcp and udp when "ANY" is selected + $protocols = array('tcp', 'udp'); + foreach ($protocols as $proto) { + $rule = array( + 'direction' => isset($params['direction']) ? $params['direction'] : 'in', + 'protocol' => $proto, + ); + + // Handle IPs - use source_ips for inbound, destination_ips for outbound + $direction = isset($params['direction']) ? $params['direction'] : 'in'; + if (!empty($params['source']) && $params['source'] !== 'Any') { + $sourceIp = trim($params['source']); + // If no CIDR notation, add /32 for IPv4 or /128 for IPv6 + if (strpos($sourceIp, '/') === false) { + if (filter_var($sourceIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $sourceIp .= '/128'; + } else { + $sourceIp .= '/32'; + } + } + if ($direction === 'out') { + $rule['destination_ips'] = array($sourceIp); + $rule['source_ips'] = array(); + } else { + $rule['source_ips'] = array($sourceIp); + $rule['destination_ips'] = array(); + } + } else { + // For "Any" or empty, use 0.0.0.0/0 and ::/0 for all IPs + if ($direction === 'out') { + $rule['destination_ips'] = array('0.0.0.0/0', '::/0'); + $rule['source_ips'] = array(); + } else { + $rule['source_ips'] = array('0.0.0.0/0', '::/0'); + $rule['destination_ips'] = array(); + } + } + + // Handle port - Hetzner expects port as a string + if (!empty($params['port'])) { + $rule['port'] = strval($params['port']); + } + + $rulesToAdd[] = $rule; + } + } else { + // Single protocol rule + $newRule = array( + 'direction' => isset($params['direction']) ? $params['direction'] : 'in', + 'protocol' => $protocol, + ); + + // Handle IPs - use source_ips for inbound, destination_ips for outbound + $direction = isset($params['direction']) ? $params['direction'] : 'in'; + if (!empty($params['source']) && $params['source'] !== 'Any') { + $sourceIp = trim($params['source']); + // If no CIDR notation, add /32 for IPv4 or /128 for IPv6 + if (strpos($sourceIp, '/') === false) { + if (filter_var($sourceIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $sourceIp .= '/128'; + } else { + $sourceIp .= '/32'; + } + } + if ($direction === 'out') { + $newRule['destination_ips'] = array($sourceIp); + $newRule['source_ips'] = array(); + } else { + $newRule['source_ips'] = array($sourceIp); + $newRule['destination_ips'] = array(); + } + } else { + // For "Any" or empty, use 0.0.0.0/0 and ::/0 for all IPs + if ($direction === 'out') { + $newRule['destination_ips'] = array('0.0.0.0/0', '::/0'); + $newRule['source_ips'] = array(); + } else { + $newRule['source_ips'] = array('0.0.0.0/0', '::/0'); + $newRule['destination_ips'] = array(); + } + } + + // Handle port - Hetzner expects port as a string + if (!empty($params['port']) && $protocol !== 'icmp') { + $newRule['port'] = strval($params['port']); + } + + $rulesToAdd[] = $newRule; + } + + // Add the new rules to existing rules + // Note: Hetzner only supports ACCEPT rules for inbound traffic + // The default policy is DROP for non-matched traffic + foreach ($rulesToAdd as $ruleToAdd) { + $existingRules[] = $ruleToAdd; + } + + // Update firewall with new rules + $updateParams = $params; + $updateParams['action'] = 'Update Firewall Rules'; + $updateParams['firewall_id'] = $firewallId; + $updateParams['rules'] = $existingRules; + + ArkHostHetznerVPS_API($updateParams); + + return array('success' => true, 'message' => 'Firewall rule added successfully'); + break; + + case 'Delete Firewall rule': + // For Hetzner, we need to get all rules, remove the one, and update + $serverId = ArkHostHetznerVPS_GetVPSID($params); + + // Get server info to find attached firewall + $serverParams = $params; + $serverParams['action'] = 'Server Info'; + $serverInfo = ArkHostHetznerVPS_API($serverParams); + + if (!isset($serverInfo['server']['public_net']['firewalls']) || empty($serverInfo['server']['public_net']['firewalls'])) { + return array('success' => false, 'message' => 'No firewall attached to this server'); + } + + $firewallId = $serverInfo['server']['public_net']['firewalls'][0]['id']; + + // Get current firewall rules + $firewallParams = $params; + $firewallParams['action'] = 'Get Firewall Details'; + $firewallParams['firewall_id'] = $firewallId; + $firewallDetails = ArkHostHetznerVPS_API($firewallParams); + + $existingRules = isset($firewallDetails['firewall']['rules']) ? $firewallDetails['firewall']['rules'] : array(); + $newRules = array(); + + // Remove the rule with matching ID (we use index as ID) + $ruleIdToDelete = $params['rule_id']; + $inIndex = 0; + $outIndex = 0; + foreach ($existingRules as $rule) { + if ($rule['direction'] === 'in') { + if ('fw_' . $firewallId . '_in_' . $inIndex != $ruleIdToDelete) { + $newRules[] = $rule; + } + $inIndex++; + } else if ($rule['direction'] === 'out') { + if ('fw_' . $firewallId . '_out_' . $outIndex != $ruleIdToDelete) { + $newRules[] = $rule; + } + $outIndex++; + } else { + // Keep other rules + $newRules[] = $rule; + } + } + + // Update firewall with remaining rules + $updateParams = $params; + $updateParams['action'] = 'Update Firewall Rules'; + $updateParams['firewall_id'] = $firewallId; + $updateParams['rules'] = $newRules; + + ArkHostHetznerVPS_API($updateParams); + + return array('success' => true, 'message' => 'Firewall rule deleted successfully'); + break; + + case 'Commit Firewall rules': + // Not applicable for Hetzner - changes are immediate + return array('success' => true, 'message' => 'Firewall changes are applied immediately in Hetzner'); + break; + + case 'ISO Images': + $url .= 'isos'; + $method = 'GET'; + break; + + case 'Load ISO': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/attach_iso'; + $method = 'POST'; + // Hetzner expects the ISO name, not ID + $data = array('iso' => $params['iso_id']); + break; + + case 'Eject ISO': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/detach_iso'; + $method = 'POST'; + break; + + case 'Reset root': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/reset_password'; + $method = 'POST'; + break; + + case 'Server Metrics': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/metrics'; + $method = 'GET'; + break; + + case 'Create Snapshot': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image'; + $method = 'POST'; + $data = array( + 'description' => $params['description'] ?? 'Snapshot created on ' . date('Y-m-d H:i:s'), + 'type' => 'snapshot' + ); + break; + + case 'List Snapshots': + $url .= 'images?type=snapshot&bound_to=' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'GET'; + break; + + case 'Floating IPs': + $url .= 'floating_ips'; + $method = 'GET'; + break; + + case 'Get Floating IP': + $url .= 'floating_ips/' . $params['floating_ip_id']; + $method = 'GET'; + break; + + case 'Assign Floating IP': + $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/assign'; + $method = 'POST'; + $data = array( + 'server' => ArkHostHetznerVPS_GetVPSID($params) + ); + break; + + case 'Unassign Floating IP': + $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/unassign'; + $method = 'POST'; + break; + + case 'Create Floating IP': + $url .= 'floating_ips'; + $method = 'POST'; + $data = array( + 'type' => $params['ip_type'] ?? 'ipv4', + 'description' => $params['description'] ?? 'Created via WHMCS', + 'labels' => $params['labels'] ?? array(), + 'home_location' => $params['location'] ?? null, + 'server' => isset($params['assign_to_server']) ? ArkHostHetznerVPS_GetVPSID($params) : null + ); + break; + + case 'Delete Floating IP': + $url .= 'floating_ips/' . $params['floating_ip_id']; + $method = 'DELETE'; + break; + + case 'Update Floating IP': + $url .= 'floating_ips/' . $params['floating_ip_id']; + $method = 'PUT'; + $data = array( + 'description' => $params['description'] ?? null, + 'labels' => $params['labels'] ?? null + ); + break; + + case 'Change Floating IP DNS': + $url .= 'floating_ips/' . $params['floating_ip_id'] . '/actions/change_dns_ptr'; + $method = 'POST'; + $data = array( + 'ip' => $params['ip'], + 'dns_ptr' => $params['dns_ptr'] + ); + break; + + case 'Server Actions': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions'; + $method = 'GET'; + break; + + case 'Change Protection': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/change_protection'; + $method = 'POST'; + $data = array( + 'delete' => isset($params['delete_protection']) ? $params['delete_protection'] : false, + 'rebuild' => isset($params['rebuild_protection']) ? $params['rebuild_protection'] : false + ); + break; + + case 'Rescue Mode': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/enable_rescue'; + $method = 'POST'; + $data = array( + 'type' => $params['rescue_type'] ?? 'linux64', + ); + break; + + case 'Disable Rescue Mode': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/disable_rescue'; + $method = 'POST'; + break; + + case 'Enable Backups': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/enable_backup'; + $method = 'POST'; + break; + + case 'Disable Backups': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/disable_backup'; + $method = 'POST'; + break; + + case 'List Volumes': + $url .= 'volumes?server=' . ArkHostHetznerVPS_GetVPSID($params); + $method = 'GET'; + break; + + case 'Create Volume': + $url .= 'volumes'; + $method = 'POST'; + $data = array( + 'size' => $params['size'] ?? 10, + 'name' => $params['name'] ?? 'volume-' . time(), + 'server' => ArkHostHetznerVPS_GetVPSID($params), + 'automount' => isset($params['automount']) ? $params['automount'] : true, + 'format' => $params['format'] ?? 'ext4' + ); + break; + + case 'Attach Volume': + $url .= 'volumes/' . $params['volume_id'] . '/actions/attach'; + $method = 'POST'; + $data = array( + 'server' => ArkHostHetznerVPS_GetVPSID($params), + 'automount' => isset($params['automount']) ? $params['automount'] : true + ); + break; + + case 'Detach Volume': + $url .= 'volumes/' . $params['volume_id'] . '/actions/detach'; + $method = 'POST'; + break; + + case 'Delete Volume': + $url .= 'volumes/' . $params['volume_id']; + $method = 'DELETE'; + break; + + case 'List Networks': + $url .= 'networks'; + $method = 'GET'; + break; + + case 'Attach to Network': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/attach_to_network'; + $method = 'POST'; + $data = array( + 'network' => $params['network_id'], + 'ip' => isset($params['ip']) ? $params['ip'] : null, + 'alias_ips' => isset($params['alias_ips']) ? $params['alias_ips'] : array() + ); + break; + + case 'Detach from Network': + $url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/detach_from_network'; + $method = 'POST'; + $data = array( + 'network' => $params['network_id'] + ); + break; + + default: + throw new Exception('Invalid action: ' . $params['action']); + break; + } + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($curl, CURLOPT_TIMEOUT, 15); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_301); + curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_setopt($curl, CURLOPT_USERAGENT, 'ArkHostHetznerVPS WHMCS'); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + 'Authorization: Bearer ' . $params['serverpassword'], + 'Content-Type: application/json' + )); + + if ($method === 'POST' || $method === 'PATCH' || $method === 'PUT') { + curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); + } + + $responseData = curl_exec($curl); + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + $responseData = json_decode($responseData, true); + + if ($statusCode === 0) throw new Exception('cURL Error: ' . curl_error($curl)); + + curl_close($curl); + + + logModuleCall( + 'ArkHostHetznerVPS', + $url, + !empty($data) ? json_encode($data) : '', + print_r($responseData, true) + ); + + // Hetzner API uses standard HTTP status codes + if ($statusCode >= 400) { + $errorMessage = 'Unknown error'; + $errorCode = ''; + + if (isset($responseData['error'])) { + $error = $responseData['error']; + $errorMessage = isset($error['message']) ? $error['message'] : $errorMessage; + $errorCode = isset($error['code']) ? ' (Code: ' . $error['code'] . ')' : ''; + + // Add details if available + if (isset($error['details'])) { + $details = is_array($error['details']) ? json_encode($error['details']) : $error['details']; + $errorMessage .= ' - Details: ' . $details; + } + } + + throw new Exception($errorMessage . $errorCode); + } + + return $responseData; +} + +function ArkHostHetznerVPS_Error($func, $params, Exception $err) { + logModuleCall('ArkHostHetznerVPS', $func, $params, $err->getMessage(), $err->getTraceAsString()); +} + +function ArkHostHetznerVPS_MetaData() { + return array( + 'DisplayName' => 'ArkHost - HetznerVPS', + 'APIVersion' => '1.1', + 'RequiresServer' => true, + ); +} + +function ArkHostHetznerVPS_ConfigOptions() { + $error = array( + 'error' => array( + 'FriendlyName' => 'Error', + 'Description' => 'Please double check if you selected a Server Group and/or your details are correct.', + 'Type' => '', + ), + ); + + $array = array( + 'planid' => array( + 'FriendlyName' => 'Server Type', + 'Description' => 'The Hetzner server type (Configurable option: planid).', + 'Type' => 'dropdown', + 'Options' => array(), + ), + 'osid' => array( + 'FriendlyName' => 'Operating System', + 'Description' => 'The Operating System image (Configurable option: osid).', + 'Type' => 'dropdown', + 'Options' => array(), + ), + 'datacenter' => array( + 'FriendlyName' => 'Datacenter', + 'Description' => 'The datacenter location for the server.', + 'Type' => 'dropdown', + 'Options' => array(), + ), + 'backups' => array( + 'FriendlyName' => 'Enable Backups', + 'Description' => 'Enable automatic backups (additional cost - billed by Hetzner, ~20% of server price).', + 'Type' => 'yesno', + ), + 'ipv6' => array( + 'FriendlyName' => 'Disable IPv6', + 'Description' => 'Disable IPv6 for this server.', + 'Type' => 'yesno', + ), + 'floating_ip_location' => array( + 'FriendlyName' => 'Floating IP Location', + 'Description' => 'Location for floating IPs (same as datacenter)', + 'Type' => 'dropdown', + 'Options' => array(), + ), + 'create_floating_ip' => array( + 'FriendlyName' => 'Create Floating IP', + 'Description' => 'Automatically create a floating IP when provisioning this server (additional cost - billed by Hetzner).', + 'Type' => 'yesno', + ), + ); + + try { + if (basename($_SERVER['SCRIPT_NAME'], '.php') === 'configproducts' && ($_REQUEST['action'] === 'module-settings' || $_POST['action'] === 'module-settings')) { + $id = 0; + $product = null; + $serverGroup = 0; + + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $id = (int) $_POST['id']; + + $product = Capsule::table('tblproducts')->where('id', $id)->first(); + $serverGroup = (int) $_POST['servergroup']; + } else { + $id = (int) $_REQUEST['id']; + + $product = Capsule::table('tblproducts')->where('id', $id)->first(); + $serverGroup = (int) $product->servergroup; + } + + // Only proceed if a server group is actually selected + if ($serverGroup == 0) { + return $array; // Return basic array structure when no server group selected + } + + $serverGroup = Capsule::table('tblservergroupsrel')->where('groupid', $serverGroup)->first(); + if (!$serverGroup) { + // Return basic array if server group not found, don't throw exception + return $array; + } + + $server = Capsule::table('tblservers')->where('id', $serverGroup->serverid)->first(); + if (!$server) { + // Return basic array if server not found, don't throw exception + return $array; + } + + $params = array( + 'serverusername' => $server->username, + 'serverpassword' => decrypt($server->password), + ); + + $params['action'] = 'Packages'; + $packageslist = ArkHostHetznerVPS_API($params); + + // Hetzner returns server types in 'server_types' array + if (isset($packageslist['server_types'])) { + foreach ($packageslist['server_types'] as $package) { + $price = isset($package['prices'][0]['price_monthly']['gross']) ? + number_format($package['prices'][0]['price_monthly']['gross'], 2) : '0.00'; + $array['planid']['Options'] += array( + $package['name'] => $package['description'] . ' (€' . $price . '/mo)' + ); + } + } + + if ($product->configoption1 == '') return $array; + + $params['action'] = 'Operating Systems'; + $params['plan_id'] = $product->configoption1; + $operatingSystems = ArkHostHetznerVPS_API($params); + + // Hetzner returns images in 'images' array + if (isset($operatingSystems['images'])) { + foreach ($operatingSystems['images'] as $operatingSystem) { + if ($operatingSystem['type'] === 'system' && $operatingSystem['status'] === 'available') { + $array['osid']['Options'] += array( + $operatingSystem['name'] => $operatingSystem['description'] ?: $operatingSystem['name'] + ); + } + } + } + + // Fetch datacenters + $params['action'] = 'Datacenters'; + $datacenters = ArkHostHetznerVPS_API($params); + + if (isset($datacenters['datacenters'])) { + foreach ($datacenters['datacenters'] as $datacenter) { + $array['datacenter']['Options'] += array( + $datacenter['name'] => $datacenter['description'] . ' (' . $datacenter['location']['city'] . ')' + ); + // Also populate floating IP locations + $array['floating_ip_location']['Options'] += array( + $datacenter['location']['name'] => $datacenter['location']['city'] . ', ' . $datacenter['location']['country'] + ); + } + } + } + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + + // Return the basic array structure with error message instead of error array + // This prevents the fields from disappearing + return $array; + } + + return $array; +} + +function ArkHostHetznerVPS_GetOption(array $params, $id, $default = NULL) { + $options = ArkHostHetznerVPS_ConfigOptions(); + + $friendlyName = $options[$id]['FriendlyName']; + + if (isset($params['configoptions'][$friendlyName]) && $params['configoptions'][$friendlyName] !== '') { + return $params['configoptions'][$friendlyName]; + } else if (isset($params['configoptions'][$id]) && $params['configoptions'][$id] !== '') { + return $params['configoptions'][$id]; + } else if (isset($params['customfields'][$friendlyName]) && $params['customfields'][$friendlyName] !== '') { + return $params['customfields'][$friendlyName]; + } else if (isset($params['customfields'][$id]) && $params['customfields'][$id] !== '') { + return $params['customfields'][$id]; + } + + $found = false; + $i = 0; + + foreach ($options as $key => $value) { + $i++; + if ($key === $id) { + $found = true; + break; + } + } + + if ($found && isset($params['configoption' . $i]) && $params['configoption' . $i] !== '') { + return $params['configoption' . $i]; + } + + return $default; +} + +function ArkHostHetznerVPS_TestConnection(array $params) { + $err = ''; + + try { + $params['action'] = 'Test'; + ArkHostHetznerVPS_API($params); + } catch (Exception $e) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $e); + $err = 'Received the error: ' . $e->getMessage() . ' Check module debug log for more detailed error.'; + } + + return [ + 'success' => $err === '', + 'error' => $err, + ]; +} + +function ArkHostHetznerVPS_CreateAccount(array $params) { + try { + $params['action'] = 'Order'; + $create = ArkHostHetznerVPS_API($params); + + // Validate Hetzner API response + if (!is_array($create) || !isset($create['server']['id'])) { + if (is_array($create)) { + } + throw new Exception('Invalid response from API'); + } + + // Store the server ID (using our format) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|VPS ID' => $create['server']['id'], + ]); + + // Store the root password if provided + if (isset($create['root_password'])) { + Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ + 'password' => encrypt($create['root_password']) + ]); + + // Save the timestamp when password was set for expiration tracking (72 hours) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Password Set Time' => time() + ]); + } + + // Handle floating IP creation if requested via Configurable Options or Module Settings + $floatingIpType = ArkHostHetznerVPS_GetConfigurableOption($params, 'floating'); + $createFloatingIP = ArkHostHetznerVPS_GetOption($params, 'create_floating_ip'); + + if (($floatingIpType && $floatingIpType !== '' && strtolower($floatingIpType) !== 'none') || + ($createFloatingIP && $createFloatingIP === 'on')) { + + // Determine floating IP type - default to ipv4 if module setting is used + $ipType = $floatingIpType && $floatingIpType !== '' ? $floatingIpType : 'ipv4'; + try { + $floatingIpParams = $params; + $floatingIpParams['action'] = 'Create Floating IP'; + $floatingIpParams['ip_type'] = $ipType; + $floatingIpParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location'); + $floatingIpParams['description'] = 'Server ID: ' . $create['server']['id']; + $floatingIpParams['assign_to_server'] = true; + + $floatingIp = ArkHostHetznerVPS_API($floatingIpParams); + + if (isset($floatingIp['floating_ip']['id'])) { + + // Store floating IP information + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Floating IP ID' => $floatingIp['floating_ip']['id'], + 'ArkHostHetznerVPS|Floating IP' => $floatingIp['floating_ip']['ip'], + ]); + } + } catch (Exception $floatingIpErr) { + // You might want to send an admin notification here + } + } + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_SuspendAccount(array $params) { + try { + $params['action'] = 'Disable'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_UnsuspendAccount(array $params) { + try { + $params['action'] = 'Enable'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_TerminateAccount(array $params) { + try { + // First, check if there's a floating IP to delete + $floatingIpId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + if ($floatingIpId) { + try { + $floatingIpParams = $params; + $floatingIpParams['action'] = 'Delete Floating IP'; + $floatingIpParams['floating_ip_id'] = $floatingIpId; + ArkHostHetznerVPS_API($floatingIpParams); + + } catch (Exception $floatingIpErr) { + } + } + + // Then terminate the server + $params['action'] = 'Cancel'; + $params['when'] = 'now'; + ArkHostHetznerVPS_API($params); + + Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(array( + 'username' => '', + 'password' => '', + )); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + + + +function ArkHostHetznerVPS_ChangePackage(array $params) { + try { + $params['action'] = 'Upgrade'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_Start(array $params) { + try { + $params['action'] = 'Start'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_Reboot(array $params) { + try { + $params['action'] = 'Reboot'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_Stop(array $params) { + try { + $params['action'] = 'Stop'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_VNC(array $params) { + try { + $params['action'] = 'VNC Console'; + $vnc = ArkHostHetznerVPS_API($params); + + // Hetzner returns wss_url and password + if (isset($vnc['wss_url']) && isset($vnc['password'])) { + $consoleUrl = $vnc['wss_url']; + $vncPassword = $vnc['password']; + + // Generate a standalone HTML file that can be opened separately + $html = ' + + + VNC Console - ' . htmlspecialchars($params['domain']) . ' + + + + + +
+ VNC Console - ' . htmlspecialchars($params['domain']) . ' +
+ Password: ' . htmlspecialchars($vncPassword) . ' +
+
+ +
Initializing VNC Client...
+
+ + + +'; + + // Set proper headers to prevent session issues + header('Content-Type: text/html; charset=utf-8'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Pragma: no-cache'); + header('Expires: 0'); + + // Output the HTML and exit immediately + echo $html; + die(); + + } else { + throw new Exception('Console URL not found in response'); + } + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + + header('Content-Type: text/html; charset=utf-8'); + echo ' + + + VNC Console Error + + + +
+

VNC Console Error

+
' . htmlspecialchars($err->getMessage()) . '
+

Unable to open VNC console. Please try again or contact support if the problem persists.

+ Close Window +
+ +'; + die(); + } +} + +function ArkHostHetznerVPS_AdminCustomButtonArray() { + return array( + 'Start' => 'Start', + 'Stop'=> 'Stop', + 'Reboot' => 'Reboot', + 'Shutdown' => 'Shutdown', + 'VNC Console'=> 'VNC', + 'Enable Rescue' => 'EnableRescue', + 'Reset Root Password' => 'ResetRoot', + 'Create Snapshot' => 'CreateSnapshot', + 'Enable Backups' => 'EnableBackups', + 'Disable Backups' => 'DisableBackups', + ); +} + +function ArkHostHetznerVPS_AdminLink(array $params) { + try { + // Check if we have a VPS ID first + $vpsId = ArkHostHetznerVPS_GetVPSID($params); + if (!$vpsId) { + // This might be called from server configuration page where there's no service + return ' Hetzner Cloud Server'; + } + + // Get server info to display + $params['action'] = 'Server Info'; + $serverInfo = ArkHostHetznerVPS_API($params); + + if (isset($serverInfo['server'])) { + $server = $serverInfo['server']; + return ' Status: ' . $server['status'] . '
IP: ' . $server['public_net']['ipv4']['ip']; + } + + return 'Server ID: ' . $vpsId; + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Unable to retrieve server info'; + } +} + +function ArkHostHetznerVPS_ClientAreaAPI(array $params) { + try { + $action = App::getFromRequest('api'); + + $actions = array('Server Info', 'Graphs', 'Reinstall', 'Reboot', 'Stop', 'Start', 'IPv4 Addresses', 'Hostname rDNS', 'Create backup', 'Delete backup', 'List backups', 'Restore backup', 'Get Firewall rules', 'Add Firewall rules', 'Delete Firewall rule', 'Commit Firewall rules', 'ISO Images', 'Load ISO', 'Eject ISO', 'Reset root', 'Create Snapshot', 'List Snapshots', 'Server Metrics', 'Rescue Mode', 'Disable Rescue Mode', 'GetFloatingIPStatus', 'AssignFloatingIP', 'UnassignFloatingIP', 'SetFloatingIPReverseDNS'); + $results = array('result' => 'success'); + + if (in_array($action, $actions)) { + foreach ($_POST as $key => $value) { + $params[$key] = $value; + } + + // Check backup permissions + $backupActions = array('Create backup', 'Delete backup', 'Restore backup'); + if (in_array($action, $backupActions)) { + // Check if backups are enabled either via module settings or configurable options + $backupsEnabled = false; + + // Check module product settings + if (isset($params['backups']) && $params['backups'] == 'on') { + $backupsEnabled = true; + } + + // Check configurable options + if (isset($params['configoptions']['Backups']) && $params['configoptions']['Backups'] == 'Yes') { + $backupsEnabled = true; + } + + if (!$backupsEnabled) { + return array( + 'result' => 'error', + 'message' => 'Backups are not enabled for this service. Please upgrade your plan to enable backups.' + ); + } + } + + // Check floating IP permissions + $floatingIPActions = array('GetFloatingIPStatus', 'AssignFloatingIP', 'UnassignFloatingIP', 'SetFloatingIPReverseDNS'); + if (in_array($action, $floatingIPActions)) { + // Check if customer has floating IP via configurable options, module settings, or existing service + $floatingIPOption = ArkHostHetznerVPS_GetConfigurableOption($params, 'Floating IP'); + $moduleFloatingIP = ArkHostHetznerVPS_GetOption($params, 'create_floating_ip'); + $hasFloatingIPService = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + + if (!$floatingIPOption && !($moduleFloatingIP && $moduleFloatingIP === 'on') && !$hasFloatingIPService) { + return array('jsonResponse' => array( + 'result' => 'error', + 'message' => 'Floating IP not available for this service. Please upgrade your plan to add floating IP.' + )); + } + + // Handle GetFloatingIPStatus specially (not a real Hetzner API call) + if ($action === 'GetFloatingIPStatus') { + try { + // Get current server ID safely + $currentServerId = null; + try { + $currentServerId = ArkHostHetznerVPS_GetVPSID($params); + } catch (Exception $vpsIdErr) { + // VPS ID retrieval failed - continue without it + } + + // First, check if this service has a specific floating IP stored + $serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + $customerFloatingIP = null; + + if ($serviceFloatingIPId) { + // Get the specific floating IP for this service + try { + $floatingIPResult = ArkHostHetznerVPS_API(array_merge($params, array( + 'action' => 'Get Floating IP', + 'floating_ip_id' => $serviceFloatingIPId + ))); + if (isset($floatingIPResult['floating_ip'])) { + $customerFloatingIP = $floatingIPResult['floating_ip']; + } + } catch (Exception $e) { + // Floating IP might have been deleted outside WHMCS + // Remove invalid ID from service properties + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Floating IP ID' => '', + 'ArkHostHetznerVPS|Floating IP' => '' + ]); + } + } + + // If no floating IP exists but customer has floating IP access, create one + if (!$customerFloatingIP && ($floatingIPOption || ($moduleFloatingIP && $moduleFloatingIP === 'on'))) { + try { + $createParams = $params; + $createParams['action'] = 'Create Floating IP'; + $createParams['ip_type'] = 'ipv4'; + $createParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location') ?: + ArkHostHetznerVPS_GetOption($params, 'datacenter'); + $createParams['description'] = 'WHMCS Service ID: ' . $params['serviceid']; + $createParams['assign_to_server'] = true; + + $createResult = ArkHostHetznerVPS_API($createParams); + + if (isset($createResult['floating_ip']['id'])) { + // Store floating IP information in service properties + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Floating IP ID' => $createResult['floating_ip']['id'], + 'ArkHostHetznerVPS|Floating IP' => $createResult['floating_ip']['ip'], + ]); + + $customerFloatingIP = $createResult['floating_ip']; + } + } catch (Exception $createErr) { + // Failed to create floating IP - log error but continue + logModuleCall( + 'ArkHostHetznerVPS', + 'CreateFloatingIPOnDemand', + $params, + 'Failed to create floating IP: ' . $createErr->getMessage(), + '', + array() + ); + } + } + + if ($customerFloatingIP) { + $floatingIPData = array( + 'hasFloatingIP' => true, + 'floatingIP' => $customerFloatingIP, + 'assigned' => $customerFloatingIP['server'] !== null, + 'server_id' => $customerFloatingIP['server'] ? $customerFloatingIP['server']['id'] : null, + 'current_server_id' => $currentServerId + ); + } else { + // Customer has floating IP access but no floating IP found - this is valid + $floatingIPData = array( + 'hasFloatingIP' => true, + 'floatingIP' => null, + 'assigned' => false, + 'server_id' => null, + 'current_server_id' => $currentServerId + ); + } + + return array('jsonResponse' => array( + 'result' => 'success', + 'data' => $floatingIPData + )); + } catch (Exception $statusErr) { + return array('jsonResponse' => array( + 'result' => 'error', + 'message' => 'Failed to load floating IP status: ' . $statusErr->getMessage() + )); + } + } + } + + $params['action'] = $action; + $result = ArkHostHetznerVPS_API($params); + + // Special handling for specific responses + if ($action === 'Graphs') { + // Handle Hetzner metrics response + if (isset($result['metrics'])) { + // Process time series data + $processedMetrics = array(); + + // Determine date format based on time period + $dateFormat = 'H:i'; // Default for hour/day + if (isset($params['time'])) { + switch ($params['time']) { + case 'hour': + $dateFormat = 'H:i'; + break; + case 'day': + $dateFormat = 'H:i'; + break; + case 'week': + $dateFormat = 'M d'; + break; + case 'month': + $dateFormat = 'M d'; + break; + case 'year': + $dateFormat = 'M Y'; + break; + } + } + + // Extract CPU usage + if (isset($result['metrics']['time_series']['cpu'])) { + $cpuData = $result['metrics']['time_series']['cpu']['values']; + $processedMetrics['cpu'] = array( + 'labels' => array(), + 'data' => array() + ); + foreach ($cpuData as $point) { + $processedMetrics['cpu']['labels'][] = date($dateFormat, $point[0]); + $processedMetrics['cpu']['data'][] = round($point[1], 2); + } + } + + // Extract disk I/O + if (isset($result['metrics']['time_series']['disk.0.iops.read'])) { + $diskReadData = $result['metrics']['time_series']['disk.0.iops.read']['values']; + $diskWriteData = $result['metrics']['time_series']['disk.0.iops.write']['values']; + $processedMetrics['disk'] = array( + 'labels' => array(), + 'read' => array(), + 'write' => array() + ); + foreach ($diskReadData as $i => $point) { + $processedMetrics['disk']['labels'][] = date($dateFormat, $point[0]); + $processedMetrics['disk']['read'][] = round($point[1], 2); + $processedMetrics['disk']['write'][] = isset($diskWriteData[$i]) ? round($diskWriteData[$i][1], 2) : 0; + } + } + + // Extract network traffic + if (isset($result['metrics']['time_series']['network.0.bandwidth.in'])) { + $netInData = $result['metrics']['time_series']['network.0.bandwidth.in']['values']; + $netOutData = $result['metrics']['time_series']['network.0.bandwidth.out']['values']; + $processedMetrics['network'] = array( + 'labels' => array(), + 'in' => array(), + 'out' => array() + ); + foreach ($netInData as $i => $point) { + $processedMetrics['network']['labels'][] = date($dateFormat, $point[0]); + // Convert bytes/s to Mbps + $processedMetrics['network']['in'][] = round(($point[1] * 8) / 1000000, 2); + $processedMetrics['network']['out'][] = isset($netOutData[$i]) ? round(($netOutData[$i][1] * 8) / 1000000, 2) : 0; + } + } + + $results['graphs'] = array( + 'type' => 'metrics', + 'data' => $processedMetrics + ); + } else { + // No metrics available + $results['graphs'] = array( + 'type' => 'none', + 'message' => 'Metrics not available for this server' + ); + } + } else if ($action === 'List backups' || $action === 'List Snapshots') { + // Handle image list response for Hetzner + if (isset($result['images'])) { + // Log the number of images found + logModuleCall( + 'ArkHostHetznerVPS', + 'ListBackups', + array('count' => count($result['images'])), + 'Found ' . count($result['images']) . ' backup images', + '', + array() + ); + + // Return backups with numeric keys for JavaScript compatibility + $backupIndex = 0; + foreach ($result['images'] as $image) { + $results[$backupIndex] = array( + 'id' => $image['id'], + 'name' => $image['description'] ?? $image['name'], + 'created' => $image['created'], + 'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API + 'type' => $image['type'] + ); + $backupIndex++; + } + } else { + // No backups found + $results['message'] = 'No backups found'; + logModuleCall( + 'ArkHostHetznerVPS', + 'ListBackups', + array('result' => $result), + 'No images key in API response', + '', + array() + ); + } + } else if ($action === 'Server Info') { + // Handle server info for Hetzner + if (isset($result['server'])) { + $results = array_merge($results, $result['server']); + } + } else if ($action === 'ISO Images') { + // Handle ISO list for Hetzner + if (isset($result['isos'])) { + $results['isos'] = $result['isos']; + } + } else if ($action === 'Get Firewall rules') { + // For Hetzner, we need to check if server has firewalls attached + // and then fetch the firewall details separately + $rules = array(); + + // Check for firewall IDs in the correct location + $firewallIds = array(); + if (isset($result['server']['public_net']['firewalls']) && !empty($result['server']['public_net']['firewalls'])) { + // Extract firewall IDs from the firewalls array + foreach ($result['server']['public_net']['firewalls'] as $firewall) { + if (isset($firewall['id'])) { + $firewallIds[] = $firewall['id']; + } + } + } + + if (!empty($firewallIds)) { + // Server has firewalls attached, fetch the actual firewall rules + foreach ($firewallIds as $firewallId) { + // Fetch firewall details + $firewallParams = $params; + $firewallParams['action'] = 'Get Firewall Details'; + $firewallParams['firewall_id'] = $firewallId; + + try { + $firewallResult = ArkHostHetznerVPS_API($firewallParams); + + if (isset($firewallResult['firewall']['rules'])) { + $inIndex = 0; + $outIndex = 0; + foreach ($firewallResult['firewall']['rules'] as $rule) { + // Process both inbound and outbound rules + if ($rule['direction'] === 'in' || $rule['direction'] === 'out') { + // Handle IPs based on direction + $ips = '0.0.0.0/0'; // Default when no IPs specified + if ($rule['direction'] === 'out') { + // For outbound rules, use destination_ips + if (isset($rule['destination_ips']) && is_array($rule['destination_ips'])) { + if (!empty($rule['destination_ips'])) { + $ips = implode(', ', $rule['destination_ips']); + } + } + } else { + // For inbound rules, use source_ips + if (isset($rule['source_ips']) && is_array($rule['source_ips'])) { + if (!empty($rule['source_ips'])) { + $ips = implode(', ', $rule['source_ips']); + } + } + } + + // Handle port range or single port + $port = ''; + if (isset($rule['port'])) { + $port = $rule['port']; + } elseif (isset($rule['port_range'])) { + $port = $rule['port_range']; + } else { + $port = 'Any'; + } + + // Generate ID based on direction and index + $ruleId = 'fw_' . $firewallId . '_' . $rule['direction'] . '_'; + if ($rule['direction'] === 'in') { + $ruleId .= $inIndex; + $inIndex++; + } else { + $ruleId .= $outIndex; + $outIndex++; + } + + $rules[] = array( + 'id' => $ruleId, + 'direction' => $rule['direction'], + 'action' => 'ACCEPT', + 'protocol' => strtoupper($rule['protocol']), + 'port' => $port, + 'source' => $ips + ); + } + } + } + } catch (Exception $e) { + // If we can't fetch firewall details, show a message + $rules[] = array( + 'id' => 'error_' . $firewallId, + 'action' => 'INFO', + 'protocol' => 'N/A', + 'port' => 'N/A', + 'source' => 'N/A', + 'note' => 'Unable to fetch firewall ' . $firewallId . ' details' + ); + } + } + + if (empty($rules)) { + $results['message'] = 'This server has ' . count($firewallIds) . ' firewall(s) attached but no inbound rules configured.'; + } + } else { + // No firewalls attached - don't show any rules + $results['message'] = 'No firewall attached to this server. All traffic is allowed by default. Create a firewall rule to enable protection.'; + // Return empty rules array + $rules = array(); + } + + // Return rules with numeric keys for JavaScript compatibility + foreach ($rules as $index => $rule) { + $results[$index] = $rule; + } + } else if ($action === 'Reinstall') { + // Handle rebuild response - save new root password if provided + if (isset($result['root_password']) && !empty($result['root_password'])) { + // Save the new root password + Capsule::table('tblhosting') + ->where('id', $params['serviceid']) + ->update(['password' => encrypt($result['root_password'])]); + + // Save the timestamp when password was set for expiration tracking (72 hours) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Password Set Time' => time() + ]); + + $results['root_password'] = $result['root_password']; + $results['message'] = 'Server rebuild initiated. New root password has been saved.'; + } else { + $results['message'] = 'Server rebuild initiated. No new password generated (SSH keys used).'; + } + $results = array_merge($results, is_array($result) ? $result : array('data' => $result)); + } else if ($action === 'AssignFloatingIP') { + // Handle floating IP assignment - only allow assignment of service's own floating IP + $serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) { + $assignResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Assign Floating IP'))); + $results = array_merge($results, is_array($assignResult) ? $assignResult : array('data' => $assignResult)); + } else { + $results['result'] = 'error'; + $results['message'] = 'Invalid floating IP ID for this service'; + } + } else if ($action === 'UnassignFloatingIP') { + // Handle floating IP unassignment - only allow unassignment of service's own floating IP + $serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) { + $unassignResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Unassign Floating IP'))); + $results = array_merge($results, is_array($unassignResult) ? $unassignResult : array('data' => $unassignResult)); + } else { + $results['result'] = 'error'; + $results['message'] = 'Invalid floating IP ID for this service'; + } + } else if ($action === 'SetFloatingIPReverseDNS') { + // Handle floating IP reverse DNS update - only allow for service's own floating IP + $serviceFloatingIPId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Floating IP ID'); + if ($serviceFloatingIPId && $serviceFloatingIPId === $params['floating_ip_id']) { + $dnsResult = ArkHostHetznerVPS_API(array_merge($params, array('action' => 'Change Floating IP DNS'))); + $results = array_merge($results, is_array($dnsResult) ? $dnsResult : array('data' => $dnsResult)); + } else { + $results['result'] = 'error'; + $results['message'] = 'Invalid floating IP ID for this service'; + } + } else if ($action === 'Reset root') { + // Handle password reset from client area + $results = array_merge($results, is_array($result) ? $result : array('data' => $result)); + + // Store the new root password and timestamp if provided + if (isset($result['root_password'])) { + Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ + 'password' => encrypt($result['root_password']) + ]); + + // Save the timestamp when password was set for expiration tracking (72 hours) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Password Set Time' => time() + ]); + } + } else { + $results = array_merge($results, is_array($result) ? $result : array('data' => $result)); + } + + return array('jsonResponse' => $results); + } else { + throw new Exception('Action not allowed'); + } + } catch (Exception $e) { + return array('jsonResponse' => array('result' => 'error', 'message' => $e->getMessage())); + } +} + +function ArkHostHetznerVPS_DeliverFile(array $params) { + try { + $dir = __DIR__ . '/template/'; + $file = App::getFromRequest('file'); + $files = array('app.min.css', 'app.min.js'); + + if (in_array($file, $files)) { + $type = ''; + + if (function_exists('ob_gzhandler')) { + ob_start('ob_gzhandler'); + } + + if (strpos($file, '.js') !== false) { + $dir .= 'js/'; + $type = 'application/javascript'; + } else if (strpos($file, '.css') !== false) { + $dir .= 'css/'; + $type = 'text/css'; + } else { + $type = 'text/html'; + } + + header('Content-Type: ' . $type . '; charset=utf-8'); + header('Cache-Control: max-age=604800, public'); + + echo file_get_contents($dir . $file); + WHMCS\Terminus::getInstance()->doExit(); + } else { + throw new Exception('File not found'); + } + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return array('jsonResponse' => array('result' => 'error', 'message' => $err->getMessage())); + } +} + + + +function ArkHostHetznerVPS_ClientAreaCustomButtonArray() { + $_LANG = ArkHostHetznerVPS_Translation(); + + return array( + $_LANG['Start'] => 'Start', + $_LANG['Stop'] => 'Stop', + $_LANG['Restart'] => 'Reboot', + $_LANG['VNC'] => 'VNC', + ); +} + +function ArkHostHetznerVPS_ClientAreaAllowedFunctions() { + return array('ClientAreaAPI', 'DeliverFile'); +} + +function ArkHostHetznerVPS_ClientArea(array $params) { + if ($params['moduletype'] !== 'ArkHostHetznerVPS') return; + + try { + // Get clean VPS ID - this handles migration from other modules + $cleanVpsId = ArkHostHetznerVPS_GetVPSID($params); + if (empty($cleanVpsId)) { + throw new Exception('VPS ID not found. Please check service configuration.'); + } + + // If we found a VPS ID but it's not stored in our format, store it now for future use + $storedVpsId = $params['model']->serviceProperties->get('ArkHostHetznerVPS|VPS ID'); + if (empty($storedVpsId) && !empty($cleanVpsId)) { + // Store the VPS ID for future use (using our format) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|VPS ID' => $cleanVpsId, + ]); + } + + + // Check if password is empty (migration from other modules) and set a dummy password + if (empty($params['password'])) { + // Set a dummy password to prevent WHMCS from showing password reset form + // This is just for display - actual server access is via API + Capsule::table('tblhosting') + ->where('id', $params['serviceid']) + ->update(['password' => encrypt('managed-via-api')]); + + // Reload params to get the updated password + $params['password'] = 'managed-via-api'; + } + + // Get server info and operating systems data + $params['action'] = 'Server Info'; + $response = ArkHostHetznerVPS_API($params); + + // Check if server info is valid + if (!is_array($response) || !isset($response['server'])) { + throw new Exception('Unable to retrieve server information from API'); + } + + // Extract server data from Hetzner response + $serverInfo = $response['server']; + + $params['action'] = 'Operating Systems - Server'; + $operatingSystemsTemp = ArkHostHetznerVPS_API($params); + + // Check if operating systems data is valid + if (!is_array($operatingSystemsTemp)) { + $operatingSystemsTemp = array(); + } + + $dirImages = __DIR__ . '/template/img/'; + $availableImages = glob($dirImages . '*.png'); + $images = array(); + + foreach ($availableImages as $key => $image) { + $images[explode('.png', explode($dirImages, $image)[1])[0]] = 'data:image/png;base64,' . base64_encode(file_get_contents($image)); + } + + $dirOS = __DIR__ . '/template/img/os/'; + $availableOS = glob($dirOS . '*.png'); + $operatingSystems = array(); + + foreach ($availableOS as $key => $os) { + $availableOS[$key] = explode('.png', explode($dirOS, $os)[1])[0]; + } + + // Process operating systems data from Hetzner API + if (!empty($operatingSystemsTemp) && isset($operatingSystemsTemp['images'])) { + // Log the raw OS data for debugging + logModuleCall( + 'ArkHostHetznerVPS', + 'ProcessOperatingSystems', + array('image_count' => count($operatingSystemsTemp['images'])), + 'Processing ' . count($operatingSystemsTemp['images']) . ' OS images', + '', + array() + ); + + foreach ($operatingSystemsTemp['images'] as $operatingSystem) { + // Skip non-system images + if ($operatingSystem['type'] !== 'system' || $operatingSystem['status'] !== 'available') { + continue; + } + + // Use description for display, fall back to name + $displayName = $operatingSystem['description'] ?: $operatingSystem['name']; + $osName = strtolower($displayName); + + // Determine the proper group based on OS name + if (strpos($osName, 'alma') !== false) { + $group = 'almalinux'; + $groupName = 'AlmaLinux'; + } elseif (strpos($osName, 'rocky') !== false) { + $group = 'rocky'; + $groupName = 'Rocky Linux'; + } elseif (strpos($osName, 'centos') !== false) { + $group = 'centos'; + $groupName = 'CentOS'; + } elseif (strpos($osName, 'debian') !== false) { + $group = 'debian'; + $groupName = 'Debian'; + } elseif (strpos($osName, 'ubuntu') !== false) { + $group = 'ubuntu'; + $groupName = 'Ubuntu'; + } elseif (strpos($osName, 'fedora') !== false) { + $group = 'fedora'; + $groupName = 'Fedora'; + } elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) { + $group = 'opensuse'; + $groupName = 'openSUSE'; + } elseif (strpos($osName, 'windows') !== false) { + $group = 'windows'; + $groupName = 'Windows'; + } else { + // For any other OS, use 'others' + $group = 'others'; + $groupName = 'Other Systems'; + } + + if (!isset($operatingSystems[$group])) { + // Map group names to correct image filenames + $imageFile = $group; + if ($group === 'rocky') { + $imageFile = 'rockylinux'; + } + + $image = file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png'); + + $operatingSystems[$group] = array( + 'name' => $groupName, + 'image' => 'data:image/png;base64,' . base64_encode($image), + 'versions' => array(), + ); + } + + // Check if this version already exists (avoid duplicates) + // Check both by ID and display name to avoid visual duplicates + $versionExists = false; + if (!empty($operatingSystems[$group]['versions'])) { + foreach ($operatingSystems[$group]['versions'] as $existingVersion) { + // Check if either the ID or display name already exists + if ($existingVersion['id'] === $operatingSystem['name'] || + $existingVersion['name'] === $displayName) { + $versionExists = true; + // Log duplicate found + logModuleCall( + 'ArkHostHetznerVPS', + 'DuplicateOSFound', + array( + 'group' => $group, + 'existing_id' => $existingVersion['id'], + 'existing_name' => $existingVersion['name'], + 'new_id' => $operatingSystem['name'], + 'new_name' => $displayName + ), + 'Duplicate OS version found', + '', + array() + ); + break; + } + } + } + + // Only add if not already present + if (!$versionExists) { + // Store both the API name (for rebuild) and display name + $operatingSystems[$group]['versions'][] = array( + 'id' => $operatingSystem['name'], // This is what we send to API + 'name' => $displayName // This is what we display + ); + } + } + + // Sort OS versions within each group for consistent display + foreach ($operatingSystems as $group => &$osData) { + if (!empty($osData['versions'])) { + usort($osData['versions'], function($a, $b) { + return strcmp($a['name'], $b['name']); + }); + } + } + + // Process operating system info for Hetzner + if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) { + $osId = $serverInfo['image']['name']; + $osName = strtolower($serverInfo['image']['description'] ?? $osId); + + // Find matching OS in available images + $currentOS = null; + foreach ($operatingSystemsTemp['images'] as $os) { + if ($os['name'] === $osId) { + $currentOS = $os; + break; + } + } + + if ($currentOS) { + $displayName = $currentOS['description'] ?? $currentOS['name']; + $osName = strtolower($displayName); + + // Determine the proper group based on current OS name (same logic as above) + if (strpos($osName, 'alma') !== false) { + $group = 'almalinux'; + } elseif (strpos($osName, 'rocky') !== false) { + $group = 'rocky'; + } elseif (strpos($osName, 'centos') !== false) { + $group = 'centos'; + } elseif (strpos($osName, 'debian') !== false) { + $group = 'debian'; + } elseif (strpos($osName, 'ubuntu') !== false) { + $group = 'ubuntu'; + } elseif (strpos($osName, 'fedora') !== false) { + $group = 'fedora'; + } elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) { + $group = 'opensuse'; + } elseif (strpos($osName, 'windows') !== false) { + $group = 'windows'; + } else { + $group = 'others'; + } + + // Map group names to correct image filenames + $imageFile = $group; + if ($group === 'rocky') { + $imageFile = 'rockylinux'; + } + + // Use the specific OS information + $serverInfo['operatingSystem'] = array( + 'name' => $displayName, + 'image' => isset($operatingSystems[$group]) ? $operatingSystems[$group]['image'] : 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png')) + ); + } else { + // Fallback if OS not found - use image info from server + $displayName = $serverInfo['image']['description'] ?? $osId; + $osName = strtolower($displayName); + + // Try to determine OS type from name + if (strpos($osName, 'alma') !== false) { + $imageFile = 'almalinux'; + } elseif (strpos($osName, 'rocky') !== false) { + $imageFile = 'rockylinux'; + } elseif (strpos($osName, 'centos') !== false) { + $imageFile = 'centos'; + } elseif (strpos($osName, 'debian') !== false) { + $imageFile = 'debian'; + } elseif (strpos($osName, 'ubuntu') !== false) { + $imageFile = 'ubuntu'; + } elseif (strpos($osName, 'fedora') !== false) { + $imageFile = 'fedora'; + } elseif (strpos($osName, 'opensuse') !== false || strpos($osName, 'suse') !== false) { + $imageFile = 'opensuse'; + } elseif (strpos($osName, 'windows') !== false) { + $imageFile = 'windows'; + } else { + $imageFile = 'others'; + } + + $serverInfo['operatingSystem'] = array( + 'name' => $displayName, + 'image' => 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . (in_array($imageFile, $availableOS) ? $imageFile : 'others') . '.png')) + ); + } + } + } + + // Set default OS info if not available + if (!isset($serverInfo['operatingSystem']) || !is_array($serverInfo['operatingSystem'])) { + $serverInfo['operatingSystem'] = array( + 'name' => 'Unknown OS', + 'image' => 'data:image/png;base64,' . base64_encode(file_get_contents($dirOS . 'others.png')) + ); + } + + // Map Hetzner status to template expectations + $serverInfo['statusImage'] = isset($images[$serverInfo['status']]) ? $images[$serverInfo['status']] : (isset($images['unknown']) ? $images['unknown'] : ''); + $serverInfo['statusDescription'] = ucfirst($serverInfo['status']); + + // Map Hetzner server data to expected format + $serverInfo['hostname'] = $serverInfo['name'] ?? 'N/A'; + $serverInfo['ip'] = isset($serverInfo['public_net']['ipv4']['ip']) ? $serverInfo['public_net']['ipv4']['ip'] : 'N/A'; + $serverInfo['ipv6'] = isset($serverInfo['public_net']['ipv6']['ip']) ? $serverInfo['public_net']['ipv6']['ip'] : 'N/A'; + + // Calculate uptime from created date + if (isset($serverInfo['created'])) { + $created = new DateTime($serverInfo['created']); + $now = new DateTime(); + $diff = $now->diff($created); + $serverInfo['uptime_text'] = $diff->format('%a days, %h hours'); + } else { + $serverInfo['uptime_text'] = 'N/A'; + } + + // Get server type details + $serverType = $serverInfo['server_type'] ?? array(); + $serverInfo['cpu'] = $serverType['cores'] ?? 0; + $serverInfo['ram'] = $serverType['memory'] ?? 0; + $serverInfo['disk'] = $serverType['disk'] ?? 0; + + // Try to fetch current metrics for the overview + try { + $metricsParams = $params; + $metricsParams['action'] = 'Graphs'; + $metricsParams['time'] = 'hour'; // Get last hour for current usage + + $metrics = ArkHostHetznerVPS_API($metricsParams); + + // Extract latest CPU usage + if (isset($metrics['metrics']['time_series']['cpu']['values'])) { + $cpuValues = $metrics['metrics']['time_series']['cpu']['values']; + if (!empty($cpuValues)) { + $latestCpu = end($cpuValues); + $serverInfo['cpu_usage'] = round($latestCpu[1], 1); + } + } + + // For bandwidth, we should use the outgoing_traffic from server info instead of calculating from metrics + // The metrics only show current bandwidth rate, not total usage + } catch (Exception $e) { + // If metrics fail, keep defaults + } + + // Hetzner doesn't provide RAM usage or disk usage via API + $serverInfo['cpu_usage'] = $serverInfo['cpu_usage'] ?? 0; + $serverInfo['ram_usage'] = 0; + $serverInfo['disk_used'] = 0; + + // Traffic information - Hetzner includes 20TB with all cloud servers + // Traffic usage is not available via the server API + $serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024) + $serverInfo['bandwidth_used'] = 0; // Not available via API + + // Add datacenter info + $serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A'; + $serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A'; + + // Get root password with expiration check (72 hours) + $passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time'); + $currentTime = time(); + $expirationPeriod = 72 * 3600; // 72 hours in seconds + + if (!empty($params['password']) && $params['password'] !== 'managed-via-api') { + // If no timestamp exists, show the password (backward compatibility or initial setup) + // If timestamp exists, check if it's within the 72-hour window + if (!$passwordSetTime || ($currentTime - $passwordSetTime) < $expirationPeriod) { + // Password is still valid - WHMCS already decrypts it for us in $params + // No need to call decrypt() as $params['password'] is already plain text + $serverInfo['install_root'] = $params['password']; + } else { + // Password has expired (timestamp exists and is older than 72 hours) + $serverInfo['install_root'] = ''; + } + } else { + $serverInfo['install_root'] = ''; + } + + // For now, always show backups tab - the API will handle permissions + $backupsEnabled = true; + + return array( + 'templatefile' => 'template/clientarea_direct', + 'templateVariables' => array( + 'images' => $images, + 'serverInfo' => $serverInfo, + 'operatingSystems' => $operatingSystems, + 'token' => generate_token('plain'), + 'ADDONLANG' => ArkHostHetznerVPS_Translation(), + 'backupsEnabled' => $backupsEnabled, + ) + ); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + + return array( + 'templatefile' => 'template/error', + 'templateVariables' => array( + 'error' => $err->getMessage(), + 'image' => 'data:image/png;base64,' . base64_encode(file_get_contents(__DIR__ . '/template/img/notice.png')) + ) + ); + } +} + +function ArkHostHetznerVPS_Translation() { + $lang = Setting::getValue('Language'); + $language = Lang::getName(); + $_ADDONLANG = []; + + if ($language === '') { + $language = $lang; + } + + if ($language) { + $addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/' . $language . '.php'; + + if (file_exists($addonLangFile)) { + swapLang($language); + + ob_start(); + require $addonLangFile; + ob_end_clean(); + } + } + + if (count($_ADDONLANG) === 0) { + $addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/' . $lang . '.php'; + + if (file_exists($addonLangFile)) { + swapLang($lang); + + ob_start(); + require $addonLangFile; + ob_end_clean(); + } + } + + if (count($_ADDONLANG) === 0) { + $addonLangFile = ROOTDIR . '/modules/servers/ArkHostHetznerVPS/lang/english.php'; + + if (file_exists($addonLangFile)) { + ob_start(); + require $addonLangFile; + ob_end_clean(); + } + } + + return $_ADDONLANG; +} + +function ArkHostHetznerVPS_EnableRescue(array $params) { + try { + $params['action'] = 'Rescue Mode'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_ResetRoot(array $params) { + try { + $params['action'] = 'Reset root'; + $result = ArkHostHetznerVPS_API($params); + + // Store the new root password if provided + if (isset($result['root_password'])) { + Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([ + 'password' => encrypt($result['root_password']) + ]); + + // Save the timestamp when password was set for expiration tracking (72 hours) + $params['model']->serviceProperties->save([ + 'ArkHostHetznerVPS|Password Set Time' => time() + ]); + } + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_CreateSnapshot(array $params) { + try { + $params['action'] = 'Create Snapshot'; + $params['description'] = 'Manual snapshot from WHMCS'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_Shutdown(array $params) { + try { + $params['action'] = 'Shutdown'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_EnableBackups(array $params) { + try { + $params['action'] = 'Enable Backups'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} + +function ArkHostHetznerVPS_DisableBackups(array $params) { + try { + $params['action'] = 'Disable Backups'; + ArkHostHetznerVPS_API($params); + } catch (Exception $err) { + ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err); + return 'Received the error: ' . $err->getMessage() . ' Check module debug log for more detailed error.'; + } + + return 'success'; +} diff --git a/modules/servers/ArkHostHetznerVPS/lang/dutch.php b/modules/servers/ArkHostHetznerVPS/lang/dutch.php new file mode 100644 index 0000000..d0bb1e7 --- /dev/null +++ b/modules/servers/ArkHostHetznerVPS/lang/dutch.php @@ -0,0 +1,247 @@ + + *} + + + + + + + +
+ + +
+
+ +
+
+ +
+
+
{$ADDONLANG.Overview.ServerInfo}
+
+
+
+
+ + + + + + + + + + {if $serverInfo['ipv6']} + + + + + {/if} + + + + +
{$ADDONLANG.Overview.Hostname}:{$serverInfo['hostname']}
{$ADDONLANG.IPv4} Address:{$serverInfo['ip']}
{$ADDONLANG.IPv6} Address:{$serverInfo['ipv6']}
{$ADDONLANG.Overview.OS}: + + {$serverInfo['operatingSystem']['name']} +
+
+
+ + + + + + + + + + + + + + + + + +
{$ADDONLANG.Overview.Status}: + + + {$serverInfo['statusDescription']} + +
{$ADDONLANG.Overview.Uptime}:{$serverInfo['uptime_text']}
{$ADDONLANG.Overview.Location}:{$serverInfo['datacenter']}
{$ADDONLANG.Overview.ServerType}:{$serverInfo['server_type']['description']|default:$ADDONLANG.ServerTypes.Standard}
+
+
+
+
+ + +
+
+
{$ADDONLANG.Overview.ResourceAllocation}
+
+
+
+
+
+ +

{$serverInfo['cpu']}

+ {$ADDONLANG.Overview.CPU_Cores} +
+
+
+
+ +

{$serverInfo['ram']} {$ADDONLANG.General.GB}

+ {$ADDONLANG.Overview.Memory} +
+
+
+
+ +

{$serverInfo['disk']} {$ADDONLANG.General.GB}

+ {$ADDONLANG.Overview.Storage} +
+
+
+
+ +

20 {$ADDONLANG.General.TB}

+ {$ADDONLANG.Overview.Traffic} +
+
+
+
+
+ + + +
+
+
{$ADDONLANG.Overview.QuickActions}
+
+
+
+ + + {if $serverInfo['status'] == 'running'} + + {/if} + + {$ADDONLANG.VNC} + +
+
+
+
+ +
+ +
+
+
{$ADDONLANG.Graphs.Title}
+ +
+
+

{$ADDONLANG.Graphs.SelectPeriod}

+
+ + + + + +
+
+
+ + + + + +
+
+
+
+
{$ADDONLANG.Graphs.CPU}
+
+
+
+

{$ADDONLANG.Graphs.Loading}

+
+
+
+
+ +
+
+
+
{$ADDONLANG.Graphs.Network}
+
+
+
+

{$ADDONLANG.Graphs.Loading}

+
+
+
+
+ +
+
+
+
{$ADDONLANG.Graphs.Disk}
+
+
+
+

{$ADDONLANG.Graphs.Loading}

+
+
+
+
+
+ +
+ + {if $backupsEnabled} +
+ +
+
+
{$ADDONLANG.Backups.Title}
+
+
+

{$ADDONLANG.Backups.Description}

+ +
+ +
+ +
+ + + + + + + + + + + + + + + +
{$ADDONLANG.Backups.Date} {$ADDONLANG.Backups.Size} {$ADDONLANG.Backups.Type} {$ADDONLANG.Backups.Status} {$ADDONLANG.Backups.Actions}
+ {$ADDONLANG.LoadingBackups} +
+
+
+
+ + +
+
Important Information
+ {$ADDONLANG.Backups.Warning|unescape:'html'} +
+
+ {/if} + +
+
+ + +
+
+
+
+
+
{$ADDONLANG.Settings.Hostname.Title}
+
+
+
+ {$ADDONLANG.Settings.Hostname.Description} +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+
+
+
{$ADDONLANG.Settings.ISO.Title}
+
+
+
+ {$ADDONLANG.Settings.ISO.Description} +
+ +
+ + +
+ +
+ + + {if $serverInfo['iso'] !== ''} + + {/if} +
+
+
+
+ +
+
+
+
{$ADDONLANG.Settings.Password.Title}
+
+
+
+ {$ADDONLANG.Settings.Password.Description} +
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
{$ADDONLANG.Settings.Reinstall.Title}
+
+
+
+ {$ADDONLANG.Settings.Reinstall.Description} +
+ +
{$ADDONLANG.Settings.Reinstall.OS}:
+ +
+ {foreach from=$operatingSystems key=$group item=$operatingSystemsGroup} +
+ +
+ {/foreach} +
+ +
+ {$ADDONLANG.General.Note}: {$ADDONLANG.Reinstall.DestroyWarning} +
+ + + +
+ +
+
+
+
+ +
+
+
+
{$ADDONLANG.Settings.Rescue.Title}
+
+
+
+
{$ADDONLANG.Settings.Rescue.AboutTitle}
+

{$ADDONLANG.Settings.Rescue.AboutDescription}

+
+

{$ADDONLANG.Settings.Rescue.Important}: {$ADDONLANG.Settings.Rescue.ImportantNote}

+
+ +
+
+
+
+ +
{$ADDONLANG.Settings.Rescue.EnableTitle}
+

{$ADDONLANG.Settings.Rescue.EnableDescription}

+ +
+
+
+
+
+
+ +
{$ADDONLANG.Settings.Rescue.EnableRebootTitle}
+

{$ADDONLANG.Settings.Rescue.EnableRebootDescription}

+ +
+
+
+
+ +
+
+
{$ADDONLANG.Settings.Rescue.DisableTitle}
+

{$ADDONLANG.Settings.Rescue.DisableDescription}

+ +
+
+
+
+
+ +
+
+
+
{$ADDONLANG.Settings.FloatingIP.Title}
+
+
+
+ {$ADDONLANG.Settings.FloatingIP.Description} +
+ +
+
+ +

Loading floating IP information...

+
+
+
+
+
+
+
+
+
+ +
+
+ {$ADDONLANG.General.Note}: {$ADDONLANG.Firewall.ResourcesAttached} +
+ + +
+
+
{$ADDONLANG.Firewall.AddNewRule}
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
{$ADDONLANG.Firewall.CurrentRules}
+
+
+
+ + + + + + + + + + + + + + + + +
{$ADDONLANG.Firewall.Direction}{$ADDONLANG.Firewall.Action}{$ADDONLANG.Firewall.Protocol}{$ADDONLANG.Firewall.Port}{$ADDONLANG.Firewall.IPCIDR}{$ADDONLANG.Firewall.Remove}
+ {$ADDONLANG.LoadingFirewallRules} +
+
+
+
+ + +
+ {$ADDONLANG.General.Note}: {$ADDONLANG.Firewall.ChangesImmediate} +
+
+
+
+
+
+ + diff --git a/modules/servers/ArkHostHetznerVPS/template/img/cloud.png b/modules/servers/ArkHostHetznerVPS/template/img/cloud.png new file mode 100644 index 0000000..fd805bb Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/cloud.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/edit.png b/modules/servers/ArkHostHetznerVPS/template/img/edit.png new file mode 100644 index 0000000..5a07524 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/edit.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/eye.png b/modules/servers/ArkHostHetznerVPS/template/img/eye.png new file mode 100644 index 0000000..87409f3 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/eye.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/installing.png b/modules/servers/ArkHostHetznerVPS/template/img/installing.png new file mode 100644 index 0000000..14adb20 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/installing.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/loading.png b/modules/servers/ArkHostHetznerVPS/template/img/loading.png new file mode 100644 index 0000000..b370a71 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/loading.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/lock.png b/modules/servers/ArkHostHetznerVPS/template/img/lock.png new file mode 100644 index 0000000..06025ed Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/lock.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/notice.png b/modules/servers/ArkHostHetznerVPS/template/img/notice.png new file mode 100644 index 0000000..4925a7b Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/notice.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/almalinux.png b/modules/servers/ArkHostHetznerVPS/template/img/os/almalinux.png new file mode 100644 index 0000000..34891bc Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/almalinux.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/centos.png b/modules/servers/ArkHostHetznerVPS/template/img/os/centos.png new file mode 100644 index 0000000..8cd1fa9 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/centos.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/debian.png b/modules/servers/ArkHostHetznerVPS/template/img/os/debian.png new file mode 100644 index 0000000..f52dcdb Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/debian.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/fedora.png b/modules/servers/ArkHostHetznerVPS/template/img/os/fedora.png new file mode 100644 index 0000000..6966c9c Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/fedora.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/iso.png b/modules/servers/ArkHostHetznerVPS/template/img/os/iso.png new file mode 100644 index 0000000..96a93e7 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/iso.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/lubuntu.png b/modules/servers/ArkHostHetznerVPS/template/img/os/lubuntu.png new file mode 100644 index 0000000..afdfd9f Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/lubuntu.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/mikrotik.png b/modules/servers/ArkHostHetznerVPS/template/img/os/mikrotik.png new file mode 100644 index 0000000..dfc5e7b Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/mikrotik.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/opensuse.png b/modules/servers/ArkHostHetznerVPS/template/img/os/opensuse.png new file mode 100644 index 0000000..077f59c Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/opensuse.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/others.png b/modules/servers/ArkHostHetznerVPS/template/img/os/others.png new file mode 100644 index 0000000..34fe6e4 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/others.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/rockylinux.png b/modules/servers/ArkHostHetznerVPS/template/img/os/rockylinux.png new file mode 100644 index 0000000..99363ca Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/rockylinux.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/ubuntu.png b/modules/servers/ArkHostHetznerVPS/template/img/os/ubuntu.png new file mode 100644 index 0000000..087c1e8 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/ubuntu.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/os/windows.png b/modules/servers/ArkHostHetznerVPS/template/img/os/windows.png new file mode 100644 index 0000000..43b2283 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/os/windows.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/running.png b/modules/servers/ArkHostHetznerVPS/template/img/running.png new file mode 100644 index 0000000..2429bcc Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/running.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/search.png b/modules/servers/ArkHostHetznerVPS/template/img/search.png new file mode 100644 index 0000000..b7af958 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/search.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/settings.png b/modules/servers/ArkHostHetznerVPS/template/img/settings.png new file mode 100644 index 0000000..bcf6362 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/settings.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/stopped.png b/modules/servers/ArkHostHetznerVPS/template/img/stopped.png new file mode 100644 index 0000000..c2ed128 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/stopped.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/suspended.png b/modules/servers/ArkHostHetznerVPS/template/img/suspended.png new file mode 100644 index 0000000..8a1de3c Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/suspended.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/under-cancellation.png b/modules/servers/ArkHostHetznerVPS/template/img/under-cancellation.png new file mode 100644 index 0000000..366b8dd Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/under-cancellation.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/img/vnc.png b/modules/servers/ArkHostHetznerVPS/template/img/vnc.png new file mode 100644 index 0000000..8793f95 Binary files /dev/null and b/modules/servers/ArkHostHetznerVPS/template/img/vnc.png differ diff --git a/modules/servers/ArkHostHetznerVPS/template/script.js b/modules/servers/ArkHostHetznerVPS/template/script.js new file mode 100644 index 0000000..b2ddf8b --- /dev/null +++ b/modules/servers/ArkHostHetznerVPS/template/script.js @@ -0,0 +1,1173 @@ +/* ArkHost Hetzner VPS Module JavaScript */ + +// Floating IP Management Functions +function loadFloatingIPStatus() { + var content = document.getElementById('floating-ip-content'); + if (!content) return; + + content.innerHTML = '

Loading floating IP information...

'; + + // Check if floating IP configurable option is enabled + ArkHostHetznerVPS_API('GetFloatingIPStatus', false, {}, function(response) { + if (response.result === 'success') { + displayFloatingIPStatus(response.data); + } else { + // Server returned a structured error (like permission denied) + content.innerHTML = '
' + + (response.message || 'Floating IP not available for this service') + '
'; + } + }, function(error) { + // Network/JavaScript error callback + var errorMsg = 'Failed to load floating IP information. Please try again later.'; + if (error && error.message) { + errorMsg = error.message; + } + content.innerHTML = '
' + errorMsg + '
'; + }); +} + +function displayFloatingIPStatus(data) { + var content = document.getElementById('floating-ip-content'); + if (!content) return; + + var html = ''; + + if (!data.hasFloatingIP) { + html = '
' + + (lang.settings.floatingIP.notAvailable || 'Floating IP not available for this service') + '
'; + } else if (!data.floatingIP) { + html = '
' + + (lang.settings.floatingIP.none || 'No floating IP assigned') + '
'; + + // Add option to create floating IP if needed (for configurable option customers) + html += '
'; + html += '

If you need a floating IP, please contact support to provision one for your service.

'; + html += '
'; + } else { + var ip = data.floatingIP; + var isAssigned = ip.server && ip.server.id; + + html += '
'; + html += '
'; + html += '
Floating IP Details
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += 'IP Address: ' + ip.ip + '
'; + html += 'Type: ' + ip.type.toUpperCase() + '
'; + html += 'Status: ' + + (isAssigned ? lang.settings.floatingIP.assigned || 'Assigned' : lang.settings.floatingIP.unassigned || 'Unassigned') + ''; + html += '
'; + html += '
'; + + if (isAssigned) { + html += '
'; + } else { + html += '
'; + } + + // Reverse DNS section + html += '
'; + html += ''; + html += '
'; + html += ' 0 ? ip.dns_ptr[0].dns_ptr : '') + '" '; + html += 'placeholder="' + (lang.settings.floatingIP.reverseDNSPlaceholder || 'example.com') + '">'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + html += '
'; + + html += '
'; + html += '
'; + html += '
'; + html += '
'; + } + + content.innerHTML = html; +} + +function assignFloatingIP(floatingIPId) { + showConfirm( + 'Assign this floating IP to the server?', + 'Assign Floating IP', + function() { + ArkHostHetznerVPS_API('AssignFloatingIP', false, {floating_ip_id: floatingIPId}, function(response) { + if (response.result === 'success') { + showNotification('Floating IP assigned successfully', 'success'); + setTimeout(loadFloatingIPStatus, 2000); + } else { + showNotification(response.message || 'Failed to assign floating IP', 'error'); + } + }); + } + ); +} + +function unassignFloatingIP(floatingIPId) { + showConfirm( + 'Unassign this floating IP from the server?', + 'Unassign Floating IP', + function() { + ArkHostHetznerVPS_API('UnassignFloatingIP', false, {floating_ip_id: floatingIPId}, function(response) { + if (response.result === 'success') { + showNotification('Floating IP unassigned successfully', 'success'); + setTimeout(loadFloatingIPStatus, 2000); + } else { + showNotification(response.message || 'Failed to unassign floating IP', 'error'); + } + }); + } + ); +} + +function setReverseDNS(floatingIPId, ip) { + var input = document.getElementById('reverse-dns-' + floatingIPId); + if (!input) return; + + var hostname = input.value.trim(); + + ArkHostHetznerVPS_API('SetFloatingIPReverseDNS', false, { + floating_ip_id: floatingIPId, + ip: ip, + dns_ptr: hostname + }, function(response) { + if (response.result === 'success') { + showNotification('Reverse DNS updated successfully', 'success'); + } else { + showNotification(response.message || 'Failed to update reverse DNS', 'error'); + } + }); +} + +// Initialize module variables from template +var productURL = productURL || ''; +var serverInfoInitial = serverInfoInitial || {}; +var lang = lang || {}; + +function confirmStop() { + showConfirm(lang.confirm.stop.message, lang.confirm.stop.title, function() { + ArkHostHetznerVPS_API('Stop', true); + }); + return false; +} + +function confirmRestart() { + showConfirm(lang.confirm.restart.message, lang.confirm.restart.title, function() { + ArkHostHetznerVPS_API('Reboot', true); + }); + return false; +} + +function confirmShutdown() { + showConfirm(lang.confirmShutdownMessage || 'Are you sure you want to shutdown this VPS?', lang.confirmShutdownTitle || 'Shutdown VPS', function() { + ArkHostHetznerVPS_API('Shutdown', true); + }); + return false; +} + +// Rescue Mode functions +function enableRescueMode() { + showConfirm( + lang.confirmEnableRescueMessage || 'Enable rescue mode?', + lang.confirmEnableRescueTitle || 'Enable Rescue Mode', + function() { + ArkHostHetznerVPS_API('Rescue Mode', true, {}, function(data) { + if (data.root_password) { + showNotification(lang.rescueModeEnabled + data.root_password + ' (' + lang.savePassword + ')', 'success', 10000); + } + }); + } + ); + return false; +} + +function enableRescueAndReboot() { + showConfirm( + lang.confirmEnableRescueRebootMessage || 'Enable rescue mode and reboot?', + lang.confirmEnableRescueRebootTitle || 'Enable & Reboot', + function() { + ArkHostHetznerVPS_API('Rescue Mode', true, {}, function(data) { + if (data.root_password) { + showNotification(lang.rescueModeEnabled + data.root_password + ' (' + lang.savePassword + ')', 'success', 10000); + // Wait a moment then reboot + setTimeout(function() { + ArkHostHetznerVPS_API('Reboot', true); + }, 2000); + } + }); + } + ); + return false; +} + +function disableRescueMode() { + showConfirm( + lang.confirmDisableRescueMessage || 'Disable rescue mode?', + lang.confirmDisableRescueTitle || 'Disable Rescue Mode', + function() { + ArkHostHetznerVPS_API('Disable Rescue Mode', true); + } + ); + return false; +} + +function resetRootPassword() { + showConfirm( + lang.confirmResetPasswordMessage || 'Reset root password?', + lang.confirmResetPasswordTitle || 'Reset Root Password', + function() { + ArkHostHetznerVPS_API('Reset root', true, {}, function(data) { + if (data.root_password) { + // Update the password field on the page + var passwordField = document.getElementById('vpsPassword'); + if (passwordField) { + passwordField.value = data.root_password; + } + + // Show password in a special modal or notification + var passwordHtml = '
' + + '
' + lang.newRootPassword + '
' + + '

' + lang.passwordPrompt + '

' + + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '

' + lang.savePassword + ' ' + lang.passwordSaved + '

' + + '
'; + + // Create a modal to show the password + var modal = document.createElement('div'); + modal.className = 'arkhost-confirm-overlay show'; + modal.innerHTML = '
' + + '
' + lang.confirmResetPasswordTitle + '
' + + '
' + passwordHtml + '
' + + '
' + + '' + + '
' + + '
'; + document.body.appendChild(modal); + } + }); + } + ); + return false; +} + +function copyToClipboard(elementId) { + var copyText = document.getElementById(elementId); + copyText.select(); + copyText.setSelectionRange(0, 99999); + document.execCommand("copy"); + showNotification(lang.passwordCopied || 'Password copied!', 'success'); +} + + +// Custom notification functions +function showNotification(message, type, duration) { + type = type || 'success'; + duration = duration || 4000; + + // Remove existing notifications + var existing = document.querySelectorAll('.arkhost-notification'); + for (var i = 0; i < existing.length; i++) { + existing[i].remove(); + } + + // Create notification element + var notification = document.createElement('div'); + notification.className = 'arkhost-notification ' + type; + + // Set icon based on type + var icon = 'fa-check-circle'; + if (type === 'error') icon = 'fa-exclamation-circle'; + else if (type === 'warning') icon = 'fa-exclamation-triangle'; + else if (type === 'info') icon = 'fa-info-circle'; + + notification.innerHTML = '' + message + ''; + + // Add to document + document.body.appendChild(notification); + + // Trigger animation + setTimeout(function() { + notification.classList.add('show'); + }, 100); + + // Auto remove + setTimeout(function() { + notification.classList.remove('show'); + setTimeout(function() { + if (notification.parentElement) { + notification.remove(); + } + }, 300); + }, duration); +} + +function showConfirm(message, title, onConfirm) { + title = title || 'Confirm Action'; + + // Remove existing confirms + var existing = document.querySelectorAll('.arkhost-confirm-overlay'); + for (var i = 0; i < existing.length; i++) { + existing[i].remove(); + } + + // Create confirm dialog + var overlay = document.createElement('div'); + overlay.className = 'arkhost-confirm-overlay'; + + overlay.innerHTML = '
' + title + '
' + message + '
'; + + // Add event listeners + var cancelBtn = overlay.querySelector('.cancel'); + var confirmBtn = overlay.querySelector('.confirm'); + + cancelBtn.onclick = function() { + overlay.classList.remove('show'); + setTimeout(function() { + if (overlay.parentElement) { + overlay.remove(); + } + }, 300); + }; + + confirmBtn.onclick = function() { + overlay.classList.remove('show'); + setTimeout(function() { + if (overlay.parentElement) { + overlay.remove(); + } + }, 300); + if (onConfirm) { + onConfirm(); + } + }; + + // Close on backdrop click + overlay.onclick = function(e) { + if (e.target === overlay) { + overlay.classList.remove('show'); + setTimeout(function() { + if (overlay.parentElement) { + overlay.remove(); + } + }, 300); + } + }; + + // Add to document and show + document.body.appendChild(overlay); + setTimeout(function() { + overlay.classList.add('show'); + }, 100); +} + +// Helper functions for API calls via WHMCS (use existing jQuery) +function ArkHostHetznerVPS_API(action, showAlert, params, callback, errorCallback) { + if (showAlert === undefined) showAlert = true; + if (params === undefined) params = {}; + if (typeof params === 'function') { + callback = params; + params = {}; + } + + var postData = Object.assign({ + action: 'productdetails', + id: window.serviceId || '', + modop: 'custom', + a: 'ClientAreaAPI', + api: action, + token: window.csrfToken || '' // Add CSRF token to prevent session timeouts + }, params); + + // Use existing WHMCS jQuery with proper headers + if (typeof jQuery !== 'undefined') { + jQuery.post({ + url: window.webRoot + '/clientarea.php', + data: postData, + dataType: 'json', + beforeSend: function(xhr) { + // Add CSRF token header for additional security + xhr.setRequestHeader('X-CSRF-Token', window.csrfToken || ''); + }, + success: function(data) { + // Check for success - WHMCS might return different success indicators + var isSuccess = (data.result === 'success') || + (data && typeof data === 'object' && !data.error && !data.message) || + (Array.isArray(data)) || + (data && Object.keys(data).some(key => !isNaN(key))); // Has numeric keys + + if (isSuccess) { + if (showAlert) showNotification(lang.moduleactionsuccess, 'success'); + + // Call callback if provided + if (callback && typeof callback === 'function') { + callback(data); + } + + // Handle specific responses + if (action === 'List backups') { + updateBackupTable(data); + } + if (action === 'Create backup') { + // Refresh backup list after creation + setTimeout(function() { + ArkHostHetznerVPS_API('List backups', false); + }, 1000); + } + if (action === 'Delete backup') { + // Refresh backup list after deletion + setTimeout(function() { + ArkHostHetznerVPS_API('List backups', false); + }, 1000); + } + if (action === 'Add Firewall rules') { + // Clear the form + document.getElementById('firewallDirection').value = 'in'; + document.getElementById('firewallAction').value = 'ACCEPT'; + document.getElementById('firewallPort').value = ''; + document.getElementById('firewallProtocol').value = 'ANY'; + document.getElementById('firewallSource').value = ''; + + // Refresh firewall rules after adding + setTimeout(function() { + ArkHostHetznerVPS_API('Get Firewall rules', false); + }, 1000); + } + if (action === 'Delete Firewall rule') { + // Refresh firewall rules after deletion + setTimeout(function() { + ArkHostHetznerVPS_API('Get Firewall rules', false); + }, 1000); + } + if (action === 'Get Firewall rules') updateFirewallTable(data); + if (action === 'ISO Images') updateISOSelect(data.isos || data); + if (action === 'List SSH Keys' && callback) callback(data); + if (action === 'Graphs') updateGraphsDisplay(data); + + // Refresh page for server state changes + if (['Start', 'Stop', 'Reboot', 'Reinstall'].indexOf(action) !== -1) { + setTimeout(function() { location.reload(); }, 2000); + } + } else { + if (showAlert) showNotification(lang.moduleactionfailed + ': ' + (data.message || 'Unknown error'), 'error'); + // Hide loading for graphs + if (action === 'Graphs') { + document.getElementById('graphs-loading').style.display = 'none'; + document.getElementById('graphs-container').style.opacity = '1'; + } + if (errorCallback) errorCallback(data); + } + }, + error: function(xhr, status, error) { + if (showAlert) showNotification(lang.moduleactionfailed + ': ' + (lang.networkError || 'Network error'), 'error'); + // Hide loading for graphs + if (action === 'Graphs') { + document.getElementById('graphs-loading').style.display = 'none'; + document.getElementById('graphs-container').style.opacity = '1'; + } + if (errorCallback) errorCallback({error: 'Network error'}); + } + }); + } + + return false; +} + +function updateBackupTable(data) { + var tableBody = document.querySelector('#backupTable tbody'); + tableBody.innerHTML = ''; + + // Handle the Hetzner API response format + var backupList = []; + + // Handle different response formats + if (Array.isArray(data)) { + // Direct array response + backupList = data; + } else if (data && typeof data === 'object') { + // Check if there are numeric keys (0, 1, 2, etc.) indicating backup array + var keys = Object.keys(data); + var numericKeys = keys.filter(function(key) { return !isNaN(key); }); + if (numericKeys.length > 0) { + backupList = numericKeys.map(function(key) { return data[key]; }); + } else if (data.result === 'success') { + // WHMCS wrapper with result='success' - look for numeric keys excluding 'result' + var filteredKeys = keys.filter(function(key) { return !isNaN(key) && key !== 'result'; }); + if (filteredKeys.length > 0) { + backupList = filteredKeys.map(function(key) { return data[key]; }); + } + } + } + + if (backupList.length > 0) { + backupList.forEach(function(backup) { + if (backup && (backup.id || backup.file)) { + var row = document.createElement('tr'); + + // Use created date for Hetzner backups + var backupDate = backup.created || backup.date || 'N/A'; + if (backupDate !== 'N/A' && backupDate.includes('T')) { + // Format ISO date to YYYY-MM-DD + backupDate = backupDate.split('T')[0]; + } + + // Determine backup type + var backupType = backup.type === 'backup' ? lang.backups.automatic : lang.backups.manual; + + // Format size (already in GB from API) + var sizeStr = 'N/A'; + if (backup.size) { + sizeStr = backup.size + ' ' + lang.general.gb; + } + + // Status is always available for Hetzner backups + var statusBadge = '' + lang.backups.available + ''; + + // Use backup ID for Hetzner + var backupId = backup.id || backup.file; + + row.innerHTML = + '' + backupDate + '' + + '' + sizeStr + '' + + '' + backupType + '' + + '' + statusBadge + '' + + '' + + '' + + '' + + ''; + tableBody.appendChild(row); + } + }); + } else { + tableBody.innerHTML = '' + lang.general.noBackupsFound + ''; + } +} + +function createBackup() { + showConfirm( + lang.confirm.createBackup.message, + lang.confirm.createBackup.title, + function() { + ArkHostHetznerVPS_API('Create backup', true); + } + ); + return false; +} + +function deleteBackup(backupId) { + showConfirm( + lang.confirm.deleteBackup.message, + lang.confirm.deleteBackup.title, + function() { + ArkHostHetznerVPS_API('Delete backup', true, { image_id: backupId }); + } + ); + return false; +} + +function restoreBackup(backupId) { + showConfirm( + lang.confirm.restoreBackup.message, + lang.confirm.restoreBackup.title, + function() { + ArkHostHetznerVPS_API('Restore backup', true, { image_id: backupId }); + } + ); + return false; +} + +function updateFirewallTable(data) { + var tableBody = document.querySelector('#firewallTable tbody'); + if (!tableBody) return; + + // Clear all rows + tableBody.innerHTML = ''; + + // Handle the API response format + var rulesList = []; + + // Handle different response formats + if (Array.isArray(data)) { + rulesList = data; + } else if (data && typeof data === 'object') { + var keys = Object.keys(data); + var numericKeys = keys.filter(function(key) { return !isNaN(key); }); + if (numericKeys.length > 0) { + rulesList = numericKeys.map(function(key) { return data[key]; }); + } else if (data.rules && Array.isArray(data.rules)) { + rulesList = data.rules; + } else if (data.result === 'success') { + var filteredKeys = keys.filter(function(key) { return !isNaN(key) && key !== 'result'; }); + if (filteredKeys.length > 0) { + rulesList = filteredKeys.map(function(key) { return data[key]; }); + } + } + } + + // Add firewall rules if any exist + if (rulesList.length > 0) { + rulesList.forEach(function(rule) { + if (rule && rule.id) { + var actionBadge; + if (rule.action === 'ACCEPT') { + actionBadge = '' + lang.firewall.accept + ''; + } else if (rule.action === 'DROP') { + actionBadge = '' + lang.firewall.drop + ''; + } else if (rule.action === 'INFO') { + actionBadge = '' + lang.firewall.info + ''; + } else { + actionBadge = '' + rule.action + ''; + } + + var protocolBadge = '' + (rule.protocol || lang.firewall.any) + ''; + + // Direction badge + var directionBadge = ''; + if (rule.direction === 'out') { + directionBadge = '' + (lang.firewall.outgoing || 'OUT') + ''; + } else { + directionBadge = '' + (lang.firewall.incoming || 'IN') + ''; + } + + var row = document.createElement('tr'); + row.innerHTML = + '' + directionBadge + '' + + '' + actionBadge + '' + + '' + protocolBadge + '' + + '' + (rule.port || '' + lang.general.any + '') + '' + + '' + (rule.source || '0.0.0.0/0') + '' + + '' + + (rule.id !== 'info' ? + '' + : '' + lang.general.emptyValue + '') + + ''; + tableBody.appendChild(row); + } + }); + } else { + tableBody.innerHTML = '' + lang.general.noFirewallRules + ''; + } +} + +function addFirewallRule() { + var direction = document.getElementById('firewallDirection').value; + var action = document.getElementById('firewallAction').value; + var protocol = document.getElementById('firewallProtocol').value; + var port = document.getElementById('firewallPort').value; + var source = document.getElementById('firewallSource').value || '0.0.0.0/0'; + + // Validate port for TCP/UDP + if ((protocol === 'TCP' || protocol === 'UDP' || protocol === 'ANY') && !port) { + showNotification(lang.messages.portRequired, 'error'); + document.getElementById('firewallPort').focus(); + return false; + } + + // Validate port format (single port or range) + if (port) { + // Check if it's a range (e.g., "8080-8090") + if (port.includes('-')) { + var parts = port.split('-'); + if (parts.length !== 2) { + showNotification('Invalid port range format. Use format: 8080-8090', 'error'); + document.getElementById('firewallPort').focus(); + return false; + } + var startPort = parseInt(parts[0]); + var endPort = parseInt(parts[1]); + if (isNaN(startPort) || isNaN(endPort) || startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + showNotification('Port range values must be between 1 and 65535', 'error'); + document.getElementById('firewallPort').focus(); + return false; + } + if (startPort >= endPort) { + showNotification('Start port must be less than end port', 'error'); + document.getElementById('firewallPort').focus(); + return false; + } + } else { + // Single port + var portNum = parseInt(port); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + showNotification('Port must be between 1 and 65535', 'error'); + document.getElementById('firewallPort').focus(); + return false; + } + } + } + + ArkHostHetznerVPS_API('Add Firewall rules', true, { + direction: direction, + firewallAction: action, + protocol: protocol, + source: source, + port: port + }); + + return false; +} + +function deleteFirewallRule(ruleId) { + showConfirm( + lang.confirmDeleteFirewallRuleMessage || 'Delete this firewall rule?', + lang.confirmDeleteFirewallRuleTitle || 'Delete Firewall Rule', + function() { + ArkHostHetznerVPS_API('Delete Firewall rule', true, { rule_id: ruleId }); + } + ); + return false; +} + +function updateISOSelect(data) { + var select = document.getElementById('isoID'); + if (!select) return; + + // Clear existing options except the first one + select.innerHTML = ''; + + // Function to check if a value looks like a valid ISO + function isValidISO(value, name) { + if (!value || !name) return false; + + // Convert to strings for comparison + var valueStr = String(value).toLowerCase(); + var nameStr = String(name).toLowerCase(); + + // Filter out common non-ISO values + var invalidValues = ['success', 'error', 'status', 'result', 'message', 'data']; + if (invalidValues.includes(valueStr) || invalidValues.includes(nameStr)) { + return false; + } + + // For Hetzner, if it has an ID and name, it's valid + return true; + } + + // Handle different possible response formats from Hetzner API + if (data) { + // Format 1: Array of ISO objects (Hetzner format) + if (Array.isArray(data)) { + data.forEach(function(iso) { + if (iso && (iso.id || iso.name)) { + // For Hetzner, ISOs are identified by name, not ID + var isoValue = iso.name || iso.id; + var isoName = iso.description || iso.name || 'ISO ' + iso.id; + + // Add architecture info if available + if (iso.architecture) { + isoName += ' (' + iso.architecture.toUpperCase() + ')'; + } + + if (isValidISO(isoValue, isoName)) { + var option = document.createElement('option'); + option.value = isoValue; + option.textContent = isoName; + select.appendChild(option); + } + } + }); + } + // Format 2: Object with ISO IDs as keys + else if (typeof data === 'object') { + Object.keys(data).forEach(function(key) { + var iso = data[key]; + if (iso && typeof iso === 'object') { + var isoValue = iso.id || iso.iso_id || iso.filename || key; + var isoName = iso.name || iso.filename || iso.label || key; + + if (isValidISO(isoValue, isoName)) { + var option = document.createElement('option'); + option.value = isoValue; + option.textContent = isoName; + select.appendChild(option); + } + } else if (typeof iso === 'string') { + // Simple key-value format + if (isValidISO(key, iso)) { + var option = document.createElement('option'); + option.value = key; + option.textContent = iso; + select.appendChild(option); + } + } + }); + } + } + + // If no ISOs were added, show a message + if (select.options.length === 1) { + var option = document.createElement('option'); + option.value = ''; + option.textContent = lang.noISOImages || 'No ISO images available'; + option.disabled = true; + select.appendChild(option); + } +} + +// Helper function to resize graph images +function resizeGraphImage(htmlContent) { + if (htmlContent.includes(']*?)>/g, ''); + } + return htmlContent; +} + +// Simple chart creation functions +function createSimpleLineChart(data, labels, title, color) { + if (!data || data.length === 0) { + return '

' + (lang.general.noDataAvailable || 'No data available') + '

'; + } + + var max = Math.max(...data); + // Ensure max is at least 100 for CPU percentage + if (title.includes('CPU') && max < 100) max = 100; + if (max < 1) max = 1; + + var html = '
'; + html += '
' + title + '
'; + html += '
'; + + // Add Y-axis labels container + html += '
'; + for (var i = 4; i >= 0; i--) { + var value = (max / 4) * i; + html += '' + Math.round(value) + '%'; + } + html += '
'; + + // Chart area with left margin for labels + html += '
'; + html += ''; + + // Grid lines + for (var i = 0; i <= 4; i++) { + var y = i * 50; + html += ''; + } + + // Add some vertical grid lines for time reference + for (var i = 0; i <= 5; i++) { + var x = i * 100; + html += ''; + } + + // Draw line + var points = ''; + for (var i = 0; i < data.length; i++) { + var x = (i / (data.length - 1)) * 500; + var y = 200 - (data[i] / max) * 190; + points += x + ',' + y + ' '; + } + html += ''; + + // Add dots on data points for better visibility + for (var i = 0; i < data.length; i++) { + var x = (i / (data.length - 1)) * 500; + var y = 200 - (data[i] / max) * 190; + html += ''; + } + + html += ''; + html += '
'; + + // Time labels at bottom + html += '
'; + if (labels && labels.length > 0) { + var step = Math.ceil(labels.length / 6); + for (var i = 0; i < labels.length; i += step) { + html += '' + labels[i] + ''; + } + html += '' + labels[labels.length - 1] + ''; + } + html += '
'; + + // Current value + html += '
'; + html += '' + (lang.graphs.current || 'Current') + lang.general.colon + Math.round(data[data.length - 1]) + lang.graphs.cpuUnit + ''; + html += '
'; + + return html; +} + +function createDoubleLineChart(data1, data2, labels, title, label1, label2, color1, color2) { + if (!data1 || data1.length === 0) { + return '

' + (lang.general.noDataAvailable || 'No data available') + '

'; + } + + var max = Math.max(...data1, ...data2); + // Ensure minimum scale for better visibility + if (max < 1) max = 1; + + // Format function for values based on title + var formatValue = function(val) { + if (title.includes(lang.graphs.networkUnit)) { + return val.toFixed(2) + ' ' + lang.graphs.networkUnit; + } else if (title.includes(lang.graphs.diskUnit)) { + return Math.round(val) + ' ' + lang.graphs.diskUnit; + } + return val.toFixed(1); + }; + + var html = '
'; + html += '
' + title + '
'; + html += '
'; + + // Add Y-axis labels container + html += '
'; + for (var i = 4; i >= 0; i--) { + var value = (max / 4) * i; + html += '' + (value >= 1 ? Math.round(value) : value.toFixed(2)) + ''; + } + html += '
'; + + // Chart area with left margin for labels + html += '
'; + html += ''; + + // Grid lines + for (var i = 0; i <= 4; i++) { + var y = i * 50; + html += ''; + } + + // Add some vertical grid lines for time reference + for (var i = 0; i <= 5; i++) { + var x = i * 100; + html += ''; + } + + // Line 1 + var points1 = ''; + for (var i = 0; i < data1.length; i++) { + var x = (i / (data1.length - 1)) * 500; + var y = 200 - (data1[i] / max) * 190; + points1 += x + ',' + y + ' '; + } + html += ''; + + // Line 2 + var points2 = ''; + for (var i = 0; i < data2.length; i++) { + var x = (i / (data2.length - 1)) * 500; + var y = 200 - (data2[i] / max) * 190; + points2 += x + ',' + y + ' '; + } + html += ''; + + html += ''; + html += '
'; + + // Time labels at bottom + html += '
'; + if (labels && labels.length > 0) { + var step = Math.ceil(labels.length / 6); + for (var i = 0; i < labels.length; i += step) { + html += '' + labels[i] + ''; + } + html += '' + labels[labels.length - 1] + ''; + } + html += '
'; + + // Legend with current values + html += '
'; + html += '' + label1 + lang.general.colon + formatValue(data1[data1.length - 1]) + ''; + html += '' + label2 + lang.general.colon + formatValue(data2[data2.length - 1]) + ''; + html += '
'; + + return html; +} + +function updateGraphsDisplay(data) { + // Hide loading indicator + document.getElementById('graphs-loading').style.display = 'none'; + document.getElementById('graphs-container').style.opacity = '1'; + + // Update graphs if data is available + if (data.graphs && data.graphs.type === 'metrics' && data.graphs.data) { + var metricsData = data.graphs.data; + + // Display CPU graph + if (metricsData.cpu) { + var cpuHtml = createSimpleLineChart(metricsData.cpu.data, metricsData.cpu.labels, lang.graphs.cpuUsage + ' (' + lang.graphs.cpuUnit + ')', '#e74c3c'); + document.getElementById('cpu-graph').innerHTML = cpuHtml; + } + + // Display Disk I/O graph + if (metricsData.disk) { + var diskHtml = createDoubleLineChart( + metricsData.disk.read, + metricsData.disk.write, + metricsData.disk.labels, + lang.graphs.diskIO + ' (' + lang.graphs.diskUnit + ')', + lang.graphs.read || 'Read', + lang.graphs.write || 'Write', + '#f39c12', + '#e67e22' + ); + document.getElementById('disk-graph').innerHTML = diskHtml; + } + + // Display Network graph + if (metricsData.network) { + var networkHtml = createDoubleLineChart( + metricsData.network.in, + metricsData.network.out, + metricsData.network.labels, + lang.graphs.networkTraffic + ' (' + lang.graphs.networkUnit + ')', + lang.graphs.inbound || 'Inbound', + lang.graphs.outbound || 'Outbound', + '#3498db', + '#2980b9' + ); + document.getElementById('network-graph').innerHTML = networkHtml; + } + + } else if (data.graphs && data.graphs.type === 'none') { + // No metrics available + var noMetricsMessage = '

' + (data.graphs.message || 'Metrics not available') + '

'; + document.getElementById('cpu-graph').innerHTML = noMetricsMessage; + document.getElementById('network-graph').innerHTML = noMetricsMessage; + document.getElementById('disk-graph').innerHTML = noMetricsMessage; + } else { + // Fallback + document.getElementById('cpu-graph').innerHTML = '

Metrics data format not recognized

'; + document.getElementById('network-graph').innerHTML = '

Metrics data format not recognized

'; + document.getElementById('disk-graph').innerHTML = '

Metrics data format not recognized

'; + } +} + +// Load initial graphs when tab is first opened +var graphsLoaded = false; +function loadInitialGraphs() { + if (!graphsLoaded) { + setTimeout(function() { + loadGraphs('day', document.querySelector('.graph-time-btn.active')); + graphsLoaded = true; + }, 100); + } +} + +// Load backups when tab is first opened +var backupsLoaded = false; +function loadBackups() { + if (!backupsLoaded) { + // Show loading state + var tableBody = document.querySelector('#backupTable tbody'); + if (tableBody) { + tableBody.innerHTML = ' ' + (lang.loadingBackups || 'Loading backups...') + ''; + } + + setTimeout(function() { + ArkHostHetznerVPS_API('List backups', false); + backupsLoaded = true; + }, 100); + } +} + +// Function to load graphs for different time periods +function loadGraphs(timePeriod, buttonElement) { + // Update active button + document.querySelectorAll('.graph-time-btn').forEach(btn => btn.classList.remove('active')); + buttonElement.classList.add('active'); + + // Show loading indicator + document.getElementById('graphs-loading').style.display = 'block'; + document.getElementById('graphs-container').style.opacity = '0.5'; + + // Use the existing ArkHostHetznerVPS_API function which now handles sessions properly + ArkHostHetznerVPS_API('Graphs', false, { time: timePeriod }); +} + +// Function to refresh all graphs with current time period +function refreshAllGraphs() { + // Find the currently active time period button + var activeButton = document.querySelector('.graph-time-btn.active'); + if (activeButton) { + // Show notification + showNotification(lang.messages.refreshingGraphs, 'info'); + // Reload graphs with current time period + activeButton.click(); + } +} + +function ArkHostVPS_ChooseOS(element) { + if (typeof jQuery !== 'undefined') { + var osId = jQuery(element).data('os'); + var group = jQuery(element).data('group'); + jQuery('#newOS').val(osId); + + // FIRST: Reset ALL OS selections (clear all groups) + jQuery('.os_badge .dropdown-toggle').removeClass('btn-success').addClass('btn-outline-secondary'); + jQuery('.os_badge .dropdown-item').removeClass('active bg-primary text-white'); + jQuery('.os_badge .version .fa-check').remove(); + jQuery('.os_badge .version').each(function() { + var originalText = jQuery(this).text().replace(' ✓', '').replace(/\s*]*><\/i>\s*/g, ''); + jQuery(this).text(originalText); + }); + + // THEN: Set the selected OS + // Update the version text with the selected OS + jQuery('#' + group + '-version').text(jQuery(element).text()); + + // Add active class to the selected item + jQuery(element).addClass('active bg-primary text-white'); + + // Update the main button to show selected state + var mainButton = jQuery('#' + group + '-os .dropdown-toggle'); + mainButton.removeClass('btn-outline-secondary').addClass('btn-success'); + + // Add checkmark to show selection + var versionSpan = jQuery('#' + group + '-version'); + versionSpan.append(' '); + + // No advanced options needed for Hetzner rebuild + } + return false; +} + +function ArkHostVPS_ShowPassword() { + if (typeof jQuery !== 'undefined') { + var input = jQuery('#vpsPassword'); + var icon = jQuery('#showPasswordIcon'); + if (input.attr('type') === 'password') { + input.attr('type', 'text'); + icon.removeClass('fa-eye').addClass('fa-eye-slash'); + } else { + input.attr('type', 'password'); + icon.removeClass('fa-eye-slash').addClass('fa-eye'); + } + } + return false; +} + +function reinstallWithAdvancedOptions() { + var osId = document.getElementById('newOS').value; + if (!osId || osId === '0') { + showNotification(lang.messages.selectOSFirst, 'warning'); + return false; + } + + // Hetzner rebuild only needs the image parameter + var params = { os: osId }; + + // Call the API + ArkHostHetznerVPS_API('Reinstall', true, params); + return false; +} \ No newline at end of file diff --git a/modules/servers/ArkHostHetznerVPS/template/style.css b/modules/servers/ArkHostHetznerVPS/template/style.css new file mode 100644 index 0000000..3697d02 --- /dev/null +++ b/modules/servers/ArkHostHetznerVPS/template/style.css @@ -0,0 +1,604 @@ +/* ArkHost Hetzner VPS Module Styles */ + +.arkhost-vps-container { + font-family: 'Arial', sans-serif; + margin: 15px 0; +} +.arkhost-vps-container .title-block { + color: #575d6f; + margin-top: 20px; + font-size: 16px; + font-weight: 600; +} +.arkhost-vps-container .dashboard-value { + font-size: 18px; + color: #3c7fb4; +} +.arkhost-vps-container .usage-details { + font-size: 14px; + color: #888; + margin-top: 5px; +} +.arkhost-vps-container .progress { + height: 20px; + margin-bottom: 20px; +} +.arkhost-vps-container .nav-tabs { + border-bottom: 1px solid #dee2e6; +} +.arkhost-vps-container .nav-tabs .nav-link { + border: none; + color: #6c757d; + font-weight: 500; + border-radius: 12px 12px 0 0; + padding: 12px 20px; + transition: all 0.2s ease; + margin-right: 4px; +} +.arkhost-vps-container .nav-tabs .nav-link:hover { + background-color: #f8f9fa; + color: #495057; +} +.arkhost-vps-container .nav-tabs .nav-link.active { + background-color: #e9ecef; + color: #495057; + border-radius: 12px 12px 0 0; +} +.arkhost-vps-container .overview-label { + color: #575d6f; + font-weight: 600; + margin-bottom: 5px; + font-size: 14px; +} +.arkhost-vps-container .submit-btn { + background: linear-gradient(135deg, #3c7fb4 0%, #2d5a87 100%); + color: white; + border: none; + padding: 10px 20px; + border-radius: 12px; + font-weight: 600; + transition: all 0.3s ease; +} +.arkhost-vps-container .danger-btn { + background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); + color: white; + border: none; + padding: 10px 20px; + border-radius: 12px; + font-weight: 600; + transition: all 0.3s ease; +} +.arkhost-vps-container .head { + border-bottom: 2px solid #3c7fb4; + margin-bottom: 20px; + padding-bottom: 15px; + margin-top: 10px; +} +.arkhost-vps-container .head img { + width: 32px; + height: 32px; + margin-right: 10px; + margin-top: -8px; + vertical-align: middle; +} +.arkhost-vps-container .v-tabs-container .nav-link { + padding: 10px 16px; + margin: 3px 0; + border-radius: 12px; + transition: all 0.2s ease; + color: #6c757d; + font-weight: 500; + text-decoration: none; + display: block; +} +.arkhost-vps-container .v-tabs-container .nav-link:hover { + background-color: #f8f9fa; + color: #495057; + text-decoration: none; +} +.arkhost-vps-container .v-tabs-container .nav-link.active { + background-color: #e9ecef; + color: #495057; +} +.arkhost-vps-container .os_badge { + margin: 8px; + border-radius: 16px; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + min-height: 140px; + width: 100%; + max-width: 400px !important; + background: #ffffff; + border: none; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + position: relative; + z-index: 1; +} + +.arkhost-vps-container .os_badge:hover { + transform: translateY(-6px); + box-shadow: 0 12px 40px rgba(60, 127, 180, 0.15); + z-index: 10; +} + +.arkhost-vps-container .os_badge .dropdown-toggle { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 2px solid #e9ecef; + border-radius: 16px; + padding: 24px 20px; + transition: all 0.3s ease; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + justify-content: center; + height: 100%; + min-height: 140px; +} + + + +.arkhost-vps-container .os_badge .dropdown-toggle:hover { + border-color: #3c7fb4; + background: linear-gradient(135deg, #ffffff 0%, #f0f4f8 100%); +} + +.arkhost-vps-container .os_badge .media-body { + min-width: 0; + flex: 1; + z-index: 2; + position: relative; + margin-top: 6px; + text-align: center; +} + +.arkhost-vps-container .os_badge .distro_name { + font-size: 1.1rem; + line-height: 1.3; + margin-bottom: 4px; + word-wrap: break-word; + overflow-wrap: break-word; + font-weight: 600; + color: #2c3e50; +} + +.arkhost-vps-container .os_badge .version { + font-size: 0.85rem; + line-height: 1.2; + color: #6c757d; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.arkhost-vps-container .distro_img { + width: 48px; + height: 48px; + margin-right: 0; + margin-top: 8px; + margin-bottom: 6px; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + z-index: 2; + position: relative; +} + +.arkhost-vps-container .os_badge:hover .distro_img { + transform: scale(1.1) rotate(5deg); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +/* Dropdown Menu Enhancements */ +.arkhost-vps-container .os_badge .dropdown-menu { + border-radius: 12px; + border: none; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + padding: 8px 0; + margin-top: 8px; + z-index: 1050; + min-width: 100%; +} + +.arkhost-vps-container .os_badge .dropdown-item { + padding: 12px 20px; + transition: all 0.2s ease; + color: #495057; + font-weight: 500; +} + +.arkhost-vps-container .os_badge .dropdown-item:hover { + background: linear-gradient(135deg, #3c7fb4 0%, #2d5a87 100%); + color: white; + transform: translateX(4px); +} +#loading { + text-align: center; + padding: 2rem; +} + +/* Rounded borders for containers */ +.arkhost-vps-container .border { + border-radius: 12px !important; + border: 1px solid #e3e6f0 !important; +} + +/* VPS Control Button Styles */ +.arkhost-vps-container .vps-control-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border-radius: 20px; + margin: 8px 4px; + transition: all 0.2s ease; + text-decoration: none; + color: white; + font-size: 0.9rem; + font-weight: 500; + min-height: 36px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border: none; + position: relative; + overflow: hidden; +} + +.arkhost-vps-container .vps-control-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + text-decoration: none; + color: white; +} + +.arkhost-vps-container .vps-control-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.arkhost-vps-container .vps-control-btn.start-btn { + background: linear-gradient(45deg, #28a745, #34ce57); + color: white; +} + +.arkhost-vps-container .vps-control-btn.start-btn:hover { + background: linear-gradient(45deg, #218838, #28a745); + color: white; +} + +.arkhost-vps-container .vps-control-btn.stop-btn { + background: linear-gradient(45deg, #dc3545, #ff4757); + color: white; +} + +.arkhost-vps-container .vps-control-btn.stop-btn:hover { + background: linear-gradient(45deg, #c82333, #dc3545); + color: white; +} + +.arkhost-vps-container .vps-control-btn.restart-btn { + background: linear-gradient(45deg, #fd7e14, #ff9f43); + color: white; +} + +.arkhost-vps-container .vps-control-btn.restart-btn:hover { + background: linear-gradient(45deg, #e65100, #fd7e14); + color: white; +} + +.arkhost-vps-container .vps-control-btn.vnc-btn { + background: linear-gradient(45deg, #6f42c1, #8854d0); + color: white; +} + +.arkhost-vps-container .vps-control-btn.vnc-btn:hover { + background: linear-gradient(45deg, #5a32a3, #6f42c1); + color: white; +} + +.arkhost-vps-container .vps-control-btn.vnc-btn img { + width: 18px; + height: 18px; + margin-right: 6px; +} + +.arkhost-vps-container .vps-control-btn i { + margin-right: 6px; + font-size: 1rem; +} + +/* Mobile Responsive - Stack VPS Control Buttons Vertically */ +@media (max-width: 767px) { + .arkhost-vps-container .vps-control-btn { + display: block; + width: 100%; + margin: 8px 0; + text-align: center; + } +} + +/* Graph Time Selection Buttons */ +.arkhost-vps-container .graph-time-btn { + background: #f8f9fa; + color: #6c757d; + border: 1px solid #dee2e6; + padding: 8px 14px; + border-radius: 12px; + margin: 0 3px; + font-weight: 500; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; + cursor: pointer; + font-size: 0.9rem; +} + +.arkhost-vps-container .graph-time-btn:hover { + background: #e9ecef; + text-decoration: none; +} + +.arkhost-vps-container .graph-time-btn.active { + background: #e9ecef; + color: #495057; + border-color: #ced4da; +} + +.arkhost-vps-container .graph-time-btn.active:hover { + background: #dee2e6; +} + +/* OS Selection Enhancements */ +.arkhost-vps-container .os_badge { + margin-bottom: 20px; +} + +.arkhost-vps-container .os_badge .dropdown-toggle::after { + display: inline-block; + margin-left: .255em; + vertical-align: .255em; + content: ""; + border-top: .3em solid; + border-right: .3em solid transparent; + border-bottom: 0; + border-left: .3em solid transparent; + margin-bottom: 10px; +} + +.arkhost-vps-container .os_badge .dropdown-toggle:hover::after { + border-top-color: #495057; +} + +.arkhost-vps-container .os_badge .dropdown-toggle.btn-success { + border: 2px solid #28a745 !important; + background: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%); + box-shadow: 0 6px 25px rgba(40, 167, 69, 0.3); + color: #155724 !important; + position: relative; +} + +.arkhost-vps-container .os_badge .dropdown-toggle.btn-success::before { + content: '✓'; + position: absolute; + top: 8px; + right: 12px; + font-size: 18px; + color: #28a745; + font-weight: bold; +} + +.arkhost-vps-container .os_badge .dropdown-toggle.btn-success .distro_name { + color: #155724 !important; + font-weight: 600; +} + +.arkhost-vps-container .os_badge .dropdown-toggle.btn-success .version { + color: #155724 !important; +} + +.arkhost-vps-container .os_badge .dropdown-item.active { + background-color: #007bff !important; + color: white !important; + font-weight: bold; + position: relative; +} + +.arkhost-vps-container .os_badge .dropdown-item.active::after { + content: "✓"; + position: absolute; + right: 15px; + font-weight: bold; + color: white; +} + +.arkhost-vps-container .os_badge .dropdown-item:hover { + background-color: #007bff; + color: white !important; +} + +.arkhost-vps-container .os_badge .dropdown-item.active:hover { + background-color: #0056b3 !important; + color: white !important; +} + +/* Fix dropdown menu positioning and styling */ +.arkhost-vps-container .os_badge .dropdown-menu { + z-index: 1050; + border: 1px solid #dee2e6; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + padding: 8px 0; + margin-top: 4px; + max-height: 300px; + overflow-y: auto; + overflow-x: hidden; +} + +.arkhost-vps-container .os_badge .dropdown-item { + padding: 8px 16px; + font-size: 0.9rem; + transition: all 0.2s ease; + border: none; + background: none; +} + +.arkhost-vps-container .os_badge .version .fa-check { + color: #28a745 !important; + margin-left: 5px; + animation: checkmark-appear 0.3s ease-in; +} + +@keyframes checkmark-appear { + 0% { opacity: 0; transform: scale(0.5); } + 100% { opacity: 1; transform: scale(1); } +} + +/* Custom Notification System */ +.arkhost-notification { + position: fixed; + top: 20px; + right: 20px; + min-width: 300px; + max-width: 400px; + z-index: 9999; + padding: 16px 20px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + color: white; + font-weight: 500; + transform: translateX(450px); + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 12px; + font-family: 'Arial', sans-serif; +} + +.arkhost-notification.show { + transform: translateX(0); +} + +.arkhost-notification.success { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); +} + +.arkhost-notification.error { + background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%); +} + +.arkhost-notification.warning { + background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%); + color: #212529; +} + +.arkhost-notification.info { + background: linear-gradient(135deg, #17a2b8 0%, #6f42c1 100%); +} + +.arkhost-notification i { + font-size: 20px; + flex-shrink: 0; +} + +.arkhost-notification .close-btn { + margin-left: auto; + background: none; + border: none; + color: inherit; + font-size: 18px; + cursor: pointer; + padding: 0; + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.arkhost-notification .close-btn:hover { + opacity: 1; +} + +/* Custom Confirm Dialog */ +.arkhost-confirm-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.arkhost-confirm-overlay.show { + opacity: 1; + visibility: visible; +} + +.arkhost-confirm-dialog { + background: white; + border-radius: 12px; + padding: 24px; + max-width: 400px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + transform: scale(0.8); + transition: transform 0.3s ease; + font-family: 'Arial', sans-serif; +} + +.arkhost-confirm-overlay.show .arkhost-confirm-dialog { + transform: scale(1); +} + +.arkhost-confirm-title { + font-size: 18px; + font-weight: 600; + color: #2c3e50; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.arkhost-confirm-message { + color: #6c757d; + margin-bottom: 20px; + line-height: 1.5; +} + +.arkhost-confirm-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.arkhost-confirm-btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.arkhost-confirm-btn.cancel { + background: #6c757d; + color: white; +} + +.arkhost-confirm-btn.cancel:hover { + background: #5a6268; +} + +.arkhost-confirm-btn.confirm { + background: #dc3545; + color: white; +} + +.arkhost-confirm-btn.confirm:hover { + background: #c82333; +} \ No newline at end of file diff --git a/screenshots/1.png b/screenshots/1.png new file mode 100644 index 0000000..75c4151 Binary files /dev/null and b/screenshots/1.png differ diff --git a/screenshots/10.png b/screenshots/10.png new file mode 100644 index 0000000..9e90e1b Binary files /dev/null and b/screenshots/10.png differ diff --git a/screenshots/2.png b/screenshots/2.png new file mode 100644 index 0000000..280eba6 Binary files /dev/null and b/screenshots/2.png differ diff --git a/screenshots/3.png b/screenshots/3.png new file mode 100644 index 0000000..4c2a913 Binary files /dev/null and b/screenshots/3.png differ diff --git a/screenshots/4.png b/screenshots/4.png new file mode 100644 index 0000000..53d13f3 Binary files /dev/null and b/screenshots/4.png differ diff --git a/screenshots/5.png b/screenshots/5.png new file mode 100644 index 0000000..8990b78 Binary files /dev/null and b/screenshots/5.png differ diff --git a/screenshots/6.png b/screenshots/6.png new file mode 100644 index 0000000..70a722c Binary files /dev/null and b/screenshots/6.png differ diff --git a/screenshots/7.png b/screenshots/7.png new file mode 100644 index 0000000..251e3d1 Binary files /dev/null and b/screenshots/7.png differ diff --git a/screenshots/8.png b/screenshots/8.png new file mode 100644 index 0000000..a9ef8f1 Binary files /dev/null and b/screenshots/8.png differ diff --git a/screenshots/9.png b/screenshots/9.png new file mode 100644 index 0000000..68109a8 Binary files /dev/null and b/screenshots/9.png differ