Files
Virtnet.bond/extensions/Servers/Proxmox/views/network.blade.php
Joseph.Rawlings 0ed29d782e Refactor Proxmox views for improved styling and consistency
- Updated subdomain.blade.php to enhance button styles and modal transitions.
- Changed text colors for better readability and consistency across the UI.
- Refactored system.blade.php to unify button components and improve layout structure.
- Adjusted modal z-index values for consistent layering.
- Enhanced accessibility by ensuring proper text contrast and hover states.
2025-12-18 09:40:58 -05:00

880 lines
51 KiB
PHP

<x-proxmox::layout :server="$server" view="network" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
<style>
@media (max-width: 767px) {
.network-tools-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 768px) and (max-width: 1149px) {
.network-tools-column {
display: none !important;
}
.network-tools-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1150px) {
.network-tools-column {
display: flex !important;
}
.network-tools-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.network-tools-with-details {
align-self: stretch;
display: flex !important;
flex-direction: column;
}
.network-tools-with-details .flex-1 {
flex: 1 1 auto;
}
/* Make Network Tools column extend to bottom of Technical Details */
.network-tools-with-details {
align-self: stretch;
position: static;
}
/* Position buttons at bottom of card when Technical Details are present */
.relative.flex.flex-col:has(.network-tools-with-details) {
position: relative;
}
.network-tools-with-details > div:last-child {
position: absolute;
bottom: 1rem;
right: 1rem;
z-index: 10;
}
}
</style>
<div class="bg-error/10 border border-error/30 rounded-lg p-4 mb-6"
x-data="{
isCollapsed: localStorage.getItem('network-policy-collapsed') === 'true',
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
toggle() {
this.isCollapsed = !this.isCollapsed;
localStorage.setItem('network-policy-collapsed', this.isCollapsed);
}
}"
x-init="
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
window.addEventListener('proxmox-view-mode-changed', (e) => {
isAdvancedView = e.detail.isAdvancedView;
});
"
x-show="isAdvancedView"
x-transition>
<div class="flex items-start gap-3">
<x-ri-error-warning-fill class="size-5 text-error flex-shrink-0 mt-0.5" />
<div class="flex-1">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-semibold">IP Address Usage Policy & Protocol Restrictions</h3>
<button @click="toggle()" type="button" class="text-base/60 hover:text-base transition-colors">
<x-ri-arrow-down-s-line x-bind:class="isCollapsed ? 'size-4 transition-transform rotate-180' : 'size-4 transition-transform'" />
</button>
</div>
<div x-show="!isCollapsed" x-transition>
<p class="text-xs text-base/70 leading-relaxed mb-3">
<strong>You cannot use any IP addresses not allocated directly to you.</strong> Violation of this policy will result in immediate termination of accounts and services.
</p>
<div class="mb-3">
<h4 class="text-xs font-semibold text-base/80 mb-2">Banned Protocols</h4>
<p class="text-xs text-base/70 leading-relaxed mb-2">The following protocols are prohibited and must not be transmitted:</p>
<ul class="text-xs text-base/70 leading-relaxed ml-4 list-disc space-y-1">
<li><strong>Link-local protocols:</strong> IRDP, ICMP redirects, vendor-specific link-layer protocols (e.g., UDLD)</li>
<li><strong>Discovery and vendor protocols:</strong> CDP (Cisco Discovery Protocol), EDP (Ethernet Discovery Protocol), LLDP (Link Layer Discovery Protocol)</li>
<li><strong>VLAN/Trunking protocols:</strong> VTP (VLAN Trunking Protocol), DTP (Dynamic Trunking Protocol)</li>
<li><strong>Interior routing protocol broadcasts:</strong> OSPF, ISIS, IGRP, EIGRP broadcasts (routing should only occur via BGP over unicast)</li>
<li><strong>Other protocols:</strong> BOOTP/DHCP, PIM-SM/DM (multicast), DVMRP</li>
<li><strong>Non-routable protocols:</strong> NetBEUI and similar non-routable protocols</li>
</ul>
</div>
<div>
<h4 class="text-xs font-semibold text-base/80 mb-2">Exceptions</h4>
<ul class="text-xs text-base/70 leading-relaxed ml-4 list-disc space-y-1">
<li><strong>ARP:</strong> Address Resolution Protocol is allowed, as it's essential for L3 address-to-L2 address resolution</li>
<li><strong>ICMPv6 Neighbor Discovery:</strong> Multicast packets for ICMPv6 Neighbor Discovery are generally permitted, though unicast is preferred</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 min-w-0">
<h2 class="text-xl font-semibold break-words">Assigned Network Addresses</h2>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-global-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight text-base/100">
Assigned Network Addresses
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Displays all IP addresses (both IPv4 and IPv6) that have been assigned to this virtual machine. These addresses are configured through the QEMU API and are part of your service allocation.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> These addresses are managed by the hosting provider and cannot be modified directly. Contact support if you need additional IP addresses or changes to your network configuration.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-background-secondary border border-neutral rounded-lg p-4 mb-6"
x-data="{ isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced' }"
x-init="
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
window.addEventListener('proxmox-view-mode-changed', (e) => {
isAdvancedView = e.detail.isAdvancedView;
});
"
x-show="isAdvancedView"
x-transition>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2 min-w-0">
<h3 class="text-lg font-semibold break-words">Network Devices</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-router-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight">
Network Devices
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Shows all network interface cards (NICs) configured for this virtual machine. You can add, remove, or modify network devices to change how your VM connects to the network.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Add Device:</span> Creates a new network interface with configurable model, bridge, and VLAN settings.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Remove Device:</span> Removes a network interface from the VM configuration. Changes take effect after the VM is restarted.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Multiqueue:</span> Enables the network adapter to distribute packet processing across multiple CPU cores. Set this value equal to your vCPU count for optimal performance in multi-core VMs. This improves network throughput significantly.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
The "Synced" indicator shows when your local changes match the server configuration; "Unsynced" indicates modifications that need to be saved.
</p>
</div>
</div>
</div>
</div>
</div>
@if($this->networkDevicesSynced)
<span class="text-xs font-semibold text-success">Synced</span>
@else
<span class="text-xs font-semibold text-warning">Unsynced</span>
@endif
</div>
<div class="overflow-x-auto rounded-lg border border-neutral/40 mb-4">
<table class="min-w-full divide-y divide-neutral/30 text-sm">
<thead class="bg-background">
<tr class="text-left text-xs font-semibold text-base/60 uppercase tracking-wide">
<th class="px-4 py-3">Device</th>
<th class="px-4 py-3">Adapter</th>
<th class="px-4 py-3">MAC Address</th>
<th class="px-4 py-3 text-center">MTU</th>
<th class="px-4 py-3 text-center">Multiqueue</th>
<th class="px-4 py-3 text-center">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral/20 text-base text-base/80">
@foreach($interfaces as $name => $type)
<tr>
<td class="px-4 py-3 font-semibold">{{ strtoupper($name) }}</td>
<td class="px-4 py-3">
<select wire:model.live="interfaceSelections.{{ $name }}"
wire:change="queueInterfaceChange('{{ $name }}')"
class="bg-background border border-neutral rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary w-full">
@foreach($interfaceTypeOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</td>
<td class="px-4 py-3 font-mono text-xs text-base/70">{{ strtoupper($interfaces[$name]['mac'] ?? 'N/A') }}</td>
<td class="px-4 py-3 text-center text-sm text-base/70">{{ $interfaces[$name]['mtu'] ?? 'Default' }}</td>
<td class="px-4 py-3 text-center">
<div x-data="{
queues: {{ $interfaces[$name]['queues'] ?? 1 }},
maxQueues: {{ $this->getCpuCores() }},
updateQueues() {
this.queues = Math.max(1, Math.min(this.queues, this.maxQueues));
$wire.updateMultiqueue('{{ $name }}', this.queues);
}
}">
<input type="number"
x-model.number="queues"
@blur="updateQueues()"
@keydown.enter="updateQueues()"
min="1"
:max="maxQueues"
class="bg-background border border-neutral rounded-md px-3 py-2 text-sm text-center focus:outline-none focus:ring-2 focus:ring-secondary w-20"
:title="'Number of queues (1-' + maxQueues + ')'">
</div>
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-2">
<button wire:click="toggleInterfaceLink('{{ $name }}')"
wire:loading.attr="disabled"
wire:target="toggleInterfaceLink('{{ $name }}')"
class="{{ ($interfaces[$name]['linkdown'] ?? false) ? 'text-error hover:text-error/80' : 'text-base/70 hover:text-base/60' }} mx-1"
title="{{ ($interfaces[$name]['linkdown'] ?? false) ? 'Connect' : 'Disconnect' }}">
<x-loading target="toggleInterfaceLink('{{ $name }}')" />
@if($interfaces[$name]['linkdown'] ?? false)
<x-ri-links-line class="inline size-4" wire:loading.remove wire:target="toggleInterfaceLink('{{ $name }}')" />
@else
<x-ri-links-fill class="inline size-4" wire:loading.remove wire:target="toggleInterfaceLink('{{ $name }}')" />
@endif
</button>
<button wire:click="removeInterface('{{ $name }}')"
wire:loading.attr="disabled"
class="text-error hover:text-error/80 disabled:text-base/30 mx-1"
:disabled="!$this->canRemoveInterfaces"
title="Remove">
<x-ri-delete-bin-fill class="inline size-4" />
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<x-button.secondary wire:click="addInterface" wire:loading.attr="disabled" class="w-full sm:w-1/3"
:disabled="!$this->canAddInterface">
<x-loading target="addInterface" />
<span wire:loading.remove wire:target="addInterface">
<x-ri-add-line class="inline size-4 mr-1" />
Add Device
</span>
<span wire:loading wire:target="addInterface">Adding...</span>
</x-button.secondary>
<x-button.secondary wire:click="refreshInterfaces" wire:loading.attr="disabled" class="w-full">
<x-loading target="refreshInterfaces" />
<span wire:loading.remove wire:target="refreshInterfaces">Refresh Interfaces</span>
<span wire:loading wire:target="refreshInterfaces">Refreshing...</span>
</x-button.secondary>
<x-button.primary wire:click="updateInterfaceType" wire:loading.attr="disabled" class="w-full"
:disabled="!$this->canUpdateInterface">
<x-loading target="updateInterfaceType" />
<span wire:loading.remove wire:target="updateInterfaceType">Save Interface Type</span>
<span wire:loading wire:target="updateInterfaceType">Saving...</span>
</x-button.primary>
</div>
<p class="text-xs text-base/50 mt-2">VirtIO provides the best performance; legacy adapters (E1000, Realtek, PCnet, VMware) improve compatibility.</p>
<p class="text-xs text-base/50 mt-1">
<strong>Multiqueue:</strong> Allows the network adapter to use multiple CPU cores for packet processing. Set to the number of vCPU cores (max {{ $this->getCpuCores() }}) for best performance. Higher values improve throughput for multi-core VMs.
</p>
</div>
@foreach($server->ipAddresses as $network)
<div class="relative flex flex-col gap-4 bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg mb-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">{{ strtoupper($network->type ?? 'IPv4') }} Address</h3>
<span class="text-xs font-medium px-2 py-1 rounded bg-background border border-neutral">
{{ $network->type === 'ipv6' ? 'IPv6' : 'IPv4' }}
</span>
</div>
<div class="grid md:grid-cols-3 grid-cols-1 gap-4 network-tools-grid">
<!-- Address -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">IP Address</h4>
<div class="text-md font-semibold text-base/60 flex items-center gap-2">
<span>{{ $network->ip }}</span>
<div
x-data="{
copied: false,
copyText: '{{ $network->ip }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
</div>
@if($cidr = $this->getCidrSuffix($network))
<p class="text-xs text-base/50 mt-1">CIDR: {{ $network->ip }}{{ $cidr }}</p>
@endif
</div>
<!-- Gateway -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">Gateway</h4>
<div class="text-md font-semibold text-base/60 flex items-center gap-2">
<span>{{ $network->gateway ?? 'N/A' }}</span>
@if($network->gateway)
<div
x-data="{
copied: false,
copyText: '{{ $network->gateway }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
@endif
</div>
<p class="text-xs text-base/50 mt-1">Default route for this network</p>
</div>
<!-- Netmask -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">Subnet Mask</h4>
<div class="text-md font-semibold text-base/60 flex items-center gap-2">
<span>{{ $network->netmask ?? 'N/A' }}</span>
@if($network->netmask)
<div
x-data="{
copied: false,
copyText: '{{ $network->netmask }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/70 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
@endif
</div>
<p class="text-xs text-base/50 mt-1 break-words">Network address range mask</p>
</div>
<!-- Network Tools -->
@php
$isPublic = $this->isPublicIp($network->ip, $network->type ?? 'ipv4');
$isIpv6 = ($network->type ?? 'ipv4') === 'ipv6' || filter_var($network->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$guestInterface = $this->getNetworkInterfaceForIp($network);
@endphp
@if($isPublic)
<div class="network-tools-column flex flex-col gap-2 relative min-w-0 @if($guestInterface) network-tools-with-details @endif">
<h4 class="text-sm font-semibold text-base/70"></h4>
<div class="flex-1"></div>
<div class="flex gap-2 justify-end mt-auto">
@if($isIpv6)
<x-button.primary
onclick="window.open('https://ping6.ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
Ping
</x-button.primary>
<x-button.primary
onclick="window.open('https://mtr6.ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
MTR
</x-button.primary>
<x-button.primary
onclick="window.open('https://bgp.ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
BGP
</x-button.primary>
@else
<x-button.primary
onclick="window.open('https://ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
Ping
</x-button.primary>
<x-button.primary
onclick="window.open('https://mtr.ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
MTR
</x-button.primary>
<x-button.primary
onclick="window.open('https://bgp.ping.pe/{{ $network->ip }}', '_blank')"
class="text-xs">
BGP
</x-button.primary>
@endif
</div>
</div>
@endif
</div>
@php
$guestInterface = $this->getNetworkInterfaceForIp($network);
@endphp
@if($guestInterface)
<!-- Additional Technical Information from QEMU Guest Agent -->
<div class="border-t border-neutral/40 pt-4 mt-2"
x-data="{ isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced' }"
x-init="
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
window.addEventListener('proxmox-view-mode-changed', (e) => {
isAdvancedView = e.detail.isAdvancedView;
});
"
x-show="isAdvancedView"
x-transition>
<h4 class="text-sm font-semibold text-base/70 mb-3">Technical Details (Guest Agent)</h4>
<div class="grid md:grid-cols-2 grid-cols-1 gap-4">
@if(isset($guestInterface['hardware-address']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">MAC Address</span>
<div class="flex items-center gap-2">
<span class="text-sm font-mono text-base/80">{{ $guestInterface['hardware-address'] }}</span>
<div
x-data="{
copied: false,
copyText: '{{ $guestInterface['hardware-address'] }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/80 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
</div>
<p class="text-xs text-base/50 mt-1">Hardware address for this interface</p>
</div>
@endif
@if(isset($guestInterface['name']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">Interface Name</span>
<span class="text-sm font-mono text-base/80">{{ $guestInterface['name'] }}</span>
<p class="text-xs text-base/50 mt-1">Network interface identifier within the guest OS</p>
</div>
@endif
@if(isset($guestInterface['statistics']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">Statistics</span>
<div class="text-xs text-base/60 space-y-1">
@if(isset($guestInterface['statistics']['rx-bytes']))
<div>RX: {{ number_format($guestInterface['statistics']['rx-bytes']) }} bytes</div>
@endif
@if(isset($guestInterface['statistics']['tx-bytes']))
<div>TX: {{ number_format($guestInterface['statistics']['tx-bytes']) }} bytes</div>
@endif
@if(isset($guestInterface['statistics']['rx-packets']))
<div>RX Packets: {{ number_format($guestInterface['statistics']['rx-packets']) }}</div>
@endif
@if(isset($guestInterface['statistics']['tx-packets']))
<div>TX Packets: {{ number_format($guestInterface['statistics']['tx-packets']) }}</div>
@endif
</div>
<p class="text-xs text-base/50 mt-1">Network traffic statistics from guest OS</p>
</div>
@endif
</div>
</div>
@endif
</div>
@endforeach
@php
$additionalAddresses = $this->getAdditionalGuestAgentAddresses();
@endphp
<div x-data="{ isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced' }"
x-init="
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
window.addEventListener('proxmox-view-mode-changed', (e) => {
isAdvancedView = e.detail.isAdvancedView;
});
"
x-show="isAdvancedView"
x-transition>
<div class="mt-6">
<div class="flex items-center gap-2 min-w-0 mb-4">
<h3 class="text-lg font-semibold break-words">Additional Network Addresses (Guest Agent)</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.away="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
<x-ri-information-line class="size-5 text-primary" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight">
Additional Network Addresses (Guest Agent)
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
Displays network addresses detected by the QEMU Guest Agent running inside your virtual machine. These addresses are reported by the guest operating system and may differ from the assigned addresses.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Note:</span> This information requires the QEMU Guest Agent to be installed and running in the guest OS. If no addresses are shown, ensure the guest agent is properly configured and the VM is running.
</p>
</div>
</div>
</div>
</div>
</div>
@if(count($additionalAddresses) > 0)
<p class="text-sm text-base/60 mb-4">
The following addresses were detected by the QEMU guest agent but are not configured in the system database.
</p>
@else
<div class="bg-info/10 border border-info/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<x-ri-information-line class="size-5 text-info flex-shrink-0 mt-0.5" />
<div class="flex-1">
<h4 class="text-sm font-semibold mb-2">QEMU Guest Agent</h4>
<p class="text-xs text-base/70 leading-relaxed mb-2">
The <strong>QEMU Guest Agent</strong> is a service that runs inside the virtual machine and provides additional information about the guest operating system to the hypervisor. When the guest agent is installed and activated, this section will display any additional network addresses detected within the guest OS that are not configured in the system database.
</p>
<p class="text-xs text-base/70 leading-relaxed">
If the guest agent is installed and running, additional addresses will appear here automatically with detailed technical information including MAC addresses, interface names, and network statistics.
</p>
</div>
</div>
</div>
@endif
</div>
@if(count($additionalAddresses) > 0)
@foreach($additionalAddresses as $address)
<div class="relative flex flex-col gap-4 bg-background-secondary hover:bg-background-secondary/80 border border-neutral p-4 rounded-lg mb-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">{{ strtoupper($address['type'] === 'ipv6' ? 'IPv6' : 'IPv4') }} Address</h3>
<span class="text-xs font-medium px-2 py-1 rounded bg-background border border-neutral">
{{ $address['type'] === 'ipv6' ? 'IPv6' : 'IPv4' }}
</span>
</div>
<div class="grid md:grid-cols-3 grid-cols-1 gap-4 network-tools-grid">
<!-- Address -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">IP Address</h4>
<div class="text-md font-semibold text-base/60 flex items-center gap-2">
<span>{{ $address['ip'] }}</span>
<div
x-data="{
copied: false,
copyText: '{{ $address['ip'] }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/80 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
</div>
@if($address['prefix'])
<p class="text-xs text-base/50 mt-1">CIDR: {{ $address['ip'] }}/{{ $address['prefix'] }}</p>
@endif
</div>
<!-- Gateway -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">Gateway</h4>
<div class="text-md font-semibold text-base/60">
<span>N/A</span>
</div>
<p class="text-xs text-base/50 mt-1">Not configured in database</p>
</div>
<!-- Netmask -->
<div class="flex flex-col gap-2 min-w-0">
<h4 class="text-sm font-semibold text-base/70">Subnet Mask</h4>
<div class="text-md font-semibold text-base/60">
<span>N/A</span>
</div>
<p class="text-xs text-base/50 mt-1 break-words">Not configured in database</p>
</div>
<!-- Network Tools -->
@php
$isPublic = $this->isPublicIp($address['ip'], $address['type']);
$isIpv6 = $address['type'] === 'ipv6' || filter_var($address['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$hasInterface = isset($address['interface']) && $address['interface'];
@endphp
@if($isPublic)
<div class="network-tools-column flex flex-col gap-2 relative min-w-0 @if($hasInterface) network-tools-with-details @endif">
<div class="flex-1"></div>
<div class="flex gap-2 justify-end mt-auto">
@if($isIpv6)
<x-button.primary
onclick="window.open('https://ping6.ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
Ping
</x-button.primary>
<x-button.primary
onclick="window.open('https://mtr6.ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
MTR
</x-button.primary>
<x-button.primary
onclick="window.open('https://bgp.ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
BGP
</x-button.primary>
@else
<x-button.primary
onclick="window.open('https://ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
Ping
</x-button.primary>
<x-button.primary
onclick="window.open('https://mtr.ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
MTR
</x-button.primary>
<x-button.primary
onclick="window.open('https://bgp.ping.pe/{{ $address['ip'] }}', '_blank')"
class="text-xs">
BGP
</x-button.primary>
@endif
</div>
</div>
@endif
</div>
@if($address['interface'])
<!-- Additional Technical Information from QEMU Guest Agent -->
<div class="border-t border-neutral/40 pt-4 mt-2">
<h4 class="text-sm font-semibold text-base/70 mb-3">Technical Details (Guest Agent)</h4>
<div class="grid md:grid-cols-2 grid-cols-1 gap-4">
@if(isset($address['interface']['hardware-address']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">MAC Address</span>
<div class="flex items-center gap-2">
<span class="text-sm font-mono text-base/80">{{ $address['interface']['hardware-address'] }}</span>
<div
x-data="{
copied: false,
copyText: '{{ $address['interface']['hardware-address'] }}',
copyToClipboard() {
navigator.clipboard.writeText(this.copyText);
this.copied = true;
setTimeout(() => this.copied = false, 3000);
}
}"
class="flex items-center">
<button
@click="copyToClipboard()"
type="button"
class="text-base/80 hover:text-base/60 relative inline-flex items-center justify-center ml-2">
<span x-show="copied" x-transition class="absolute">
<x-ri-check-fill class="inline size-4 text-success stroke-current" />
</span>
<span x-show="!copied" x-transition class="absolute">
<x-ri-file-copy-fill class="inline size-4" />
</span>
</button>
</div>
</div>
<p class="text-xs text-base/50 mt-1">Hardware address for this interface</p>
</div>
@endif
@if(isset($address['interface']['name']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">Interface Name</span>
<span class="text-sm font-mono text-base/80">{{ $address['interface']['name'] }}</span>
<p class="text-xs text-base/50 mt-1">Network interface identifier within the guest OS</p>
</div>
@endif
@if(isset($address['interface']['statistics']))
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-base/60">Statistics</span>
<div class="text-xs text-base/60 space-y-1">
@if(isset($address['interface']['statistics']['rx-bytes']))
<div>RX: {{ number_format($address['interface']['statistics']['rx-bytes']) }} bytes</div>
@endif
@if(isset($address['interface']['statistics']['tx-bytes']))
<div>TX: {{ number_format($address['interface']['statistics']['tx-bytes']) }} bytes</div>
@endif
@if(isset($address['interface']['statistics']['rx-packets']))
<div>RX Packets: {{ number_format($address['interface']['statistics']['rx-packets']) }}</div>
@endif
@if(isset($address['interface']['statistics']['tx-packets']))
<div>TX Packets: {{ number_format($address['interface']['statistics']['tx-packets']) }}</div>
@endif
</div>
<p class="text-xs text-base/50 mt-1">Network traffic statistics from guest OS</p>
</div>
@endif
</div>
</div>
@endif
</div>
@endforeach
@endif
</div>
</x-proxmox::layout>