Quelltext des aktuellen Standes
<?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. 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>