22 Commits
Author SHA1 Message Date
ArkHostandGitHub 9049937d8e v1.1.1
Fixed
- Firewall rules now accept source 0.0.0.0/0 for all IPs
- "ANY" protocol creates both TCP and UDP rules automatically
- Outbound firewall rules now use correct destination_ips field

Added
- Incoming/Outgoing direction support for firewall rules
- Proper CIDR notation validation (supports /0, /4, /8, /16, etc.)

Improved
- Removed description/note field from firewall interface
- Changed "Source/Destination" column to "IP/CIDR" for clarity
- Translated all firewall column headers to all supported languages
- Cleaned up unused language strings
2025-09-09 20:54:17 +02:00
ArkHostandGitHub fb4e94b086 Update ArkHostHetznerVPS.php 2025-09-08 21:24:45 +02:00
ArkHostandGitHub 3bfc327763 Update ArkHostHetznerVPS.php
- Firewall Display: Removed fake "default open" rules that didn't exist in Hetzner dashboard
- Firewall Creation: Fixed "name already used" error with unique timestamp naming
- Firewall Association: Fixed API errors when attaching firewalls to servers
2025-09-08 21:24:29 +02:00
ArkHostandGitHub 8b7d6b2c62 Merge pull request #5 from notelseit/main
Update README.md
2025-08-19 00:02:58 +02:00
Notelseit.comandGitHub 3c526260cd Update README.md
fix view  Server Type read me
2025-08-16 11:27:36 +02:00
Notelseit.comandGitHub 8d4673378c Update README.md
update list Operating System
2025-08-16 11:25:49 +02:00
Notelseit.comandGitHub 5421cd0b30 Update README.md
correct datacenter data
2025-08-16 11:17:43 +02:00
ArkHostandGitHub 8887733e0e Update README.md 2025-08-12 19:39:15 +02:00
ArkHostandGitHub af56d91bff translation strings fixes 2025-07-21 21:49:06 +02:00
ArkHostandGitHub 60fb7b0ce0 translation strings fixes 2025-07-21 21:48:33 +02:00
ArkHostandGitHub 64dde17f0e Update README.md 2025-07-21 21:47:58 +02:00
ArkHostandGitHub 6b29ab199c update screenshot - translation fix 2025-07-21 21:47:00 +02:00
ArkHostandGitHub ff62c3a64c missing translations 2025-07-21 21:27:45 +02:00
ArkHostandGitHub 0de4798583 Update README.md 2025-07-21 21:00:49 +02:00
ArkHostandGitHub ef4c336049 Update README.md 2025-07-21 21:00:16 +02:00
ArkHostandGitHub f7df8b6492 Update README.md 2025-07-21 15:30:36 +02:00
ArkHostandGitHub 7a846f7fba screenshots 2025-07-21 15:28:33 +02:00
ArkHostandGitHub 332e21970d Update README.md 2025-07-21 15:28:04 +02:00
ArkHostandGitHub 394f7e8af8 v1.0
Initial release
2025-07-21 15:25:29 +02:00
ArkHostandGitHub 16f72f786c Update LICENSE 2025-07-21 15:22:35 +02:00
ArkHostandGitHub 4181c45085 Update README.md 2025-07-21 15:22:09 +02:00
ArkHostandGitHub ff492c7d85 Initial commit 2025-07-21 15:15:42 +02:00
8 changed files with 142 additions and 803 deletions
+1 -1
View File
@@ -18,4 +18,4 @@ 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.
SOFTWARE.
+54 -224
View File
@@ -12,7 +12,6 @@ WHMCS server module for Hetzner Cloud VPS management.
**Advanced**
- Cloud-init support for automated server configuration
- Floating IP management with reverse DNS
- Backup creation and restoration
- Firewall rule management
@@ -39,15 +38,14 @@ WHMCS server module for Hetzner Cloud VPS management.
## Installation
1. Upload files to `/path/to/whmcs/modules/servers/ArkHostHetznerVPS/`
2. Upload `hooks.php` to `/path/to/whmcs/includes/hooks/` (for enhanced UI)
3. Create server and add it to a server group:
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
4. Create products:
3. Create products:
- Setup → Products/Services → Products/Services → Create New Product
- Type: `VPS/Dedicated Server`
- Module: `ArkHostHetznerVPS`
@@ -57,12 +55,11 @@ WHMCS server module for Hetzner Cloud VPS management.
**Module Settings**
- Server Type: `cx23`, `cx33`, `cx43`, `cx53` (Cost-Optimized), `cpx11`, `cpx21`, `cax11`, `ccx13`, etc.
- Operating System: `ubuntu-22.04`, `debian-11`, `rocky-9`, etc.
- Location: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`, `sin`
- 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
- Cloud-Init YAML: Optional custom cloud-init configuration
**Custom Field (Required)**
@@ -84,29 +81,23 @@ Create in Setup → Products/Services → Configurable Options:
- `No|None`
- `Yes|1 Floating IP`
**2\. Location Selection**
**2\. Datacenter Selection**
- Option Name: `Location`
- Option Name: `datacenter`
- Option Type: `Dropdown`
- Options:
- `fsn1|Falkenstein, Germany`
- `nbg1|Nuremberg, Germany`
- `hel1|Helsinki, Finland`
- `ash|Ashburn, USA`
- `hil|Hillsboro, USA`
- `sin|Singapore, Singapore`
Existing configurable options named `datacenter` remain supported. Legacy values such as `fsn1-dc14` are automatically converted to their corresponding location (`fsn1`) when provisioning.
- `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:
- `cx23|CX23 - 2 vCPU, 4 GB RAM, 40 GB Disk, shared (Cost-Optimized)`
- `cx33|CX33 - 4 vCPU, 8 GB RAM, 80 GB Disk, shared (Cost-Optimized)`
- `cx43|CX43 - 8 vCPU, 16 GB RAM, 160 GB Disk, shared (Cost-Optimized)`
- `cx53|CX53 - 16 vCPU, 32 GB RAM, 320 GB Disk, shared (Cost-Optimized)`
- `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`
@@ -130,36 +121,34 @@ Existing configurable options named `datacenter` remain supported. Legacy values
**4\. Operating System**
- Option Name: `osid`
- Option Type: `Dropdown`
- 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`
- `debian-12|Debian 12`
- `debian-13|Debian 13`
- `rocky-8|Rocky Linux 8`
- `centos-stream-9|CentOS Stream 9`
- `ubuntu-22.04|Ubuntu 22.04`
- `ubuntu-24.04|Ubuntu 24.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`
- `rocky-10|Rocky Linux 10`
- `ubuntu-22.04|Ubuntu 22.04`
- `alma-8|AlmaLinux 8`
- `alma-9|AlmaLinux 9`
- `alma-10|AlmaLinux 10`
- `centos-stream-9|CentOS Stream 9`
- `centos-stream-10|CentOS Stream 10`
- `opensuse-15|openSUSE 15`
- `fedora-41|Fedora 41`
- `fedora-42|Fedora 42`
- `docker-ce|Docker CE`
- `lamp|LAMP Stack`
- `wordpress|WordPress`
- `nextcloud|Nextcloud`
- `gitlab|GitLab`
- `jitsi|Jitsi`
- `wireguard|WireGuard`
- `prometheus-grafana|Prometheus + Grafana`
- `owncast|Owncast`
- `photoprism|PhotoPrism`
- `rustdesk|RustDesk`
- `jitsi|jitsi`
- `owncast|owncast`
@@ -194,102 +183,6 @@ This ensures proper key isolation and security for each customer.
We're exploring secure implementations using cloud-init or per-server user data to enable SSH key management while maintaining proper isolation.
## Cloud-Init Support
Cloud-init allows automatic server configuration during first boot. Unlike project-level SSH keys, cloud-init user_data is per-server, ensuring proper isolation in multi-tenant environments.
**Configuration:**
1. Navigate to Setup → Products/Services → Products/Services
2. Edit your product → Module Settings tab
3. Find "Cloud-Init YAML (Optional)" textarea
4. Enter your cloud-init configuration in YAML format
5. Leave empty to skip cloud-init
**Common Use Cases:**
**1. Change APT Mirrors** (original use case - avoid Hetzner mirrors):
```yaml
#cloud-config
apt:
primary:
- arches: [default]
uri: http://de.archive.ubuntu.com/ubuntu/
```
**2. Add SSH Keys** (secure alternative to project-level keys):
```yaml
#cloud-config
users:
- name: admin
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2E... user@laptop
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
```
**3. Install Docker:**
```yaml
#cloud-config
packages:
- docker.io
- docker-compose
runcmd:
- systemctl enable docker
- systemctl start docker
```
**4. Security Hardening:**
```yaml
#cloud-config
packages:
- fail2ban
- ufw
runcmd:
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow 22/tcp
- ufw --force enable
- systemctl enable fail2ban
- systemctl start fail2ban
```
**5. Combined Configuration:**
```yaml
#cloud-config
apt:
primary:
- arches: [default]
uri: http://mirror.example.com/ubuntu/
users:
- name: admin
ssh_authorized_keys:
- ssh-rsa AAAAB3... admin@company
sudo: ALL=(ALL) NOPASSWD:ALL
packages:
- fail2ban
- ufw
- docker.io
runcmd:
- ufw allow 22/tcp
- ufw --force enable
- systemctl enable docker fail2ban
```
**Important Notes:**
- YAML must be valid syntax (use a YAML validator if unsure)
- If you don't start with `#cloud-config`, the module will add it automatically
- Cloud-init runs ONCE on first boot only
- Invalid YAML will cause provisioning to fail silently
- Maximum size: 32 KiB (Hetzner API limit)
- Documentation: https://docs.hetzner.cloud/#servers-create-a-server
## Screenshots
![Screenshot 1](screenshots/1.png)
![Screenshot 2](screenshots/2.png)
@@ -308,101 +201,38 @@ runcmd:
Enable module debugging: Configuration → System Logs → Module Log
## Support
## Support Policy
This module is free and open source, and we maintain it in our spare time. We hope it makes running Hetzner Cloud on WHMCS a little easier for you.
This is free, open-source software provided AS-IS.
If something isn't working as expected:
**NO SUPPORT** is provided except through paid channels:
- Premium Support: €100/hour (2 hour minimum)
- Email: support@arkhost.com (paid support only)
1. Check the module log first (Configuration → System Logs → Module Log). It usually points straight to the cause.
2. Open an issue with what you saw, the steps that led to it, and the relevant log output. The more detail you share, the quicker we can help.
### Before Requesting Support
- English only
- Must provide complete error logs
- Must provide steps to reproduce
- Vague reports like "doesn't work" will be ignored
We can't promise a response time, but we read every report and genuinely appreciate bug reports, ideas, and pull requests. Contributions are always welcome.
### 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. Use it, fork it, improve it.
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")
## Changelog
### v1.2.5
**Fixed:**
- Restored server provisioning after Hetzner removed the deprecated `datacenter` request property on July 1, 2026
- Replaced datacenter discovery with the Hetzner Locations API
- Updated client-area location display for the current server response format
- Added automatic conversion of legacy datacenter values such as `fsn1-dc14` to `fsn1`
### v1.2.4
**Fixed:**
- IP address now stored on the service at provisioning (and backfilled for existing services) so it displays in WHMCS instead of showing blank
- Service username now set to `root` to match the actual VPS login
- Install password now actually removed after the 72-hour window (was staying visible via the eye icon, including for older services that predated expiry tracking)
### v1.2.3
**Added:**
- VNC console key combination bar with Ctrl+Alt+Del, modifier keys (Shift, Alt, Ctrl), Tab, Enter, Del, Backspace, Esc, Shift+Up, and Fullscreen toggle
### v1.2.2
- WHMCS v9 compatibility: Updated Smarty template syntax for Smarty v4
### v1.2.1
**Added:**
- Shutdown button functionality (graceful server shutdown)
- Enhanced backup permission detection (automatically detects Hetzner-enabled backups)
- Improved Actions sidebar with custom Font Awesome icons
- VNC console opens in new tab
- hooks.php for cleaner UI (hides default WHMCS elements)
**Fixed:**
- Backup creation/deletion API response format
- Shutdown action now properly whitelisted in ClientAreaAPI
### v1.2.0
**Added:**
- Cloud-init support (optional per-product YAML configuration)
- CX Gen3 server types: CX23, CX33, CX43, CX53
- OS images: Debian 13, AlmaLinux 10, Rocky 10, CentOS Stream 10, openSUSE 15, Fedora 41/42
- Password expiration (72-hour visibility window)
**Fixed:**
- API pagination - server types and OS images now show all available options (per_page=50)
- Password reset timestamp tracking
- Password display in client area after reset
**Documentation:**
- Cloud-init configuration examples
- Updated server types and OS lists
### v1.1.1
**Fixed:**
- Firewall rules now accept source 0.0.0.0/0 for all IPs
- "ANY" protocol creates both TCP and UDP rules automatically
- Outbound firewall rules now use correct destination_ips field
**Added:**
- Incoming/Outgoing direction support for firewall rules
- Proper CIDR notation validation (supports /0, /4, /8, /16, etc.)
**Improved:**
- Removed description/note field from firewall interface
- Changed "Source/Destination" column to "IP/CIDR" for clarity
- Translated all firewall column headers to all supported languages
- Cleaned up unused language strings
### v1.1
**Fixed:**
- Firewall Display: Removed fake "default open" rules that didn't exist in Hetzner dashboard
- Firewall Creation: Fixed "name already used" error with unique timestamp naming
- Firewall Association: Fixed API errors when attaching firewalls to servers
- API Compatibility: Corrected data formats for Hetzner API (ports as strings, proper server object structure)
### v1.0
- Initial release
© 2025 ArkHost
<br>
@@ -3,7 +3,7 @@
* WHMCS Server Module - Hetzner VPS
*
* @package WHMCS
* @version 1.2.5
* @version 1.1.1
* @copyright Copyright (c) ArkHost 2025
* @author ArkHost <support@arkhost.com>
* @link https://arkhost.com
@@ -104,57 +104,6 @@ function ArkHostHetznerVPS_GetConfigurableOption(array $params, $optionName) {
return null;
}
/**
* Convert a legacy Hetzner datacenter name (for example fsn1-dc14) to the
* corresponding location name (fsn1). Location names are normalized to
* lowercase, while numeric IDs remain unchanged.
*/
function ArkHostHetznerVPS_NormalizeLocation($location) {
if (!is_scalar($location)) {
return '';
}
$location = trim((string)$location);
if ($location === '') {
return '';
}
if (preg_match('/^([a-z0-9]+)-dc[0-9]+$/i', $location, $matches)) {
return strtolower($matches[1]);
}
return strtolower($location);
}
/**
* Resolve the server location while remaining compatible with configurable
* options named either "Location" or the former "Datacenter".
*/
function ArkHostHetznerVPS_GetLocationOption(array $params, $default = NULL) {
foreach (array('configoptions', 'customfields') as $collection) {
if (!isset($params[$collection]) || !is_array($params[$collection])) {
continue;
}
// Prefer the current option name if both current and legacy options
// happen to exist on the same product.
foreach (array('location', 'datacenter') as $optionName) {
foreach ($params[$collection] as $name => $value) {
$normalizedName = strtolower(trim((string)$name));
if ($normalizedName === $optionName && $value !== '') {
return ArkHostHetznerVPS_NormalizeLocation($value);
}
}
}
}
// Keep using the original module setting key so existing WHMCS products
// retain their configoption3 value after upgrading.
return ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'datacenter', $default)
);
}
function ArkHostHetznerVPS_API(array $params) {
$url = 'https://api.hetzner.cloud/v1/';
$data = [];
@@ -167,17 +116,17 @@ function ArkHostHetznerVPS_API(array $params) {
break;
case 'Packages':
$url .= 'server_types?per_page=50';
$url .= 'server_types';
$method = 'GET';
break;
case 'Operating Systems':
$url .= 'images?type=system&per_page=50';
$url .= 'images?type=system';
$method = 'GET';
break;
case 'Locations':
$url .= 'locations?per_page=50';
case 'Datacenters':
$url .= 'datacenters';
$method = 'GET';
break;
@@ -211,11 +160,10 @@ function ArkHostHetznerVPS_API(array $params) {
)
);
// Hetzner removed the datacenter request property on 1 July 2026.
// Normalize legacy values such as fsn1-dc14 before sending location.
$location = ArkHostHetznerVPS_GetLocationOption($params);
if ($location) {
$data['location'] = $location;
// Add datacenter if specified
$datacenter = ArkHostHetznerVPS_GetOption($params, 'datacenter');
if ($datacenter) {
$data['datacenter'] = $datacenter;
}
// Handle backups
@@ -228,21 +176,7 @@ function ArkHostHetznerVPS_API(array $params) {
if (ArkHostHetznerVPS_GetOption($params, 'ipv6') === 'on') {
$data['enable_ipv6'] = false;
}
// Handle Cloud-Init user_data if provided
$cloudInitYaml = ArkHostHetznerVPS_GetOption($params, 'cloud_init_yaml');
if ($cloudInitYaml && trim($cloudInitYaml) !== '') {
// Basic YAML validation - check if it starts with #cloud-config
$trimmedYaml = trim($cloudInitYaml);
if (strpos($trimmedYaml, '#cloud-config') !== 0) {
// Auto-prepend #cloud-config if missing
$cloudInitYaml = "#cloud-config\n" . $cloudInitYaml;
}
// Pass as plain text - Hetzner API accepts user_data as plain string (max 32KiB)
$data['user_data'] = $cloudInitYaml;
}
// Handle SSH keys from custom field if needed
// This would need to be implemented with a custom field
break;
@@ -305,7 +239,7 @@ function ArkHostHetznerVPS_API(array $params) {
break;
case 'Operating Systems - Server':
$url .= 'images?type=system&per_page=50';
$url .= 'images?type=system';
$method = 'GET';
break;
@@ -397,11 +331,11 @@ function ArkHostHetznerVPS_API(array $params) {
break;
case 'Create backup':
// Create manual backup (still type=backup, but with distinct description)
// Use snapshots
$url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image';
$method = 'POST';
$data = array(
'description' => 'Manual backup - ' . date('Y-m-d H:i:s'),
'description' => 'Backup created on ' . date('Y-m-d H:i:s'),
'type' => 'backup'
);
break;
@@ -955,7 +889,7 @@ function ArkHostHetznerVPS_API(array $params) {
$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));
@@ -1027,11 +961,9 @@ function ArkHostHetznerVPS_ConfigOptions() {
'Type' => 'dropdown',
'Options' => array(),
),
// Keep the original array key and position for existing WHMCS product
// settings. Only the Hetzner-facing value and admin label have changed.
'datacenter' => array(
'FriendlyName' => 'Location',
'Description' => 'The Hetzner location for the server (Configurable option: Location or legacy datacenter).',
'FriendlyName' => 'Datacenter',
'Description' => 'The datacenter location for the server.',
'Type' => 'dropdown',
'Options' => array(),
),
@@ -1047,7 +979,7 @@ function ArkHostHetznerVPS_ConfigOptions() {
),
'floating_ip_location' => array(
'FriendlyName' => 'Floating IP Location',
'Description' => 'Location for floating IPs (normally the same as the server location).',
'Description' => 'Location for floating IPs (same as datacenter)',
'Type' => 'dropdown',
'Options' => array(),
),
@@ -1056,13 +988,6 @@ function ArkHostHetznerVPS_ConfigOptions() {
'Description' => 'Automatically create a floating IP when provisioning this server (additional cost - billed by Hetzner).',
'Type' => 'yesno',
),
'cloud_init_yaml' => array(
'FriendlyName' => 'Cloud-Init YAML (Optional)',
'Description' => 'Custom cloud-init configuration in YAML format (max 32KiB). Passed as user_data to Hetzner API during server creation. Leave empty to skip cloud-init. <a href="https://docs.hetzner.cloud/#servers-create-a-server" target="_blank">Documentation</a>',
'Type' => 'textarea',
'Rows' => '10',
'Cols' => '60',
),
);
try {
@@ -1136,31 +1061,19 @@ function ArkHostHetznerVPS_ConfigOptions() {
}
}
// Fetch locations. Hetzner's datacenter endpoints are deprecated
// and are scheduled for removal after 1 October 2026.
$params['action'] = 'Locations';
$locations = ArkHostHetznerVPS_API($params);
if (isset($locations['locations'])) {
foreach ($locations['locations'] as $location) {
if (empty($location['name'])) {
continue;
}
$place = array_filter(array(
$location['city'] ?? '',
$location['country'] ?? ''
));
$description = !empty($location['description'])
? $location['description']
: $location['name'];
$label = $description;
if (!empty($place)) {
$label .= ' (' . implode(', ', $place) . ')';
}
$array['datacenter']['Options'][$location['name']] = $label;
$array['floating_ip_location']['Options'][$location['name']] = $label;
// 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']
);
}
}
}
@@ -1241,34 +1154,12 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|VPS ID' => $create['server']['id'],
]);
// Store the primary IP address and login username so they display in WHMCS.
// Without this, the admin service page shows a blank Dedicated IP and the
// auto-generated order username instead of the real VPS login ("root").
$primaryIp = '';
if (!empty($create['server']['public_net']['ipv4']['ip'])) {
$primaryIp = $create['server']['public_net']['ipv4']['ip'];
} elseif (!empty($create['server']['public_net']['ipv6']['ip'])) {
// IPv6-only servers report the network as <addr>/64; strip the prefix length.
$primaryIp = explode('/', $create['server']['public_net']['ipv6']['ip'])[0];
}
$hostingUpdate = array('username' => 'root');
if ($primaryIp !== '') {
$hostingUpdate['dedicatedip'] = $primaryIp;
}
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update($hostingUpdate);
// 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
@@ -1284,9 +1175,7 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
$floatingIpParams = $params;
$floatingIpParams['action'] = 'Create Floating IP';
$floatingIpParams['ip_type'] = $ipType;
$floatingIpParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
);
$floatingIpParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location');
$floatingIpParams['description'] = 'Server ID: ' . $create['server']['id'];
$floatingIpParams['assign_to_server'] = true;
@@ -1438,10 +1327,6 @@ function ArkHostHetznerVPS_VNC(array $params) {
<style>
body { margin: 0; background-color: #000; height: 100vh; display: flex; flex-direction: column; font-family: Arial, sans-serif; }
#top_bar { background-color: #333; color: white; padding: 10px; text-align: center; font-size: 14px; }
#key_bar { background-color: #2a2a2a; padding: 8px 10px; text-align: center; display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
#key_bar button { background-color: #555; color: white; border: 1px solid #777; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-family: Arial, sans-serif; }
#key_bar button:hover { background-color: #777; }
#key_bar button:active { background-color: #999; }
#status { text-align: center; padding: 10px; background: #555; color: white; }
#screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; }
#screen canvas { margin: auto; }
@@ -1458,19 +1343,6 @@ function ArkHostHetznerVPS_VNC(array $params) {
Password: <span style="font-family: monospace; background: #444; padding: 2px 6px; border-radius: 3px;">' . htmlspecialchars($vncPassword) . '</span>
</div>
</div>
<div id="key_bar">
<button onclick="sendCtrlAltDel()">Ctrl + Alt + Del</button>
<button onclick="sendKey(0xFFE3, \'ShiftLeft\')">Shift</button>
<button onclick="sendKey(0xFFE9, \'AltLeft\')">Alt</button>
<button onclick="sendKey(0xFFE1, \'ControlLeft\')">Ctrl</button>
<button onclick="sendKey(0xFF09, \'Tab\')">Tab</button>
<button onclick="sendKey(0xFF0D, \'Enter\')">Enter</button>
<button onclick="sendKey(0xFFFF, \'Delete\')">Del</button>
<button onclick="sendKey(0xFF08, \'Backspace\')">Backspace</button>
<button onclick="sendKey(0xFF1B, \'Escape\')">Esc</button>
<button onclick="sendKey(0xFFE3, \'ShiftLeft\'); sendKey(0xFF52, \'ArrowUp\'); sendKey(0xFFE3, \'ShiftLeft\', false);">Shift+Up</button>
<button onclick="toggleFullscreen()">Fullscreen</button>
</div>
<div id="status">Initializing VNC Client...</div>
<div id="screen"></div>
@@ -1495,33 +1367,6 @@ function ArkHostHetznerVPS_VNC(array $params) {
}
}
window.sendCtrlAltDel = function sendCtrlAltDel() {
if (rfb) {
rfb.sendCtrlAltDel();
rfb.focus();
}
};
window.sendKey = function sendKey(keysym, code, down) {
if (rfb) {
if (down === undefined) {
rfb.sendKey(keysym, code, true);
rfb.sendKey(keysym, code, false);
} else {
rfb.sendKey(keysym, code, down);
}
rfb.focus();
}
};
window.toggleFullscreen = function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
};
function connect() {
updateStatus("Connecting to VNC server...", "warning");
@@ -1644,32 +1489,9 @@ function ArkHostHetznerVPS_AdminLink(array $params) {
if (isset($serverInfo['server'])) {
$server = $serverInfo['server'];
// Self-heal: backfill IP/username for services created before this was
// stored at provisioning time (and keep the IP current after rebuilds).
$primaryIp = '';
if (!empty($server['public_net']['ipv4']['ip'])) {
$primaryIp = $server['public_net']['ipv4']['ip'];
} elseif (!empty($server['public_net']['ipv6']['ip'])) {
$primaryIp = explode('/', $server['public_net']['ipv6']['ip'])[0];
}
if (!empty($params['serviceid'])) {
$hostingUpdate = array();
if ($primaryIp !== '' && ($params['dedicatedip'] ?? '') !== $primaryIp) {
$hostingUpdate['dedicatedip'] = $primaryIp;
}
if (($params['username'] ?? '') !== 'root') {
$hostingUpdate['username'] = 'root';
}
if (!empty($hostingUpdate)) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update($hostingUpdate);
}
}
return '<i class="fa fa-server"></i> Status: ' . $server['status'] . '<br><i class="fa fa-network-wired"></i> IP: ' . ($primaryIp !== '' ? $primaryIp : 'N/A');
return '<i class="fa fa-server"></i> Status: ' . $server['status'] . '<br><i class="fa fa-network-wired"></i> IP: ' . $server['public_net']['ipv4']['ip'];
}
return 'Server ID: ' . $vpsId;
} catch (Exception $err) {
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
@@ -1681,7 +1503,7 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
try {
$action = App::getFromRequest('api');
$actions = array('Server Info', 'Graphs', 'Reinstall', 'Reboot', 'Stop', 'Shutdown', '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');
$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)) {
@@ -1692,14 +1514,24 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
// Check backup permissions
$backupActions = array('Create backup', 'Delete backup', 'Restore backup');
if (in_array($action, $backupActions)) {
// Check if backups are enabled in module settings
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
// 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('jsonResponse' => array(
return array(
'result' => 'error',
'message' => 'Backups are not enabled for this service. Please upgrade your plan to enable backups.'
));
);
}
}
@@ -1759,9 +1591,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
$createParams = $params;
$createParams['action'] = 'Create Floating IP';
$createParams['ip_type'] = 'ipv4';
$createParams['location'] = ArkHostHetznerVPS_NormalizeLocation(
ArkHostHetznerVPS_GetOption($params, 'floating_ip_location')
) ?: ArkHostHetznerVPS_GetLocationOption($params);
$createParams['location'] = ArkHostHetznerVPS_GetOption($params, 'floating_ip_location') ?:
ArkHostHetznerVPS_GetOption($params, 'datacenter');
$createParams['description'] = 'WHMCS Service ID: ' . $params['serviceid'];
$createParams['assign_to_server'] = true;
@@ -1931,8 +1762,7 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
'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'],
'status' => $image['status'] ?? 'available' // Include status from Hetzner API
'type' => $image['type']
);
$backupIndex++;
}
@@ -2074,12 +1904,7 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
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 {
@@ -2116,21 +1941,6 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
$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));
}
@@ -2190,7 +2000,6 @@ function ArkHostHetznerVPS_ClientAreaCustomButtonArray() {
$_LANG['Start'] => 'Start',
$_LANG['Stop'] => 'Stop',
$_LANG['Restart'] => 'Reboot',
$_LANG['Shutdown'] => 'Shutdown',
$_LANG['VNC'] => 'VNC',
);
}
@@ -2239,7 +2048,7 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
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'];
@@ -2387,32 +2196,17 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
// Process operating system info for Hetzner
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
$osId = $serverInfo['image']['name'];
$imageType = $serverInfo['image']['type'] ?? 'system';
// If this is a backup or snapshot image, try to get the actual OS info from os_flavor/os_version
if (($imageType === 'backup' || $imageType === 'snapshot') && isset($serverInfo['image']['os_flavor'])) {
// Use os_flavor and os_version to construct a meaningful OS name
$osName = strtolower($serverInfo['image']['os_flavor']);
$displayName = ucfirst($serverInfo['image']['os_flavor']);
if (isset($serverInfo['image']['os_version']) && !empty($serverInfo['image']['os_version'])) {
$displayName .= ' ' . $serverInfo['image']['os_version'];
}
} else {
$osName = strtolower($serverInfo['image']['description'] ?? $osId);
$displayName = $serverInfo['image']['description'] ?? $osId;
}
// Find matching OS in available images (only for system images)
$osName = strtolower($serverInfo['image']['description'] ?? $osId);
// Find matching OS in available images
$currentOS = null;
if ($imageType === 'system') {
foreach ($operatingSystemsTemp['images'] as $os) {
if ($os['name'] === $osId) {
$currentOS = $os;
break;
}
foreach ($operatingSystemsTemp['images'] as $os) {
if ($os['name'] === $osId) {
$currentOS = $os;
break;
}
}
if ($currentOS) {
$displayName = $currentOS['description'] ?? $currentOS['name'];
$osName = strtolower($displayName);
@@ -2450,15 +2244,10 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
'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 - already have $displayName and $osName from above
// Just ensure they are set
if (!isset($displayName)) {
$displayName = $serverInfo['image']['description'] ?? $osId;
}
if (!isset($osName)) {
$osName = strtolower($displayName);
}
// 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';
@@ -2554,64 +2343,12 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
$serverInfo['bandwidth_used'] = 0; // Not available via API
// Hetzner now returns location directly on the server. Retain a fallback
// for cached/older responses that still contain datacenter.location.
$locationInfo = array();
if (isset($serverInfo['location']) && is_array($serverInfo['location'])) {
$locationInfo = $serverInfo['location'];
} elseif (isset($serverInfo['datacenter']['location']) && is_array($serverInfo['datacenter']['location'])) {
$locationInfo = $serverInfo['datacenter']['location'];
}
// 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';
$city = $locationInfo['city'] ?? '';
$country = $locationInfo['country'] ?? '';
if ($city && $country) {
$locationDisplay = $city . ', ' . $country;
} elseif ($city) {
$locationDisplay = $city;
} elseif ($country) {
$locationDisplay = $country;
} else {
$locationDisplay = 'N/A';
}
$serverInfo['location_display'] = $locationDisplay;
// Preserve the former display key for third-party/custom templates.
$serverInfo['datacenter'] = $locationDisplay;
// 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
$hasPassword = !empty($params['password']) && $params['password'] !== 'managed-via-api';
// Backfill the timestamp for services created before expiry tracking existed.
// Without this, a missing timestamp meant the password was shown forever.
// The service registration date is the best estimate of when it was set.
if ($hasPassword && !$passwordSetTime) {
$regdate = Capsule::table('tblhosting')->where('id', $params['serviceid'])->value('regdate');
$passwordSetTime = ($regdate && $regdate !== '0000-00-00') ? strtotime($regdate) : $currentTime;
$params['model']->serviceProperties->save([
'ArkHostHetznerVPS|Password Set Time' => $passwordSetTime
]);
}
if ($hasPassword && ($currentTime - $passwordSetTime) < $expirationPeriod) {
// Password is still within the 72-hour window - WHMCS already decrypts it for us
$serverInfo['install_root'] = $params['password'];
} else {
// 72-hour window has elapsed: actually remove the install password from our
// systems so the stored copy matches what the client area promises.
if ($hasPassword) {
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(['password' => '']);
}
$serverInfo['install_root'] = '';
}
// Check if backups are enabled in module settings
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
// For now, always show backups tab - the API will handle permissions
$backupsEnabled = true;
return array(
'templatefile' => 'template/clientarea_direct',
@@ -2622,7 +2359,6 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
'token' => generate_token('plain'),
'ADDONLANG' => ArkHostHetznerVPS_Translation(),
'backupsEnabled' => $backupsEnabled,
'productName' => $params['productname'] ?? $params['configoption1'] ?? 'VPS',
)
);
} catch (Exception $err) {
@@ -2700,17 +2436,12 @@ 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);
@@ -1,72 +0,0 @@
<?php
/**
* WHMCS Server Module - Hetzner VPS Hooks
*
* @package WHMCS
* @version 1.2.5
* @copyright Copyright (c) ArkHost 2025
* @author ArkHost <support@arkhost.com>
* @link https://arkhost.com
*/
use WHMCS\Database\Capsule;
/**
* Hide default WHMCS product details and cog icons for ArkHostHetznerVPS products
* Injects CSS early to prevent flash of content
*/
add_hook('ClientAreaHeadOutput', 1, function($vars) {
// Only run on product details page
if (!isset($_GET['action']) || $_GET['action'] !== 'productdetails') {
return '';
}
// Get the service ID from the request
$serviceId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if (!$serviceId) {
return '';
}
// Check if this service uses the ArkHostHetznerVPS module
try {
$service = Capsule::table('tblhosting')
->where('id', $serviceId)
->first();
if (!$service || $service->servertype !== 'ArkHostHetznerVPS') {
return '';
}
} catch (Exception $e) {
return '';
}
// Inject CSS early in the HEAD to prevent flash
// This works for both active and suspended services
return <<<HTML
<style>
/* Hide WHMCS default product details (Domain, Username, Server Name, IP, Visit Website) for ArkHostHetznerVPS */
#domain > .row:nth-child(2),
#domain > .row:nth-child(3),
#domain > .row:nth-child(4) {
display: none !important;
}
#domain > br {
display: none !important;
}
#domain > p {
display: none !important;
}
/* Hide cog icons from custom action buttons IMMEDIATELY */
.list-group-item[href*="modop=custom"] i.fa-cog,
.list-group-item[href*="modop=custom"] i.fas.fa-cog,
.list-group-item[href*="modop=custom"] .fa-cog,
.list-group-item[href*="modop=custom"] i.fa-wrench,
a[href*="modop=custom"] i.fa-cog,
a[href*="modop=custom"] i.fas.fa-cog,
a[href*="modop=custom"] .fa-cog {
display: none !important;
}
</style>
HTML;
});
@@ -69,7 +69,7 @@ $_ADDONLANG['Overview']['ResourceAllocation'] = 'Resource Allocation';
$_ADDONLANG['Overview']['QuickActions'] = 'Quick Actions';
$_ADDONLANG['Overview']['Hostname'] = 'Hostname';
$_ADDONLANG['Overview']['Status'] = 'Status';
$_ADDONLANG['Overview']['OS'] = 'OS';
$_ADDONLANG['Overview']['OS'] = 'Operating System';
$_ADDONLANG['Overview']['Location'] = 'Location';
$_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores';
$_ADDONLANG['Overview']['Memory'] = 'Memory';
@@ -143,7 +143,7 @@ $_ADDONLANG['Settings']['Password']['Submit'] = 'Reset Password';
### Reinstall
$_ADDONLANG['Settings']['Reinstall']['Title'] = 'Reinstall';
$_ADDONLANG['Settings']['Reinstall']['Description'] = 'Please understand that by reinstalling, all the data will be wiped from the server. This action is irreversible!';
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'OS';
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'Operating System';
$_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION';
$_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall';
@@ -13,7 +13,7 @@
<script type="text/javascript">
// Initialize template variables for external script
var productURL = '{$WEB_ROOT}/clientarea.php?action=productdetails&id={$serviceid}';
var serverInfoInitial = JSON.parse('{$serverInfo|json_encode}');
var serverInfoInitial = JSON.parse('{$serverInfo|@json_encode}');
var serviceId = '{$serviceid}';
var csrfToken = '{$token}';
var webRoot = '{$WEB_ROOT}';
@@ -207,11 +207,11 @@
</tr>
<tr>
<td class="text-muted">{$ADDONLANG.Overview.Location}:</td>
<td><i class="fa fa-map-marker-alt mr-1"></i>{$serverInfo['location_display']}</td>
<td><i class="fa fa-map-marker-alt mr-1"></i>{$serverInfo['datacenter']}</td>
</tr>
<tr>
<td class="text-muted">{$ADDONLANG.Overview.ServerType}:</td>
<td><strong>{$productName}</strong></td>
<td><strong>{$serverInfo['server_type']['description']|default:$ADDONLANG.ServerTypes.Standard}</strong></td>
</tr>
</table>
</div>
@@ -756,66 +756,4 @@
</div>
</div>
<script>
// Wait for WHMCS jQuery to be ready and use it
jQuery(document).ready(function($) {
// Inject custom icons into sidebar action buttons
$('.list-group-item').each(function() {
var $link = $(this);
var href = $link.attr('href');
if (!href) return;
// Add custom Font Awesome icons based on action
if (href.indexOf('modop=custom&a=Start') !== -1) {
// Prepend play icon to Start button
$link.html('<i class="fa fa-play"></i> ' + $link.text().trim());
// Intercept click to use our API
$link.off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
ArkHostHetznerVPS_API('Start', true);
return false;
});
} else if (href.indexOf('modop=custom&a=Stop') !== -1) {
// Prepend stop icon to Stop button
$link.html('<i class="fa fa-stop"></i> ' + $link.text().trim());
$link.off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
confirmStop();
return false;
});
} else if (href.indexOf('modop=custom&a=Reboot') !== -1) {
// Prepend sync icon to Restart button
$link.html('<i class="fa fa-sync"></i> ' + $link.text().trim());
$link.off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
confirmRestart();
return false;
});
} else if (href.indexOf('modop=custom&a=Shutdown') !== -1) {
// Prepend power-off icon to Shutdown button
$link.html('<i class="fa fa-power-off"></i> ' + $link.text().trim());
$link.off('click').on('click', function(e) {
e.preventDefault();
e.stopImmediatePropagation();
confirmShutdown();
return false;
});
} else if (href.indexOf('modop=custom&a=VNC') !== -1) {
// Prepend desktop icon to VNC button
$link.html('<i class="fa fa-desktop"></i> ' + $link.text().trim());
// Open VNC in new tab
$link.attr('target', '_blank');
}
});
});
</script>
<script src="{$WEB_ROOT}/modules/servers/ArkHostHetznerVPS/template/script.js"></script>
@@ -224,12 +224,6 @@ function resetRootPassword() {
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 = '<div class="alert alert-success">' +
'<h5>' + lang.newRootPassword + '</h5>' +
@@ -242,7 +236,7 @@ function resetRootPassword() {
'</div>' +
'<p class="mt-2 mb-0"><small>' + lang.savePassword + ' ' + lang.passwordSaved + '</small></p>' +
'</div>';
// Create a modal to show the password
var modal = document.createElement('div');
modal.className = 'arkhost-confirm-overlay show';
@@ -521,10 +515,8 @@ function updateBackupTable(data) {
backupDate = backupDate.split('T')[0];
}
// Determine backup type based on description
// Manual backups have descriptions starting with "Manual backup -"
var backupName = backup.name || '';
var backupType = backupName.indexOf('Manual backup -') === 0 ? lang.backups.manual : lang.backups.automatic;
// 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';
@@ -532,40 +524,22 @@ function updateBackupTable(data) {
sizeStr = backup.size + ' ' + lang.general.gb;
}
// Determine status badge based on size first (more reliable than API status)
var statusBadge;
if (!backup.size || backup.size === 0 || backup.size === '0') {
// Backup is still being created (size not available yet)
statusBadge = '<span class="badge badge-warning">' + lang.backups.creating + '</span>';
} else if (backup.status === 'available' || backup.size > 0) {
// Backup is ready (has a size)
statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>';
} else if (backup.status === 'creating') {
// Status says creating but has size (unlikely but possible)
statusBadge = '<span class="badge badge-warning">' + lang.backups.creating + '</span>';
} else {
// Other statuses (error, etc)
statusBadge = '<span class="badge badge-danger">' + (backup.status || lang.backups.error) + '</span>';
}
// Status is always available for Hetzner backups
var statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>';
// Use backup ID for Hetzner
var backupId = backup.id || backup.file;
// Disable buttons if backup is still being created (size not available)
var isCreating = (!backup.size || backup.size === 0 || backup.size === '0');
var disabledAttr = isCreating ? ' disabled' : '';
var disabledClass = isCreating ? ' disabled' : '';
row.innerHTML =
row.innerHTML =
'<td>' + backupDate + '</td>' +
'<td>' + sizeStr + '</td>' +
'<td>' + backupType + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td style="white-space: nowrap;">' +
'<button class="btn btn-xs btn-primary mr-1' + disabledClass + '" style="padding: 4px 8px; font-size: 12px;" onclick="restoreBackup(\'' + backupId + '\'); return false;" title="' + lang.restore + '"' + disabledAttr + '>' +
'<button class="btn btn-xs btn-primary mr-1" style="padding: 4px 8px; font-size: 12px;" onclick="restoreBackup(\'' + backupId + '\'); return false;" title="' + lang.restore + '">' +
'<i class="fa fa-undo"></i>' +
'</button>' +
'<button class="btn btn-xs btn-danger' + disabledClass + '" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '"' + disabledAttr + '>' +
'<button class="btn btn-xs btn-danger" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '">' +
'<i class="fa fa-trash"></i>' +
'</button>' +
'</td>';
@@ -68,68 +68,6 @@
font-weight: 600;
transition: all 0.3s ease;
}
.arkhost-vps-container .btn-danger,
.arkhost-vps-container button.btn-danger,
.arkhost-vps-container .btn.btn-danger,
.arkhost-vps-container button.btn.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
background-color: #dc3545 !important;
color: white !important;
border: none !important;
border-color: #dc3545 !important;
padding: 10px 20px !important;
border-radius: 12px !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3) !important;
opacity: 1 !important;
}
.arkhost-vps-container .btn-danger:hover,
.arkhost-vps-container button.btn-danger:hover,
.arkhost-vps-container .btn.btn-danger:hover,
.arkhost-vps-container button.btn.btn-danger:hover,
.arkhost-vps-container .btn-danger:focus,
.arkhost-vps-container button.btn-danger:focus {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%) !important;
background-color: #c82333 !important;
color: white !important;
border-color: #c82333 !important;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.5) !important;
transform: translateY(-1px) !important;
opacity: 1 !important;
}
.arkhost-vps-container .btn-danger:active,
.arkhost-vps-container button.btn-danger:active {
background: #bd2130 !important;
background-color: #bd2130 !important;
border-color: #bd2130 !important;
color: white !important;
}
/* Extra specific selectors for theme compatibility (Lagom, etc) */
.arkhost-vps-container .card .btn-danger,
.arkhost-vps-container .card-body .btn-danger,
.arkhost-vps-container .tab-pane .btn-danger,
#ArkHostVPS .btn-danger,
#ArkHostVPS button.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
background-color: #dc3545 !important;
color: #ffffff !important;
border-color: #dc3545 !important;
opacity: 1 !important;
}
.arkhost-vps-container .card .btn-danger:hover,
.arkhost-vps-container .card-body .btn-danger:hover,
.arkhost-vps-container .tab-pane .btn-danger:hover,
#ArkHostVPS .btn-danger:hover,
#ArkHostVPS button.btn-danger:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%) !important;
background-color: #c82333 !important;
color: #ffffff !important;
border-color: #c82333 !important;
opacity: 1 !important;
}
.arkhost-vps-container .head {
border-bottom: 2px solid #3c7fb4;
margin-bottom: 20px;