PL
<?phpnamespace App\View;use App\Gui\Icons;use App\Gui\Units;use App\Model\Cache;use App\Model\Connection;use App\Model\LogEntry;use App\Model\Session;use App\Xapi\Rrd;/** * The content of the tabs of XenCenter for the object selected: tabs($class) lists them, page() builds one. */final class Pages{    public function __construct(private Session $session)    {    }    /** @return list<string> the tabs of a kind of object */    public static function tabs(string $kind): array    {        return match ($kind) {            'root' => ['Dashboard', 'Home', 'Search'],          // Dashboard: src/Feature/Pools/Dashboard.php            'disconnected' => ['General'],            'pool' => ['General', 'Memory', 'Storage', 'Networking', 'Search', 'Updates', 'Logs'],            'host' => ['General', 'Memory', 'Storage', 'Networking', 'NICs', 'Performance', 'Search', 'Updates', 'Logs'],            'vm' => ['General', 'Memory', 'Storage', 'Networking', 'Console', 'Performance', 'Snapshots', 'Logs'],            'template' => ['General', 'Memory', 'Storage', 'Networking', 'Logs'],            'snapshot' => ['General', 'Storage', 'Networking'],            'sr' => ['General', 'Storage', 'Logs'],            'network' => ['General', 'Networking'],            'notifications' => ['Alerts', 'Events'],            'group' => ['Search'],            default => ['General'],        };    }    /** What kind of object a ref of a connection is: pool, host, vm, template, snapshot, sr, network. */    public static function kind(?Connection $c, string $ref): string    {        if (str_starts_with($ref, 'notif:')) {            return 'notifications';        }        if ($ref === TreeBuilder::ROOT || $c === null) {            return str_starts_with($ref, 'type:') ? 'group' : 'root';        }        if (!$c->isConnected()) {            return 'disconnected';        }        $class = $c->cache->classOf($ref);        if ($class === 'vm') {            $vm = $c->cache->get($ref) ?? [];            return !empty($vm['is_a_snapshot']) ? 'snapshot' : (!empty($vm['is_a_template']) ? 'template' : 'vm');        }        return $class ?? 'root';    }    public function page(?Connection $c, string $ref, string $tab, ?Rrd $rrd = null): Page    {        $kind = self::kind($c, $ref);        if ($kind === 'notifications') {            return $tab === 'Events' ? $this->events(null, null) : $this->alerts(null, null);        }        if ($kind === 'root' || $kind === 'group') {            return $tab === 'Search' ? $this->search(null, null, $rrd) : new Page($tab === 'Dashboard' && $kind === 'root' ? 'dashboard' : 'home');        }        if ($kind === 'disconnected' && $c !== null) {            return $this->disconnected($c);        }        /** @var Connection $c */        // + the buttons of storage, networks and disks (src/Feature/Storage)        return \App\Feature\Storage\Menus::page($c, $ref, $kind, $tab, match ($tab) {            'General' => $this->general($c, $ref, $kind),            'Memory' => $this->memory($c, $ref, $kind),            'Storage' => $this->storage($c, $ref, $kind),            'Networking' => $this->networking($c, $ref, $kind),            'NICs' => $this->nics($c, $ref),            'Snapshots' => $this->snapshots($c, $ref),            'Search' => $this->search($c, $kind === 'host' ? $ref : null, $rrd),            'Logs' => $this->logs($c, $ref, $kind),            'Console' => $this->console($c, $ref),            'Performance' => $this->performance($c, $ref, $kind, $rrd),            'Updates' => \App\Feature\Updates\UpdatesPage::page($c, $ref, $kind),            default => Page::text(''),        });    }    // general ---------------------------------------------------------------------------------------------------------------    private function disconnected(Connection $c): Page    {        $state = match ($c->state) {            Connection::CONNECTING => $c->phase ?: 'Connecting…',            Connection::FAILED => 'Failed: ' . $c->error,            default => 'Disconnected',        };        return Page::sheet([['title' => 'General', 'rows' => [            ['Server:', $c->profile->host], ['Connection:', \App\Model\ServerProfile::METHODS[$c->profile->method] ?? $c->profile->method],            ['User name:', $c->profile->user], ['State:', $state],        ]]], [['id' => 'server.connect', 'text' => 'Connect', 'enabled' => $c->state !== Connection::CONNECTING, 'style' => 'primary'],            ['id' => 'server.remove', 'text' => 'Remove from XenCenter']]);    }    private function general(Connection $c, string $ref, string $kind): Page    {        $cache = $c->cache;        $r = $cache->get($ref) ?? [];        $props = [['id' => 'object.properties', 'text' => 'Properties']];        switch ($kind) {            case 'pool':                $master = (string)$cache->master();                $sw = (array)$cache->field($master, 'software_version', []);                $cpus = 0;                foreach ($cache->hosts() as $h) {                    $cpus += (int)($h['cpu_info']['cpu_count'] ?? 0);                }                return Page::sheet([                    ['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)],                        ['Folder:', (string)($r['other_config']['folder'] ?? '') ?: '(none)'], ['Pool master:', $cache->name($master)], ['Number of servers:', (string)count($cache->hosts())],                        ['Number of VMs:', (string)count($cache->vms())], ['Default storage:', $cache->name((string)($r['default_SR'] ?? ''))], ['UUID:', (string)$r['uuid']]]],                    ['title' => 'Software', 'rows' => [['Product:', trim(($sw['product_brand'] ?? '') . ' ' . ($sw['product_version'] ?? ''))], ['Build:', (string)($sw['build_number'] ?? '')],                        ['XAPI:', (string)($sw['xapi'] ?? '')], ['Xen:', (string)($sw['xen'] ?? '')]]],                    ['title' => 'CPUs', 'rows' => [['Total physical CPUs:', (string)$cpus]]],                    ['title' => 'High Availability', 'rows' => [['HA:', !empty($r['ha_enabled']) ? 'Enabled' : 'Not configured']]],                ], $props);            case 'host':                $metrics = $cache->get((string)($r['metrics'] ?? '')) ?? [];                $sw = (array)($r['software_version'] ?? []);                $cpu = (array)($r['cpu_info'] ?? []);                $boot = (int)($r['other_config']['boot_time'] ?? 0);                $agent = (int)($r['other_config']['agent_start_time'] ?? 0);                $mgmt = [];                foreach ($cache->pifsOf($ref) as $pif) {                    if (!empty($pif['management'])) {                        $mgmt[] = $pif['IP'] . ' (' . $pif['device'] . ')';                    }                }                $isMaster = $ref === $cache->master();                return Page::sheet([                    ['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)],                        ['Pool master:', $isMaster ? 'Yes' : 'No'], ['Enabled:', ($r['enabled'] ?? true) ? 'Yes' : 'No (maintenance mode)'],                        ['iSCSI IQN:', (string)($r['iscsi_iqn'] ?? '')], ['Server uptime:', $boot > 0 ? Units::duration(time() - $boot) : ''],                        ['Toolstack uptime:', $agent > 0 ? Units::duration(time() - $agent) : ''], ['Live:', ($metrics['live'] ?? false) ? 'Yes' : 'No'],                        ['UUID:', (string)$r['uuid']]]],                    ['title' => 'Management Interfaces', 'rows' => [['Address:', (string)($r['address'] ?? '')], ['Hostname:', (string)($r['hostname'] ?? '')],                        ['Management interface:', implode("\n", $mgmt) ?: '(none)']]],                    ['title' => 'Memory', 'rows' => [['Server:', self::memoryLine($cache->hostMemory($ref))], ['Xen:', Units::bytes((float)($r['memory_overhead'] ?? 0))]]],                    ['title' => 'Version Details', 'rows' => [['Product:', trim(($sw['product_brand'] ?? '') . ' ' . ($sw['product_version'] ?? ''))],                        ['Build number:', (string)($sw['build_number'] ?? '')], ['Build date:', (string)($sw['date'] ?? '')], ['XAPI:', (string)($sw['xapi'] ?? '')],                        ['Xen:', (string)($sw['xen'] ?? '')], ['Kernel:', (string)($sw['linux'] ?? '')]]],                    ['title' => 'CPUs', 'rows' => [['Number of CPUs:', (string)($cpu['cpu_count'] ?? '') . ' (' . ($cpu['socket_count'] ?? '?') . ' sockets)'],                        ['Vendor:', (string)($cpu['vendor'] ?? '')], ['Model:', (string)($cpu['modelname'] ?? '')], ['Speed:', Units::mhz((string)($cpu['speed'] ?? '0'))]]],                    ['title' => 'BIOS', 'rows' => [['Vendor:', (string)($r['bios_strings']['system-manufacturer'] ?? '')], ['Model:', (string)($r['bios_strings']['system-product-name'] ?? '')],                        ['Serial number:', (string)($r['bios_strings']['system-serial-number'] ?? '')]]],                ], $props);            case 'vm':            case 'template':            case 'snapshot':                $metrics = $cache->vmMetrics($ref);                $guest = $cache->guestMetrics($ref);                $os = (string)($guest['os_version']['name'] ?? '');                $pv = (array)($guest['PV_drivers_version'] ?? []);                $start = Units::time((string)($metrics['start_time'] ?? ''));                $running = ($r['power_state'] ?? '') === 'Running';                $tools = $guest === [] ? 'Not installed' : (($guest['PV_drivers_up_to_date'] ?? false) ? 'Optimized (version ' . implode('.', array_slice(array_values($pv), 0, 3)) . ')' : 'Out of date');                $sections = [['title' => 'General', 'rows' => array_values(array_filter([                    ['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)],                    ['Operating System:', $os !== '' ? $os : ($kind === 'vm' ? 'Unknown' : '')],                    $kind === 'vm' ? ['Virtualization mode:', strtoupper((string)($r['domain_type'] ?? 'hvm'))] : null,                    $kind === 'vm' ? ['Power state:', (string)$r['power_state']] : null,                    $kind === 'vm' ? ['Home server:', ($a = (string)($r['affinity'] ?? '')) !== '' && $cache->get($a) !== null ? $cache->name($a) : '(none)'] : null,                    $kind === 'vm' && $running ? ['Running on:', $cache->name((string)$r['resident_on'])] : null,                    $kind === 'vm' && $running ? ['Time since startup:', $start > 0 ? Units::duration(time() - $start) : ''] : null,                    $kind === 'vm' ? ['Management agent:', $tools] : null,                    $kind === 'snapshot' ? ['Snapshot of:', $cache->name((string)$r['snapshot_of'])] : null,                    $kind === 'snapshot' ? ['Created:', Units::date((string)$r['snapshot_time'])] : null,                    $kind === 'vm' && $cache->ipsOf($ref) !== [] ? ['IP addresses:', implode("\n", $cache->ipsOf($ref))] : null,                    ['UUID:', (string)$r['uuid']],                ]))]];                $sections[] = ['title' => 'CPUs', 'rows' => [['vCPUs at startup:', (string)$r['VCPUs_at_startup']], ['Maximum vCPUs:', (string)$r['VCPUs_max']],                    ['Topology:', self::topology($r)]]];                $sections[] = ['title' => 'Boot Options', 'rows' => [['Boot order:', self::bootOrder((string)($r['HVM_boot_params']['order'] ?? 'cd'))],                    ['Firmware:', strtoupper((string)($r['HVM_boot_params']['firmware'] ?? 'bios'))], ['Secure Boot:', ($r['platform']['secureboot'] ?? 'false') === 'true' ? 'Enabled' : 'Disabled'],                    ['Auto-restart after crash:', ($r['actions_after_crash'] ?? '') === 'restart' ? 'Yes' : 'No']]];                if ($kind === 'vm') {                    $sections[] = ['title' => 'High Availability', 'rows' => [['Restart priority:', ($r['ha_restart_priority'] ?? '') === '' ? 'Do not restart' : (string)$r['ha_restart_priority']]]];                }                return Page::sheet($sections, $kind === 'snapshot' ? [] : $props);            case 'sr':                $pbds = [];                foreach ((array)($r['PBDs'] ?? []) as $pbd) {                    $p = $cache->get((string)$pbd) ?? [];                    $pbds[] = $cache->name((string)($p['host'] ?? '')) . ': ' . (!empty($p['currently_attached']) ? 'Connected' : 'Unplugged');                }                $size = (float)($r['physical_size'] ?? 0);                $used = (float)($r['physical_utilisation'] ?? 0);                return Page::sheet([                    ['title' => 'General', 'rows' => [['Name:', $cache->name($ref)], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)],                        ['Type:', self::srType((string)$r['type'])], ['Shared:', !empty($r['shared']) ? 'Yes' : 'No'], ['UUID:', (string)$r['uuid']]]],                    ['title' => 'Status', 'rows' => [['State:', 'OK'], ['Connected to:', implode("\n", $pbds) ?: '(none)']]],                    ['title' => 'Size', 'rows' => [['Usage:', ['bar' => $size > 0 ? $used / $size : 0, 'text' => Units::bytes($used) . ' used of ' . Units::bytes($size)]],                        ['Virtual allocation:', Units::bytes((float)($r['virtual_allocation'] ?? 0))]]],                ], [['id' => 'object.properties', 'text' => 'Properties'], ['id' => 'sr.rescan', 'text' => 'Rescan']]);            case 'network':                $hosts = [];                foreach ((array)($r['PIFs'] ?? []) as $pif) {                    $p = $cache->get((string)$pif) ?? [];                    $hosts[] = $cache->name((string)($p['host'] ?? '')) . ': ' . ($p['device'] ?? '') . ((int)($p['VLAN'] ?? -1) >= 0 ? ' (VLAN ' . $p['VLAN'] . ')' : '');                }                return Page::sheet([['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']],                    ['Bridge:', (string)($r['bridge'] ?? '')], ['MTU:', (string)($r['MTU'] ?? '')], ['NICs:', implode("\n", $hosts) ?: '(internal network)'],                    ['VIFs:', (string)count((array)($r['VIFs'] ?? []))], ['UUID:', (string)$r['uuid']]]]], $props);        }        return Page::text('');    }    // memory -------------------------------------------------------------------------------------------------------------    private function memory(Connection $c, string $ref, string $kind): Page    {        $cache = $c->cache;        if ($kind === 'pool') {            $sections = [];            foreach ($cache->hosts() as $host => $h) {                $sections[] = ['title' => (string)$h['name_label'], 'rows' => $this->hostMemoryRows($cache, $host)];            }            return Page::sheet($sections);        }        if ($kind === 'host') {            return Page::sheet([['title' => 'Server memory', 'rows' => $this->hostMemoryRows($cache, $ref)]]);        }        $r = $cache->get($ref) ?? [];        $metrics = $cache->vmMetrics($ref);        $actual = (float)($metrics['memory_actual'] ?? 0);        $max = (float)$r['memory_static_max'];        $running = in_array($r['power_state'] ?? '', ['Running', 'Paused'], true);        return Page::sheet([['title' => 'Memory', 'rows' => [            ['Current usage:', $running ? ['bar' => $max > 0 ? $actual / $max : 0, 'text' => Units::bytes($actual) . ' of ' . Units::bytes($max)] : 'Not running'],            ['Static maximum:', Units::bytes($max)], ['Dynamic minimum:', Units::bytes((float)$r['memory_dynamic_min'])],            ['Dynamic maximum:', Units::bytes((float)$r['memory_dynamic_max'])], ['Static minimum:', Units::bytes((float)$r['memory_static_min'])],            ['Dynamic Memory Control:', (float)$r['memory_dynamic_min'] < (float)$r['memory_dynamic_max'] ? 'Enabled' : 'Off (fixed memory)'],        ]]], $kind === 'snapshot' ? [] : [['id' => 'vm.memory', 'text' => 'Edit…', 'enabled' => ($r['power_state'] ?? '') === 'Halted']]);    }    /** @return list<array{0: string, 1: mixed}> */    private function hostMemoryRows(Cache $cache, string $host): array    {        $m = $cache->hostMemory($host);        $rows = [['Used:', ['bar' => $m['total'] > 0 ? $m['used'] / $m['total'] : 0, 'text' => self::memoryLine($m)]],            ['Free:', Units::bytes($m['free'])], ['Total:', Units::bytes($m['total'])]];        $vms = [];        foreach ($cache->all('vm') as $ref => $vm) {            if (($vm['resident_on'] ?? '') === $host) {                $vms[] = $vm['name_label'] . ': ' . Units::bytes((float)($cache->vmMetrics($ref)['memory_actual'] ?? $vm['memory_dynamic_max']));            }        }        sort($vms, SORT_NATURAL | SORT_FLAG_CASE);        $rows[] = ['VMs (' . count($vms) . '):', implode("\n", $vms) ?: '(none)'];        return $rows;    }    // storage -----------------------------------------------------------------------------------------------------------    private function storage(Connection $c, string $ref, string $kind): Page    {        $cache = $c->cache;        if ($kind === 'vm' || $kind === 'template' || $kind === 'snapshot') {            $rows = [];            foreach ($cache->disksOf($ref, true) as $d) {                $vbd = $d['vbd'];                $vdi = $d['vdi'];                $cd = ($vbd['type'] ?? '') === 'CD';                $rows[] = ['id' => $d['ref'], 'icon' => $cd ? 'sr-iso' : 'disk', 'cells' => [                    (string)$vbd['userdevice'], $cd ? ($vdi === null ? 'DVD drive (empty)' : (string)$vdi['name_label']) : (string)($vdi['name_label'] ?? ''),                    (string)($vdi['name_description'] ?? ''), $vdi === null ? '' : $cache->name((string)$vdi['SR']),                    $vdi === null ? '' : Units::bytes((float)$vdi['virtual_size']), ($vbd['mode'] ?? '') === 'RO' ? 'Yes' : 'No',                    !empty($vbd['currently_attached']) ? 'Yes' : 'No', '/dev/' . ($vbd['device'] ?? '')],                    'sort' => [str_pad((string)$vbd['userdevice'], 3, '0', STR_PAD_LEFT)]];            }            return Page::grid([['title' => 'Position', 'width' => 0.8], ['title' => 'Name', 'width' => 2.2], ['title' => 'Description', 'width' => 1.6], ['title' => 'SR', 'width' => 1.6],                ['title' => 'Size', 'width' => 1, 'align' => 'right'], ['title' => 'Read only', 'width' => 0.9], ['title' => 'Active', 'width' => 0.7], ['title' => 'Device path', 'width' => 1.1]],                $rows, [], 'This VM has no virtual disk.');        }        if ($kind === 'sr') {            $rows = [];            foreach ((array)$cache->field($ref, 'VDIs', []) as $vdiRef) {                $vdi = $cache->get((string)$vdiRef);                if ($vdi === null) {                    continue;                }                $vms = [];                foreach ((array)$vdi['VBDs'] as $vbd) {                    $vm = (string)$cache->field((string)$vbd, 'VM', '');                    if ($vm !== '') {                        $vms[] = $cache->name($vm);                    }                }                $rows[] = ['id' => (string)$vdiRef, 'icon' => 'disk', 'cells' => [(string)$vdi['name_label'], (string)$vdi['name_description'],                    Units::bytes((float)$vdi['virtual_size']), implode(', ', $vms)], 'sort' => [null, null, (float)$vdi['virtual_size']]];            }            return Page::grid([['title' => 'Name', 'width' => 2.5], ['title' => 'Description', 'width' => 2], ['title' => 'Size', 'width' => 1, 'align' => 'right'], ['title' => 'VM', 'width' => 2]],                $rows, [['id' => 'sr.rescan', 'text' => 'Rescan']], 'No virtual disk on this storage.');        }        $srs = $kind === 'host' ? $cache->srsOf($ref) : $cache->srs();        $rows = [];        foreach ($srs as $sr => $r) {            $size = (float)$r['physical_size'];            $used = (float)$r['physical_utilisation'];            $rows[] = ['id' => $sr, 'icon' => Icons::of('sr', $r), 'cells' => [$cache->name($sr), (string)$r['name_description'], self::srType((string)$r['type']),                !empty($r['shared']) ? 'Yes' : 'No', ['bar' => $size > 0 ? $used / $size : 0, 'text' => $size > 0 ? round(100 * $used / $size) . '% (' . Units::bytes($used) . ' used)' : ''],                Units::bytes($size), Units::bytes((float)$r['virtual_allocation'])], 'sort' => [null, null, null, null, null, $size, (float)$r['virtual_allocation']]];        }        return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'Description', 'width' => 2.4], ['title' => 'Type', 'width' => 1], ['title' => 'Shared', 'width' => 0.7],            ['title' => 'Usage', 'width' => 1.8], ['title' => 'Size', 'width' => 0.9, 'align' => 'right'], ['title' => 'Virtual allocation', 'width' => 1.2, 'align' => 'right']], $rows);    }    // networks ----------------------------------------------------------------------------------------------------------    private function networking(Connection $c, string $ref, string $kind): Page    {        $cache = $c->cache;        if ($kind === 'vm' || $kind === 'template' || $kind === 'snapshot') {            $ips = $cache->guestMetrics($ref)['networks'] ?? [];            $rows = [];            foreach ($cache->vifsOf($ref) as $v) {                $vif = $v['vif'];                $rows[] = ['id' => $v['ref'], 'icon' => 'network', 'cells' => [(string)$vif['device'], (string)$vif['MAC'], ($vif['qos_algorithm_params']['kbps'] ?? '') !== '' ? $vif['qos_algorithm_params']['kbps'] . ' kbit/s' : '',                    $cache->name((string)$vif['network']), (string)($ips[$vif['device'] . '/ip'] ?? $ips[$vif['device'] . '/ipv4/0'] ?? ''), !empty($vif['currently_attached']) ? 'Yes' : 'No']];            }            return Page::grid([['title' => 'Device', 'width' => 0.7], ['title' => 'MAC', 'width' => 1.5], ['title' => 'Limit', 'width' => 1], ['title' => 'Network', 'width' => 2.6],                ['title' => 'IP address', 'width' => 1.5], ['title' => 'Active', 'width' => 0.7]], $rows, [], 'This VM has no network interface.');        }        $networks = $kind === 'network' ? [$ref => $cache->get($ref) ?? []] : $cache->networks();        $rows = [];        foreach ($networks as $net => $n) {            $pif = null;            foreach ((array)($n['PIFs'] ?? []) as $p) {                $record = $cache->get((string)$p);                if ($record !== null && ($kind !== 'host' || $record['host'] === $ref)) {                    $pif = $record;                    break;                }            }            if ($kind === 'host' && $pif === null && !empty($n['PIFs'])) {                continue;            }            $carrier = $pif !== null ? (bool)($cache->get((string)($pif['metrics'] ?? ''))['carrier'] ?? false) : false;            $rows[] = ['id' => $net, 'icon' => 'network', 'cells' => [(string)$n['name_label'], (string)$n['name_description'], $pif === null ? '' : (string)$pif['device'],                $pif !== null && (int)$pif['VLAN'] >= 0 ? (string)$pif['VLAN'] : '-', ($n['other_config']['automatic'] ?? 'true') === 'false' ? 'No' : 'Yes',                $pif === null ? '-' : ($carrier ? 'Connected' : 'Disconnected'), $pif === null ? '-' : (string)$pif['MAC'], (string)($n['MTU'] ?? '')]];        }        return Page::grid([['title' => 'Name', 'width' => 2.4], ['title' => 'Description', 'width' => 1.8], ['title' => 'NIC', 'width' => 0.7], ['title' => 'VLAN', 'width' => 0.6],            ['title' => 'Auto', 'width' => 0.6], ['title' => 'Link status', 'width' => 1.1], ['title' => 'MAC', 'width' => 1.4], ['title' => 'MTU', 'width' => 0.6, 'align' => 'right']], $rows);    }    private function nics(Connection $c, string $host): Page    {        $cache = $c->cache;        $rows = [];        foreach ($cache->pifsOf($host) as $ref => $pif) {            if ((int)$pif['VLAN'] >= 0) {                continue;            }            $m = $cache->get((string)($pif['metrics'] ?? '')) ?? [];            $speed = (int)($m['speed'] ?? 0);            $rows[] = ['id' => $ref, 'icon' => 'nic', 'cells' => [(string)$pif['device'], (string)$pif['MAC'], !empty($m['carrier']) ? 'Connected' : 'Disconnected',                $speed >= 1000 ? ($speed / 1000) . ' Gbit/s' : $speed . ' Mbit/s', !empty($m['duplex']) ? 'Full' : 'Half', (string)($m['vendor_name'] ?? ''), (string)($m['device_name'] ?? ''),                (string)($m['pci_bus_path'] ?? '')]];        }        return Page::grid([['title' => 'NIC', 'width' => 0.6], ['title' => 'MAC', 'width' => 1.4], ['title' => 'Link status', 'width' => 1], ['title' => 'Speed', 'width' => 0.9],            ['title' => 'Duplex', 'width' => 0.6], ['title' => 'Vendor', 'width' => 1.5], ['title' => 'Device', 'width' => 2.6], ['title' => 'PCI bus path', 'width' => 1.1]], $rows);    }    // snapshots -----------------------------------------------------------------------------------------------------------    private function snapshots(Connection $c, string $vm): Page    {        $rows = [];        foreach ($c->cache->snapshotsOf($vm) as $ref => $s) {            $rows[] = ['id' => $ref, 'icon' => 'snapshot', 'cells' => [(string)$s['name_label'], Units::date((string)$s['snapshot_time']), (string)$s['name_description'], self::tags($s)],                'sort' => [null, (string)$s['snapshot_time']]];        }        return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'Created', 'width' => 1.6], ['title' => 'Description', 'width' => 2.4], ['title' => 'Tags', 'width' => 1]], $rows,            [['id' => 'vm.snapshot', 'text' => 'Take Snapshot…', 'style' => 'primary'], ['id' => 'snapshot.revert', 'text' => 'Revert To…', 'needsRow' => true],                ['id' => 'snapshot.delete', 'text' => 'Delete', 'needsRow' => true, 'style' => 'danger']], 'There are no snapshots of this VM. Take one to keep its state.');    }    // search ---------------------------------------------------------------------------------------------------------------    /** The Search tab: every VM and server (of a connection, a host, or all), with live figures. */    public function search(?Connection $only, ?string $host, ?Rrd $rrd): Page    {        $rows = [];        foreach ($this->session->connections as $c) {            if (!$c->isConnected() || ($only !== null && $c !== $only)) {                continue;            }            $cache = $c->cache;            foreach ($cache->hosts() as $ref => $h) {                if ($host !== null && $ref !== $host) {                    continue;                }                $m = $cache->hostMemory($ref);                $cpu = self::lastAverage($rrd, 'host', (string)$h['uuid'], 'cpu');                $boot = (int)($h['other_config']['boot_time'] ?? 0);                $rows[] = ['id' => $ref, 'icon' => Icons::of('host', $h, $cache), 'cells' => [(string)$h['name_label'],                    $cpu === null ? '' : ['bar' => $cpu, 'text' => round($cpu * 100) . '% of ' . ($h['cpu_info']['cpu_count'] ?? '?') . ' CPUs'],                    ['bar' => $m['total'] > 0 ? $m['used'] / $m['total'] : 0, 'text' => Units::bytes($m['used']) . ' of ' . Units::bytes($m['total'])],                    '', '', (string)$h['address'], $boot > 0 ? Units::duration(time() - $boot) : ''], 'sort' => ['0' . $h['name_label'], $cpu, $m['used']]];            }            foreach ($cache->vms() as $ref => $vm) {                if ($host !== null && ($vm['resident_on'] ?? '') !== $host) {                    continue;                }                $running = $vm['power_state'] === 'Running';                $cpu = $running ? self::lastAverage($rrd, 'vm', (string)$vm['uuid'], 'cpu') : null;                $mem = (float)($cache->vmMetrics($ref)['memory_actual'] ?? 0);                $freeSeries = $rrd !== null && $running ? $rrd->series('vm', (string)$vm['uuid'], 'memory_internal_free') : [];                $free = $freeSeries === [] ? false : end($freeSeries);                $used = $free !== false ? max(0.0, $mem - $free[1] * 1024) : $mem;                $start = Units::time((string)($cache->vmMetrics($ref)['start_time'] ?? ''));                $net = $running && $rrd !== null ? self::lastSum($rrd, 'vm', (string)$vm['uuid'], ['vif_0_rx', 'vif_0_tx']) : null;                $disk = $running && $rrd !== null ? self::lastSum($rrd, 'vm', (string)$vm['uuid'], ['vbd_xvda_read', 'vbd_xvda_write']) : null;                $rows[] = ['id' => $ref, 'icon' => Icons::of('vm', $vm), 'cells' => ['  ' . $vm['name_label'],                    $cpu === null ? '' : ['bar' => $cpu, 'text' => round($cpu * 100) . '% of ' . $vm['VCPUs_at_startup'] . ' vCPUs'],                    $running ? ['bar' => $mem > 0 ? $used / $mem : 0, 'text' => Units::bytes($used) . ' of ' . Units::bytes($mem)] : '',                    $disk === null ? '' : Units::bytes($disk) . '/s', $net === null ? '' : Units::bytes($net) . '/s', implode(', ', array_slice($cache->ipsOf($ref), 0, 1)),                    $running && $start > 0 ? Units::duration(time() - $start) : ''],                    'muted' => !$running, 'sort' => ['1' . $vm['name_label'], $cpu, $used, $disk, $net]];            }        }        return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'CPU usage', 'width' => 1.6], ['title' => 'Used memory', 'width' => 1.8], ['title' => 'Disks', 'width' => 0.9],            ['title' => 'Network', 'width' => 0.9], ['title' => 'Address', 'width' => 1.2], ['title' => 'Uptime', 'width' => 1.3]], $rows, [], 'Nothing to show: add a server first.');    }    private static function lastAverage(?Rrd $rrd, string $kind, string $uuid, string $prefix): ?float    {        if ($rrd === null) {            return null;        }        $names = array_values(array_filter($rrd->names($kind, $uuid), fn ($n) => preg_match('/^' . $prefix . '\d+$/', $n)));        if ($names === []) {            return null;        }        $sum = 0.0;        foreach ($names as $n) {            $series = $rrd->series($kind, $uuid, $n);            $sum += $series === [] ? 0 : (float)end($series)[1];        }        return $sum / count($names);    }    private static function lastSum(Rrd $rrd, string $kind, string $uuid, array $names): ?float    {        $found = false;        $sum = 0.0;        foreach ($names as $n) {            $series = $rrd->series($kind, $uuid, $n);            if ($series !== []) {                $found = true;                $sum += (float)end($series)[1];            }        }        return $found ? $sum : null;    }    // logs, alerts, events ---------------------------------------------------------------------------------------------------    /** The columns of every list of events and alerts (Logs, Notifications): one shape, so they mix and sort by date. */    public const LOG_COLUMNS = [['title' => 'Message', 'width' => 4], ['title' => 'Location', 'width' => 1.3], ['title' => 'Status', 'width' => 1.3],        ['title' => 'Date', 'width' => 1.6]];    /** The buttons over them: Cancel (a running task), Dismiss (the selected line), Dismiss all (the lines shown). */    public const LOG_ACTIONS = [['id' => 'log.cancel', 'text' => 'Cancel', 'needsRow' => 'running'], ['id' => 'log.dismiss', 'text' => 'Dismiss', 'needsRow' => 'done'],        ['id' => 'log.dismissAll', 'text' => 'Dismiss all']];    /** The Logs tab: what happened to this object (the pool: to everything of the pool), events and alerts together, newest first. */    private function logs(Connection $c, string $ref, string $kind): Page    {        $rows = [...$this->alertRows($c, $kind === 'pool' ? null : $ref), ...$this->eventRows($c, $kind === 'pool' ? null : $ref)];        return Page::grid(self::LOG_COLUMNS, self::newestFirst($rows), self::LOG_ACTIONS, 'Nothing happened to it yet: what you do and the alerts of the server are listed here.');    }    public function events(?Connection $c, ?string $target): Page    {        return Page::grid(self::LOG_COLUMNS, self::newestFirst($this->eventRows($c, $target)), self::LOG_ACTIONS,            'No event: what you do (start a VM, take a snapshot…) is listed here.');    }    public function alerts(?Connection $only, ?string $target): Page    {        return Page::grid(self::LOG_COLUMNS, self::newestFirst($this->alertRows($only, $target)), self::LOG_ACTIONS, 'There are no alerts.');    }    /** @return list<array<string, mixed>> the actions of this session (ids "log:<index>") */    private function eventRows(?Connection $c, ?string $target): array    {        $rows = [];        foreach ($this->session->log as $e) {            if (($c !== null && $e->server !== $c->name()) || ($target !== null && $e->target !== $target)) {                continue;            }            $running = $e->status === LogEntry::RUNNING;            $status = match ($e->status) {                LogEntry::RUNNING => ['bar' => $e->progress, 'text' => round($e->progress * 100) . '%', 'color' => '#6ea8f0'],                LogEntry::DONE => 'Completed',                LogEntry::CANCELLED => 'Cancelled',                default => 'Failed',            };            $rows[] = ['id' => 'log:' . $e->id, 'running' => $running, 'done' => !$running, 'at' => $e->at,                'icon' => match ($e->status) { LogEntry::RUNNING => 'info', LogEntry::DONE => 'ok', LogEntry::CANCELLED => 'warning', default => 'alert'},                'cells' => [$e->title . ($e->detail !== '' ? ' — ' . $e->detail : ''), $e->server, $status, Units::stamp($e->at)],                'color' => $e->status === LogEntry::FAILED ? '#b42318' : null];        }        return $rows;    }    /** @return list<array<string, mixed>> the alerts XAPI keeps (ids "msg:<ref>") */    private function alertRows(?Connection $only, ?string $target): array    {        $rows = [];        foreach ($this->session->connections as $c) {            if (!$c->isConnected() || ($only !== null && $c !== $only)) {                continue;            }            foreach ($c->cache->messages() as $ref => $m) {                $object = $c->cache->objectOfMessage($m);                if ($target !== null && $object !== $target) {                    continue;                }                $priority = (int)($m['priority'] ?? 5);                $at = Units::time((string)$m['timestamp']);                $rows[] = ['id' => 'msg:' . $ref, 'running' => false, 'done' => true, 'at' => $at,                    'icon' => $priority <= 2 ? 'alert' : ($priority <= 4 ? 'warning' : 'info'),                    'cells' => [self::messageText((string)($m['name'] ?? ''), (string)$m['body']), $object !== null ? $c->cache->name($object) : $c->name(),                        $priority <= 2 ? 'Critical' : ($priority <= 4 ? 'Warning' : 'Information'), Units::stamp($at)]];            }        }        return $rows;    }    /** @param list<array<string, mixed>> $rows @return list<array<string, mixed>> */    /**     * The text of an alert: XAPI's performance alarms are "value: 0.95 / config: <variable><name value="cpu_usage"/>…" on several     * lines; they become "CPU usage at 95% (alarm above 90% for 60 s)". Other bodies: their lines joined, XML tags removed.     */    public static function messageText(string $name, string $body): string    {        if (preg_match('/value:\s*([-0-9.eE]+)/', $body, $v) && preg_match('/<name value="([^"]+)"/', $body, $n)) {            $value = (float)$v[1];            $what = $n[1];            $level = preg_match('/<alarm_trigger_level value="([^"]+)"/', $body, $l) ? (float)$l[1] : null;            $period = preg_match('/<alarm_trigger_period value="([^"]+)"/', $body, $p) ? (int)$p[1] : null;            $labels = ['cpu_usage' => 'CPU usage', 'network_usage' => 'Network usage', 'disk_usage' => 'Disk usage', 'mem_usage' => 'Memory usage',                'memory_free_kib' => 'Free memory', 'fs_usage' => 'Root file system usage', 'log_fs_usage' => 'Log file system usage',                'mem_usage_ratio' => 'Memory usage', 'sr_io_throughput_total_per_host' => 'Storage throughput', 'physical_utilisation' => 'Storage usage'];            if ($what === 'sr_io_throughput_total_per_host') {                $value *= 1048576;          // perfmon counts it in MiB/s                $level = $level !== null ? $level * 1048576 : null;            }            $label = $labels[$what] ?? ucfirst(str_replace('_', ' ', $what));            $ratio = str_ends_with($what, '_usage') || str_ends_with($what, '_ratio') || $what === 'physical_utilisation';            $show = fn (float $x): string => $ratio && $x <= 1.5 ? round($x * 100) . '%'                : ($what === 'memory_free_kib' ? Units::bytes($x * 1024) : (str_contains($what, 'throughput') || $what === 'network_usage' || $what === 'disk_usage' ? Units::bytes($x) . '/s' : (string)round($x, 2)));            $text = "$label at " . $show($value);            if ($level !== null) {                $text .= ' (alarm ' . ($what === 'memory_free_kib' ? 'below ' : 'above ') . $show($level) . ($period ? " for $period s" : '') . ')';            }            return $text;        }        $plain = trim((string)preg_replace('/\s+/u', ' ', strip_tags(str_replace(['<', '>'], [' <', '> '], $body))));        return $plain !== '' ? $plain : ucfirst(strtolower(str_replace('_', ' ', $name)));    }    private static function newestFirst(array $rows): array    {        usort($rows, fn ($a, $b) => [$b['at'], $b['id']] <=> [$a['at'], $a['id']]);        foreach ($rows as &$row) {            $row['sort'] = [null, null, null, sprintf('%012d', $row['at'])];       // a click on Date sorts by the time, not the text        }        return $rows;    }    // console, performance ----------------------------------------------------------------------------------------------------    private function console(Connection $c, string $vm): Page    {        $r = $c->cache->get($vm) ?? [];        if (!in_array($r['power_state'] ?? '', ['Running', 'Paused'], true)) {            return Page::text("The console is only available while the VM is running.\n\nStart the VM to see its screen.",                [['id' => 'vm.start', 'text' => 'Start', 'style' => 'primary', 'enabled' => ($r['power_state'] ?? '') === 'Halted']]);        }        $page = new Page('console');        $page->text = $vm;        return $page;    }    private function performance(Connection $c, string $ref, string $kind, ?Rrd $rrd): Page    {        $r = $c->cache->get($ref) ?? [];        $uuid = (string)($r['uuid'] ?? '');        $type = $kind === 'host' ? 'host' : 'vm';        if ($kind === 'vm' && ($r['power_state'] ?? '') !== 'Running') {            return Page::text('Performance figures are recorded while the VM runs.');        }        $page = new Page('charts');        if ($rrd === null) {            $page->charts = [['title' => 'CPU', 'format' => 'percent', 'series' => []]];            return $page;        }        $cpus = array_values(array_filter($rrd->names($type, $uuid), fn ($n) => preg_match('/^cpu\d+$/', $n)));        natsort($cpus);        $cpuSeries = [['label' => 'Average', 'points' => $rrd->average($type, $uuid, $cpus)]];        foreach (array_slice(array_values($cpus), 0, 4) as $n) {            $cpuSeries[] = ['label' => strtoupper(substr($n, 0, 3)) . ' ' . substr($n, 3), 'points' => $rrd->series($type, $uuid, $n)];        }        $charts = [['title' => 'CPU performance', 'format' => 'percent', 'series' => $cpuSeries]];        if ($type === 'host') {            $total = $rrd->series('host', $uuid, 'memory_total_kib');            $free = $rrd->series('host', $uuid, 'memory_free_kib');            $byTime = array_column($total, 1, 0);            $used = [];            foreach ($free as [$t, $v]) {                if (isset($byTime[$t])) {                    $used[] = [$t, max(0.0, ($byTime[$t] - $v) * 1024)];                }            }            $charts[] = ['title' => 'Memory performance', 'format' => 'bytes', 'max' => ($total === [] ? null : end($total)[1] * 1024), 'series' => [['label' => 'Used memory', 'points' => $used]]];            $charts[] = ['title' => 'Network performance', 'format' => 'rate', 'series' => [['label' => 'eth0 receive', 'points' => $rrd->series('host', $uuid, 'pif_eth0_rx')],                ['label' => 'eth0 send', 'points' => $rrd->series('host', $uuid, 'pif_eth0_tx')]]];            $charts[] = ['title' => 'Load average', 'format' => 'number', 'series' => [['label' => 'Load average', 'points' => $rrd->series('host', $uuid, 'loadavg')]]];        } else {            $mem = $rrd->series('vm', $uuid, 'memory');            $free = $rrd->series('vm', $uuid, 'memory_internal_free');            $byTime = array_column($mem, 1, 0);            $used = [];            foreach ($free as [$t, $v]) {                if (isset($byTime[$t])) {                    $used[] = [$t, max(0.0, $byTime[$t] - $v * 1024)];                }            }            $charts[] = ['title' => 'Memory performance', 'format' => 'bytes', 'max' => $mem === [] ? null : end($mem)[1], 'series' => [['label' => 'Used memory', 'points' => $used]]];            $charts[] = ['title' => 'Network performance', 'format' => 'rate', 'series' => [['label' => 'Receive', 'points' => $rrd->series('vm', $uuid, 'vif_0_rx')],                ['label' => 'Send', 'points' => $rrd->series('vm', $uuid, 'vif_0_tx')]]];            $charts[] = ['title' => 'Disk performance', 'format' => 'rate', 'series' => [['label' => 'Read', 'points' => $rrd->series('vm', $uuid, 'vbd_xvda_read')],                ['label' => 'Write', 'points' => $rrd->series('vm', $uuid, 'vbd_xvda_write')]]];        }        $page->charts = $charts;        return $page;    }    // little helpers -------------------------------------------------------------------------------------------------------    private static function topology(array $r): string    {        $cores = max(1, (int)($r['platform']['cores-per-socket'] ?? 1));        $sockets = max(1, intdiv(max(1, (int)$r['VCPUs_max']), $cores));        return $sockets . ($sockets === 1 ? ' socket' : ' sockets') . ' with ' . $cores . ($cores === 1 ? ' core' : ' cores') . ' per socket';    }    private static function tags(array $r): string    {        $tags = (array)($r['tags'] ?? []);        return $tags === [] ? '<None>' : implode(', ', $tags);    }    /** @param array{total: float, free: float, used: float} $m */    private static function memoryLine(array $m): string    {        return Units::bytes($m['used']) . ' used of ' . Units::bytes($m['total']) . ($m['total'] > 0 ? ' (' . round(100 * $m['used'] / $m['total']) . '%)' : '');    }    public static function srType(string $type): string    {        return match ($type) {            'lvm' => 'LVM', 'ext' => 'EXT', 'nfs' => 'NFS', 'lvmoiscsi' => 'iSCSI', 'lvmohba' => 'HBA', 'iso' => 'ISO', 'udev' => 'Removable', 'smb' => 'SMB', 'xfs' => 'XFS',            'zfs' => 'ZFS', 'glusterfs' => 'GlusterFS', 'cephfs' => 'CephFS', 'linstor' => 'XOSTOR', default => strtoupper($type),        };    }    private static function bootOrder(string $order): string    {        $names = ['c' => 'Hard disk', 'd' => 'DVD drive', 'n' => 'Network'];        return implode(', ', array_map(fn ($ch) => $names[$ch] ?? $ch, str_split($order)));    }}