diff --git a/extensions/Servers/Proxmox/Livewire/Statistics.php b/extensions/Servers/Proxmox/Livewire/Statistics.php index 6e5ede5bb..7d1d99e2e 100644 --- a/extensions/Servers/Proxmox/Livewire/Statistics.php +++ b/extensions/Servers/Proxmox/Livewire/Statistics.php @@ -154,23 +154,45 @@ class Statistics extends Component { Log::debug('Network Analysis: loadNetworkAnalysis() called', [ 'current_loading_state' => $this->networkAnalysisLoading, - 'has_last_updated' => !empty($this->networkAnalysis['lastUpdated']), - 'has_error' => !empty($this->networkAnalysis['error']), + 'has_last_updated' => ! empty($this->networkAnalysis['lastUpdated']), + 'has_error' => ! empty($this->networkAnalysis['error']), ]); - - // Prevent duplicate loads - if ($this->networkAnalysisLoading) { - Log::debug('Network Analysis: Already loading, skipping'); + + // Use a short-lived cache to avoid repeatedly performing heavy analysis + $cacheKey = 'proxmox_network_analysis_' . $this->server->id; + + // If we already have a fairly recent result, reuse it immediately + $cachedAnalysis = Cache::get($cacheKey); + if (is_array($cachedAnalysis) && ! empty($cachedAnalysis['lastUpdated']) && empty($this->networkAnalysis['error'])) { + Log::debug('Network Analysis: served from cache', [ + 'cache_key' => $cacheKey, + 'last_updated' => $cachedAnalysis['lastUpdated'] ?? null, + ]); + + $this->networkAnalysis = $cachedAnalysis; + $this->networkAnalysisLoading = false; + + // Reset retry count when we successfully hydrate from cache + $this->networkAnalysisRetryCount = 0; + return; } - - if (!$this->coreRouterEnabled()) { + + // Prevent duplicate loads while a request is already in-flight + if ($this->networkAnalysisLoading) { + Log::debug('Network Analysis: Already loading, skipping new request'); + + return; + } + + if (! $this->coreRouterEnabled()) { $this->networkAnalysis['error'] = 'Core Router API is not configured.'; $this->networkAnalysis['connections'] = []; $this->networkAnalysis['topConnections'] = []; $this->networkAnalysis['topPorts'] = []; $this->networkAnalysis['security'] = []; $this->networkAnalysisLoading = false; + return; } @@ -182,6 +204,7 @@ class Statistics extends Component $this->networkAnalysis['topPorts'] = []; $this->networkAnalysis['security'] = []; $this->networkAnalysisLoading = false; + return; } @@ -190,20 +213,20 @@ class Statistics extends Component try { $client = app(CoreRouterClient::class); - + // Fetch all firewall states (no API-level filtering) $allStates = $client->listStates(); - + // Normalize server IPs for matching - $normalizedServerIps = array_map(function($ip) { + $normalizedServerIps = array_map(static function ($ip) { return strtolower(trim($ip)); }, $serverIps); - - // Sample a few states to debug extraction + + // Sample a few states to debug extraction (kept minimal for performance) $sampleStatesForDebug = []; foreach (array_slice($allStates, 0, 10) as $sampleState) { - $rawSrc = (string)($sampleState['source'] ?? ''); - $rawDst = (string)($sampleState['destination'] ?? ''); + $rawSrc = (string) ($sampleState['source'] ?? ''); + $rawDst = (string) ($sampleState['destination'] ?? ''); $sampleStatesForDebug[] = [ 'raw_source' => $rawSrc, 'raw_destination' => $rawDst, @@ -212,35 +235,35 @@ class Statistics extends Component 'protocol' => $sampleState['protocol'] ?? 'N/A', ]; } - + Log::debug('Core Router filtering states', [ 'total_states' => count($allStates), 'server_ips' => $serverIps, 'normalized_server_ips' => $normalizedServerIps, 'sample_states_extraction' => $sampleStatesForDebug, ]); - + // Filter states: keep only where source OR destination matches a server IP $filteredStates = []; $sampleChecks = []; foreach ($allStates as $state) { - $rawSource = (string)($state['source'] ?? ''); - $rawDestination = (string)($state['destination'] ?? ''); - + $rawSource = (string) ($state['source'] ?? ''); + $rawDestination = (string) ($state['destination'] ?? ''); + // Extract IP from "IP[port]" or "IP:port" format $sourceIp = $this->extractIpFromField($rawSource); $destinationIp = $this->extractIpFromField($rawDestination); - + $sourceNormalized = strtolower(trim($sourceIp)); $destNormalized = strtolower(trim($destinationIp)); - + $matches = false; $matchReason = ''; - + // Normalize raw fields for case-insensitive matching $rawSourceLower = strtolower($rawSource); $rawDestinationLower = strtolower($rawDestination); - + // Check exact match foreach ($normalizedServerIps as $serverIp) { if ($sourceNormalized === $serverIp) { @@ -265,10 +288,10 @@ class Statistics extends Component break; } } - + if ($matches) { $filteredStates[] = $state; - + // Log first few matches for debugging if (count($sampleChecks) < 5) { $sampleChecks[] = [ @@ -284,41 +307,10 @@ class Statistics extends Component } } } - - // Also search for specific target IPs in raw states for debugging (check ALL states) - $targetIp = '2a0b:4e07:8:1::2:10'; - $targetIpLower = strtolower($targetIp); - $targetIpv4 = '45.154.99.28'; - $targetIpv4Lower = strtolower($targetIpv4); - $statesWithTargetIp = []; - foreach ($allStates as $state) { - $rawSrc = (string)($state['source'] ?? ''); - $rawDst = (string)($state['destination'] ?? ''); - $rawSrcLower = strtolower($rawSrc); - $rawDstLower = strtolower($rawDst); - - // Check both IPv6 and IPv4 target IPs - if (stripos($rawSrcLower, $targetIpLower) !== false || - stripos($rawDstLower, $targetIpLower) !== false || - stripos($rawSrcLower, $targetIpv4Lower) !== false || - stripos($rawDstLower, $targetIpv4Lower) !== false) { - $statesWithTargetIp[] = [ - 'id' => $state['id'] ?? null, - 'source' => $rawSrc, - 'destination' => $rawDst, - 'protocol' => $state['protocol'] ?? 'N/A', - 'extracted_source' => $this->extractIpFromField($rawSrc), - 'extracted_destination' => $this->extractIpFromField($rawDst), - ]; - if (count($statesWithTargetIp) >= 20) break; // Increase limit for better debugging - } - } - + Log::debug('Core Router filtered states result', [ 'filtered_count' => count($filteredStates), 'sample_matches' => $sampleChecks, - 'target_ip' => $targetIp, - 'states_with_target_ip_in_raw' => $statesWithTargetIp, ]); // Format connections @@ -333,7 +325,7 @@ class Statistics extends Component 'sample_connections' => array_slice($connections, 0, 3), ]); - $this->networkAnalysis = [ + $analysis = [ 'connections' => array_slice($connections, 0, 100), 'topConnections' => array_slice($topConnections, 0, 10), 'topPorts' => array_slice($topPorts, 0, 10), @@ -341,33 +333,41 @@ class Statistics extends Component 'lastUpdated' => now()->toDateTimeString(), 'error' => null, ]; - + + $this->networkAnalysis = $analysis; + + // Persist the computed analysis for a short period to speed up subsequent loads + Cache::put($cacheKey, $analysis, now()->addSeconds(30)); + // Reset retry count on successful load $this->networkAnalysisRetryCount = 0; } catch (\Throwable $e) { report($e); - + $errorMessage = $e->getMessage(); $is502Error = str_contains($errorMessage, 'HTTP 502') || str_contains($errorMessage, '(HTTP 502)'); - + // Always initialize error arrays first $this->networkAnalysis['connections'] = []; $this->networkAnalysis['topConnections'] = []; $this->networkAnalysis['topPorts'] = []; $this->networkAnalysis['security'] = []; - + + // On failure, do not leave any stale cache behind for this server + Cache::forget($cacheKey); + // Check if it's a 502 error and we haven't exceeded max retries if ($is502Error && $this->networkAnalysisRetryCount < $this->networkAnalysisMaxRetries) { $this->networkAnalysisRetryCount++; - + Log::warning('Core Router API returned 502 error, will retry', [ 'retry_count' => $this->networkAnalysisRetryCount, 'max_retries' => $this->networkAnalysisMaxRetries, ]); - + // Set error message indicating retry $this->networkAnalysis['error'] = 'Temporary server error (HTTP 502). Retrying... (Attempt ' . $this->networkAnalysisRetryCount . '/' . $this->networkAnalysisMaxRetries . ')'; - + // Dispatch event to retry after a delay $this->dispatch('network-analysis-502-retry', [ 'retryCount' => $this->networkAnalysisRetryCount, @@ -378,7 +378,7 @@ class Statistics extends Component $this->networkAnalysis['error'] = 'Unable to load network analysis: ' . $errorMessage; } } finally { - // Always clear loading state in finally block + // Always clear loading state in finally block so UI can recover $this->networkAnalysisLoading = false; } } @@ -388,9 +388,10 @@ class Statistics extends Component */ public function retryNetworkAnalysis(): void { - // Reset loading state before retry + // Simple helper that can be invoked from Livewire/JS if needed. + // We intentionally avoid blocking sleep here; the browser-side + // retry logic already introduces a delay between attempts. $this->networkAnalysisLoading = false; - sleep(2); // Wait 2 seconds before retry $this->loadNetworkAnalysis(); }