Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0cce929a | ||
|
|
b0c12c4fb1 | ||
|
|
c3d10228dd | ||
|
|
3212d70db6 | ||
|
|
79a206e95b | ||
|
|
928018285c | ||
|
|
df819f2d40 | ||
|
|
1663668e7c | ||
|
|
914acf3632 | ||
|
|
fbf990d6af | ||
|
|
42cbc6d108 | ||
|
|
d18c723884 | ||
|
|
651abc4880 | ||
|
|
b1052d87c9 | ||
|
|
e21a7fc3e7 | ||
|
|
0056719950 | ||
|
|
719735b81c | ||
|
|
ef7752ab89 | ||
|
|
6186b9f896 | ||
|
|
b86ba210f2 | ||
|
|
1b56f4ac8e |
58
CHANGELOG.md
@@ -1,9 +1,63 @@
|
||||
# Changelog
|
||||
All notable changes to Proxmox VE for WHMCS will be documented in this file.
|
||||
|
||||
## [1.3.x] - TBC 2026-??-??
|
||||
## [1.3.4] - 2026-02-06 - _"IPv6, Discovery & cloud-init"_
|
||||
|
||||
https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/milestones
|
||||
### 🚀 Feature
|
||||
- IPv6 Node: Added IPv6 support to node logic & client
|
||||
- Clusters: Node auto-discovery from cloud-init templates
|
||||
- IPv4 Pool: Service Link for any IPv4 that matches a Service
|
||||
- Admin Area, Client Area: Dedicated IPv4 propagation into field
|
||||
|
||||
### 💅 Polish
|
||||
- VMs: Network config of cloned VMs via cloud-init
|
||||
- VMs: Default nameserver configuration
|
||||
- VMs: SCSI controller defaults
|
||||
- Post-clone: On-boot enact
|
||||
|
||||
## [1.3.3] - 2026-02-05 - _"Tidy Nodes & RRD"_
|
||||
|
||||
### 💅 Polish
|
||||
- Nodes tab: Improve presentation of data points
|
||||
- Client Area, RRD error: Mention migration tool (#188)
|
||||
|
||||
## [1.3.2] - 2026-01-10 - _"VNC, Cleaning, etc"_
|
||||
|
||||
### 🚀 Feature
|
||||
- Custom Fields: TPL_Node_QEMU/LXC (Template Storage Node)
|
||||
- Template Node: Honour Fields if set, else fallback (#186)
|
||||
|
||||
### 💅 Polish
|
||||
- QEMU CPU: Add AMD EPYC-Milan-v2 processor model
|
||||
- QEMU CPU: Link to Admin Guide for CPU comparisons
|
||||
- Spacing: Clean-up all files to space concatenations
|
||||
- Update Available: Links directly to the latest release
|
||||
- Naming: $srv -> $pve; $res -> $resource; $v -> $guest_type
|
||||
|
||||
### 🐛 Bug Fix
|
||||
- VNC: Resolve node as root, then connect VNC as limited user (#183)
|
||||
- nextid: No param, so we get nextid; then declare as required (#185)
|
||||
- CSS: If node authentication fails, close div so render is OK (#177)
|
||||
|
||||
## [1.3.1] - 2025-12-12 - _"Relativity & Nodes"_
|
||||
|
||||
### 💅 Polish
|
||||
- Clusters: Requests are made to the node hosting Guest (#16)
|
||||
|
||||
### 🐛 Bug Fix
|
||||
- Client Area: Images load in sub-dir installs (relative src)
|
||||
|
||||
## [1.3.0] - 2025-12-03 - _"RRD: Clients & Admins"_
|
||||
|
||||
### 🚀 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)"_
|
||||
|
||||
|
||||
@@ -10,8 +10,12 @@ This document seeks to say "cheers", "many thanks" & "love your work" to the peo
|
||||
- [@nodespacehosting](https://github.com/nodespacehosting)
|
||||
- [@WaldperlachFabi](https://github.com/WaldperlachFabi)
|
||||
- [@is7Qin](https://github.com/is7Qin)
|
||||
- [@chrismfz](https://github.com/chrismfz)
|
||||
- [@hliasa](https://github.com/hliasa)
|
||||
- [@InnocentCivilian](https://github.com/InnocentCivilian)
|
||||
- [@jdomenechg](https://github.com/jdomenechg)
|
||||
|
||||
## Why not make it even better?
|
||||
|
||||
> [!TIP]
|
||||
> You can always step up and write some improvements to the Module. Why not?
|
||||
> You can always step up and write some improvements to the Module. Why not?
|
||||
|
||||
48
README.md
@@ -22,13 +22,25 @@
|
||||
|
||||
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:**
|
||||
> [!IMPORTANT]
|
||||
> Nodes must be using RRD `PVE-$TYPE-9.0` format. You can use `proxmox-rrd-migration-tool` to migrate.
|
||||
>
|
||||
> Old RRD Dir for VMs: `/var/lib/rrdcached/db/pve2-vm/` <br>
|
||||
> New RRD Dir for VMs: `/var/lib/rrdcached/db/pve-vm-9.0/`
|
||||
>
|
||||
> You can research more online, and <a href="https://www.mail-archive.com/pve-devel@lists.proxmox.com/msg29223.html" target="_blank">here is part of a patch series</a> showing the new logic.
|
||||
|
||||
<img alt="Admin Area GUI for the Module, showing the Nodes & Guests" src="_images/zClusterGuests.png">
|
||||
**Admin Area GUI - PVE Nodes:**
|
||||
|
||||
<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!
|
||||
|
||||
@@ -46,7 +58,7 @@ https://github.com/The-Network-Crew/Proxmox-VE-for-WHMCS/
|
||||
- **(PHP)** v8.x.x (latest stable version)
|
||||
- **(PHP)** max_execution_time = 300
|
||||
- **(Proxmox)** 2 users (API & VNC)
|
||||
- **(Proxmox)** VE v8.x.x (current)
|
||||
- **(Proxmox)** VE v8.4/v9 (latest)
|
||||
|
||||
Please note specific VNC & Network requirements below - read 100% of the README.md. :-)
|
||||
|
||||
@@ -211,25 +223,31 @@ This needs configuring for each `WHMCS Admin > Products & Services` entry.
|
||||
|
||||
Firstly, create the Template VM in PVE. You need its unique PVE ID.
|
||||
|
||||
Use that ID in the Custom Field `KVMTemplate`, as in `ID|Name`.
|
||||
Secondly, use that ID in the Custom Field `KVMTemplate`, as in `ID|Name`.
|
||||
|
||||
> **Note**: `ID` is the Unique ID that your Template VM has in PVE.<br>
|
||||
> **Note**: `Name` is what will be displayed to your Clients in WHMCS.
|
||||
|
||||
Thirdly, add another Custom Field `TPL_Node_QEMU` with the node short name.
|
||||
|
||||
### VM Option 2: QEMU, WHMCS Plan + PVE ISO
|
||||
|
||||
Firstly, create the Plan in WHMCS Module. Then too in WHMCS Config > Services.
|
||||
|
||||
> Under the Service, you need to add a Custom Field `ISO` with the full location.<br>
|
||||
> This ISO must be located on the PVE Host, and not on the WHMCS installation side.
|
||||
> This ISO must be located on all PVE Nodes, and not on the WHMCS installation side.
|
||||
|
||||
### CT Option 1: LXC, PVE Template File
|
||||
|
||||
Firstly, store the Template in PVE. You need its storage, folder & File Name.
|
||||
|
||||
> Use that prefixed file name in the Custom Field `Template`, as in:<br>
|
||||
Secondly, use that prefixed file name in the Custom Field `Template`.
|
||||
|
||||
> Here is the syntax for that field, including display name:<br>
|
||||
> `local:vztmpl/ubuntu-99.99-standard_amd64.tar.gz|Ubuntu 99`
|
||||
|
||||
Thirdly, add another Custom Field `TPL_Node_LXC` with the node short name.
|
||||
|
||||
### VM/CT Import/Associate Existing Guest
|
||||
|
||||
You can associate an existing PVE Guest through the WHMCS Module too, like this:
|
||||
@@ -261,6 +279,14 @@ Create a 2nd Custom Field `Password` for the Container's root user on all CT Ser
|
||||
|
||||
### Updating to a newer release!
|
||||
|
||||
> [!WARNING]
|
||||
> There are 2x states that new Proxmox VE for WHMCS releases typically go through.
|
||||
>
|
||||
> 1. Module shows Update Available, but GitHub repo does NOT have a published release.<br>
|
||||
> In this state, it is ready for testing - but we do not recommend deploying to prod.
|
||||
> 2. Module shows Update Available, and GitHub repo DOES have a published release.<br>
|
||||
> In this state, it is tested and considered ready for production usage.
|
||||
|
||||
1. Download the new version
|
||||
2. Upload it over the top (FTP)
|
||||
3. Login to WHMCS Admin
|
||||
@@ -269,14 +295,14 @@ Create a 2nd Custom Field `Password` for the Container's root user on all CT Ser
|
||||
|
||||
> **Logging in _should_ trigger the self-upgrade procedure for the SQL database.**
|
||||
>
|
||||
> (**Beta in v1.2.x:** for now, verify yourself that updates were successful)
|
||||
> (**Beta Feature:** For now, verify yourself that updates were successful)
|
||||
|
||||
### SQL: Keeping your DB up-to-date
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since v1.3.0, logging into WHMCS Admin & opening the module should run any needed SQL Ops.
|
||||
> Since v1.3.x, logging into WHMCS Admin & opening the module should run any needed SQL Ops.
|
||||
>
|
||||
> v1.2.9 & below, consult the **_docs/UPDATE-SQL.md** file, open your SQL DB & run statements.
|
||||
> v1.2.x & below, consult the **_docs/UPDATE-SQL.md** file, open your SQL DB & run statements.
|
||||
|
||||
Then you're done with each update!
|
||||
|
||||
|
||||
|
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: 422 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 |
BIN
modules/addons/pvewhmcs/img/forbidden.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
modules/addons/pvewhmcs/img/logo-stacked.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -46,7 +46,10 @@ class PVE2_API {
|
||||
}
|
||||
// Check hostname resolves.
|
||||
if (gethostbyname($hostname) == $hostname && !filter_var($hostname, FILTER_VALIDATE_IP)) {
|
||||
throw new PVE2_Exception("PVE2 API: Cannot resolve {$hostname}.", 2);
|
||||
// Fallback: check for IPv6 (AAAA) records if IPv4 lookup failed
|
||||
if (!checkdnsrr($hostname, 'AAAA')) {
|
||||
throw new PVE2_Exception("PVE2 API: Cannot resolve {$hostname}.", 2);
|
||||
}
|
||||
}
|
||||
// Check port is between 1 and 65535.
|
||||
if (!filter_var($port, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 65535]])) {
|
||||
@@ -81,7 +84,14 @@ class PVE2_API {
|
||||
|
||||
// Perform login request.
|
||||
$prox_ch = curl_init();
|
||||
curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->hostname}:{$this->port}/api2/json/access/ticket");
|
||||
|
||||
// Handle IPv6 literals in URL
|
||||
$host_url = $this->hostname;
|
||||
if (filter_var($this->hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$host_url = '[' . $this->hostname . ']';
|
||||
}
|
||||
|
||||
curl_setopt($prox_ch, CURLOPT_URL, "https://{$host_url}:{$this->port}/api2/json/access/ticket");
|
||||
curl_setopt($prox_ch, CURLOPT_POST, true);
|
||||
curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $login_postfields_string);
|
||||
@@ -171,7 +181,7 @@ class PVE2_API {
|
||||
private function action ($action_path, $http_method, $put_post_parameters = null) {
|
||||
// Check if we have a prefixed / on the path, if not add one.
|
||||
if (substr($action_path, 0, 1) != "/") {
|
||||
$action_path = "/".$action_path;
|
||||
$action_path = "/" . $action_path;
|
||||
}
|
||||
|
||||
if (!$this->check_login_ticket()) {
|
||||
@@ -180,7 +190,14 @@ class PVE2_API {
|
||||
|
||||
// Prepare cURL resource.
|
||||
$prox_ch = curl_init();
|
||||
curl_setopt($prox_ch, CURLOPT_URL, "https://{$this->hostname}:{$this->port}/api2/json{$action_path}");
|
||||
|
||||
// Handle IPv6 literals in URL
|
||||
$host_url = $this->hostname;
|
||||
if (filter_var($this->hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$host_url = '[' . $this->hostname . ']';
|
||||
}
|
||||
|
||||
curl_setopt($prox_ch, CURLOPT_URL, "https://{$host_url}:{$this->port}/api2/json{$action_path}");
|
||||
|
||||
$put_post_http_headers = array();
|
||||
$put_post_http_headers[] = "CSRFPreventionToken: {$this->login_ticket['CSRFPreventionToken']}";
|
||||
@@ -225,7 +242,7 @@ class PVE2_API {
|
||||
|
||||
curl_setopt($prox_ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=".$this->login_ticket['ticket']);
|
||||
curl_setopt($prox_ch, CURLOPT_COOKIE, "PVEAuthCookie=" . $this->login_ticket['ticket']);
|
||||
curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
|
||||
@@ -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">Kernel</span> <span class="spec-sublabel">(OS)</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/migrate RRD Data using: <code>proxmox-rrd-migration-tool</code>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.4 KiB |