From af62a1635446de5af4d5a814308dcf2b01566a8b Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Mon, 23 Feb 2026 20:02:41 -0500 Subject: [PATCH] Refactor modal close behavior and improve styling - Changed modal close event from `@click.away` to `@click.self` for better control over closing modals. - Updated various modal button styles for consistency and improved accessibility. - Adjusted transition durations for modal animations to enhance user experience. - Added aria-labels to close buttons for better screen reader support. --- .../Http/Controllers/ServerIconController.php | 27 +- .../Http/Controllers/ServerTagController.php | 9 + .../Servers/Proxmox/Livewire/Firewall.php | 16 +- .../Proxmox/Livewire/Firewall.php.backup | 1276 ------- .../Servers/Proxmox/Livewire/Reinstall.php | 13 +- .../Servers/Proxmox/Livewire/Server.php | 21 +- .../Servers/Proxmox/views/backups.blade.php | 2 +- .../Proxmox/views/boot-order.blade.php | 16 +- .../views/components/dropdown.blade.php | 2 +- .../Proxmox/views/components/layout.blade.php | 34 +- .../views/components/section-header.blade.php | 2 +- .../views/components/stat-card.blade.php | 4 +- .../components/ui/dropdown-item.blade.php | 2 +- .../components/ui/editable-field.blade.php | 26 +- .../views/components/ui/help-modal.blade.php | 34 +- .../views/components/ui/icon-picker.blade.php | 32 +- .../views/components/ui/input.blade.php | 10 +- .../views/components/ui/modal.blade.php | 14 +- .../ui/network-address-card.blade.php | 58 +- .../components/ui/notice-banner.blade.php | 37 +- .../views/components/ui/progress.blade.php | 6 +- .../components/ui/resource-gauge.blade.php | 14 +- .../components/ui/section-header.blade.php | 40 +- .../views/components/ui/select.blade.php | 8 +- .../components/ui/server-status.blade.php | 4 +- .../views/components/ui/stat-card.blade.php | 12 +- .../views/components/ui/table-row.blade.php | 4 +- .../views/components/ui/table-td.blade.php | 6 +- .../views/components/ui/table-th.blade.php | 4 +- .../views/components/ui/table.blade.php | 10 +- .../views/components/ui/tabs.blade.php | 14 +- .../views/components/ui/toggle.blade.php | 6 +- .../views/components/ui/tooltip.blade.php | 2 +- .../Servers/Proxmox/views/display.blade.php | 6 +- .../Servers/Proxmox/views/firewall.blade.php | 68 +- .../Proxmox/views/firewall.blade.php.backup | 2922 ----------------- .../Servers/Proxmox/views/network.blade.php | 45 +- .../Servers/Proxmox/views/overview.blade.php | 97 +- .../Servers/Proxmox/views/reinstall.blade.php | 6 +- .../Proxmox/views/serverplaceholder.blade.php | 14 +- .../Proxmox/views/statistics.blade.php | 75 +- .../Servers/Proxmox/views/storage.blade.php | 58 +- .../Servers/Proxmox/views/subdomain.blade.php | 18 +- .../Servers/Proxmox/views/system.blade.php | 11 +- .../lucentui/views/components/modal.blade.php | 8 +- 45 files changed, 393 insertions(+), 4700 deletions(-) delete mode 100644 extensions/Servers/Proxmox/Livewire/Firewall.php.backup delete mode 100644 extensions/Servers/Proxmox/views/firewall.blade.php.backup diff --git a/extensions/Servers/Proxmox/Http/Controllers/ServerIconController.php b/extensions/Servers/Proxmox/Http/Controllers/ServerIconController.php index 7743ff1ce..7ee44ca0d 100644 --- a/extensions/Servers/Proxmox/Http/Controllers/ServerIconController.php +++ b/extensions/Servers/Proxmox/Http/Controllers/ServerIconController.php @@ -12,20 +12,27 @@ class ServerIconController { $request->validate([ 'icon' => 'nullable|string|max:64', - 'color' => 'nullable|string|max:32', - 'prefix' => 'nullable|string|max:32', + 'color' => 'nullable|string|max:32|regex:/^[a-zA-Z0-9#\-\/]+$/', + 'prefix' => 'nullable|string|max:32|regex:/^[a-zA-Z0-9\-]+$/', ]); - $server = Server::where('id', $serverId) - ->where('user_id', Auth::id()) - ->firstOrFail(); + try { + $server = Server::where('id', $serverId) + ->where('user_id', Auth::id()) + ->firstOrFail(); - $server->user_set_icon = $request->input('icon'); - $server->user_set_icon_color = $request->input('color'); - $server->user_set_icon_prefix = $request->input('prefix', 'ri'); - $server->save(); + $server->user_set_icon = $request->input('icon'); + $server->user_set_icon_color = $request->input('color'); + $server->user_set_icon_prefix = $request->input('prefix', 'ri'); + $server->save(); - return response()->json(['success' => true]); + return response()->json(['success' => true]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => 'Failed to update icon.', + ], 500); + } } public function show($serverId) diff --git a/extensions/Servers/Proxmox/Http/Controllers/ServerTagController.php b/extensions/Servers/Proxmox/Http/Controllers/ServerTagController.php index dfb6fc8f2..f3e9abbd6 100644 --- a/extensions/Servers/Proxmox/Http/Controllers/ServerTagController.php +++ b/extensions/Servers/Proxmox/Http/Controllers/ServerTagController.php @@ -33,6 +33,15 @@ class ServerTagController */ public function update(Request $request, $serverId) { + $request->validate([ + 'tag1' => 'nullable|string|max:32', + 'tag1color' => 'nullable|string|max:16|regex:/^#[0-9a-fA-F]{3,8}$/', + 'tag2' => 'nullable|string|max:32', + 'tag2color' => 'nullable|string|max:16|regex:/^#[0-9a-fA-F]{3,8}$/', + 'tag3' => 'nullable|string|max:32', + 'tag3color' => 'nullable|string|max:16|regex:/^#[0-9a-fA-F]{3,8}$/', + ]); + try { $server = Server::where('id', $serverId) ->where('user_id', Auth::id()) diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index eb4dfdda5..342cff935 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -712,7 +712,9 @@ class Firewall extends Component $this->showRuleForm = false; $this->loadFirewallRules(); } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $errorBody = $response->json(); + $errors = $errorBody['errors'] ?? null; + $message = is_array($errors) ? implode(', ', array_values($errors)) : ($response->body() ?: 'Unknown error'); $this->error = 'Failed to save rule: ' . $message; $this->showErrorModal = true; } @@ -926,7 +928,9 @@ class Firewall extends Component $this->showAliasForm = false; $this->loadAliases(); } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $errorBody = $response->json(); + $errors = $errorBody['errors'] ?? null; + $message = is_array($errors) ? implode(', ', array_values($errors)) : ($response->body() ?: 'Unknown error'); $this->error = 'Failed to save alias: ' . $message; $this->showErrorModal = true; } @@ -1048,7 +1052,9 @@ class Firewall extends Component $this->showIPSetForm = false; $this->loadIPSets(); } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $errorBody = $response->json(); + $errors = $errorBody['errors'] ?? null; + $message = is_array($errors) ? implode(', ', array_values($errors)) : ($response->body() ?: 'Unknown error'); $this->error = 'Failed to create IPSet: ' . $message; $this->showErrorModal = true; } @@ -1192,7 +1198,9 @@ class Firewall extends Component $this->showIPSetMemberForm = false; $this->loadIPSetMembers($this->selectedIPSetName); } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $errorBody = $response->json(); + $errors = $errorBody['errors'] ?? null; + $message = is_array($errors) ? implode(', ', array_values($errors)) : ($response->body() ?: 'Unknown error'); $this->error = 'Failed to add member: ' . $message; $this->showErrorModal = true; } diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php.backup b/extensions/Servers/Proxmox/Livewire/Firewall.php.backup deleted file mode 100644 index a2ca45e3c..000000000 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php.backup +++ /dev/null @@ -1,1276 +0,0 @@ -server->loadMissing('ipAddresses'); - $this->loadAvailableIps(); - $this->loadFirewallOptions(); - $this->loadFirewallRules(); - $this->loadNetworkAdapters(); - $this->loadFirewallLogs(); - } - - protected function loadAvailableIps(): void - { - $allIps = collect(); - if ($this->server->primaryIpv4) { - $allIps->push(['ip' => $this->server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']); - } - if ($this->server->primaryIpv6) { - $allIps->push(['ip' => $this->server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']); - } - foreach ($this->server->ipAddresses as $ipAddress) { - $allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']); - } - $this->availableIps = $allIps->unique('ip')->values()->toArray(); - } - - /** - * Make a firewall API request using the dedicated firewall credentials - */ - protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response - { - $baseUrl = 'https://pve.virtnet.bond'; - $user = 'root@pam'; - $tokenId = 'firewall'; - $secret = '0a439967-0a44-4b94-8906-c36b3a40cf68'; - - $fullUrl = rtrim($baseUrl, '/') . '/api2/json' . $url; - - // Proxmox API token format: PVEAPIToken=USER@REALM!TOKENID=SECRET - $authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret; - - // For firewall log endpoint, accept text/plain as well - $acceptHeader = str_contains($url, '/firewall/log') - ? 'application/json, text/plain, */*' - : 'application/json'; - - $request = Http::withHeaders([ - 'Authorization' => $authHeader, - 'Accept' => $acceptHeader, - ])->withOptions([ - 'verify' => true, - ]); - - // For PUT/POST requests, Proxmox API expects form data - if (in_array(strtolower($method), ['put', 'post']) && !empty($data)) { - return $request->asForm()->$method($fullUrl, $data)->throw(); - } - - // For GET requests, if data is provided, it should be query parameters - if (strtolower($method) === 'get' && !empty($data)) { - $queryString = http_build_query($data); - $fullUrl .= (str_contains($fullUrl, '?') ? '&' : '?') . $queryString; - return $request->$method($fullUrl)->throw(); - } - - return $request->$method($fullUrl)->throw(); - } - - public function loadFirewallOptions(): void - { - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options' - ); - $data = $response->json()['data'] ?? []; - $this->firewallEnabled = isset($data['enable']) && $data['enable'] == 1; - $this->error = ''; - $this->showErrorModal = false; - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - // Try to extract more detailed error from response if available - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to load firewall options: ' . $errorMessage; - $this->showErrorModal = true; - $this->firewallEnabled = false; - } - } - - public function toggleFirewall(): void - { - $this->loading = true; - try { - // Store the current state before toggling - $currentState = $this->firewallEnabled; - // Calculate the new desired state (opposite of current) - $newState = $currentState ? 0 : 1; - $payload = ['enable' => $newState]; - - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', - 'put', - $payload - ); - - if ($response->successful()) { - // Reload from API to get the actual state (don't assume it worked) - $this->loadFirewallOptions(); - $this->notify('Firewall ' . ($this->firewallEnabled ? 'enabled' : 'disabled') . ' successfully', 'success'); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->notify('Failed to toggle firewall: ' . $message, 'error'); - // Reload to ensure UI matches actual state - $this->loadFirewallOptions(); - } - } catch (\Illuminate\Http\Client\RequestException $e) { - $errorMessage = $e->getMessage(); - // Try to extract more detailed error from response - if ($e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to toggle firewall: ' . $errorMessage; - $this->showErrorModal = true; - // Reload to ensure UI matches actual state - $this->loadFirewallOptions(); - } catch (\Exception $e) { - $this->error = 'Failed to toggle firewall: ' . $e->getMessage(); - $this->showErrorModal = true; - // Reload to ensure UI matches actual state - $this->loadFirewallOptions(); - } finally { - $this->loading = false; - } - } - - public function loadFirewallRules(): void - { - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules' - ); - $data = $response->json()['data'] ?? []; - $this->rules = is_array($data) ? $data : []; - $this->error = ''; - $this->showErrorModal = false; - // Dispatch event to sync Alpine.js rules array - $this->dispatch('rulesUpdated'); - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - // Try to extract more detailed error from response if available - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to load firewall rules: ' . $errorMessage; - $this->showErrorModal = true; - $this->rules = []; - } - } - - public function startCreateRule(): void - { - $this->resetRuleForm(); - - // Set default destination to first IP address - if (!empty($this->availableIps)) { - $this->ruleDest = $this->availableIps[0]['ip']; - } - - // Set default interface to first network adapter - if (!empty($this->networkAdapters) && isset($this->networkAdapters[0]) && isset($this->networkAdapters[0]['name'])) { - $this->ruleIface = $this->networkAdapters[0]['name']; - } - - $this->editingRulePos = null; - $this->showRuleForm = true; - } - - public function startEditRule(int $index): void - { - if (!isset($this->rules[$index])) { - $this->error = 'Rule not found'; - $this->showErrorModal = true; - return; - } - - $rule = $this->rules[$index]; - $this->editingRulePos = $index; - $this->showRuleForm = true; - - $this->ruleAction = $rule['action'] ?? 'ACCEPT'; - $this->ruleSource = $rule['source'] ?? ''; - $this->ruleDest = $rule['dest'] ?? ''; - $this->ruleProto = $rule['proto'] ?? 'tcp'; - $this->ruleDport = $rule['dport'] ?? ''; - $this->ruleSport = $rule['sport'] ?? ''; - $this->ruleComment = $rule['comment'] ?? ''; - $this->ruleEnable = !isset($rule['enable']) || $rule['enable'] != 0; - $this->ruleMacro = $rule['macro'] ?? ''; - $this->ruleIface = $rule['iface'] ?? ''; - $this->ruleLog = $rule['log'] ?? 'nolog'; - } - - public function resetRuleForm(): void - { - $this->ruleAction = 'ACCEPT'; - $this->ruleSource = ''; - $this->ruleDest = ''; - $this->ruleProto = 'tcp'; - $this->ruleDport = ''; - $this->ruleSport = ''; - $this->ruleComment = ''; - $this->ruleEnable = true; - $this->ruleMacro = ''; - $this->ruleIface = ''; - $this->ruleLog = 'nolog'; - } - - public function saveRule(): void - { - $this->loading = true; - try { - // Build payload - always include action, enable, and type - // Type defaults to 'in' for incoming rules (can be 'in' or 'out') - $payload = [ - 'action' => $this->ruleAction, - 'enable' => $this->ruleEnable ? 1 : 0, - 'type' => 'in', // Default to incoming rule - ]; - - // Add optional parameters only if they have values - // Note: Proxmox allows rules with just action, but some parameters may be required in certain contexts - if (!empty(trim($this->ruleSource))) { - $payload['source'] = trim($this->ruleSource); - } - // Destination is required, always include it - if (!empty(trim($this->ruleDest))) { - $payload['dest'] = trim($this->ruleDest); - } - if (!empty(trim($this->ruleProto))) { - $payload['proto'] = trim($this->ruleProto); - } - if (!empty(trim($this->ruleDport))) { - $payload['dport'] = trim($this->ruleDport); - } - if (!empty(trim($this->ruleSport))) { - $payload['sport'] = trim($this->ruleSport); - } - if (!empty(trim($this->ruleComment))) { - $payload['comment'] = trim($this->ruleComment); - } - if (!empty(trim($this->ruleMacro))) { - $payload['macro'] = trim($this->ruleMacro); - } - // Interface is required, always include it - if (!empty(trim($this->ruleIface))) { - $payload['iface'] = trim($this->ruleIface); - } - if (!empty(trim($this->ruleLog))) { - $payload['log'] = trim($this->ruleLog); - } - - if ($this->editingRulePos !== null && isset($this->rules[$this->editingRulePos])) { - // Update existing rule - $rule = $this->rules[$this->editingRulePos]; - $pos = $rule['pos']; - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, - 'put', - $payload - ); - } else { - // Create new rule - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules', - 'post', - $payload - ); - } - - if ($response->successful()) { - $this->notify('Rule ' . ($this->editingRulePos !== null ? 'updated' : 'created') . ' successfully', 'success'); - $this->resetRuleForm(); - $this->editingRulePos = null; - $this->showRuleForm = false; - $this->loadFirewallRules(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to save rule: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - $fullErrorDetails = ''; - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors'])) { - // Try to get the full error details - $errorMessages = []; - foreach ($errorData['errors'] as $error) { - if (is_array($error)) { - $errorMessages[] = json_encode($error); - } else { - $errorMessages[] = $error; - } - } - $fullErrorDetails = implode(' ', $errorMessages); - $errorMessage = !empty($fullErrorDetails) ? $fullErrorDetails : ($errorData['errors'][0]['message'] ?? $errorMessage); - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - if (isset($errorData['errors'])) { - $errorMessage .= ' ' . json_encode($errorData['errors']); - } - } - } - $this->error = 'Failed to save rule: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function toggleRule(int $index): void - { - try { - if (!isset($this->rules[$index])) { - $this->error = 'Rule not found'; - $this->showErrorModal = true; - return; - } - - $rule = $this->rules[$index]; - $pos = $rule['pos']; - - // Toggle the enable state (0 = disabled, 1 = enabled) - $currentEnable = isset($rule['enable']) ? (int)$rule['enable'] : 1; - $newEnable = $currentEnable == 0 ? 1 : 0; - - // Optimistically update the local state for immediate UI feedback - $this->rules[$index]['enable'] = $newEnable; - - // Update only the enable field - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, - 'put', - ['enable' => $newEnable] - ); - - if ($response->successful()) { - $this->notify('Rule ' . ($newEnable == 1 ? 'enabled' : 'disabled') . ' successfully', 'success'); - // Reload rules to ensure data consistency - $this->loadFirewallRules(); - } else { - // Revert the optimistic update on failure - $this->rules[$index]['enable'] = $currentEnable; - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to toggle rule: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - // Revert any optimistic updates - $this->loadFirewallRules(); - - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to toggle rule: ' . $errorMessage; - $this->showErrorModal = true; - } - } - - public function confirmDeleteRule(int $index): void - { - if (!isset($this->rules[$index])) { - $this->error = 'Rule not found'; - $this->showErrorModal = true; - return; - } - - $this->deletingRulePos = $index; - $this->showDeleteModal = true; - } - - public function cancelDeleteRule(): void - { - $this->deletingRulePos = null; - $this->showDeleteModal = false; - } - - public function deleteRule(): void - { - if ($this->deletingRulePos === null || !isset($this->rules[$this->deletingRulePos])) { - return; - } - - $rule = $this->rules[$this->deletingRulePos]; - $pos = $rule['pos']; - $this->loading = true; - $this->showDeleteModal = false; - - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, - 'delete' - ); - - if ($response->successful()) { - $this->notify('Rule deleted successfully', 'success'); - $this->loadFirewallRules(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to delete rule: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to delete rule: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - $this->deletingRulePos = null; - } - } - - public function reorderRules(int $fromPos, int $toPos): void - { - try { - // Proxmox API uses PUT with moveto parameter to reorder rules - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $fromPos, - 'put', - ['moveto' => $toPos] - ); - - if ($response->successful()) { - $this->notify('Rule reordered successfully', 'success'); - $this->loadFirewallRules(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to reorder rule: ' . $message; - $this->showErrorModal = true; - \Log::error('Failed to reorder firewall rule', [ - 'from' => $fromPos, - 'to' => $toPos, - 'response' => $response->json() - ]); - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - try { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } catch (\Exception $jsonError) { - // Response might not be JSON - } - } - $this->error = 'Failed to reorder rule: ' . $errorMessage; - $this->showErrorModal = true; - \Log::error('Exception reordering firewall rule', [ - 'from' => $fromPos, - 'to' => $toPos, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - } - } - - public function loadAliases(): void - { - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases' - ); - $data = $response->json()['data'] ?? []; - $this->aliases = is_array($data) ? $data : []; - } catch (\Exception $e) { - $this->aliases = []; - } - } - - public function createAlias(): void - { - if (empty($this->aliasName) || empty($this->aliasCidr)) { - $this->error = 'Name and CIDR are required'; - $this->showErrorModal = true; - return; - } - - $this->loading = true; - try { - $payload = [ - 'name' => $this->aliasName, - 'cidr' => $this->aliasCidr, - ]; - if ($this->aliasComment) { - $payload['comment'] = $this->aliasComment; - } - - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases', - 'post', - $payload - ); - - if ($response->successful()) { - $this->notify('Alias created successfully', 'success'); - $this->aliasName = ''; - $this->aliasCidr = ''; - $this->aliasComment = ''; - $this->loadAliases(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to create alias: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to create alias: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function deleteAlias(string $name): void - { - $this->loading = true; - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $name, - 'delete' - ); - - if ($response->successful()) { - $this->notify('Alias deleted successfully', 'success'); - $this->loadAliases(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to delete alias: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to delete alias: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function loadIpsets(): void - { - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset' - ); - $data = $response->json()['data'] ?? []; - $this->ipsets = is_array($data) ? $data : []; - } catch (\Exception $e) { - $this->ipsets = []; - } - } - - public function createIpset(): void - { - if (empty($this->ipsetName)) { - $this->error = 'Name is required'; - $this->showErrorModal = true; - return; - } - - $this->loading = true; - try { - $payload = [ - 'name' => $this->ipsetName, - ]; - if ($this->ipsetComment) { - $payload['comment'] = $this->ipsetComment; - } - - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset', - 'post', - $payload - ); - - if ($response->successful()) { - $this->notify('IP set created successfully', 'success'); - $this->ipsetName = ''; - $this->ipsetComment = ''; - $this->loadIpsets(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to create IP set: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to create IP set: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function deleteIpset(string $name): void - { - $this->loading = true; - try { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name, - 'delete' - ); - - if ($response->successful()) { - $this->notify('IP set deleted successfully', 'success'); - $this->loadIpsets(); - } else { - $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; - $this->error = 'Failed to delete IP set: ' . $message; - $this->showErrorModal = true; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to delete IP set: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function loadNetworkAdapters(): void - { - try { - $proxmox = new Proxmox; - $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; - - $this->networkAdapters = []; - $this->adapterFirewallStates = []; - - foreach ($config as $key => $value) { - if (!is_string($key) || !str_starts_with($key, 'net')) { - continue; - } - - $configString = (string) $value; - $segments = array_filter(array_map('trim', explode(',', $configString))); - $model = 'virtio'; - $mac = null; - $firewallEnabled = false; - - foreach ($segments as $segment) { - if (!str_contains($segment, '=')) { - continue; - } - - [$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2)); - $segmentKeyLower = strtolower($segmentKey); - - if (in_array($segmentKeyLower, ['virtio', 'e1000', 'e1000e', 'rtl8139', 'vmxnet3', 'pcnet'], true)) { - $model = $segmentKeyLower; - $mac = $segmentValue; - } elseif ($segmentKeyLower === 'firewall') { - $firewallEnabled = ($segmentValue == '1' || $segmentValue === '1'); - } - } - - $this->networkAdapters[] = [ - 'name' => $key, - 'model' => $model, - 'mac' => $mac, - ]; - $this->adapterFirewallStates[$key] = $firewallEnabled; - } - } catch (\Exception $e) { - $this->networkAdapters = []; - $this->adapterFirewallStates = []; - } - } - - public function toggleAdapterFirewall(string $adapterName): void - { - $this->loading = true; - try { - $proxmox = new Proxmox; - $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; - - if (!isset($config[$adapterName])) { - $this->error = 'Network adapter not found: ' . $adapterName; - $this->showErrorModal = true; - $this->loading = false; - return; - } - - $currentConfig = (string) $config[$adapterName]; - $segments = array_filter(array_map('trim', explode(',', $currentConfig))); - $newSegments = []; - $firewallFound = false; - $currentFirewallState = $this->adapterFirewallStates[$adapterName] ?? false; - $newFirewallState = !$currentFirewallState; - - foreach ($segments as $segment) { - if (!str_contains($segment, '=')) { - $newSegments[] = $segment; - continue; - } - - [$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2)); - - if (strtolower($segmentKey) === 'firewall') { - $firewallFound = true; - if ($newFirewallState) { - $newSegments[] = 'firewall=1'; - } - // If disabling, we skip adding firewall=1, effectively removing it - } else { - $newSegments[] = $segment; - } - } - - // If firewall wasn't in the config and we're enabling it, add it - if (!$firewallFound && $newFirewallState) { - $newSegments[] = 'firewall=1'; - } - - $newConfig = implode(',', $newSegments); - - $proxmox->request( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', - 'put', - [$adapterName => $newConfig] - ); - - $this->adapterFirewallStates[$adapterName] = $newFirewallState; - $this->notify('Firewall ' . ($newFirewallState ? 'enabled' : 'disabled') . ' for ' . $adapterName, 'success'); - $this->loadNetworkAdapters(); // Reload to ensure consistency - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - $errorData = $e->response()->json(); - if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) { - $errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage; - } elseif (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } - $this->error = 'Failed to toggle firewall for adapter: ' . $errorMessage; - $this->showErrorModal = true; - } finally { - $this->loading = false; - } - } - - public function loadFirewallLogs(): void - { - $this->logError = ''; - try { - // Proxmox API: GET /api2/json/nodes/{node}/qemu/{vmid}/firewall/log - // Documentation: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/firewall/log - // Returns: text/plain format with log entries, one per line - // Fetch logs in batches and collect all of them - $allRawLogs = []; - $start = 0; - $batchSize = 500; - $maxLogs = $this->logLimit; - $fetchedCount = 0; - $totalBodyLength = 0; - $lastContentType = ''; - $lastIsJson = false; - - // Keep fetching until we have all logs or reach the limit - do { - $response = $this->firewallRequest( - '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', - 'get', - [ - 'limit' => $batchSize, - 'start' => $start - ] - ); - - if (! $response->successful()) { - $errorData = []; - try { - $errorData = $response->json(); - } catch (\Exception $e) { - // Response might not be JSON - } - $errorMsg = $errorData['message'] ?? 'HTTP ' . $response->status(); - $this->logError = 'Failed to fetch logs: ' . $errorMsg; - \Log::warning('Firewall log API request failed', [ - 'status' => $response->status(), - 'body' => substr($response->body(), 0, 500), - 'server_id' => $this->server->id, - 'vm_id' => $this->server->vm_id, - ]); - $this->firewallLogs = []; - return; - } - - // Proxmox firewall log endpoint can return either text/plain or JSON format - // Try to parse as JSON first, then fall back to text/plain - $body = $response->body(); - $totalBodyLength += strlen($body); - $batchRawLogs = []; - - // Check if response is JSON - $contentType = $response->header('Content-Type'); - $lastContentType = $contentType ?? ''; - $isJson = str_contains($contentType ?? '', 'json'); - $lastIsJson = $isJson; - - if ($isJson) { - try { - $jsonData = $response->json(); - // Proxmox may return logs in different formats: - // 1. { "data": [ {"n": 1, "t": "log text"}, ... ] } - // 2. { "data": [ "line1", "line2", ... ] } - // 3. { "data": "line1\nline2\n..." } - // 4. [ {"n": 1, "t": "log text"}, ... ] - - if (isset($jsonData['data'])) { - if (is_array($jsonData['data'])) { - // Handle array of log objects or strings - foreach ($jsonData['data'] as $item) { - if (is_array($item) && isset($item['t'])) { - // Proxmox format: {"n": 1, "t": "log text"} - $batchRawLogs[] = $item['t']; - } elseif (is_string($item)) { - // Plain string format - $batchRawLogs[] = $item; - } elseif (is_array($item)) { - // Other array format, try to get text from various fields - $text = $item['text'] ?? $item['message'] ?? $item['line'] ?? json_encode($item); - $batchRawLogs[] = $text; - } - } - } elseif (is_string($jsonData['data'])) { - // Multi-line string format - $lines = preg_split('/\r\n|\r|\n/', $jsonData['data']); - $batchRawLogs = array_filter($lines, static function ($line) { - return ! empty(trim($line)); - }); - } - } elseif (is_array($jsonData)) { - // Array at root level - foreach ($jsonData as $item) { - if (is_array($item) && isset($item['t'])) { - $batchRawLogs[] = $item['t']; - } elseif (is_string($item)) { - $batchRawLogs[] = $item; - } - } - } - } catch (\Exception $e) { - // Fall through to text parsing - \Log::warning('Failed to parse JSON response, falling back to text', [ - 'error' => $e->getMessage(), - 'response_sample' => substr($body, 0, 500) - ]); - } - } - - // If no JSON data or JSON parsing failed, try text/plain format - if (empty($batchRawLogs) && ! empty($body)) { - // Split by newlines (handle both \n and \r\n) - $lines = preg_split('/\r\n|\r|\n/', $body); - $batchRawLogs = array_filter($lines, static function ($line) { - return ! empty(trim($line)); - }); - } - - // Re-index array after filtering - $batchRawLogs = array_values($batchRawLogs); - - // Add this batch to all logs - $allRawLogs = array_merge($allRawLogs, $batchRawLogs); - $fetchedCount = count($batchRawLogs); - - // Move to next batch - $start += $batchSize; - - // Break if we got fewer logs than requested (meaning we've reached the end) - // or if we've reached our limit - if ($fetchedCount < $batchSize || count($allRawLogs) >= $maxLogs) { - break; - } - } while (true); - - // Limit to maxLogs - if (count($allRawLogs) > $maxLogs) { - $allRawLogs = array_slice($allRawLogs, 0, $maxLogs); - } - - $rawLogs = $allRawLogs; - - \Log::info('Firewall logs fetched from Proxmox API', [ - 'raw_count' => count($rawLogs), - 'total_body_length' => $totalBodyLength, - 'last_content_type' => $lastContentType, - 'last_is_json' => $lastIsJson, - 'batches_fetched' => ($start / $batchSize), - 'first_log_type' => ! empty($rawLogs) ? gettype($rawLogs[0]) : 'empty', - 'first_log_length' => ! empty($rawLogs) && is_string($rawLogs[0]) ? strlen($rawLogs[0]) : 'N/A', - 'sample' => ! empty($rawLogs) ? substr(is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : $rawLogs[0], 0, 300) : null, - 'first_3_logs_full' => array_slice($rawLogs, 0, 3), - ]); - - $this->firewallLogs = []; - - $successfullyParsed = 0; - $skippedEmpty = 0; - $skippedInvalid = 0; - - foreach ($rawLogs as $index => $logEntry) { - $logText = ''; - - if (is_string($logEntry)) { - $logText = trim($logEntry); - } elseif (is_array($logEntry)) { - // Handle various possible array formats from Proxmox API - // Note: 't' is the text field, 'n' is the line number - prioritize 't'! - $logText = trim( - $logEntry['t'] // Proxmox format: {"n": 1, "t": "log text"} - ?? $logEntry['text'] // Alternative text field - ?? $logEntry['raw'] // Raw log field - ?? $logEntry['message'] // Message field - ?? $logEntry['log'] // Log field - ?? $logEntry['line'] // Line field - ?? json_encode($logEntry) // Last resort: encode the whole thing - ); - } - - if ($logText === '' || strlen($logText) < 10) { - $skippedEmpty++; - continue; - } - - try { - $parsed = $this->parseFirewallLog($logText); - // Check if the log was actually parsed (has at least action or source/destination) - if ($parsed !== null && ($parsed['action'] !== null || $parsed['source'] !== null || $parsed['destination'] !== null)) { - $this->firewallLogs[] = $parsed; - $successfullyParsed++; - } else { - $skippedInvalid++; - if ($index < 5) { // Log first 5 unparseable entries for debugging - \Log::warning('Firewall log could not be parsed (missing key fields)', [ - 'entry' => substr($logText, 0, 300), - 'parsed' => $parsed - ]); - } - } - } catch (\Exception $e) { - $skippedInvalid++; - if ($index < 5) { // Log first 5 errors for debugging - \Log::warning('Failed to parse firewall log entry', [ - 'entry' => substr($logText, 0, 300), - 'error' => $e->getMessage() - ]); - } - } - } - - \Log::info('Firewall log parsing summary', [ - 'total_raw_logs' => count($rawLogs), - 'successfully_parsed' => $successfullyParsed, - 'skipped_empty' => $skippedEmpty, - 'skipped_invalid' => $skippedInvalid - ]); - - \Log::info('Firewall logs final count', [ - 'total_logs' => count($this->firewallLogs), - 'first_3_sources' => array_slice(array_column($this->firewallLogs, 'source'), 0, 3), - ]); - - if (empty($this->firewallLogs) && ! empty($rawLogs)) { - $this->logError = 'Logs received (' . count($rawLogs) . ') but could not be processed. Successfully parsed: ' . $successfullyParsed . ', Skipped invalid: ' . $skippedInvalid . '. Check Laravel logs for details.'; - } elseif (empty($this->firewallLogs)) { - $this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).'; - } - } catch (\Exception $e) { - $errorMessage = $e->getMessage(); - if (method_exists($e, 'response') && $e->response()) { - try { - $errorData = $e->response()->json(); - if (isset($errorData['message'])) { - $errorMessage = $errorData['message']; - } - } catch (\Exception $jsonError) { - // Response might not be JSON - } - } - $this->logError = 'Error loading logs: ' . $errorMessage; - \Log::error('Failed to load firewall logs', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - 'server_id' => $this->server->id, - 'vm_id' => $this->server->vm_id, - ]); - $this->firewallLogs = []; - } - } - - protected function parseFirewallLog(string $logText): array - { - $result = [ - 'raw' => $logText, - 'timestamp_display' => null, - 'timestamp_iso' => null, - 'action' => null, - 'action_badge_class' => 'bg-base/20 text-base/70', - 'level_numeric' => null, - 'level_label' => null, - 'level_badge_class' => 'bg-base/20 text-base/70', - 'protocol' => null, - 'source' => null, - 'source_port' => null, - 'destination' => null, - 'destination_port' => null, - 'iface' => null, - ]; - - // Extract level and interface (first tokens) - if (preg_match('/^\S+\s+(\d+)\s+([^\s]+)/', $logText, $matches)) { - $levelNumeric = (int) $matches[1]; - $iface = $matches[2]; - $priMap = [ - 0 => 'emerg', - 1 => 'alert', - 2 => 'crit', - 3 => 'err', - 4 => 'warning', - 5 => 'notice', - 6 => 'info', - 7 => 'debug', - ]; - $levelLabel = $priMap[$levelNumeric] ?? 'info'; - $levelBadgeMap = [ - 'emerg' => 'bg-error text-white', - 'alert' => 'bg-error/80 text-white', - 'crit' => 'bg-error/60 text-white', - 'err' => 'bg-error/20 text-error', - 'warning' => 'bg-warning/20 text-warning', - 'notice' => 'bg-primary/20 text-primary', - 'info' => 'bg-primary/20 text-primary', - 'debug' => 'bg-base/20 text-base/70', - ]; - - $result['level_numeric'] = $levelNumeric; - $result['level_label'] = strtoupper($levelLabel); - $result['level_badge_class'] = $levelBadgeMap[$levelLabel] ?? 'bg-base/20 text-base/70'; - $result['iface'] = $iface; - } - - // Extract timestamp and timezone - if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})\s+([+-]\d{4})/', $logText, $matches)) { - $timestamp = $matches[1]; - $tz = $matches[2]; - $tzNormalized = substr($tz, 0, 3) . ':' . substr($tz, 3); - try { - $carbon = Carbon::createFromFormat('d/M/Y:H:i:s P', $timestamp . ' ' . $tzNormalized); - $result['timestamp_iso'] = $carbon->toIso8601String(); - $result['timestamp_display'] = $carbon->format('Y-m-d H:i:s T'); - } catch (\Exception $e) { - $result['timestamp_display'] = str_replace('/', '-', $timestamp) . ' ' . $tzNormalized; - } - } - - // Extract action (REJECT, ACCEPT, DROP) - if (preg_match('/\b(REJECT|ACCEPT|DROP):/i', $logText, $matches)) { - $action = strtoupper($matches[1]); - $actionBadgeMap = [ - 'ACCEPT' => 'bg-success/20 text-success', - 'DROP' => 'bg-error/20 text-error', - 'REJECT' => 'bg-warning/20 text-warning', - ]; - $result['action'] = $action; - $result['action_badge_class'] = $actionBadgeMap[$action] ?? 'bg-base/20 text-base/70'; - } - - // Extract protocol - if (preg_match('/PROTO=([^\s]+)/', $logText, $matches)) { - $result['protocol'] = strtoupper($matches[1]); - } - - // Extract source/destination IPs and ports - if (preg_match('/SRC=([^\s]+)/', $logText, $matches)) { - $result['source'] = $matches[1]; - } - - if (preg_match('/DST=([^\s]+)/', $logText, $matches)) { - $result['destination'] = $matches[1]; - } - - if (preg_match('/SPT=(\d+)/', $logText, $matches)) { - $result['source_port'] = $matches[1]; - } - - if (preg_match('/DPT=(\d+)/', $logText, $matches)) { - $result['destination_port'] = $matches[1]; - } - - return $result; - } - - /** - * Fetch IP location data from ip-api.com - */ - public function fetchIPLocation(string $ip): array - { - try { - // Validate IP address - if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return [ - 'status' => 'fail', - 'message' => 'Invalid or private IP address' - ]; - } - - // Fetch location data from ip-api.com (HTTP endpoint) - $response = Http::timeout(5)->get("http://ip-api.com/json/{$ip}"); - - if ($response->successful()) { - $data = $response->json(); - - // Check if the API returned success - if (isset($data['status']) && $data['status'] === 'success') { - return $data; - } else { - return [ - 'status' => 'fail', - 'message' => $data['message'] ?? 'Unable to fetch location data' - ]; - } - } - - return [ - 'status' => 'fail', - 'message' => 'Failed to fetch location data (HTTP ' . $response->status() . ')' - ]; - } catch (\Exception $e) { - return [ - 'status' => 'fail', - 'message' => 'Error: ' . $e->getMessage() - ]; - } - } - - public function render() - { - return view('proxmox::firewall')->layoutData([ - 'server' => $this->server, - 'status' => $this->status, - 'sidebar' => true, - ]); - } -} diff --git a/extensions/Servers/Proxmox/Livewire/Reinstall.php b/extensions/Servers/Proxmox/Livewire/Reinstall.php index 5e53edee4..d8768f202 100644 --- a/extensions/Servers/Proxmox/Livewire/Reinstall.php +++ b/extensions/Servers/Proxmox/Livewire/Reinstall.php @@ -57,12 +57,19 @@ class Reinstall extends Component $this->server->os_id = $this->os; $this->server->save(); - $proxmox = new Proxmox; - $proxmox->reinstall($this->server); + try { + $proxmox = new Proxmox; + $proxmox->reinstall($this->server); + } catch (\Throwable $e) { + $this->notify('Reinstall failed: ' . $e->getMessage(), 'error'); + return; + } sleep(10); - $this->redirect(route('proxmox.index'), true); + // Use full page reload (navigate: false) so lazy-loaded server cards + // properly initialise after the Livewire SPA navigation. + $this->redirect(route('proxmox.index')); } public function render() diff --git a/extensions/Servers/Proxmox/Livewire/Server.php b/extensions/Servers/Proxmox/Livewire/Server.php index 95fb5aa14..aacf7338c 100644 --- a/extensions/Servers/Proxmox/Livewire/Server.php +++ b/extensions/Servers/Proxmox/Livewire/Server.php @@ -65,8 +65,7 @@ class Server extends Component return 10; // Minimum threshold of 10 GiB } - $totalTraffic = 0; - $serverCount = 0; + $trafficValues = []; foreach ($allServers as $server) { // Get the earliest and latest statistics for this month for each server @@ -86,23 +85,21 @@ class Server extends Component $netoutDiff = max(0, $latestStat->netout - $firstStat->netout); $totalBytes = $netinDiff + $netoutDiff; - // Convert to GiB and add to total - $totalTraffic += $totalBytes / (1024 * 1024 * 1024); - $serverCount++; + // Convert to GiB and collect + $trafficValues[] = $totalBytes / (1024 * 1024 * 1024); } } - if ($serverCount === 0) { + if (empty($trafficValues)) { return 10; // Minimum threshold of 10 GiB } - // Calculate average bandwidth across all servers - $averageBandwidth = $totalTraffic / $serverCount; + // Calculate actual 95th percentile by sorting and taking the value at the 95th rank + sort($trafficValues); + $index = (int) ceil(0.95 * count($trafficValues)) - 1; + $threshold95Percent = $trafficValues[$index]; - // Calculate 95% of average - $threshold95Percent = $averageBandwidth * 0.95; - - // Return the maximum of 95% average or 10 GiB minimum + // Return the maximum of 95th percentile or 10 GiB minimum return round(max($threshold95Percent, 10), 2); }); } diff --git a/extensions/Servers/Proxmox/views/backups.blade.php b/extensions/Servers/Proxmox/views/backups.blade.php index 7129c154c..e59464912 100644 --- a/extensions/Servers/Proxmox/views/backups.blade.php +++ b/extensions/Servers/Proxmox/views/backups.blade.php @@ -29,7 +29,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> diff --git a/extensions/Servers/Proxmox/views/boot-order.blade.php b/extensions/Servers/Proxmox/views/boot-order.blade.php index 1ab34a902..1d3da9696 100644 --- a/extensions/Servers/Proxmox/views/boot-order.blade.php +++ b/extensions/Servers/Proxmox/views/boot-order.blade.php @@ -99,7 +99,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -172,7 +172,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -254,7 +254,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -381,7 +381,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -483,7 +483,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -600,7 +600,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -700,7 +700,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -759,7 +759,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> diff --git a/extensions/Servers/Proxmox/views/components/dropdown.blade.php b/extensions/Servers/Proxmox/views/components/dropdown.blade.php index 0ea180051..bb60299b4 100644 --- a/extensions/Servers/Proxmox/views/components/dropdown.blade.php +++ b/extensions/Servers/Proxmox/views/components/dropdown.blade.php @@ -26,7 +26,7 @@
merge(['class' => 'relative inline-block']) }} > diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php index de710a30a..6170a1efc 100644 --- a/extensions/Servers/Proxmox/views/components/layout.blade.php +++ b/extensions/Servers/Proxmox/views/components/layout.blade.php @@ -59,8 +59,8 @@ -
-
+
+
@@ -1363,8 +1363,8 @@ -
-
+
+
@@ -1601,11 +1601,13 @@ /* Page Transition Styles - Only slide-in for new content */ .proxmox-page-wrapper { opacity: 1; - transform: translateX(0); + /* Do NOT set transform here — any transform (even translateX(0)) creates a + CSS containing block, which makes 'fixed' children (modal backdrops) + position relative to this element instead of the viewport. */ } .proxmox-page-wrapper.slide-in { - animation: slideIn 0.2s cubic-bezier(0.22, 1, 0.36, 1) forwards; + animation: slideIn 0.2s cubic-bezier(0.22, 1, 0.36, 1); } @keyframes slideIn { @@ -1615,7 +1617,7 @@ } to { opacity: 1; - transform: translateX(0); + transform: none; } } @@ -1623,7 +1625,6 @@ .proxmox-page-wrapper.slide-in { animation: none; opacity: 1; - transform: none; } } @@ -2089,15 +2090,14 @@ isAdvancedView = false; } isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced'; - window.addEventListener('proxmox-view-mode-changed', (e) => { - isAdvancedView = e.detail.isAdvancedView; - }); $watch('open', () => { if (open) { isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced'; } }); - "> + " + @proxmox-view-mode-changed.window="isAdvancedView = $event.detail.isAdvancedView" + > - -
- -
-
-
-
- -
-
-

Firewall Status

-

Enable or disable the firewall for this virtual machine

-
-
-
- Disabled - - Enabled -
-
-
- - -
-
-
- -
-
-

Network Adapters

-

Enable or disable firewall for each network adapter

-
-
- - @if(count($networkAdapters) > 0) -
- @foreach($networkAdapters as $adapter) -
-
-
-
-

{{ $adapter['name'] }}

- - {{ ucfirst($adapter['model'] ?? 'Unknown') }} - -
-
- @if($adapter['mac']) -
MAC: {{ $adapter['mac'] }}
- @endif -
-

Enables or disables the firewall ability for this network adapter

-
-
- Network Adapter Firewall -
- Disabled - - Enabled -
-
-
-
- @endforeach -
- @else -
- -

No network adapters found

-
- @endif -
- - @if($firewallEnabled) - -
-
-
-
- -
-
-

Firewall Rules

-

Manage allow and block rules for network traffic

-
-
-
- - -
-
- - - @if(count($rules) > 0) -
- - - - - - - - - - - - - - - @foreach($rules as $index => $rule) - - - - - - - - - - - @endforeach - -
OrderActionSourceDestinationProtocolPortStatusActions
{{ $rule['pos'] ?? '-' }} - @php - $actionClass = match($rule['action'] ?? '') { - 'ACCEPT' => 'bg-success/20 text-success', - 'REJECT' => 'bg-warning/20 text-warning', - 'DROP' => 'bg-error/20 text-error', - default => '' - }; - @endphp - - {{ $rule['action'] ?? '-' }} - - -
- {{ $rule['source'] ?? '-' }} -
- - - -
- -
-
-
- {{ $rule['dest'] ?? '-' }} -
- - - -
- -
-
{{ strtoupper($rule['proto'] ?? 'any') }}{{ $rule['dport'] ?? $rule['sport'] ?? '-' }} - @if(isset($rule['enable']) && $rule['enable'] == 0) - Disabled - @else - Enabled - @endif - -
- - - -
-
-
- @else -
- -

No firewall rules configured

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

Firewall Logs

-

- Recent firewall activity and blocked/allowed connections - - ({{ count($firewallLogs) }} entries loaded) - -

-
-
- -
- - @if(!empty($logError)) -
-
- -
-

{{ $logError }}

-
-
-
- @endif - - @if(count($firewallLogs) > 0) -
- -
- -
- Policy: - - - - - - -
- - -
- Protocol: - - - - - - - - - - -
- - -
-
- - -
-
-
- Showing - - of - -
-
- Page - - / - -
-
-
-
- -
- - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - Actions -
-
- - -
- - - - - -
- -
- -

No logs matched your search.

-
-
- @elseif(empty($logError)) -
- -

No firewall logs available

-

Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options)

-
- @endif -
- @else -
- -

Firewall is Disabled

-

Enable the firewall above to manage firewall rules.

-
- @endif -
- -@script - -@endscript - -
-
-
- - - - - - -@if($error && $showErrorModal) - -
-
- -
-
- -
- -
-

Firewall Error

-
-

{{ $error }}

-
-
-
-
-
-@endif - - -@if($showRuleForm) - -
-
- -
-
- -
-

{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-@endif - - -@if($showDeleteModal && $deletingRulePos !== null) - @php - $ruleToDelete = collect($rules)->firstWhere('pos', $deletingRulePos); - @endphp - -
-
- -
-
- -
-
-
- -
-
-

Delete Firewall Rule

-

- Are you sure you want to delete this firewall rule? This action cannot be undone and may affect your network security. -

- @if($ruleToDelete) -
-
-
- Action: - - {{ $ruleToDelete['action'] ?? '-' }} - -
- @if(!empty($ruleToDelete['source'])) -
- Source: - {{ $ruleToDelete['source'] }} -
- @endif - @if(!empty($ruleToDelete['dest'])) -
- Destination: - {{ $ruleToDelete['dest'] }} -
- @endif - @if(!empty($ruleToDelete['proto'])) -
- Protocol: - {{ strtoupper($ruleToDelete['proto']) }} -
- @endif - @if(!empty($ruleToDelete['dport'])) -
- Port: - {{ $ruleToDelete['dport'] }} -
- @endif - @if(!empty($ruleToDelete['comment'])) -
- Comment: - {{ $ruleToDelete['comment'] }} -
- @endif -
-
- @endif -
-
-
-
- - -
-
-
-@endif diff --git a/extensions/Servers/Proxmox/views/network.blade.php b/extensions/Servers/Proxmox/views/network.blade.php index a323f7222..47b341fc5 100644 --- a/extensions/Servers/Proxmox/views/network.blade.php +++ b/extensions/Servers/Proxmox/views/network.blade.php @@ -54,12 +54,7 @@ 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; - }); - " + @proxmox-view-mode-changed.window="isAdvancedView = $event.detail.isAdvancedView" x-show="isAdvancedView" x-transition>
@@ -129,7 +124,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -164,12 +159,7 @@
@@ -200,7 +190,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -296,7 +286,8 @@ 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' }}"> + title="{{ ($interfaces[$name]['linkdown'] ?? false) ? 'Connect' : 'Disconnect' }}" + aria-label="{{ ($interfaces[$name]['linkdown'] ?? false) ? 'Connect interface' : 'Disconnect interface' }}"> @if($interfaces[$name]['linkdown'] ?? false) @@ -305,10 +296,12 @@ @endif
@@ -348,7 +341,7 @@
@foreach($server->ipAddresses as $network) -
+

{{ strtoupper($network->type ?? 'IPv4') }} Address

@@ -519,12 +512,7 @@

Technical Details (Guest Agent)

@@ -603,12 +591,7 @@ @endphp
@@ -639,7 +622,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -692,7 +675,7 @@ @if(count($additionalAddresses) > 0) @foreach($additionalAddresses as $address) -
+

{{ strtoupper($address['type'] === 'ipv6' ? 'IPv6' : 'IPv4') }} Address

diff --git a/extensions/Servers/Proxmox/views/overview.blade.php b/extensions/Servers/Proxmox/views/overview.blade.php index a910c8eba..fc835c713 100644 --- a/extensions/Servers/Proxmox/views/overview.blade.php +++ b/extensions/Servers/Proxmox/views/overview.blade.php @@ -38,12 +38,7 @@ localStorage.setItem('overview-notice-collapsed', this.isCollapsed); } }" - x-init=" - isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced'; - window.addEventListener('proxmox-view-mode-changed', (e) => { - isAdvancedView = e.detail.isAdvancedView; - }); - " + @proxmox-view-mode-changed.window="isAdvancedView = $event.detail.isAdvancedView" x-show="isAdvancedView" x-transition>
@@ -103,7 +98,7 @@ 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" + @click.self="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -314,7 +309,7 @@ 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" + @click.self="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -470,7 +465,7 @@ 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="showHostnameEditModal = false; $wire.cancelEditingHostname()" + @click.self="showHostnameEditModal = false; $wire.cancelEditingHostname()" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -634,7 +629,7 @@ 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="showPasswordViewModal = false" + @click.self="showPasswordViewModal = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;" x-data="{ @@ -692,7 +687,7 @@ 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="showPasswordEditModal = false; $wire.cancelEditingPassword()" + @click.self="showPasswordEditModal = false; $wire.cancelEditingPassword()" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -796,7 +791,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -894,7 +889,7 @@ 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" + @click.self="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -1096,7 +1091,7 @@ 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" + @click.self="open = false" @keydown.escape.window="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;"> @@ -1163,7 +1158,7 @@ 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="showExportErrorModal = false" + @click.self="showExportErrorModal = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -1269,7 +1264,7 @@ 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" + @click.self="open = false" class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -1307,27 +1302,27 @@
-

+

Windows Server Networking (IPv4/IPv6 Setup):

-

+

Cloud-init does not automatically configure IPv4 or IPv6 networking on Windows Server installations. Please configure the addresses manually as assigned in the control panel of your installation.

-

+

Windows Server Activation (Licensing):

-

+

No Windows Server license key is provided by Virtnet.bond. It is the responsibility of the account holder to provide and maintain active and compliant licensing with all Windows Server products.

-

+

Periodic Licensing Audits and User Compliance:

-

+

Virtnet.bond reserves the right to perform periodic audits of Windows Server installations to ensure compliance with Microsoft's licensing terms. Non-compliant installations may be subject to suspension or termination. If you have any questions regarding licensing compliance, please contact our support team.

@@ -1442,7 +1437,6 @@ ); if (historyLength > 0) { - console.log('Creating chart for interface:', interfaceId); requestAnimationFrame(() => { setTimeout(() => { if (!charts[interfaceId]) { @@ -1455,7 +1449,6 @@ } if (retries > 0) { - console.log(`Chart data not ready for ${interfaceId}, retrying... (${retries} attempts left)`); setTimeout(() => attemptCreateChart(retries - 1, delay), delay); } }; @@ -1594,15 +1587,6 @@ let uploadRate = interfaceData.uploadRate ?? 0; // Debug: Log interface data to verify each interface gets unique data - console.log(`Updating interface ${interfaceId} (${interfaceData.id}):`, { - downloadRate, - uploadRate, - totalIn: interfaceData.totalIn, - totalOut: interfaceData.totalOut, - hasHistory: !!interfaceData.history, - historyLength: interfaceData.history?.download?.length || 0 - }); - // Update peak rates if (!peakRates[interfaceId]) { peakRates[interfaceId] = { download: 0, upload: 0 }; @@ -1651,7 +1635,6 @@ interfaceData.history.upload?.length || 0 ); if (historyLength > 0) { - console.log('Creating chart for', interfaceId, 'on data update (container was open)'); setTimeout(() => createChart(interfaceId, interfaceData), 100); } } @@ -1800,15 +1783,6 @@ }); // Debug logging - console.log('Chart data for', interfaceId, { - interfaceName: interfaceData.name, - downloadData: downloadData, - uploadData: uploadData, - downloadMax: downloadData.length > 0 ? Math.max(...downloadData, 0) : 0, - uploadMax: uploadData.length > 0 ? Math.max(...uploadData, 0) : 0, - hasData: downloadData.some(v => v > 0) || uploadData.some(v => v > 0) - }); - // Calculate 95th percentile from combined traffic const combinedTraffic = downloadData.map((d, i) => d + (uploadData[i] || 0)); const sortedTraffic = [...combinedTraffic].filter(v => v > 0).sort((a, b) => a - b); @@ -1818,14 +1792,6 @@ p95Value = sortedTraffic[Math.min(p95Index, sortedTraffic.length - 1)]; } const p95Data = Array(downloadData.length).fill(p95Value); - - console.log('95th Percentile calculation:', { - combinedTraffic: combinedTraffic, - sortedTraffic: sortedTraffic, - p95Value: p95Value, - p95DataLength: p95Data.length - }); - // Create a local formatRate function that's guaranteed to be in scope const formatRateForChart = (bytesPerSec) => { if (typeof bytesPerSec !== 'number' || isNaN(bytesPerSec)) { @@ -1939,9 +1905,6 @@ const poll = async () => { if (pollInFlight || !pollingEndpoint) return; pollInFlight = true; - - console.log('Polling network data from:', pollingEndpoint); - try { const csrfElement = document.querySelector('meta[name="csrf-token"]'); const csrfToken = csrfElement ? csrfElement.content : ''; @@ -1955,13 +1918,8 @@ }, credentials: 'same-origin', }); - - console.log('Network Traffic Response status:', response.status); - if (response.ok) { const data = await response.json(); - console.log('Network Traffic Data received:', data); - const container = document.getElementById('network-traffic-container'); const wasEmpty = !networkData || !networkData.interfaces || networkData.interfaces.length === 0; const hasExistingInterfaces = container && container.querySelectorAll('.chart-toggle').length > 0; @@ -1983,7 +1941,6 @@ // 3. No existing interfaces found in container if (data.interfaces && data.interfaces.length > 0) { if (showsPlaceholder || !hasExistingInterfaces || wasEmpty) { - console.log('Rendering interfaces - showsPlaceholder:', showsPlaceholder, 'hasExistingInterfaces:', hasExistingInterfaces, 'wasEmpty:', wasEmpty); // Render immediately - no delay renderInterfaces(); // Also update data immediately after render to ensure values are set @@ -2027,7 +1984,6 @@ ); if (historyLength > 0) { - console.log('Creating chart for interface:', interfaceId, 'after tab switch'); requestAnimationFrame(() => { setTimeout(() => { if (!charts[interfaceId]) { @@ -2087,10 +2043,6 @@ const container = document.getElementById('network-traffic-container'); pollingEndpoint = container?.dataset?.liveTrafficEndpoint || null; - - console.log('Network Traffic Bootstrap: Container found:', !!container); - console.log('Network Traffic Bootstrap: Endpoint:', pollingEndpoint); - if (pollingEndpoint && container) { // Check if container exists and has endpoint - be less strict about visibility // Sometimes container exists but offsetParent check fails initially @@ -2099,8 +2051,6 @@ if (isVisible || containerExists) { // Always poll immediately on bootstrap (don't wait for interval) - console.log('Network Traffic: Polling immediately on bootstrap...'); - // Poll immediately - don't wait poll(); @@ -2109,11 +2059,9 @@ setTimeout(() => { if (!pollInterval) { pollInterval = setInterval(poll, 30000); - console.log('Network Traffic: Polling interval set up (30 seconds)'); } }, 100); } else { - console.log('Network Traffic: Container not ready, will retry via visibility check'); } } }; @@ -2145,7 +2093,6 @@ } } else { // Successfully bootstrapped, also try immediate poll as backup - console.log('Network Traffic: Bootstrap successful, ensuring immediate poll...'); } // Re-initialize on Livewire lifecycle events (immediate) @@ -2208,12 +2155,6 @@ ); if (historyLength > 0) { - console.log('Creating chart for interface:', interfaceId, 'with data:', { - id: interfaceData.id, - name: interfaceData.name, - historyLength: historyLength - }); - requestAnimationFrame(() => { setTimeout(() => { if (!charts[interfaceId]) { @@ -2226,7 +2167,6 @@ } if (retries > 0) { - console.log(`Chart data not ready for ${interfaceId}, retrying... (${retries} attempts left)`); setTimeout(() => attemptCreateChart(retries - 1, delay), delay); } else { console.warn('Failed to create chart for', interfaceId, 'after multiple attempts'); @@ -2256,7 +2196,6 @@ // 1. No polling interval (not initialized), OR // 2. Shows placeholder (was reset) if (isVisible && hasEndpoint && (showsPlaceholder || !pollInterval)) { - console.log('Network Traffic: Container needs initialization (placeholder:', showsPlaceholder, 'no interval:', !pollInterval, '), re-initializing...'); bootstrap(); } @@ -2266,7 +2205,6 @@ if (!pollingEndpoint) { pollingEndpoint = hasEndpoint; } - console.log('Network Traffic: Container visible with endpoint but no polling active, polling immediately...'); poll(); setTimeout(() => { if (!pollInterval) { @@ -2281,7 +2219,6 @@ // Register cleanup function for Livewire navigation window.__networkTrafficCleanup = () => { - console.log('Network Traffic: Cleaning up for navigation...'); if (visibilityCheckInterval) { clearInterval(visibilityCheckInterval); } diff --git a/extensions/Servers/Proxmox/views/reinstall.blade.php b/extensions/Servers/Proxmox/views/reinstall.blade.php index 30afbe77c..6750d7f28 100644 --- a/extensions/Servers/Proxmox/views/reinstall.blade.php +++ b/extensions/Servers/Proxmox/views/reinstall.blade.php @@ -122,7 +122,7 @@