OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

host_show.php

33 KB
<?php
$this->layout('layouts/main', [
    'page_title'    => $host['name'],
    'current_route' => 'vms',
    'full_width'    => true,
]);

$host         = $host         ?? [];
$vms          = $vms          ?? [];
$sync_history = $sync_history ?? [];
$can_manage        = $can_manage        ?? false; // manage_vms (VM-Aktionen)
$can_manage_hosts  = $can_manage_hosts  ?? false; // manage_vm_hosts (Host bearbeiten/löschen)
$can_sync          = $can_sync          ?? false; // sync_vms (Verbindung testen/synchronisieren)
$can_admin         = $can_admin         ?? false; // admin_vms (API-Einstellungen)

$statusCfg = [
    'online'      => ['label' => 'Online',   'cls' => 'status-pill--success'],
    'offline'     => ['label' => 'Offline',  'cls' => 'status-pill--neutral'],
    'maintenance' => ['label' => 'Wartung',  'cls' => 'status-pill--warning'],
];
$st    = $host['status'] ?? 'offline';
$stCfg = $statusCfg[$st] ?? ['label' => $st, 'cls' => 'status-pill--neutral'];

$hypLabels = [
    'vmware_esxi' => 'VMware ESXi',
    'proxmox'     => 'Proxmox VE',
    'hyper_v'     => 'Microsoft Hyper-V',
    'kvm'         => 'KVM / QEMU',
    'xen'         => 'Xen',
    'other'       => 'Sonstige',
];

$vmStatusCfg = [
    'running' => ['label' => 'Laufend',  'cls' => 'status-pill--success'],
    'stopped' => ['label' => 'Gestoppt', 'cls' => 'status-pill--neutral'],
    'paused'  => ['label' => 'Pausiert', 'cls' => 'status-pill--warning'],
    'error'   => ['label' => 'Fehler',   'cls' => 'status-pill--danger'],
];

$ramTotal = (float)($host['ram_gb']           ?? 0);
$ramAlloc = (float)($host['allocated_ram_gb'] ?? 0);
$cpuTotal = (int)  ($host['cpu_cores']        ?? 0);
$cpuAlloc = (int)  ($host['allocated_cpu']    ?? 0);
$ramPct   = $ramTotal > 0 ? min(100, round($ramAlloc / $ramTotal * 100)) : 0;
$cpuPct   = $cpuTotal > 0 ? min(100, round($cpuAlloc / $cpuTotal * 100)) : 0;

$syncStatus = $host['sync_status'] ?? 'never';
$syncCfg = [
    'ok'    => ['label' => 'OK',          'cls' => 'status-pill--success'],
    'error' => ['label' => 'Fehler',      'cls' => 'status-pill--danger'],
    'never' => ['label' => 'Noch nie',    'cls' => 'status-pill--neutral'],
];
$scCfg = $syncCfg[$syncStatus] ?? $syncCfg['never'];
?>

<div class="vhd-wrap">

    <div class="content-header">
        <div>
            <h1>
                <span class="status-pill <?= $stCfg['cls'] ?>" style="vertical-align:middle;margin-right:0.4rem;"><?= $stCfg['label'] ?></span>
                <?= $this->e($host['name']) ?>
            </h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item"><a href="/vms"><?= $this->te('Virtualisierung') ?></a></li>
                <li class="breadcrumb-item"><a href="/vms/hosts"><?= $this->te('Hosts') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->e($host['name']) ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <?php if ($can_sync && !empty($host['hypervisor_url'])): ?>
            <button id="vhd-btn-test" class="btn btn-default">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
                <?= $this->te('Verbindung testen') ?>
            </button>
            <button id="vhd-btn-sync" class="btn btn-info">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
                <?= $this->te('Synchronisieren') ?>
            </button>
            <?php endif; ?>
            <?php if ($can_manage_hosts): ?>
            <a href="/vms/hosts/<?= $host['id'] ?>/edit" class="btn btn-default"><?= $this->te('Bearbeiten') ?></a>
            <form method="POST" action="/vms/hosts/<?= $host['id'] ?>/delete" style="display:inline;"
                  onsubmit="return confirm('Host ' + <?= $this->e(json_encode($host['name'], JSON_HEX_APOS | JSON_HEX_QUOT)) ?> + ' wirklich löschen?')">
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                <button class="btn btn-danger"><?= $this->te('Löschen') ?></button>
            </form>
            <?php endif; ?>
            <a href="/vms/hosts" class="btn btn-default">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
                <?= $this->te('Zurück') ?>
            </a>
        </div>
    </div>

    <div class="page-body">

        <!-- KPI Small Boxes -->
        <div class="kpi-row">
            <div class="kpi-col">
                <div class="small-box bg-info">
                    <div class="inner">
                        <h3><?= $cpuTotal ?: '—' ?></h3>
                        <p><?= $this->te('CPU-Kerne') ?></p>
                    </div>
                    <div class="icon"><i class="fas fa-microchip"></i></div>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box bg-primary">
                    <div class="inner">
                        <h3><?= $ramTotal ? number_format($ramTotal, 0, ',', '.') . ' GB' : '—' ?></h3>
                        <p><?= $this->te('Arbeitsspeicher') ?></p>
                    </div>
                    <div class="icon"><i class="fas fa-memory"></i></div>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box bg-success">
                    <div class="inner">
                        <h3><?= $host['storage_gb'] ? number_format((float)$host['storage_gb'], 0, ',', '.') . ' GB' : '—' ?></h3>
                        <p><?= $this->te('Speicher') ?></p>
                    </div>
                    <div class="icon"><i class="fas fa-hdd"></i></div>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box <?= ($host['vm_count'] ?? 0) > 0 ? 'bg-secondary' : 'bg-warning' ?>">
                    <div class="inner">
                        <h3><?= (int)($host['vm_count'] ?? 0) ?></h3>
                        <p><?= $this->te('VMs') ?></p>
                    </div>
                    <div class="icon"><i class="fas fa-server"></i></div>
                </div>
            </div>
        </div>

        <div class="vhd-layout">

            <!-- Hauptspalte: VM-Liste -->
            <main class="vhd-main">

                <?php if ($cpuTotal > 0 || $ramTotal > 0): ?>
                <section class="detail-card">
                    <div class="detail-card-header">
                        <h2 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><rect x="2" y="3" width="20" height="14" rx="2"/><polyline points="8 21 12 17 16 21"/></svg>
                            <?= $this->te('Ressourcenauslastung') ?>
                        </h2>
                    </div>
                    <div class="detail-card-body vhd-util-grid">
                        <?php if ($cpuTotal > 0): ?>
                        <div class="vhd-util-item">
                            <div class="vhd-util-label">
                                <span>CPU</span>
                                <span><?= $this->te('{belegt} / {gesamt} vCPU zugewiesen ({prozent}%)', ['belegt' => $cpuAlloc, 'gesamt' => $cpuTotal, 'prozent' => $cpuPct]) ?></span>
                            </div>
                            <div class="vhd-bar-bg">
                                <div class="vhd-bar-fill <?= $cpuPct >= 90 ? 'vhd-bar-danger' : ($cpuPct >= 75 ? 'vhd-bar-warn' : 'vhd-bar-ok') ?>"
                                     style="width:<?= $cpuPct ?>%"></div>
                            </div>
                        </div>
                        <?php endif; ?>
                        <?php if ($ramTotal > 0): ?>
                        <div class="vhd-util-item">
                            <div class="vhd-util-label">
                                <span>RAM</span>
                                <span><?= $this->te('{belegt} / {gesamt} GB zugewiesen ({prozent}%)', ['belegt' => number_format($ramAlloc, 0, ',', '.'), 'gesamt' => number_format($ramTotal, 0, ',', '.'), 'prozent' => $ramPct]) ?></span>
                            </div>
                            <div class="vhd-bar-bg">
                                <div class="vhd-bar-fill <?= $ramPct >= 90 ? 'vhd-bar-danger' : ($ramPct >= 75 ? 'vhd-bar-warn' : 'vhd-bar-ok') ?>"
                                     style="width:<?= $ramPct ?>%"></div>
                            </div>
                        </div>
                        <?php endif; ?>
                    </div>
                </section>
                <?php endif; ?>

                <section class="detail-card">
                    <div class="detail-card-header">
                        <h2 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
                            <?= $this->te('Virtuelle Maschinen') ?>
                            <span class="vhd-count-badge"><?= count($vms) ?></span>
                        </h2>
                    </div>

                    <?php if (empty($vms)): ?>
                    <div class="empty-state" style="border:none;">
                        <span class="empty-icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" width="36" height="36"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
                        </span>
                        <p><?= $this->te('Keine VMs auf diesem Host.') ?></p>
                        <?php if ($can_manage): ?>
                        <a href="/vms/create" class="btn btn-sm btn-primary"><?= $this->te('VM anlegen') ?></a>
                        <?php endif; ?>
                    </div>
                    <?php else: ?>
                    <div class="vhd-vm-table-wrap">
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th scope="col"><?= $this->te('Name') ?></th>
                                    <th scope="col" style="width:90px;"><?= $this->te('Status') ?></th>
                                    <th scope="col">IP</th>
                                    <th scope="col">OS</th>
                                    <th scope="col" style="width:70px;">CPU</th>
                                    <th scope="col" style="width:80px;">RAM</th>
                                    <th scope="col" style="width:50px;"></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ($vms as $v): ?>
                                <?php
                                $vstCfg = $vmStatusCfg[$v['status']] ?? ['label' => $v['status'], 'cls' => 'status-pill--neutral'];
                                ?>
                                <tr class="clickable" onclick="window.location='/vms/<?= $v['id'] ?>'">
                                    <td>
                                        <span class="vhd-vm-name"><?= $this->e($v['name']) ?></span>
                                        <?php if (!empty($v['purpose'])): ?>
                                        <span class="vhd-vm-purpose"><?= $this->e($v['purpose']) ?></span>
                                        <?php endif; ?>
                                    </td>
                                    <td>
                                        <span class="status-pill <?= $vstCfg['cls'] ?>"><?= $vstCfg['label'] ?></span>
                                    </td>
                                    <td class="vhd-muted">
                                        <?= $v['ip_address'] ? '<code>' . $this->e($v['ip_address']) . '</code>' : '—' ?>
                                    </td>
                                    <td class="vhd-muted"><?= $this->e($v['os'] ?: '—') ?></td>
                                    <td class="vhd-muted"><?= $v['cpu_cores'] ? (int)$v['cpu_cores'] . ' vCPU' : '—' ?></td>
                                    <td class="vhd-muted"><?= $v['ram_gb'] ? number_format((float)$v['ram_gb'], 0, ',', '.') . ' GB' : '—' ?></td>
                                    <td class="vhd-vm-actions" onclick="event.stopPropagation()">
                                        <?php if ($can_manage): ?>
                                        <a href="/vms/<?= $v['id'] ?>/edit" class="btn btn-xs btn-default" title="<?= $this->te('Bearbeiten') ?>">
                                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
                                        </a>
                                        <?php endif; ?>
                                    </td>
                                </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                    <?php endif; ?>
                </section>

                <?php if (!empty($datastores)): ?>
                <section class="detail-card">
                    <div class="detail-card-header">
                        <h2 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"/></svg>
                            <?= $this->te('Speicherorte') ?>
                            <span class="vhd-count-badge"><?= count($datastores) ?></span>
                        </h2>
                    </div>
                    <div class="vhd-vm-table-wrap">
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th scope="col"><?= $this->te('Name') ?></th>
                                    <th scope="col"><?= $this->te('Art') ?></th>
                                    <th scope="col"><?= $this->te('Kapazit&auml;t') ?></th>
                                    <th scope="col"><?= $this->te('Frei') ?></th>
                                    <th scope="col" style="width:9rem;"><?= $this->te('Belegt') ?></th>
                                </tr>
                            </thead>
                            <tbody>
                            <?php foreach ($datastores as $ds): ?>
                                <?php $pct = $ds['belegt_pct'] !== null ? (float)$ds['belegt_pct'] : null; ?>
                                <tr>
                                    <td><?= $this->e($ds['name']) ?></td>
                                    <td class="text-muted"><?= $this->e($ds['type'] ?: '—') ?></td>
                                    <td><?= $ds['capacity_gb'] !== null ? number_format((float)$ds['capacity_gb'], 0, ',', '.') . ' GB' : '—' ?></td>
                                    <td><?= $ds['free_gb'] !== null ? number_format((float)$ds['free_gb'], 0, ',', '.') . ' GB' : '—' ?></td>
                                    <td>
                                        <?php if ($pct === null): ?>&mdash;<?php else: ?>
                                        <span class="status-pill <?= $pct >= 90 ? 'status-pill--danger' : ($pct >= 75 ? 'status-pill--warning' : 'status-pill--success') ?>">
                                            <?= number_format($pct, 1, ',', '.') ?>&thinsp;%
                                        </span>
                                        <?php endif; ?>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                    <div class="detail-card-body" style="padding-top:0;">
                        <?= $this->t('<small class="form-hint"> Die Gesamtkapazit&auml;t oben ist die Summe dieser Speicherorte. Gemeldet werden sie beim Abgleich; was der Hypervisor nicht mehr f&uuml;hrt, verschwindet hier. </small>') ?>
                    </div>
                </section>
                <?php endif; ?>

                <?php if (!empty($sync_history)): ?>
                <section class="detail-card">
                    <div class="detail-card-header">
                        <h2 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
                            <?= $this->te('Sync-Verlauf') ?>
                            <span class="vhd-count-badge"><?= count($sync_history) ?></span>
                        </h2>
                    </div>
                    <div class="vhd-vm-table-wrap">
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th scope="col"><?= $this->te('Zeitpunkt') ?></th>
                                    <th scope="col" style="width:80px;"><?= $this->te('Status') ?></th>
                                    <th scope="col" style="width:80px;"><?= $this->te('Dauer') ?></th>
                                    <th scope="col" style="width:60px;"><?= $this->te('Neu') ?></th>
                                    <th scope="col" style="width:60px;"><?= $this->te('Akt.') ?></th>
                                    <th scope="col"><?= $this->t('Info') ?></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ($sync_history as $sh): ?>
                                <tr>
                                    <td class="vhd-muted"><?= substr($sh['synced_at'] ?? '', 0, 16) ?></td>
                                    <td>
                                        <?php if ($sh['status'] === 'ok'): ?>
                                        <span class="status-pill status-pill--success">OK</span>
                                        <?php else: ?>
                                        <span class="status-pill status-pill--danger"><?= $this->te('Fehler') ?></span>
                                        <?php endif; ?>
                                    </td>
                                    <td class="vhd-muted">
                                        <?= $sh['duration_ms'] !== null ? number_format((int)$sh['duration_ms'] / 1000, 1, ',', '.') . ' s' : '—' ?>
                                    </td>
                                    <td class="vhd-muted"><?= (int)($sh['vms_created'] ?? 0) ?></td>
                                    <td class="vhd-muted"><?= (int)($sh['vms_updated'] ?? 0) ?></td>
                                    <td class="vhd-muted" style="font-size:0.78rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
                                        <?php if ($sh['status'] === 'ok'): ?>
                                        <?= (int)($sh['vms_synced'] ?? 0) ?> VMs
                                        <?php else: ?>
                                        <span style="color:var(--red-700);" title="<?= $this->e($sh['error_message'] ?? '') ?>">
                                            <?= $this->e(mb_strimwidth((string)($sh['error_message'] ?? ''), 0, 80, '…')) ?>
                                        </span>
                                        <?php endif; ?>
                                    </td>
                                </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                </section>
                <?php endif; ?>

            </main>

            <!-- Sidebar -->
            <aside class="vhd-sidebar">

                <section class="detail-card">
                    <div class="detail-card-header">
                        <h3 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
                            <?= $this->te('Host-Details') ?>
                        </h3>
                    </div>
                    <div class="detail-card-body">
                    <dl class="vhd-dl">

                        <dt><?= $this->te('Status') ?></dt>
                        <dd><span class="status-pill <?= $stCfg['cls'] ?>"><?= $stCfg['label'] ?></span></dd>

                        <dt><?= $this->te('Hypervisor') ?></dt>
                        <dd>
                            <span class="vhd-hyp-pill"><?= $this->e($hypLabels[$host['hypervisor'] ?? ''] ?? ($host['hypervisor'] ?? '—')) ?></span>
                        </dd>

                        <?php if (!empty($host['ip_address'])): ?>
                        <dt><?= $this->te('IP-Adresse') ?></dt>
                        <dd>
                            <code><?= $this->e($host['ip_address']) ?></code>
                            <?php if ($host['ipam_address_id'] ?? null): ?>
                                <a href="/ipam/addresses/<?= $host['ipam_address_id'] ?>" class="vhd-ipam-link" title="<?= $this->te('In IPAM anzeigen') ?>">IPAM</a>
                            <?php endif; ?>
                        </dd>
                        <?php endif; ?>

                        <?php if (!empty($host['cluster_name'])): ?>
                        <dt><?= $this->te('Cluster') ?></dt>
                        <dd><?= $this->e($host['cluster_name']) ?></dd>
                        <?php endif; ?>

                        <?php if (!empty($host['location'])): ?>
                        <dt><?= $this->te('Standort') ?></dt>
                        <dd><?= $this->e($host['location']) ?></dd>
                        <?php endif; ?>

                        <dt>CPU</dt>
                        <dd><?= $cpuTotal ? $cpuTotal . ' Kerne' : '—' ?></dd>

                        <dt>RAM</dt>
                        <dd><?= $ramTotal ? number_format($ramTotal, 0, ',', '.') . ' GB' : '—' ?></dd>

                        <dt><?= $this->te('Speicher') ?></dt>
                        <dd><?= $host['storage_gb'] ? number_format((float)$host['storage_gb'], 0, ',', '.') . ' GB' : '—' ?></dd>

                        <?php if (!empty($host['description'])): ?>
                        <dt><?= $this->te('Beschreibung') ?></dt>
                        <dd class="vhd-muted-text"><?= $this->e($host['description']) ?></dd>
                        <?php endif; ?>

                        <dt><?= $this->te('Angelegt') ?></dt>
                        <dd class="vhd-muted-text"><?= substr($host['created_at'] ?? '', 0, 10) ?: '—' ?></dd>

                    </dl>
                    </div>
                </section>

                <?php if (!empty($host['hypervisor_url'])): ?>
                <section class="detail-card">
                    <div class="detail-card-header">
                        <h3 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
                            <?= $this->te('API-Synchronisierung') ?>
                        </h3>
                    </div>
                    <div class="detail-card-body">
                    <dl class="vhd-dl">
                        <dt><?= $this->te('Sync-Status') ?></dt>
                        <dd><span class="status-pill <?= $scCfg['cls'] ?>"><?= $scCfg['label'] ?></span></dd>

                        <?php if ($host['last_sync_at'] ?? null): ?>
                        <dt><?= $this->te('Letzter Sync') ?></dt>
                        <dd class="vhd-muted-text"><?= substr($host['last_sync_at'], 0, 16) ?></dd>
                        <?php endif; ?>

                        <?php if ($syncStatus === 'error' && !empty($host['sync_error'])): ?>
                        <dt><?= $this->te('Fehler') ?></dt>
                        <dd class="vhd-sync-error-text"><?= $this->e($host['sync_error']) ?></dd>
                        <?php endif; ?>
                    </dl>
                    <?php if ($can_admin): ?>
                    <div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--gray-100);">
                        <a href="/admin/vms/hosts/<?= $host['id'] ?>/sync-settings" class="btn btn-sm btn-outline-primary">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>
                            <?= $this->te('API-Einstellungen') ?>
                        </a>
                    </div>
                    <?php endif; ?>
                    </div>
                </section>
                <?php elseif ($can_admin): ?>
                <section class="detail-card">
                    <div class="detail-card-header">
                        <h3 class="detail-card-title">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
                            <?= $this->te('API-Synchronisierung') ?>
                        </h3>
                    </div>
                    <div class="detail-card-body">
                        <p class="vhd-scard-hint"><?= $this->te('Noch keine API-Verbindung konfiguriert.') ?></p>
                        <a href="/admin/vms/hosts/<?= $host['id'] ?>/sync-settings" class="btn btn-sm btn-outline-primary"><?= $this->te('API einrichten') ?></a>
                    </div>
                </section>
                <?php endif; ?>

                <?php if ($can_manage_hosts): ?>
                <div class="vhd-actions">
                    <a href="/vms/hosts/<?= $host['id'] ?>/edit" class="btn btn-primary btn-block">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
                        <?= $this->te('Bearbeiten') ?>
                    </a>
                </div>
                <?php endif; ?>

            </aside>
        </div>
    </div>
</div>

<?php if ($can_sync && !empty($host['hypervisor_url'])): ?>
<script>
(function () {
    var hostId = <?= (int)$host['id'] ?>;
    var csrf   = <?= json_encode($this->csrf()) ?>;

    function apiPost(path, btn, loadingText, cb) {
        var orig = btn.innerHTML;
        btn.disabled = true;
        btn.innerHTML = loadingText;
        fetch(path, {
            method: 'POST',
            headers: {'X-CSRF-Token': csrf, 'Content-Type': 'application/json'},
            body: JSON.stringify({_csrf: csrf})
        })
        .then(function (r) { return r.json(); })
        .then(function (d) { btn.disabled = false; btn.innerHTML = orig; cb(d); })
        .catch(function (e) { btn.disabled = false; btn.innerHTML = orig; alert('Fehler: ' + e); });
    }

    var btnTest = document.getElementById('vhd-btn-test');
    if (btnTest) {
        btnTest.addEventListener('click', function () {
            apiPost('/vms/hosts/' + hostId + '/test-connection', btnTest, '⏳ Teste…', function (d) {
                if (d.ok) {
                    var msg = '✓ Verbindung erfolgreich!';
                    if (d.version) msg += ' Version: ' + d.version;
                    typeof toastr !== 'undefined' ? toastr.success(msg) : alert(msg);
                } else {
                    var msg = '✗ ' + (d.error || 'Verbindung fehlgeschlagen');
                    typeof toastr !== 'undefined' ? toastr.error(msg) : alert(msg);
                }
            });
        });
    }

    var btnSync = document.getElementById('vhd-btn-sync');
    if (btnSync) {
        btnSync.addEventListener('click', function () {
            apiPost('/vms/hosts/' + hostId + '/sync', btnSync, '⏳ Synchronisiere…', function (d) {
                if (d.ok) {
                    var msg = '✓ Sync: ' + d.vms_synced + ' VMs (' + d.vms_created + ' neu, ' + d.vms_updated + ' aktualisiert)';
                    if (d.vms_missing) { msg += ', ' + d.vms_missing + ' nicht mehr gefunden'; }
                    if (d.snapshots)   { msg += ', ' + d.snapshots + ' Snapshots'; }
                    if (d.datastores)  { msg += ', ' + d.datastores + ' Speicherorte'; }
                    if (d.warnungen && d.warnungen.length) { msg += ' — ' + d.warnungen.join('; '); }
                    typeof toastr !== 'undefined' ? toastr.success(msg) : alert(msg);
                    setTimeout(function () { location.reload(); }, 1500);
                } else {
                    var msg = '✗ Sync fehlgeschlagen: ' + (d.error || 'Unbekannter Fehler');
                    typeof toastr !== 'undefined' ? toastr.error(msg) : alert(msg);
                }
            });
        });
    }
}());
</script>
<?php endif; ?>

<style>
/* ====================================================
   Host-Detail  ·  Prefix: vhd-
   ==================================================== */
.vhd-wrap { display:flex; flex-direction:column; }
.vhd-layout { display:grid; grid-template-columns:1fr 310px; gap:1.5rem; align-items:start; }
.vhd-main { display:flex; flex-direction:column; min-width:0; }
.vhd-sidebar { display:flex; flex-direction:column; position:sticky; top:1rem; }

/* Titel mit Icon + Zähler (modul-spezifischer Zusatz zur Standard-Detail-Karte) */
.vhd-layout .detail-card-title { display:flex; align-items:center; gap:0.5rem; margin:0; flex:1; }
.vhd-layout .detail-card-title svg { color:var(--gray-400); flex-shrink:0; }

/* Hypervisor pill */
.vhd-hyp-pill {
    display:inline-block; font-size:0.72rem; font-weight:600;
    padding:0.15rem 0.5rem; border-radius:0.2rem;
    background:var(--color-primary-pale); color:var(--blue-700);
}

.vhd-count-badge {
    margin-left:auto; display:inline-flex; align-items:center; justify-content:center;
    min-width:22px; height:18px; padding:0 0.35rem;
    background:var(--gray-200); color:var(--gray-500); border-radius:9999px; font-size:0.72rem; font-weight:700;
}

/* Utilisation bars */
.vhd-util-grid { display:flex; flex-direction:column; gap:1rem; }
.vhd-util-item { display:flex; flex-direction:column; gap:0.35rem; }
.vhd-util-label { display:flex; justify-content:space-between; font-size:0.78rem; color:var(--gray-500); }
.vhd-bar-bg { height:6px; background:var(--gray-100); border-radius:9999px; overflow:hidden; }
.vhd-bar-fill { height:100%; border-radius:9999px; transition:width .3s; }
.vhd-bar-ok     { background:var(--green-500); }
.vhd-bar-warn   { background:var(--amber-500); }
.vhd-bar-danger { background:var(--red-500); }

/* VM Table */
.vhd-vm-table-wrap { overflow-x:auto; }
.vhd-vm-name { display:block; font-weight:500; color:var(--gray-900); }
.vhd-vm-purpose { display:block; font-size:0.75rem; color:var(--gray-400); margin-top:0.1rem; }
.vhd-muted { color:var(--gray-500); font-size:0.82rem; }
.vhd-vm-actions { text-align:right; white-space:nowrap; }

/* Sidebar */
.vhd-scard-hint { font-size:0.82rem; color:var(--gray-400); margin:0 0 .75rem; }
.vhd-dl { margin:0; display:grid; grid-template-columns:auto 1fr; gap:0.5rem 1rem; align-items:baseline; }
.vhd-dl dt { font-size:0.72rem; color:var(--gray-400); text-transform:uppercase; letter-spacing:0.04em; white-space:nowrap; }
.vhd-dl dd { margin:0; font-size:0.875rem; color:var(--gray-900); font-weight:500; }
.vhd-muted-text { color:var(--gray-500) !important; font-weight:400 !important; font-size:0.8rem !important; }
.vhd-sync-error-text { color:var(--red-700) !important; font-weight:400 !important; font-size:0.78rem !important; overflow-wrap:break-word; word-break:break-word; max-height:6rem; overflow-y:auto; }
.vhd-ipam-link {
    display:inline-block; margin-left:0.4rem; font-size:0.68rem; font-weight:600;
    padding:0.1rem 0.35rem; border-radius:0.2rem;
    background:var(--color-primary-soft); color:var(--color-primary-strong); text-decoration:none; vertical-align:middle;
}
.vhd-ipam-link:hover { background:var(--color-primary-soft); text-decoration:none; }

/* Actions */
.vhd-actions { display:flex; flex-direction:column; gap:0.5rem; }

@media (max-width:900px) { .vhd-layout { grid-template-columns:1fr; } .vhd-sidebar { position:static; } }
@media (max-width:480px) { .vhd-dl { grid-template-columns:1fr; gap:0.1rem; } .vhd-dl dt { margin-top:0.5rem; } }
</style>