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 '
+ Node
+ Version
+ Uptime
+ Status
+ IPv4
+ CPU %
+ RAM %
+ ';
+
+ 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 ''.htmlspecialchars($n_name).' ';
+ echo ''.htmlspecialchars($n_version).' ';
+ echo ''.htmlspecialchars($n_uptime).' ';
+ echo ''.htmlspecialchars($n_status).' ';
+ echo ''.htmlspecialchars($serverip).' ';
+ echo ''.$n_cpu_pct.' ';
+ echo ''.$n_mem_pct.' ';
+ echo ' ';
+ }
+ echo '
';
+
+ // -------- Active Guests (running only) --------
+ echo '
Active Guests ';
+ echo '
';
+ echo '
+ Node
+ Type
+ VMID
+ Name
+ Uptime
+ Status
+ CPU %
+ RAM %
+ Disk %
+ ';
+
+ 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 ''.htmlspecialchars($g_node).' ';
+ echo ''.htmlspecialchars($g_type).' ';
+ echo ''.$g_vmid.' ';
+ echo ''.htmlspecialchars($g_name).' ';
+ echo ''.htmlspecialchars($g_uptime).' ';
+ echo ''.htmlspecialchars($g['status']).' ';
+ echo ''.$g_cpu_pct.' ';
+ echo ''.$g_mem_pct.' ';
+ echo ''.$g_dsk_pct.' ';
+ echo ' ';
+ }
+ echo '
';
+
+ 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 '
- Node
- Status
- IPv4
- Uptime
- CPU %
- RAM %
- ';
-
- 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 ''.htmlspecialchars($n_name).' ';
- echo ''.htmlspecialchars($n_status).' ';
- echo ''.htmlspecialchars($serverip).' ';
- echo ''.htmlspecialchars($n_uptime).' ';
- echo ''.$n_cpu_pct.' ';
- echo ''.$n_mem_pct.' ';
- echo ' ';
- }
- echo '
';
-
- // -------- Active Guests (running only) --------
- echo '
Active Guests (running) ';
- echo '
';
- echo '
- Node
- Type
- VMID
- Name
- Uptime
- Status
- CPU %
- RAM %
- Disk %
- ';
-
- 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 ''.htmlspecialchars($g_node).' ';
- echo ''.htmlspecialchars($g_type).' ';
- echo ''.$g_vmid.' ';
- echo ''.htmlspecialchars($g_name).' ';
- echo ''.htmlspecialchars($g_uptime).' ';
- echo ''.htmlspecialchars($g['status']).' ';
- echo ''.$g_cpu_pct.' ';
- echo ''.$g_mem_pct.' ';
- echo ''.$g_dsk_pct.' ';
- echo ' ';
- }
- echo '
';
-
- 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?
- debug_mode=="1" ? "checked" : "").'> Whether or not you want Debug Logging enabled - must also enable WHMCS Module Log (WHMCS debug) via /admin/logs/module-log
+ debug_mode=="1" ? "checked" : "").'> Whether or not you want Debug Logging enabled - must also enable WHMCS Module Log (WHMCS debug) & then view at this link here.
@@ -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 '
+ Node
+ Type
+ Status
+ User
+ Duration
+ Start
+ End
+ ';
+
+ 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 '' . htmlspecialchars($node) . ' ';
+ echo '' . htmlspecialchars($type) . ' ';
+ echo '' . $badge . htmlspecialchars($status) . ' ';
+ echo '' . htmlspecialchars($user) . ' ';
+ echo '' . htmlspecialchars($durH) . ' ';
+ echo '' . htmlspecialchars($start) . ' ';
+ echo '' . htmlspecialchars($end) . ' ';
+ echo ' ';
+ }
+ echo '
';
+ // 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 'VM / CT ';
echo '(VM) QEMU ';
echo '(CT) LXC ';
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