, cpu: array, ram: array, netin: array, netout: array, available: bool} */ public function forHost(Host $host): array { return Cache::get($this->key($host), $this->empty()); } /** * Fetch the hour from Proxmox and put it where the console can read it. * * Runs ONLY where the tunnel is — see the class docblock. Failure is logged * rather than swallowed: an empty tile that cannot say whether the host is * silent or unreachable costs an afternoon to diagnose, and did. */ public function collect(Host $host): void { try { $rows = $this->pve->forHost($host)->nodeRrdData($host->node ?? 'pve', 'hour'); } catch (Throwable $e) { Log::warning('host load: could not read the RRD history', [ 'host' => $host->name, 'node' => $host->node, 'reason' => $e->getMessage(), ]); return; } if ($rows === []) { return; } Cache::put($this->key($host), $this->series($rows), self::TTL_SECONDS); } /** * The cache key carries the shape's version. * * An entry outlives a deploy, and one written before the network series has * no netin/netout — read as one of today's it kills the host page with an * undefined key for the length of the TTL, which is exactly the minute * somebody is looking to see whether the update went through. The key moves * with the shape; the old entry is never read again and expires on its own. */ private function key(Host $host): string { return 'host-load:v2:'.$host->id; } /** @return array{labels: array, cpu: array, ram: array, netin: array, netout: array, available: bool} */ private function empty(): array { return ['labels' => [], 'cpu' => [], 'ram' => [], 'netin' => [], 'netout' => [], 'available' => false]; } /** * @param array> $rows * @return array{labels: array, cpu: array, ram: array, netin: array, netout: array, available: bool} */ private function series(array $rows): array { $labels = []; $cpu = []; $ram = []; $netin = []; $netout = []; foreach ($rows as $row) { $labels[] = (int) ($row['time'] ?? 0); $cpu[] = $this->percent($row['cpu'] ?? null, 1); $ram[] = $this->percent($row['memused'] ?? null, $row['memtotal'] ?? null); $netin[] = $this->rate($row['netin'] ?? null); $netout[] = $this->rate($row['netout'] ?? null); } return [ 'labels' => $labels, 'cpu' => $cpu, 'ram' => $ram, 'netin' => $netin, 'netout' => $netout, 'available' => true, ]; } /** * A share of a whole, in percent — or null when either half is missing. * * Null rather than 0.0, and that is the whole care taken here: Proxmox * simply omits the fields for a sample it has no data for, and charting * that omission as zero draws a quiet hour the host never had. The same * distinction `instance_metrics` makes for its nullable columns, for the * same reason. */ private function percent(mixed $part, mixed $whole): ?float { if (! is_numeric($part) || ! is_numeric($whole) || (float) $whole <= 0.0) { return null; } return round((float) $part / (float) $whole * 100, 2); } /** Bytes per second as MiB/s — or null, for the same reason percent() returns null. */ private function rate(mixed $bytesPerSecond): ?float { if (! is_numeric($bytesPerSecond)) { return null; } return round((float) $bytesPerSecond / self::MIB, 2); } }