Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<?php
$statusPills = ['active'=>['status-pill--success','status-dot--success','Aktiv'],'reserved'=>['status-pill--warning','status-dot--warning','Reserviert'],'offline'=>['status-pill--neutral','','Offline'],'free'=>['status-pill--info','','Frei']];
[$spill,$sdot,$slbl] = $statusPills[$address['status']] ?? ['status-pill--neutral','','?'];
?>
<style>
a { text-decoration:none; } a:hover { text-decoration:none; }
.addr-layout { display:grid; grid-template-columns:1fr 280px; gap:1.75rem; align-items:start; }
.addr-info-list { display:flex; flex-direction:column; gap:0.75rem; }
.addr-info-row { display:flex; flex-direction:column; gap:0.15rem; }
.addr-info-label { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-400); }
.addr-info-value { font-size:0.88rem; color:var(--gray-700); }
.addr-info-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
/* Detail-Liste */
.addr-detail-row { display:flex; gap:1rem; padding:0.6rem 0; border-bottom:1px solid var(--gray-50); align-items:baseline; }
.addr-detail-row:last-child { border-bottom:none; }
.addr-detail-key { font-size:0.78rem; font-weight:700; color:var(--gray-500); min-width:120px; flex-shrink:0; }
.addr-detail-val { font-size:0.88rem; color:var(--gray-700); flex:1; word-break:break-word; white-space:pre-wrap; }
/* Verlauf / Historie */
.addr-hist-list { display:flex; flex-direction:column; }
.addr-hist-item { padding:0.7rem 0; border-bottom:1px solid var(--gray-100); }
.addr-hist-item:last-child { border-bottom:none; }
.addr-hist-meta { display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap; margin-bottom:0.35rem; }
.addr-hist-badge { font-size:0.66rem; font-weight:700; text-transform:uppercase; letter-spacing:0.04em; padding:0.1rem 0.5rem; border-radius:9999px; }
.addr-hist-time { font-size:0.76rem; color:var(--gray-500); }
.addr-hist-user { font-size:0.76rem; color:var(--gray-400); }
.addr-hist-body { display:flex; flex-direction:column; gap:0.2rem; }
.addr-hist-change { font-size:0.83rem; color:var(--gray-700); line-height:1.5; }
.addr-hist-field { font-weight:600; color:var(--gray-500); }
.addr-hist-old { color:var(--gray-400); text-decoration:line-through; }
.addr-hist-arrow { color:var(--gray-400); margin:0 0.15rem; }
.addr-hist-new { font-weight:600; color:var(--gray-900); }
/* 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) {
.addr-layout { grid-template-columns:1fr; }
.addr-info-grid { grid-template-columns:1fr; }
}
</style>
<div>
<!-- Content Header -->
<div class="content-header">
<div>
<h1 style="font-family:monospace;"><?= $this->e($address['ip_address']) ?></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"><a href="/ipam/subnets/<?= $address['subnet_id'] ?>"><?= $this->e($address['subnet_name']) ?></a></li>
<li class="breadcrumb-item active"><?= $this->e($address['ip_address']) ?></li>
</ol>
</div>
<?php if ($can_manage_ips): ?>
<div class="content-header-actions">
<span class="status-pill <?= $spill ?>">
<span class="status-dot <?= $sdot ?>"></span>
<?= $slbl ?>
</span>
<a href="/ipam/addresses/<?= $address['id'] ?>/edit" class="btn btn-default btn-sm"><?= $this->te('Bearbeiten') ?></a>
<button type="button" class="btn btn-danger btn-sm"
onclick="ipamConfirm('IP-Adresse löschen?', 'Die Adresse <?= $this->e($address['ip_address']) ?> wird unwiderruflich gelöscht.', function(){ document.getElementById('addrDeleteForm').submit(); })">
<?= $this->te('Löschen') ?>
</button>
<form method="POST" action="/ipam/addresses/<?= $address['id'] ?>/delete" id="addrDeleteForm" style="display:none;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
</form>
</div>
<?php endif; ?>
</div>
<div class="page-body addr-layout">
<div>
<!-- Basisinfos -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Allgemein') ?></span></div>
<div class="detail-card-body">
<div class="addr-info-grid">
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('IP-Adresse') ?></span>
<span class="addr-info-value" style="font-family:monospace;font-weight:700;"><?= $this->e($address['ip_address']) ?></span>
</div>
<?php if ($address['hostname']): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Hostname') ?></span>
<span class="addr-info-value" style="font-weight:600;"><?= $this->e($address['hostname']) ?></span>
</div>
<?php endif; ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Typ') ?></span>
<span class="addr-info-value"><?= $this->e($type_labels[$address['type']] ?? $address['type']) ?></span>
</div>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Status') ?></span>
<span class="addr-info-value"><?= $this->e($status_labels[$address['status']] ?? $address['status']) ?></span>
</div>
<?php if ($address['mac_address']): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('MAC-Adresse') ?></span>
<span class="addr-info-value" style="font-family:monospace;"><?= $this->e($address['mac_address']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($address['asset_id'])): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Verknüpftes Asset') ?></span>
<span class="addr-info-value">
<a href="/inventory/<?= (int)$address['asset_id'] ?>"><?= $this->e($address['linked_asset_inventory'] ?? ('Asset #' . (int)$address['asset_id'])) ?></a>
<?php if (!empty($address['linked_asset_serial'])): ?>
<span style="color:var(--gray-500);">(S/N: <?= $this->e($address['linked_asset_serial']) ?>)</span>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<?php if ($address['responsible']): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Verantwortlich') ?></span>
<span class="addr-info-value"><?= $this->e($address['responsible']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($address['last_seen_at'])): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Zuletzt online') ?></span>
<span class="addr-info-value"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($address['last_seen_at']))]) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($address['subnet_gateway']) && $address['subnet_gateway'] === $address['ip_address']): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Rolle') ?></span>
<span class="addr-info-value"><span class="status-pill status-pill--purple"><?= $this->te('Gateway') ?></span></span>
</div>
<?php endif; ?>
</div>
<?php if ($address['description']): ?>
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--gray-100);">
<span class="addr-info-label"><?= $this->te('Beschreibung') ?></span>
<p style="font-size:0.88rem;color:var(--gray-700);margin:0.35rem 0 0;white-space:pre-line;"><?= $this->e($address['description']) ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Zusatzinfos / Server-Details -->
<?php if (!empty($details)): ?>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Details') ?></span></div>
<div class="detail-card-body" style="padding:0.5rem 1.25rem;">
<?php foreach ($details as $d): ?>
<div class="addr-detail-row">
<span class="addr-detail-key"><?= $this->e($d['detail_key']) ?></span>
<span class="addr-detail-val"><?= $this->e($d['detail_val'] ?? '') ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Änderungs-Historie -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Verlauf') ?></span></div>
<div class="detail-card-body" style="padding:<?= empty($history) ? '1.25rem' : '0.5rem 1.25rem 1rem' ?>;">
<?php if (empty($history)): ?>
<p style="font-size:0.85rem;color:var(--gray-400);margin:0;"><?= $this->te('Noch keine Änderungen protokolliert.') ?></p>
<?php else:
// Wertanzeige: Status/Typ in Klartext-Labels übersetzen
$fmtVal = function ($field, $val) use ($status_labels, $type_labels) {
if ($val === null || $val === '') return '—';
if ($field === 'status') return $status_labels[$val] ?? $val;
if ($field === 'type') return $type_labels[$val] ?? $val;
return $val;
};
$srcStyle = [
'manual' => 'background:#e0e7ff;color:#3730a3;',
'web_scan' => 'background:#cffafe;color:#155e75;',
'scan_agent' => 'background:#dcfce7;color:#15803d;',
'system' => 'background:#f3f4f6;color:#6b7280;',
];
?>
<div class="addr-hist-list">
<?php foreach ($history as $h):
$src = $h['source'] ?? 'system';
$changes = json_decode($h['changes'] ?? '[]', true) ?: [];
?>
<div class="addr-hist-item">
<div class="addr-hist-meta">
<span class="addr-hist-badge" style="<?= $srcStyle[$src] ?? $srcStyle['system'] ?>"><?= $this->e($source_labels[$src] ?? $src) ?></span>
<span class="addr-hist-time"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($h['created_at']))]) ?></span>
<?php if (!empty($h['user_name'])): ?>
<span class="addr-hist-user">· <?= $this->e($h['user_name']) ?></span>
<?php endif; ?>
</div>
<div class="addr-hist-body">
<?php if (($h['action'] ?? '') === 'created'): ?>
<div class="addr-hist-change"><strong><?= $this->te('Eintrag angelegt') ?></strong><?php if ($changes): ?> <?= $this->te('mit') ?>
<?= implode(', ', array_map(fn($c) => $this->e($c['label']) . ' „' . $this->e($fmtVal($c['field'], $c['new'])) . '“', $changes)) ?>
<?php endif; ?></div>
<?php elseif (empty($changes)): ?>
<div class="addr-hist-change"><?= $this->te('Aktualisiert') ?></div>
<?php else: foreach ($changes as $c): ?>
<div class="addr-hist-change">
<span class="addr-hist-field"><?= $this->e($c['label']) ?>:</span>
<span class="addr-hist-old"><?= $this->e($fmtVal($c['field'], $c['old'])) ?></span>
<span class="addr-hist-arrow">→</span>
<span class="addr-hist-new"><?= $this->e($fmtVal($c['field'], $c['new'])) ?></span>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Sidebar -->
<div>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Subnetz') ?></span></div>
<div class="detail-card-body">
<a href="/ipam/subnets/<?= $address['subnet_id'] ?>" style="display:flex;align-items:center;gap:0.75rem;padding:0.6rem;background:var(--gray-50);border-radius:0.25rem;border:1px solid var(--gray-200);color:inherit;transition:background 0.1s;" onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#f9fafb'">
<span style="width:10px;height:10px;border-radius:50%;background:<?= $this->e($address['subnet_color']) ?>;flex-shrink:0;"></span>
<div>
<div style="font-size:0.88rem;font-weight:700;color:var(--gray-900);"><?= $this->e($address['subnet_name']) ?></div>
<div style="font-size:0.78rem;color:var(--gray-500);font-family:monospace;"><?= $this->e($address['subnet_network']) ?>/<?= $address['subnet_cidr'] ?></div>
</div>
</a>
<?php if (!empty($address['subnet_gateway']) || !empty($address['subnet_dns'])): ?>
<div style="margin-top:0.75rem;display:flex;flex-direction:column;gap:0.6rem;">
<?php if (!empty($address['subnet_gateway'])): ?>
<div class="addr-info-row"><span class="addr-info-label"><?= $this->te('Gateway') ?></span><span class="addr-info-value" style="font-family:monospace;"><?= $this->e($address['subnet_gateway']) ?></span></div>
<?php endif; ?>
<?php if (!empty($address['subnet_dns'])): ?>
<div class="addr-info-row"><span class="addr-info-label">DNS</span><span class="addr-info-value" style="font-family:monospace;"><?= $this->e($address['subnet_dns']) ?></span></div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php
$lvm = $links['vm'] ?? null;
$luni = $links['unifi'] ?? null;
if ($lvm || $luni):
?>
<!-- Modul-übergreifende Verknüpfungen -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Verknüpfungen') ?></span></div>
<div class="detail-card-body">
<div class="addr-info-list">
<?php if ($lvm): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Virtuelle Maschine') ?></span>
<span class="addr-info-value"><a href="/vms/<?= (int)$lvm['id'] ?>"><?= $this->e($lvm['name'] ?: ('VM #' . (int)$lvm['id'])) ?></a></span>
</div>
<?php endif; ?>
<?php if ($luni): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te($luni['kind'] === 'device' ? 'UniFi-Gerät' : 'UniFi-Client') ?></span>
<span class="addr-info-value">
<a href="/unifi" title="<?= $this->te('Zur UniFi-Übersicht') ?>"><?= $this->e($luni['name']) ?></a>
<?php if (!empty($luni['mac'])): ?><span style="color:var(--gray-400);font-family:monospace;font-size:0.78rem;"> · <?= $this->e($luni['mac']) ?></span><?php endif; ?>
</span>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Protokoll') ?></span></div>
<div class="detail-card-body">
<div class="addr-info-list">
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Angelegt von') ?></span>
<span class="addr-info-value"><?= $this->e($address['created_by_name'] ?? '–') ?></span>
</div>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Angelegt am') ?></span>
<span class="addr-info-value"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($address['created_at']))]) ?></span>
</div>
<?php if ($address['updated_by_name'] && $address['updated_by'] !== $address['created_by']): ?>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Zuletzt geändert von') ?></span>
<span class="addr-info-value"><?= $this->e($address['updated_by_name']) ?></span>
</div>
<div class="addr-info-row">
<span class="addr-info-label"><?= $this->te('Zuletzt geändert') ?></span>
<span class="addr-info-value"><?= $this->te('{zeitpunkt} Uhr', ['zeitpunkt' => date('d.m.Y H:i', strtotime($address['updated_at']))]) ?></span>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</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">×</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>
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(); });
})();
</script>