diff --git a/CHANGELOG.md b/CHANGELOG.md index d0203bf..21c7c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,24 @@ All notable changes to Proxmox VE for WHMCS will be documented in this file. ### 💅 Polish - SQL Expansion: Prepare for Nodes/ISOs/TPLs/Logs/SSH Keys/etc -- Client Area: Improved layout and formatting of guest details (#155) - -### 🐛 Bug Fix -- Client Area, Swap %: "NaN%" replaced with "0%" for QEMU cases (#154) https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/milestones +## [1.2.14] - 2025-08-19 - _"Client Area tidy"_ + +### 🚀 Feature +- Cluster Tasks: Show the cluster history in Admin GUI (#50) + +### 💅 Polish +- Admin Area, Server Test: Renamed to "Proxmox VE" for brevity +- Admin Area, Pane Titles: Renamed most panes to make it simpler +- Client Area: Improved layout and formatting of Guest Info (#155) +- Client Area: Improved naming and ordering of Actions menu (#157) +- Client Area: Updated 64x64px icons for Running/Suspended/Offline + +### 🐛 Bug Fix +- Client Area, Swap %: "NaN%" replaced with "0%" for QEMU (#154) + ## [1.2.13] - 2025-08-13 - _"Little Things"_ ### 💅 Polish diff --git a/modules/addons/pvewhmcs/pvewhmcs.php b/modules/addons/pvewhmcs/pvewhmcs.php index 4bd44a7..36b79e9 100644 --- a/modules/addons/pvewhmcs/pvewhmcs.php +++ b/modules/addons/pvewhmcs/pvewhmcs.php @@ -36,7 +36,7 @@ function pvewhmcs_config() { $configarray = array( "name" => "Proxmox VE for WHMCS", "description" => "Proxmox VE (Virtual Environment) & WHMCS, integrated & open-source! Provisioning & Management of VMs/CTs.".is_pvewhmcs_outdated(), - "version" => "1.2.13", + "version" => "1.2.14", "author" => "The Network Crew Pty Ltd", 'language' => 'English' ); @@ -45,7 +45,7 @@ function pvewhmcs_config() { // VERSION: also stored in repo/version (for update-available checker) function pvewhmcs_version(){ - return "1.2.13"; + return "1.2.14"; } // WHMCS MODULE: ACTIVATION of the ADDON MODULE @@ -166,19 +166,20 @@ function pvewhmcs_output($vars) { // Set the active tab based on the GET parameter, default to 'vmplans' if (!isset($_GET['tab'])) { - $_GET['tab'] = 'vmplans'; + $_GET['tab'] = 'nodes'; } // Start the HTML output for the Admin GUI echo '
@@ -204,7 +205,155 @@ function pvewhmcs_output($vars) { save_lxc_plan() ; } - // VM/CT PLANS tab in ADMIN GUI + // NODES / GUESTS tab in ADMIN GUI + echo '
' ; + echo ('

/cluster/resources

'); + + // Fetch all enabled Servers that use pvewhmcs + $servers = Capsule::table('tblservers') + ->where('type', '=', 'pvewhmcs') + ->where('disabled', '=', 0) + ->orderBy('id', 'asc') + ->get(); + + // Catch no-servers case early + if ($servers->isEmpty()) { + echo '
No enabled WHMCS servers found for module type pvewhmcs. Add/enable a server in Setup > Products/Services > Servers.
'; + } else { + foreach ($servers as $srv) { + // Decrypt server password (same approach as ClientArea) + $api_data = array('password2' => $srv->password); + $serverpassword = localAPI('DecryptPassword', $api_data); + $serverip = $srv->ipaddress; + $serverusername = $srv->username; + $serverlabel = !empty($srv->name) ? $srv->name : ('Server #'.$srv->id); + + // Login + get cluster/resources + $proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']); + if (!$proxmox->login()) { + echo '
Unable to log in to PVE API on '.htmlspecialchars($serverip).'. Check credentials / connectivity.
'; + continue; + } + + $cluster_resources = $proxmox->get('/cluster/resources'); // returns nodes, qemu, lxc, storage, pools, etc. + + // Debug logging (same style as ClientArea) + if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) { + logModuleCall( + 'pvewhmcs', + __FUNCTION__, + 'CLUSTER RESOURCES ['.$serverlabel.']:', + json_encode($cluster_resources) + ); + } + + if (!is_array($cluster_resources) || empty($cluster_resources)) { + echo '
No resources returned.
'; + continue; + } + + // Split resources + $nodes = []; + $guests = []; // qemu + lxc + foreach ($cluster_resources as $res) { + if (!isset($res['type'])) { + continue; + } + if ($res['type'] === 'node') { + $nodes[] = $res; + } elseif ($res['type'] === 'qemu' || $res['type'] === 'lxc') { + $guests[] = $res; + } + } + + // -------- Nodes table -------- + echo ''; + echo ' + + + + + + + + '; + + foreach ($nodes as $n) { + $n_cpu_pct = isset($n['cpu']) ? round($n['cpu'] * 100, 2) : 0; + $n_mem_pct = (isset($n['maxmem']) && $n['maxmem'] > 0) + ? intval(($n['mem'] ?? 0) * 100 / $n['maxmem']) + : 0; + $n_uptime = isset($n['uptime']) ? time2format($n['uptime']) : '—'; + $n_status = isset($n['status']) ? $n['status'] : 'unknown'; + $n_name = isset($n['node']) ? $n['node'] : '(node)'; + $n_version = $proxmox->get_version(); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
NodeVersionUptimeStatusIPv4CPU %RAM %
'.htmlspecialchars($n_name).''.htmlspecialchars($n_version).''.htmlspecialchars($n_uptime).''.htmlspecialchars($n_status).''.htmlspecialchars($serverip).''.$n_cpu_pct.''.$n_mem_pct.'
'; + + // -------- Active Guests (running only) -------- + echo '

Active Guests

'; + echo ''; + echo ' + + + + + + + + + + '; + + foreach ($guests as $g) { + // Only running guests for the "active" overview + if (!isset($g['status']) || $g['status'] !== 'running') { + continue; + } + $g_node = $g['node'] ?? '—'; + $g_type = $g['type'] ?? '—'; + $g_vmid = isset($g['vmid']) ? (int)$g['vmid'] : 0; + $g_name = $g['name'] ?? ''; + $g_uptime = isset($g['uptime']) ? time2format($g['uptime']) : '—'; + + $g_cpu_pct = isset($g['cpu']) ? round($g['cpu'] * 100, 2) : 0; + $g_mem_pct = (isset($g['maxmem']) && $g['maxmem'] > 0) + ? intval(($g['mem'] ?? 0) * 100 / $g['maxmem']) + : 0; + $g_dsk_pct = (isset($g['maxdisk']) && $g['maxdisk'] > 0) + ? intval(($g['disk'] ?? 0) * 100 / $g['maxdisk']) + : 0; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
NodeTypeVMIDNameUptimeStatusCPU %RAM %Disk %
'.htmlspecialchars($g_node).''.htmlspecialchars($g_type).''.$g_vmid.''.htmlspecialchars($g_name).''.htmlspecialchars($g_uptime).''.htmlspecialchars($g['status']).''.$g_cpu_pct.''.$g_mem_pct.''.$g_dsk_pct.'
'; + + echo '
'; + } + } + echo '
'; + + // VM / CT PLANS tab in ADMIN GUI echo '
@@ -248,7 +397,7 @@ function pvewhmcs_output($vars) { lxc_plan_add() ; } - // List of VM/CT Plans + // List of VM / CT Plans if ($_GET['action']=='planlist') { echo ' @@ -349,162 +498,16 @@ function pvewhmcs_output($vars) { '; - // NODES / CLUSTER tab in ADMIN GUI - echo '
' ; - echo ('

PVE: /cluster/resources

'); - - // Fetch all enabled servers that use this provisioning module - $servers = Capsule::table('tblservers') - ->where('type', '=', 'pvewhmcs') // module system name - ->where('disabled', '=', 0) - ->orderBy('id', 'asc') - ->get(); - - if ($servers->isEmpty()) { - echo '
No enabled WHMCS servers found for module type pvewhmcs. Add/enable a server in Setup > Products/Services > Servers.
'; - } else { - foreach ($servers as $srv) { - // Decrypt server password (same approach as ClientArea) - $api_data = array('password2' => $srv->password); - $serverpassword = localAPI('DecryptPassword', $api_data); - $serverip = $srv->ipaddress; - $serverusername = $srv->username; - $serverlabel = !empty($srv->name) ? $srv->name : ('Server #'.$srv->id); - - // Login + get cluster/resources - $proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']); - if (!$proxmox->login()) { - echo '
Unable to log in to PVE API on '.htmlspecialchars($serverip).'. Check credentials / connectivity.
'; - continue; - } - - $cluster_resources = $proxmox->get('/cluster/resources'); // returns nodes, qemu, lxc, storage, pools, etc. - - // Debug logging (same style as ClientArea) - if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) { - logModuleCall( - 'pvewhmcs', - __FUNCTION__, - 'CLUSTER RESOURCES ['.$serverlabel.']:', - json_encode($cluster_resources) - ); - } - - if (!is_array($cluster_resources) || empty($cluster_resources)) { - echo '
No resources returned.
'; - continue; - } - - // Split resources - $nodes = []; - $guests = []; // qemu + lxc - foreach ($cluster_resources as $res) { - if (!isset($res['type'])) { - continue; - } - if ($res['type'] === 'node') { - $nodes[] = $res; - } elseif ($res['type'] === 'qemu' || $res['type'] === 'lxc') { - $guests[] = $res; - } - } - - // -------- Nodes table -------- - echo '
'; - echo ' - - - - - - - '; - - foreach ($nodes as $n) { - $n_cpu_pct = isset($n['cpu']) ? round($n['cpu'] * 100, 2) : 0; - $n_mem_pct = (isset($n['maxmem']) && $n['maxmem'] > 0) - ? intval(($n['mem'] ?? 0) * 100 / $n['maxmem']) - : 0; - $n_uptime = isset($n['uptime']) ? time2format($n['uptime']) : '—'; - $n_status = isset($n['status']) ? $n['status'] : 'unknown'; - $n_name = isset($n['node']) ? $n['node'] : '(node)'; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
NodeStatusIPv4UptimeCPU %RAM %
'.htmlspecialchars($n_name).''.htmlspecialchars($n_status).''.htmlspecialchars($serverip).''.htmlspecialchars($n_uptime).''.$n_cpu_pct.''.$n_mem_pct.'
'; - - // -------- Active Guests (running only) -------- - echo '

Active Guests (running)

'; - echo ''; - echo ' - - - - - - - - - - '; - - foreach ($guests as $g) { - // Only running guests for the "active" overview - if (!isset($g['status']) || $g['status'] !== 'running') { - continue; - } - $g_node = $g['node'] ?? '—'; - $g_type = $g['type'] ?? '—'; - $g_vmid = isset($g['vmid']) ? (int)$g['vmid'] : 0; - $g_name = $g['name'] ?? ''; - $g_uptime = isset($g['uptime']) ? time2format($g['uptime']) : '—'; - - $g_cpu_pct = isset($g['cpu']) ? round($g['cpu'] * 100, 2) : 0; - $g_mem_pct = (isset($g['maxmem']) && $g['maxmem'] > 0) - ? intval(($g['mem'] ?? 0) * 100 / $g['maxmem']) - : 0; - $g_dsk_pct = (isset($g['maxdisk']) && $g['maxdisk'] > 0) - ? intval(($g['disk'] ?? 0) * 100 / $g['maxdisk']) - : 0; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
NodeTypeVMIDNameUptimeStatusCPU %RAM %Disk %
'.htmlspecialchars($g_node).''.htmlspecialchars($g_type).''.$g_vmid.''.htmlspecialchars($g_name).''.htmlspecialchars($g_uptime).''.htmlspecialchars($g['status']).''.$g_cpu_pct.''.$g_mem_pct.''.$g_dsk_pct.'
'; - - echo '
'; - } - } - echo '
'; - - // ACTIONS / LOGS tab in ADMIN GUI + // ACTIONS tab in ADMIN GUI echo '
' ; - echo ('

WHMCS: Module Logging

'); - echo ('Click here
(Module Config > Debug Mode = ON)'); echo ('

Module: Action History

'); echo ('Coming in v1.3.x'); echo ('

Module: Failed Actions

'); echo ('Coming in v1.3.x
View the milestones/versions on GitHub'); echo '
'; - // SUPPORT / HEALTH tab in ADMIN GUI - echo ('
') ; + // SUPPORT tab in ADMIN GUI + echo ('
') ; echo ('❤️ Proxmox for WHMCS is open-source and free to use & improve on!
https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/

'); echo ('Your 5-star review on WHMCS Marketplace will help the module grow!
*****: https://marketplace.whmcs.com/product/6935-proxmox-ve-for-whmcs

'); echo ('

System Environment

Proxmox VE for WHMCS v' . pvewhmcs_version() . ' (GitHub reports latest as v' . get_pvewhmcs_latest_version() . ')' . '
PHP v' . phpversion() . ' running on ' . $_SERVER['SERVER_SOFTWARE'] . ' Web Server (' . $_SERVER['SERVER_NAME'] . ')

'); @@ -531,10 +534,10 @@ function pvewhmcs_output($vars) { - Debugging? + Debug? @@ -545,9 +548,103 @@ function pvewhmcs_output($vars) {
'; - echo '
'; + // LOGS tab in ADMIN GUI + echo '
'; + echo '

Cluster History

'; + + try { + // If a client exists already, reuse it; else initialise once from the first enabled pvewhmcs server + if (!isset($proxmox)) { + $srv = Capsule::table('tblservers') + ->where('type', 'pvewhmcs') + ->where('disabled', 0) + ->orderBy('id', 'asc') + ->first(); + + if (!$srv) { + throw new Exception('No enabled WHMCS server found for module type pvewhmcs.'); + } + + $dec = localAPI('DecryptPassword', ['password2' => $srv->password]); + $serverpassword = $dec['password'] ?? ''; + if (!$serverpassword) { + throw new Exception('Could not decrypt Proxmox server password.'); + } + + $proxmox = new PVE2_API($srv->ipaddress, $srv->username, 'pam', $serverpassword); + if (!$proxmox->login()) { + throw new Exception('Login to Proxmox API failed.'); + } + } + + // Fetch recent cluster-wide tasks once + $limit = 150; + $tasks = $proxmox->get('/cluster/tasks', ['limit' => $limit]); + + // Optional debug logging + if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) { + logModuleCall('pvewhmcs', 'ADMIN LOGS: /cluster/tasks', 'limit=' . $limit, json_encode($tasks)); + } + + if (!is_array($tasks) || empty($tasks)) { + echo '
No recent cluster tasks were returned.
'; + } else { + // Sort newest first (defensive) + usort($tasks, function ($a, $b) { + return (intval($b['starttime'] ?? 0)) <=> (intval($a['starttime'] ?? 0)); + }); + + echo ''; + echo ' + + + + + + + + '; + + foreach ($tasks as $t) { + $node = $t['node'] ?? '—'; + $type = $t['type'] ?? ''; + $user = $t['user'] ?? ''; + $startTs = (int)($t['starttime'] ?? 0); + $endTs = isset($t['endtime']) ? (int)$t['endtime'] : null; + + $start = $startTs ? date('Y-m-d H:i:s', $startTs) : '—'; + $end = $endTs ? date('Y-m-d H:i:s', $endTs) : '—'; + + $durSec = $startTs ? (is_null($endTs) ? (time() - $startTs) : max(0, $endTs - $startTs)) : null; + $durH = is_null($durSec) + ? '—' + : sprintf('%02d:%02d:%02d', intdiv($durSec, 3600), intdiv($durSec % 3600, 60), $durSec % 60); + + $status = $t['status'] ?? (is_null($endTs) ? 'running' : ''); + $badge = ($status === 'OK') + ? '✅' + : ((preg_match('/(error|fail|aborted|unknown)/i', (string)$status)) ? '❌' : '⏳'); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
NodeTypeStatusUserDurationStartEnd
' . htmlspecialchars($node) . '' . htmlspecialchars($type) . '' . $badge . htmlspecialchars($status) . '' . htmlspecialchars($user) . '' . htmlspecialchars($durH) . '' . htmlspecialchars($start) . '' . htmlspecialchars($end) . '
'; + // Always close the tab-pane div + echo '
'; + } + } catch (Throwable $e) { + echo '
Could not retrieve PVE Cluster history: ' + . htmlspecialchars($e->getMessage()) . '
'; + } echo '
'; // End of tabbed content @@ -670,7 +767,7 @@ function import_guest() { echo ''; // Guest Type dropdown - echo 'VM/CT'; echo ''; echo ''; echo ''; diff --git a/modules/servers/pvewhmcs/img/stopped.png b/modules/servers/pvewhmcs/img/stopped.png index 8271acd..cf7e18b 100644 Binary files a/modules/servers/pvewhmcs/img/stopped.png and b/modules/servers/pvewhmcs/img/stopped.png differ diff --git a/modules/servers/pvewhmcs/pvewhmcs.php b/modules/servers/pvewhmcs/pvewhmcs.php index 8a3bb79..c04d8c7 100644 --- a/modules/servers/pvewhmcs/pvewhmcs.php +++ b/modules/servers/pvewhmcs/pvewhmcs.php @@ -47,8 +47,7 @@ function pvewhmcs_MetaData() { * AdminLink: show a direct link to the Proxmox UI on :8006. * Falls back to server IP if hostname is empty. */ -function pvewhmcs_AdminLink(array $params) -{ +function pvewhmcs_AdminLink(array $params) { $host = $params['serverhostname'] ?: $params['serverip']; if (!$host) { // Nothing to link to – return the module page as a safe fallback @@ -644,7 +643,7 @@ function pvewhmcs_TerminateAccount(array $params) { $guest=Capsule::table('mod_pvewhmcs_vms')->where('id', '=', $params['serviceid'])->get()[0]; $pve_cmdparam = array(); // Stop the service if it is not already stopped - $guest_specific = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid.'/status/current'); + $guest_specific = $proxmox->get('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/current'); if ($guest_specific['status'] != 'stopped') { $proxmox->post('/nodes/' . $first_node . '/' . $guest->vtype . '/' . $guest->vmid . '/status/stop' , $pve_cmdparam); sleep(30); diff --git a/version b/version index 9579e1f..d79a5f8 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.2.13 \ No newline at end of file +1.2.14 \ No newline at end of file