mirror of
https://gitlab.com/ArkHost/WHMCS-ArkHost-HetznerVPS.git
synced 2026-07-24 15:55:53 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26bdabc534 | ||
|
|
815d1a442b | ||
|
|
372bff51eb | ||
|
|
feb2c7c805 | ||
|
|
378b3a0ba3 |
@@ -39,14 +39,15 @@ WHMCS server module for Hetzner Cloud VPS management.
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Upload files to `/path/to/whmcs/modules/servers/ArkHostHetznerVPS/`
|
1. Upload files to `/path/to/whmcs/modules/servers/ArkHostHetznerVPS/`
|
||||||
2. Create server and add it to a server group:
|
2. Upload `hooks.php` to `/path/to/whmcs/includes/hooks/` (for enhanced UI)
|
||||||
|
3. Create server and add it to a server group:
|
||||||
- Setup → Products/Services → Servers → Create New Group
|
- Setup → Products/Services → Servers → Create New Group
|
||||||
- Name: `Hetzner Cloud`
|
- Name: `Hetzner Cloud`
|
||||||
- Type: `ArkHostHetznerVPS`
|
- Type: `ArkHostHetznerVPS`
|
||||||
- Hostname: `localhost`
|
- Hostname: `localhost`
|
||||||
- Username: Hetzner Project ID (optional)
|
- Username: Hetzner Project ID (optional)
|
||||||
- Password: Hetzner Cloud API Token
|
- Password: Hetzner Cloud API Token
|
||||||
3. Create products:
|
4. Create products:
|
||||||
- Setup → Products/Services → Products/Services → Create New Product
|
- Setup → Products/Services → Products/Services → Create New Product
|
||||||
- Type: `VPS/Dedicated Server`
|
- Type: `VPS/Dedicated Server`
|
||||||
- Module: `ArkHostHetznerVPS`
|
- Module: `ArkHostHetznerVPS`
|
||||||
@@ -337,4 +338,73 @@ Free and open source. Commercial support available.
|
|||||||
|
|
||||||
Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://arkhost.com/whmcs-modules.php "https://arkhost.com/whmcs-modules.php")
|
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.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
|
© 2025 ArkHost
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* WHMCS Server Module - Hetzner VPS
|
* WHMCS Server Module - Hetzner VPS
|
||||||
*
|
*
|
||||||
* @package WHMCS
|
* @package WHMCS
|
||||||
* @version 1.1.1
|
* @version 1.2.4
|
||||||
* @copyright Copyright (c) ArkHost 2025
|
* @copyright Copyright (c) ArkHost 2025
|
||||||
* @author ArkHost <support@arkhost.com>
|
* @author ArkHost <support@arkhost.com>
|
||||||
* @link https://arkhost.com
|
* @link https://arkhost.com
|
||||||
@@ -345,11 +345,11 @@ function ArkHostHetznerVPS_API(array $params) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Create backup':
|
case 'Create backup':
|
||||||
// Use snapshots
|
// Create manual backup (still type=backup, but with distinct description)
|
||||||
$url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image';
|
$url .= 'servers/' . ArkHostHetznerVPS_GetVPSID($params) . '/actions/create_image';
|
||||||
$method = 'POST';
|
$method = 'POST';
|
||||||
$data = array(
|
$data = array(
|
||||||
'description' => 'Backup created on ' . date('Y-m-d H:i:s'),
|
'description' => 'Manual backup - ' . date('Y-m-d H:i:s'),
|
||||||
'type' => 'backup'
|
'type' => 'backup'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -1176,6 +1176,23 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
|
|||||||
'ArkHostHetznerVPS|VPS ID' => $create['server']['id'],
|
'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
|
// Store the root password if provided
|
||||||
if (isset($create['root_password'])) {
|
if (isset($create['root_password'])) {
|
||||||
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
||||||
@@ -1353,6 +1370,10 @@ function ArkHostHetznerVPS_VNC(array $params) {
|
|||||||
<style>
|
<style>
|
||||||
body { margin: 0; background-color: #000; height: 100vh; display: flex; flex-direction: column; font-family: Arial, sans-serif; }
|
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; }
|
#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; }
|
#status { text-align: center; padding: 10px; background: #555; color: white; }
|
||||||
#screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; }
|
#screen { flex: 1; display: flex; width: 100%; height: 100%; background: #000; position: relative; }
|
||||||
#screen canvas { margin: auto; }
|
#screen canvas { margin: auto; }
|
||||||
@@ -1369,6 +1390,19 @@ function ArkHostHetznerVPS_VNC(array $params) {
|
|||||||
Password: <span style="font-family: monospace; background: #444; padding: 2px 6px; border-radius: 3px;">' . htmlspecialchars($vncPassword) . '</span>
|
Password: <span style="font-family: monospace; background: #444; padding: 2px 6px; border-radius: 3px;">' . htmlspecialchars($vncPassword) . '</span>
|
||||||
</div>
|
</div>
|
||||||
</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="status">Initializing VNC Client...</div>
|
||||||
<div id="screen"></div>
|
<div id="screen"></div>
|
||||||
@@ -1393,6 +1427,33 @@ 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() {
|
function connect() {
|
||||||
updateStatus("Connecting to VNC server...", "warning");
|
updateStatus("Connecting to VNC server...", "warning");
|
||||||
|
|
||||||
@@ -1515,7 +1576,30 @@ function ArkHostHetznerVPS_AdminLink(array $params) {
|
|||||||
|
|
||||||
if (isset($serverInfo['server'])) {
|
if (isset($serverInfo['server'])) {
|
||||||
$server = $serverInfo['server'];
|
$server = $serverInfo['server'];
|
||||||
return '<i class="fa fa-server"></i> Status: ' . $server['status'] . '<br><i class="fa fa-network-wired"></i> IP: ' . $server['public_net']['ipv4']['ip'];
|
|
||||||
|
// 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 'Server ID: ' . $vpsId;
|
return 'Server ID: ' . $vpsId;
|
||||||
@@ -1529,7 +1613,7 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
|||||||
try {
|
try {
|
||||||
$action = App::getFromRequest('api');
|
$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');
|
$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');
|
||||||
$results = array('result' => 'success');
|
$results = array('result' => 'success');
|
||||||
|
|
||||||
if (in_array($action, $actions)) {
|
if (in_array($action, $actions)) {
|
||||||
@@ -1540,24 +1624,14 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
|||||||
// Check backup permissions
|
// Check backup permissions
|
||||||
$backupActions = array('Create backup', 'Delete backup', 'Restore backup');
|
$backupActions = array('Create backup', 'Delete backup', 'Restore backup');
|
||||||
if (in_array($action, $backupActions)) {
|
if (in_array($action, $backupActions)) {
|
||||||
// Check if backups are enabled either via module settings or configurable options
|
// Check if backups are enabled in module settings
|
||||||
$backupsEnabled = false;
|
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
|
||||||
|
|
||||||
// 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) {
|
if (!$backupsEnabled) {
|
||||||
return array(
|
return array('jsonResponse' => array(
|
||||||
'result' => 'error',
|
'result' => 'error',
|
||||||
'message' => 'Backups are not enabled for this service. Please upgrade your plan to enable backups.'
|
'message' => 'Backups are not enabled for this service. Please upgrade your plan to enable backups.'
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1788,7 +1862,8 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
|||||||
'name' => $image['description'] ?? $image['name'],
|
'name' => $image['description'] ?? $image['name'],
|
||||||
'created' => $image['created'],
|
'created' => $image['created'],
|
||||||
'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API
|
'size' => isset($image['image_size']) ? round($image['image_size'], 2) : 0, // Already in GB from API
|
||||||
'type' => $image['type']
|
'type' => $image['type'],
|
||||||
|
'status' => $image['status'] ?? 'available' // Include status from Hetzner API
|
||||||
);
|
);
|
||||||
$backupIndex++;
|
$backupIndex++;
|
||||||
}
|
}
|
||||||
@@ -2046,6 +2121,7 @@ function ArkHostHetznerVPS_ClientAreaCustomButtonArray() {
|
|||||||
$_LANG['Start'] => 'Start',
|
$_LANG['Start'] => 'Start',
|
||||||
$_LANG['Stop'] => 'Stop',
|
$_LANG['Stop'] => 'Stop',
|
||||||
$_LANG['Restart'] => 'Reboot',
|
$_LANG['Restart'] => 'Reboot',
|
||||||
|
$_LANG['Shutdown'] => 'Shutdown',
|
||||||
$_LANG['VNC'] => 'VNC',
|
$_LANG['VNC'] => 'VNC',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2242,16 +2318,31 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
|||||||
// Process operating system info for Hetzner
|
// Process operating system info for Hetzner
|
||||||
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
|
if (isset($serverInfo['image']) && !empty($operatingSystemsTemp['images'])) {
|
||||||
$osId = $serverInfo['image']['name'];
|
$osId = $serverInfo['image']['name'];
|
||||||
$osName = strtolower($serverInfo['image']['description'] ?? $osId);
|
$imageType = $serverInfo['image']['type'] ?? 'system';
|
||||||
|
|
||||||
// Find matching OS in available images
|
// 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)
|
||||||
$currentOS = null;
|
$currentOS = null;
|
||||||
|
if ($imageType === 'system') {
|
||||||
foreach ($operatingSystemsTemp['images'] as $os) {
|
foreach ($operatingSystemsTemp['images'] as $os) {
|
||||||
if ($os['name'] === $osId) {
|
if ($os['name'] === $osId) {
|
||||||
$currentOS = $os;
|
$currentOS = $os;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($currentOS) {
|
if ($currentOS) {
|
||||||
$displayName = $currentOS['description'] ?? $currentOS['name'];
|
$displayName = $currentOS['description'] ?? $currentOS['name'];
|
||||||
@@ -2290,9 +2381,14 @@ 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'))
|
'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 {
|
} else {
|
||||||
// Fallback if OS not found - use image info from server
|
// 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;
|
$displayName = $serverInfo['image']['description'] ?? $osId;
|
||||||
|
}
|
||||||
|
if (!isset($osName)) {
|
||||||
$osName = strtolower($displayName);
|
$osName = strtolower($displayName);
|
||||||
|
}
|
||||||
|
|
||||||
// Try to determine OS type from name
|
// Try to determine OS type from name
|
||||||
if (strpos($osName, 'alma') !== false) {
|
if (strpos($osName, 'alma') !== false) {
|
||||||
@@ -2389,32 +2485,54 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
|||||||
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
|
$serverInfo['bandwidth'] = 20480; // 20TB in GB (20 * 1024)
|
||||||
$serverInfo['bandwidth_used'] = 0; // Not available via API
|
$serverInfo['bandwidth_used'] = 0; // Not available via API
|
||||||
|
|
||||||
// Add datacenter info
|
// Add datacenter info - format as "City, Country"
|
||||||
$serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A';
|
$city = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : '';
|
||||||
$serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A';
|
$country = isset($serverInfo['datacenter']['location']['country']) ? $serverInfo['datacenter']['location']['country'] : '';
|
||||||
|
|
||||||
|
if ($city && $country) {
|
||||||
|
$serverInfo['datacenter'] = $city . ', ' . $country;
|
||||||
|
} elseif ($city) {
|
||||||
|
$serverInfo['datacenter'] = $city;
|
||||||
|
} elseif ($country) {
|
||||||
|
$serverInfo['datacenter'] = $country;
|
||||||
|
} else {
|
||||||
|
$serverInfo['datacenter'] = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverInfo['location'] = $city ?: 'N/A';
|
||||||
|
|
||||||
// Get root password with expiration check (72 hours)
|
// Get root password with expiration check (72 hours)
|
||||||
$passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time');
|
$passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time');
|
||||||
$currentTime = time();
|
$currentTime = time();
|
||||||
$expirationPeriod = 72 * 3600; // 72 hours in seconds
|
$expirationPeriod = 72 * 3600; // 72 hours in seconds
|
||||||
|
|
||||||
if (!empty($params['password']) && $params['password'] !== 'managed-via-api') {
|
$hasPassword = !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
|
// Backfill the timestamp for services created before expiry tracking existed.
|
||||||
if (!$passwordSetTime || ($currentTime - $passwordSetTime) < $expirationPeriod) {
|
// Without this, a missing timestamp meant the password was shown forever.
|
||||||
// Password is still valid - WHMCS already decrypts it for us in $params
|
// The service registration date is the best estimate of when it was set.
|
||||||
// No need to call decrypt() as $params['password'] is already plain text
|
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'];
|
$serverInfo['install_root'] = $params['password'];
|
||||||
} else {
|
} else {
|
||||||
// Password has expired (timestamp exists and is older than 72 hours)
|
// 72-hour window has elapsed: actually remove the install password from our
|
||||||
$serverInfo['install_root'] = '';
|
// systems so the stored copy matches what the client area promises.
|
||||||
|
if ($hasPassword) {
|
||||||
|
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update(['password' => '']);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$serverInfo['install_root'] = '';
|
$serverInfo['install_root'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, always show backups tab - the API will handle permissions
|
// Check if backups are enabled in module settings
|
||||||
$backupsEnabled = true;
|
$backupsEnabled = (ArkHostHetznerVPS_GetOption($params, 'backups') === 'on');
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'templatefile' => 'template/clientarea_direct',
|
'templatefile' => 'template/clientarea_direct',
|
||||||
@@ -2425,6 +2543,7 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
|||||||
'token' => generate_token('plain'),
|
'token' => generate_token('plain'),
|
||||||
'ADDONLANG' => ArkHostHetznerVPS_Translation(),
|
'ADDONLANG' => ArkHostHetznerVPS_Translation(),
|
||||||
'backupsEnabled' => $backupsEnabled,
|
'backupsEnabled' => $backupsEnabled,
|
||||||
|
'productName' => $params['productname'] ?? $params['configoption1'] ?? 'VPS',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WHMCS Server Module - Hetzner VPS Hooks
|
||||||
|
*
|
||||||
|
* @package WHMCS
|
||||||
|
* @version 1.2.4
|
||||||
|
* @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']['QuickActions'] = 'Quick Actions';
|
||||||
$_ADDONLANG['Overview']['Hostname'] = 'Hostname';
|
$_ADDONLANG['Overview']['Hostname'] = 'Hostname';
|
||||||
$_ADDONLANG['Overview']['Status'] = 'Status';
|
$_ADDONLANG['Overview']['Status'] = 'Status';
|
||||||
$_ADDONLANG['Overview']['OS'] = 'Operating System';
|
$_ADDONLANG['Overview']['OS'] = 'OS';
|
||||||
$_ADDONLANG['Overview']['Location'] = 'Location';
|
$_ADDONLANG['Overview']['Location'] = 'Location';
|
||||||
$_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores';
|
$_ADDONLANG['Overview']['CPU_Cores'] = 'vCPU Cores';
|
||||||
$_ADDONLANG['Overview']['Memory'] = 'Memory';
|
$_ADDONLANG['Overview']['Memory'] = 'Memory';
|
||||||
@@ -143,7 +143,7 @@ $_ADDONLANG['Settings']['Password']['Submit'] = 'Reset Password';
|
|||||||
### Reinstall
|
### Reinstall
|
||||||
$_ADDONLANG['Settings']['Reinstall']['Title'] = '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']['Description'] = 'Please understand that by reinstalling, all the data will be wiped from the server. This action is irreversible!';
|
||||||
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'Operating System';
|
$_ADDONLANG['Settings']['Reinstall']['OS'] = 'OS';
|
||||||
$_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION';
|
$_ADDONLANG['Settings']['Reinstall']['Version'] = 'CHOOSE VERSION';
|
||||||
$_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall';
|
$_ADDONLANG['Settings']['Reinstall']['Submit'] = 'Reinstall';
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Initialize template variables for external script
|
// Initialize template variables for external script
|
||||||
var productURL = '{$WEB_ROOT}/clientarea.php?action=productdetails&id={$serviceid}';
|
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 serviceId = '{$serviceid}';
|
||||||
var csrfToken = '{$token}';
|
var csrfToken = '{$token}';
|
||||||
var webRoot = '{$WEB_ROOT}';
|
var webRoot = '{$WEB_ROOT}';
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted">{$ADDONLANG.Overview.ServerType}:</td>
|
<td class="text-muted">{$ADDONLANG.Overview.ServerType}:</td>
|
||||||
<td><strong>{$serverInfo['server_type']['description']|default:$ADDONLANG.ServerTypes.Standard}</strong></td>
|
<td><strong>{$productName}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -756,4 +756,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script src="{$WEB_ROOT}/modules/servers/ArkHostHetznerVPS/template/script.js"></script>
|
||||||
|
|||||||
@@ -521,8 +521,10 @@ function updateBackupTable(data) {
|
|||||||
backupDate = backupDate.split('T')[0];
|
backupDate = backupDate.split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine backup type
|
// Determine backup type based on description
|
||||||
var backupType = backup.type === 'backup' ? lang.backups.automatic : lang.backups.manual;
|
// 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;
|
||||||
|
|
||||||
// Format size (already in GB from API)
|
// Format size (already in GB from API)
|
||||||
var sizeStr = 'N/A';
|
var sizeStr = 'N/A';
|
||||||
@@ -530,22 +532,40 @@ function updateBackupTable(data) {
|
|||||||
sizeStr = backup.size + ' ' + lang.general.gb;
|
sizeStr = backup.size + ' ' + lang.general.gb;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status is always available for Hetzner backups
|
// Determine status badge based on size first (more reliable than API status)
|
||||||
var statusBadge = '<span class="badge badge-success">' + lang.backups.available + '</span>';
|
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>';
|
||||||
|
}
|
||||||
|
|
||||||
// Use backup ID for Hetzner
|
// Use backup ID for Hetzner
|
||||||
var backupId = backup.id || backup.file;
|
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>' + backupDate + '</td>' +
|
||||||
'<td>' + sizeStr + '</td>' +
|
'<td>' + sizeStr + '</td>' +
|
||||||
'<td>' + backupType + '</td>' +
|
'<td>' + backupType + '</td>' +
|
||||||
'<td>' + statusBadge + '</td>' +
|
'<td>' + statusBadge + '</td>' +
|
||||||
'<td style="white-space: nowrap;">' +
|
'<td style="white-space: nowrap;">' +
|
||||||
'<button class="btn btn-xs btn-primary mr-1" style="padding: 4px 8px; font-size: 12px;" onclick="restoreBackup(\'' + backupId + '\'); return false;" title="' + lang.restore + '">' +
|
'<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 + '>' +
|
||||||
'<i class="fa fa-undo"></i>' +
|
'<i class="fa fa-undo"></i>' +
|
||||||
'</button>' +
|
'</button>' +
|
||||||
'<button class="btn btn-xs btn-danger" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '">' +
|
'<button class="btn btn-xs btn-danger' + disabledClass + '" style="padding: 4px 8px; font-size: 12px;" onclick="deleteBackup(\'' + backupId + '\'); return false;" title="' + lang.delete + '"' + disabledAttr + '>' +
|
||||||
'<i class="fa fa-trash"></i>' +
|
'<i class="fa fa-trash"></i>' +
|
||||||
'</button>' +
|
'</button>' +
|
||||||
'</td>';
|
'</td>';
|
||||||
|
|||||||
@@ -68,6 +68,68 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
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 {
|
.arkhost-vps-container .head {
|
||||||
border-bottom: 2px solid #3c7fb4;
|
border-bottom: 2px solid #3c7fb4;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user