v0.9.0: no-MAC device tracking, IP-change dashboard, extended search
This commit is contained in:
@@ -63,6 +63,9 @@
|
||||
<x-dropdown-link :href="route('network.search')">
|
||||
🔍 Suche
|
||||
</x-dropdown-link>
|
||||
<x-dropdown-link :href="route('network.history')">
|
||||
📅 IP-Verlauf
|
||||
</x-dropdown-link>
|
||||
<x-dropdown-link :href="route('network.import')">
|
||||
⬆️ Scan importieren
|
||||
</x-dropdown-link>
|
||||
|
||||
@@ -34,10 +34,83 @@
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Offene Ereignisse</p>
|
||||
<p class="mt-1 text-2xl font-bold text-yellow-600 dark:text-yellow-400">{{ $recentEvents->count() }}</p>
|
||||
<p class="mt-1 text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||
{{ $recentEvents->count() + $ipChangeEvents->count() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ⚠️ IP-Wechsel – prominenter Warnblock --}}
|
||||
@if($ipChangeEvents->count() > 0)
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded-lg overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-amber-200 dark:border-amber-700 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-amber-800 dark:text-amber-300 flex items-center gap-2">
|
||||
⚠️ IP-Adressen-Wechsel erkannt
|
||||
<span class="bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-200 text-xs px-2 py-0.5 rounded-full font-bold">
|
||||
{{ $ipChangeEvents->count() }}
|
||||
</span>
|
||||
</h3>
|
||||
<span class="text-xs text-amber-600 dark:text-amber-400">Bitte prüfen und quittieren</span>
|
||||
</div>
|
||||
<div class="divide-y divide-amber-100 dark:divide-amber-800">
|
||||
@foreach($ipChangeEvents as $event)
|
||||
<div class="px-5 py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
{{-- Gerät und IP-Wechsel --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center flex-wrap gap-2 mb-1">
|
||||
<a href="{{ route('network.device', $event->device) }}"
|
||||
class="font-semibold text-gray-900 dark:text-gray-100 hover:text-indigo-600">
|
||||
{{ $event->device->display_name }}
|
||||
</a>
|
||||
@if($event->device->mac_address)
|
||||
<span class="font-mono text-xs text-gray-400 bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded">
|
||||
{{ $event->device->mac_address }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-mono text-sm font-bold text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 px-2 py-0.5 rounded">
|
||||
{{ $event->old_value }}
|
||||
</span>
|
||||
<span class="text-gray-400">→</span>
|
||||
<span class="font-mono text-sm font-bold text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 px-2 py-0.5 rounded">
|
||||
{{ $event->new_value }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ $event->created_at->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Bestätigen mit Notiz --}}
|
||||
<form method="POST" action="{{ route('network.document', $event) }}"
|
||||
class="flex gap-2 items-center shrink-0">
|
||||
@csrf
|
||||
<input type="text" name="description"
|
||||
placeholder="Notiz zur Änderung (optional)"
|
||||
class="text-xs border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded px-2 py-1.5 w-56 focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
<button type="submit"
|
||||
class="shrink-0 text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-700 transition font-medium">
|
||||
✓ Quittieren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{{-- IP-Verlauf (sofern vorhanden) --}}
|
||||
@php
|
||||
$ipCount = $event->device->hosts()
|
||||
->selectRaw('DISTINCT ip_address')
|
||||
->count();
|
||||
@endphp
|
||||
@if($ipCount > 1)
|
||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
📋 Dieses Gerät hatte {{ $ipCount }} verschiedene IP-Adressen —
|
||||
<a href="{{ route('network.device', $event->device) }}" class="underline hover:no-underline">IP-Verlauf anzeigen →</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Globale Suche --}}
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-5">
|
||||
<form method="GET" action="{{ route('network.search') }}" class="flex gap-3">
|
||||
@@ -80,9 +153,7 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($segments as $segment)
|
||||
@php
|
||||
$lastScan = $latestScans->get($segment->id)?->first();
|
||||
@endphp
|
||||
@php $lastScan = $latestScans->get($segment->id)?->first(); @endphp
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-block w-2 h-2 rounded-full {{ $segment->active ? 'bg-green-500' : 'bg-gray-300' }}"></span>
|
||||
@@ -117,7 +188,7 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Undokumentierte Ereignisse --}}
|
||||
{{-- Sonstige undokumentierte Ereignisse --}}
|
||||
@if($recentEvents->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
@@ -128,14 +199,14 @@
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($recentEvents as $event)
|
||||
<div class="flex items-center justify-between px-5 py-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex items-center justify-between px-5 py-3 gap-4">
|
||||
<div class="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<span class="w-2 h-2 rounded-full flex-shrink-0
|
||||
{{ $event->event_color === 'green' ? 'bg-green-500' :
|
||||
($event->event_color === 'blue' ? 'bg-blue-500' :
|
||||
($event->event_color === 'red' ? 'bg-red-500' : 'bg-yellow-500')) }}">
|
||||
</span>
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $event->event_label }}</span>
|
||||
<span class="text-xs text-gray-400 ml-2">
|
||||
<a href="{{ route('network.device', $event->device) }}" class="hover:text-indigo-600">
|
||||
@@ -143,10 +214,23 @@
|
||||
</a>
|
||||
· {{ $event->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if($event->old_value && $event->new_value)
|
||||
<span class="ml-2 text-xs text-gray-400">
|
||||
<span class="font-mono">{{ $event->old_value }}</span> → <span class="font-mono">{{ $event->new_value }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('network.device', $event->device) }}"
|
||||
class="text-xs text-indigo-600 hover:underline">Detail →</a>
|
||||
<form method="POST" action="{{ route('network.document', $event) }}"
|
||||
class="flex gap-2 items-center shrink-0">
|
||||
@csrf
|
||||
<input type="text" name="description" placeholder="Notiz..."
|
||||
class="text-xs border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded px-2 py-1 w-40 focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
<button type="submit"
|
||||
class="text-xs px-3 py-1 rounded border border-green-300 text-green-700 hover:bg-green-50 dark:hover:bg-green-900/20 transition whitespace-nowrap">
|
||||
✓ Quittieren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -101,10 +101,33 @@
|
||||
|
||||
{{-- IP-Verlauf --}}
|
||||
@if($ipHistory->count() > 0)
|
||||
@php
|
||||
$distinctIps = $ipHistory->pluck('ip_address')->unique()->values();
|
||||
@endphp
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">IP-Verlauf</h3>
|
||||
@if($distinctIps->count() > 1)
|
||||
<span class="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">
|
||||
⚠️ {{ $distinctIps->count() }} verschiedene IP-Adressen
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Distinct IPs auf einen Blick --}}
|
||||
@if($distinctIps->count() > 1)
|
||||
<div class="px-5 py-3 bg-amber-50 dark:bg-amber-900/10 border-b border-amber-100 dark:border-amber-800">
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400 font-medium mb-1">Bekannte IP-Adressen dieses Geräts:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($distinctIps as $dip)
|
||||
<span class="font-mono text-xs px-2 py-1 rounded {{ $dip === $device->current_ip ? 'bg-green-100 text-green-800 font-bold' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300' }}">
|
||||
{{ $dip }}{{ $dip === $device->current_ip ? ' ← aktuell' : '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-100 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
@@ -116,9 +139,11 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($ipHistory as $h)
|
||||
<tr>
|
||||
<tr class="{{ $h->ip_address !== $device->current_ip ? 'opacity-60' : '' }}">
|
||||
<td class="px-4 py-2 text-gray-500 dark:text-gray-400">{{ $h->created_at->format('d.m.Y H:i') }}</td>
|
||||
<td class="px-4 py-2 font-mono text-gray-900 dark:text-gray-100">{{ $h->ip_address }}</td>
|
||||
<td class="px-4 py-2 font-mono font-medium {{ $h->ip_address === $device->current_ip ? 'text-green-700 dark:text-green-400' : 'text-gray-600 dark:text-gray-400' }}">
|
||||
{{ $h->ip_address }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<span class="text-xs {{ $h->status === 'online' ? 'text-green-600' : 'text-gray-400' }}">{{ $h->status }}</span>
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">Chronologischer IP-Verlauf</h2>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $entries->total() }} Einträge</span>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-4">
|
||||
|
||||
{{-- Filter --}}
|
||||
<form method="GET" action="{{ route('network.history') }}"
|
||||
class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">IP-Adresse</label>
|
||||
<input type="text" name="ip" value="{{ request('ip') }}"
|
||||
placeholder="z.B. 192.168.86"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">MAC-Adresse</label>
|
||||
<input type="text" name="mac" value="{{ request('mac') }}"
|
||||
placeholder="z.B. 00:50:56"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Hostname / Bezeichnung</label>
|
||||
<input type="text" name="hostname" value="{{ request('hostname') }}"
|
||||
placeholder="z.B. frigo-nas"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Segment</label>
|
||||
<select name="segment"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">Alle Segmente</option>
|
||||
@foreach($segments as $seg)
|
||||
<option value="{{ $seg->id }}" {{ request('segment') == $seg->id ? 'selected' : '' }}>
|
||||
{{ $seg->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Status</label>
|
||||
<select name="status"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">Alle</option>
|
||||
<option value="online" {{ request('status') === 'online' ? 'selected' : '' }}>Online</option>
|
||||
<option value="offline" {{ request('status') === 'offline' ? 'selected' : '' }}>Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Von Datum</label>
|
||||
<input type="date" name="from" value="{{ request('from') }}"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Bis Datum</label>
|
||||
<input type="date" name="to" value="{{ request('to') }}"
|
||||
class="block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<button type="submit" style="background-color: var(--color-primary)"
|
||||
class="flex-1 py-2 text-white text-sm font-medium rounded-md hover:opacity-90 transition">
|
||||
Filtern
|
||||
</button>
|
||||
@if(request()->hasAny(['ip','mac','hostname','segment','status','from','to']))
|
||||
<a href="{{ route('network.history') }}"
|
||||
class="py-2 px-3 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
✕
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Tabelle --}}
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Datum / Zeit</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">IP-Adresse</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">MAC-Adresse</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hostname / Bezeichnung</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hersteller</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Segment</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ping</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@forelse($entries as $entry)
|
||||
<tr class="{{ $entry->status === 'online' ? '' : 'opacity-60' }} hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-3 py-2 text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{{ \Carbon\Carbon::parse($entry->scan_time)->format('d.m.Y H:i') }}
|
||||
@if($entry->scanner)
|
||||
<br><span class="text-gray-400">{{ $entry->scanner }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full {{ $entry->status === 'online' ? 'bg-green-500' : 'bg-gray-300' }}"></span>
|
||||
<span class="text-xs {{ $entry->status === 'online' ? 'text-green-700 dark:text-green-400' : 'text-gray-400' }}">
|
||||
{{ $entry->status }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 font-mono font-medium text-gray-900 dark:text-gray-100">
|
||||
@if($entry->device_id)
|
||||
<a href="{{ route('network.device', $entry->device_id) }}"
|
||||
class="text-indigo-600 hover:underline">
|
||||
{{ $entry->ip_address }}
|
||||
</a>
|
||||
@else
|
||||
{{ $entry->ip_address }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
|
||||
@if($entry->mac_address)
|
||||
<span title="{{ $entry->mac_address }}">{{ $entry->mac_address }}</span>
|
||||
@else
|
||||
<span class="text-gray-300">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2 text-gray-700 dark:text-gray-300">
|
||||
@if($entry->device_label)
|
||||
<span class="font-medium">{{ $entry->device_label }}</span>
|
||||
@if($entry->hostname)
|
||||
<span class="text-xs text-gray-400 ml-1">({{ $entry->hostname }})</span>
|
||||
@endif
|
||||
@else
|
||||
{{ $entry->hostname ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-600 dark:text-gray-400">{{ $entry->mac_vendor ?? '—' }}</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $entry->segment_name ?? '—' }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $entry->ping_ms !== null ? $entry->ping_ms . ' ms' : '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-10 text-center text-gray-500">
|
||||
Keine Einträge gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($entries->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
{{ $entries->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Auto-Refresh alle 60 Sekunden wenn kein Filter aktiv --}}
|
||||
@unless(request()->hasAny(['ip','mac','hostname','segment','status','from','to']))
|
||||
<script>
|
||||
setTimeout(function() { window.location.reload(); }, 60000);
|
||||
</script>
|
||||
@endunless
|
||||
</x-app-layout>
|
||||
@@ -1,9 +1,25 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ route('network.index') }}" class="text-gray-500 hover:text-gray-700">Netzwerk</a>
|
||||
<span class="text-gray-400">/</span>
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">Scan {{ $scan->created_at->format('d.m.Y H:i') }}</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($scan->segment)
|
||||
<a href="{{ route('network.segments.show', $scan->segment) }}" class="text-gray-500 hover:text-gray-700">{{ $scan->segment->name }}</a>
|
||||
<span class="text-gray-400">/</span>
|
||||
@endif
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">Scan {{ $scan->created_at->format('d.m.Y H:i') }}</h2>
|
||||
</div>
|
||||
@if($scan->segment)
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('network.segments.export.xlsx', $scan->segment) }}"
|
||||
class="px-3 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
📊 Excel
|
||||
</a>
|
||||
<a href="{{ route('network.segments.export.pdf', $scan->segment) }}"
|
||||
class="px-3 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
📄 PDF
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
@@ -32,6 +48,7 @@
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hersteller</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ping</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ports</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Bemerkung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@@ -54,6 +71,25 @@
|
||||
<td class="px-4 py-2 text-xs text-gray-600 dark:text-gray-400">{{ $host->mac_vendor ?? '—' }}</td>
|
||||
<td class="px-4 py-2 text-xs text-gray-600 dark:text-gray-400">{{ $host->ping_ms ? $host->ping_ms . ' ms' : '—' }}</td>
|
||||
<td class="px-4 py-2 text-xs text-gray-600 dark:text-gray-400 max-w-xs truncate">{{ $host->ports ?? '—' }}</td>
|
||||
<td class="px-4 py-2 min-w-48">
|
||||
@if($scan->segment_id)
|
||||
<div class="note-cell" data-ip="{{ $host->ip_address }}">
|
||||
<span class="note-display text-xs text-gray-600 dark:text-gray-400 cursor-pointer hover:text-indigo-600 italic"
|
||||
title="Klicken zum Bearbeiten">
|
||||
{{ $notes[$host->ip_address] ?? '+ Bemerkung' }}
|
||||
</span>
|
||||
<div class="note-form hidden flex gap-1 items-center">
|
||||
<input type="text" maxlength="500"
|
||||
class="note-input flex-1 text-xs border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded px-2 py-1 focus:ring-indigo-500 focus:border-indigo-500"
|
||||
value="{{ $notes[$host->ip_address] ?? '' }}" />
|
||||
<button class="note-save text-xs px-2 py-1 rounded text-white" style="background-color: var(--color-primary)">✓</button>
|
||||
<button class="note-cancel text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs text-gray-300">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@@ -61,4 +97,61 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($scan->segment_id)
|
||||
<script>
|
||||
(function() {
|
||||
const saveUrl = '{{ route('network.segments.ip-notes', $scan->segment_id) }}';
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
|
||||
document.querySelectorAll('.note-cell').forEach(function(cell) {
|
||||
const ip = cell.dataset.ip;
|
||||
const display = cell.querySelector('.note-display');
|
||||
const form = cell.querySelector('.note-form');
|
||||
const input = cell.querySelector('.note-input');
|
||||
const save = cell.querySelector('.note-save');
|
||||
const cancel = cell.querySelector('.note-cancel');
|
||||
|
||||
display.addEventListener('click', function() {
|
||||
display.classList.add('hidden');
|
||||
form.classList.remove('hidden');
|
||||
input.focus();
|
||||
});
|
||||
|
||||
cancel.addEventListener('click', function() {
|
||||
form.classList.add('hidden');
|
||||
display.classList.remove('hidden');
|
||||
});
|
||||
|
||||
save.addEventListener('click', function() {
|
||||
fetch(saveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ip_address: ip, note: input.value }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
display.textContent = input.value || '+ Bemerkung';
|
||||
form.classList.add('hidden');
|
||||
display.classList.remove('hidden');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
alert('Fehler beim Speichern.');
|
||||
});
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') save.click();
|
||||
if (e.key === 'Escape') cancel.click();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
</x-app-layout>
|
||||
|
||||
@@ -26,11 +26,17 @@
|
||||
@if($q && strlen($q) < 2)
|
||||
<p class="text-sm text-gray-500">Mindestens 2 Zeichen eingeben.</p>
|
||||
@elseif($q)
|
||||
@php $totalFound = $devices->total() + $hostResults->count(); @endphp
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $devices->total() }} Ergebnis(se) für „{{ $q }}" über alle Segmente
|
||||
{{ $totalFound }} Ergebnis(se) für „{{ $q }}" über alle Segmente
|
||||
</p>
|
||||
|
||||
{{-- Ergebnisse aus network_devices (getracked) --}}
|
||||
@if($devices->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Geräte (bekannt)</span>
|
||||
</div>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
@@ -44,20 +50,23 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@forelse($devices as $device)
|
||||
@foreach($devices as $device)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-4 py-3">
|
||||
<span class="w-2 h-2 rounded-full inline-block
|
||||
{{ $device->status === 'online' ? 'bg-green-500' : 'bg-red-400' }}"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-gray-900 dark:text-gray-100 font-medium">{{ $device->current_ip }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">{{ $device->mac_address }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">{{ $device->mac_address ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">
|
||||
@if($device->label)
|
||||
<span class="font-medium">{{ $device->label }}</span>
|
||||
<span class="text-gray-400 text-xs ml-1">({{ $device->hostname }})</span>
|
||||
@else
|
||||
{{ $device->hostname ?? '—' }}
|
||||
{{ $device->hostname ?? $device->netbios_name ?? '—' }}
|
||||
@endif
|
||||
@if($device->netbios_name && $device->netbios_name !== $device->hostname)
|
||||
<span class="block text-xs text-gray-400">NetBIOS: {{ $device->netbios_name }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">{{ $device->mac_vendor ?? '—' }}</td>
|
||||
@@ -69,20 +78,70 @@
|
||||
class="text-xs text-indigo-600 hover:underline">Detail →</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-10 text-center text-gray-500">Keine Geräte gefunden.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if($devices->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
{{ $devices->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Ergebnisse aus network_hosts (Scan-Treffer ohne Device-Eintrag) --}}
|
||||
@if($hostResults->count() > 0)
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-800 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wider">
|
||||
In Scan-Verlauf gefunden
|
||||
</span>
|
||||
<span class="text-xs text-blue-400">Noch kein Geräteeintrag — wird beim nächsten Scan angelegt</span>
|
||||
</div>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">IP-Adresse</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">MAC-Adresse</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hostname</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hersteller</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Segment</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Letzter Scan</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($hostResults as $host)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-4 py-3">
|
||||
<span class="w-2 h-2 rounded-full inline-block
|
||||
{{ $host->status === 'online' ? 'bg-green-500' : 'bg-gray-300' }}"></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono font-medium text-gray-900 dark:text-gray-100">{{ $host->ip_address }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-gray-500 dark:text-gray-400">{{ $host->mac_address ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ $host->hostname ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">{{ $host->mac_vendor ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">{{ $host->segment_name ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-400">
|
||||
{{ \Carbon\Carbon::parse($host->scan_time)->format('d.m.Y H:i') }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('network.scan', $host->scan_id) }}"
|
||||
class="text-xs text-indigo-600 hover:underline">Scan →</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($totalFound === 0)
|
||||
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg px-4 py-10 text-center text-gray-500">
|
||||
Keine Ergebnisse für „{{ $q }}" gefunden.
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,16 @@
|
||||
|
||||
<div>
|
||||
<x-input-label for="subnet" value="Subnetz *" />
|
||||
<x-text-input id="subnet" name="subnet" type="text" class="mt-1 block w-full font-mono"
|
||||
value="{{ old('subnet') }}" placeholder="z.B. 192.168.1.0/24 oder 10.0.0.0/8" required />
|
||||
<div class="flex gap-2 mt-1">
|
||||
<x-text-input id="subnet" name="subnet" type="text" class="block w-full font-mono"
|
||||
value="{{ old('subnet') }}" placeholder="z.B. 192.168.1.0/24 oder 10.0.0.0/8" required />
|
||||
<button type="button" id="detect-subnet-btn"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
🔍 Erkennen
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('subnet')" class="mt-1" />
|
||||
<div id="subnet-suggestions" class="mt-1 hidden"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -41,6 +48,29 @@
|
||||
placeholder="Kurze Beschreibung dieses Segments...">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="scan_interval_minutes" value="Scan-Zyklus (automatisch)" />
|
||||
<select id="scan_interval_minutes" name="scan_interval_minutes"
|
||||
class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">Manuell (kein Auto-Scan)</option>
|
||||
<option value="5" {{ old('scan_interval_minutes') == '5' ? 'selected' : '' }}>Alle 5 Minuten</option>
|
||||
<option value="15" {{ old('scan_interval_minutes') == '15' ? 'selected' : '' }}>Alle 15 Minuten</option>
|
||||
<option value="30" {{ old('scan_interval_minutes') == '30' ? 'selected' : '' }}>Alle 30 Minuten</option>
|
||||
<option value="60" {{ old('scan_interval_minutes') == '60' ? 'selected' : '' }}>Stündlich</option>
|
||||
<option value="360" {{ old('scan_interval_minutes') == '360' ? 'selected' : '' }}>Alle 6 Stunden</option>
|
||||
<option value="720" {{ old('scan_interval_minutes') == '720' ? 'selected' : '' }}>Alle 12 Stunden</option>
|
||||
<option value="1440" {{ old('scan_interval_minutes') == '1440' ? 'selected' : '' }}>Täglich</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500">Erfordert aktiven Laravel Scheduler (Cron).</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="nmap_options" value="nmap-Parameter" />
|
||||
<x-text-input id="nmap_options" name="nmap_options" type="text" class="mt-1 block w-full font-mono"
|
||||
value="{{ old('nmap_options', '-sn') }}" placeholder="-sn" />
|
||||
<p class="mt-1 text-xs text-gray-500">-sn = Ping-Scan (schnell) · -sn -p 22,80,443 = mit Ports</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="active" name="active" value="1"
|
||||
{{ old('active', '1') ? 'checked' : '' }}
|
||||
@@ -62,4 +92,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('detect-subnet-btn').addEventListener('click', function() {
|
||||
const btn = this;
|
||||
const suggestions = document.getElementById('subnet-suggestions');
|
||||
btn.textContent = '⏳ Suche...';
|
||||
btn.disabled = true;
|
||||
|
||||
fetch('{{ route('network.detect-subnets') }}', {
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(function(subnets) {
|
||||
btn.textContent = '🔍 Erkennen';
|
||||
btn.disabled = false;
|
||||
if (!subnets.length) {
|
||||
suggestions.innerHTML = '<p class="text-xs text-gray-400">Keine Daten in der Datenbank gefunden.</p>';
|
||||
suggestions.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
suggestions.innerHTML = '<p class="text-xs text-gray-500 mb-1">Erkannte Subnetze – klicken zum Übernehmen:</p>' +
|
||||
subnets.map(function(s) {
|
||||
return '<button type="button" onclick="document.getElementById(\'subnet\').value=\'' + s + '\'" ' +
|
||||
'class="mr-1 mb-1 px-2 py-1 text-xs font-mono rounded border border-indigo-300 text-indigo-700 hover:bg-indigo-50 transition">' + s + '</button>';
|
||||
}).join('');
|
||||
suggestions.classList.remove('hidden');
|
||||
})
|
||||
.catch(function() {
|
||||
btn.textContent = '🔍 Erkennen';
|
||||
btn.disabled = false;
|
||||
suggestions.innerHTML = '<p class="text-xs text-red-500">Fehler bei der Erkennung.</p>';
|
||||
suggestions.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -22,9 +22,16 @@
|
||||
|
||||
<div>
|
||||
<x-input-label for="subnet" value="Subnetz *" />
|
||||
<x-text-input id="subnet" name="subnet" type="text" class="mt-1 block w-full font-mono"
|
||||
value="{{ old('subnet', $segment->subnet) }}" required />
|
||||
<div class="flex gap-2 mt-1">
|
||||
<x-text-input id="subnet" name="subnet" type="text" class="block w-full font-mono"
|
||||
value="{{ old('subnet', $segment->subnet) }}" required />
|
||||
<button type="button" id="detect-subnet-btn"
|
||||
class="shrink-0 px-3 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
🔍 Erkennen
|
||||
</button>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('subnet')" class="mt-1" />
|
||||
<div id="subnet-suggestions" class="mt-1 hidden"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -40,6 +47,26 @@
|
||||
class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">{{ old('description', $segment->description) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="scan_interval_minutes" value="Scan-Zyklus (automatisch)" />
|
||||
<select id="scan_interval_minutes" name="scan_interval_minutes"
|
||||
class="mt-1 block w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">Manuell (kein Auto-Scan)</option>
|
||||
@foreach([5 => 'Alle 5 Minuten', 15 => 'Alle 15 Minuten', 30 => 'Alle 30 Minuten', 60 => 'Stündlich', 360 => 'Alle 6 Stunden', 720 => 'Alle 12 Stunden', 1440 => 'Täglich'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ old('scan_interval_minutes', $segment->scan_interval_minutes) == $val ? 'selected' : '' }}>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="nmap_options" value="nmap-Parameter" />
|
||||
<x-text-input id="nmap_options" name="nmap_options" type="text" class="mt-1 block w-full font-mono"
|
||||
value="{{ old('nmap_options', $segment->nmap_options) }}" placeholder="-sn" />
|
||||
<p class="mt-1 text-xs text-gray-500">-sn = Ping-Scan · -sn -p 22,80,443 = mit Ports</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="active" name="active" value="1"
|
||||
{{ old('active', $segment->active) ? 'checked' : '' }}
|
||||
@@ -61,4 +88,38 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('detect-subnet-btn').addEventListener('click', function() {
|
||||
const btn = this;
|
||||
const suggestions = document.getElementById('subnet-suggestions');
|
||||
btn.textContent = '⏳ Suche...';
|
||||
btn.disabled = true;
|
||||
|
||||
fetch('{{ route('network.detect-subnets') }}', {
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(function(subnets) {
|
||||
btn.textContent = '🔍 Erkennen';
|
||||
btn.disabled = false;
|
||||
if (!subnets.length) {
|
||||
suggestions.innerHTML = '<p class="text-xs text-gray-400">Keine Daten in der Datenbank gefunden.</p>';
|
||||
suggestions.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
suggestions.innerHTML = '<p class="text-xs text-gray-500 mb-1">Erkannte Subnetze – klicken zum Übernehmen:</p>' +
|
||||
subnets.map(function(s) {
|
||||
return '<button type="button" onclick="document.getElementById(\'subnet\').value=\'' + s + '\'" ' +
|
||||
'class="mr-1 mb-1 px-2 py-1 text-xs font-mono rounded border border-indigo-300 text-indigo-700 hover:bg-indigo-50 transition">' + s + '</button>';
|
||||
}).join('');
|
||||
suggestions.classList.remove('hidden');
|
||||
})
|
||||
.catch(function() {
|
||||
btn.textContent = '🔍 Erkennen';
|
||||
btn.disabled = false;
|
||||
suggestions.innerHTML = '<p class="text-xs text-red-500">Fehler bei der Erkennung.</p>';
|
||||
suggestions.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Segment {{ $segment->name }} – Export</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
color: #1f2937;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
h1 { font-size: 18px; font-weight: bold; color: #111827; }
|
||||
|
||||
.meta {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.print-btn:hover { background: #4338ca; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
thead th {
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: #6b7280;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 5px 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) td { background-color: #f9fafb; }
|
||||
tbody tr:hover td { background-color: #eff6ff; }
|
||||
|
||||
.online { color: #16a34a; font-weight: 700; }
|
||||
.offline { color: #9ca3af; }
|
||||
.mono { font-family: 'Courier New', monospace; }
|
||||
.note { color: #4b5563; font-style: italic; }
|
||||
|
||||
.footer {
|
||||
margin-top: 12px;
|
||||
font-size: 9px;
|
||||
color: #9ca3af;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Print-Stile */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
body { padding: 0; font-size: 10px; }
|
||||
thead th { background-color: #f3f4f6 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
tbody tr:nth-child(even) td { background-color: #f9fafb !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
@page { size: A4 landscape; margin: 1.5cm; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>🌐 Segment: {{ $segment->name }}</h1>
|
||||
<div class="meta">
|
||||
Subnetz: <strong>{{ $segment->subnet }}</strong>
|
||||
@if($segment->vlan_id) · VLAN {{ $segment->vlan_id }} @endif
|
||||
· {{ $hosts->count() }} Hosts
|
||||
· {{ $hosts->where('status', 'online')->count() }} online
|
||||
<br>
|
||||
Exportiert: {{ now()->format('d.m.Y H:i') }}
|
||||
@if($segment->description) · {{ $segment->description }} @endif
|
||||
</div>
|
||||
</div>
|
||||
<button class="print-btn no-print" onclick="window.print()">🖨️ Als PDF drucken</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:7%">Status</th>
|
||||
<th style="width:12%">IP-Adresse</th>
|
||||
<th style="width:16%">MAC-Adresse</th>
|
||||
<th style="width:18%">Hostname</th>
|
||||
<th style="width:14%">Hersteller</th>
|
||||
<th style="width:7%">Ping</th>
|
||||
<th>Bemerkung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($hosts as $host)
|
||||
<tr>
|
||||
<td class="{{ $host->status === 'online' ? 'online' : 'offline' }}">
|
||||
{{ $host->status === 'online' ? '● Online' : '○ Offline' }}
|
||||
</td>
|
||||
<td class="mono">{{ $host->ip_address }}</td>
|
||||
<td class="mono">{{ $host->mac_address ?? '—' }}</td>
|
||||
<td>{{ $host->hostname ?? '—' }}</td>
|
||||
<td>{{ $host->mac_vendor ?? '—' }}</td>
|
||||
<td class="mono">{{ $host->ping_ms !== null ? $host->ping_ms . ' ms' : '—' }}</td>
|
||||
<td class="note">{{ $notes[$host->ip_address] ?? '' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; padding:24px; color:#9ca3af;">
|
||||
Keine Hosts vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
Network-MGMT · MMS System Service · {{ now()->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Automatisch Druckdialog öffnen (nur wenn direkt als PDF aufgerufen)
|
||||
if (window.location.pathname.includes('/export/pdf')) {
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(function() { window.print(); }, 300);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,14 +6,37 @@
|
||||
<span class="text-gray-400">/</span>
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">{{ $segment->name }}</h2>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
{{-- Auto-Refresh Indikator --}}
|
||||
<span id="refresh-indicator" class="text-xs text-gray-400 dark:text-gray-500 hidden">
|
||||
↻ wird aktualisiert...
|
||||
</span>
|
||||
|
||||
{{-- Jetzt scannen --}}
|
||||
@if($segment->active)
|
||||
<form method="POST" action="{{ route('network.segments.scan', $segment) }}">
|
||||
@csrf
|
||||
<button type="submit" style="background-color: var(--color-primary)"
|
||||
class="px-4 py-2 text-white text-sm font-medium rounded-md hover:opacity-90 transition">
|
||||
▶ Jetzt scannen
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('network.import') }}?segment={{ $segment->id }}"
|
||||
style="background-color: var(--color-primary)"
|
||||
class="px-4 py-2 text-white text-sm font-medium rounded-md hover:opacity-90 transition">
|
||||
+ Scan importieren
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
⬆ Import
|
||||
</a>
|
||||
<a href="{{ route('network.segments.export.xlsx', $segment) }}"
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
📊 Excel
|
||||
</a>
|
||||
<a href="{{ route('network.segments.export.pdf', $segment) }}"
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
📄 PDF
|
||||
</a>
|
||||
<a href="{{ route('network.segments.edit', $segment) }}"
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 transition">
|
||||
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
Bearbeiten
|
||||
</a>
|
||||
</div>
|
||||
@@ -39,6 +62,12 @@
|
||||
{{ $segment->active ? 'Aktiv' : 'Inaktiv' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Auto-Scan</p>
|
||||
<p class="mt-1 font-bold text-gray-900 dark:text-gray-100 text-sm">
|
||||
{{ $segment->scan_interval_label }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Scans gesamt</p>
|
||||
<p class="mt-1 font-bold text-gray-900 dark:text-gray-100">{{ $scans->total() }}</p>
|
||||
@@ -70,6 +99,7 @@
|
||||
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Scan-Historie</h3>
|
||||
</div>
|
||||
<div data-scan-count="{{ $scans->total() }}"></div>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
@@ -112,4 +142,33 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success') && str_contains(session('success'), 'gestartet'))
|
||||
{{-- Auto-Refresh nach "Jetzt scannen": alle 10s prüfen ob neuer Scan vorliegt --}}
|
||||
<script>
|
||||
(function() {
|
||||
const currentScanCount = {{ $scans->total() }};
|
||||
const indicator = document.getElementById('refresh-indicator');
|
||||
let checks = 0;
|
||||
|
||||
const interval = setInterval(function() {
|
||||
if (++checks > 18) { clearInterval(interval); return; } // max 3 Min
|
||||
if (indicator) indicator.classList.remove('hidden');
|
||||
|
||||
fetch(window.location.href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newCount = parseInt(doc.querySelector('[data-scan-count]')?.dataset?.scanCount || '0');
|
||||
if (newCount > currentScanCount) {
|
||||
clearInterval(interval);
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, 10000);
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
</x-app-layout>
|
||||
|
||||
Reference in New Issue
Block a user