From 9e899cb947fd77f731c76e917e715fac6949cdc3 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Sat, 28 Mar 2026 11:38:56 -0400 Subject: [PATCH] feat(proxmox): rebuild statistics UI and client runtime New Blade layout, copy, and visual system for the monitoring tab; RRD hosts use data-statistics-chart hooks; tables use pxStatTableKit and data-statistics-data-row; network and comparison panels use new selectors; CSV helpers renamed (pxExport*); modals and telemetry refreshed. Made-with: Cursor --- .../statistics/comparison-inner.blade.php | 322 + .../statistics/history-inner.blade.php | 433 ++ .../statistics/lifecycle-inner.blade.php | 427 ++ .../statistics/modals-inner.blade.php | 201 + .../statistics/network-inner.blade.php | 1507 +++++ .../statistics/scripts-inner.blade.php | 2008 ++++++ .../partials/statistics/tasks-inner.blade.php | 505 ++ .../statistics/telemetry-inner.blade.php | 308 + .../Proxmox/views/statistics.blade.php | 5921 +---------------- 9 files changed, 5856 insertions(+), 5776 deletions(-) create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/comparison-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/history-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/lifecycle-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/modals-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/network-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/scripts-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/tasks-inner.blade.php create mode 100644 extensions/Servers/Proxmox/views/partials/statistics/telemetry-inner.blade.php diff --git a/extensions/Servers/Proxmox/views/partials/statistics/comparison-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/comparison-inner.blade.php new file mode 100644 index 000000000..545ffa039 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/comparison-inner.blade.php @@ -0,0 +1,322 @@ + + @php + $historicalComparison = $this->historicalComparison; + @endphp +
+
+
+
+ +
+
+
+

Historical Data Comparison

+
+ + + + + +
+
+
+
+
+ +
+
+ +
+ +
+
+ + + @if($historicalPeriod === 'custom') + + @endif +
+ +
+ + + @if($currentPeriod === 'custom') + + @endif +
+
+
+ + @if($historicalComparison && !isset($historicalComparison['error'])) + + @php + $current = $historicalComparison['current']; + $historical = $historicalComparison['historical']; + $diff = $historicalComparison['differences']; + @endphp + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric +
+ + Historical + ({{ $historical->DateTime->format('M d, Y H:i') }}) +
+
+
+ + Current + ({{ $current->DateTime->format('M d, Y H:i') }}) +
+
Change
+
+ + Processor Utilization +
+
+ {{ number_format($diff['cpu']['historical_percent'], 1) }}% + + {{ number_format($diff['cpu']['current_percent'], 1) }}% + + + {{ $diff['cpu']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['cpu']['percent_change'], 1) }}% + +
+
+ + Memory Utilization +
+
+ {{ number_format($diff['memory']['historical_percent'], 1) }}% + + {{ number_format($diff['memory']['current_percent'], 1) }}% + + + {{ $diff['memory']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['memory']['percent_change'], 1) }}% + +
+
+ + Disk Read Rate +
+
+ {{ $this->formatBytes($diff['disk_read']['historical_rate']) }}/s + + {{ $this->formatBytes($diff['disk_read']['current_rate']) }}/s + + + {{ $diff['disk_read']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['disk_read']['percent_change'], 1) }}% + +
+
+ + Disk Write Rate +
+
+ {{ $this->formatBytes($diff['disk_write']['historical_rate']) }}/s + + {{ $this->formatBytes($diff['disk_write']['current_rate']) }}/s + + + {{ $diff['disk_write']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['disk_write']['percent_change'], 1) }}% + +
+
+ + Network Download Rate +
+
+ {{ $this->formatBytes($diff['network_in']['historical_rate']) }}/s + + {{ $this->formatBytes($diff['network_in']['current_rate']) }}/s + + + {{ $diff['network_in']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['network_in']['percent_change'], 1) }}% + +
+
+ + Network Upload Rate +
+
+ {{ $this->formatBytes($diff['network_out']['historical_rate']) }}/s + + {{ $this->formatBytes($diff['network_out']['current_rate']) }}/s + + + {{ $diff['network_out']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['network_out']['percent_change'], 1) }}% + +
+
+ @elseif($historicalComparison && isset($historicalComparison['error'])) +
+
+
+
+ +
+
+

{{ $historicalComparison['message'] }}

+ @if(isset($historicalComparison['total_stats'])) +

+ Total statistics in database: {{ $historicalComparison['total_stats'] }} + @if($historicalComparison['total_stats'] == 0) + - Statistics are collected automatically every hour. You can also create manual statistics using the "Create Statistics" button above. + @elseif($historicalComparison['total_stats'] < 2) + - At least 2 statistics are needed for comparison. Wait for more data to be collected or create a manual statistic. + @endif +

+ @endif +
+
+
+
+ @else +
+
+
+ +
+
+

No comparison data available

+

Please select periods with available statistics data.

+
+
+
+ @endif +
+ + diff --git a/extensions/Servers/Proxmox/views/partials/statistics/history-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/history-inner.blade.php new file mode 100644 index 000000000..492029819 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/history-inner.blade.php @@ -0,0 +1,433 @@ + + @php + $statisticsExportRows = []; + @endphp +
+
+
+
+ +
+
+
+

Archived guest samples

+
+ + + + + +
+
+
+
+
+ + + +
+ Capture now +
+
+ Saving… +
+
+ + +
+
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + @forelse($this->statistics as $stat) + @php + $networkStats = $this->getCurrentNetworkStats($stat); + $diskStats = $this->getCurrentDiskStats($stat); + $cpuPercent = $stat->cpumax > 0 ? ($stat->cpuused / $stat->cpumax * 100) : 0; + $memPercent = $stat->memorymax > 0 ? ($stat->memoryused / $stat->memorymax * 100) : 0; + $diskPercent = ($stat->diskmax ?? 0) > 0 ? (($stat->diskused ?? 0) / ($stat->diskmax ?? 1) * 100) : 0; + $statisticsExportRows[] = [ + 'date_time' => $stat->DateTime ? $stat->DateTime->format('M d, Y H:i:s') : '-', + 'status' => $stat->vmstatus ? ucfirst($stat->vmstatus) : '-', + 'cpu' => number_format($cpuPercent, 1) . '%', + 'memory' => number_format($memPercent, 1) . '%', + 'disk' => (($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0) ? number_format($diskPercent, 1) . '%' : 'N/A', + 'network_in' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']), + 'network_out' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']), + 'disk_io' => sprintf( + '%s / %s', + Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0), + Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0) + ), + 'uptime' => $stat->uptime > 0 ? $this->formatUptime($stat->uptime) : '-', + ]; + @endphp + + + + + + + + + + + + + @empty + + + + @endforelse + +
+
+ Captured + +
+
+
+ Power + +
+
+
+ CPU + +
+
+
+ RAM + +
+
+
+ Disk + +
+
+
+ Net in + +
+
+
+ Net out + +
+
+
+ Block I/O + +
+
+
+ Uptime + +
+
Actions
+
{{ $stat->DateTime?->format('M d, Y') ?? '-' }}
+
{{ $stat->DateTime?->format('H:i:s') ?? '' }}
+
+ @if($stat->vmstatus) + + {{ ucfirst($stat->vmstatus) }} + + @else + - + @endif + +
+
+
+
+ {{ number_format($cpuPercent, 1) }}% +
+
+
+
+
+
+ {{ number_format($memPercent, 1) }}% +
+
+ @if(($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0) +
+
+
+
+ {{ number_format($diskPercent, 1) }}% +
+ @else + N/A + @endif +
+
+ + {{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']) }} +
+ @if($networkStats['netin_rate'] > 0) +
{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netin_rate']) }}/s
+ @endif +
+
+ + {{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']) }} +
+ @if($networkStats['netout_rate'] > 0) +
{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netout_rate']) }}/s
+ @endif +
+
+
+ + {{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0) }} +
+
+ + {{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0) }} +
+
+
+ @if($stat->uptime > 0) + {{ $this->formatUptime($stat->uptime) }} + @else + - + @endif + + +
+
+
+ +
+
+

No samples yet

+

Hourly automation will populate this grid; you can also pin a manual capture.

+
+
+
+
+
+ + + @if($this->statistics->total() > 0) +
+ {{-- Records per page selector --}} +
+ + + each page +
+ + {{-- Page info and navigation --}} +
+ + Showing {{ (($this->statistics->currentPage() - 1) * $this->statistics->perPage()) + 1 }} - {{ min($this->statistics->currentPage() * $this->statistics->perPage(), $this->statistics->total()) }} of {{ $this->statistics->total() }} entries + + + @if($this->statistics->hasPages()) +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $statsTotalPages = $this->statistics->lastPage(); + $statsCurrentPage = $this->statistics->currentPage(); + $statsStartPage = max(1, $statsCurrentPage - 2); + $statsEndPage = min($statsTotalPages, $statsCurrentPage + 2); + @endphp + + @if($statsStartPage > 1) + + @if($statsStartPage > 2) + ... + @endif + @endif + + @for($i = $statsStartPage; $i <= $statsEndPage; $i++) + + @endfor + + @if($statsEndPage < $statsTotalPages) + @if($statsEndPage < $statsTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+ @endif +
+
+ @endif +
+ + + diff --git a/extensions/Servers/Proxmox/views/partials/statistics/lifecycle-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/lifecycle-inner.blade.php new file mode 100644 index 000000000..e8bb3ed47 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/lifecycle-inner.blade.php @@ -0,0 +1,427 @@ + + @php + $lifecycleEventsExportRows = []; + @endphp +
+
+
+
+ +
+
+
+

Guest lifecycle timeline

+
+ + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + @forelse($this->paginatedLifecycleEvents as $index => $event) + @php + $startTime = isset($event['starttime']) ? \Carbon\Carbon::createFromTimestamp($event['starttime']) : null; + $endTime = isset($event['endtime']) && $event['endtime'] > 0 ? \Carbon\Carbon::createFromTimestamp($event['endtime']) : null; + $duration = $event['duration'] ?? null; + $eventUpid = $event['upid'] ?? $event['timestamp'] ?? ''; + $eventKey = md5($eventUpid . ($event['starttime'] ?? '')); + $isExpanded = in_array($eventUpid, $this->expandedTaskUpids ?? []); + + $eventColorMap = [ + 'start' => 'text-success', + 'shutdown' => 'text-danger', + 'reboot' => 'text-info', + 'reset' => 'text-warning', + 'backup' => 'text-purple-200', + ]; + $eventColor = $eventColorMap[$event['type']] ?? 'text-base/70'; + + $lifecycleEventsExportRows[] = [ + 'type' => $event['label'] ?? 'N/A', + 'status' => ucfirst($event['status'] ?? '-'), + 'start_time' => $startTime ? $startTime->format('M d, Y H:i:s') : '-', + 'end_time' => $endTime ? $endTime->format('M d, Y H:i:s') : '-', + 'duration' => $duration !== null ? $this->formatUptime($duration) : '-', + ]; + @endphp + + + + + + + + + + + + + + + @empty + + + + + + @endforelse +
+
+ Type + +
+
+
+ Status + +
+
+
+ Start Time + +
+
+
+ End Time + +
+
+
+ Duration + +
+
+ + + {{ $event['label'] ?? 'N/A' }} + + @if(isset($event['status'])) + @if(strtolower($event['status']) === 'ok') + + Success + + @elseif(strtolower($event['status']) === 'running') + + Running + + @else + + Failure + + @endif + @else + - + @endif + + @if($startTime) +
{{ $startTime->format('M d, Y') }}
+
{{ $startTime->format('H:i:s') }}
+ @else + - + @endif +
+ @if($endTime) +
{{ $endTime->format('M d, Y') }}
+
{{ $endTime->format('H:i:s') }}
+ @else + - + @endif +
+ @if($duration !== null) + {{ $this->formatUptime($duration) }} + @else + - + @endif +
+
+

+ + Event Information +

+ +
+
+ Type + {{ $event['original_type'] ?? 'N/A' }} +
+
+ Status + {{ ucfirst($event['status'] ?? 'Unknown') }} +
+
+ Event Type + {{ $event['label'] ?? 'N/A' }} +
+
+ +
+ @if(isset($event['upid'])) +
+ Task ID + {{ str_replace([':root@pam!paymenter:', ':root@pam:'], '', $event['upid']) }} +
+ @endif +
+
+
+
+
+ +
+
+

No lifecycle events available

+

Lifecycle events will appear here as operations are performed on the server.

+
+
+
+
+
+ + + @php + $lifecycleTotalCount = count($this->chartEvents); + @endphp + @if($lifecycleTotalCount > 0) +
+ {{-- Records per page selector --}} +
+ + + per page +
+ + {{-- Page info and navigation --}} + @php + $lifecycleTotalPages = $this->lifecycleEventsTotalPages; + $lifecycleCurrentPage = $lifecycleEventsCurrentPage; + @endphp +
+ + Showing {{ (($lifecycleCurrentPage - 1) * $lifecycleEventsPerPage) + 1 }} - {{ min($lifecycleCurrentPage * $lifecycleEventsPerPage, $lifecycleTotalCount) }} of {{ $lifecycleTotalCount }} events + + + @if($lifecycleTotalPages > 1) +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $lifecycleStartPage = max(1, $lifecycleCurrentPage - 2); + $lifecycleEndPage = min($lifecycleTotalPages, $lifecycleCurrentPage + 2); + @endphp + + @if($lifecycleStartPage > 1) + + @if($lifecycleStartPage > 2) + ... + @endif + @endif + + @for($i = $lifecycleStartPage; $i <= $lifecycleEndPage; $i++) + + @endfor + + @if($lifecycleEndPage < $lifecycleTotalPages) + @if($lifecycleEndPage < $lifecycleTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+ @endif +
+
+ @endif +
+ + diff --git a/extensions/Servers/Proxmox/views/partials/statistics/modals-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/modals-inner.blade.php new file mode 100644 index 000000000..38b8d03c4 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/modals-inner.blade.php @@ -0,0 +1,201 @@ + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ diff --git a/extensions/Servers/Proxmox/views/partials/statistics/network-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/network-inner.blade.php new file mode 100644 index 000000000..155917e97 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/network-inner.blade.php @@ -0,0 +1,1507 @@ + +
+
+
+
+ +
+
+
+

Network Analysis

+
+ + + + + +
+
+

+ Deep visibility into firewall sessions, security threats, and top consumers of bandwidth powered by the Core Router API. +

+
+
+
+ +
+
+
+
+
+ + + + +
+
+ + @if($networkAnalysisLoading || ($networkAnalysis['lastUpdated'] === null && $networkAnalysis['error'] === null)) +
+ + + + + + +

+ Fetching data from Core Router... + reload page +

+
+ @elseif($networkAnalysis['error']) +
+
+

{{ $networkAnalysis['error'] }}

+

Ensure the Core Router API credentials are configured in your environment.

+
+
+ @else +
+
+ @if(($networkAnalysis['connectionCount'] ?? 0) == 0) +
No matching connections tracked for this server.
+ @else +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + @foreach($this->paginatedConnections as $index => $connection) + @php + // Use raw bytes value from backend for reliable numeric sorting + $bytesForSort = (int)($connection['bytes'] ?? 0); + @endphp + + + + + + + + + @endforeach + +
+
+ Direction + +
+
+
+ Protocol + +
+
+
+ Source + +
+
+
+ Destination + +
+
+
+ State + +
+
+
+ Traffic + +
+
+ @php + $direction = $connection['direction'] ?? '—'; + @endphp + @if($direction !== '—') +
+ {{ $direction }} +
+ + + + + +
+
+ @else + — + @endif +
+ @php + $protocol = $connection['protocol'] ?? '—'; + @endphp + @if($protocol !== '—') +
+ {{ $protocol }} +
+ + + + + +
+
+ @else + — + @endif +
+ @php + $sourceIP = $connection['source'] ?? '—'; + $isSourcePublic = $sourceIP !== '—' && $this->checkIsPublicIP($sourceIP); + @endphp +
+ {{ $sourceIP }} + @if($isSourcePublic) +
+ + + +
+ @endif +
+ @if(isset($connection['source_port'])) + {{ $connection['source_port'] }} + @endif +
+ @php + $destIP = $connection['destination'] ?? '—'; + $isDestPublic = $destIP !== '—' && $this->checkIsPublicIP($destIP); + @endphp +
+ {{ $destIP }} + @if($isDestPublic) +
+ + + +
+ @endif +
+ @if(isset($connection['destination_port'])) + {{ $connection['destination_port'] }} + @endif +
+ @php + $state = $connection['state'] ?? '—'; + @endphp + @if($state !== '—') +
+ + {{ $state }} + +
+ + + + + +
+
+ @else + — + @endif +
+
+ {{ $connection['bytes_human'] ?? '—' }} + @if(isset($connection['packets'])) + {{ number_format($connection['packets']) }} packets + @endif +
+
+
+ + {{-- Connections Pagination Controls --}} + @if(($networkAnalysis['connectionCount'] ?? 0) > 0) +
+ {{-- Records per page selector --}} +
+ + + per page +
+ + {{-- Page info and navigation --}} +
+ + Showing {{ (($connectionsCurrentPage - 1) * $connectionsPerPage) + 1 }} - {{ min($connectionsCurrentPage * $connectionsPerPage, $networkAnalysis['connectionCount'] ?? 0) }} of {{ $networkAnalysis['connectionCount'] ?? 0 }} connections + + +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $connectionsTotalPages = $this->connectionsTotalPages; + $startPage = max(1, $connectionsCurrentPage - 2); + $endPage = min($connectionsTotalPages, $connectionsCurrentPage + 2); + @endphp + + @if($startPage > 1) + + @if($startPage > 2) + ... + @endif + @endif + + @for($i = $startPage; $i <= $endPage; $i++) + + @endfor + + @if($endPage < $connectionsTotalPages) + @if($endPage < $connectionsTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+
+
+ @endif +
+ @endif +
+ +
+ @if(($networkAnalysis['topConnectionCount'] ?? 0) == 0) +
No connection data available.
+ @else +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + @foreach($this->paginatedTopConnections as $index => $row) + @php + $endpointIP = $row['endpoint'] ?? '—'; + $port = $row['port'] ?? 0; + $connections = $row['count'] ?? 0; + $bytes = $row['bytes'] ?? 0; + $bytesForSort = $bytes; + @endphp + + + + + + + @endforeach + +
+
+ Endpoint + +
+
+
+ Port + +
+
+
+ Connections + +
+
+
+ Traffic + +
+
+ @php + $isEndpointPublic = $endpointIP !== '—' && $this->checkIsPublicIP($endpointIP); + @endphp +
+ {{ $endpointIP }} + @if($isEndpointPublic) +
+ + + +
+ @endif +
+
+ @if(isset($row['port']) && $row['port'] !== '—') +
+ {{ $row['port'] }} +
+ + + + + +
+
+ @else + — + @endif +
{{ number_format($row['count']) }} +
+ {{ $row['bytes_human'] }} + {{ number_format($row['packets']) }} packets +
+
+
+ + {{-- Top Connections Pagination Controls --}} + @if(($networkAnalysis['topConnectionCount'] ?? 0) > 0) +
+ {{-- Records per page selector --}} +
+ + + per page +
+ + {{-- Page info and navigation --}} +
+ + Showing {{ (($topConnectionsCurrentPage - 1) * $topConnectionsPerPage) + 1 }} - {{ min($topConnectionsCurrentPage * $topConnectionsPerPage, $networkAnalysis['topConnectionCount'] ?? 0) }} of {{ $networkAnalysis['topConnectionCount'] ?? 0 }} peers + + +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $topConnectionsTotalPages = $this->topConnectionsTotalPages; + $startPage = max(1, $topConnectionsCurrentPage - 2); + $endPage = min($topConnectionsTotalPages, $topConnectionsCurrentPage + 2); + @endphp + + @if($startPage > 1) + + @if($startPage > 2) + ... + @endif + @endif + + @for($i = $startPage; $i <= $endPage; $i++) + + @endfor + + @if($endPage < $topConnectionsTotalPages) + @if($endPage < $topConnectionsTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+
+
+ @endif +
+ @endif +
+ +
+ @if(($networkAnalysis['topPortCount'] ?? 0) == 0) +
No port statistics are available.
+ @else +
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + @foreach($this->paginatedTopPorts as $index => $row) + @php + $port = $row['port'] ?? 0; + $connections = $row['connections'] ?? 0; + $bytes = $row['bytes'] ?? 0; + $bytesForSort = $bytes; + @endphp + + + + + + @endforeach + +
+
+ Port + +
+
+
+ Connections + +
+
+
+ Traffic + +
+
+
+ {{ $row['port'] }} +
+ + + + + +
+
+
{{ number_format($row['connections']) }} +
+ {{ $row['bytes_human'] }} + {{ number_format($row['packets']) }} packets +
+
+
+ + {{-- Top Ports Pagination Controls --}} + @if(($networkAnalysis['topPortCount'] ?? 0) > 0) +
+ {{-- Records per page selector --}} +
+ + + per page +
+ + {{-- Page info and navigation --}} +
+ + Showing {{ (($topPortsCurrentPage - 1) * $topPortsPerPage) + 1 }} - {{ min($topPortsCurrentPage * $topPortsPerPage, $networkAnalysis['topPortCount'] ?? 0) }} of {{ $networkAnalysis['topPortCount'] ?? 0 }} ports + + +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $topPortsTotalPages = $this->topPortsTotalPages; + $startPage = max(1, $topPortsCurrentPage - 2); + $endPage = min($topPortsTotalPages, $topPortsCurrentPage + 2); + @endphp + + @if($startPage > 1) + + @if($startPage > 2) + ... + @endif + @endif + + @for($i = $startPage; $i <= $endPage; $i++) + + @endfor + + @if($endPage < $topPortsTotalPages) + @if($endPage < $topPortsTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+
+
+ @endif +
+ @endif +
+ +
+ @if(empty($networkAnalysis['security'])) +
No security threats detected. All connections appear to be safe.
+ @else +
+ @foreach($networkAnalysis['security'] as $warning) + @php + $conn = $warning['connection'] ?? []; + @endphp + @php + $riskLevel = $warning['risk_level'] ?? 'Medium'; + $attackType = $warning['attack_type'] ?? 'Unknown Threat'; + $riskDescription = $warning['risk_description'] ?? 'Malicious IP detected from blocklist'; + $riskBadgeClass = match($riskLevel) { + 'Critical' => 'bg-danger/20 text-danger border-danger/30', + 'High' => 'bg-warning/20 text-warning border-warning/30', + 'Medium' => 'bg-warning/10 text-warning border-warning/20', + 'Low' => 'bg-base/10 text-base/70 border-base/20', + default => 'bg-warning/10 text-warning border-warning/20', + }; + @endphp +
+
+ +
+
+
+ Security Threat Detected + + {{ $warning['type'] }} + + + {{ $riskLevel }} Risk + + + {{ $attackType }} + + @if(isset($warning['quantity']) && $warning['quantity'] > 1) + + {{ $warning['quantity'] }}x Repeated + + @endif +
+ +
+

+ {{ $warning['threat'] }} + @if(isset($warning['quantity']) && $warning['quantity'] > 1) + ({{ $warning['quantity'] }} similar connection{{ $warning['quantity'] > 1 ? 's' : '' }} detected) + @endif +

+ @php + $threatIP = $warning['ip'] ?? '—'; + $isThreatPublic = $threatIP !== '—' && $this->checkIsPublicIP($threatIP); + @endphp +
+ IP Address: + {{ $threatIP }} + @if($isThreatPublic) +
+ + + +
+ @endif +
+ +
+
+
+ @endforeach +
+ @endif +
+
+ @endif +
+
+ diff --git a/extensions/Servers/Proxmox/views/partials/statistics/scripts-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/scripts-inner.blade.php new file mode 100644 index 000000000..e09c36e89 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/scripts-inner.blade.php @@ -0,0 +1,2008 @@ + @script + + @endscript diff --git a/extensions/Servers/Proxmox/views/partials/statistics/tasks-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/tasks-inner.blade.php new file mode 100644 index 000000000..9950c1465 --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/tasks-inner.blade.php @@ -0,0 +1,505 @@ + + @php + $taskHistoryExportRows = []; + @endphp +
+
+
+
+ +
+
+
+

Hypervisor jobs

+
+ + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + @forelse($this->paginatedTaskHistory as $task) + @php + $startTime = isset($task['starttime']) ? \Carbon\Carbon::createFromTimestamp($task['starttime']) : null; + $endTime = isset($task['endtime']) && $task['endtime'] > 0 ? \Carbon\Carbon::createFromTimestamp($task['endtime']) : null; + $duration = null; + if ($startTime && $endTime) { + $duration = $startTime->diffInSeconds($endTime); + } elseif ($startTime && isset($task['status']) && $task['status'] === 'running') { + $duration = $startTime->diffInSeconds(now()); + } + $taskUpid = $task['upid'] ?? $task['id'] ?? ''; + $taskKey = md5($taskUpid . ($task['starttime'] ?? '')); + $isExpanded = in_array($taskUpid, $this->expandedTaskUpids ?? []); + $taskHistoryExportRows[] = [ + 'type' => $task['type'] ?? 'N/A', + 'status' => $task['status'] ?? '-', + 'start_time' => $startTime ? $startTime->format('M d, Y H:i:s') : '-', + 'end_time' => $endTime ? $endTime->format('M d, Y H:i:s') : '-', + 'duration' => $duration !== null ? $this->formatUptime($duration) : '-', + ]; + @endphp + + + + + + + + + + + + + + + @empty + + + + + + @endforelse +
+
+ Type + +
+
+
+ Status + +
+
+
+ Start Time + +
+
+
+ End Time + +
+
+
+ Duration + +
+
+ + + {{ $task['type'] ?? 'N/A' }} + + @if(isset($task['status'])) + @if($task['status'] === 'OK') + + Success + + @else + + Failure + + @endif + @else + - + @endif + + @if($startTime) +
{{ $startTime->format('M d, Y') }}
+
{{ $startTime->format('H:i:s') }}
+ @else + - + @endif +
+ @if($endTime) +
{{ $endTime->format('M d, Y') }}
+
{{ $endTime->format('H:i:s') }}
+ @else + - + @endif +
+ @if($duration !== null) + {{ $this->formatUptime($duration) }} + @else + - + @endif +
+
+

+ + Task Information +

+ + +
+
+ + + + + Loading task details... +
+
+ + +
+ + +
+ +
+ + + + + +
+ + +
+

No detailed information available for this task.

+
+
+
+
+
+ +
+
+

No task history available

+

Task history will appear here as tasks are executed on the server.

+
+
+
+
+
+ + + @if(count($this->taskHistory) > 0) +
+ {{-- Records per page selector --}} +
+ + + per page +
+ + {{-- Page info and navigation --}} + @php + $taskTotalPages = $this->taskHistoryTotalPages; + $taskCurrentPage = $taskHistoryCurrentPage; + $taskTotalCount = count($this->taskHistory); + @endphp +
+ + Showing {{ (($taskCurrentPage - 1) * $taskHistoryPerPage) + 1 }} - {{ min($taskCurrentPage * $taskHistoryPerPage, $taskTotalCount) }} of {{ $taskTotalCount }} tasks + + + @if($taskTotalPages > 1) +
+ {{-- First page --}} + + + {{-- Previous page --}} + + + {{-- Page numbers --}} + @php + $taskStartPage = max(1, $taskCurrentPage - 2); + $taskEndPage = min($taskTotalPages, $taskCurrentPage + 2); + @endphp + + @if($taskStartPage > 1) + + @if($taskStartPage > 2) + ... + @endif + @endif + + @for($i = $taskStartPage; $i <= $taskEndPage; $i++) + + @endfor + + @if($taskEndPage < $taskTotalPages) + @if($taskEndPage < $taskTotalPages - 1) + ... + @endif + + @endif + + {{-- Next page --}} + + + {{-- Last page --}} + +
+ @endif +
+
+ @endif +
+ + diff --git a/extensions/Servers/Proxmox/views/partials/statistics/telemetry-inner.blade.php b/extensions/Servers/Proxmox/views/partials/statistics/telemetry-inner.blade.php new file mode 100644 index 000000000..7ee51446c --- /dev/null +++ b/extensions/Servers/Proxmox/views/partials/statistics/telemetry-inner.blade.php @@ -0,0 +1,308 @@ + +
+
+
+
+ + Synthesizing RRD frames… +
+
+ +
+
+
+

Node RRD studio

+
+ + + +
+
+

Four synchronized canvases fed straight from the node RRD cache.

+
+

Auto-refresh every two minutes while this screen stays mounted.

+
+ +
+ {{-- CPU --}} +
+
+
+
+ +
+
+

Processor load

+

Percent of allocated vCPU

+
+
+
+ + + +
+
+
+
+
+ Mounting canvas… +
+
+
+
+
+ + {{-- Network --}} +
+
+
+
+ +
+
+

NIC throughput

+

In / out / P95

+
+
+
+ + + +
+
+
+
+
+ Mounting canvas… +
+
+
+
+
+ + {{-- Memory --}} +
+
+
+
+ +
+
+

RAM footprint

+

Bytes from RRD

+
+
+
+ + + +
+
+
+
+
+ Mounting canvas… +
+
+
+
+
+ + {{-- Disk --}} +
+
+
+
+ +
+
+

Block throughput

+

Read vs write

+
+
+
+ + + +
+
+
+
+
+ Mounting canvas… +
+
+
+
+
+
+
+
diff --git a/extensions/Servers/Proxmox/views/statistics.blade.php b/extensions/Servers/Proxmox/views/statistics.blade.php index 9edcfc04f..76a1bbc2f 100644 --- a/extensions/Servers/Proxmox/views/statistics.blade.php +++ b/extensions/Servers/Proxmox/views/statistics.blade.php @@ -12,5799 +12,168 @@ setTimeout(() => icon.classList.remove('text-success'), 1500); } window.dispatchEvent(new CustomEvent('toast', { - detail: { message: 'Copied to clipboard', type: 'success' } + detail: { message: 'Value copied to clipboard', type: 'success' } })); }).catch(() => {}); }; } -
- -
-
- Overview - - Statistics -
-
-
-
-

Statistics

-
- - - - - -
-
-

- Monitor your server's performance metrics and resource utilization over time. -

-
-
-
- -
-
-
-
-
- Loading telemetry… -
-
+
-
-
-
-

Live Telemetry

-
- - - -
-
-

CPU, memory, network (with 95th percentile), and disk I/O from Proxmox rrddata.

-
-

Polls with graph refresh every 2 minutes.

-
- -
- {{-- CPU --}} -
-
-
-
- -
-
-

Processor

-

Utilization (%)

-
-
-
- - - -
-
-
-
-
- Preparing chart… -
-
-
-
-
- - {{-- Network --}} -
-
-
-
- -
-
-

Network

-

Throughput + 95th percentile

-
-
-
- - - -
-
-
-
-
- Preparing chart… -
-
-
-
-
- - {{-- Memory --}} -
-
-
-
- -
-
-

Memory

-

Guest RAM (bytes)

-
-
-
- - - -
-
-
-
-
- Preparing chart… -
-
-
-
-
- - {{-- Disk --}} -
-
-
-
- -
-
-

Disk I/O

-

Read / write rates

-
-
-
- - - -
-
-
-
-
- Preparing chart… -
-
-
-
-
-
-
-
- - - @php - $statisticsExportRows = []; - @endphp -
-
-
-
- -
-
-
-

Statistics History

-
- - - - - -
-
-
-
-
- - - -
- New Report -
-
- Creating... -
-
- - -
-
-
-
-
-
- -
- -
-
-
- - - - - - - - - - - - - - - - - @forelse($this->statistics as $stat) - @php - $networkStats = $this->getCurrentNetworkStats($stat); - $diskStats = $this->getCurrentDiskStats($stat); - $cpuPercent = $stat->cpumax > 0 ? ($stat->cpuused / $stat->cpumax * 100) : 0; - $memPercent = $stat->memorymax > 0 ? ($stat->memoryused / $stat->memorymax * 100) : 0; - $diskPercent = ($stat->diskmax ?? 0) > 0 ? (($stat->diskused ?? 0) / ($stat->diskmax ?? 1) * 100) : 0; - $statisticsExportRows[] = [ - 'date_time' => $stat->DateTime ? $stat->DateTime->format('M d, Y H:i:s') : '-', - 'status' => $stat->vmstatus ? ucfirst($stat->vmstatus) : '-', - 'cpu' => number_format($cpuPercent, 1) . '%', - 'memory' => number_format($memPercent, 1) . '%', - 'disk' => (($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0) ? number_format($diskPercent, 1) . '%' : 'N/A', - 'network_in' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']), - 'network_out' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']), - 'disk_io' => sprintf( - '%s / %s', - Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0), - Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0) - ), - 'uptime' => $stat->uptime > 0 ? $this->formatUptime($stat->uptime) : '-', - ]; - @endphp - - - - - - - - - - - - - @empty - - - - @endforelse - -
-
- Date & Time - -
-
-
- Status - -
-
-
- CPU - -
-
-
- Memory - -
-
-
- Disk - -
-
-
- Network In - -
-
-
- Network Out - -
-
-
- Disk I/O - -
-
-
- Uptime - -
-
Actions
-
{{ $stat->DateTime?->format('M d, Y') ?? '-' }}
-
{{ $stat->DateTime?->format('H:i:s') ?? '' }}
-
- @if($stat->vmstatus) - - {{ ucfirst($stat->vmstatus) }} - - @else - - - @endif - -
-
-
-
- {{ number_format($cpuPercent, 1) }}% -
-
-
-
-
-
- {{ number_format($memPercent, 1) }}% -
-
- @if(($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0) -
-
-
-
- {{ number_format($diskPercent, 1) }}% -
- @else - N/A - @endif -
-
- - {{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']) }} -
- @if($networkStats['netin_rate'] > 0) -
{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netin_rate']) }}/s
- @endif -
-
- - {{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']) }} -
- @if($networkStats['netout_rate'] > 0) -
{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netout_rate']) }}/s
- @endif -
-
-
- - {{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0) }} -
-
- - {{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0) }} -
-
-
- @if($stat->uptime > 0) - {{ $this->formatUptime($stat->uptime) }} - @else - - - @endif - - -
-
-
- -
-
-

No statistics available

-

Statistics are collected every 5 minutes automatically.

-
-
-
-
-
- - - @if($this->statistics->total() > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} -
- - Showing {{ (($this->statistics->currentPage() - 1) * $this->statistics->perPage()) + 1 }} - {{ min($this->statistics->currentPage() * $this->statistics->perPage(), $this->statistics->total()) }} of {{ $this->statistics->total() }} entries - - - @if($this->statistics->hasPages()) -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $statsTotalPages = $this->statistics->lastPage(); - $statsCurrentPage = $this->statistics->currentPage(); - $statsStartPage = max(1, $statsCurrentPage - 2); - $statsEndPage = min($statsTotalPages, $statsCurrentPage + 2); - @endphp - - @if($statsStartPage > 1) - - @if($statsStartPage > 2) - ... - @endif - @endif - - @for($i = $statsStartPage; $i <= $statsEndPage; $i++) - - @endfor - - @if($statsEndPage < $statsTotalPages) - @if($statsEndPage < $statsTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
- @endif -
-
- @endif -
- - - @php - $taskHistoryExportRows = []; - @endphp -
-
-
-
- -
-
-
-

Task History

-
- - - - - -
-
-
-
-
- -
-
-
-
-
-
- -
- -
-
-
- - - - - - - - - - - - @forelse($this->paginatedTaskHistory as $task) - @php - $startTime = isset($task['starttime']) ? \Carbon\Carbon::createFromTimestamp($task['starttime']) : null; - $endTime = isset($task['endtime']) && $task['endtime'] > 0 ? \Carbon\Carbon::createFromTimestamp($task['endtime']) : null; - $duration = null; - if ($startTime && $endTime) { - $duration = $startTime->diffInSeconds($endTime); - } elseif ($startTime && isset($task['status']) && $task['status'] === 'running') { - $duration = $startTime->diffInSeconds(now()); - } - $taskUpid = $task['upid'] ?? $task['id'] ?? ''; - $taskKey = md5($taskUpid . ($task['starttime'] ?? '')); - $isExpanded = in_array($taskUpid, $this->expandedTaskUpids ?? []); - $taskHistoryExportRows[] = [ - 'type' => $task['type'] ?? 'N/A', - 'status' => $task['status'] ?? '-', - 'start_time' => $startTime ? $startTime->format('M d, Y H:i:s') : '-', - 'end_time' => $endTime ? $endTime->format('M d, Y H:i:s') : '-', - 'duration' => $duration !== null ? $this->formatUptime($duration) : '-', - ]; - @endphp - - - - - - - - - - - - - - - @empty - - - - - - @endforelse -
-
- Type - -
-
-
- Status - -
-
-
- Start Time - -
-
-
- End Time - -
-
-
- Duration - -
-
- - - {{ $task['type'] ?? 'N/A' }} - - @if(isset($task['status'])) - @if($task['status'] === 'OK') - - Success - - @else - - Failure - - @endif - @else - - - @endif - - @if($startTime) -
{{ $startTime->format('M d, Y') }}
-
{{ $startTime->format('H:i:s') }}
- @else - - - @endif -
- @if($endTime) -
{{ $endTime->format('M d, Y') }}
-
{{ $endTime->format('H:i:s') }}
- @else - - - @endif -
- @if($duration !== null) - {{ $this->formatUptime($duration) }} - @else - - - @endif -
-
-

- - Task Information -

- - -
-
- - - - - Loading task details... -
-
- - -
- - -
- -
- - - - - -
- - -
-

No detailed information available for this task.

-
-
-
-
-
- -
-
-

No task history available

-

Task history will appear here as tasks are executed on the server.

-
-
-
-
-
- - - @if(count($this->taskHistory) > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} - @php - $taskTotalPages = $this->taskHistoryTotalPages; - $taskCurrentPage = $taskHistoryCurrentPage; - $taskTotalCount = count($this->taskHistory); - @endphp -
- - Showing {{ (($taskCurrentPage - 1) * $taskHistoryPerPage) + 1 }} - {{ min($taskCurrentPage * $taskHistoryPerPage, $taskTotalCount) }} of {{ $taskTotalCount }} tasks - - - @if($taskTotalPages > 1) -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $taskStartPage = max(1, $taskCurrentPage - 2); - $taskEndPage = min($taskTotalPages, $taskCurrentPage + 2); - @endphp - - @if($taskStartPage > 1) - - @if($taskStartPage > 2) - ... - @endif - @endif - - @for($i = $taskStartPage; $i <= $taskEndPage; $i++) - - @endfor - - @if($taskEndPage < $taskTotalPages) - @if($taskEndPage < $taskTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
- @endif -
-
- @endif -
- - - @php - $lifecycleEventsExportRows = []; - @endphp -
-
-
-
- -
-
-
-

Recent Lifecycle Events

-
- - - - - -
-
-
-
-
- -
-
-
-
-
-
- -
- -
-
-
- - - - - - - - - - - - @forelse($this->paginatedLifecycleEvents as $index => $event) - @php - $startTime = isset($event['starttime']) ? \Carbon\Carbon::createFromTimestamp($event['starttime']) : null; - $endTime = isset($event['endtime']) && $event['endtime'] > 0 ? \Carbon\Carbon::createFromTimestamp($event['endtime']) : null; - $duration = $event['duration'] ?? null; - $eventUpid = $event['upid'] ?? $event['timestamp'] ?? ''; - $eventKey = md5($eventUpid . ($event['starttime'] ?? '')); - $isExpanded = in_array($eventUpid, $this->expandedTaskUpids ?? []); - - $eventColorMap = [ - 'start' => 'text-success', - 'shutdown' => 'text-danger', - 'reboot' => 'text-info', - 'reset' => 'text-warning', - 'backup' => 'text-purple-200', - ]; - $eventColor = $eventColorMap[$event['type']] ?? 'text-base/70'; - - $lifecycleEventsExportRows[] = [ - 'type' => $event['label'] ?? 'N/A', - 'status' => ucfirst($event['status'] ?? '-'), - 'start_time' => $startTime ? $startTime->format('M d, Y H:i:s') : '-', - 'end_time' => $endTime ? $endTime->format('M d, Y H:i:s') : '-', - 'duration' => $duration !== null ? $this->formatUptime($duration) : '-', - ]; - @endphp - - - - - - - - - - - - - - - @empty - - - - - - @endforelse -
-
- Type - -
-
-
- Status - -
-
-
- Start Time - -
-
-
- End Time - -
-
-
- Duration - -
-
- - - {{ $event['label'] ?? 'N/A' }} - - @if(isset($event['status'])) - @if(strtolower($event['status']) === 'ok') - - Success - - @elseif(strtolower($event['status']) === 'running') - - Running - - @else - - Failure - - @endif - @else - - - @endif - - @if($startTime) -
{{ $startTime->format('M d, Y') }}
-
{{ $startTime->format('H:i:s') }}
- @else - - - @endif -
- @if($endTime) -
{{ $endTime->format('M d, Y') }}
-
{{ $endTime->format('H:i:s') }}
- @else - - - @endif -
- @if($duration !== null) - {{ $this->formatUptime($duration) }} - @else - - - @endif -
-
-

- - Event Information -

- -
-
- Type - {{ $event['original_type'] ?? 'N/A' }} -
-
- Status - {{ ucfirst($event['status'] ?? 'Unknown') }} -
-
- Event Type - {{ $event['label'] ?? 'N/A' }} -
-
- -
- @if(isset($event['upid'])) -
- Task ID - {{ str_replace([':root@pam!paymenter:', ':root@pam:'], '', $event['upid']) }} -
- @endif -
-
-
-
-
- -
-
-

No lifecycle events available

-

Lifecycle events will appear here as operations are performed on the server.

-
-
-
-
-
- - - @php - $lifecycleTotalCount = count($this->chartEvents); - @endphp - @if($lifecycleTotalCount > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} - @php - $lifecycleTotalPages = $this->lifecycleEventsTotalPages; - $lifecycleCurrentPage = $lifecycleEventsCurrentPage; - @endphp -
- - Showing {{ (($lifecycleCurrentPage - 1) * $lifecycleEventsPerPage) + 1 }} - {{ min($lifecycleCurrentPage * $lifecycleEventsPerPage, $lifecycleTotalCount) }} of {{ $lifecycleTotalCount }} events - - - @if($lifecycleTotalPages > 1) -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $lifecycleStartPage = max(1, $lifecycleCurrentPage - 2); - $lifecycleEndPage = min($lifecycleTotalPages, $lifecycleCurrentPage + 2); - @endphp - - @if($lifecycleStartPage > 1) - - @if($lifecycleStartPage > 2) - ... - @endif - @endif - - @for($i = $lifecycleStartPage; $i <= $lifecycleEndPage; $i++) - - @endfor - - @if($lifecycleEndPage < $lifecycleTotalPages) - @if($lifecycleEndPage < $lifecycleTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
- @endif -
-
- @endif -
-
- - - @php - $historicalComparison = $this->historicalComparison; - @endphp -
-
-
-
- -
-
-
-

Historical Data Comparison

-
- - - - -
-
- -
-
- -
- -
-
- - - @if($historicalPeriod === 'custom') - - @endif -
- -
- - - @if($currentPeriod === 'custom') - - @endif -
-
-
+

+ Align what the hypervisor reports right now with what it recorded yesterday—without hopping tabs. +

+ - @if($historicalComparison && !isset($historicalComparison['error'])) - - @php - $current = $historicalComparison['current']; - $historical = $historicalComparison['historical']; - $diff = $historicalComparison['differences']; - @endphp - - -
- - - - - - - - - - - - - - - - - + @php + $statCpuCores = max(1, (int) ($status?->cpus ?? 1)); + $statLiveCpuPct = round(max(0, ($status?->cpu ?? 0) * 100), 1); + $statMemMax = (int) ($status?->maxmem ?? 0); + $statMemUsed = (int) ($status?->mem ?? 0); + $statLiveMemPct = $statMemMax > 0 ? round(($statMemUsed / $statMemMax) * 100, 1) : 0; + $statSnapshotCount = $this->statistics->total(); + $statTaskBufferCount = count($this->taskHistory ?? []); + $statLifecyclePreview = count($this->chartEvents ?? []); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Metric -
- - Historical - ({{ $historical->DateTime->format('M d, Y H:i') }}) -
-
-
- - Current - ({{ $current->DateTime->format('M d, Y H:i') }}) -
-
Change
-
- - Processor Utilization -
-
- {{ number_format($diff['cpu']['historical_percent'], 1) }}% - - {{ number_format($diff['cpu']['current_percent'], 1) }}% - - - {{ $diff['cpu']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['cpu']['percent_change'], 1) }}% - -
-
- - Memory Utilization -
-
- {{ number_format($diff['memory']['historical_percent'], 1) }}% - - {{ number_format($diff['memory']['current_percent'], 1) }}% - - - {{ $diff['memory']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['memory']['percent_change'], 1) }}% - -
-
- - Disk Read Rate -
-
- {{ $this->formatBytes($diff['disk_read']['historical_rate']) }}/s - - {{ $this->formatBytes($diff['disk_read']['current_rate']) }}/s - - - {{ $diff['disk_read']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['disk_read']['percent_change'], 1) }}% - -
-
- - Disk Write Rate -
-
- {{ $this->formatBytes($diff['disk_write']['historical_rate']) }}/s - - {{ $this->formatBytes($diff['disk_write']['current_rate']) }}/s - - - {{ $diff['disk_write']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['disk_write']['percent_change'], 1) }}% - -
-
- - Network Download Rate -
-
- {{ $this->formatBytes($diff['network_in']['historical_rate']) }}/s - - {{ $this->formatBytes($diff['network_in']['current_rate']) }}/s - - - {{ $diff['network_in']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['network_in']['percent_change'], 1) }}% - -
-
- - Network Upload Rate -
-
- {{ $this->formatBytes($diff['network_out']['historical_rate']) }}/s - - {{ $this->formatBytes($diff['network_out']['current_rate']) }}/s - - - {{ $diff['network_out']['percent_change'] > 0 ? '+' : '' }}{{ number_format($diff['network_out']['percent_change'], 1) }}% - -
-
- @elseif($historicalComparison && isset($historicalComparison['error'])) -
-
-
-
- -
-
-

{{ $historicalComparison['message'] }}

- @if(isset($historicalComparison['total_stats'])) -

- Total statistics in database: {{ $historicalComparison['total_stats'] }} - @if($historicalComparison['total_stats'] == 0) - - Statistics are collected automatically every hour. You can also create manual statistics using the "Create Statistics" button above. - @elseif($historicalComparison['total_stats'] < 2) - - At least 2 statistics are needed for comparison. Wait for more data to be collected or create a manual statistic. - @endif -

- @endif -
-
-
-
- @else -
-
-
- -
-
-

No comparison data available

-

Please select periods with available statistics data.

-
-
-
- @endif -
-
- - -
-
-
-
- -
-
-
-

Network Analysis

-
- - - - - -
-
-

- Deep visibility into firewall sessions, security threats, and top consumers of bandwidth powered by the Core Router API. -

-
-
-
- -
-
-
-
-
- - - - -
-
- - @if($networkAnalysisLoading || ($networkAnalysis['lastUpdated'] === null && $networkAnalysis['error'] === null)) -
- - - - - - -

- Fetching data from Core Router... - reload page -

-
- @elseif($networkAnalysis['error']) -
-
-

{{ $networkAnalysis['error'] }}

-

Ensure the Core Router API credentials are configured in your environment.

-
-
- @else -
-
- @if(($networkAnalysis['connectionCount'] ?? 0) == 0) -
No matching connections tracked for this server.
- @else -
-
-
-
- -
- -
-
-
- - - - - - - - - - - - - @foreach($this->paginatedConnections as $index => $connection) - @php - // Use raw bytes value from backend for reliable numeric sorting - $bytesForSort = (int)($connection['bytes'] ?? 0); - @endphp - - - - - - - - - @endforeach - -
-
- Direction - -
-
-
- Protocol - -
-
-
- Source - -
-
-
- Destination - -
-
-
- State - -
-
-
- Traffic - -
-
- @php - $direction = $connection['direction'] ?? '—'; - @endphp - @if($direction !== '—') -
- {{ $direction }} -
- - - - - -
-
- @else - — - @endif -
- @php - $protocol = $connection['protocol'] ?? '—'; - @endphp - @if($protocol !== '—') -
- {{ $protocol }} -
- - - - - -
-
- @else - — - @endif -
- @php - $sourceIP = $connection['source'] ?? '—'; - $isSourcePublic = $sourceIP !== '—' && $this->checkIsPublicIP($sourceIP); - @endphp -
- {{ $sourceIP }} - @if($isSourcePublic) -
- - - -
- @endif -
- @if(isset($connection['source_port'])) - {{ $connection['source_port'] }} - @endif -
- @php - $destIP = $connection['destination'] ?? '—'; - $isDestPublic = $destIP !== '—' && $this->checkIsPublicIP($destIP); - @endphp -
- {{ $destIP }} - @if($isDestPublic) -
- - - -
- @endif -
- @if(isset($connection['destination_port'])) - {{ $connection['destination_port'] }} - @endif -
- @php - $state = $connection['state'] ?? '—'; - @endphp - @if($state !== '—') -
- - {{ $state }} - -
- - - - - -
-
- @else - — - @endif -
-
- {{ $connection['bytes_human'] ?? '—' }} - @if(isset($connection['packets'])) - {{ number_format($connection['packets']) }} packets - @endif -
-
-
- - {{-- Connections Pagination Controls --}} - @if(($networkAnalysis['connectionCount'] ?? 0) > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} -
- - Showing {{ (($connectionsCurrentPage - 1) * $connectionsPerPage) + 1 }} - {{ min($connectionsCurrentPage * $connectionsPerPage, $networkAnalysis['connectionCount'] ?? 0) }} of {{ $networkAnalysis['connectionCount'] ?? 0 }} connections - - -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $connectionsTotalPages = $this->connectionsTotalPages; - $startPage = max(1, $connectionsCurrentPage - 2); - $endPage = min($connectionsTotalPages, $connectionsCurrentPage + 2); - @endphp - - @if($startPage > 1) - - @if($startPage > 2) - ... - @endif - @endif - - @for($i = $startPage; $i <= $endPage; $i++) - - @endfor - - @if($endPage < $connectionsTotalPages) - @if($endPage < $connectionsTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
-
-
- @endif -
- @endif -
- -
- @if(($networkAnalysis['topConnectionCount'] ?? 0) == 0) -
No connection data available.
- @else -
-
-
-
- -
- -
-
-
- - - - - - - - - - - @foreach($this->paginatedTopConnections as $index => $row) - @php - $endpointIP = $row['endpoint'] ?? '—'; - $port = $row['port'] ?? 0; - $connections = $row['count'] ?? 0; - $bytes = $row['bytes'] ?? 0; - $bytesForSort = $bytes; - @endphp - - - - - - - @endforeach - -
-
- Endpoint - -
-
-
- Port - -
-
-
- Connections - -
-
-
- Traffic - -
-
- @php - $isEndpointPublic = $endpointIP !== '—' && $this->checkIsPublicIP($endpointIP); - @endphp -
- {{ $endpointIP }} - @if($isEndpointPublic) -
- - - -
- @endif -
-
- @if(isset($row['port']) && $row['port'] !== '—') -
- {{ $row['port'] }} -
- - - - - -
-
- @else - — - @endif -
{{ number_format($row['count']) }} -
- {{ $row['bytes_human'] }} - {{ number_format($row['packets']) }} packets -
-
-
- - {{-- Top Connections Pagination Controls --}} - @if(($networkAnalysis['topConnectionCount'] ?? 0) > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} -
- - Showing {{ (($topConnectionsCurrentPage - 1) * $topConnectionsPerPage) + 1 }} - {{ min($topConnectionsCurrentPage * $topConnectionsPerPage, $networkAnalysis['topConnectionCount'] ?? 0) }} of {{ $networkAnalysis['topConnectionCount'] ?? 0 }} peers - - -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $topConnectionsTotalPages = $this->topConnectionsTotalPages; - $startPage = max(1, $topConnectionsCurrentPage - 2); - $endPage = min($topConnectionsTotalPages, $topConnectionsCurrentPage + 2); - @endphp - - @if($startPage > 1) - - @if($startPage > 2) - ... - @endif - @endif - - @for($i = $startPage; $i <= $endPage; $i++) - - @endfor - - @if($endPage < $topConnectionsTotalPages) - @if($endPage < $topConnectionsTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
-
-
- @endif -
- @endif -
- -
- @if(($networkAnalysis['topPortCount'] ?? 0) == 0) -
No port statistics are available.
- @else -
-
-
-
- -
- -
-
-
- - - - - - - - - - @foreach($this->paginatedTopPorts as $index => $row) - @php - $port = $row['port'] ?? 0; - $connections = $row['connections'] ?? 0; - $bytes = $row['bytes'] ?? 0; - $bytesForSort = $bytes; - @endphp - - - - - - @endforeach - -
-
- Port - -
-
-
- Connections - -
-
-
- Traffic - -
-
-
- {{ $row['port'] }} -
- - - - - -
-
-
{{ number_format($row['connections']) }} -
- {{ $row['bytes_human'] }} - {{ number_format($row['packets']) }} packets -
-
-
- - {{-- Top Ports Pagination Controls --}} - @if(($networkAnalysis['topPortCount'] ?? 0) > 0) -
- {{-- Records per page selector --}} -
- - - per page -
- - {{-- Page info and navigation --}} -
- - Showing {{ (($topPortsCurrentPage - 1) * $topPortsPerPage) + 1 }} - {{ min($topPortsCurrentPage * $topPortsPerPage, $networkAnalysis['topPortCount'] ?? 0) }} of {{ $networkAnalysis['topPortCount'] ?? 0 }} ports - - -
- {{-- First page --}} - - - {{-- Previous page --}} - - - {{-- Page numbers --}} - @php - $topPortsTotalPages = $this->topPortsTotalPages; - $startPage = max(1, $topPortsCurrentPage - 2); - $endPage = min($topPortsTotalPages, $topPortsCurrentPage + 2); - @endphp - - @if($startPage > 1) - - @if($startPage > 2) - ... - @endif - @endif - - @for($i = $startPage; $i <= $endPage; $i++) - - @endfor - - @if($endPage < $topPortsTotalPages) - @if($endPage < $topPortsTotalPages - 1) - ... - @endif - - @endif - - {{-- Next page --}} - - - {{-- Last page --}} - -
-
-
- @endif -
- @endif -
- -
- @if(empty($networkAnalysis['security'])) -
No security threats detected. All connections appear to be safe.
- @else -
- @foreach($networkAnalysis['security'] as $warning) - @php - $conn = $warning['connection'] ?? []; - @endphp - @php - $riskLevel = $warning['risk_level'] ?? 'Medium'; - $attackType = $warning['attack_type'] ?? 'Unknown Threat'; - $riskDescription = $warning['risk_description'] ?? 'Malicious IP detected from blocklist'; - $riskBadgeClass = match($riskLevel) { - 'Critical' => 'bg-danger/20 text-danger border-danger/30', - 'High' => 'bg-warning/20 text-warning border-warning/30', - 'Medium' => 'bg-warning/10 text-warning border-warning/20', - 'Low' => 'bg-base/10 text-base/70 border-base/20', - default => 'bg-warning/10 text-warning border-warning/20', - }; - @endphp -
-
- -
-
-
- Security Threat Detected - - {{ $warning['type'] }} - - - {{ $riskLevel }} Risk - - - {{ $attackType }} - - @if(isset($warning['quantity']) && $warning['quantity'] > 1) - - {{ $warning['quantity'] }}x Repeated - - @endif -
- -
-

- {{ $warning['threat'] }} - @if(isset($warning['quantity']) && $warning['quantity'] > 1) - ({{ $warning['quantity'] }} similar connection{{ $warning['quantity'] > 1 ? 's' : '' }} detected) - @endif -

- @php - $threatIP = $warning['ip'] ?? '—'; - $isThreatPublic = $threatIP !== '—' && $this->checkIsPublicIP($threatIP); - @endphp -
- IP Address: - {{ $threatIP }} - @if($isThreatPublic) -
- - - -
- @endif -
- -
-
-
- @endforeach -
- @endif -
-
- @endif -
-
- - -
- - - - -
- - -
- - - - -
- - -
- - - - -
- - @script - - @endscript - - - + $toneRing = [ + 'sky' => 'ring-sky-500/30 bg-sky-500/[0.04]', + 'emerald' => 'ring-emerald-500/30 bg-emerald-500/[0.04]', + 'violet' => 'ring-violet-500/30 bg-violet-500/[0.04]', + 'amber' => 'ring-amber-500/30 bg-amber-500/[0.04]', + ]; + @endphp + +
+
+
+

Live pulse tiles

+

Numbers mirror the Overview status ticker; charts underneath carry the longer story.

+
+

Point-in-time only—scroll for RRD and archives.

+
+
+ @foreach ($statGlanceCards as $card) +
+
+

{{ $card['label'] }}

+
+ +
+
+

{{ $card['value'] }}

+

{{ $card['hint'] }}

+
+ @endforeach +
+
+ + @include('proxmox::partials.statistics.telemetry-inner') + + @include('proxmox::partials.statistics.history-inner') + +
+
+ @include('proxmox::partials.statistics.tasks-inner') +
+
+ @include('proxmox::partials.statistics.lifecycle-inner') +
+
+ + @include('proxmox::partials.statistics.comparison-inner') + + @include('proxmox::partials.statistics.network-inner') + + @include('proxmox::partials.statistics.modals-inner') + + @include('proxmox::partials.statistics.scripts-inner') + +