OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

subnet_form.php

20 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; }
.sf-wrap { padding:0; }


/* Body */
.sf-body { padding:1.5rem 1rem 3rem; background:var(--color-bg-alt); }
.sf-layout { display:grid; grid-template-columns:1fr 260px; gap:1.5rem; align-items:start; }

/* Cards */
.sf-card { background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.25rem; overflow:hidden; margin-bottom:1.25rem; }
.sf-card:last-child { margin-bottom:0; }
.sf-card-header {
    display:flex; align-items:center; gap:0.5rem;
    font-size:0.82rem; font-weight:700; text-transform:uppercase;
    letter-spacing:0.06em; color:var(--gray-500);
    padding:0.9rem 1.25rem; border-bottom:1px solid var(--gray-100); background:var(--gray-50);
}
.sf-card-header svg { color:var(--color-primary); flex-shrink:0; }
.sf-card-body { padding:1.25rem; display:flex; flex-direction:column; gap:1rem; }

/* Fields */
.sf-row-2 { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.sf-field { display:flex; flex-direction:column; gap:0.35rem; }
.sf-label { font-size:0.82rem; font-weight:600; color:var(--gray-700); }
.sf-req { color:var(--red-500); }
.sf-input { padding:0.55rem 0.75rem; border:1px solid var(--gray-300); border-radius:0.25rem;
    font-size:0.88rem; outline:none; font-family:inherit; background:var(--color-bg);
    transition:border-color 0.15s, box-shadow 0.15s; box-sizing:border-box; width:100%; }
.sf-input:focus { border-color:var(--color-primary); box-shadow:0 0 0 3px rgba(var(--color-primary-rgb), 0.15); }
.sf-hint { font-size:0.75rem; color:var(--gray-400); margin-top:0.15rem; line-height:1.4; }
textarea.sf-input { resize:vertical; min-height:90px; }

/* Color picker */
.sf-color-row { display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap; }
.sf-color-opt { width:26px; height:26px; border-radius:50%; border:2px solid transparent; cursor:pointer; transition:transform 0.1s, border-color 0.1s; flex-shrink:0; }
.sf-color-opt:hover { transform:scale(1.2); }
.sf-color-opt.selected { border-color:var(--slate-900); box-shadow:0 0 0 2px var(--color-bg) inset; }
.sf-color-custom-wrap { display:flex; align-items:center; gap:0.5rem; margin-top:0.25rem; }
.sf-color-swatch { width:26px; height:26px; border-radius:50%; border:2px solid var(--gray-300); cursor:pointer; flex-shrink:0; }
.sf-color-hex { width:90px; padding:0.3rem 0.5rem; border:1px solid var(--gray-300); border-radius:0.375rem; font-size:0.8rem; font-family:monospace; }

/* Actions */
.sf-actions { display:flex; gap:0.75rem; align-items:center; padding-top:0.25rem; }

/* Sidebar tip */
.sf-tip { background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.25rem; overflow:hidden; margin-bottom:1rem; }
.sf-tip:last-child { margin-bottom:0; }
.sf-tip-header { padding:0.75rem 1rem; border-bottom:1px solid var(--gray-100); background:var(--gray-50);
    font-size:0.75rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--gray-400); }
.sf-tip-body { padding:1rem; font-size:0.82rem; color:var(--gray-500); line-height:1.6; }
.sf-tip-body p { margin:0 0 0.5rem; }
.sf-tip-body p:last-child { margin:0; }
.sf-tip-body strong { color:var(--gray-700); }

@media(max-width:900px) {
    .sf-layout { grid-template-columns:1fr; }
}
@media(max-width:640px) {
    .sf-body { padding:1rem 0.75rem 2rem; }
    .sf-row-2 { grid-template-columns:1fr; }
}
</style>

<div class="sf-wrap">
    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1><?= $subnet ? 'Subnetz bearbeiten' : 'Subnetz anlegen' ?></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>
                <?php if ($subnet): ?>
                <li class="breadcrumb-item"><a href="/ipam/subnets/<?= $subnet['id'] ?>"><?= $this->e($subnet['name']) ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Bearbeiten') ?></li>
                <?php else: ?>
                <li class="breadcrumb-item active"><?= $this->te('Neues Subnetz') ?></li>
                <?php endif; ?>
            </ol>
        </div>
    </div>

    <div class="sf-body">
        <?php $action = $subnet ? '/ipam/subnets/' . $subnet['id'] : '/ipam/subnets'; ?>
        <form method="POST" action="<?= $action ?>">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">

            <div class="sf-layout">
                <div>
                    <!-- Netzwerk -->
                    <div class="sf-card">
                        <div class="sf-card-header">
                            <svg width="14" height="14" 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('Netzwerk') ?>
                        </div>
                        <div class="sf-card-body">
                            <div class="sf-field">
                                <label class="sf-label"><?= $this->te('Name') ?> <span class="sf-req">*</span></label>
                                <input class="sf-input" type="text" name="name" required
                                       placeholder="<?= $this->te('z.B. Server-LAN, Management-Netz') ?>"
                                       value="<?= $this->e($subnet['name'] ?? '') ?>">
                            </div>
                            <div class="sf-row-2">
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('Netzwerkadresse') ?> <span class="sf-req">*</span></label>
                                    <input class="sf-input" type="text" name="network" required
                                           placeholder="192.168.10.0"
                                           value="<?= $this->e($subnet['network'] ?? ($prefill['network'] ?? '')) ?>">
                                    <span class="sf-hint"><?= $this->te('Netzadresse ohne Präfixlänge') ?></span>
                                </div>
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('CIDR-Präfix') ?> <span class="sf-req">*</span></label>
                                    <input class="sf-input" type="number" name="cidr" required min="1" max="128"
                                           placeholder="24"
                                           value="<?= $this->e($subnet['cidr'] ?? ($prefill['cidr'] ?? '')) ?>">
                                    <span class="sf-hint"><?= $this->te('z.B. 24 für /24') ?></span>
                                </div>
                                <div class="sf-field">
                                    <label class="sf-label">VLAN</label>
                                    <input class="sf-input" type="text" name="vlan"
                                           placeholder="z.B. 10"
                                           value="<?= $this->e($subnet['vlan'] ?? '') ?>">
                                </div>
                                <div class="sf-field">
                                    <label for="f-3432-location" class="sf-label"><?= $this->te('Standort') ?></label>
                                    <?php
                                        $sfLocations = $locations ?? [];
                                        $sfLocation  = (string)($subnet['location'] ?? '');
                                        $sfKnown     = false;
                                        foreach ($sfLocations as $sfLoc) {
                                            if ((string)$sfLoc['name'] === $sfLocation) { $sfKnown = true; break; }
                                        }
                                    ?>
                                    <select id="f-3432-location" class="sf-input" name="location" data-searchable data-allow-custom>
                                        <option value=""><?= $this->te('— kein Standort —') ?></option>
                                        <?php foreach ($sfLocations as $sfLoc): ?>
                                        <option value="<?= $this->e($sfLoc['name']) ?>"
                                                data-depth="<?= (int)($sfLoc['_depth'] ?? 0) ?>"
                                                <?= $sfKnown && (string)$sfLoc['name'] === $sfLocation ? 'selected' : '' ?>>
                                            <?= $this->e($sfLoc['name']) ?>
                                        </option>
                                        <?php endforeach; ?>
                                        <?php if ($sfLocation !== '' && !$sfKnown): ?>
                                        <!-- Bisher frei eingetragener Wert bleibt wählbar -->
                                        <option value="<?= $this->e($sfLocation) ?>" data-custom="1" selected><?= $this->e($sfLocation) ?></option>
                                        <?php endif; ?>
                                    </select>
                                    <span class="sf-hint"><?= $this->te('Standort auswählen oder eigenen Text eintippen und übernehmen.') ?></span>
                                </div>
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('Gateway') ?></label>
                                    <input class="sf-input" type="text" name="gateway"
                                           placeholder="z.B. 192.168.10.1"
                                           value="<?= $this->e($subnet['gateway'] ?? '') ?>">
                                    <span class="sf-hint"><?= $this->te('Standard-Gateway des Netzes') ?></span>
                                </div>
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('DNS-Server') ?></label>
                                    <input class="sf-input" type="text" name="dns_servers"
                                           placeholder="z.B. 192.168.10.1, 1.1.1.1"
                                           value="<?= $this->e($subnet['dns_servers'] ?? '') ?>">
                                    <span class="sf-hint"><?= $this->te('Kommasepariert') ?></span>
                                </div>
                            </div>
                            <div class="sf-field">
                                <label class="sf-label"><?= $this->te('Beschreibung') ?></label>
                                <textarea class="sf-input" name="description" rows="3"
                                          placeholder="<?= $this->te('Zweck, Besonderheiten…') ?>"><?= $this->e($subnet['description'] ?? '') ?></textarea>
                            </div>
                        </div>
                    </div>

                    <!-- DHCP -->
                    <div class="sf-card">
                        <div class="sf-card-header">
                            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
                            <?= $this->te('DHCP-Bereich') ?>
                        </div>
                        <div class="sf-card-body">
                            <div class="sf-row-2">
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('Bereich Start') ?></label>
                                    <input class="sf-input" type="text" name="dhcp_range_start"
                                           placeholder="z.B. 192.168.10.100"
                                           value="<?= $this->e($subnet['dhcp_range_start'] ?? '') ?>">
                                    <span class="sf-hint"><?= $this->te('Erste DHCP-Adresse (wird beim Scan übersprungen)') ?></span>
                                </div>
                                <div class="sf-field">
                                    <label class="sf-label"><?= $this->te('Bereich Ende') ?></label>
                                    <input class="sf-input" type="text" name="dhcp_range_end"
                                           placeholder="z.B. 192.168.10.200"
                                           value="<?= $this->e($subnet['dhcp_range_end'] ?? '') ?>">
                                    <span class="sf-hint"><?= $this->te('Letzte DHCP-Adresse (wird beim Scan übersprungen)') ?></span>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Scan -->
                    <div class="sf-card">
                        <div class="sf-card-header">
                            <svg width="14" height="14" 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>
                            <?= $this->te('Netzwerk-Scan') ?>
                        </div>
                        <div class="sf-card-body">
                            <label style="display:flex;align-items:center;gap:0.6rem;cursor:pointer;">
                                <input type="checkbox" name="scan_enabled" value="1"
                                       style="width:1rem;height:1rem;"
                                       <?= (!isset($subnet['scan_enabled']) || (int)$subnet['scan_enabled'] === 1) ? 'checked' : '' ?>>
                                <span class="sf-label" style="margin:0;"><?= $this->te('Dieses Subnetz scannen') ?></span>
                            </label>
                            <span class="sf-hint">
                                <?= $this->te('Steuert, ob der Remote-Scan-Agent dieses Netz scannt. Deaktivieren für Netze, die der Agent nicht abfragen soll (z.&nbsp;B. Fremdnetze oder rein dokumentierte Bereiche).') ?>
                            </span>
                        </div>
                    </div>

                    <!-- Farbe -->
                    <div class="sf-card">
                        <div class="sf-card-header">
                            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 010 20"/></svg>
                            <?= $this->te('Farbe') ?>
                        </div>
                        <div class="sf-card-body">
                            <div class="sf-field">
                                <label class="sf-label"><?= $this->te('Vorschläge') ?></label>
                                <div class="sf-color-row" id="colorPicker">
                                    <?php
                                    $colors = ['#3c8dbc','#7c3aed','#db2777','#dc2626','#d97706','#16a34a','#0891b2','#0f172a','#6b7280','#3c8dbc','#059669','#9333ea','#ea580c','#0284c7','#be123c'];
                                    $current = $subnet['color'] ?? '#3c8dbc';
                                    foreach ($colors as $c): ?>
                                    <div class="sf-color-opt <?= $c === $current ? 'selected' : '' ?>"
                                         style="background:<?= $c ?>;"
                                         data-color="<?= $c ?>"
                                         onclick="selectColor('<?= $c ?>', this)"
                                         title="<?= $c ?>"></div>
                                    <?php endforeach; ?>
                                </div>
                            </div>
                            <div class="sf-field">
                                <label class="sf-label"><?= $this->te('Eigene Farbe') ?></label>
                                <div class="sf-color-custom-wrap">
                                    <input type="color" id="colorNative" class="sf-color-swatch"
                                           value="<?= $this->e($current) ?>"
                                           oninput="onNativeColor(this.value)"
                                           title="<?= $this->te('Farbwähler öffnen') ?>">
                                    <input type="text" id="colorHex" class="sf-color-hex" maxlength="7"
                                           value="<?= $this->e($current) ?>"
                                           placeholder="<?= $this->te('#3c8dbc') ?>"
                                           oninput="onHexInput(this.value)">
                                    <span class="sf-hint" style="margin:0;"><?= $this->te('Hex-Code eingeben oder Wähler nutzen') ?></span>
                                </div>
                                <input type="hidden" name="color" id="colorInput" value="<?= $this->e($current) ?>">
                            </div>
                        </div>
                    </div>

                    <div class="sf-actions">
                        <button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
                        <a href="<?= $subnet ? '/ipam/subnets/' . $subnet['id'] : '/ipam' ?>" class="btn btn-default"><?= $this->te('Abbrechen') ?></a>
                    </div>
                </div>

                <!-- Sidebar -->
                <div>
                    <div class="sf-tip">
                        <div class="sf-tip-header"><?= $this->te('Hinweise') ?></div>
                        <div class="sf-tip-body">
                            <p><?= $this->t('<strong>Netzwerkadresse</strong> ist die Basisadresse ohne Host-Teil (z.B. <code>192.168.1.0</code>).') ?></p>
                            <p><?= $this->t('<strong>CIDR-Präfix</strong> gibt die Länge der Netzmaske an (z.B. <code>24</code> = 254 Hosts).') ?></p>
                            <p><?= $this->t('<strong>DHCP-Bereich</strong> wird beim automatischen IP-Scan übersprungen.') ?></p>
                        </div>
                    </div>
                    <div class="sf-tip">
                        <div class="sf-tip-header"><?= $this->te('IP-Scanner') ?></div>
                        <div class="sf-tip-body">
                            <p><?= $this->t('Der Scan wird <strong>serverseitig</strong> ausgeführt – er erreicht Netze, auf die der Server Zugriff hat.') ?></p>
                            <p><?= $this->t('Maximal <strong>/22</strong> (1024 Hosts) pro Scan unterstützt.') ?></p>
                        </div>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>

<script>
var currentColor = <?= json_encode($current) ?>;

function selectColor(hex, el) {
    currentColor = hex;
    document.querySelectorAll('.sf-color-opt').forEach(function(e) { e.classList.remove('selected'); });
    el.classList.add('selected');
    document.getElementById('colorInput').value = hex;
    document.getElementById('colorHex').value = hex;
    document.getElementById('colorNative').value = hex;
}

function onNativeColor(hex) {
    currentColor = hex;
    document.getElementById('colorInput').value = hex;
    document.getElementById('colorHex').value = hex;
    // Deselect preset swatches
    document.querySelectorAll('.sf-color-opt').forEach(function(e) { e.classList.remove('selected'); });
    // Highlight matching preset if any
    document.querySelectorAll('.sf-color-opt').forEach(function(e) {
        if (e.dataset.color.toLowerCase() === hex.toLowerCase()) e.classList.add('selected');
    });
}

function onHexInput(val) {
    val = val.trim();
    if (/^#[0-9a-fA-F]{6}$/.test(val)) {
        currentColor = val;
        document.getElementById('colorInput').value = val;
        document.getElementById('colorNative').value = val;
        document.querySelectorAll('.sf-color-opt').forEach(function(e) { e.classList.remove('selected'); });
        document.querySelectorAll('.sf-color-opt').forEach(function(e) {
            if (e.dataset.color.toLowerCase() === val.toLowerCase()) e.classList.add('selected');
        });
    }
}
</script>