status)) { $this->loadVmStatus(); } $this->hostname = $server->hostname ?? ($this->status->name ?? ''); // Load notes from Proxmox VM description try { $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); $config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $this->notes = $config['description'] ?? ''; } catch (\Exception $e) { $this->notes = ''; } } public function checkStatus() { $currentStatus = $this->status?->status; $this->loadVmStatus(); if ($this->status?->status != $currentStatus) { $this->fastPolling = false; } } #[On('proxmox:stats')] public function stats() { // Group graph types by their (timeRange, aggregation) to batch API calls. // The Proxmox /rrddata endpoint returns ALL metrics in a single response, // so we can serve multiple graph types from one API call when they share parameters. $groups = []; $graphConfigs = [ 'cpu' => [$this->cpuTimeRange, $this->cpuAggregation], 'memory' => [$this->memoryTimeRange, $this->memoryAggregation], 'network' => [$this->networkTimeRange, $this->networkAggregation], 'disk' => [$this->diskTimeRange, $this->diskAggregation], ]; foreach ($graphConfigs as $graphType => [$timeRange, $aggregation]) { $key = $timeRange . '|' . $aggregation; $groups[$key]['timeRange'] = $timeRange; $groups[$key]['aggregation'] = $aggregation; $groups[$key]['types'][] = $graphType; } foreach ($groups as $group) { $this->fetchGraphDataBatch($group['types'], $group['timeRange'], $group['aggregation']); } } /** * Fetch rrddata once and dispatch to multiple graph types that share the same parameters. */ protected function fetchGraphDataBatch(array $graphTypes, string $timeRange, string $aggregation): void { $allowedRanges = ['hour', 'day', 'week', 'month', 'year']; $allowedAggs = ['average', 'maximum']; if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) { return; } // Use a shared cache key for the raw API data (not per-graph-type) $cacheKey = 'proxmox-stats-' . $this->server->id . '-rrd-' . $timeRange . '-' . $aggregation; $data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) { $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); $stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [ 'timeframe' => $timeRange, 'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE', ]); return $stats->json()['data'] ?? []; }); foreach ($graphTypes as $graphType) { // Also store per-graph-type cache keys for individual update/invalidation $perGraphKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation; Cache::put($perGraphKey, $data, 60); $this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data); } } protected function fetchGraphData(string $graphType, string $timeRange, string $aggregation) { $allowedRanges = ['hour', 'day', 'week', 'month', 'year']; $allowedAggs = ['average', 'maximum']; if (!in_array($timeRange, $allowedRanges, true) || !in_array($aggregation, $allowedAggs, true)) { return; } $cacheKey = 'proxmox-stats-' . $this->server->id . '-' . $graphType . '-' . $timeRange . '-' . $aggregation; $data = Cache::remember($cacheKey, 60, function () use ($timeRange, $aggregation) { $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); $stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [ 'timeframe' => $timeRange, 'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE', ]); return $stats->json()['data'] ?? []; }); $this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data); } public function updatedCpuTimeRange() { Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation); $this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation); } public function updatedCpuAggregation() { Cache::forget('proxmox-stats-' . $this->server->id . '-cpu-' . $this->cpuTimeRange . '-' . $this->cpuAggregation); $this->fetchGraphData('cpu', $this->cpuTimeRange, $this->cpuAggregation); } public function updatedMemoryTimeRange() { Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation); $this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation); } public function updatedMemoryAggregation() { Cache::forget('proxmox-stats-' . $this->server->id . '-memory-' . $this->memoryTimeRange . '-' . $this->memoryAggregation); $this->fetchGraphData('memory', $this->memoryTimeRange, $this->memoryAggregation); } public function updatedNetworkTimeRange() { Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation); $this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation); } public function updatedNetworkAggregation() { Cache::forget('proxmox-stats-' . $this->server->id . '-network-' . $this->networkTimeRange . '-' . $this->networkAggregation); $this->fetchGraphData('network', $this->networkTimeRange, $this->networkAggregation); } public function updatedDiskTimeRange() { Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation); $this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation); } public function updatedDiskAggregation() { Cache::forget('proxmox-stats-' . $this->server->id . '-disk-' . $this->diskTimeRange . '-' . $this->diskAggregation); $this->fetchGraphData('disk', $this->diskTimeRange, $this->diskAggregation); } protected function formatSeconds(?int $seconds): string { if (is_null($seconds) || $seconds < 0) { return 'N/A'; } if ($seconds < 60) { return $seconds . 's'; } if ($seconds < 3600) { $minutes = floor($seconds / 60); $remaining = $seconds % 60; return sprintf('%dm %ds', $minutes, $remaining); } if ($seconds < 86400) { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); return sprintf('%dh %dm', $hours, $minutes); } $days = floor($seconds / 86400); $hours = floor(($seconds % 86400) / 3600); return sprintf('%dd %dh', $days, $hours); } protected function getTaskEventCounts(): array { $counts = [ 'starts' => 0, 'shutdowns' => 0, 'reboots' => 0, ]; try { $proxmox = new Proxmox; $response = $proxmox->request( '/nodes/' . $this->server->node->name . '/tasks', 'get', [ 'vmid' => $this->server->vm_id, 'limit' => 200, ] ); $tasks = $response->json()['data'] ?? []; foreach ($tasks as $task) { $type = strtolower($task['type'] ?? ''); if (in_array($type, ['qmstart', 'qmresume'], true)) { $counts['starts']++; } elseif (in_array($type, ['qmstop', 'qmshutdown'], true)) { $counts['shutdowns']++; } elseif (in_array($type, ['qmreboot', 'qmreset'], true)) { $counts['reboots']++; } } } catch (\Throwable $e) { // If task fetch fails we simply return zeros } return $counts; } public function getPowerEventStatsProperty(): array { // Compute uptime percentage from DB statistics $stats = ServerStatistics::where('server_id', $this->server->id) ->orderBy('DateTime') ->limit(500) ->get(['vmstatus']); $samples = $stats->count(); $runningSamples = 0; foreach ($stats as $stat) { if (strtolower($stat->vmstatus ?? '') === 'running') { $runningSamples++; } } $uptimePercent = 0; if ($samples > 0) { $uptimePercent = round(($runningSamples / $samples) * 100, 1); } elseif ($this->status) { $uptimePercent = strtolower($this->status->status ?? '') === 'running' ? 100 : 0; } // Get start/shutdown/reboot counts from Proxmox API (authoritative source) $taskCounts = $this->getTaskEventCounts(); return [ 'starts' => $taskCounts['starts'], 'shutdowns' => $taskCounts['shutdowns'], 'reboots' => $taskCounts['reboots'], 'samples' => $samples, 'running_samples' => $runningSamples, 'uptime_percent' => $uptimePercent, ]; } public function getNetworkRateStatsProperty(): array { $stats = ServerStatistics::where('server_id', $this->server->id) ->orderByDesc('DateTime') ->take(2) ->get(['DateTime', 'netin', 'netout']); if ($stats->count() < 2) { return ['in_rate' => 0, 'out_rate' => 0]; } $current = $stats->first(); $previous = $stats->last(); $currentTime = $current->DateTime ? Carbon::parse($current->DateTime) : null; $previousTime = $previous->DateTime ? Carbon::parse($previous->DateTime) : null; if (!$currentTime || !$previousTime) { return ['in_rate' => 0, 'out_rate' => 0]; } $seconds = max($currentTime->diffInSeconds($previousTime), 1); $inDelta = max(0, ($current->netin ?? 0) - ($previous->netin ?? 0)); $outDelta = max(0, ($current->netout ?? 0) - ($previous->netout ?? 0)); return [ 'in_rate' => $inDelta / $seconds, 'out_rate' => $outDelta / $seconds, ]; } public function startEditingHostname() { $this->isEditingHostname = true; $this->hostname = $this->server->hostname ?? $this->status->name ?? ''; $this->hostnameError = ''; } public function cancelEditingHostname() { $this->isEditingHostname = false; $this->hostname = $this->server->hostname ?? $this->status->name ?? ''; $this->hostnameError = ''; } public function updateHostname() { // Validate hostname format (RFC-1035 compliant) $validator = Validator::make( ['hostname' => $this->hostname], ['hostname' => 'required|regex:/^(?!.*\.\.)[a-z0-9]([a-z0-9.-]{0,126}[a-z0-9])?$/'] ); if ($validator->fails()) { $this->hostnameError = 'Hostname must be RFC-1035 compliant (lowercase letters, numbers, hyphens, and dots only).'; return false; } try { // Update hostname in database $this->server->hostname = $this->hostname; $this->server->save(); // Refresh the server model to get latest data $this->server->refresh(); // Update service property for the order if ($this->server->service) { $this->server->service->properties()->updateOrCreate( ['key' => 'hostname'], [ 'value' => $this->hostname, 'name' => 'Hostname', ] ); } $this->notify('Hostname saved to database successfully', 'success'); $this->isEditingHostname = false; $this->hostnameError = ''; return true; } catch (\Exception $e) { $this->hostnameError = 'Failed to save hostname: ' . $e->getMessage(); $this->notify('Failed to save hostname', 'error'); return false; } } public function applyHostname() { // First, save the hostname if it hasn't been saved if ($this->server->hostname !== $this->hostname || !$this->server->hostname) { if (!$this->updateHostname()) { return; // Exit if save failed } } try { $proxmox = new Proxmox; // Update VM name - Proxmox uses the VM name as the hostname for cloud-init // Cloud-init will pick up the new VM name on the next boot $vmType = $this->server->node->vmType(); $response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [ $this->server->node->is_lxc ? 'hostname' : 'name' => $this->hostname, ]); // The request() method throws on error, so if we get here, it was successful $this->notify('Hostname applied to Proxmox successfully. The VM name has been updated. The new hostname will be configured on the next boot via cloud-init.', 'success'); $this->isEditingHostname = false; $this->hostnameError = ''; // Refresh status to get updated name $this->status = (object) ($proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/status/current')->json()['data'] ?? []); } catch (\Exception $e) { $this->hostnameError = 'Failed to apply hostname: ' . $e->getMessage(); $this->notify('Failed to apply hostname to Proxmox', 'error'); } } public function startEditingPassword() { $this->isEditingPassword = true; $this->password = ''; $this->password_confirmation = ''; $this->passwordError = ''; } public function cancelEditingPassword() { $this->isEditingPassword = false; $this->password = ''; $this->password_confirmation = ''; $this->passwordError = ''; } public function updatePassword() { // Validate password using same rules as registration $validator = Validator::make( [ 'password' => $this->password, 'password_confirmation' => $this->password_confirmation, ], [ 'password' => 'required|string|min:8|confirmed', ] ); if ($validator->fails()) { $errors = $validator->errors()->all(); $this->passwordError = implode(' ', $errors); return false; } try { // Update password in database $this->server->password = $this->password; $this->server->save(); // Refresh the server model to get latest data $this->server->refresh(); $this->notify('Password saved to database successfully', 'success'); $this->isEditingPassword = false; $this->password = ''; $this->password_confirmation = ''; $this->passwordError = ''; return true; } catch (\Exception $e) { $this->passwordError = 'Failed to save password: ' . $e->getMessage(); $this->notify('Failed to save password', 'error'); return false; } } public function applyPassword() { // Validate password first $validator = Validator::make( [ 'password' => $this->password, 'password_confirmation' => $this->password_confirmation, ], [ 'password' => 'required|string|min:8|confirmed', ] ); if ($validator->fails()) { $errors = $validator->errors()->all(); $this->passwordError = implode(' ', $errors); return; } try { // Update password in database first $this->server->password = $this->password; $this->server->save(); // Refresh the server model to get latest data $this->server->refresh(); // LXC containers don't support password change via config API if ($this->server->node->is_lxc) { $this->notify('Password saved to database. Note: LXC container passwords cannot be changed via API - use console access to change root password if needed.', 'warning'); $this->isEditingPassword = false; $this->password = ''; $this->password_confirmation = ''; $this->passwordError = ''; return; } // Apply password via Proxmox API using cloud-init (KVM only) $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); // For KVM: cipassword sets the password via cloud-init on next boot $response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [ 'cipassword' => $this->password, ]); // The request() method throws on error, so if we get here, it was successful $this->notify('Password applied to Proxmox successfully. The new password will be configured on the next boot via cloud-init. Please reboot your server for the changes to take effect.', 'success'); $this->isEditingPassword = false; $this->password = ''; $this->password_confirmation = ''; $this->passwordError = ''; } catch (\Exception $e) { $this->passwordError = 'Failed to apply password: ' . $e->getMessage(); $this->notify('Failed to apply password to Proxmox', 'error'); } } public function startEditingNotes() { $this->isEditingNotes = true; $this->notesError = ''; } public function cancelEditingNotes() { $this->isEditingNotes = false; $this->notesError = ''; // Reload notes from Proxmox try { $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); $config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $this->notes = $config['description'] ?? ''; } catch (\Exception $e) { $this->notes = ''; } } public function saveNotes() { try { $proxmox = new Proxmox; $vmType = $this->server->node->vmType(); // Save notes to Proxmox VM description (limit to 8KB to avoid bloating Proxmox config) $notes = mb_substr($this->notes ?? '', 0, 8192); $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/config', 'put', [ 'description' => $notes, ]); $this->notify('Notes saved successfully', 'success'); $this->isEditingNotes = false; $this->notesError = ''; } catch (\Exception $e) { $this->notesError = 'Failed to save notes: ' . $e->getMessage(); $this->notify('Failed to save notes', 'error'); } } public function render() { return view('proxmox::overview')->layoutData([ 'sidebar' => true, ]); } }