From 0ed29d782e4cd7f0e7f715f5f462d8c3e13a88d0 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Thu, 18 Dec 2025 09:40:58 -0500 Subject: [PATCH] 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. --- .../Servers/Proxmox/Livewire/Component.php | 9 +- .../Servers/Proxmox/Livewire/Firewall.php | 389 +- .../Servers/Proxmox/Livewire/Overview.php | 85 +- .../Servers/Proxmox/Livewire/Server.php | 10 +- .../Servers/Proxmox/Livewire/Statistics.php | 39 +- .../Servers/Proxmox/views/backups.blade.php | 191 +- .../Proxmox/views/backups/create.blade.php | 14 +- .../Proxmox/views/boot-order.blade.php | 188 +- .../Proxmox/views/cloud-init.blade.php | 264 +- .../Proxmox/views/components/layout.blade.php | 521 ++- .../Servers/Proxmox/views/display.blade.php | 68 +- .../Servers/Proxmox/views/firewall.blade.php | 3542 +++++++++++------ .../Servers/Proxmox/views/help.blade.php | 972 +++-- .../Servers/Proxmox/views/index.blade.php | 40 +- .../Servers/Proxmox/views/network.blade.php | 195 +- .../Servers/Proxmox/views/overview.blade.php | 2829 ++++++++++--- .../Servers/Proxmox/views/reinstall.blade.php | 66 +- .../Servers/Proxmox/views/server.blade.php | 194 +- .../Proxmox/views/statistics.blade.php | 498 ++- .../Servers/Proxmox/views/storage.blade.php | 218 +- .../Servers/Proxmox/views/subdomain.blade.php | 103 +- .../Servers/Proxmox/views/system.blade.php | 112 +- 22 files changed, 6734 insertions(+), 3813 deletions(-) diff --git a/extensions/Servers/Proxmox/Livewire/Component.php b/extensions/Servers/Proxmox/Livewire/Component.php index 0c7875dbc..32997a9c1 100644 --- a/extensions/Servers/Proxmox/Livewire/Component.php +++ b/extensions/Servers/Proxmox/Livewire/Component.php @@ -34,14 +34,7 @@ class Component extends BaseComponent abort(403); } - try { - $this->loadVmStatus(); - } catch (\Throwable $e) { - \Log::error('Failed to load VM status for server ' . $server->id, [ - 'error' => $e->getMessage(), - ]); - $this->status = null; - } + $this->loadVmStatus(); } /** diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index b9d291e8b..02de467d5 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -346,12 +346,11 @@ class Firewall extends Component $headers['Pragma'] = 'no-cache'; } - // Proxmox API requires form-encoded data for POST/PUT requests $request = Http::withHeaders($headers)->withoutVerifying(); $response = match($method) { - 'post' => $request->asForm()->post($fullUrl, $data), - 'put' => $request->asForm()->put($fullUrl, $data), + 'post' => $request->post($fullUrl, $data), + 'put' => $request->put($fullUrl, $data), 'delete' => $request->delete($fullUrl), default => $request->get($fullUrl, $data), }; @@ -410,23 +409,21 @@ class Firewall extends Component public function toggleAdapterFirewall(string $adapter): void { - $currentState = $this->adapterFirewallStates[$adapter] ?? false; - $newState = !$currentState; - - // Get the full network configuration string - $configString = $this->adapterConfigs[$adapter] ?? ''; - - if (empty($configString)) { - $this->error = 'Network adapter configuration not found'; - $this->showErrorModal = true; - return; - } - - // Optimistic UI update - update state immediately - $this->adapterFirewallStates[$adapter] = $newState; - $this->skipRender(); + $this->loading = true; try { + $currentState = $this->adapterFirewallStates[$adapter] ?? false; + $newState = !$currentState; + + // Get the full network configuration string + $configString = $this->adapterConfigs[$adapter] ?? ''; + + if (empty($configString)) { + $this->error = 'Network adapter configuration not found'; + $this->showErrorModal = true; + return; + } + // Parse the configuration string and modify the firewall parameter $parts = explode(',', $configString); $firewallFound = false; @@ -459,21 +456,18 @@ class Firewall extends Component ); if ($response->successful()) { + $this->adapterFirewallStates[$adapter] = $newState; $this->adapterConfigs[$adapter] = $newConfigString; // Update stored config - $this->dispatch('notify', 'Adapter firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); + $this->notify('Adapter firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { - // Revert on failure - $this->adapterFirewallStates[$adapter] = $currentState; - $this->error = 'Failed to toggle adapter firewall: ' . $response->body(); $this->showErrorModal = true; } } catch (\Exception $e) { - // Revert on failure - $this->adapterFirewallStates[$adapter] = $currentState; - $this->error = 'Failed to toggle adapter firewall: ' . $e->getMessage(); $this->showErrorModal = true; + } finally { + $this->loading = false; } } @@ -499,13 +493,11 @@ class Firewall extends Component public function toggleFirewall(): void { - $newState = !$this->firewallEnabled; - - // Optimistic UI update - update state immediately - $this->firewallEnabled = $newState; - $this->skipRender(); + $this->loading = true; try { + $newState = !$this->firewallEnabled; + // Always force DHCP disabled and NDP enabled $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', @@ -518,11 +510,9 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'Firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); + $this->firewallEnabled = $newState; + $this->notify('Firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { - // Revert on failure - $this->firewallEnabled = !$newState; - // Show detailed error message from API $errorBody = $response->json(); $errorMsg = $errorBody['errors'] ?? $response->body(); @@ -530,11 +520,10 @@ class Firewall extends Component $this->showErrorModal = true; } } catch (\Exception $e) { - // Revert on failure - $this->firewallEnabled = !$newState; - $this->error = 'Failed to toggle firewall: ' . $e->getMessage(); $this->showErrorModal = true; + } finally { + $this->loading = false; } } @@ -555,7 +544,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'Firewall policies updated successfully', 'success'); + $this->notify('Firewall policies updated successfully', 'success'); $this->loadFirewallOptions(); } else { $this->error = 'Failed to update firewall policies'; @@ -597,61 +586,51 @@ class Firewall extends Component return $this->macroDefinitions[$macro] ?? null; } - // ==================== FIREWALL RULES ==================== - - /** - * Load all firewall rules from Proxmox API - */ protected function loadFirewallRules(): void { try { - $url = '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules'; - $response = $this->firewallRequest($url); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules' + ); if ($response->successful()) { - $data = $response->json()['data'] ?? []; - // Sort rules by position - usort($data, fn($a, $b) => ($a['pos'] ?? 0) <=> ($b['pos'] ?? 0)); - $this->rules = $data; - - Log::debug('Loaded firewall rules', ['count' => count($this->rules)]); - } else { - Log::error('Failed to load firewall rules', [ - 'status' => $response->status(), - 'body' => $response->body() - ]); + $this->rules = $response->json()['data'] ?? []; } } catch (\Exception $e) { - Log::error('Exception loading firewall rules: ' . $e->getMessage()); - $this->rules = []; + Log::error('Failed to load firewall rules: ' . $e->getMessage()); } } - /** - * Open the form to create a new rule - */ public function startCreateRule(): void { $this->resetRuleForm(); + + // Set default destination to first IP + if (!empty($this->availableIps)) { + $this->ruleDest = $this->availableIps[0]['ip']; + } + + // Set default interface to first adapter + if (!empty($this->networkAdapters)) { + $this->ruleIface = $this->networkAdapters[0]['name']; + } + $this->editingRuleIndex = null; $this->showRuleForm = true; } - /** - * Open the form to edit an existing rule - */ public function startEditRule(int $index): void { if (!isset($this->rules[$index])) { - $this->error = 'Rule not found at index ' . $index; + $this->error = 'Rule not found'; $this->showErrorModal = true; return; } $rule = $this->rules[$index]; $this->editingRuleIndex = $index; - - // Populate form with existing rule data + $this->showRuleForm = true; + $this->ruleAction = $rule['action'] ?? 'ACCEPT'; $this->ruleType = $rule['type'] ?? 'in'; $this->ruleSource = $rule['source'] ?? ''; @@ -660,196 +639,126 @@ class Firewall extends Component $this->ruleDport = $rule['dport'] ?? ''; $this->ruleSport = $rule['sport'] ?? ''; $this->ruleComment = $rule['comment'] ?? ''; - $this->ruleEnable = !isset($rule['enable']) || (int)$rule['enable'] !== 0; + $this->ruleEnable = !isset($rule['enable']) || $rule['enable'] != 0; $this->ruleMacro = $rule['macro'] ?? ''; $this->ruleIface = $rule['iface'] ?? ''; $this->ruleLog = $rule['log'] ?? 'nolog'; - - $this->showRuleForm = true; } - /** - * Create or update a firewall rule via Proxmox API - */ public function saveRule(): void { $this->loading = true; try { - // Build the API endpoint - $baseUrl = '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules'; + $payload = [ + 'action' => $this->ruleAction, + 'type' => $this->ruleType, + 'enable' => $this->ruleEnable ? 1 : 0, + ]; + + // Note: ruleMacro is only used for UI convenience to auto-populate fields + // It is NOT sent to Proxmox API - only the actual proto/dport values are sent - // Build payload - Proxmox requires specific field names - $payload = []; - - // Required fields - $payload['action'] = strtoupper(trim($this->ruleAction)); - $payload['type'] = strtolower(trim($this->ruleType)); - - // Enable/disable (Proxmox uses 1/0) - $payload['enable'] = $this->ruleEnable ? '1' : '0'; - - // Optional fields - only add if not empty - if (!empty($this->ruleSource)) { - $payload['source'] = trim($this->ruleSource); - } - - if (!empty($this->ruleDest)) { - $payload['dest'] = trim($this->ruleDest); - } - - // Protocol must be lowercase - if (!empty($this->ruleProto)) { - $payload['proto'] = strtolower(trim($this->ruleProto)); - } - - if (!empty($this->ruleDport)) { - $payload['dport'] = trim($this->ruleDport); - } - - if (!empty($this->ruleSport)) { - $payload['sport'] = trim($this->ruleSport); - } - - if (!empty($this->ruleComment)) { - $payload['comment'] = trim($this->ruleComment); - } - - if (!empty($this->ruleIface)) { + if (!empty(trim($this->ruleIface))) { $payload['iface'] = trim($this->ruleIface); } - if (!empty($this->ruleLog) && $this->ruleLog !== 'nolog') { - $payload['log'] = $this->ruleLog; + if (!empty(trim($this->ruleSource))) { + $payload['source'] = trim($this->ruleSource); } - // If macro is set, use it instead of manual proto/port - if (!empty($this->ruleMacro)) { - $payload['macro'] = trim($this->ruleMacro); - // Remove proto/dport/sport when using macro - unset($payload['proto'], $payload['dport'], $payload['sport']); + 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->ruleLog)) && $this->ruleLog !== 'nolog') { + $payload['log'] = $this->ruleLog; } - Log::info('Saving firewall rule', [ - 'editing' => $this->editingRuleIndex !== null, - 'payload' => $payload - ]); - - // Determine if creating or updating if ($this->editingRuleIndex !== null && isset($this->rules[$this->editingRuleIndex])) { - // UPDATE existing rule $pos = $this->rules[$this->editingRuleIndex]['pos']; - $url = $baseUrl . '/' . $pos; - - Log::info('Updating rule at position ' . $pos, ['url' => $url]); - $response = $this->firewallRequest($url, 'put', $payload); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, + 'put', + $payload + ); } else { - // CREATE new rule - Log::info('Creating new rule', ['url' => $baseUrl]); - $response = $this->firewallRequest($baseUrl, 'post', $payload); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules', + 'post', + $payload + ); } - Log::info('API Response', [ - 'status' => $response->status(), - 'successful' => $response->successful(), - 'body' => $response->body() - ]); - if ($response->successful()) { - $action = $this->editingRuleIndex !== null ? 'updated' : 'created'; - $this->dispatch('notify', "Firewall rule {$action} successfully", 'success'); - - // Reset form and close modal + $this->notify('Rule ' . ($this->editingRuleIndex !== null ? 'updated' : 'created') . ' successfully', 'success'); $this->resetRuleForm(); $this->editingRuleIndex = null; $this->showRuleForm = false; - - // Reload rules from Proxmox $this->loadFirewallRules(); } else { - // Parse error from response - $responseData = $response->json(); - $errorMsg = 'Unknown error'; - - if (isset($responseData['errors'])) { - $errorMsg = is_array($responseData['errors']) - ? json_encode($responseData['errors']) - : $responseData['errors']; - } elseif (isset($responseData['message'])) { - $errorMsg = $responseData['message']; - } elseif (isset($responseData['data'])) { - $errorMsg = is_string($responseData['data']) ? $responseData['data'] : json_encode($responseData['data']); - } else { - $errorMsg = $response->body(); - } - - Log::error('Failed to save firewall rule', [ - 'status' => $response->status(), - 'error' => $errorMsg, - 'payload' => $payload - ]); - - $this->error = 'Failed to save rule: ' . $errorMsg; + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->error = 'Failed to save rule: ' . $message; $this->showErrorModal = true; } } catch (\Exception $e) { - Log::error('Exception in saveRule', [ - 'message' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - $this->error = 'Error: ' . $e->getMessage(); + $this->error = 'Failed to save rule: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } - /** - * Toggle a rule's enabled/disabled state - */ public function toggleRule(int $index): void { - if (!isset($this->rules[$index])) { - $this->error = 'Rule not found'; - $this->showErrorModal = true; - return; - } - try { + if (!isset($this->rules[$index])) { + return; + } + $rule = $this->rules[$index]; $pos = $rule['pos']; - $currentlyEnabled = isset($rule['enable']) ? (int)$rule['enable'] : 1; - $newState = $currentlyEnabled === 0 ? '1' : '0'; + $currentEnable = isset($rule['enable']) ? (int)$rule['enable'] : 1; + $newEnable = $currentEnable == 0 ? 1 : 0; - $url = '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos; - - $response = $this->firewallRequest($url, 'put', ['enable' => $newState]); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, + 'put', + ['enable' => $newEnable] + ); if ($response->successful()) { - $stateText = $newState === '1' ? 'enabled' : 'disabled'; - $this->dispatch('notify', "Rule {$stateText}", 'success'); + $this->notify('Rule ' . ($newEnable == 1 ? 'enabled' : 'disabled'), 'success'); $this->loadFirewallRules(); } else { - Log::error('Failed to toggle rule', ['response' => $response->body()]); $this->error = 'Failed to toggle rule'; $this->showErrorModal = true; } } catch (\Exception $e) { - Log::error('Exception toggling rule: ' . $e->getMessage()); - $this->error = 'Error: ' . $e->getMessage(); + $this->error = 'Failed to toggle rule: ' . $e->getMessage(); $this->showErrorModal = true; } } - /** - * Show confirmation modal for deleting a rule - */ public function confirmDeleteRule(int $index): void { if (!isset($this->rules[$index])) { - $this->error = 'Rule not found'; - $this->showErrorModal = true; return; } @@ -857,40 +766,32 @@ class Firewall extends Component $this->showDeleteRuleModal = true; } - /** - * Delete a firewall rule - */ public function deleteRule(): void { if ($this->deletingRuleIndex === null || !isset($this->rules[$this->deletingRuleIndex])) { - $this->showDeleteRuleModal = false; return; } + $rule = $this->rules[$this->deletingRuleIndex]; + $pos = $rule['pos']; $this->loading = true; $this->showDeleteRuleModal = false; try { - $rule = $this->rules[$this->deletingRuleIndex]; - $pos = $rule['pos']; - - $url = '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos; - - Log::info('Deleting firewall rule', ['url' => $url, 'pos' => $pos]); - - $response = $this->firewallRequest($url, 'delete'); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, + 'delete' + ); if ($response->successful()) { - $this->dispatch('notify', 'Rule deleted successfully', 'success'); + $this->notify('Rule deleted successfully', 'success'); $this->loadFirewallRules(); } else { - Log::error('Failed to delete rule', ['response' => $response->body()]); - $this->error = 'Failed to delete rule: ' . $response->body(); + $this->error = 'Failed to delete rule'; $this->showErrorModal = true; } } catch (\Exception $e) { - Log::error('Exception deleting rule: ' . $e->getMessage()); - $this->error = 'Error: ' . $e->getMessage(); + $this->error = 'Failed to delete rule: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; @@ -898,50 +799,41 @@ class Firewall extends Component } } - /** - * Cancel delete confirmation - */ public function cancelDeleteRule(): void { $this->deletingRuleIndex = null; $this->showDeleteRuleModal = false; } - /** - * Reorder rules by moving one rule to another position - */ public function reorderRules(int $fromPos, int $toPos): void { try { - $url = '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $fromPos; - - $response = $this->firewallRequest($url, 'put', ['moveto' => (string)$toPos]); + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $fromPos, + 'put', + ['moveto' => $toPos] + ); if ($response->successful()) { - $this->dispatch('notify', 'Rules reordered', 'success'); + $this->notify('Rules reordered successfully', 'success'); $this->loadFirewallRules(); } else { - Log::error('Failed to reorder rules', ['response' => $response->body()]); $this->error = 'Failed to reorder rules'; $this->showErrorModal = true; } } catch (\Exception $e) { - Log::error('Exception reordering rules: ' . $e->getMessage()); - $this->error = 'Error: ' . $e->getMessage(); + $this->error = 'Failed to reorder rules: ' . $e->getMessage(); $this->showErrorModal = true; } } - /** - * Reset the rule form to default values - */ protected function resetRuleForm(): void { $this->ruleAction = 'ACCEPT'; $this->ruleType = 'in'; $this->ruleSource = ''; $this->ruleDest = ''; - $this->ruleProto = 'tcp'; + $this->ruleProto = ''; $this->ruleDport = ''; $this->ruleSport = ''; $this->ruleComment = ''; @@ -951,9 +843,6 @@ class Firewall extends Component $this->ruleLog = 'nolog'; } - /** - * Cancel and close the rule form - */ public function cancelRuleForm(): void { $this->resetRuleForm(); @@ -1032,7 +921,7 @@ class Firewall extends Component } if ($response->successful()) { - $this->dispatch('notify', 'Alias ' . ($this->editingAliasName ? 'updated' : 'created') . ' successfully', 'success'); + $this->notify('Alias ' . ($this->editingAliasName ? 'updated' : 'created') . ' successfully', 'success'); $this->resetAliasForm(); $this->editingAliasName = null; $this->showAliasForm = false; @@ -1072,7 +961,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'Alias deleted successfully', 'success'); + $this->notify('Alias deleted successfully', 'success'); $this->loadAliases(); } else { $this->error = 'Failed to delete alias'; @@ -1155,7 +1044,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'IPSet created successfully', 'success'); + $this->notify('IPSet created successfully', 'success'); $this->resetIPSetForm(); $this->showIPSetForm = false; $this->loadIPSets(); @@ -1194,7 +1083,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'IPSet deleted successfully', 'success'); + $this->notify('IPSet deleted successfully', 'success'); $this->loadIPSets(); // Clear selected IPSet if it was deleted @@ -1297,7 +1186,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'IP/CIDR added to IPSet successfully', 'success'); + $this->notify('IP/CIDR added to IPSet successfully', 'success'); $this->memberCidr = ''; $this->memberComment = ''; $this->memberNomatch = false; @@ -1341,7 +1230,7 @@ class Firewall extends Component ); if ($response->successful()) { - $this->dispatch('notify', 'IP/CIDR removed from IPSet successfully', 'success'); + $this->notify('IP/CIDR removed from IPSet successfully', 'success'); $this->loadIPSetMembers($this->selectedIPSetName); } else { $this->error = 'Failed to remove member'; @@ -1469,7 +1358,7 @@ class Firewall extends Component $this->lastLogRefresh = now()->format('Y-m-d H:i:s'); // Notify user of successful refresh - $this->dispatch('notify', 'Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success'); + $this->notify('Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success'); Log::info('Firewall log parsing summary', [ 'total_raw_logs' => count($allRawLogs), @@ -2034,7 +1923,7 @@ class Firewall extends Component 'outgoing_policy' => $configData['policyOut'], ]); - $this->dispatch('notify', 'Configuration saved successfully', 'success'); + $this->notify('Configuration saved successfully', 'success'); $this->closeSaveConfigModal(); $this->loadSavedConfigurations(); } catch (\Exception $e) { @@ -2315,9 +2204,9 @@ class Firewall extends Component $message = 'Configuration applied successfully'; if ($incompatibleRulesDisabled > 0) { $message .= ". {$incompatibleRulesDisabled} rule(s) were disabled due to incompatibility with this VPS."; - $this->dispatch('notify', $message, 'warning'); + $this->notify($message, 'warning'); } else { - $this->dispatch('notify', $message, 'success'); + $this->notify($message, 'success'); } $this->closeApplyConfigModal(); @@ -2377,7 +2266,7 @@ class Firewall extends Component $config->delete(); - $this->dispatch('notify', 'Configuration deleted successfully', 'success'); + $this->notify('Configuration deleted successfully', 'success'); $this->cancelDeleteConfiguration(); $this->loadSavedConfigurations(); } catch (\Exception $e) { @@ -2456,7 +2345,7 @@ class Firewall extends Component 'configuration_name' => trim($this->renameConfigName), ]); - $this->dispatch('notify', 'Configuration renamed successfully', 'success'); + $this->notify('Configuration renamed successfully', 'success'); $this->closeRenameConfigModal(); $this->loadSavedConfigurations(); } catch (\Exception $e) { @@ -2705,9 +2594,9 @@ class Firewall extends Component $totalCount = count($results); if ($successCount === $totalCount) { - $this->dispatch('notify', "Configuration applied to all {$successCount} servers successfully", 'success'); + $this->notify("Configuration applied to all {$successCount} servers successfully", 'success'); } elseif ($successCount > 0) { - $this->dispatch('notify', "Configuration applied to {$successCount} of {$totalCount} servers", 'warning'); + $this->notify("Configuration applied to {$successCount} of {$totalCount} servers", 'warning'); } else { $this->error = 'Failed to apply configuration to any servers'; $this->showErrorModal = true; @@ -2797,4 +2686,4 @@ class Firewall extends Component 'sidebar' => true, ]); } -} +} \ No newline at end of file diff --git a/extensions/Servers/Proxmox/Livewire/Overview.php b/extensions/Servers/Proxmox/Livewire/Overview.php index ff4514dfa..a31faf9ab 100644 --- a/extensions/Servers/Proxmox/Livewire/Overview.php +++ b/extensions/Servers/Proxmox/Livewire/Overview.php @@ -43,11 +43,7 @@ class Overview extends Component parent::mount($server); if (is_null($this->status)) { - try { - $this->loadVmStatus(); - } catch (\Throwable $e) { - \Log::error('Overview: Failed to load VM status', ['error' => $e->getMessage()]); - } + $this->loadVmStatus(); } $this->hostname = $server->hostname ?? ($this->status->name ?? ''); @@ -60,86 +56,13 @@ class Overview extends Component } catch (\Exception $e) { $this->notes = ''; } - - // Preload Statistics telemetry data in background to warm the cache - // This ensures when user navigates to Statistics page, data is already available - $this->preloadStatisticsData(); - } - - /** - * Preload statistics data to warm the cache for faster Statistics page loading. - * Runs as a non-blocking background operation using dispatch_async if available, - * otherwise falls back to synchronous cache warming. - */ - protected function preloadStatisticsData(): void - { - // Only preload if cache is empty (don't waste resources if already cached) - $graphTypes = ['cpu', 'memory', 'network', 'disk']; - $timeRange = 'hour'; - $aggregation = 'average'; - - foreach ($graphTypes as $graphType) { - $cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation; - - // Skip if already cached - if (Cache::has($cacheKey)) { - continue; - } - - // Warm the cache in background - try { - Cache::remember($cacheKey, 30, function () use ($timeRange, $aggregation) { - $proxmox = new Proxmox; - $stats = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/rrddata', data: [ - 'timeframe' => $timeRange, - 'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE', - ]); - - $rawData = $stats->json()['data'] ?? []; - - // Limit data points to prevent memory issues (same logic as Statistics) - $maxPoints = 40; - if (count($rawData) > $maxPoints) { - $step = ceil(count($rawData) / $maxPoints); - $sampledData = []; - for ($i = 0; $i < count($rawData); $i += $step) { - $sampledData[] = $rawData[$i]; - } - if (end($rawData) !== end($sampledData)) { - $sampledData[] = end($rawData); - } - return $sampledData; - } - - return $rawData; - }); - } catch (\Throwable $e) { - // Silently fail - this is just a preload optimization - \Log::debug('Overview: Stats preload failed for ' . $graphType, ['error' => $e->getMessage()]); - } - } } public function checkStatus() { - if (!$this->status) { - try { - $this->loadVmStatus(); - } catch (\Throwable $e) { - \Log::error('Overview: checkStatus failed', ['error' => $e->getMessage()]); - } - return; - } - - $currentStatus = $this->status->status ?? null; - try { - $this->loadVmStatus(); - } catch (\Throwable $e) { - \Log::error('Overview: checkStatus loadVmStatus failed', ['error' => $e->getMessage()]); - return; - } - - if ($this->status && ($this->status->status ?? null) != $currentStatus) { + $currentStatus = $this->status->status; + $this->loadVmStatus(); + if ($this->status->status != $currentStatus) { $this->fastPolling = false; } } diff --git a/extensions/Servers/Proxmox/Livewire/Server.php b/extensions/Servers/Proxmox/Livewire/Server.php index 08606ad9e..e62ca7f63 100644 --- a/extensions/Servers/Proxmox/Livewire/Server.php +++ b/extensions/Servers/Proxmox/Livewire/Server.php @@ -13,14 +13,8 @@ class Server extends Component { parent::mount($server); - // Check and enforce bandwidth throttling if exceeded (wrapped in try-catch to prevent lazy load failures) - try { - $this->checkAndEnforceBandwidthLimit(); - } catch (\Throwable $e) { - \Log::warning('Failed to check bandwidth limit for server ' . $server->id, [ - 'error' => $e->getMessage(), - ]); - } + // Check and enforce bandwidth throttling if exceeded + $this->checkAndEnforceBandwidthLimit(); } public function getMonthlyTrafficProperty() diff --git a/extensions/Servers/Proxmox/Livewire/Statistics.php b/extensions/Servers/Proxmox/Livewire/Statistics.php index 6a84219a1..9de9d690a 100644 --- a/extensions/Servers/Proxmox/Livewire/Statistics.php +++ b/extensions/Servers/Proxmox/Livewire/Statistics.php @@ -152,25 +152,23 @@ class Statistics extends Component { parent::mount($server); $this->server->loadMissing('ipAddresses'); + // Network Analysis is loaded lazily via JavaScript to improve page load speed + // Initialize error state if not configured to prevent infinite loading + if (!$this->coreRouterEnabled()) { + $this->networkAnalysis['error'] = 'Core Router API is not configured.'; + $this->networkAnalysis['connections'] = []; + $this->networkAnalysis['topConnections'] = []; + $this->networkAnalysis['topPorts'] = []; + $this->networkAnalysis['security'] = []; + } - // Data loading is deferred to loadInitialData() which is triggered via wire:init - // This ensures JavaScript charts are fully initialized before data events are dispatched - } - - /** - * Load all initial data for the statistics page. - * Called via wire:init to ensure JavaScript is ready to receive the data. - */ - public function loadInitialData(): void - { - // Fetch graph data - each dispatch includes the actual data for JavaScript charts - $this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation); - $this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation); - $this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation); - $this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation); - - // Load Network Analysis - $this->loadNetworkAnalysis(); + // Preload graph data on mount to speed up initial display + // Dispatch individual chart events for more reliable parallel loading + // Each chart loads independently so a failure in one doesn't block others + $this->dispatch('proxmox:stats:cpu'); + $this->dispatch('proxmox:stats:memory'); + $this->dispatch('proxmox:stats:network'); + $this->dispatch('proxmox:stats:disk'); } #[On('proxmox:stats')] @@ -419,10 +417,7 @@ class Statistics extends Component $this->topPortsCurrentPage = 1; // Persist the computed analysis for a short period to speed up subsequent loads - // Cache duration is set to 25 seconds (slightly less than the 30s poll interval) - // This ensures fresh data is fetched on each poll cycle while still preventing - // duplicate requests within the same cycle - Cache::put($cacheKey, $analysis, now()->addSeconds(25)); + Cache::put($cacheKey, $analysis, now()->addSeconds(30)); // Reset retry count on successful load $this->networkAnalysisRetryCount = 0; diff --git a/extensions/Servers/Proxmox/views/backups.blade.php b/extensions/Servers/Proxmox/views/backups.blade.php index 143ce594b..8f3712062 100644 --- a/extensions/Servers/Proxmox/views/backups.blade.php +++ b/extensions/Servers/Proxmox/views/backups.blade.php @@ -1,88 +1,121 @@ -
-
-
-

Backups

-
- - -