4 Commits
Author SHA1 Message Date
Yuri Karamian 26bdabc534 v1.2.4: fix IP/username display and 72h password removal
- Store IP on the service at provisioning + backfill existing services
- Set service username to root to match the VPS login
- Actually delete the install password after the 72h window (was still
  visible via the eye icon, including legacy services without a timestamp)

Fixes #16
2026-06-22 01:13:55 +02:00
Yuri Karamian 815d1a442b v1.2.3 2026-05-14 22:57:14 +02:00
Yuri 372bff51eb lagom theme css fix 2025-11-13 16:17:16 +01:00
Yuri feb2c7c805 v1.2.1-fix 2025-11-13 13:38:15 +01:00
7 changed files with 272 additions and 76 deletions
+13
View File
@@ -340,6 +340,19 @@ Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://ar
## Changelog ## 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 ### v1.2.1
**Added:** **Added:**
- Shutdown button functionality (graceful server shutdown) - Shutdown button functionality (graceful server shutdown)
@@ -3,7 +3,7 @@
* WHMCS Server Module - Hetzner VPS * WHMCS Server Module - Hetzner VPS
* *
* @package WHMCS * @package WHMCS
* @version 1.2.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;
@@ -1540,35 +1624,8 @@ 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 not enabled in WHMCS settings, check if backups are actually enabled on Hetzner's side
if (!$backupsEnabled) {
try {
$serverInfoParams = $params;
$serverInfoParams['action'] = 'Server Info';
$serverInfoResult = ArkHostHetznerVPS_API($serverInfoParams);
// Check if the server has backups enabled (indicated by backup_window being set)
if (isset($serverInfoResult['server']['backup_window']) && $serverInfoResult['server']['backup_window'] !== null) {
$backupsEnabled = true;
}
} catch (Exception $e) {
// If we can't check, assume not enabled
$backupsEnabled = false;
}
}
if (!$backupsEnabled) { if (!$backupsEnabled) {
return array('jsonResponse' => array( return array('jsonResponse' => array(
@@ -1805,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++;
} }
@@ -2260,14 +2318,29 @@ 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;
foreach ($operatingSystemsTemp['images'] as $os) { if ($imageType === 'system') {
if ($os['name'] === $osId) { foreach ($operatingSystemsTemp['images'] as $os) {
$currentOS = $os; if ($os['name'] === $osId) {
break; $currentOS = $os;
break;
}
} }
} }
@@ -2308,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
$displayName = $serverInfo['image']['description'] ?? $osId; // Just ensure they are set
$osName = strtolower($displayName); if (!isset($displayName)) {
$displayName = $serverInfo['image']['description'] ?? $osId;
}
if (!isset($osName)) {
$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) {
@@ -2407,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) {
$serverInfo['install_root'] = $params['password']; $regdate = Capsule::table('tblhosting')->where('id', $params['serviceid'])->value('regdate');
} else { $passwordSetTime = ($regdate && $regdate !== '0000-00-00') ? strtotime($regdate) : $currentTime;
// Password has expired (timestamp exists and is older than 72 hours) $params['model']->serviceProperties->save([
$serverInfo['install_root'] = ''; '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 { } 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'] = ''; $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',
@@ -2443,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) {
+1 -1
View File
@@ -3,7 +3,7 @@
* WHMCS Server Module - Hetzner VPS Hooks * WHMCS Server Module - Hetzner VPS Hooks
* *
* @package WHMCS * @package WHMCS
* @version 1.2.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
@@ -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>
@@ -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;