Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
719735b81c | ||
|
|
ef7752ab89 | ||
|
|
6186b9f896 | ||
|
|
b86ba210f2 | ||
|
|
1b56f4ac8e |
12
CHANGELOG.md
@@ -1,9 +1,17 @@
|
||||
# Changelog
|
||||
All notable changes to Proxmox VE for WHMCS will be documented in this file.
|
||||
|
||||
## [1.3.x] - TBC 2026-??-??
|
||||
## [1.3.0] - 2025-12-03 - _"RRD: Clients & Admins"_
|
||||
|
||||
https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/milestones
|
||||
### 🚀 Feature
|
||||
- Nodes RRD: View CPU/RAM/Network/Disk graphs in Admin Area
|
||||
|
||||
### 💅 Polish
|
||||
- Admin Area: Overhaul Nodes, Guests, Support, Config & Logs tabs
|
||||
- Client Area: Overhaul Graphs, Specs Table, and Statistics display
|
||||
|
||||
### 🐛 Bug Fix
|
||||
- RRD Schema Update: Adjustments to Client/Admin Areas (#162)
|
||||
|
||||
## [1.2.19] - 2025-10-24 - _"Remove TigerVNC (Java)"_
|
||||
|
||||
|
||||
12
README.md
@@ -22,13 +22,17 @@
|
||||
|
||||
https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/
|
||||
|
||||
**Client Area GUI - Landing:**
|
||||
**Client Area GUI - w/ Stats:**
|
||||
|
||||
<img alt="Client Area GUI showing management of a powered-on VM" src="_images/zVMclientGUI.png">
|
||||
<img alt="Client Area GUI showing management of a powered-on Guest, after the Statistics action has been clicked resulting in the graphs at the bottom." src="_images/zVMclientGUI.png">
|
||||
|
||||
**Admin Area GUI - Landing:**
|
||||
**Admin Area GUI - PVE Nodes:**
|
||||
|
||||
<img alt="Admin Area GUI for the Module, showing the Nodes & Guests" src="_images/zClusterGuests.png">
|
||||
<img alt="Admin Area GUI for the Module, showing the Nodes tab that you land on upon opening the Module." src="_images/zClusterNodes.png">
|
||||
|
||||
**Admin Area GUI - Guests:**
|
||||
|
||||
<img alt="Admin Area GUI for the Module, showing the Guests tab which lists and details Virtual Machines and Containers." src="_images/zClusterGuests.png">
|
||||
|
||||
# ❤️ RTFM: Read the Manual & Review the Module!
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 113 KiB |
BIN
_images/zClusterNodes.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 277 KiB |
@@ -45,7 +45,7 @@ function pvewhmcs_config() {
|
||||
|
||||
// VERSION: also stored in repo/version (for update-available checker)
|
||||
function pvewhmcs_version(){
|
||||
return "1.2.19";
|
||||
return "1.3.0";
|
||||
}
|
||||
|
||||
// WHMCS MODULE: ACTIVATION of the ADDON MODULE
|
||||
@@ -174,6 +174,48 @@ function get_pvewhmcs_latest_version(){
|
||||
return str_replace("\n", "", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch RRD statistics from Proxmox with graceful error handling (Admin Area).
|
||||
*
|
||||
* Proxmox RRD schema changed in PVE 9 from pve2-{type} to pve-{type}-9.0.
|
||||
* The ds parameter names (cpu, mem, netin, netout, etc.) remain valid.
|
||||
*
|
||||
* RRD data may be unavailable when:
|
||||
* - Node/VM was just created (RRD takes ~60s to populate)
|
||||
* - RRD schema migration is incomplete on the PVE host
|
||||
* - RRD files are corrupted or missing
|
||||
*
|
||||
* @param PVE2_API $proxmox The Proxmox API client instance
|
||||
* @param string $path The RRD API path (e.g., /nodes/{node}/rrd)
|
||||
* @param string $timeframe RRD timeframe: 'hour', 'day', 'week', 'month', 'year'
|
||||
* @param string $ds Data source(s): 'cpu', 'mem', 'netin,netout', etc.
|
||||
* @return string|null Base64-encoded PNG image, or null if unavailable
|
||||
*/
|
||||
function pvewhmcs_addon_fetch_rrd($proxmox, $path, $timeframe, $ds) {
|
||||
$rrd_params = '?timeframe=' . $timeframe . '&ds=' . $ds . '&cf=AVERAGE';
|
||||
|
||||
try {
|
||||
$rrd_data = $proxmox->get($path . $rrd_params);
|
||||
|
||||
if (isset($rrd_data['image']) && !empty($rrd_data['image'])) {
|
||||
$image = utf8_decode($rrd_data['image']);
|
||||
return base64_encode($image);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// RRD data unavailable - log if debug mode on
|
||||
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
|
||||
logModuleCall(
|
||||
'pvewhmcs',
|
||||
'pvewhmcs_addon_fetch_rrd',
|
||||
'RRD fetch failed: ' . $path . ' (' . $ds . ', ' . $timeframe . ')',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ADMIN MODULE GUI: output (HTML etc)
|
||||
function pvewhmcs_output($vars) {
|
||||
$modulelink = $vars['modulelink'];
|
||||
@@ -206,17 +248,60 @@ function pvewhmcs_output($vars) {
|
||||
echo '
|
||||
<div id="clienttabs">
|
||||
<ul class="nav nav-tabs admin-tabs">
|
||||
<li class="'.($_GET['tab']=="nodes" ? "active" : "").'"><a id="tabLink1" data-toggle="tab" role="tab" href="#nodes">Nodes & Guests</a></li>
|
||||
<li class="'.($_GET['tab']=="vmplans" ? "active" : "").'"><a id="tabLink2" data-toggle="tab" role="tab" href="#plans">Plans: VM & CT</a></li>
|
||||
<li class="'.($_GET['tab']=="ippools" ? "active" : "").'"><a id="tabLink3" data-toggle="tab" role="tab" href="#ippools">IPv4 Pools</a></li>
|
||||
<li class="'.($_GET['tab']=="actions" ? "active" : "").'"><a id="tabLink4" data-toggle="tab" role="tab" href="#actions">Actions</a></li>
|
||||
<li class="'.($_GET['tab']=="support" ? "active" : "").'"><a id="tabLink5" data-toggle="tab" role="tab" href="#support">Support</a></li>
|
||||
<li class="'.($_GET['tab']=="config" ? "active" : "").'"><a id="tabLink6" data-toggle="tab" role="tab" href="#config">Config</a></li>
|
||||
<li class="'.($_GET['tab']=="logs" ? "active" : "").'"><a id="tabLink7" data-toggle="tab" role="tab" href="#logs">Logs</a></li>
|
||||
<li class="'.($_GET['tab']=="nodes" ? "active" : "").'"><a id="tabLink1" data-toggle="tab" role="tab" href="#nodes">Nodes</a></li>
|
||||
<li class="'.($_GET['tab']=="guests" ? "active" : "").'"><a id="tabLink2" data-toggle="tab" role="tab" href="#guests">Guests</a></li>
|
||||
<li class="'.($_GET['tab']=="vmplans" ? "active" : "").'"><a id="tabLink3" data-toggle="tab" role="tab" href="#plans">Plans</a></li>
|
||||
<li class="'.($_GET['tab']=="ippools" ? "active" : "").'"><a id="tabLink4" data-toggle="tab" role="tab" href="#ippools">IPv4</a></li>
|
||||
<li class="'.($_GET['tab']=="actions" ? "active" : "").'"><a id="tabLink5" data-toggle="tab" role="tab" href="#actions">Actions</a></li>
|
||||
<li class="'.($_GET['tab']=="support" ? "active" : "").'"><a id="tabLink6" data-toggle="tab" role="tab" href="#support">Support</a></li>
|
||||
<li class="'.($_GET['tab']=="config" ? "active" : "").'"><a id="tabLink7" data-toggle="tab" role="tab" href="#config">Config</a></li>
|
||||
<li class="'.($_GET['tab']=="logs" ? "active" : "").'"><a id="tabLink8" data-toggle="tab" role="tab" href="#logs">Logs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content admin-tabs">
|
||||
' ;
|
||||
<style>
|
||||
.pve-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.pve-table thead th {
|
||||
background: #f8f8f8;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.pve-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.pve-table tbody tr:hover {
|
||||
background: #faf8fc;
|
||||
}
|
||||
.pve-table tbody tr:not(:last-child) td {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.pve-table tbody td {
|
||||
padding: 10px 15px;
|
||||
color: #333;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.pve-table code {
|
||||
background: #f4f0f7;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #5c3d7a;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<div class="tab-content admin-tabs">';
|
||||
|
||||
// Handle form submissions for saving or updating plans
|
||||
if (isset($_POST['addnewkvmplan']))
|
||||
@@ -240,7 +325,6 @@ function pvewhmcs_output($vars) {
|
||||
|
||||
// NODES / GUESTS tab in ADMIN GUI
|
||||
echo '<div id="nodes" class="tab-pane '.($_GET['tab']=="nodes" ? "active" : "").'" >' ;
|
||||
echo ('<strong><h2>Cluster Members</h2></strong>');
|
||||
|
||||
// Fetch all enabled Servers that use pvewhmcs
|
||||
$servers = Capsule::table('tblservers')
|
||||
@@ -299,88 +383,203 @@ function pvewhmcs_output($vars) {
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Nodes table --------
|
||||
echo '<table class="datatable" border="0" cellpadding="3" cellspacing="1" width="100%">';
|
||||
echo '<tbody><tr>
|
||||
<th>Node</th>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>IPv4</th>
|
||||
<th>CPU %</th>
|
||||
<th>RAM %</th>
|
||||
<th>Uptime</th>
|
||||
</tr>';
|
||||
// Count running guests
|
||||
$running_guests = array_filter($guests, function($g) {
|
||||
return isset($g['status']) && $g['status'] === 'running';
|
||||
});
|
||||
|
||||
// ======== CLUSTER HEADER PANEL ========
|
||||
echo '<div class="panel panel-default" style="margin-bottom:20px;">';
|
||||
echo '<div class="panel-heading" style="background:#5c3d7a;color:#fff;">';
|
||||
echo '<h3 class="panel-title" style="margin:0;"><i class="fa fa-server"></i> '.htmlspecialchars($serverlabel).' <small style="color:#ccc;">('.htmlspecialchars($serverip).')</small></h3>';
|
||||
echo '</div>';
|
||||
echo '<div class="panel-body">';
|
||||
|
||||
// -------- Per-Node Info with RRD Graphs --------
|
||||
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_name = isset($n['node']) ? $n['node'] : '(node)';
|
||||
$n_status = isset($n['status']) ? $n['status'] : 'unknown';
|
||||
$n_name = isset($n['node']) ? $n['node'] : '(node)';
|
||||
$n_uptime = isset($n['uptime']) ? time2format($n['uptime']) : '—';
|
||||
$n_version = $proxmox->get_version();
|
||||
$n_cpu_pct = isset($n['cpu']) ? round($n['cpu'] * 100, 1) : 0;
|
||||
$n_maxcpu = isset($n['maxcpu']) ? $n['maxcpu'] : 0;
|
||||
$n_mem_pct = (isset($n['mem']) && isset($n['maxmem']) && $n['maxmem'] > 0)
|
||||
? round($n['mem'] * 100 / $n['maxmem'], 1)
|
||||
: 0;
|
||||
$n_mem_used = isset($n['mem']) ? round($n['mem'] / 1073741824, 1) : 0;
|
||||
$n_mem_max = isset($n['maxmem']) ? round($n['maxmem'] / 1073741824, 1) : 0;
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td><strong>'.htmlspecialchars($n_name).'</strong></td>';
|
||||
echo '<td>'.htmlspecialchars($n_version).'</td>';
|
||||
echo '<td>'.htmlspecialchars($n_status).'</td>';
|
||||
echo '<td>'.htmlspecialchars($serverip).'</td>';
|
||||
echo '<td>'.$n_cpu_pct.'</td>';
|
||||
echo '<td>'.$n_mem_pct.'</td>';
|
||||
echo '<td>'.htmlspecialchars($n_uptime).'</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$status_color = ($n_status === 'online') ? '#5cb85c' : '#d9534f';
|
||||
|
||||
// -------- Active Guests (running only) --------
|
||||
echo '<h2 style="margin-top:16px;">Active Guests</h2>';
|
||||
echo '<table class="datatable" border="0" cellpadding="3" cellspacing="1" width="100%">';
|
||||
echo '<tbody><tr>
|
||||
<th>VMID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Node</th>
|
||||
<th>CPU %</th>
|
||||
<th>RAM %</th>
|
||||
<th>Disk %</th>
|
||||
<th>Uptime</th>
|
||||
</tr>';
|
||||
echo '<div style="border:1px solid #ddd;border-radius:4px;padding:15px;margin-bottom:15px;background:#fafafa;">';
|
||||
|
||||
// Node Header Row
|
||||
echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;border-bottom:1px solid #eee;padding-bottom:10px;">';
|
||||
echo '<div>';
|
||||
echo '<h4 style="margin:0 0 5px 0;"><i class="fa fa-cube" style="color:#5c3d7a;"></i> '.htmlspecialchars($n_name).'</h4>';
|
||||
echo '<span style="color:#555;font-size:12px;">PVE '.htmlspecialchars($n_version).' • Uptime: '.htmlspecialchars($n_uptime).'</span>';
|
||||
echo '</div>';
|
||||
echo '<div style="text-align:right;">';
|
||||
echo '<span style="display:inline-block;padding:4px 12px;border-radius:3px;background:'.$status_color.';color:#fff;font-weight:bold;text-transform:uppercase;font-size:11px;">'.htmlspecialchars($n_status).'</span>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
|
||||
foreach ($guests as $g) {
|
||||
// Only running guests for the "active" overview
|
||||
if (!isset($g['status']) || $g['status'] !== 'running') {
|
||||
continue;
|
||||
// Live Stats Row
|
||||
echo '<div style="display:flex;gap:20px;margin-bottom:15px;">';
|
||||
echo '<div style="flex:1;text-align:center;padding:10px;background:#fff;border-radius:4px;border:1px solid #eee;">';
|
||||
echo '<div style="font-size:24px;font-weight:bold;color:#5c3d7a;">'.$n_cpu_pct.'%</div>';
|
||||
echo '<div style="font-size:11px;color:#555;">CPU ('.$n_maxcpu.' cores)</div>';
|
||||
echo '</div>';
|
||||
echo '<div style="flex:1;text-align:center;padding:10px;background:#fff;border-radius:4px;border:1px solid #eee;">';
|
||||
echo '<div style="font-size:24px;font-weight:bold;color:#5c3d7a;">'.$n_mem_pct.'%</div>';
|
||||
echo '<div style="font-size:11px;color:#555;">RAM ('.$n_mem_used.'/'.$n_mem_max.' GB)</div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
|
||||
// RRD Graphs Section
|
||||
$rrd_path = '/nodes/' . $n_name . '/rrd';
|
||||
$rrd_cpu = pvewhmcs_addon_fetch_rrd($proxmox, $rrd_path, 'hour', 'cpu');
|
||||
$rrd_mem = pvewhmcs_addon_fetch_rrd($proxmox, $rrd_path, 'hour', 'memused');
|
||||
$rrd_net = pvewhmcs_addon_fetch_rrd($proxmox, $rrd_path, 'hour', 'netin,netout');
|
||||
$rrd_io = pvewhmcs_addon_fetch_rrd($proxmox, $rrd_path, 'hour', 'iowait');
|
||||
|
||||
if ($rrd_cpu || $rrd_mem || $rrd_net || $rrd_io) {
|
||||
echo '<div style="margin-top:10px;">';
|
||||
echo '<div style="font-size:12px;color:#555;margin-bottom:8px;"><i class="fa fa-line-chart"></i> Performance Graphs (Last Hour)</div>';
|
||||
// Row 1: CPU and Memory
|
||||
echo '<div style="display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px;">';
|
||||
if ($rrd_cpu) {
|
||||
echo '<div style="flex:1;min-width:45%;"><img src="data:image/png;base64,'.$rrd_cpu.'" style="width:100%;border-radius:3px;border:1px solid #ddd;"/></div>';
|
||||
}
|
||||
if ($rrd_mem) {
|
||||
echo '<div style="flex:1;min-width:45%;"><img src="data:image/png;base64,'.$rrd_mem.'" style="width:100%;border-radius:3px;border:1px solid #ddd;"/></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
// Row 2: Network and I/O
|
||||
echo '<div style="display:flex;flex-wrap:wrap;gap:10px;">';
|
||||
if ($rrd_net) {
|
||||
echo '<div style="flex:1;min-width:45%;"><img src="data:image/png;base64,'.$rrd_net.'" style="width:100%;border-radius:3px;border:1px solid #ddd;"/></div>';
|
||||
}
|
||||
if ($rrd_io) {
|
||||
echo '<div style="flex:1;min-width:45%;"><img src="data:image/png;base64,'.$rrd_io.'" style="width:100%;border-radius:3px;border:1px solid #ddd;"/></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<div class="alert alert-warning" style="margin:10px 0 0 0;padding:10px;font-size:12px;">';
|
||||
echo '<i class="fa fa-exclamation-triangle"></i> RRD data unavailable. Please ask Support to upgrade RRD stored data from 2.x to 9.0 format on their Nodes.';
|
||||
echo '</div>';
|
||||
}
|
||||
$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 '<tr>';
|
||||
echo '<td><strong>'.$g_vmid.'</strong></td>';
|
||||
echo '<td><strong>'.htmlspecialchars($g_name).'</strong></td>';
|
||||
echo '<td>'.htmlspecialchars($g['status']).'</td>';
|
||||
echo '<td>'.htmlspecialchars($g_type).'</td>';
|
||||
echo '<td>'.htmlspecialchars($g_node).'</td>';
|
||||
echo '<td>'.$g_cpu_pct.'</td>';
|
||||
echo '<td>'.$g_mem_pct.'</td>';
|
||||
echo '<td>'.$g_dsk_pct.'</td>';
|
||||
echo '<td>'.htmlspecialchars($g_uptime).'</td>';
|
||||
echo '</tr>';
|
||||
echo '</div>'; // End node panel
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
|
||||
echo '<hr style="margin:24px 0;">';
|
||||
echo '</div>'; // panel-body
|
||||
echo '</div>'; // panel
|
||||
}
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// ======== GUESTS TAB ========
|
||||
echo '<div id="guests" class="tab-pane '.($_GET['tab']=="guests" ? "active" : "").'" >';
|
||||
|
||||
// Re-use servers data for guests tab
|
||||
$servers = Capsule::table('tblservers')
|
||||
->where('type', '=', 'pvewhmcs')
|
||||
->where('disabled', '=', 0)
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
|
||||
if ($servers->isEmpty()) {
|
||||
echo '<div class="alert alert-warning">No enabled WHMCS servers found for module type <code>pvewhmcs</code>.</div>';
|
||||
} else {
|
||||
foreach ($servers as $srv) {
|
||||
$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);
|
||||
|
||||
$proxmox = new PVE2_API($serverip, $serverusername, "pam", $serverpassword['password']);
|
||||
if (!$proxmox->login()) {
|
||||
echo '<div class="alert alert-danger">Unable to log in to PVE API on '.htmlspecialchars($serverip).'.</div>';
|
||||
continue;
|
||||
}
|
||||
|
||||
$cluster_resources = $proxmox->get('/cluster/resources');
|
||||
if (!is_array($cluster_resources) || empty($cluster_resources)) {
|
||||
echo '<div class="alert alert-info">No resources returned.</div>';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter guests only
|
||||
$guests = [];
|
||||
foreach ($cluster_resources as $res) {
|
||||
if (isset($res['type']) && ($res['type'] === 'qemu' || $res['type'] === 'lxc')) {
|
||||
$guests[] = $res;
|
||||
}
|
||||
}
|
||||
|
||||
$running_count = count(array_filter($guests, function($g) { return ($g['status'] ?? '') === 'running'; }));
|
||||
$stopped_count = count(array_filter($guests, function($g) { return ($g['status'] ?? '') === 'stopped'; }));
|
||||
|
||||
echo '<div class="panel panel-default" style="margin-bottom:20px;">';
|
||||
echo '<div class="panel-heading" style="background:#5c3d7a;color:#fff;">';
|
||||
echo '<h3 class="panel-title" style="margin:0;"><i class="fa fa-desktop"></i> '.htmlspecialchars($serverlabel).' <small style="color:#ddd;">('.count($guests).' guests: '.$running_count.' running, '.$stopped_count.' stopped)</small></h3>';
|
||||
echo '</div>';
|
||||
echo '<div class="panel-body" style="padding:0;padding-top:8px;">';
|
||||
|
||||
if (count($guests) > 0) {
|
||||
echo '<table class="pve-table">';
|
||||
echo '<thead><tr>
|
||||
<th>VMID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Node</th>
|
||||
<th>CPU %</th>
|
||||
<th>RAM %</th>
|
||||
<th>Disk %</th>
|
||||
<th>Uptime</th>
|
||||
</tr></thead><tbody>';
|
||||
|
||||
foreach ($guests as $g) {
|
||||
$g_node = $g['node'] ?? '—';
|
||||
$g_type = $g['type'] ?? '—';
|
||||
$g_vmid = isset($g['vmid']) ? (int)$g['vmid'] : 0;
|
||||
$g_name = $g['name'] ?? '';
|
||||
$g_status = $g['status'] ?? 'unknown';
|
||||
$g_uptime = isset($g['uptime']) ? time2format($g['uptime']) : '—';
|
||||
$g_cpu_pct = isset($g['cpu']) ? round($g['cpu'] * 100, 1) : 0;
|
||||
$g_mem_pct = (isset($g['maxmem']) && $g['maxmem'] > 0)
|
||||
? round(($g['mem'] ?? 0) * 100 / $g['maxmem'], 1)
|
||||
: 0;
|
||||
$g_dsk_pct = (isset($g['maxdisk']) && $g['maxdisk'] > 0)
|
||||
? round(($g['disk'] ?? 0) * 100 / $g['maxdisk'], 1)
|
||||
: 0;
|
||||
|
||||
$type_icon = ($g_type === 'qemu') ? 'fa-desktop' : 'fa-cube';
|
||||
$status_color = ($g_status === 'running') ? '#5cb85c' : '#999';
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td><code>'.$g_vmid.'</code></td>';
|
||||
echo '<td><i class="fa '.$type_icon.'" style="color:#666;"></i> <strong>'.htmlspecialchars($g_name).'</strong></td>';
|
||||
echo '<td><span style="display:inline-block;padding:2px 8px;border-radius:3px;background:'.$status_color.';color:#fff;font-size:10px;text-transform:uppercase;">'.htmlspecialchars($g_status).'</span></td>';
|
||||
echo '<td><span style="text-transform:uppercase;font-size:10px;background:#eee;padding:2px 6px;border-radius:3px;">'.htmlspecialchars($g_type).'</span></td>';
|
||||
echo '<td>'.htmlspecialchars($g_node).'</td>';
|
||||
echo '<td>'.$g_cpu_pct.'%</td>';
|
||||
echo '<td>'.$g_mem_pct.'%</td>';
|
||||
echo '<td>'.$g_dsk_pct.'%</td>';
|
||||
echo '<td>'.htmlspecialchars($g_uptime).'</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
} else {
|
||||
echo '<div class="alert alert-info" style="margin:15px;">No guests found on this cluster.</div>';
|
||||
}
|
||||
|
||||
echo '</div>'; // panel-body
|
||||
echo '</div>'; // panel
|
||||
}
|
||||
}
|
||||
echo '</div>';
|
||||
@@ -533,58 +732,109 @@ function pvewhmcs_output($vars) {
|
||||
// ACTIONS tab in ADMIN GUI
|
||||
echo '<div id="actions" class="tab-pane '.($_GET['tab']=="actions" ? "active" : "").'" >' ;
|
||||
echo ('<strong><h2>Module: Action History</h2></strong>');
|
||||
echo ('Coming in v1.3.x');
|
||||
echo ('Coming soon!<br><br>');
|
||||
echo ('<strong><h2>Module: Failed Actions</h2></strong>');
|
||||
echo ('Coming in v1.3.x<br><br><strong><a href=\'https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/milestones\' target=\'_blank\'>View the milestones/versions on GitHub</a></strong>');
|
||||
echo ('Coming soon!<br><br>');
|
||||
echo '</div>';
|
||||
|
||||
// SUPPORT tab in ADMIN GUI
|
||||
echo ('<div id="support" class="tab-pane '.($_GET['tab']=="support" ? "active" : "").'" >') ;
|
||||
echo ('<strong><h2>System Environment</h2></strong><b>Proxmox VE for WHMCS</b> v' . pvewhmcs_version() . ' (GitHub reports latest as <b>v' . get_pvewhmcs_latest_version() . '</b>)' . '<br><b>PHP</b> v' . phpversion() . ' running on <b>' . $_SERVER['SERVER_SOFTWARE'] . '</b> Web Server (' . $_SERVER['SERVER_NAME'] . ')<br><br>');
|
||||
echo ('<b>❤️ PVEWHMCS is open-source and free to use & improve on!</b><br><a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank">https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/</a><br><br>');
|
||||
echo ('<b style="color:darkgreen;">Your 5-star review on WHMCS Marketplace will help the module grow!</b><br>*****: <a href="https://marketplace.whmcs.com/product/6935-proxmox-ve-for-whmcs" target="_blank">https://marketplace.whmcs.com/product/6935-proxmox-ve-for-whmcs</a><br><br>');
|
||||
echo ('<strong><h2>Issues: Common Causes</h2></strong>1. Save your Package (Plan/Pool)! (configproducts.php?action=edit&id=...#tab=3)<br>2. Where possible, we pass-through the exact error to WHMCS Admin. Check it for info!<br><br>');
|
||||
echo ('<strong><h2>Module Technical Support</h2></strong>Our README contains a wealth of information:<br><a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank">https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/</a><br>Please only raise an <a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/issues/new/choose" target="_blank"><u>Issue</u></a> on GitHub - inc. logs - if you\'ve properly tried.<br><br><b>Help is not guaranteed (FOSS). We will need your assistance.</b> Thank you!<br><br>');
|
||||
echo '<div id="support" class="tab-pane '.($_GET['tab']=="support" ? "active" : "").'" >';
|
||||
echo '
|
||||
<div style="max-width:800px;">
|
||||
<div style="background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:25px;margin-bottom:20px;">
|
||||
<h3 style="margin:0 0 15px 0;color:#5c3d7a;font-weight:600;"><span style="font-size:24px;">⚙</span> System Environment</h3>
|
||||
<table style="width:100%;font-size:14px;">
|
||||
<tr><td style="padding:6px 0;width:180px;color:#666;">Module Version</td><td style="padding:6px 0;"><code style="background:#f4f0f7;padding:3px 8px;border-radius:3px;color:#5c3d7a;">v' . pvewhmcs_version() . '</code></td></tr>
|
||||
<tr><td style="padding:6px 0;color:#666;">Latest Available</td><td style="padding:6px 0;"><code style="background:#f4f0f7;padding:3px 8px;border-radius:3px;color:#5c3d7a;">v' . get_pvewhmcs_latest_version() . '</code></td></tr>
|
||||
<tr><td style="padding:6px 0;color:#666;">Web Server</td><td style="padding:6px 0;"><code style="background:#f4f0f7;padding:3px 8px;border-radius:3px;color:#5c3d7a;">' . htmlspecialchars($_SERVER['SERVER_SOFTWARE']) . '</code></td></tr>
|
||||
<tr><td style="padding:6px 0;color:#666;">PHP Version</td><td style="padding:6px 0;"><code style="background:#f4f0f7;padding:3px 8px;border-radius:3px;color:#5c3d7a;">v' . phpversion() . '</code></td></tr>
|
||||
<tr><td style="padding:6px 0;color:#666;">Server Name</td><td style="padding:6px 0;"><code style="background:#f4f0f7;padding:3px 8px;border-radius:3px;color:#5c3d7a;">' . htmlspecialchars($_SERVER['SERVER_NAME']) . '</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="background:#faf8fc;border:1px solid #e0d4e8;border-radius:8px;padding:25px;margin-bottom:20px;">
|
||||
<h3 style="margin:0 0 15px 0;color:#5c3d7a;font-weight:600;"><span style="font-size:24px;">♥</span> Open Source</h3>
|
||||
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;color:#333;">PVEWHMCS is open-source and free to use & improve on!</p>
|
||||
<p style="margin:0;">
|
||||
<a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank" style="color:#5c3d7a;">➔ GitHub Repository</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background:#f8fff8;border:1px solid #c3e6c3;border-radius:8px;padding:25px;margin-bottom:20px;">
|
||||
<h3 style="margin:0 0 15px 0;color:#2d7a2d;font-weight:600;"><span style="font-size:24px;">★</span> Leave a Review</h3>
|
||||
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;color:#333;">Your 5-star review on WHMCS Marketplace helps the module grow!</p>
|
||||
<p style="margin:0;">
|
||||
<a href="https://marketplace.whmcs.com/product/6935-proxmox-ve-for-whmcs" target="_blank" style="color:#2d7a2d;">★★★★★ Rate on WHMCS Marketplace</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:25px;">
|
||||
<h3 style="margin:0 0 15px 0;color:#5c3d7a;font-weight:600;"><span style="font-size:24px;">⚙</span> Technical Support</h3>
|
||||
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;">Our README contains a wealth of information. Please review it before raising issues.</p>
|
||||
<p style="margin:0 0 15px 0;">
|
||||
<a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank" style="color:#5c3d7a;">➔ View Documentation</a>
|
||||
</p>
|
||||
<p style="margin:0 0 12px 0;font-size:14px;line-height:1.6;">Only raise a GitHub Issue — including logs — if you have properly tried to resolve it first.</p>
|
||||
<p style="margin:0 0 15px 0;">
|
||||
<a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/issues/new/choose" target="_blank" style="color:#5c3d7a;">➔ Open an Issue</a>
|
||||
</p>
|
||||
<p style="margin:0;padding:12px;background:#fff8f0;border-radius:6px;font-size:13px;color:#856404;border:1px solid #ffc107;">⚠ Help is not guaranteed (FOSS). We will need your assistance to troubleshoot.</p>
|
||||
</div>
|
||||
</div>';
|
||||
echo '</div>';
|
||||
|
||||
// Config Tab
|
||||
$config= Capsule::table('mod_pvewhmcs')->where('id', '=', '1')->get()[0];
|
||||
echo '<div id="config" class="tab-pane '.($_GET['tab']=="config" ? "active" : "").'" >' ;
|
||||
echo '
|
||||
<div style="max-width:800px;">
|
||||
<div style="background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:25px;">
|
||||
<h3 style="margin:0 0 20px 0;color:#5c3d7a;font-weight:600;">⚙ Module Configuration</h3>
|
||||
<form method="post">
|
||||
<table class="form" border="0" cellpadding="3" cellspacing="1" width="100%">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td class="fieldlabel">VNC Secret</td>
|
||||
<td class="fieldarea">
|
||||
<input type="text" size="35" name="vnc_secret" id="vnc_secret" value="'.$config->vnc_secret.'"> Password of "vnc"@"pve" user. Mandatory for VNC proxying. (See the <a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank">README</a> for more info)
|
||||
</td>
|
||||
<td style="padding:15px 0;border-bottom:1px solid #eee;width:160px;vertical-align:top;">
|
||||
<label style="font-weight:600;color:#333;">VNC Secret</label>
|
||||
</td>
|
||||
<td style="padding:15px 0;border-bottom:1px solid #eee;">
|
||||
<input type="text" style="width:100%;max-width:300px;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;" name="vnc_secret" id="vnc_secret" value="'.$config->vnc_secret.'">
|
||||
<p style="margin:8px 0 0 0;font-size:13px;color:#666;">Password for <code style="background:#f4f0f7;padding:2px 6px;border-radius:3px;color:#5c3d7a;">vnc@pve</code> user. Required for VNC proxying. <a href="https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/" target="_blank" style="color:#5c3d7a;"><u>View README</u></a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fieldlabel">VMID Start</td>
|
||||
<td class="fieldarea">
|
||||
<input type="text" size="35" name="start_vmid" id="start_vmid" value="'.$config->start_vmid.'"> Starting VMID (PVE). Default is 100. (Module will increment this until vacant VMID is found)
|
||||
</td>
|
||||
<td style="padding:15px 0;border-bottom:1px solid #eee;vertical-align:top;">
|
||||
<label style="font-weight:600;color:#333;">VMID Start</label>
|
||||
</td>
|
||||
<td style="padding:15px 0;border-bottom:1px solid #eee;">
|
||||
<input type="text" style="width:100%;max-width:300px;padding:8px 12px;border:1px solid #ddd;border-radius:4px;font-size:14px;" name="start_vmid" id="start_vmid" value="'.$config->start_vmid.'">
|
||||
<p style="margin:8px 0 0 0;font-size:13px;color:#666;">For Guests. Increments until a vacant VMID found. Default is <code style="background:#f4f0f7;padding:2px 6px;border-radius:3px;color:#5c3d7a;">100</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fieldlabel">Debug?</td>
|
||||
<td class="fieldarea">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" name="debug_mode" value="1" '. ($config->debug_mode=="1" ? "checked" : "").'> Whether or not you want Debug Logging enabled - must also enable WHMCS Module Log (WHMCS Debug) & then view <u><a href="/admin/index.php?rp=/admin/logs/module-log">at this link here.</a></u>
|
||||
</label>
|
||||
</td>
|
||||
<td style="padding:15px 0;vertical-align:top;">
|
||||
<label style="font-weight:600;color:#333;">Debug Mode</label>
|
||||
</td>
|
||||
<td style="padding:15px 0;">
|
||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">
|
||||
<input type="checkbox" name="debug_mode" value="1" '. ($config->debug_mode=="1" ? "checked" : "").' style="width:18px;height:18px;">
|
||||
<span style="font-size:14px;color:#333;">Enable Debug Logging</span>
|
||||
</label>
|
||||
<p style="margin:8px 0 0 0;font-size:13px;color:#666;">Must also enable WHMCS Module Log. <a href="/admin/index.php?rp=/admin/logs/module-log" style="color:#5c3d7a;"><u>View Module Logs</u></a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="btn-container">
|
||||
<input type="submit" class="btn btn-primary" value="Save Changes" name="save_config" id="save_config">
|
||||
<input type="reset" class="btn btn-default" value="Cancel Changes">
|
||||
<div style="margin-top:25px;padding-top:20px;border-top:1px solid #eee;">
|
||||
<input type="submit" style="background:#5c3d7a;color:#fff;border:none;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;margin-right:10px;" value="Save Changes" name="save_config" id="save_config">
|
||||
<input type="reset" style="background:#f5f5f5;color:#333;border:1px solid #ddd;padding:10px 24px;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;" value="Cancel">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
echo '</div>';
|
||||
|
||||
// LOGS tab in ADMIN GUI
|
||||
echo '<div id="logs" class="tab-pane ' . (isset($_GET['tab']) && $_GET['tab'] === 'logs' ? 'active' : '') . '">';
|
||||
echo '<strong><h2>Cluster History</h2></strong>';
|
||||
|
||||
try {
|
||||
// If a client exists already, reuse it; else initialise once from the first enabled pvewhmcs server
|
||||
@@ -628,8 +878,8 @@ function pvewhmcs_output($vars) {
|
||||
return (intval($b['starttime'] ?? 0)) <=> (intval($a['starttime'] ?? 0));
|
||||
});
|
||||
|
||||
echo '<table class="datatable" border="0" cellpadding="3" cellspacing="1" width="100%">';
|
||||
echo '<tbody><tr>
|
||||
echo '<table class="pve-table">';
|
||||
echo '<thead><tr>
|
||||
<th>Task</th>
|
||||
<th>VMID</th>
|
||||
<th>Status</th>
|
||||
@@ -638,7 +888,7 @@ function pvewhmcs_output($vars) {
|
||||
<th>Duration</th>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
</tr>';
|
||||
</tr></thead><tbody>';
|
||||
|
||||
foreach ($tasks as $t) {
|
||||
$node = $t['node'] ?? '—';
|
||||
@@ -678,10 +928,10 @@ function pvewhmcs_output($vars) {
|
||||
: ((preg_match('/(error|fail|aborted|unknown)/i', (string)$status)) ? '❌' : '⏳');
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td><strong>' . htmlspecialchars($type) . '</strong></td>';
|
||||
echo '<td>' . htmlspecialchars($vmid) . '</td>';
|
||||
echo '<td><code>' . htmlspecialchars($type) . '</code></td>';
|
||||
echo '<td><code>' . htmlspecialchars($vmid) . '</code></td>';
|
||||
echo '<td>' . $badge . ' ' . htmlspecialchars($status) . '</td>';
|
||||
echo '<td><strong>' . htmlspecialchars($node) . '</strong></td>';
|
||||
echo '<td>' . htmlspecialchars($node) . '</td>';
|
||||
echo '<td>' . htmlspecialchars($user) . '</td>';
|
||||
echo '<td>' . htmlspecialchars($durH) . '</td>';
|
||||
echo '<td>' . htmlspecialchars($start) . '</td>';
|
||||
|
||||
@@ -1,146 +1,503 @@
|
||||
<div class="row">
|
||||
<div style="text-align : left;">
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="row">
|
||||
<div class="col col-md-3">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<img src="/modules/servers/pvewhmcs/img/{$vm_config['vtype']}.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<img src="/modules/servers/pvewhmcs/img/os/{$vm_config['ostype']}.png"/>
|
||||
</div>
|
||||
</div>
|
||||
{* Proxmox VE for WHMCS - Client Area Template *}
|
||||
|
||||
<style>
|
||||
.pve-client-area {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
.pve-header-panel {
|
||||
background: #fafafa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pve-status-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.pve-vm-icons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pve-vm-icons img {
|
||||
max-width: 56px;
|
||||
height: auto;
|
||||
}
|
||||
.pve-status-badge {
|
||||
text-align: center;
|
||||
padding: 14px 18px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
min-width: 95px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pve-status-badge img {
|
||||
max-width: 44px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.pve-status-badge .status-text {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: #5c3d7a;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.pve-status-badge .uptime-text {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
.pve-gauges {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: nowrap;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.pve-gauge-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
min-width: 75px;
|
||||
}
|
||||
.pve-gauge-item .circle {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.pve-gauge-item strong {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Specs Table */
|
||||
.pve-specs-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.pve-specs-table tr:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.pve-specs-table td {
|
||||
padding: 13px 16px;
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pve-specs-table td:first-child {
|
||||
background: #f8f8f8;
|
||||
width: 180px;
|
||||
font-weight: 500;
|
||||
color: #444;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.pve-specs-table td:last-child {
|
||||
color: #333;
|
||||
}
|
||||
.pve-specs-table .spec-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #5c3d7a;
|
||||
}
|
||||
.pve-specs-table .spec-sublabel {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
font-weight: normal;
|
||||
}
|
||||
.pve-specs-table .spec-value {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.pve-specs-table .spec-detail {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.pve-specs-table code {
|
||||
background: #f4f0f7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 13px;
|
||||
color: #5c3d7a;
|
||||
}
|
||||
|
||||
/* Statistics Section */
|
||||
.pve-stats-section {
|
||||
margin-top: 25px;
|
||||
}
|
||||
.pve-stats-section h4 {
|
||||
color: #5c3d7a;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #5c3d7a;
|
||||
}
|
||||
.pve-stats-tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.pve-stats-tabs li {
|
||||
margin: 0;
|
||||
}
|
||||
.pve-stats-tabs li a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.pve-stats-tabs li a:hover {
|
||||
background: #eee;
|
||||
color: #5c3d7a;
|
||||
}
|
||||
.pve-stats-tabs li.active a {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-color: #ddd;
|
||||
}
|
||||
.pve-stats-content {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.pve-stats-content .tab-pane {
|
||||
display: none;
|
||||
}
|
||||
.pve-stats-content .tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
.pve-graphs-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.pve-graphs-grid img {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
/* Responsive: Large screens - maximize graph space */
|
||||
@media (min-width: 1200px) {
|
||||
.pve-status-section {
|
||||
gap: 25px;
|
||||
}
|
||||
.pve-gauges {
|
||||
gap: 15px;
|
||||
}
|
||||
.pve-gauge-item {
|
||||
padding: 14px;
|
||||
min-width: 80px;
|
||||
}
|
||||
.pve-graphs-grid img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive: Medium screens (tablets) */
|
||||
@media (min-width: 769px) and (max-width: 1199px) {
|
||||
.pve-status-section {
|
||||
gap: 15px;
|
||||
}
|
||||
.pve-vm-icons img {
|
||||
max-width: 48px;
|
||||
}
|
||||
.pve-gauges {
|
||||
gap: 8px;
|
||||
}
|
||||
.pve-gauge-item {
|
||||
padding: 10px;
|
||||
min-width: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive: Small screens (mobile) */
|
||||
@media (max-width: 768px) {
|
||||
.pve-status-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.pve-status-section > * {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.pve-vm-icons {
|
||||
flex-direction: row;
|
||||
gap: 15px;
|
||||
}
|
||||
.pve-gauges {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.pve-gauge-item {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
max-width: calc(50% - 8px);
|
||||
}
|
||||
.pve-specs-table td:first-child {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.pve-alert-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
color: #856404;
|
||||
font-size: 13px;
|
||||
}
|
||||
.pve-alert-warning i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pve-client-area">
|
||||
{* Header Panel with VM Type, Status, and Gauges *}
|
||||
<div class="pve-header-panel">
|
||||
<div class="pve-status-section">
|
||||
{* VM Type & OS Icons *}
|
||||
<div class="pve-vm-icons">
|
||||
<img src="/modules/servers/pvewhmcs/img/{$vm_config['vtype']}.png" alt="{$vm_config['vtype']}" title="Type: {$vm_config['vtype']}"/>
|
||||
<img src="/modules/servers/pvewhmcs/img/os/{$vm_config['ostype']}.png" alt="{$vm_config['ostype']}" title="OS: {$vm_config['ostype']}"/>
|
||||
</div>
|
||||
<div class="col col-md-2">
|
||||
<img src="/modules/servers/pvewhmcs/img/{$vm_status['status']}.png"/><br/>
|
||||
<span style="text-transform: uppercase"><strong><i>{$vm_status['status']}</i></strong></span><br/>
|
||||
Up: {$vm_status['uptime']}
|
||||
|
||||
{* Status Badge *}
|
||||
<div class="pve-status-badge">
|
||||
<img src="/modules/servers/pvewhmcs/img/{$vm_status['status']}.png" alt="{$vm_status['status']}"/>
|
||||
<span class="status-text">{$vm_status['status']}</span>
|
||||
<span class="uptime-text">Up {$vm_status['uptime']}</span>
|
||||
</div>
|
||||
<div class="col col-md-7">
|
||||
<div class="row">
|
||||
<script src="/modules/servers/pvewhmcs/js/CircularLoader.js"></script>
|
||||
<div class="col col-md-3" style="height:106px;">
|
||||
<div id="c1" class="circle" data-percent="{$vm_status['cpu']}"><strong>CPU</strong></div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div id="c2" class="circle" data-percent="{$vm_status['memusepercent']}"><strong>RAM</strong></div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div id="c3" class="circle" data-percent="{$vm_status['diskusepercent']}"><strong>Disk</strong></div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div id="c4" class="circle" data-percent="{$vm_status['swapusepercent']}"><strong>Swap</strong></div>
|
||||
</div>
|
||||
|
||||
{* Resource Gauges *}
|
||||
<div class="pve-gauges">
|
||||
<script src="/modules/servers/pvewhmcs/js/CircularLoader.js"></script>
|
||||
<div class="pve-gauge-item">
|
||||
<div id="c1" class="circle" data-percent="{$vm_status['cpu']}"></div>
|
||||
<strong>CPU</strong>
|
||||
</div>
|
||||
<div class="pve-gauge-item">
|
||||
<div id="c2" class="circle" data-percent="{$vm_status['memusepercent']}"></div>
|
||||
<strong>RAM</strong>
|
||||
</div>
|
||||
<div class="pve-gauge-item">
|
||||
<div id="c3" class="circle" data-percent="{$vm_status['diskusepercent']}"></div>
|
||||
<strong>Disk</strong>
|
||||
</div>
|
||||
<div class="pve-gauge-item">
|
||||
<div id="c4" class="circle" data-percent="{$vm_status['swapusepercent']}"></div>
|
||||
<strong>Swap</strong>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.circle').each(function(){
|
||||
$(this).circularloader({
|
||||
progressPercent: $(this).attr("data-percent"),
|
||||
fontSize: "13px",
|
||||
radius: 30,
|
||||
progressBarWidth: 8,
|
||||
progressBarBackground: "#D6B1F9",
|
||||
progressBarColor: "#802DBC",
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.circle').each(function(){
|
||||
$(this).circularloader({
|
||||
progressPercent: $(this).attr("data-percent"),
|
||||
fontSize: "12px",
|
||||
radius: 27,
|
||||
progressBarWidth: 7,
|
||||
progressBarBackground: "#D6B1F9",
|
||||
progressBarColor: "#802DBC",
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
{* Specifications Table *}
|
||||
<table class="pve-specs-table">
|
||||
<tr>
|
||||
<td><strong>Memory</strong> (RAM)</td>
|
||||
<td><strong>{$vm_config['memory']}MB</strong></td>
|
||||
<td><span class="spec-label">Memory</span> <span class="spec-sublabel">(RAM)</span></td>
|
||||
<td><span class="spec-value">{$vm_config['memory']}MB</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Compute</strong> (CPU)</td>
|
||||
<td><strong>{$vm_config['cpu']} {$vm_config['cores']} core/s on {$vm_config['sockets']} socket/s
|
||||
</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Storage</strong> (SSD/HDD)</td>
|
||||
<td><span class="spec-label">Compute</span> <span class="spec-sublabel">(CPU)</span></td>
|
||||
<td>
|
||||
{$rootfs=(","|explode:$vm_config['rootfs'])}
|
||||
{$disk=(","|explode:$vm_config['ide0'])}
|
||||
{$disk[1]}
|
||||
{$rootfs[1]}
|
||||
{($vm_config['scsi0']|replace:',':'<br/>')}
|
||||
{($vm_config['virtio0']|replace:',':'<br/>')}
|
||||
<span class="spec-value">{$vm_config['cores']} core(s)</span>
|
||||
<div class="spec-detail">on {$vm_config['sockets']} socket(s)</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Boot Order</strong> (1st > 2nd)</td>
|
||||
<td>{($vm_config['boot']|replace:'order=':''|replace:';':' > ')}</td>
|
||||
<td><span class="spec-label">Storage</span> <span class="spec-sublabel">(SSD/HDD)</span></td>
|
||||
<td>
|
||||
{if $vm_config['rootfs']}
|
||||
{assign var="rootfs_parts" value=","|explode:$vm_config['rootfs']}
|
||||
{foreach from=$rootfs_parts item=rpart}
|
||||
{if $rpart|strpos:"size=" !== false}<span class="spec-value">{$rpart|replace:'size=':''}</span> <span class="spec-detail">(rootfs)</span>{/if}
|
||||
{/foreach}
|
||||
{/if}
|
||||
{if $vm_config['ide0']}
|
||||
{assign var="ide0_parts" value=","|explode:$vm_config['ide0']}
|
||||
{foreach from=$ide0_parts item=ipart}
|
||||
{if $ipart|strpos:"size=" !== false}<div class="spec-detail"><span class="spec-value">{$ipart|replace:'size=':''}</span> (ide0)</div>{/if}
|
||||
{/foreach}
|
||||
{/if}
|
||||
{if $vm_config['scsi0']}
|
||||
{assign var="scsi0_parts" value=","|explode:$vm_config['scsi0']}
|
||||
{foreach from=$scsi0_parts item=spart}
|
||||
{if $spart|strpos:"size=" !== false}<div class="spec-detail"><span class="spec-value">{$spart|replace:'size=':''}</span> (scsi0)</div>{/if}
|
||||
{/foreach}
|
||||
{/if}
|
||||
{if $vm_config['virtio0']}
|
||||
{assign var="virtio0_parts" value=","|explode:$vm_config['virtio0']}
|
||||
{foreach from=$virtio0_parts item=vpart}
|
||||
{if $vpart|strpos:"size=" !== false}<div class="spec-detail"><span class="spec-value">{$vpart|replace:'size=':''}</span> (virtio0)</div>{/if}
|
||||
{/foreach}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>IPv4</strong> (Networking)</td><td><strong>{$vm_config['ipv4']}</strong><br/>Mask: {$vm_config['netmask4']}<br/>Gateway: {$vm_config['gateway4']}</td>
|
||||
<td><span class="spec-label">Boot Order</span></td>
|
||||
<td><span class="spec-value">{($vm_config['boot']|replace:'order=':''|replace:';':' → ')}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>ipconfig</strong> (IPv4/v6)</td><td><strong>NIC #0</strong>: {$vm_config['ipconfig0']}<br><strong>NIC #1</strong>: {$vm_config['ipconfig1']}</td>
|
||||
<td><span class="spec-label">IPv4</span> <span class="spec-sublabel">(Networking)</span></td>
|
||||
<td>
|
||||
<span class="spec-value">{$vm_config['ipv4']}</span>
|
||||
<div class="spec-detail">Mask: {$vm_config['netmask4']} • Gateway: {$vm_config['gateway4']}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>NIC #0</strong> (IPv4)</td>
|
||||
<td>{($vm_config['net0']|replace:',':'<br/>'|replace:'=':': ')}</td>
|
||||
<td><span class="spec-label">IP Config</span> <span class="spec-sublabel">(IPv4/v6)</span></td>
|
||||
<td>
|
||||
{if $vm_config['ipconfig0']}<div class="spec-detail"><strong>NIC #0:</strong> {($vm_config['ipconfig0']|replace:',':' • '|replace:'=':': ')}</div>{/if}
|
||||
{if $vm_config['ipconfig1']}<div class="spec-detail"><strong>NIC #1:</strong> {($vm_config['ipconfig1']|replace:',':' • '|replace:'=':': ')}</div>{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>NIC #1</strong> (IPv6)</td>
|
||||
<td>{($vm_config['net1']|replace:',':'<br/>'|replace:'=':': ')}</td>
|
||||
<td><span class="spec-label">NIC #0</span> <span class="spec-sublabel">(Primary)</span></td>
|
||||
<td>
|
||||
{assign var="net0_parts" value=","|explode:$vm_config['net0']}
|
||||
{foreach from=$net0_parts item=part name=netloop}
|
||||
{if $part|strpos:"=" !== false}
|
||||
{assign var="kv" value="="|explode:$part}
|
||||
{if $kv[0] == 'virtio' || $kv[0] == 'e1000' || $kv[0] == 'rtl8139'}
|
||||
<div class="spec-detail"><strong>{$kv[0]}</strong>: <code>{$kv[1]}</code></div>
|
||||
{elseif $kv[0] == 'bridge' || $kv[0] == 'link_down' || $kv[0] == 'firewall' || $kv[0] == 'tag'}
|
||||
<div class="spec-detail"><strong>{$kv[0]}</strong>: {$kv[1]}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/foreach}
|
||||
</td>
|
||||
</tr>
|
||||
{if $vm_config['net1']}
|
||||
<tr>
|
||||
<td><strong>Config</strong> (Tweaks)</td>
|
||||
<td>on_boot: {$vm_config['onboot']}</td>
|
||||
<td><span class="spec-label">NIC #1</span> <span class="spec-sublabel">(Secondary)</span></td>
|
||||
<td>
|
||||
{assign var="net1_parts" value=","|explode:$vm_config['net1']}
|
||||
{foreach from=$net1_parts item=part name=netloop}
|
||||
{if $part|strpos:"=" !== false}
|
||||
{assign var="kv" value="="|explode:$part}
|
||||
{if $kv[0] == 'virtio' || $kv[0] == 'e1000' || $kv[0] == 'rtl8139'}
|
||||
<div class="spec-detail"><strong>{$kv[0]}</strong>: <code>{$kv[1]}</code></div>
|
||||
{elseif $kv[0] == 'bridge' || $kv[0] == 'link_down' || $kv[0] == 'firewall' || $kv[0] == 'tag'}
|
||||
<div class="spec-detail"><strong>{$kv[0]}</strong>: {$kv[1]}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/foreach}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<td><strong>SSH Keys</strong> (Public)</td>
|
||||
<td>{$vm_config['sshkeys']}</td>
|
||||
<td><span class="spec-label">Config</span> <span class="spec-sublabel">(Tweaks)</span></td>
|
||||
<td><span class="spec-detail"><strong>On-boot?</strong> {if $vm_config['onboot']}Yes!{else}No (Contact Support){/if}</span></td>
|
||||
</tr>
|
||||
{if $vm_config['sshkeys']}
|
||||
<tr>
|
||||
<td><strong>OS</strong> (System Kernel)</td>
|
||||
<td><strong>{$vm_config['ostype']}</strong></td>
|
||||
<td><span class="spec-label">SSH Keys</span> <span class="spec-sublabel">(Public)</span></td>
|
||||
<td><div class="spec-detail" style="word-break:break-all;">{$vm_config['sshkeys']}</div></td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr>
|
||||
<td><span class="spec-label">Operating System</span></td>
|
||||
<td><span class="spec-value">{$vm_config['ostype']}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{* Statistics Section *}
|
||||
{if ($smarty.get.a eq 'vmStat')}
|
||||
<h4>VM Statistics</h4>
|
||||
<ul class="nav nav-tabs client-tabs" role="tab-list">
|
||||
<li class="active"><a id="dailytab" data-toggle="tab" role="tab" href="#dailystat">Daily</a></li>
|
||||
<li><a id="dailytab" data-toggle="tab" role="tab" href="#weeklystat">Weekly</a></li>
|
||||
<li><a id="dailytab" data-toggle="tab" role="tab" href="#monthlystat">Monthly</a></li>
|
||||
<li><a id="dailytab" data-toggle="tab" role="tab" href="#yearlystat">Yearly</a></li>
|
||||
</ul>
|
||||
<div class="tab-content admin-tabs">
|
||||
<div id="dailystat" class="tab-pane active">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['day']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['maxmem']['day']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['day']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['day']}"/>
|
||||
<div class="pve-stats-section">
|
||||
<h4><i class="fa fa-line-chart"></i> Guest Statistics</h4>
|
||||
|
||||
{if $vm_statistics['cpu']['day']}
|
||||
<ul class="pve-stats-tabs" role="tablist">
|
||||
<li class="active"><a data-toggle="tab" role="tab" href="#dailystat">Daily</a></li>
|
||||
<li><a data-toggle="tab" role="tab" href="#weeklystat">Weekly</a></li>
|
||||
<li><a data-toggle="tab" role="tab" href="#monthlystat">Monthly</a></li>
|
||||
<li><a data-toggle="tab" role="tab" href="#yearlystat">Yearly</a></li>
|
||||
</ul>
|
||||
<div class="pve-stats-content tab-content">
|
||||
<div id="dailystat" class="tab-pane active">
|
||||
<div class="pve-graphs-grid">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['day']}" alt="CPU (Daily)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['mem']['day']}" alt="Memory (Daily)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['day']}" alt="Network I/O (Daily)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['day']}" alt="Disk I/O (Daily)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="weeklystat" class="tab-pane">
|
||||
<div class="pve-graphs-grid">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['week']}" alt="CPU (Weekly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['mem']['week']}" alt="Memory (Weekly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['week']}" alt="Network I/O (Weekly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['week']}" alt="Disk I/O (Weekly)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="monthlystat" class="tab-pane">
|
||||
<div class="pve-graphs-grid">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['month']}" alt="CPU (Monthly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['mem']['month']}" alt="Memory (Monthly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['month']}" alt="Network I/O (Monthly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['month']}" alt="Disk I/O (Monthly)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="yearlystat" class="tab-pane">
|
||||
<div class="pve-graphs-grid">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['year']}" alt="CPU (Yearly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['mem']['year']}" alt="Memory (Yearly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['year']}" alt="Network I/O (Yearly)"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['year']}" alt="Disk I/O (Yearly)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="weeklystat" class="tab-pane">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['week']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['maxmem']['week']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['week']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['week']}"/>
|
||||
</div>
|
||||
<div id="monthlystat" class="tab-pane">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['month']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['maxmem']['month']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['month']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['month']}"/>
|
||||
</div>
|
||||
<div id="yearlystat" class="tab-pane">
|
||||
<img src="data:image/png;base64,{$vm_statistics['cpu']['year']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['maxmem']['year']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['netinout']['year']}"/>
|
||||
<img src="data:image/png;base64,{$vm_statistics['diskrw']['year']}"/>
|
||||
{else}
|
||||
<div class="pve-alert-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
Stats Error: RRD Unavailable. Ask Support to upgrade guest RRD Data from 2.x to 9.0 format on their Node/s!
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -887,6 +887,63 @@ function pvewhmcs_ClientAreaCustomButtonArray() {
|
||||
return $buttonarray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch RRD statistics from Proxmox with graceful error handling.
|
||||
*
|
||||
* Proxmox RRD schema changed in PVE 9 from pve2-{type} to pve-{type}-9.0.
|
||||
* The ds parameter names (cpu, mem, netin, netout, diskread, diskwrite) remain valid
|
||||
* across both old and new schemas - verified in pve-cluster/src/pmxcfs/status.c.
|
||||
*
|
||||
* RRD data may be unavailable when:
|
||||
* - VM/CT was just created (RRD takes ~60s to populate)
|
||||
* - RRD schema migration is incomplete on the PVE host
|
||||
* - RRD files are corrupted or missing
|
||||
*
|
||||
* Refs:
|
||||
* - Issue #162: https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/issues/162
|
||||
* - PVE RRD schema: https://github.com/proxmox/pve-cluster/blob/master/src/pmxcfs/status.c
|
||||
* - Schema change: https://www.mail-archive.com/pve-devel@lists.proxmox.com/msg28317.html
|
||||
*
|
||||
* @param PVE2_API $proxmox The Proxmox API client instance
|
||||
* @param string $node The Proxmox node name
|
||||
* @param string $vtype Guest type: 'qemu' or 'lxc'
|
||||
* @param int $vmid The VM/CT ID
|
||||
* @param string $timeframe RRD timeframe: 'day', 'week', 'month', 'year'
|
||||
* @param string $ds Data source(s): 'cpu', 'mem', 'netin,netout', 'diskread,diskwrite'
|
||||
* @return string|null Base64-encoded PNG image, or null if unavailable
|
||||
*/
|
||||
function pvewhmcs_fetch_rrd_stat($proxmox, $node, $vtype, $vmid, $timeframe, $ds) {
|
||||
// Build the API path and query params for RRD image
|
||||
$rrd_path = '/nodes/' . $node . '/' . $vtype . '/' . $vmid . '/rrd';
|
||||
$rrd_params = '?timeframe=' . $timeframe . '&ds=' . $ds . '&cf=AVERAGE';
|
||||
|
||||
try {
|
||||
// Attempt to fetch RRD graph image from PVE API
|
||||
$vm_rrd = $proxmox->get($rrd_path . $rrd_params);
|
||||
|
||||
// Check if we got a valid response with image data
|
||||
if (isset($vm_rrd['image']) && !empty($vm_rrd['image'])) {
|
||||
// Decode and re-encode the image data for template use
|
||||
$image = utf8_decode($vm_rrd['image']);
|
||||
return base64_encode($image);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// RRD data unavailable - this is normal for new VMs or during migration.
|
||||
// Log if debug mode is on, but don't crash the Client Area.
|
||||
if (Capsule::table('mod_pvewhmcs')->where('id', '1')->value('debug_mode') == 1) {
|
||||
logModuleCall(
|
||||
'pvewhmcs',
|
||||
'pvewhmcs_fetch_rrd_stat',
|
||||
'RRD fetch failed for ' . $vtype . '/' . $vmid . ' (' . $ds . ', ' . $timeframe . ')',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return null when RRD data is unavailable
|
||||
return null;
|
||||
}
|
||||
|
||||
// OUTPUT: Module output to the Client Area
|
||||
function pvewhmcs_ClientArea($params) {
|
||||
// Retrieve virtual machine info from table mod_pvewhmcs_vms
|
||||
@@ -969,103 +1026,35 @@ function pvewhmcs_ClientArea($params) {
|
||||
echo "VM/CT not found in Cluster Resources.";
|
||||
}
|
||||
|
||||
// Max CPU usage Yearly
|
||||
$rrd_params = '?timeframe=year&ds=cpu&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid . '/rrd' . $rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['cpu']['year'] = base64_encode($vm_rrd['image']);
|
||||
// ----------------------------------------------------------------
|
||||
// Fetch RRD statistics graphs from Proxmox.
|
||||
// Uses pvewhmcs_fetch_rrd_stat() for graceful error handling.
|
||||
// RRD data may be unavailable for new VMs or during PVE migration.
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Max CPU usage monthly
|
||||
$rrd_params = '?timeframe=month&ds=cpu&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['cpu']['month'] = base64_encode($vm_rrd['image']);
|
||||
// CPU usage statistics (day/week/month/year)
|
||||
$vm_statistics['cpu']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'cpu');
|
||||
$vm_statistics['cpu']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'cpu');
|
||||
$vm_statistics['cpu']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'cpu');
|
||||
$vm_statistics['cpu']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'cpu');
|
||||
|
||||
// Max CPU usage weekly
|
||||
$rrd_params = '?timeframe=week&ds=cpu&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['cpu']['week'] = base64_encode($vm_rrd['image']);
|
||||
// Memory usage statistics (day/week/month/year)
|
||||
$vm_statistics['mem']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'mem');
|
||||
$vm_statistics['mem']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'mem');
|
||||
$vm_statistics['mem']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'mem');
|
||||
$vm_statistics['mem']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'mem');
|
||||
|
||||
// Max CPU usage daily
|
||||
$rrd_params = '?timeframe=day&ds=cpu&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['cpu']['day'] = base64_encode($vm_rrd['image']);
|
||||
// Network I/O statistics (day/week/month/year)
|
||||
$vm_statistics['netinout']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'netin,netout');
|
||||
$vm_statistics['netinout']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'netin,netout');
|
||||
$vm_statistics['netinout']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'netin,netout');
|
||||
$vm_statistics['netinout']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'netin,netout');
|
||||
|
||||
// Max memory Yearly
|
||||
$rrd_params = '?timeframe=year&ds=maxmem&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['maxmem']['year'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max memory monthly
|
||||
$rrd_params = '?timeframe=month&ds=maxmem&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['maxmem']['month'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max memory weekly
|
||||
$rrd_params = '?timeframe=week&ds=maxmem&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['maxmem']['week'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max memory daily
|
||||
$rrd_params = '?timeframe=day&ds=maxmem&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['maxmem']['day'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Network rate Yearly
|
||||
$rrd_params = '?timeframe=year&ds=netin,netout&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['netinout']['year'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Network rate monthly
|
||||
$rrd_params = '?timeframe=month&ds=netin,netout&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['netinout']['month'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Network rate weekly
|
||||
$rrd_params = '?timeframe=week&ds=netin,netout&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['netinout']['week'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Network rate daily
|
||||
$rrd_params = '?timeframe=day&ds=netin,netout&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['netinout']['day'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max IO Yearly
|
||||
$rrd_params = '?timeframe=year&ds=diskread,diskwrite&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['diskrw']['year'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max IO monthly
|
||||
$rrd_params = '?timeframe=month&ds=diskread,diskwrite&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['diskrw']['month'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max IO weekly
|
||||
$rrd_params = '?timeframe=week&ds=diskread,diskwrite&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['diskrw']['week'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
// Max IO daily
|
||||
$rrd_params = '?timeframe=day&ds=diskread,diskwrite&cf=AVERAGE';
|
||||
$vm_rrd = $proxmox->get('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$guest->vmid .'/rrd'.$rrd_params) ;
|
||||
$vm_rrd['image'] = utf8_decode($vm_rrd['image']) ;
|
||||
$vm_statistics['diskrw']['day'] = base64_encode($vm_rrd['image']);
|
||||
|
||||
unset($vm_rrd) ;
|
||||
// Disk I/O statistics (day/week/month/year)
|
||||
$vm_statistics['diskrw']['year'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'year', 'diskread,diskwrite');
|
||||
$vm_statistics['diskrw']['month'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'month', 'diskread,diskwrite');
|
||||
$vm_statistics['diskrw']['week'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'week', 'diskread,diskwrite');
|
||||
$vm_statistics['diskrw']['day'] = pvewhmcs_fetch_rrd_stat($proxmox, $first_node, $guest->vtype, $guest->vmid, 'day', 'diskread,diskwrite');
|
||||
|
||||
$vm_config['vtype'] = $guest->vtype ;
|
||||
$vm_config['ipv4'] = $guest->ipaddress ;
|
||||
|
||||