OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

vm_show.php

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

$vm         = $vm         ?? [];
$can_manage = $can_manage ?? false;

$statusCfg = [
    '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'],
];
$st    = $vm['status'] ?? 'stopped';
$stCfg = $statusCfg[$st] ?? ['label' => $st, 'cls' => 'status-pill--neutral'];
?>

<div class="vmd-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($vm['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 active"><?= $this->e($vm['name']) ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <?php if ($can_manage): ?>
            <a href="/vms/<?= $vm['id'] ?>/edit" class="btn btn-default"><?= $this->te('Bearbeiten') ?></a>
            <form method="POST" action="/vms/<?= $vm['id'] ?>/delete" style="display:inline;"
                  onsubmit="return confirm('VM &quot;<?= $this->e(addslashes($vm['name'])) ?>&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" 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><?= $vm['cpu_cores'] ? (int)$vm['cpu_cores'] : '—' ?></h3>
                        <p><?= $this->te('vCPU-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><?= $vm['ram_gb'] ? number_format((float)$vm['ram_gb'], 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><?= $vm['storage_gb'] ? number_format((float)$vm['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 <?= $vm['host_id'] ? 'bg-secondary' : 'bg-warning' ?>">
                    <div class="inner">
                        <h3 style="font-size:1.2rem;"><?= $this->e($vm['host_name'] ?? '—') ?></h3>
                        <p><?= $this->te('Host') ?></p>
                    </div>
                    <div class="icon"><i class="fas fa-server"></i></div>
                </div>
            </div>
        </div>

        <div class="vmd-layout">

            <!-- Hauptspalte -->
            <main class="vmd-main">

                <?php if (!empty($vm['purpose'])): ?>
                <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"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
                            <?= $this->te('Zweck / Verwendung') ?>
                        </h2>
                    </div>
                    <div class="detail-card-body">
                        <p class="vmd-text"><?= nl2br($this->e($vm['purpose'])) ?></p>
                    </div>
                </section>
                <?php endif; ?>

                <?php if (!empty($vm['notes'])): ?>
                <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"><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('Notizen') ?>
                        </h2>
                    </div>
                    <div class="detail-card-body">
                        <p class="vmd-text"><?= nl2br($this->e($vm['notes'])) ?></p>
                    </div>
                </section>
                <?php endif; ?>

                <?php if (empty($vm['purpose']) && empty($vm['notes'])): ?>
                <section class="detail-card">
                    <div class="detail-card-body">
                        <p class="vmd-empty-text"><?= $this->te('Keine weiteren Informationen hinterlegt.') ?></p>
                    </div>
                </section>
                <?php endif; ?>

                <?php
                $snapshots = $snapshots ?? [];
                $warnTage  = (int)($snapshot_warn_days ?? 0);
                /** Alter eines Snapshots in ganzen Tagen. */
                $snapAlter = static function (?string $zeitpunkt): ?int {
                    if ($zeitpunkt === null || $zeitpunkt === '') {
                        return null;
                    }
                    $ts = strtotime($zeitpunkt);
                    return $ts !== false ? (int)floor((time() - $ts) / 86400) : null;
                };
                ?>
                <?php if (!empty($snapshots)): ?>
                <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"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
                            <?= $this->te('Snapshots') ?>
                            <span class="badge"><?= count($snapshots) ?></span>
                        </h2>
                    </div>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th scope="col"><?= $this->te('Name') ?></th>
                                    <th scope="col"><?= $this->te('Aufgenommen') ?></th>
                                    <th scope="col"><?= $this->te('Alter') ?></th>
                                    <th scope="col"><?= $this->te('Gr&ouml;&szlig;e') ?></th>
                                </tr>
                            </thead>
                            <tbody>
                            <?php foreach ($snapshots as $snap): ?>
                                <?php
                                $alter = $snapAlter($snap['created_at_hv'] ?? null);
                                $altZu = $warnTage > 0 && $alter !== null && $alter >= $warnTage;
                                ?>
                                <tr>
                                    <td>
                                        <?= $this->e($snap['name']) ?>
                                        <?php if (!empty($snap['is_current'])): ?>
                                            <span class="status-pill status-pill--info"><?= $this->te('aktueller Stand') ?></span>
                                        <?php endif; ?>
                                        <?php if (!empty($snap['description'])): ?>
                                            <div class="vmd-muted" style="font-size:.75rem;"><?= $this->e($snap['description']) ?></div>
                                        <?php endif; ?>
                                    </td>
                                    <td class="vmd-muted">
                                        <?= !empty($snap['created_at_hv']) ? $this->e(date('d.m.Y H:i', strtotime((string)$snap['created_at_hv']))) : '&mdash;' ?>
                                    </td>
                                    <td>
                                        <?php if ($alter === null): ?>&mdash;<?php else: ?>
                                        <span class="status-pill <?= $altZu ? 'status-pill--warning' : 'status-pill--neutral' ?>">
                                            <?= $alter === 0 ? 'heute' : $alter . ' Tag(e)' ?>
                                        </span>
                                        <?php endif; ?>
                                    </td>
                                    <td class="vmd-muted"><?= $snap['size_gb'] !== null ? number_format((float)$snap['size_gb'], 1, ',', '.') . ' GB' : '—' ?></td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                    <div class="detail-card-body" style="padding-top:.5rem;">
                        <small class="form-hint">
                            <?php if ($warnTage > 0): ?>
                                <?= $this->t('Ab {tage} Tagen gilt ein Snapshot als alt. Ein lange gehaltener Snapshot belegt Speicher und macht die Wiederherstellung langsamer.', ['tage' => $this->e($warnTage)]) ?>
                            <?php else: ?>
                                <?= $this->te('Die Warnung bei alten Snapshots ist abgeschaltet.') ?>
                            <?php endif; ?>
                            <?= $this->te('Angelegt und gel&ouml;scht werden Snapshots im Hypervisor; OpenNIT zeigt nur den Stand.') ?>
                        </small>
                    </div>
                </section>
                <?php endif; ?>

            </main>

            <!-- Sidebar -->
            <aside class="vmd-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('VM-Details') ?>
                        </h3>
                    </div>
                    <div class="detail-card-body">
                    <dl class="vmd-dl">

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

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

                        <?php if (!empty($vm['guest_hostname'])): ?>
                        <dt><?= $this->te('Rechnername im Gast') ?></dt>
                        <dd><?= $this->e($vm['guest_hostname']) ?></dd>
                        <?php endif; ?>

                        <?php if (!empty($vm['guest_tools_version'])): ?>
                        <dt><?= $this->te('Gast-Tools') ?></dt>
                        <dd class="vmd-muted"><?= $this->e($vm['guest_tools_version']) ?></dd>
                        <?php endif; ?>

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

                        <dt><?= $this->te('vCPU') ?></dt>
                        <dd><?= $vm['cpu_cores'] ? (int)$vm['cpu_cores'] . ' Kerne' : '—' ?></dd>

                        <dt>RAM</dt>
                        <dd><?= $vm['ram_gb'] ? number_format((float)$vm['ram_gb'], 0, ',', '.') . ' GB' : '—' ?></dd>

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

                        <dt><?= $this->te('Host') ?></dt>
                        <dd>
                            <?php if ($vm['host_id'] ?? null): ?>
                                <a href="/vms/hosts/<?= $vm['host_id'] ?>" class="vmd-text-link"><?= $this->e($vm['host_name'] ?? '—') ?></a>
                            <?php else: ?>—<?php endif; ?>
                        </dd>

                        <?php if ($vm['hypervisor_vm_id'] ?? null): ?>
                        <dt><?= $this->te('Hypervisor-ID') ?></dt>
                        <dd><code class="vmd-code"><?= $this->e($vm['hypervisor_vm_id']) ?></code></dd>
                        <?php endif; ?>

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

                        <?php if ($vm['missing_since'] ?? null): ?>
                        <dt><?= $this->te('Nicht mehr gefunden') ?></dt>
                        <dd>
                            <span class="status-pill status-pill--danger"><?= $this->te('seit {zeitpunkt}', ['zeitpunkt' => date('d.m.Y H:i', strtotime((string)$vm['missing_since']))]) ?></span>
                            <div class="vmd-muted" style="margin-top:.25rem;">
                                <?= $this->t('Der Hypervisor meldet diese VM nicht mehr. In OpenNIT bleibt sie stehen, bis sie unter <a href="/vms/missing" class="table-link">Nicht mehr gefunden</a> aufger&auml;umt wird.') ?>
                            </div>
                        </dd>
                        <?php endif; ?>

                        <dt><?= $this->te('Angelegt') ?></dt>
                        <dd class="vmd-muted"><?= substr($vm['created_at'] ?? '', 0, 10) ?></dd>

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

                <?php if ($can_manage): ?>
                <div class="vmd-actions">
                    <a href="/vms/<?= $vm['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>
                    <form method="POST" action="/vms/<?= $vm['id'] ?>/delete" onsubmit="return confirm('VM wirklich löschen?')">
                        <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                        <button class="btn btn-danger btn-block"><?= $this->te('Löschen') ?></button>
                    </form>
                </div>
                <?php endif; ?>

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

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

/* Titel mit Icon (modul-spezifischer Zusatz zur Standard-Detail-Karte) */
.vmd-layout .detail-card-title { display:flex; align-items:center; gap:0.5rem; margin:0; }
.vmd-layout .detail-card-title svg { color:var(--gray-400); flex-shrink:0; }
.vmd-text { margin:0; font-size:0.9rem; color:var(--gray-700); line-height:1.65; }
.vmd-empty-text { margin:0; font-size:0.85rem; color:var(--gray-400); text-align:center; padding:1.5rem 0; }

.vmd-dl { margin:0; display:grid; grid-template-columns:auto 1fr; gap:0.5rem 1rem; align-items:baseline; }
.vmd-dl dt { font-size:0.72rem; color:var(--gray-400); text-transform:uppercase; letter-spacing:0.04em; white-space:nowrap; }
.vmd-dl dd { margin:0; font-size:0.875rem; color:var(--gray-900); font-weight:500; }
.vmd-muted { color:var(--gray-500) !important; font-weight:400 !important; }
.vmd-text-link { color:var(--color-primary); text-decoration:none; font-weight:500; }
.vmd-text-link:hover { text-decoration:underline; }
.vmd-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;
}
.vmd-ipam-link:hover { background:var(--color-primary-soft); text-decoration:none; }
.vmd-code { font-size:0.8rem; color:var(--gray-700); }

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

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