OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

subnet_show.php

39 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<?php
$util = $utilization;
$utilColor = function(?int $pct): string {
    if ($pct === null) return 'var(--gray-400)';
    if ($pct >= 90) return 'var(--red-600)';
    if ($pct >= 75) return 'var(--amber-600)';
    if ($pct >= 50) return 'var(--color-primary)';
    return 'var(--green-600)';
};
$freeCount = ($capacity['usable'] !== null) ? max(0, $capacity['usable'] - $used_count) : null;
?>
<style>
a { text-decoration:none; } a:hover { text-decoration:none; }

/* Übersichts-Karten oben */
.sn-overview { display:grid; grid-template-columns:1.4fr 1fr; gap:1rem; margin-bottom:1.25rem; }
@media(max-width:900px){ .sn-overview { grid-template-columns:1fr; } }
.sn-overview .detail-card { margin-bottom:0; }
.sn-overview .detail-card-title,
.sn-map-card-title { display:inline-flex; align-items:center; gap:0.4rem; }

/* Auslastungs-Panel */
.sn-util-top { display:flex; align-items:flex-end; justify-content:space-between; margin-bottom:0.6rem; }
.sn-util-pct { font-size:2rem; font-weight:800; line-height:1; color:var(--gray-900); }
.sn-util-pct small { font-size:0.9rem; font-weight:700; color:var(--gray-400); }
.sn-util-bar { height:10px; background:var(--slate-100); border-radius:9999px; overflow:hidden; }
.sn-util-fill { height:100%; border-radius:9999px; transition:width .5s ease; }
.sn-util-legend { display:flex; gap:1.25rem; margin-top:0.85rem; flex-wrap:wrap; }
.sn-util-stat { display:flex; flex-direction:column; gap:0.1rem; }
.sn-util-stat-v { font-size:1.05rem; font-weight:800; color:var(--gray-900); }
.sn-util-stat-l { font-size:0.68rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-400); font-weight:700; }

/* Meta-Panel */
.sn-meta-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.85rem 1.25rem; }
.sn-meta-row { display:flex; flex-direction:column; gap:0.1rem; }
.sn-meta-l { font-size:0.68rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-400); font-weight:700; }
.sn-meta-v { font-size:0.86rem; color:var(--gray-700); }
.sn-meta-mono { font-family:monospace; }

/* Toolbar */
.sn-toolbar { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:1rem; flex-wrap:wrap; }
.sn-filter { display:flex; gap:0.4rem; flex-wrap:wrap; }
.sn-filter-btn { padding:0.25rem 0.75rem; border-radius:0.25rem; font-size:0.78rem; font-weight:600;
    border:1px solid var(--color-gray-400); background:var(--color-bg); color:var(--color-gray-700); cursor:pointer; transition:all 0.1s; text-decoration:none; }
.sn-filter-btn.active, .sn-filter-btn:hover { background:var(--color-primary); color:var(--color-on-accent); border-color:var(--color-primary); }
.sn-tool-right { display:flex; gap:0.4rem; align-items:center; }

/* IP-Karte (Visualisierung) */
.sn-map-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(34px, 1fr)); gap:4px; }
.sn-map-cell { aspect-ratio:1/1; border-radius:4px; display:flex; align-items:center; justify-content:center;
    font-size:0.6rem; font-weight:700; cursor:default; position:relative; user-select:none;
    border:1px solid transparent; transition:transform .08s, box-shadow .08s; min-height:30px; }
.sn-map-cell:hover { transform:scale(1.12); box-shadow:0 2px 8px rgba(0,0,0,.18); z-index:2; }
.sn-map-cell.is-link { cursor:pointer; }
.sn-cell-free      { background:var(--gray-100); color:var(--gray-400); border-color:var(--gray-200); }
.sn-cell-used      { background:var(--color-primary); color:var(--color-on-accent); }
.sn-cell-used.s-reserved { background:var(--amber-600); }
.sn-cell-used.s-offline  { background:var(--gray-400); }
.sn-cell-used.s-free     { background:var(--color-primary-pale); color:var(--color-primary-dark); border-color:var(--color-primary-muted); }
.sn-cell-dhcp      { background:var(--color-primary-pale); color:var(--color-primary-dark); border:1px dashed var(--color-primary-light); }
.sn-cell-gateway   { background:var(--violet-600); color:var(--color-on-accent); }
.sn-gw-suggest { display:inline-flex; align-items:center; gap:0.5rem; flex-wrap:wrap; }
.sn-gw-tag { font-size:0.66rem; font-weight:700; text-transform:uppercase; letter-spacing:0.03em; background:var(--violet-100); color:var(--violet-700); border-radius:9999px; padding:0.1rem 0.45rem; }
.sn-gw-apply { font-size:0.72rem; font-weight:600; cursor:pointer; border:1px solid var(--violet-600); background:var(--violet-600); color:var(--color-on-accent); border-radius:0.35rem; padding:0.15rem 0.55rem; transition:background .12s; }
.sn-gw-apply:hover { background:var(--violet-700); }
.sn-cell-network, .sn-cell-broadcast { background:var(--gray-800); color:var(--slate-300); }
.sn-map-legend { display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.9rem; font-size:0.74rem; color:var(--gray-500); }
.sn-leg { display:inline-flex; align-items:center; gap:0.35rem; }
.sn-leg-swatch { width:13px; height:13px; border-radius:3px; display:inline-block; }
.sn-map-octet { grid-column:1/-1; font-size:0.68rem; font-weight:700; color:var(--gray-400); text-transform:uppercase; letter-spacing:0.05em; margin-top:0.5rem; padding-top:0.4rem; border-top:1px dashed var(--slate-100); }
.sn-map-octet:first-child { margin-top:0; padding-top:0; border-top:none; }

/* Tabelle: Standard .table-wrapper/.data-table aus app.css */
.sn-ip { font-family:monospace; font-weight:700; color:var(--gray-900); }
.sn-host { color:var(--gray-700); }
.sn-type-icon { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:0.375rem; background:var(--gray-100); }

/* Confirm-Modal: zentrale .modal-*-Komponenten aus app.css; lokal nur Zustands-Toggle und Maße */
.ipam-modal { display:none; z-index:1080; }
.ipam-modal.open { display:flex; }
.ipam-modal .modal-card { max-width:420px; }
.ipam-modal-text { font-size:0.88rem; color:var(--gray-500); margin:0; line-height:1.5; }

@media(max-width:960px) {
    .sn-meta-grid { grid-template-columns:1fr; }
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>

<div>
    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1>
                <span style="display:inline-block;width:12px;height:12px;background:<?= $this->e($subnet['color']) ?>;border-radius:50%;margin-right:0.5rem;vertical-align:middle;"></span>
                <?= $this->e($subnet['name']) ?>
            </h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item"><a href="/ipam">IPAM</a></li>
                <li class="breadcrumb-item active"><?= $this->e($subnet['name']) ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <?php if ($can_manage_ips): ?>
            <?php if ($next_free_ip): ?>
            <a href="/ipam/subnets/<?= $subnet['id'] ?>/addresses/create?ip=<?= urlencode($next_free_ip) ?>" class="btn btn-default btn-sm" title="<?= $this->te('Erste freie Adresse: {adresse}', ['adresse' => $next_free_ip]) ?>">
                <i class="fas fa-magic"></i> <?= $this->te('Nächste freie IP') ?>
            </a>
            <?php endif; ?>
            <a href="/ipam/subnets/<?= $subnet['id'] ?>/addresses/create" class="btn btn-primary btn-sm">
                <i class="fas fa-plus"></i> <?= $this->te('IP anlegen') ?>
            </a>
            <?php endif; ?>
            <?php if (!empty($addresses)): ?>
            <a href="/ipam/subnets/<?= $subnet['id'] ?>/export" class="btn btn-default btn-sm" title="<?= $this->te('Adressen als CSV exportieren') ?>">
                <i class="fas fa-file-csv"></i> <?= $this->te('Export') ?>
            </a>
            <?php endif; ?>
            <?php if ($can_manage_subnets): ?>
            <button type="button" class="btn btn-default btn-sm" id="scanBtn" onclick="startScan()">
                <i class="fas fa-search"></i> <?= $this->te('Scan starten') ?>
            </button>
            <a href="/ipam/subnets/<?= $subnet['id'] ?>/edit" class="btn btn-default btn-sm"><?= $this->te('Bearbeiten') ?></a>
            <button type="button" class="btn btn-danger btn-sm"
                    onclick="ipamConfirm('Subnetz löschen?', 'Das Subnetz und alle <?= count($addresses) ?> erfassten IP-Adressen werden unwiderruflich gelöscht.', function(){ document.getElementById('snDeleteForm').submit(); })">
                <?= $this->te('Löschen') ?>
            </button>
            <form method="POST" action="/ipam/subnets/<?= $subnet['id'] ?>/delete" id="snDeleteForm" style="display:none;">
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            </form>
            <?php endif; ?>
        </div>
    </div>

    <div class="page-body">

        <!-- Übersicht: Auslastung + Netzdaten -->
        <div class="sn-overview">
            <!-- Auslastung -->
            <div class="detail-card">
                <div class="detail-card-header">
                    <span class="detail-card-title" style="display:inline-flex;align-items:center;gap:0.4rem;">
                        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg>
                        <?= $this->te('Auslastung') ?>
                    </span>
                </div>
                <div class="detail-card-body">
                    <?php if ($capacity['usable'] !== null): ?>
                    <div class="sn-util-top">
                        <div class="sn-util-pct"><?= $util ?? 0 ?><small>%</small></div>
                        <div style="text-align:right;font-size:0.78rem;color:var(--gray-500);">
                            <?= $this->te('{belegt} belegt / {nutzbar} nutzbar', ['belegt' => (int)$used_count, 'nutzbar' => number_format((int)$capacity['usable'], 0, ',', '.')]) ?>
                        </div>
                    </div>
                    <div class="sn-util-bar">
                        <div class="sn-util-fill" style="width:<?= $util ?? 0 ?>%;background:<?= $utilColor($util) ?>;"></div>
                    </div>
                    <div class="sn-util-legend">
                        <div class="sn-util-stat"><span class="sn-util-stat-v" style="color:var(--color-primary);"><?= (int)$used_count ?></span><span class="sn-util-stat-l"><?= $this->te('Belegt') ?></span></div>
                        <div class="sn-util-stat"><span class="sn-util-stat-v" style="color:var(--green-600);"><?= (int)$freeCount ?></span><span class="sn-util-stat-l"><?= $this->te('Frei') ?></span></div>
                        <div class="sn-util-stat"><span class="sn-util-stat-v"><?= number_format((int)$capacity['usable'], 0, ',', '.') ?></span><span class="sn-util-stat-l"><?= $this->te('Nutzbar') ?></span></div>
                    </div>
                    <?php else: ?>
                    <p style="font-size:0.85rem;color:var(--gray-400);margin:0;">
                        <?= $capacity['is_ipv6'] ? 'IPv6-Netz – Auslastung wird nicht abgezählt.' : 'Auslastung für dieses Netz nicht berechenbar.' ?>
                        <br><strong style="color:var(--gray-700);"><?= (int)$used_count ?></strong> <?= $this->te('Adressen erfasst.') ?>
                    </p>
                    <?php endif; ?>
                </div>
            </div>

            <!-- Netzdaten -->
            <div class="detail-card">
                <div class="detail-card-header">
                    <span class="detail-card-title" style="display:inline-flex;align-items:center;gap:0.4rem;">
                        <svg width="13" height="13" 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>
                        <?= $this->te('Netzdaten') ?>
                    </span>
                </div>
                <div class="detail-card-body">
                    <div class="sn-meta-grid">
                        <div class="sn-meta-row"><span class="sn-meta-l"><?= $this->te('Netz / CIDR') ?></span><span class="sn-meta-v sn-meta-mono"><?= $this->e($subnet['network']) ?>/<?= $subnet['cidr'] ?></span></div>
                        <?php if ($capacity['usable'] !== null): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l"><?= $this->te('Host-Bereich') ?></span><span class="sn-meta-v sn-meta-mono"><?= $this->e($capacity['first']) ?> – <?= $this->e($capacity['last']) ?></span></div>
                        <?php endif; ?>
                        <?php if (!empty($subnet['gateway'])): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l"><?= $this->te('Gateway') ?></span><span class="sn-meta-v sn-meta-mono"><?= $this->e($subnet['gateway']) ?></span></div>
                        <?php elseif (!empty($subnet['gateway_suggested'])): ?>
                        <div class="sn-meta-row">
                            <span class="sn-meta-l"><?= $this->te('Gateway') ?></span>
                            <span class="sn-meta-v sn-gw-suggest">
                                <span class="sn-meta-mono"><?= $this->e($subnet['gateway_suggested']) ?></span>
                                <span class="sn-gw-tag"><?= $this->te('Scan-Vorschlag') ?></span>
                                <?php if (!empty($can_manage_subnets)): ?>
                                <form method="POST" action="/ipam/subnets/<?= (int)$subnet['id'] ?>/apply-gateway" style="margin:0;">
                                    <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                                    <button type="submit" class="sn-gw-apply" title="<?= $this->te('Vorgeschlagenes Gateway übernehmen') ?>"><?= $this->te('Übernehmen') ?></button>
                                </form>
                                <?php endif; ?>
                            </span>
                        </div>
                        <?php endif; ?>
                        <?php if (!empty($subnet['dns_servers'])): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l">DNS</span><span class="sn-meta-v sn-meta-mono"><?= $this->e($subnet['dns_servers']) ?></span></div>
                        <?php endif; ?>
                        <?php if (!empty($subnet['vlan'])): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l">VLAN</span><span class="sn-meta-v"><?= $this->e($subnet['vlan']) ?></span></div>
                        <?php endif; ?>
                        <?php if (!empty($subnet['location'])): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l"><?= $this->te('Standort') ?></span><span class="sn-meta-v"><?= $this->e($subnet['location']) ?></span></div>
                        <?php endif; ?>
                        <?php if (!empty($subnet['dhcp_range_start']) && !empty($subnet['dhcp_range_end'])): ?>
                        <div class="sn-meta-row"><span class="sn-meta-l"><?= $this->te('DHCP-Bereich') ?></span><span class="sn-meta-v sn-meta-mono"><?= $this->e($subnet['dhcp_range_start']) ?> – <?= $this->e($subnet['dhcp_range_end']) ?></span></div>
                        <?php endif; ?>
                        <div class="sn-meta-row">
                            <span class="sn-meta-l"><?= $this->te('Remote-Scan') ?></span>
                            <span class="sn-meta-v">
                                <?php if (!empty($subnet['scan_enabled'])): ?>
                                    <span class="status-pill status-pill--success"><?= $this->te('Aktiviert') ?></span>
                                <?php else: ?>
                                    <span class="status-pill status-pill--neutral"><?= $this->te('Deaktiviert') ?></span>
                                <?php endif; ?>
                            </span>
                        </div>
                    </div>
                    <?php if (!empty($subnet['description'])): ?>
                    <div style="margin-top:0.9rem;padding-top:0.85rem;border-top:1px solid var(--gray-100);font-size:0.84rem;color:var(--gray-500);line-height:1.5;"><?= nl2br($this->e($subnet['description'])) ?></div>
                    <?php endif; ?>
                </div>
            </div>
        </div>

        <!-- Scan-Ergebnis (wird via JS befüllt) -->
        <div id="scanResult" style="display:none;margin-bottom:1rem;"></div>

        <!-- Visuelle IP-Karte -->
        <?php if ($address_map !== null && !empty($address_map)): ?>
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title" style="display:inline-flex;align-items:center;gap:0.4rem;">
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
                    <?= $this->te('IP-Karte') ?>
                </span>
                <span style="font-size:0.8rem;font-weight:600;color:var(--gray-400);"><?= $this->te('{anzahl} Adressen', ['anzahl' => count($address_map)]) ?></span>
            </div>
            <div class="detail-card-body">
                <div class="sn-map-grid">
                    <?php
                    $lastOctetGroup = null;
                    foreach ($address_map as $cell):
                        $parts = explode('.', $cell['ip']);
                        $octet = count($parts) === 4 ? ($parts[0].'.'.$parts[1].'.'.$parts[2].'.x') : null;
                        // Bei großen Netzen Gruppentrenner je /24 anzeigen
                        if (count($address_map) > 256 && $octet !== null && $octet !== $lastOctetGroup):
                            $lastOctetGroup = $octet;
                            echo '<div class="sn-map-octet">' . htmlspecialchars($octet, ENT_QUOTES) . '</div>';
                        endif;

                        $st   = $cell['status'];
                        $addr = $cell['address'];
                        $host = $parts[3] ?? $cell['ip'];
                        $cls  = 'sn-cell-' . $st;
                        $title = $cell['ip'];
                        $link  = null;

                        if ($st === 'used') {
                            $cls .= ' s-' . ($addr['status'] ?? 'active');
                            $title = $cell['ip'] . ($addr['hostname'] ? ' · ' . $addr['hostname'] : '') . ' · ' . ($status_labels[$addr['status']] ?? $addr['status']);
                            $link  = '/ipam/addresses/' . $addr['id'];
                        } elseif ($st === 'free') {
                            $title = $cell['ip'] . ' · frei';
                            if ($can_manage_ips) $link = '/ipam/subnets/' . $subnet['id'] . '/addresses/create?ip=' . urlencode($cell['ip']);
                        } elseif ($st === 'dhcp') {
                            $title = $cell['ip'] . ' · DHCP-Bereich';
                        } elseif ($st === 'gateway') {
                            $title = $cell['ip'] . ' · Gateway'
                                . ($addr && !empty($addr['hostname']) ? ' · ' . $addr['hostname'] : '');
                            if ($addr) {
                                $link = '/ipam/addresses/' . $addr['id'];
                            }
                        } elseif ($st === 'network') {
                            $title = $cell['ip'] . ' · Netzadresse';
                        } elseif ($st === 'broadcast') {
                            $title = $cell['ip'] . ' · Broadcast';
                        }
                    ?>
                        <?php if ($link): ?>
                        <a href="<?= $link ?>" class="sn-map-cell is-link <?= $cls ?>" title="<?= htmlspecialchars($title, ENT_QUOTES) ?>"><?= htmlspecialchars($host, ENT_QUOTES) ?></a>
                        <?php else: ?>
                        <span class="sn-map-cell <?= $cls ?>" title="<?= htmlspecialchars($title, ENT_QUOTES) ?>"><?= htmlspecialchars($host, ENT_QUOTES) ?></span>
                        <?php endif; ?>
                    <?php endforeach; ?>
                </div>
                <div class="sn-map-legend">
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--color-primary);"></span> <?= $this->te('Belegt') ?></span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--amber-600);"></span> <?= $this->te('Reserviert') ?></span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--gray-400);"></span> <?= $this->te('Offline') ?></span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--gray-100);border:1px solid var(--gray-200);"></span> <?= $this->te('Frei') ?></span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--color-primary-pale);border:1px dashed var(--color-primary-light);"></span> DHCP</span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--violet-600);"></span> <?= $this->te('Gateway') ?></span>
                    <span class="sn-leg"><span class="sn-leg-swatch" style="background:var(--gray-800);"></span> <?= $this->te('Netz/Broadcast') ?></span>
                </div>
            </div>
        </div>
        <?php endif; ?>

        <!-- Filter-Toolbar -->
        <div class="sn-toolbar">
            <div class="sn-filter" id="filterBar">
                <a href="#" class="sn-filter-btn active" data-filter="all" onclick="filterRows('all',this);return false;"><?= $this->t('Alle') ?> (<?= count($addresses) ?>)</a>
                <?php
                    $counts = array_count_values(array_column($addresses, 'status'));
                    foreach (['active'=>'Aktiv','reserved'=>'Reserviert','offline'=>'Offline','free'=>'Frei'] as $k=>$lbl):
                        if (!isset($counts[$k])) continue;
                ?>
                <a href="#" class="sn-filter-btn" data-filter="<?= $k ?>" onclick="filterRows('<?= $k ?>',this);return false;"><?= $lbl ?> (<?= $counts[$k] ?>)</a>
                <?php endforeach; ?>
            </div>
        </div>

        <?php if (empty($addresses)): ?>
        <div class="empty-state">
            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" style="margin:0 auto 0.75rem;display:block;"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
            <p style="margin:0 0 0.75rem;font-size:0.9rem;"><?= $this->te('Noch keine IP-Adressen in diesem Subnetz.') ?></p>
            <?php if ($can_manage_ips): ?>
            <a href="/ipam/subnets/<?= $subnet['id'] ?>/addresses/create" style="color:var(--color-primary);font-size:0.85rem;font-weight:600;"><?= $this->te('Erste IP anlegen →') ?></a>
            <?php endif; ?>
        </div>
        <?php else: ?>
        <div class="table-wrapper">
            <table class="data-table">
                <thead>
                    <tr>
                        <th scope="col"><?= $this->te('IP-Adresse') ?></th>
                        <th scope="col"><?= $this->te('Hostname') ?></th>
                        <th scope="col"><?= $this->te('Typ') ?></th>
                        <th scope="col"><?= $this->te('Status') ?></th>
                        <th scope="col"><?= $this->te('Verantwortlich') ?></th>
                        <th scope="col"><?= $this->te('Beschreibung') ?></th>
                        <th scope="col"></th>
                    </tr>
                </thead>
                <tbody id="ipTable">
                <?php foreach ($addresses as $a): ?>
                <?php
                    $typeIcons = [
                        'server'=>'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><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>',
                        'workstation'=>'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
                        'printer'=>'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>',
                        'switch'=>'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 11h.01M6 13h.01M10 11h.01"/></svg>',
                        'router'=>'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M4.93 4.93l2.83 2.83M16.24 7.76l2.83-2.83M4.93 19.07l2.83-2.83M16.24 16.24l2.83 2.83M2 12h2M20 12h2M12 2v2M12 20v2"/></svg>',
                    ];
                    $icon = $typeIcons[$a['type']] ?? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>';
                    $pillMap = ['active' => 'status-pill--success', 'reserved' => 'status-pill--warning', 'offline' => 'status-pill--neutral', 'free' => 'status-pill--info'];
                    $badgeClass = $pillMap[$a['status']] ?? 'status-pill--neutral';
                    $isGw = !empty($subnet['gateway']) && $subnet['gateway'] === $a['ip_address'];
                ?>
                <tr data-status="<?= $a['status'] ?>">
                    <td>
                        <a href="/ipam/addresses/<?= $a['id'] ?>" class="sn-ip"><?= $this->e($a['ip_address']) ?></a>
                        <?php if ($isGw): ?><span class="status-pill status-pill--purple" style="margin-left:0.4rem;">GW</span><?php endif; ?>
                    </td>
                    <td class="sn-host"><?= $this->e($a['hostname'] ?? '–') ?></td>
                    <td>
                        <span class="sn-type-icon" title="<?= $this->e($type_labels[$a['type']] ?? $a['type']) ?>"><?= $icon ?></span>
                        <span style="font-size:0.78rem;color:var(--gray-500);margin-left:0.35rem;"><?= $this->e($type_labels[$a['type']] ?? $a['type']) ?></span>
                    </td>
                    <td><span class="status-pill <?= $badgeClass ?>"><?= $this->e($status_labels[$a['status']] ?? $a['status']) ?></span></td>
                    <td style="font-size:0.82rem;"><?= $this->e($a['responsible'] ?? '–') ?></td>
                    <td style="font-size:0.82rem;color:var(--gray-500);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= $this->e($a['description'] ?? '') ?></td>
                    <td style="text-align:right;">
                        <?php if ($can_manage_ips): ?>
                        <a href="/ipam/addresses/<?= $a['id'] ?>/edit" style="font-size:0.78rem;color:var(--gray-500);"><?= $this->te('Bearbeiten') ?></a>
                        <?php endif; ?>
                    </td>
                </tr>
                <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <?php endif; ?>
    </div>
</div>

<!-- Confirm-Modal -->
<div class="modal-overlay ipam-modal" id="ipamModal">
    <div class="modal-card">
        <div class="modal-header">
            <h2 id="ipamModalTitle"><?= $this->te('Bestätigen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" id="ipamModalClose">&times;</button>
        </div>
        <div class="modal-body">
            <p class="ipam-modal-text" id="ipamModalText"></p>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default btn-sm" id="ipamModalCancel"><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-danger btn-sm" id="ipamModalOk"><?= $this->te('Löschen') ?></button>
        </div>
    </div>
</div>

<script>
// ===== Confirm-Modal =====
var _ipamOnOk = null;
function ipamConfirm(title, text, onOk) {
    document.getElementById('ipamModalTitle').textContent = title || 'Bestätigen';
    document.getElementById('ipamModalText').textContent  = text || '';
    _ipamOnOk = onOk;
    document.getElementById('ipamModal').classList.add('open');
}
(function(){
    var m = document.getElementById('ipamModal');
    function close(){ m.classList.remove('open'); _ipamOnOk = null; }
    document.getElementById('ipamModalCancel').addEventListener('click', close);
    document.getElementById('ipamModalClose').addEventListener('click', close);
    document.getElementById('ipamModalOk').addEventListener('click', function(){ var f=_ipamOnOk; close(); if(f) f(); });
    m.addEventListener('click', function(e){ if(e.target === m) close(); });
    document.addEventListener('keydown', function(e){ if(e.key==='Escape' && m.classList.contains('open')) close(); });
})();

// ===== Status-Filter =====
function filterRows(status, el) {
    document.querySelectorAll('#filterBar .sn-filter-btn').forEach(function(b) { b.classList.remove('active'); });
    el.classList.add('active');
    document.querySelectorAll('#ipTable tr').forEach(function(row) {
        row.style.display = (status === 'all' || row.dataset.status === status) ? '' : 'none';
    });
}

// ===== Scanner =====
var _scanSource = null;

function startScan() {
    var btn = document.getElementById('scanBtn');
    var resultDiv = document.getElementById('scanResult');
    if (!btn || !resultDiv || btn.disabled) return;

    if (_scanSource) { _scanSource.close(); _scanSource = null; }

    btn.disabled = true;
    btn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 1s linear infinite"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg> Scan läuft…';

    var online = [], offline = [], dhcpSkipped = 0, scanned = 0;
    <?php
        $cidr = (int)$subnet['cidr'];
        $numHostsJs = $cidr <= 30 ? ((1 << (32 - $cidr)) - 2) : 0;
    ?>
    var total = <?= max(0, $numHostsJs) ?>;

    resultDiv.style.display = 'block';
    resultDiv.innerHTML = _scanProgressHtml(0, total, 0);

    _scanSource = new EventSource('/ipam/subnets/<?= $subnet['id'] ?>/scan-stream');

    _scanSource.onmessage = function(e) {
        var d;
        try { d = JSON.parse(e.data); } catch(ex) { return; }

        if (d.error) {
            _scanSource.close(); _scanSource = null;
            _resetScanBtn(btn);
            resultDiv.innerHTML = '<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:0.25rem;padding:0.75rem 1rem;color:#dc2626;font-size:0.83rem;">Fehler: ' + escHtml(d.error) + '</div>';
            return;
        }

        if (d.done) {
            _scanSource.close(); _scanSource = null;
            _resetScanBtn(btn);
            _showScanResults(online, offline, dhcpSkipped, d.network || '', d.gateway_suggestion || null);
            return;
        }

        if (d.skipped) {
            dhcpSkipped++;
        } else {
            scanned++;
            if (d.status === 'online') online.push({
                ip: d.ip,
                hostname: d.hostname || null,
                mac: d.mac || null,
                type: d.type || null,
                ports: d.ports || [],
                details: d.details || {}
            });
            else offline.push({ ip: d.ip, hostname: d.hostname || null });
        }
        resultDiv.innerHTML = _scanProgressHtml(scanned, total - dhcpSkipped, online.length);
    };

    _scanSource.onerror = function() {
        if (!_scanSource || _scanSource.readyState === EventSource.CLOSED) return;
        _scanSource.close(); _scanSource = null;
        _resetScanBtn(btn);
        resultDiv.innerHTML = '<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:0.25rem;padding:0.75rem 1rem;color:#dc2626;font-size:0.83rem;">Verbindung zum Scan unterbrochen.</div>';
    };
}

function _resetScanBtn(btn) {
    btn.disabled = false;
    btn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg> Scan starten';
}

function _scanProgressHtml(scanned, total, online) {
    var pct = total > 0 ? Math.round(scanned / total * 100) : 0;
    return '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:0.25rem;padding:1rem 1.25rem;">' +
        '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem;">' +
        '<span style="font-size:0.82rem;color:#374151;font-weight:600;">Scan läuft…</span>' +
        '<span style="font-size:0.78rem;color:#6b7280;">' + scanned + ' / ' + total + ' IPs &nbsp;·&nbsp; <span style="color:#15803d;font-weight:700;">' + online + ' online</span></span>' +
        '</div>' +
        '<div style="height:6px;background:#f3f4f6;border-radius:9999px;overflow:hidden;">' +
        '<div style="height:100%;width:' + pct + '%;background:linear-gradient(90deg,#3c8dbc,#7c3aed);border-radius:9999px;transition:width 0.2s;"></div>' +
        '</div>' +
        '</div>';
}

var _scanTypeLabels = {server:'Server', workstation:'Workstation', printer:'Drucker', switch:'Switch', router:'Router', ap:'Access Point', camera:'Kamera', phone:'Telefon/VoIP'};

function _scanHostTitle(h) {
    var lines = [];
    if (h.details) {
        for (var k in h.details) {
            if (Object.prototype.hasOwnProperty.call(h.details, k) && h.details[k]) lines.push(k + ': ' + h.details[k]);
        }
    }
    if (h.mac) lines.push('MAC: ' + h.mac);
    return lines.join('\n');
}

function _showScanResults(online, offline, dhcpSkipped, network, gateway) {
    var resultDiv = document.getElementById('scanResult');
    if (!resultDiv) return;

    var html = '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:0.25rem;overflow:hidden;">';
    html += '<div style="padding:0.875rem 1.25rem;background:#f9fafb;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:1.5rem;flex-wrap:wrap;">';
    html += '<strong style="font-size:0.82rem;color:#111827;">Scan-Ergebnis: ' + escHtml(network) + '</strong>';
    html += '<span style="font-size:0.78rem;padding:0.2rem 0.55rem;border-radius:9999px;background:#dcfce7;color:#15803d;font-weight:700;">' + online.length + ' Online</span>';
    html += '<span style="font-size:0.78rem;padding:0.2rem 0.55rem;border-radius:9999px;background:#f3f4f6;color:#6b7280;font-weight:700;">' + offline.length + ' Offline</span>';
    if (dhcpSkipped > 0) {
        html += '<span style="font-size:0.78rem;padding:0.2rem 0.55rem;border-radius:9999px;background:#e8f4fd;color:#367fa9;font-weight:700;">' + dhcpSkipped + ' DHCP übersprungen</span>';
    }
    if (gateway) {
        html += '<span style="font-size:0.78rem;padding:0.2rem 0.55rem;border-radius:9999px;background:#ede9fe;color:#7c3aed;font-weight:700;" title="Wird beim Import als Gateway-Vorschlag gespeichert">Gateway-Vorschlag: ' + escHtml(gateway) + '</span>';
    }
    html += '</div>';

    if (online.length > 0) {
        html += '<div style="padding:0.75rem 1.25rem;display:flex;flex-wrap:wrap;gap:0.5rem;">';
        online.forEach(function(h) {
            html += '<div style="display:inline-flex;flex-direction:column;background:#dcfce7;border-radius:0.375rem;padding:0.25rem 0.6rem;max-width:180px;" title="' + escHtml(_scanHostTitle(h)) + '">';
            html += '<span style="font-family:monospace;font-size:0.8rem;font-weight:700;color:#15803d;">' + escHtml(h.ip) + '</span>';
            if (h.hostname) {
                html += '<span style="font-size:0.68rem;color:#166534;opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="' + escHtml(h.hostname) + '">' + escHtml(h.hostname) + '</span>';
            }
            var meta = [];
            if (h.type && _scanTypeLabels[h.type]) meta.push(_scanTypeLabels[h.type]);
            if (h.ports && h.ports.length) meta.push(h.ports.length + ' Port' + (h.ports.length !== 1 ? 's' : ''));
            if (h.mac) meta.push('MAC');
            if (meta.length) {
                html += '<span style="font-size:0.65rem;color:#166534;opacity:0.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + escHtml(meta.join(' · ')) + '</span>';
            }
            html += '</div>';
        });
        html += '</div>';
        <?php if ($can_manage_ips): ?>
        html += '<div style="padding:0.6rem 1.25rem;border-top:1px solid #f3f4f6;display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">';
        html += '<button type="button" id="importScanBtn" class="btn btn-sm btn-primary">&#128229; Alle ' + online.length + ' importieren</button>';
        html += '<span id="importScanMsg" style="font-size:0.8rem;color:#6b7280;"></span>';
        html += '</div>';
        resultDiv.innerHTML = html + '</div>';
        document.getElementById('importScanBtn').addEventListener('click', function() { importScan(online, gateway); });
        return;
        <?php endif; ?>
    } else {
        html += '<div style="padding:0.875rem 1.25rem;font-size:0.83rem;color:#9ca3af;">Keine erreichbaren Hosts gefunden.</div>';
    }

    html += '</div>';
    resultDiv.innerHTML = html;
}

function importScan(ips, gateway) {
    var btn = document.getElementById('importScanBtn');
    var msg = document.getElementById('importScanMsg');
    if (!btn || !msg) return;
    btn.disabled = true;
    btn.textContent = 'Importiere…';
    msg.textContent = '';
    msg.style.color = '#6b7280';

    var csrf = document.querySelector('meta[name="csrf-token"]');
    fetch('/ipam/subnets/<?= $subnet['id'] ?>/import-scan', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrf ? csrf.content : '',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ ips: ips, gateway_suggestion: gateway || null })
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
        btn.disabled = false;
        if (data.success) {
            btn.textContent = '✓ Importiert';
            btn.style.background = '#16a34a';
            msg.textContent = data.message;
            msg.style.color = '#16a34a';
            if (data.created > 0) {
                setTimeout(function() { location.reload(); }, 1500);
            }
        } else {
            btn.textContent = 'Fehler';
            btn.style.background = '#dc2626';
            msg.textContent = data.error || 'Import fehlgeschlagen';
            msg.style.color = '#dc2626';
        }
    })
    .catch(function() {
        btn.disabled = false;
        btn.textContent = 'Fehler';
        msg.textContent = 'Verbindungsfehler';
        msg.style.color = '#dc2626';
    });
}

function escHtml(str) {
    var d = document.createElement('div');
    d.textContent = str || '';
    return d.innerHTML;
}
</script>