mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 19:23:31 +00:00
Fix: Improve table layout and consistency across tabs
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Feature Idea: Detailed Disk Information (PVE)
|
||||
|
||||
**Goal:** Display more granular disk information for VMs and Containers beyond just the current total I/O.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* Individual virtual disk usage/size.
|
||||
* Disk throughput per virtual disk (if available separately from total I/O).
|
||||
* Physical disk S.M.A.R.T. health status for the underlying host nodes.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/{type}/{vmid}/config`: Contains configuration details for virtual disks attached to a VM/CT (size, storage, format).
|
||||
* `/nodes/{node}/disks/list`: Lists physical disks on the node.
|
||||
* `/nodes/{node}/disks/smart?disk={diskname}`: Retrieves S.M.A.R.T. data for a specific physical disk on the node (requires `Disk.Audit` or higher permissions).
|
||||
* `/nodes/{node}/{type}/{vmid}/status/current`: Already used, provides aggregate disk I/O (`diskread`, `diskwrite`). Check if this contains per-disk details already.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires parsing the `config` endpoint to identify virtual disks.
|
||||
* Fetching S.M.A.R.T. data requires an additional API call per physical disk and potentially higher permissions than `PVEAuditor`.
|
||||
* Consider how to display this: added columns, a modal/popup, or a separate "details" view.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Feature Idea: Network Interface Details (PVE)
|
||||
|
||||
**Goal:** Display configuration details for network interfaces attached to VMs and Containers.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* MAC Address per interface.
|
||||
* Connected Bridge/Network.
|
||||
* VLAN Tag (if applicable).
|
||||
* Model/Type of virtual NIC.
|
||||
* Potentially live throughput per interface (if API provides this separately from total).
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/{type}/{vmid}/config`: Contains configuration details for network interfaces (e.g., `net0`, `net1`) including MAC, bridge, tag, model.
|
||||
* `/nodes/{node}/{type}/{vmid}/status/current`: Already used, provides aggregate network I/O (`netin`, `netout`). Check if this contains per-interface details already.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires parsing the `config` endpoint to identify and extract details for each network interface (e.g., `net0`, `net1`, ...).
|
||||
* Displaying live per-interface stats might not be feasible or may already be aggregated in the `/status/current` data.
|
||||
* Focus on displaying the static configuration first (MAC, bridge, VLAN).
|
||||
* Consider how to display: added columns (might get wide), a modal/popup, or a details view.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Feature Idea: VM/CT Backup Status (PVE)
|
||||
|
||||
**Goal:** Show the status or timestamp of the last backup taken *by Proxmox VE itself* (snapshot backups, not necessarily PBS) for each VM and Container.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* Timestamp of the last successful PVE snapshot/backup.
|
||||
* Name/description of the last snapshot.
|
||||
* Indication if a backup task is currently running for the guest.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/{type}/{vmid}/snapshot`: Lists all snapshots for the specific VM or Container. This includes backups created via the PVE GUI/API. Need to parse the output to find the most recent one, potentially filtering by name convention if backups have specific names.
|
||||
* `/nodes/{node}/tasks`: Could potentially show running backup tasks related to a specific VMID, but filtering might be complex.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires an additional API call per VM/CT (`/snapshot`). Consider performance implications if there are many guests.
|
||||
* Parsing the snapshot list is needed to identify the most recent backup.
|
||||
* PBS backups are handled separately; this focuses only on backups visible directly within PVE for the guest.
|
||||
* Display could be a dedicated column ("Last PVE Backup") or integrated into a status indicator.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Feature Idea: Hardware/PCI Passthrough Info (PVE)
|
||||
|
||||
**Goal:** Display information about hardware devices (PCI, USB) passed through to specific VMs.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* List of PCI devices passed through (Vendor/Device ID, Name).
|
||||
* List of USB devices passed through.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/qemu/{vmid}/config`: Contains configuration details for VMs, including `hostpci` entries for PCI passthrough and `usb` entries for USB passthrough.
|
||||
* `/nodes/{node}/hardware/pci`: Lists available PCI devices on the host.
|
||||
* `/nodes/{node}/hardware/usb`: Lists available USB devices on the host.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires parsing the VM's `config` endpoint.
|
||||
* This information is static configuration.
|
||||
* Likely only relevant for a subset of users.
|
||||
* Display could be in a details view or a modal, as it's not primary monitoring data.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Feature Idea: Guest IP Addresses (PVE)
|
||||
|
||||
**Goal:** Display the IP addresses assigned *within* the guest operating system for VMs.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* IPv4 addresses.
|
||||
* IPv6 addresses.
|
||||
* Associated MAC address (to link IP to a NIC).
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/qemu/{vmid}/agent/get-network-ifs`: Retrieves network interface details, including IP addresses, directly from the guest OS via the QEMU Guest Agent.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* **Crucially depends on the QEMU Guest Agent being installed and running within the VM.** If the agent is not present or running, the API call will fail or return no data.
|
||||
* Requires an additional API call per running VM.
|
||||
* Need robust error handling for when the agent is unavailable.
|
||||
* UI should clearly indicate if IPs are missing due to the agent not running.
|
||||
* Consider how to display multiple IPs/interfaces (e.g., tooltip, comma-separated list in a column).
|
||||
* This is likely **high-value** information for users.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Feature Idea: Firewall Rules (PVE)
|
||||
|
||||
**Goal:** Provide visibility into the Proxmox VE firewall rules.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* Display firewall rules configured at the Datacenter level.
|
||||
* Display firewall rules configured at the Node level.
|
||||
* Display firewall rules configured per VM/CT.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/cluster/firewall/rules`: Gets firewall rules defined at the datacenter level.
|
||||
* `/nodes/{node}/firewall/rules`: Gets firewall rules defined for a specific node.
|
||||
* `/nodes/{node}/{type}/{vmid}/firewall/rules`: Gets firewall rules defined for a specific VM or Container.
|
||||
* Other related endpoints under `/cluster/firewall/`, `/nodes/{node}/firewall/`, `/nodes/{node}/{type}/{vmid}/firewall/` for options, groups, aliases etc.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Firewall rules can be numerous and complex.
|
||||
* Displaying this effectively in the main dashboard is likely impractical.
|
||||
* Might be better suited for a dedicated "Firewall" view or section within the application.
|
||||
* Requires careful consideration of which rules (datacenter, node, guest) are most relevant to display and how.
|
||||
* `PVEAuditor` role should be sufficient for reading rules.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Feature Idea: High Availability (HA) Group Information (PVE)
|
||||
|
||||
**Goal:** Display more detailed information about the High Availability configuration and status.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* List of HA groups configured.
|
||||
* Members (VMs/CTs) within each HA group.
|
||||
* Current state of HA services/resources (e.g., master node, resource states).
|
||||
* HA fencing status/configuration.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/cluster/ha/resources`: Currently used, lists resources managed by HA.
|
||||
* `/cluster/ha/groups`: Lists the configured HA groups and their settings (nodes, priorities).
|
||||
* `/cluster/ha/status/current`: Provides the current status of the HA manager, including master node and resource states.
|
||||
* `/cluster/ha/status/manager_status`: Detailed status of the HA manager.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires additional API calls to `/groups` and `/status/current`.
|
||||
* Could be displayed in a dedicated HA status section or integrated subtly into the existing views (e.g., indicating which guests are HA managed).
|
||||
* Useful for users relying on Proxmox HA features.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Feature Idea: PBS Task Log Summary
|
||||
|
||||
**Goal:** Provide a quick view of the end of the task log, especially for failed Proxmox Backup Server (PBS) tasks.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* Display the last N lines (e.g., 5-10) of a task log.
|
||||
* Show the exit status or error message from the log.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/tasks/{upid}/log`: Fetches the log content for a specific task UPID.
|
||||
* `/nodes/{node}/tasks/{upid}/status`: Fetches the final status of a completed task.
|
||||
* `/nodes/{node}/tasks`: Currently used to list tasks.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires an additional API call per task for which logs are needed (likely only failed or recent tasks).
|
||||
* The `/log` endpoint returns the full log; need to process the response to extract only the last few lines (or use API parameters if available to limit the output, e.g., `?start=X&limit=Y`). Check API docs for log limiting parameters.
|
||||
* Could be displayed in a modal when clicking on a task, or inline if kept very brief.
|
||||
* Helps quickly diagnose failures without needing to open the PBS UI.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Feature Idea: PBS Datastore Verification/GC Schedule
|
||||
|
||||
**Goal:** Display the configured schedules for maintenance jobs (Verification, Garbage Collection) on Proxmox Backup Server (PBS) datastores.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* Garbage collection schedule (e.g., "daily at 02:00").
|
||||
* Verification schedule.
|
||||
* Pruning schedule associated with the datastore.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/config/datastore`: Lists configured datastores.
|
||||
* `/config/datastore/{store}`: Gets the detailed configuration for a specific datastore (`{store}`), which includes schedule properties for `gc-schedule` and `verify-schedule`.
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* Requires fetching the configuration for each datastore listed by `/config/datastore`.
|
||||
* The schedule format returned by the API needs to be parsed into a human-readable string.
|
||||
* Could be displayed alongside the datastore usage information already fetched.
|
||||
* Provides useful context about the maintenance routines configured on the PBS.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Feature Idea: PBS Tape Backup Information
|
||||
|
||||
**Goal:** Display status and information related to tape backup operations within Proxmox Backup Server (PBS), if used.
|
||||
|
||||
**Potential Data:**
|
||||
|
||||
* List of configured tape drives and their status.
|
||||
* Status of ongoing or recent tape backup/restore jobs.
|
||||
* Information about media pools and tapes.
|
||||
|
||||
**Relevant API Endpoints:**
|
||||
|
||||
* `/nodes/{node}/tape/drive`: Lists tape drives and their status.
|
||||
* `/nodes/{node}/tape/job`: Lists tape backup/restore jobs.
|
||||
* `/nodes/{node}/tape/pool`: Lists media pools.
|
||||
* `/nodes/{node}/tape/media`: Lists tapes (media).
|
||||
* Various endpoints under these for specific details (e.g., job logs, drive status).
|
||||
|
||||
**Implementation Notes:**
|
||||
|
||||
* This is a niche feature, only relevant if the user has configured tape backups in PBS.
|
||||
* Requires multiple additional API calls.
|
||||
* Consider adding a dedicated section or view for Tape Backup status, only visible if tape drives are detected or configured.
|
||||
* `Audit` role might be sufficient, but needs verification.
|
||||
+88
-46
@@ -209,8 +209,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
style="width: ${numericPercent}%;"
|
||||
>
|
||||
</div>
|
||||
<!-- Text is centered within the outer container -->
|
||||
<span class="absolute inset-0 flex items-center justify-center text-xs font-medium ${textColorClass} px-1 truncate">
|
||||
<!-- Text is centered within the outer container - Add whitespace-nowrap -->
|
||||
<span class="absolute inset-0 flex items-center justify-center text-xs font-medium ${textColorClass} px-1 truncate whitespace-nowrap">
|
||||
${text}
|
||||
</span>
|
||||
</div>
|
||||
@@ -594,7 +594,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const diskColorClass = getUsageColor(diskPercent);
|
||||
|
||||
// Create tooltips and bar HTML using correct fields
|
||||
const cpuTooltipText = `${cpuPercent.toFixed(1)}%`;
|
||||
const cpuTooltipText = `${cpuPercent.toFixed(1)}%${node.maxcpu && node.maxcpu > 0 ? ` (${(node.cpu * node.maxcpu).toFixed(1)}/${node.maxcpu} cores)` : ''}`;
|
||||
const memTooltipText = `${formatBytes(memUsed)} / ${formatBytes(memTotal)} (${memPercent.toFixed(1)}%)`;
|
||||
// ---- START DEBUG LOG ----
|
||||
// console.log(`[Node: ${node.node}] diskUsed raw: ${diskUsed}, diskTotal raw: ${diskTotal}`);
|
||||
@@ -633,10 +633,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap font-medium text-gray-900 dark:text-gray-100" title="${node.node || 'N/A'}">${node.node || 'N/A'}</td>
|
||||
<!-- Add metric-tooltip-trigger and data-tooltip for custom tooltip -->
|
||||
<td class="p-1 px-2 text-right">${cpuBarHTML}</td>
|
||||
<td class="p-1 px-2 text-right">${memoryBarHTML}</td>
|
||||
<td class="p-1 px-2 text-right">${diskBarHTML}</td>
|
||||
<!-- Add min-width to progress bar columns -->
|
||||
<td class="p-1 px-2 text-right min-w-[200px]">${cpuBarHTML}</td>
|
||||
<td class="p-1 px-2 text-right min-w-[200px]">${memoryBarHTML}</td>
|
||||
<td class="p-1 px-2 text-right min-w-[200px]">${diskBarHTML}</td>
|
||||
<td class="p-1 px-2 text-right whitespace-nowrap">${uptimeFormatted}</td>
|
||||
<td class="p-1 px-2 text-right whitespace-nowrap">${normalizedLoadFormatted}</td>
|
||||
`;
|
||||
@@ -889,19 +889,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Create ONE table for all nodes
|
||||
const table = document.createElement('table');
|
||||
table.className = 'w-full text-sm border-collapse table-fixed';
|
||||
// Add min-w-full and table-auto for consistency and scrolling
|
||||
table.className = 'w-full text-sm border-collapse table-auto min-w-full';
|
||||
|
||||
const thead = document.createElement('thead');
|
||||
// Define widths for most columns, leave Usage column without width
|
||||
// REMOVED explicit width classes (w-X/Y, w-[...px])
|
||||
thead.innerHTML = `
|
||||
<tr class="border-b border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-3/12">Storage</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-2/12">Content</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-1/12">Type</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-[80px]">Shared</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-[150px]">Usage</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-1/12">Avail</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300 w-1/12">Total</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Storage</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Content</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Type</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Shared</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Usage</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Avail</th>
|
||||
<th class="text-left p-2 px-3 font-semibold text-gray-700 dark:text-gray-300">Total</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
@@ -964,19 +966,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const contentBadges = contentTypes.map(ct => {
|
||||
const details = getContentBadgeDetails(ct); // Use the updated helper
|
||||
// Re-add data-tooltip with the purpose, add trigger class and cursor
|
||||
return `<span data-tooltip="${details.tooltip}" class="storage-tooltip-trigger inline-block ${details.badgeClass} rounded px-1.5 py-0.5 text-xs font-medium mr-1 mb-1 cursor-default">${ct}</span>`;
|
||||
return `<span data-tooltip="${details.tooltip}" class="storage-tooltip-trigger inline-block ${details.badgeClass} rounded px-1.5 py-0.5 text-xs font-medium mr-1 cursor-default">${ct}</span>`;
|
||||
}).join('');
|
||||
|
||||
const usageBarHTML = createProgressTextBarHTML(usagePercent, usageTooltipText, usageColorClass);
|
||||
|
||||
// Standardize padding to p-1 px-2 like other tables
|
||||
row.innerHTML = `
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-900 dark:text-gray-100 font-medium">${store.storage || 'N/A'}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs">${contentBadges || '-'}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-600 dark:text-gray-300">${store.type || 'N/A'}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-center storage-tooltip-trigger cursor-default" data-tooltip="${sharedIconTooltip}">${sharedIcon}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-600 dark:text-gray-300">${usageBarHTML}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-600 dark:text-gray-300 text-right">${formatBytes(store.avail)}</td>
|
||||
<td class="p-2 px-3 py-1 whitespace-nowrap text-gray-600 dark:text-gray-300 text-right">${formatBytes(store.total)}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-900 dark:text-gray-100 font-medium">${store.storage || 'N/A'}</td>
|
||||
<!-- Add flex items-center for vertical alignment -->
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs flex items-center">${contentBadges || '-'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${store.type || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-center storage-tooltip-trigger cursor-default" data-tooltip="${sharedIconTooltip}">${sharedIcon}</td>
|
||||
<!-- Remove min-width to allow auto-sizing -->
|
||||
<!-- Remove whitespace-nowrap from parent TD -->
|
||||
<!-- RE-ADD min-width -->
|
||||
<td class="p-1 px-2 text-gray-600 dark:text-gray-300 min-w-[250px]">${usageBarHTML}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-right">${formatBytes(store.avail)}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-right">${formatBytes(store.total)}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -1315,9 +1322,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function createGuestRow(guest) {
|
||||
// console.log('[createGuestRow] Received guest data:', guest);
|
||||
const row = document.createElement('tr');
|
||||
// Add more prominent hover background, shadow, lift effect, and transition
|
||||
// Also add opacity and grayscale for stopped guests
|
||||
row.className = `transition-all duration-150 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md hover:-translate-y-px ${guest.status === 'stopped' ? 'opacity-60 grayscale' : ''}`;
|
||||
// ---> MODIFIED: Remove responsive classes from the row <---
|
||||
row.className = `border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 ${guest.status === 'stopped' ? 'opacity-60 grayscale' : ''}`;
|
||||
// ---> END MODIFIED <---
|
||||
row.setAttribute('data-name', guest.name.toLowerCase());
|
||||
row.setAttribute('data-type', guest.type.toLowerCase());
|
||||
row.setAttribute('data-node', guest.node.toLowerCase());
|
||||
@@ -1334,17 +1341,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (guest.status === 'running') {
|
||||
const cpuPercent = Math.round(guest.cpu * 100);
|
||||
const memoryPercent = guest.memory;
|
||||
const memoryPercent = guest.memory;
|
||||
const diskPercent = guest.disk;
|
||||
|
||||
// Revert to original calculation using guest.cpus
|
||||
const cpuTooltipText = `${cpuPercent}% ${guest.cpus ? `(${(guest.cpu * guest.cpus).toFixed(1)}/${guest.cpus} cores)` : ''}`;
|
||||
const memoryTooltipText = guest.memoryTotal ? `${formatBytesInt(guest.memoryCurrent)} / ${formatBytesInt(guest.memoryTotal)} (${memoryPercent}%)` : `${memoryPercent}%`;
|
||||
const diskTooltipText = guest.diskTotal ? `${formatBytesInt(guest.diskCurrent)} / ${formatBytesInt(guest.diskTotal)} (${diskPercent}%)` : `${diskPercent}%`;
|
||||
|
||||
|
||||
const cpuColorClass = getUsageColor(cpuPercent);
|
||||
const memColorClass = getUsageColor(memoryPercent);
|
||||
const diskColorClass = getUsageColor(diskPercent);
|
||||
|
||||
|
||||
cpuBarHTML = createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass);
|
||||
memoryBarHTML = createProgressTextBarHTML(memoryPercent, memoryTooltipText, memColorClass);
|
||||
diskBarHTML = createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass);
|
||||
@@ -1357,7 +1365,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- End calculation block ---
|
||||
|
||||
const typeIconClass = guest.type === 'VM'
|
||||
? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium'
|
||||
? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium'
|
||||
: 'ct-icon bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 px-1.5 py-0.5 font-medium';
|
||||
const typeIcon = `<span class="type-icon inline-block rounded text-xs align-middle ${typeIconClass}">${guest.type}</span>`;
|
||||
|
||||
@@ -1374,9 +1382,47 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<td class="p-1 px-2 text-right whitespace-nowrap">${netInFormatted}</td>
|
||||
<td class="p-1 px-2 text-right whitespace-nowrap">${netOutFormatted}</td>
|
||||
`;
|
||||
|
||||
// ---> ADDED: Loop through TDs to add responsive classes <---
|
||||
// REMOVED: Responsive class and data-label logic
|
||||
/*
|
||||
const cells = row.querySelectorAll('td');
|
||||
cells.forEach(td => {
|
||||
td.classList.add('block', 'sm:table-cell', 'px-3', 'py-2', 'align-middle', 'text-sm'); // Add base responsive and style classes
|
||||
// Add a pseudo-element for the label on mobile if needed
|
||||
const headerText = getHeaderTextForCell(td); // You'll need to implement getHeaderTextForCell
|
||||
if (headerText) {
|
||||
td.setAttribute('data-label', headerText);
|
||||
}
|
||||
});
|
||||
*/
|
||||
// ---> END ADDED <---
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// --- ADDED HELPER FUNCTION ---
|
||||
function getHeaderTextForCell(td) {
|
||||
try {
|
||||
const cellIndex = Array.from(td.parentNode.children).indexOf(td);
|
||||
const headerRow = document.querySelector('#main-table thead tr');
|
||||
if (!headerRow) {
|
||||
console.warn("Header row not found for #main-table");
|
||||
return 'Unknown Header';
|
||||
}
|
||||
const headerCell = headerRow.children[cellIndex];
|
||||
if (!headerCell) {
|
||||
console.warn(`Header cell not found at index ${cellIndex}`);
|
||||
return 'Unknown Header';
|
||||
}
|
||||
return headerCell.textContent || headerCell.innerText || 'Unknown Header';
|
||||
} catch (error) {
|
||||
console.error("Error getting header text for cell:", error, td);
|
||||
return 'Error Header';
|
||||
}
|
||||
}
|
||||
// --- END ADDED HELPER FUNCTION ---
|
||||
|
||||
// --- WebSocket Message Handling ---
|
||||
// Add a generic listener to catch *any* events from the server
|
||||
socket.onAny((eventName, ...args) => {
|
||||
@@ -1475,12 +1521,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// ---> END CHANGE
|
||||
|
||||
function requestFullData() {
|
||||
// console.log("Requesting full data...");
|
||||
if (socket.connected) {
|
||||
socket.emit('requestData'); // Standard emit
|
||||
} else {
|
||||
console.warn("Socket not connected, cannot request full data.");
|
||||
}
|
||||
console.log('Requesting full data reload from server...');
|
||||
socket.emit('requestData'); // Ensure this uses the correct event name
|
||||
}
|
||||
|
||||
// --- Function to Reset Dashboard Filters/Sort ---
|
||||
@@ -1854,11 +1896,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-150 ease-in-out';
|
||||
row.innerHTML = `
|
||||
<td class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300">${target}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">${statusIcon}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${startTime}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${duration}</td>
|
||||
<td class="px-3 py-2 text-xs font-mono text-gray-400 dark:text-gray-500 truncate" title="${upid}">${shortUpid}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">${target}</td>
|
||||
<td class="px-4 py-2 text-sm text-center">${statusIcon}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${startTime}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${duration}</td>
|
||||
<td class="px-4 py-2 text-xs font-mono text-gray-400 dark:text-gray-500 truncate" title="${upid}">${shortUpid}</td>
|
||||
`; // Display shortened UPID, full UPID in title
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
@@ -1885,11 +1927,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors duration-150 ease-in-out';
|
||||
row.innerHTML = `
|
||||
<td class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300">${target}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">${statusIcon}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${startTime}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${duration}</td>
|
||||
<td class="px-3 py-2 text-xs font-mono text-gray-400 dark:text-gray-500 truncate" title="${upid}">${shortUpid}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">${target}</td>
|
||||
<td class="px-4 py-2 text-sm text-center">${statusIcon}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${startTime}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">${duration}</td>
|
||||
<td class="px-4 py-2 text-xs font-mono text-gray-400 dark:text-gray-500 truncate" title="${upid}">${shortUpid}</td>
|
||||
`; // Display shortened UPID, full UPID in title
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
|
||||
+32
-33
@@ -33,12 +33,11 @@
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 p-2 font-sans">
|
||||
<div class="container max-w-[95%] mx-auto">
|
||||
<!-- Header - removed bottom border and padding -->
|
||||
<div class="header flex justify-between items-center mb-2">
|
||||
<!-- Empty div for spacing, allows logo to center -->
|
||||
<div class="flex-1"></div>
|
||||
<!-- Logo and Title Container (Centered) -->
|
||||
<div class="flex items-center gap-1 flex-none">
|
||||
<!-- Header - Add flex-col sm:flex-row for stacking on small screens -->
|
||||
<div class="header flex flex-col sm:flex-row justify-between items-center mb-2">
|
||||
<!-- Removed flex-1 spacer -->
|
||||
<!-- Logo and Title Container (Centered by default, pushed left on sm+) -->
|
||||
<div class="flex items-center gap-1 flex-none mb-2 sm:mb-0">
|
||||
<!-- Pulse Logo SVG -->
|
||||
<svg width="20" height="20" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" class="pulse-logo">
|
||||
<title>Pulse Logo</title>
|
||||
@@ -47,8 +46,8 @@
|
||||
</svg>
|
||||
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">Pulse</span>
|
||||
</div>
|
||||
<!-- Header Controls (Aligned Right) -->
|
||||
<div class="header-controls flex flex-1 justify-end items-center gap-4">
|
||||
<!-- Header Controls (Full width on small, aligned right on sm+) -->
|
||||
<div class="header-controls flex w-full sm:w-auto sm:flex-1 justify-center sm:justify-end items-center gap-4">
|
||||
<!-- Start: UI Scale Slider -->
|
||||
<!-- <div class="ui-scale-slider flex items-center opacity-50 hover:opacity-100 focus-within:opacity-100 transition-opacity duration-300" title="Adjust UI Scale" style="gap: 4px;">
|
||||
<input type="range" id="ui-scale-slider" min="0.75" max="1.25" step="0.05" value="1.0" class="cursor-pointer accent-blue-600 dark:accent-blue-500" style="width: 80px; height: 16px;">
|
||||
@@ -71,7 +70,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs flex mb-2 border-b border-gray-300 dark:border-gray-700">
|
||||
<div class="tabs flex mb-2 border-b border-gray-300 dark:border-gray-700 overflow-x-auto whitespace-nowrap">
|
||||
<!-- Add Tailwind classes, JS will toggle active classes -->
|
||||
<div class="tab active px-3 py-1.5 cursor-pointer bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 rounded-t text-sm -mb-px flex items-center gap-1.5" data-tab="main">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
@@ -102,16 +101,16 @@
|
||||
<div id="nodes" class="tab-content hidden bg-white dark:bg-gray-800 rounded-b rounded-tr shadow p-3 mb-2">
|
||||
<h2 class="text-lg font-medium mb-2 text-gray-800 dark:text-gray-200">Nodes</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table id="nodes-table" class="min-w-full divide-y divide-gray-700 table-fixed">
|
||||
<table id="nodes-table" class="min-w-full divide-y divide-gray-700 table-auto text-sm">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th scope="col" class="sortable px-6 py-3 w-1/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="status">Status</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-3/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="node">Node Name</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-2/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="cpu">CPU</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-2/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="memory">Mem</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-2/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="disk">Disk</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-1/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="uptime">Uptime</th>
|
||||
<th scope="col" class="sortable px-6 py-3 w-1/12 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="loadnorm">Load (Norm)</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="status">Status</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="node">Node Name</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="cpu">CPU</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="memory">Mem</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="disk">Disk</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="uptime">Uptime</th>
|
||||
<th scope="col" class="sortable p-1 px-2 text-left font-semibold cursor-pointer select-none whitespace-nowrap" data-sort="loadnorm">Load (Norm)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="nodes-table-body" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -168,10 +167,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- --- Table Container --- -->
|
||||
<div class="table-container max-h-[80vh] overflow-y-auto mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
||||
<div class="table-container max-h-[80vh] overflow-y-auto overflow-x-auto mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
||||
<table id="main-table" class="w-full table-fixed text-sm">
|
||||
<!-- Restore colgroup -->
|
||||
<colgroup>
|
||||
<!-- Define col widths using style attribute for Tailwind compatibility -->
|
||||
<col class="name-col" style="width: var(--name-col-width, 150px);">
|
||||
<col class="type-col" style="width: 50px;">
|
||||
<col class="id-col" style="width: 50px;">
|
||||
@@ -186,19 +185,19 @@
|
||||
</colgroup>
|
||||
<thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-800">
|
||||
<!-- Apply sticky header styles -->
|
||||
<tr class="border-b border-gray-300 dark:border-gray-700">
|
||||
<!-- Header cells with Tailwind classes -->
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="name">Name</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="type">Type</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="id">ID</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="uptime">Uptime</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="cpu">CPU</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="memory">Mem</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="disk">Disk</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="diskread">Disk Read</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="diskwrite">Disk Write</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="netin">Net In</th>
|
||||
<th class="sortable text-left p-1 px-2 font-semibold text-gray-700 dark:text-gray-300 cursor-pointer select-none whitespace-nowrap" data-sort="netout">Net Out</th>
|
||||
<tr class="text-xs font-medium tracking-wider text-left text-gray-600 uppercase bg-gray-100 dark:bg-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
|
||||
<!-- Ensure padding matches createGuestRow -->
|
||||
<th class="sortable p-1 px-2" data-sort="name" style="width: var(--name-col-width, 150px);">Name</th>
|
||||
<th class="p-1 px-1 text-center" style="width: 50px;">Type</th> <!-- Keep centered, adjust padding -->
|
||||
<th class="sortable p-1 px-2 text-center" data-sort="id" style="width: 50px;">ID</th> <!-- Keep centered -->
|
||||
<th class="sortable p-1 px-2" data-sort="uptime" style="width: var(--uptime-col-width, 80px);">Uptime</th>
|
||||
<th class="sortable p-1 px-2" data-sort="cpu" style="width: 110px;">CPU</th>
|
||||
<th class="sortable p-1 px-2" data-sort="memory" style="width: 110px;">Memory</th>
|
||||
<th class="sortable p-1 px-2" data-sort="disk" style="width: 110px;">Disk</th>
|
||||
<th class="sortable p-1 px-2 text-right" data-sort="diskread" style="width: 70px;">Read</th>
|
||||
<th class="sortable p-1 px-2 text-right" data-sort="diskwrite" style="width: 70px;">Write</th>
|
||||
<th class="sortable p-1 px-2 text-right" data-sort="netin" style="width: 70px;">Net In</th>
|
||||
<th class="sortable p-1 px-2 text-right" data-sort="netout" style="width: 70px;">Net Out</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -219,7 +218,7 @@
|
||||
<div id="storage" class="tab-content hidden bg-white dark:bg-gray-800 rounded-b rounded-tr shadow p-3 mb-2">
|
||||
<h2 class="text-lg font-medium mb-2 text-gray-800 dark:text-gray-200">Storage Information</h2>
|
||||
<!-- Add table container wrapper similar to main dashboard -->
|
||||
<div class="table-container max-h-[80vh] overflow-y-auto mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
||||
<div class="table-container max-h-[80vh] overflow-y-auto overflow-x-auto mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
|
||||
<div id="storage-info-content">
|
||||
<p class="text-gray-500 dark:text-gray-400 p-4 text-center">Loading storage data...</p> <!-- Keep placeholder -->
|
||||
<!-- Storage table will be populated here by JS -->
|
||||
|
||||
Reference in New Issue
Block a user