OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

38 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<style>
a { text-decoration:none; } a:hover { text-decoration:none; }

.ipam-wrap { display:flex; flex-direction:column; }

.ipam-util-box { background:var(--color-bg); border:1px solid rgba(0,0,0,.08); border-radius:0.25rem;
    box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.08); padding:7px 12px; display:flex;
    flex-direction:column; justify-content:center; flex:1; min-width:240px; }
.ipam-util-head { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:0.35rem; }
.ipam-util-title { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-400); }
.ipam-util-pct { font-size:1.2rem; font-weight:800; color:var(--gray-900); line-height:1; }
.ipam-util-bar { height:7px; background:var(--slate-100); border-radius:9999px; overflow:hidden; }
.ipam-util-fill { height:100%; border-radius:9999px; transition:width .4s ease; }
.ipam-util-sub { font-size:0.72rem; color:var(--gray-400); margin-top:0.3rem; }

.ipam-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:1rem; }
.ipam-card { background:var(--color-bg); border:1px solid rgba(0,0,0,.125);
    border-radius:0.25rem; overflow:hidden; box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);
    transition:box-shadow 0.15s, transform 0.15s; cursor:pointer; text-decoration:none; color:inherit; display:block; }
.ipam-card:hover { box-shadow:0 0 1px rgba(0,0,0,.125),0 4px 12px rgba(0,0,0,.15); transform:translateY(-1px); }
.ipam-card-stripe { height:3px; }
.ipam-card-body { padding:1rem 1.25rem 0.875rem; }
.ipam-card-title { font-size:0.95rem; font-weight:600; color:var(--color-gray-900); margin-bottom:0.2rem; }
.ipam-card-cidr { font-size:0.8rem; color:var(--color-secondary); font-family:monospace; }
.ipam-card-meta { display:flex; gap:1rem; margin-top:0.75rem; flex-wrap:wrap; }
.ipam-card-stat { display:flex; flex-direction:column; gap:0.1rem; }
.ipam-card-stat-val { font-size:1rem; font-weight:700; color:var(--color-gray-900); }
.ipam-card-stat-lbl { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--color-secondary); }
.ipam-card-footer { padding:0.5rem 1.25rem; background:var(--color-gray-50); border-top:1px solid rgba(0,0,0,.125);
    font-size:0.75rem; color:var(--color-secondary); }

/* utilization bar on card */
.ipam-card-util { margin-top:0.85rem; }
.ipam-card-util-head { display:flex; align-items:center; justify-content:space-between; font-size:0.72rem; color:var(--gray-400); margin-bottom:0.3rem; }
.ipam-card-util-head strong { color:var(--gray-700); font-weight:700; }
.ipam-card-util-bar { height:6px; background:var(--slate-100); border-radius:9999px; overflow:hidden; }
.ipam-card-util-fill { height:100%; border-radius:9999px; transition:width .4s ease; }

/* Search results */
.ipam-search-results { background:var(--color-bg); border:1px solid rgba(0,0,0,.125); border-radius:0.25rem;
    box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2); overflow:hidden; margin-bottom:1.5rem; }
.ipam-search-row { display:flex; align-items:center; gap:1rem; padding:0.65rem 1.25rem;
    border-bottom:1px solid var(--color-gray-50); text-decoration:none; color:inherit; transition:background 0.1s; }
.ipam-search-row:last-child { border-bottom:none; }
.ipam-search-row:hover { background:var(--color-gray-50); }
.ipam-search-ip { font-family:monospace; font-size:0.9rem; font-weight:700; color:var(--color-gray-900); min-width:120px; }
.ipam-search-host { font-size:0.85rem; color:var(--color-gray-700); flex:1; }
.ipam-search-sub { font-size:0.75rem; color:var(--color-secondary); }

@media(max-width:768px) {
    .ipam-grid { grid-template-columns:1fr; }
}

/* View Toggle */
.ipam-view-toggle { display:flex; background:var(--gray-100); border:1px solid var(--gray-300); border-radius:0.25rem; overflow:hidden; }
.ipam-view-btn { background:none; border:none; padding:0.4rem 0.6rem; cursor:pointer; color:var(--gray-500); display:flex; align-items:center; transition:all 0.15s; }
.ipam-view-btn:hover { color:var(--gray-700); background:var(--gray-200); }
.ipam-view-btn.active { background:var(--color-primary); color:var(--color-on-accent); }

/* List view */
.ipam-list-name { font-weight:600; color:var(--color-gray-900); }
.ipam-list-row:hover .ipam-list-name { color:var(--color-primary); }
.ipam-list-mono { font-family:monospace; font-size:0.82rem; color:var(--color-gray-700); }
.ipam-list-muted { font-size:0.82rem; color:var(--color-secondary); }
.ipam-list-num { font-size:0.85rem; font-weight:600; color:var(--color-gray-900); }
.ipam-list-util { min-width:120px; }
.ipam-list-util-bar { height:6px; background:var(--slate-100); border-radius:9999px; overflow:hidden; width:90px; display:inline-block; vertical-align:middle; margin-right:0.5rem; }
.ipam-list-util-fill { height:100%; border-radius:9999px; }
.ipam-list-desc { font-size:0.78rem; color:var(--gray-400); max-width:240px; }

/* Tree view (Supernetz → Kind-Subnetze) */
.ipam-tree { background:var(--color-bg); border:1px solid rgba(0,0,0,.125); border-radius:0.25rem;
    box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2); overflow:hidden; }
.ipam-tree-row { display:flex; align-items:center; gap:0.75rem; padding:0.55rem 1rem;
    border-bottom:1px solid var(--color-gray-50); text-decoration:none; color:inherit; transition:background 0.1s; }
.ipam-tree-row:last-child { border-bottom:none; }
.ipam-tree-row:hover { background:var(--color-gray-50); }
.ipam-tree-guide { color:var(--gray-300); font-family:monospace; white-space:pre; flex-shrink:0; user-select:none; }
.ipam-tree-dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; }
.ipam-tree-name { font-weight:600; color:var(--color-gray-900); }
.ipam-tree-cidr { font-family:monospace; font-size:0.82rem; color:var(--color-secondary); }
.ipam-tree-meta { margin-left:auto; display:flex; align-items:center; gap:1rem; }
.ipam-tree-util-bar { height:6px; width:80px; background:var(--slate-100); border-radius:9999px; overflow:hidden; display:inline-block; vertical-align:middle; }
.ipam-tree-util-fill { height:100%; border-radius:9999px; }
.ipam-tree-num { font-size:0.82rem; color:var(--gray-500); min-width:64px; text-align:right; }

/* Freiraum-Suche Modal (Basis: app.css .modal-*; hier nur Zustand + Maße) */
.iff-modal { display:none; z-index:1080; }
.iff-modal.open { display:flex; }
.iff-modal .modal-card { max-width:560px; width:100%; }
.iff-form { display:flex; flex-wrap:wrap; gap:0.75rem; align-items:flex-end; }
.iff-field { display:flex; flex-direction:column; gap:0.25rem; }
.iff-field label { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.04em; color:var(--gray-400); }
.iff-results { margin-top:1rem; max-height:340px; overflow:auto; }
.iff-res-row { display:flex; align-items:center; justify-content:space-between; gap:0.75rem;
    padding:0.45rem 0.25rem; border-bottom:1px solid var(--gray-100); }
.iff-res-row:last-child { border-bottom:none; }
.iff-res-net { font-family:monospace; font-size:0.9rem; font-weight:700; color:var(--gray-900); }
.iff-hint { font-size:0.78rem; color:var(--gray-400); margin-top:0.5rem; }
</style>

<?php
// Hilfsfunktion für die Auslastungsfarbe
$utilColor = function(?int $pct): string {
    if ($pct === null) return '#9ca3af';
    if ($pct >= 90) return '#dc2626';
    if ($pct >= 75) return '#d97706';
    if ($pct >= 50) return '#3c8dbc';
    return '#16a34a';
};
?>

<div class="ipam-wrap">
    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1><?= $this->te('IP-Adressverwaltung') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item active">IPAM</li>
            </ol>
        </div>
        <div class="content-header-actions">
            <div class="ipam-view-toggle" id="ipamViewToggle">
                <button class="ipam-view-btn active" data-mode="cards" title="<?= $this->te('Kachelansicht') ?>" aria-label="<?= $this->te('Kachelansicht') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                        <rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
                        <rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
                    </svg>
                </button>
                <button class="ipam-view-btn" data-mode="list" title="<?= $this->te('Listenansicht') ?>" aria-label="<?= $this->te('Listenansicht') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                        <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
                        <line x1="8" y1="18" x2="21" y2="18"/>
                        <line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/>
                        <line x1="3" y1="18" x2="3.01" y2="18"/>
                    </svg>
                </button>
                <button class="ipam-view-btn" data-mode="tree" title="<?= $this->te('Baumansicht (Supernetze)') ?>" aria-label="<?= $this->te('Baumansicht') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
                        <rect x="3" y="3" width="7" height="5" rx="1"/><rect x="14" y="9" width="7" height="5" rx="1"/>
                        <rect x="14" y="16" width="7" height="5" rx="1"/>
                        <path d="M6.5 8v9.5a1 1 0 0 0 1 1H14M6.5 11.5H14"/>
                    </svg>
                </button>
            </div>
            <?php if ($can_manage_subnets): ?>
            <button type="button" class="btn btn-default" onclick="iffOpen()" title="<?= $this->te('Freien Adressraum innerhalb eines Bereichs finden') ?>"><?= $this->te('Freies Subnetz finden') ?></button>
            <a href="/ipam/subnets/create" class="btn btn-primary"><?= $this->te('Subnetz anlegen') ?></a>
            <?php endif; ?>
        </div>
    </div>

    <div class="page-body">

        <!-- KPI Small Boxes -->
        <?php if (!empty($subnets) || $stats['addresses'] > 0): ?>
        <div class="kpi-row">
            <div class="kpi-col">
            <div class="small-box bg-info">
                <div class="inner"><h3><?= (int)$stats['subnets'] ?></h3><p><?= $this->te('Subnetze') ?></p></div>
                <div class="icon"><i class="fas fa-network-wired"></i></div>
            </div>
            </div>
            <div class="kpi-col">
            <div class="small-box bg-primary">
                <div class="inner"><h3><?= (int)$stats['addresses'] ?></h3><p><?= $this->te('IP-Adressen') ?></p></div>
                <div class="icon"><i class="fas fa-server"></i></div>
            </div>
            </div>
            <div class="kpi-col">
            <div class="small-box bg-success">
                <div class="inner"><h3><?= (int)$stats['active'] ?></h3><p><?= $this->te('Aktiv') ?></p></div>
                <div class="icon"><i class="fas fa-check-circle"></i></div>
            </div>
            </div>
            <div class="kpi-col">
            <div class="small-box bg-warning">
                <div class="inner"><h3><?= (int)$stats['reserved'] ?></h3><p><?= $this->te('Reserviert') ?></p></div>
                <div class="icon"><i class="fas fa-bookmark"></i></div>
            </div>
            </div>
            <!-- Auslastung gesamt -->
            <div class="kpi-col" style="flex:2 1 320px;">
            <div class="ipam-util-box">
                <div class="ipam-util-head">
                    <span class="ipam-util-title"><?= $this->te('Auslastung gesamt') ?></span>
                    <span class="ipam-util-pct"><?= $stats['utilization'] !== null ? $stats['utilization'] . '%' : '–' ?></span>
                </div>
                <div class="ipam-util-bar">
                    <div class="ipam-util-fill" style="width:<?= $stats['utilization'] ?? 0 ?>%;background:<?= $utilColor($stats['utilization']) ?>;"></div>
                </div>
                <div class="ipam-util-sub">
                    <?= $this->te('{anzahl} belegt', ['anzahl' => (int)$stats['used']]) ?><?= $stats['capacity'] !== null ? $this->te(' von {gesamt} nutzbar', ['gesamt' => number_format((int)$stats['capacity'], 0, ',', '.')]) : '' ?>
                </div>
            </div>
            </div>
        </div>
        <?php endif; ?>

        <!-- Filter Card -->
        <div class="card card-outline card-primary mb-3">
            <div class="filter-card-body">
                <form method="GET" action="/ipam" style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
                    <div style="position:relative;flex:1;min-width:200px;max-width:340px;">
                                                <input type="text" name="q" value="<?= $this->e($search_query) ?>" class="form-control form-control-sm" style="padding-left:2rem;" placeholder="<?= $this->te('IP, Hostname oder Beschreibung…') ?>">
                    </div>
                    <?php if ($search_query === '' && !empty($subnets)):
                        $vlans = array_filter(array_unique(array_column($subnets, 'vlan')));
                        sort($vlans);
                        $locs = array_filter(array_unique(array_column($subnets, 'location')));
                        sort($locs);
                    ?>
                    <div style="position:relative;flex:1;min-width:160px;max-width:260px;">
                        <svg style="position:absolute;left:0.6rem;top:50%;transform:translateY(-50%);color:var(--gray-400);" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
                        <input type="text" id="ipam-f-q" class="form-control form-control-sm" style="padding-left:2rem;"
                               placeholder="<?= $this->te('Netz filtern (Name, IP …)') ?>" oninput="ipamFilter()">
                    </div>
                    <?php if (!empty($vlans)): ?>
                    <select id="ipam-f-vlan" class="form-control form-control-sm" style="width:auto;" onchange="ipamFilter()">
                        <option value=""><?= $this->te('Alle VLANs') ?></option>
                        <?php foreach ($vlans as $v): ?>
                        <option value="<?= $this->e($v) ?>">VLAN <?= $this->e($v) ?></option>
                        <?php endforeach; ?>
                    </select>
                    <?php endif; ?>
                    <?php if (!empty($locs)): ?>
                    <select id="ipam-f-loc" class="form-control form-control-sm" style="width:auto;" onchange="ipamFilter()">
                        <option value=""><?= $this->te('Alle Standorte') ?></option>
                        <?php foreach ($locs as $l): ?>
                        <option value="<?= $this->e($l) ?>"><?= $this->e($l) ?></option>
                        <?php endforeach; ?>
                    </select>
                    <?php endif; ?>
                    <span id="ipam-f-count" style="font-size:.78rem;color:var(--color-secondary);white-space:nowrap;"></span>
                    <button id="ipam-f-reset" type="button" class="btn btn-default btn-sm" style="display:none" onclick="ipamFilterReset()">
                        <?= $this->te('&#10005; Filter zurücksetzen') ?>
                    </button>
                    <?php endif; ?>
                    <?php if ($search_query !== ''): ?>
                    <a href="/ipam" class="btn btn-default btn-sm"><?= $this->te('&#10005; Suche zurücksetzen') ?></a>
                    <?php endif; ?>
                    <button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Suchen') ?></button>
                </form>
            </div>
        </div>

        <?php if ($search_query !== ''): ?>
        <p style="font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--gray-400);margin-bottom:0.6rem;">
            <?= $this->te('{anzahl} Treffer für „{suche}"', ['anzahl' => count($search_results), 'suche' => $search_query]) ?>
        </p>
        <?php if (!empty($search_results)): ?>
        <div class="ipam-search-results">
            <?php foreach ($search_results as $r): ?>
            <a href="/ipam/addresses/<?= $r['id'] ?>" class="ipam-search-row">
                <span class="ipam-search-ip"><?= $this->e($r['ip_address']) ?></span>
                <span class="ipam-search-host"><?= $this->e($r['hostname'] ?? '–') ?></span>
                <?php
                    $bc = ['active'=>'success','reserved'=>'warning','offline'=>'secondary','free'=>'info'];
                    $bk = $bc[$r['status']] ?? 'secondary';
                    $bl = ['active'=>'Aktiv','reserved'=>'Reserviert','offline'=>'Offline','free'=>'Frei'];
                ?>
                <span class="badge badge-<?= $bk ?>"><?= $bl[$r['status']] ?? $this->e($r['status']) ?></span>
                <span class="ipam-search-sub"><?= $this->e($r['subnet_name']) ?> (<?= $this->e($r['subnet_network']) ?>/<?= $r['subnet_cidr'] ?>)</span>
            </a>
            <?php endforeach; ?>
        </div>
        <?php else: ?>
        <p style="font-size:0.88rem;color:var(--gray-500);margin-bottom:1.5rem;"><?= $this->te('Keine Einträge gefunden.') ?></p>
        <?php endif; ?>
        <?php endif; ?>

        <?php if (empty($subnets) && $search_query === ''): ?>
        <div style="text-align:center;padding:3rem 1rem;color:var(--gray-400);">
            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" style="margin:0 auto 1rem;display:block;"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
            <p style="font-size:0.9rem;margin:0 0 1rem;"><?= $this->te('Noch keine Subnetze erfasst.') ?></p>
            <?php if ($can_manage_subnets): ?>
            <a href="/ipam/subnets/create" style="font-size:0.85rem;color:var(--color-primary);font-weight:600;"><?= $this->te('Erstes Subnetz anlegen →') ?></a>
            <?php endif; ?>
        </div>
        <?php elseif (!empty($subnets)): ?>
        <!-- Subnetz-Kacheln -->
        <div class="ipam-grid" id="ipam-grid">
            <?php foreach ($subnets as $s): $util = $s['utilization'] ?? null; ?>
            <a href="/ipam/subnets/<?= $s['id'] ?>" class="ipam-card"
               data-name="<?= strtolower(htmlspecialchars($s['name'], ENT_QUOTES)) ?>"
               data-network="<?= htmlspecialchars($s['network'] . '/' . $s['cidr'], ENT_QUOTES) ?>"
               data-vlan="<?= htmlspecialchars($s['vlan'] ?? '', ENT_QUOTES) ?>"
               data-loc="<?= strtolower(htmlspecialchars($s['location'] ?? '', ENT_QUOTES)) ?>"
               data-desc="<?= strtolower(htmlspecialchars($s['description'] ?? '', ENT_QUOTES)) ?>">
                <div class="ipam-card-stripe" style="background:<?= $this->e($s['color']) ?>;"></div>
                <div class="ipam-card-body">
                    <div class="ipam-card-title" style="display:flex;align-items:center;gap:0.4rem;justify-content:space-between;">
                        <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= $this->e($s['name']) ?></span>
                        <?php if (empty($s['scan_enabled'])): ?>
                            <span class="status-pill status-pill--neutral" title="<?= $this->te('Remote-Scan deaktiviert') ?>"><?= $this->te('Scan&nbsp;aus') ?></span>
                        <?php else: ?>
                            <span class="status-pill status-pill--success" title="<?= $this->te('Remote-Scan aktiviert') ?>">
                                <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="11" cy="11" r="7"/><path d="M21 21l-3.5-3.5"/></svg>
                                <?= $this->te('Scan') ?>
                            </span>
                        <?php endif; ?>
                    </div>
                    <div class="ipam-card-cidr"><?= $this->e($s['network']) ?>/<?= $s['cidr'] ?><?= $s['vlan'] ? ' &nbsp;·&nbsp; VLAN ' . $this->e($s['vlan']) : '' ?><?= $s['location'] ? ' &nbsp;·&nbsp; ' . $this->e($s['location']) : '' ?></div>
                    <div class="ipam-card-meta">
                        <div class="ipam-card-stat"><span class="ipam-card-stat-val"><?= $s['total_ips'] ?></span><span class="ipam-card-stat-lbl"><?= $this->te('Erfasst') ?></span></div>
                        <?php if ($s['active_ips'] > 0): ?>
                        <div class="ipam-card-stat"><span class="ipam-card-stat-val" style="color:var(--green-600);"><?= $s['active_ips'] ?></span><span class="ipam-card-stat-lbl"><?= $this->te('Aktiv') ?></span></div>
                        <?php endif; ?>
                        <?php if ($s['reserved_ips'] > 0): ?>
                        <div class="ipam-card-stat"><span class="ipam-card-stat-val" style="color:var(--amber-600);"><?= $s['reserved_ips'] ?></span><span class="ipam-card-stat-lbl"><?= $this->te('Reserviert') ?></span></div>
                        <?php endif; ?>
                        <?php if ($s['offline_ips'] > 0): ?>
                        <div class="ipam-card-stat"><span class="ipam-card-stat-val" style="color:var(--gray-400);"><?= $s['offline_ips'] ?></span><span class="ipam-card-stat-lbl"><?= $this->te('Offline') ?></span></div>
                        <?php endif; ?>
                    </div>
                    <!-- Auslastung -->
                    <div class="ipam-card-util">
                        <div class="ipam-card-util-head">
                            <span><?= $util !== null ? 'Auslastung' : ($s['is_ipv6'] ? 'IPv6-Netz' : 'Kapazität') ?></span>
                            <?php if ($util !== null): ?>
                            <span><strong><?= $util ?>%</strong> &middot; <?= (int)$s['used'] ?>/<?= number_format((int)$s['capacity'], 0, ',', '.') ?></span>
                            <?php else: ?>
                            <span><?= $this->te('{anzahl} belegt', ['anzahl' => (int)$s['used']]) ?></span>
                            <?php endif; ?>
                        </div>
                        <div class="ipam-card-util-bar">
                            <div class="ipam-card-util-fill" style="width:<?= $util ?? 0 ?>%;background:<?= $utilColor($util) ?>;"></div>
                        </div>
                    </div>
                </div>
                <?php if ($s['description']): ?>
                <div class="ipam-card-footer"><?= $this->e(mb_strimwidth($s['description'], 0, 80, '…')) ?></div>
                <?php endif; ?>
            </a>
            <?php endforeach; ?>
        </div>

        <!-- Subnetz-Liste -->
        <div class="ipam-list-wrap" id="ipam-list" style="display:none;">
            <div class="table-wrapper">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col" style="width:4px;padding:0;"></th>
                            <th scope="col"><?= $this->te('Netz') ?></th>
                            <th scope="col">CIDR</th>
                            <th scope="col">VLAN</th>
                            <th scope="col"><?= $this->te('Standort') ?></th>
                            <th scope="col"><?= $this->te('Scan') ?></th>
                            <th scope="col"><?= $this->te('IPs') ?></th>
                            <th scope="col"><?= $this->te('Auslastung') ?></th>
                            <th scope="col"><?= $this->te('Beschreibung') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($subnets as $s): $util = $s['utilization'] ?? null; ?>
                    <tr class="ipam-list-row"
                        data-name="<?= strtolower(htmlspecialchars($s['name'], ENT_QUOTES)) ?>"
                        data-network="<?= htmlspecialchars($s['network'] . '/' . $s['cidr'], ENT_QUOTES) ?>"
                        data-vlan="<?= htmlspecialchars($s['vlan'] ?? '', ENT_QUOTES) ?>"
                        data-loc="<?= strtolower(htmlspecialchars($s['location'] ?? '', ENT_QUOTES)) ?>"
                        data-desc="<?= strtolower(htmlspecialchars($s['description'] ?? '', ENT_QUOTES)) ?>"
                        onclick="location.href='/ipam/subnets/<?= $s['id'] ?>'"
                        style="cursor:pointer;">
                        <td style="padding:0;width:4px;"><div style="width:4px;height:100%;min-height:38px;background:<?= $this->e($s['color']) ?>;border-radius:2px 0 0 2px;"></div></td>
                        <td class="ipam-list-name"><?= $this->e($s['name']) ?></td>
                        <td class="ipam-list-mono"><?= $this->e($s['network']) ?>/<?= $s['cidr'] ?></td>
                        <td class="ipam-list-muted"><?= $s['vlan'] ? $this->e($s['vlan']) : '–' ?></td>
                        <td class="ipam-list-muted"><?= $s['location'] ? $this->e($s['location']) : '–' ?></td>
                        <td>
                            <?php if (empty($s['scan_enabled'])): ?>
                                <span class="status-pill status-pill--neutral" title="<?= $this->te('Remote-Scan deaktiviert') ?>"><?= $this->te('aus') ?></span>
                            <?php else: ?>
                                <span class="status-pill status-pill--success" title="<?= $this->te('Remote-Scan aktiviert') ?>"><?= $this->te('an') ?></span>
                            <?php endif; ?>
                        </td>
                        <td class="ipam-list-num"><?= $s['total_ips'] ?></td>
                        <td class="ipam-list-util">
                            <?php if ($util !== null): ?>
                            <span class="ipam-list-util-bar"><span class="ipam-list-util-fill" style="width:<?= $util ?>%;background:<?= $utilColor($util) ?>;"></span></span>
                            <span class="ipam-list-muted" style="font-weight:700;color:var(--gray-700);"><?= $util ?>%</span>
                            <?php else: ?>
                            <span class="ipam-list-muted"><?= $s['is_ipv6'] ? 'IPv6' : '–' ?></span>
                            <?php endif; ?>
                        </td>
                        <td class="ipam-list-desc"><?= $s['description'] ? $this->e(mb_strimwidth($s['description'], 0, 60, '…')) : '' ?></td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- Subnetz-Baum (Supernetz → Kind-Subnetze) -->
        <?php if (!empty($subnet_tree)): ?>
        <div class="ipam-tree" id="ipam-tree" style="display:none;">
            <?php foreach ($subnet_tree as $s):
                $util  = $s['utilization'] ?? null;
                $depth = (int)($s['depth'] ?? 0);
            ?>
            <a href="/ipam/subnets/<?= $s['id'] ?>" class="ipam-tree-row">
                <?php if ($depth > 0): ?><span class="ipam-tree-guide"><?= str_repeat(' ', $depth - 1) ?>└─</span><?php endif; ?>
                <span class="ipam-tree-dot" style="background:<?= $this->e($s['color']) ?>;"></span>
                <span class="ipam-tree-name"><?= $this->e($s['name']) ?></span>
                <span class="ipam-tree-cidr"><?= $this->e($s['network']) ?>/<?= $s['cidr'] ?><?= $s['vlan'] ? ' · VLAN ' . $this->e($s['vlan']) : '' ?></span>
                <span class="ipam-tree-meta">
                    <?php if ($util !== null): ?>
                    <span class="ipam-tree-util-bar"><span class="ipam-tree-util-fill" style="width:<?= $util ?>%;background:<?= $utilColor($util) ?>;"></span></span>
                    <span class="ipam-list-muted" style="font-weight:700;color:var(--gray-700);min-width:34px;"><?= $util ?>%</span>
                    <?php else: ?>
                    <span class="ipam-list-muted" style="min-width:34px;"><?= $s['is_ipv6'] ? 'IPv6' : '–' ?></span>
                    <?php endif; ?>
                    <span class="ipam-tree-num"><?= $this->te('{anzahl} IPs', ['anzahl' => $s['total_ips']]) ?></span>
                </span>
            </a>
            <?php endforeach; ?>
        </div>
        <?php endif; ?>

        <!-- Kein Treffer nach Filtern -->
        <div id="ipam-no-results" style="display:none;text-align:center;padding:2.5rem 1rem;color:var(--gray-400);">
            <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" style="margin:0 auto .75rem;display:block;"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/><path d="M8 11h6"/></svg>
            <p style="font-size:.88rem;margin:0;"><?= $this->te('Kein Netz entspricht dem Filter.') ?></p>
        </div>
        <?php endif; ?>
    </div>
</div>

<?php if ($can_manage_subnets): ?>
<!-- Freiraum-Suche Modal -->
<div class="modal-overlay iff-modal" id="iffModal">
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Freies Subnetz finden') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="iffClose()">&times;</button>
        </div>
        <div class="modal-body">
            <div class="iff-form">
                <?php
                    $v4subnets = array_values(array_filter($subnets, fn($s) => empty($s['is_ipv6'])));
                ?>
                <?php if (!empty($v4subnets)): ?>
                <div class="iff-field" style="flex:1;min-width:180px;">
                    <label for="iff-parent"><?= $this->te('Aus Subnetz übernehmen') ?></label>
                    <select id="iff-parent" class="form-control form-control-sm" onchange="iffParent()">
                        <option value=""><?= $this->te('– manuell eingeben –') ?></option>
                        <?php foreach ($v4subnets as $s): ?>
                        <option value="<?= $this->e($s['network']) ?>|<?= (int)$s['cidr'] ?>"><?= $this->e($s['name']) ?> (<?= $this->e($s['network']) ?>/<?= (int)$s['cidr'] ?>)</option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>
                <div class="iff-field" style="flex:1;min-width:130px;">
                    <label><?= $this->te('Bereich (Netz)') ?></label>
                    <input type="text" id="iff-network" class="form-control form-control-sm" placeholder="10.0.0.0">
                </div>
                <div class="iff-field" style="width:80px;">
                    <label>/CIDR</label>
                    <input type="number" id="iff-cidr" class="form-control form-control-sm" min="0" max="32" placeholder="16" oninput="iffRefreshWanted()">
                </div>
                <div class="iff-field" style="width:110px;">
                    <label for="iff-wanted"><?= $this->te('Gesuchtes /') ?></label>
                    <select id="iff-wanted" class="form-control form-control-sm"></select>
                </div>
                <button type="button" class="btn btn-primary btn-sm" onclick="iffSearch()"><?= $this->te('Suchen') ?></button>
            </div>
            <div class="iff-hint" id="iff-hint" style="display:none;"></div>
            <div class="iff-results" id="iff-results"></div>
        </div>
    </div>
</div>
<?php endif; ?>

<script>
function ipamFilter() {
    var q    = (document.getElementById('ipam-f-q')?.value   || '').toLowerCase().trim();
    var vlan = (document.getElementById('ipam-f-vlan')?.value || '');
    var loc  = (document.getElementById('ipam-f-loc')?.value  || '').toLowerCase();
    var active = q || vlan || loc;

    var cards = document.querySelectorAll('#ipam-grid .ipam-card');
    var rows  = document.querySelectorAll('#ipam-list .ipam-list-row');
    var shown = 0;

    function matchItem(el) {
        var matchQ = !q   || el.dataset.name.includes(q) || el.dataset.network.includes(q) || el.dataset.desc.includes(q);
        var matchV = !vlan || el.dataset.vlan === vlan;
        var matchL = !loc  || el.dataset.loc  === loc;
        return matchQ && matchV && matchL;
    }

    cards.forEach(function(c) {
        var show = matchItem(c);
        c.style.display = show ? '' : 'none';
        if (show) shown++;
    });
    rows.forEach(function(r) {
        r.style.display = matchItem(r) ? '' : 'none';
    });

    var countEl = document.getElementById('ipam-f-count');
    if (countEl) countEl.textContent = active ? (shown + ' / ' + cards.length + ' Netze') : '';

    var resetBtn = document.getElementById('ipam-f-reset');
    if (resetBtn) resetBtn.style.display = active ? '' : 'none';

    var noRes = document.getElementById('ipam-no-results');
    if (noRes) noRes.style.display = (active && shown === 0) ? '' : 'none';
}

function ipamFilterReset() {
    ['ipam-f-q','ipam-f-vlan','ipam-f-loc'].forEach(function(id) {
        var el = document.getElementById(id);
        if (el) el.value = '';
    });
    ipamFilter();
}

// ---- Freiraum-Suche (freie Subnetz-Blöcke innerhalb eines Bereichs) ----
function iffEsc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }

function iffRefreshWanted() {
    var sel = document.getElementById('iff-wanted');
    if (!sel) return;
    var parent = parseInt(document.getElementById('iff-cidr').value);
    if (isNaN(parent) || parent < 0) parent = 0;
    var prev = parseInt(sel.value);
    var html = '';
    for (var c = Math.max(parent, 1); c <= 32; c++) {
        html += '<option value="' + c + '">/' + c + '</option>';
    }
    sel.innerHTML = html;
    // sinnvolle Vorauswahl: vorherige Wahl beibehalten, sonst /24 (bzw. Bereichsgröße)
    var def = (!isNaN(prev) && prev >= parent && prev <= 32) ? prev : Math.max(parent, 24);
    sel.value = String(def);
}

function iffParent() {
    var v = document.getElementById('iff-parent').value;
    if (!v) return;
    var parts = v.split('|');
    document.getElementById('iff-network').value = parts[0] || '';
    document.getElementById('iff-cidr').value    = parts[1] || '';
    iffRefreshWanted();
}

function iffOpen() {
    document.getElementById('iff-results').innerHTML = '';
    var hint = document.getElementById('iff-hint');
    if (hint) hint.style.display = 'none';
    iffRefreshWanted();
    document.getElementById('iffModal').classList.add('open');
}
function iffClose() { document.getElementById('iffModal').classList.remove('open'); }

function iffSearch() {
    var network = document.getElementById('iff-network').value.trim();
    var cidr    = document.getElementById('iff-cidr').value.trim();
    var wanted  = document.getElementById('iff-wanted').value;
    var res     = document.getElementById('iff-results');
    var hint    = document.getElementById('iff-hint');
    if (!network) { res.innerHTML = '<p class="iff-hint">Bitte einen Bereich angeben.</p>'; return; }
    res.innerHTML = '<p class="iff-hint">Suche …</p>';
    hint.style.display = 'none';

    var url = '/ipam/find-free?network=' + encodeURIComponent(network)
            + '&cidr=' + encodeURIComponent(cidr) + '&wanted=' + encodeURIComponent(wanted);
    fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(d){
            if (!d.ok) { res.innerHTML = '<p class="iff-hint" style="color:#dc2626;">' + iffEsc(d.error || 'Fehler.') + '</p>'; return; }
            if (!d.free || d.free.length === 0) {
                res.innerHTML = '<p class="iff-hint">Kein freier Block dieser Größe im Bereich gefunden.</p>';
                return;
            }
            var rows = d.free.map(function(b){
                var label = iffEsc(b.network) + '/' + b.cidr;
                var createUrl = '/ipam/subnets/create?network=' + encodeURIComponent(b.network) + '&cidr=' + encodeURIComponent(b.cidr);
                return '<div class="iff-res-row"><span class="iff-res-net">' + label + '</span>'
                     + '<a class="btn btn-default btn-sm" href="' + createUrl + '">Hier anlegen</a></div>';
            }).join('');
            res.innerHTML = rows;
            if (hint) {
                var total = d.total_free || d.free.length;
                hint.textContent = d.truncated
                    ? ('Mindestens ' + total + ' freie Blöcke – die ersten ' + d.free.length + ' werden angezeigt.')
                    : (total + ' freie Blöcke gefunden.');
                hint.style.display = '';
            }
        })
        .catch(function(e){ res.innerHTML = '<p class="iff-hint" style="color:#dc2626;">Fehler: ' + iffEsc(e.message) + '</p>'; });
}

(function(){
    var m = document.getElementById('iffModal');
    if (!m) return;
    m.addEventListener('click', function(e){ if (e.target === m) iffClose(); });
    document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && m.classList.contains('open')) iffClose(); });
})();

// View Toggle
(function () {
    var gridEl = document.getElementById('ipam-grid');
    var listEl = document.getElementById('ipam-list');
    var treeEl = document.getElementById('ipam-tree');
    var btns   = document.querySelectorAll('.ipam-view-btn');
    var STORAGE_KEY = 'ipam_view_mode';

    function applyMode(mode) {
        // Baumansicht nur möglich, wenn der Baum existiert (nicht während der Suche)
        if (mode === 'tree' && !treeEl) mode = 'cards';
        btns.forEach(function(b) { b.classList.toggle('active', b.dataset.mode === mode); });
        gridEl && (gridEl.style.display = mode === 'cards' ? '' : 'none');
        listEl && (listEl.style.display = mode === 'list'  ? '' : 'none');
        treeEl && (treeEl.style.display = mode === 'tree'  ? '' : 'none');
        try { localStorage.setItem(STORAGE_KEY, mode); } catch(e) {}
    }

    try {
        var saved = localStorage.getItem(STORAGE_KEY);
        if (saved === 'list' || saved === 'tree') applyMode(saved);
    } catch(e) {}

    btns.forEach(function(btn) {
        btn.addEventListener('click', function() { applyMode(btn.dataset.mode); });
    });
}());
</script>