Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 402537805d | |||
| eb57be730b |
+38
-1
@@ -9,6 +9,41 @@ Dieses Projekt verwendet [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-06-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Netzwerk-Modul: Menüpunkt „Netzwerk" auf Ebene 0 für alle eingeloggten Benutzer
|
||||||
|
- Import von Angry IP Scanner `.txt`-Exporten (tab-getrennt) via Datei-Upload
|
||||||
|
- Automatische Erkennung und Speicherung von Netzwerkgeräten anhand MAC-Adresse
|
||||||
|
- Chronologische Scan-Sessions mit Metadaten (Subnetz, Quelle, Gesamt-/Online-Hosts)
|
||||||
|
- Änderungserkennung: neue Geräte, IP-Wechsel, Online/Offline-Statuswechsel
|
||||||
|
- Ereignis-Protokoll pro Gerät mit Bestätigungs-Workflow (✓ Bestätigen)
|
||||||
|
- Geräte-Detailansicht: Stammdaten, Bezeichnung, Notizen, IP-Verlauf, Ereignislog
|
||||||
|
- Geräte-Übersicht mit Suche und Statusfilter (Online/Offline)
|
||||||
|
- Scan-Detailansicht mit vollständiger Host-Tabelle
|
||||||
|
- Manuelle Notizen zu Geräten hinzufügbar
|
||||||
|
- 4 neue Datenbanktabellen: `network_scans`, `network_devices`, `network_hosts`, `network_device_events`
|
||||||
|
- `NetworkScanImporter`-Service für Parser-Logik (MAC-Normalisierung, Spalten-Aliase)
|
||||||
|
- `NetworkController` mit 9 Routen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-06-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Einstellungen → Layout: Seitenname, Logo-Upload, Button-Farbe (Colorpicker), Dark/Light-Mode
|
||||||
|
- Settings-Tabelle als Key-Value-Store in der Datenbank
|
||||||
|
- SettingsService mit Cache-Layer (automatische Invalidierung bei Änderung)
|
||||||
|
- SettingsServiceProvider: Einstellungen werden global in alle Views injiziert
|
||||||
|
- Dark-Mode via `dark`-Klasse auf HTML-Element (Tailwind CSS)
|
||||||
|
- CSS-Variable `--color-primary` für dynamische Button-Farbe
|
||||||
|
- Hilfe-Menü auf Ebene 0 (Dropdown) für alle eingeloggten Benutzer
|
||||||
|
- Hilfe → Handbuch: Übersicht über Rollen, Funktionen, Bedienung
|
||||||
|
- Hilfe → Changelog: Changelog direkt im Browser lesbar
|
||||||
|
- Navigation: Einstellungen-Dropdown um Layout erweitert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.3.0] - 2026-06-27
|
## [0.3.0] - 2026-06-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -46,7 +81,9 @@ Dieses Projekt verwendet [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
- Grundlegende PHP-Projektstruktur (public/, src/, config/)
|
- Grundlegende PHP-Projektstruktur (public/, src/, config/)
|
||||||
- composer.json, .gitignore, README.md
|
- composer.json, .gitignore, README.md
|
||||||
|
|
||||||
[Unreleased]: http://localhost:3000/admin/Network-MGMT/compare/v0.3.0...HEAD
|
[Unreleased]: http://localhost:3000/admin/Network-MGMT/compare/v0.5.0...HEAD
|
||||||
|
[0.5.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.4.0...v0.5.0
|
||||||
|
[0.4.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.3.0...v0.4.0
|
||||||
[0.3.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.2.0...v0.3.0
|
[0.3.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.2.0...v0.3.0
|
||||||
[0.2.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.1.0...v0.2.0
|
[0.2.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.1.0...v0.2.0
|
||||||
[0.1.0]: http://localhost:3000/admin/Network-MGMT/releases/tag/v0.1.0
|
[0.1.0]: http://localhost:3000/admin/Network-MGMT/releases/tag/v0.1.0
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\SettingsService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class LayoutController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private SettingsService $settings) {}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.layout.index', [
|
||||||
|
'settings' => $this->settings->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'site_name' => ['required', 'string', 'max:100'],
|
||||||
|
'button_color' => ['required', 'string', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||||
|
'theme_mode' => ['required', 'in:light,dark'],
|
||||||
|
'site_logo' => ['nullable', 'image', 'max:2048'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Logo-Upload verarbeiten
|
||||||
|
if ($request->hasFile('site_logo')) {
|
||||||
|
$path = $request->file('site_logo')->store('logos', 'public');
|
||||||
|
$this->settings->set('site_logo', $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->settings->setMany([
|
||||||
|
'site_name' => $validated['site_name'],
|
||||||
|
'button_color' => $validated['button_color'],
|
||||||
|
'theme_mode' => $validated['theme_mode'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.layout.index')
|
||||||
|
->with('success', 'Layout-Einstellungen gespeichert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeLogo(): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->settings->set('site_logo', '');
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('admin.layout.index')
|
||||||
|
->with('success', 'Logo entfernt.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.users.index')
|
->route('admin.users.index')
|
||||||
->with('success', "Benutzer „{$user->name}" wurde angelegt.");
|
->with('success', "Benutzer \"{$user->name}\" wurde angelegt.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit(User $user): View
|
public function edit(User $user): View
|
||||||
@@ -79,7 +79,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.users.index')
|
->route('admin.users.index')
|
||||||
->with('success', "Benutzer „{$user->name}" wurde aktualisiert.");
|
->with('success', "Benutzer \"{$user->name}\" wurde aktualisiert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(User $user): RedirectResponse
|
public function destroy(User $user): RedirectResponse
|
||||||
@@ -95,6 +95,6 @@ class UserController extends Controller
|
|||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('admin.users.index')
|
->route('admin.users.index')
|
||||||
->with('success', "Benutzer „{$name}" wurde gelöscht.");
|
->with('success', "Benutzer \"{$name}\" wurde gelöscht.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class HelpController extends Controller
|
||||||
|
{
|
||||||
|
public function manual(): View
|
||||||
|
{
|
||||||
|
return view('help.manual');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changelog(): View
|
||||||
|
{
|
||||||
|
$changelogPath = base_path('CHANGELOG.md');
|
||||||
|
$content = file_exists($changelogPath)
|
||||||
|
? file_get_contents($changelogPath)
|
||||||
|
: 'Kein Changelog gefunden.';
|
||||||
|
|
||||||
|
return view('help.changelog', compact('content'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\NetworkDevice;
|
||||||
|
use App\Models\NetworkDeviceEvent;
|
||||||
|
use App\Models\NetworkScan;
|
||||||
|
use App\Services\NetworkScanImporter;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NetworkController extends Controller
|
||||||
|
{
|
||||||
|
// --- Übersicht ---
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$latestScan = NetworkScan::latest()->first();
|
||||||
|
$totalDevices = NetworkDevice::count();
|
||||||
|
$onlineDevices = NetworkDevice::where('status', 'online')->count();
|
||||||
|
$recentEvents = NetworkDeviceEvent::with('device')
|
||||||
|
->where('documented', false)
|
||||||
|
->latest()
|
||||||
|
->take(10)
|
||||||
|
->get();
|
||||||
|
$scans = NetworkScan::latest()->take(10)->get();
|
||||||
|
|
||||||
|
return view('network.index', compact(
|
||||||
|
'latestScan', 'totalDevices', 'onlineDevices', 'recentEvents', 'scans'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Alle Geräte ---
|
||||||
|
public function devices(Request $request): View
|
||||||
|
{
|
||||||
|
$query = NetworkDevice::with('events')
|
||||||
|
->orderBy('current_ip');
|
||||||
|
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$s = $request->search;
|
||||||
|
$query->where(function ($q) use ($s) {
|
||||||
|
$q->where('current_ip', 'like', "%{$s}%")
|
||||||
|
->orWhere('mac_address', 'like', "%{$s}%")
|
||||||
|
->orWhere('hostname', 'like', "%{$s}%")
|
||||||
|
->orWhere('label', 'like', "%{$s}%")
|
||||||
|
->orWhere('mac_vendor', 'like', "%{$s}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$devices = $query->paginate(50)->withQueryString();
|
||||||
|
|
||||||
|
return view('network.devices', compact('devices'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Geräte-Detail ---
|
||||||
|
public function device(NetworkDevice $device): View
|
||||||
|
{
|
||||||
|
$device->load(['events.documentedBy', 'hosts.scan']);
|
||||||
|
$ipHistory = $device->hosts()
|
||||||
|
->select('ip_address', 'status', 'ping_ms', 'created_at')
|
||||||
|
->join('network_scans', 'network_scans.id', '=', 'network_hosts.scan_id')
|
||||||
|
->orderByDesc('network_hosts.created_at')
|
||||||
|
->take(50)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('network.device', compact('device', 'ipHistory'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gerät: Label/Notiz speichern ---
|
||||||
|
public function updateDevice(Request $request, NetworkDevice $device): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'label' => ['nullable', 'string', 'max:100'],
|
||||||
|
'notes' => ['nullable', 'string', 'max:1000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$oldLabel = $device->label;
|
||||||
|
$device->update($validated);
|
||||||
|
|
||||||
|
if ($oldLabel !== $validated['label']) {
|
||||||
|
NetworkDeviceEvent::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'event_type' => 'label_changed',
|
||||||
|
'old_value' => $oldLabel,
|
||||||
|
'new_value' => $validated['label'],
|
||||||
|
'description'=> 'Bezeichnung manuell geändert',
|
||||||
|
'documented' => true,
|
||||||
|
'documented_by' => auth()->id(),
|
||||||
|
'documented_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('network.device', $device)
|
||||||
|
->with('success', 'Gerät aktualisiert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ereignis dokumentieren ---
|
||||||
|
public function documentEvent(Request $request, NetworkDeviceEvent $event): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'description' => ['nullable', 'string', 'max:500'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$event->update([
|
||||||
|
'documented' => true,
|
||||||
|
'documented_by' => auth()->id(),
|
||||||
|
'documented_at' => now(),
|
||||||
|
'description' => $request->description ?? $event->description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Ereignis dokumentiert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Manuelle Notiz hinzufügen ---
|
||||||
|
public function addNote(Request $request, NetworkDevice $device): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'description' => ['required', 'string', 'max:500'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
NetworkDeviceEvent::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'event_type' => 'manual_note',
|
||||||
|
'description' => $request->description,
|
||||||
|
'documented' => true,
|
||||||
|
'documented_by' => auth()->id(),
|
||||||
|
'documented_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Notiz hinzugefügt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Import ---
|
||||||
|
public function showImport(): View
|
||||||
|
{
|
||||||
|
return view('network.import');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import(Request $request, NetworkScanImporter $importer): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'scan_file' => ['required', 'file', 'mimes:txt,csv', 'max:5120'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$path = $request->file('scan_file')->store('imports', 'local');
|
||||||
|
$fullPath = storage_path('app/' . $path);
|
||||||
|
|
||||||
|
$scan = $importer->importAngryIpScannerFile($fullPath, auth()->id());
|
||||||
|
|
||||||
|
return redirect()->route('network.scan', $scan)
|
||||||
|
->with('success', "Import abgeschlossen: {$scan->online_hosts} online, {$scan->new_devices} neue Geräte.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Scan-Detail ---
|
||||||
|
public function scan(NetworkScan $scan): View
|
||||||
|
{
|
||||||
|
$hosts = $scan->hosts()
|
||||||
|
->orderByRaw("INET_ATON(ip_address)")
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('network.scan', compact('scan', 'hosts'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class NetworkDevice extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'mac_address', 'current_ip', 'hostname', 'mac_vendor',
|
||||||
|
'status', 'netbios_name', 'ttl', 'ports', 'notes',
|
||||||
|
'label', 'first_seen_at', 'last_seen_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'first_seen_at' => 'datetime',
|
||||||
|
'last_seen_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function hosts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(NetworkHost::class, 'device_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function events(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(NetworkDeviceEvent::class, 'device_id')->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayNameAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->label
|
||||||
|
?? $this->hostname
|
||||||
|
?? $this->current_ip
|
||||||
|
?? $this->mac_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->status) {
|
||||||
|
'online' => 'green',
|
||||||
|
'offline' => 'red',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class NetworkDeviceEvent extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'device_id', 'scan_id', 'event_type', 'old_value',
|
||||||
|
'new_value', 'description', 'documented',
|
||||||
|
'documented_by', 'documented_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'documented' => 'boolean',
|
||||||
|
'documented_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function device(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(NetworkDevice::class, 'device_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function documentedBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'documented_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->event_type) {
|
||||||
|
'new_device' => 'Neues Gerät',
|
||||||
|
'ip_changed' => 'IP-Adresse geändert',
|
||||||
|
'came_online' => 'Gerät online',
|
||||||
|
'went_offline' => 'Gerät offline',
|
||||||
|
'manual_note' => 'Manuelle Notiz',
|
||||||
|
'label_changed' => 'Bezeichnung geändert',
|
||||||
|
default => $this->event_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->event_type) {
|
||||||
|
'new_device' => 'blue',
|
||||||
|
'ip_changed' => 'yellow',
|
||||||
|
'came_online' => 'green',
|
||||||
|
'went_offline' => 'red',
|
||||||
|
'manual_note' => 'purple',
|
||||||
|
'label_changed' => 'gray',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class NetworkHost extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'scan_id', 'device_id', 'ip_address', 'mac_address',
|
||||||
|
'hostname', 'mac_vendor', 'status', 'ping_ms',
|
||||||
|
'netbios_info', 'ttl', 'ports', 'http_sender', 'web_detection',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function scan(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(NetworkScan::class, 'scan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function device(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(NetworkDevice::class, 'device_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class NetworkScan extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'subnet', 'source', 'scanner', 'total_hosts',
|
||||||
|
'online_hosts', 'new_devices', 'changed_devices',
|
||||||
|
'notes', 'created_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function hosts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(NetworkHost::class, 'scan_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Setting extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['key', 'value'];
|
||||||
|
|
||||||
|
public static function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return static::where('key', $key)->value('value') ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
static::updateOrCreate(['key' => $key], ['value' => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allAsArray(): array
|
||||||
|
{
|
||||||
|
return static::pluck('value', 'key')->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services\SettingsService;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class SettingsServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->singleton(SettingsService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Settings nur laden wenn die Tabelle existiert (z.B. vor Migration schützen)
|
||||||
|
if (!$this->app->runningInConsole() && Schema::hasTable('settings')) {
|
||||||
|
$settings = $this->app->make(SettingsService::class)->all();
|
||||||
|
|
||||||
|
View::share('appSettings', $settings);
|
||||||
|
} else {
|
||||||
|
View::share('appSettings', [
|
||||||
|
'site_name' => config('app.name'),
|
||||||
|
'site_logo' => '',
|
||||||
|
'button_color' => '#4f46e5',
|
||||||
|
'theme_mode' => 'light',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\NetworkDevice;
|
||||||
|
use App\Models\NetworkDeviceEvent;
|
||||||
|
use App\Models\NetworkHost;
|
||||||
|
use App\Models\NetworkScan;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class NetworkScanImporter
|
||||||
|
{
|
||||||
|
private NetworkScan $scan;
|
||||||
|
private int $newDevices = 0;
|
||||||
|
private int $changedDevices = 0;
|
||||||
|
private int $onlineHosts = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importiert eine Angry IP Scanner .txt Exportdatei.
|
||||||
|
*/
|
||||||
|
public function importAngryIpScannerFile(string $filePath, int $createdBy): NetworkScan
|
||||||
|
{
|
||||||
|
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
$scanner = '';
|
||||||
|
$subnet = '';
|
||||||
|
$headers = [];
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (str_starts_with($line, 'Erstellt von') || str_starts_with($line, 'Created by')) {
|
||||||
|
$scanner = trim(str_replace(['Erstellt von ', 'Created by '], '', $line));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_starts_with($line, 'Gescannt') || str_starts_with($line, 'Scanned')) {
|
||||||
|
// "Gescannt 192.168.86.0 - 192.168.86.255"
|
||||||
|
preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $line, $m);
|
||||||
|
$subnet = $m[1] ?? '0.0.0.0';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_starts_with($line, 'http') || str_starts_with($line, 'https')) {
|
||||||
|
continue; // URL-Zeile überspringen
|
||||||
|
}
|
||||||
|
// Header-Zeile (beginnt mit "IP")
|
||||||
|
if (str_starts_with($line, 'IP') && empty($headers)) {
|
||||||
|
$headers = preg_split('/\t+/', $line);
|
||||||
|
$headers = array_map('trim', $headers);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Datenzeile
|
||||||
|
if (!empty($headers) && str_starts_with(trim($line), '') && preg_match('/^\d{1,3}\./', trim($line))) {
|
||||||
|
$cols = preg_split('/\t+/', $line);
|
||||||
|
$row = [];
|
||||||
|
foreach ($headers as $i => $h) {
|
||||||
|
$row[$h] = trim($cols[$i] ?? '');
|
||||||
|
}
|
||||||
|
$rows[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($rows, $scanner, $subnet, $createdBy) {
|
||||||
|
$this->scan = NetworkScan::create([
|
||||||
|
'subnet' => $subnet,
|
||||||
|
'source' => 'import',
|
||||||
|
'scanner' => $scanner,
|
||||||
|
'created_by' => $createdBy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$this->processRow($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->scan->update([
|
||||||
|
'total_hosts' => count($rows),
|
||||||
|
'online_hosts' => $this->onlineHosts,
|
||||||
|
'new_devices' => $this->newDevices,
|
||||||
|
'changed_devices'=> $this->changedDevices,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->scan;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processRow(array $row): void
|
||||||
|
{
|
||||||
|
$ip = $this->extractColumn($row, ['IP']);
|
||||||
|
$ping = $this->extractColumn($row, ['Ping']);
|
||||||
|
$host = $this->extractColumn($row, ['Hostname']);
|
||||||
|
$mac = $this->normalizeMAC($this->extractColumn($row, ['MAC Addresse', 'MAC Address']));
|
||||||
|
$vendor = $this->extractColumn($row, ['MAC Hersteller', 'MAC Vendor']);
|
||||||
|
$ttl = (int) $this->extractColumn($row, ['TTL']) ?: null;
|
||||||
|
$ports = $this->extractColumn($row, ['Ports']);
|
||||||
|
$netbios= $this->extractColumn($row, ['NetBIOS Info']);
|
||||||
|
$http = $this->extractColumn($row, ['HTTP Sender']);
|
||||||
|
$web = $this->extractColumn($row, ['Web Erkennung', 'Web Detection']);
|
||||||
|
|
||||||
|
$pingMs = null;
|
||||||
|
$status = 'offline';
|
||||||
|
|
||||||
|
if (!empty($ping) && $ping !== 'n/a' && $ping !== '-') {
|
||||||
|
preg_match('/(\d+)/', $ping, $m);
|
||||||
|
$pingMs = isset($m[1]) ? (int)$m[1] : null;
|
||||||
|
$status = 'online';
|
||||||
|
$this->onlineHosts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host-Eintrag speichern
|
||||||
|
$host_record = NetworkHost::create([
|
||||||
|
'scan_id' => $this->scan->id,
|
||||||
|
'ip_address' => $ip,
|
||||||
|
'mac_address' => $mac ?: null,
|
||||||
|
'hostname' => $host ?: null,
|
||||||
|
'mac_vendor' => $vendor ?: null,
|
||||||
|
'status' => $status,
|
||||||
|
'ping_ms' => $pingMs,
|
||||||
|
'netbios_info' => $netbios ?: null,
|
||||||
|
'ttl' => $ttl,
|
||||||
|
'ports' => $ports ?: null,
|
||||||
|
'http_sender' => $http ?: null,
|
||||||
|
'web_detection' => $web ?: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Geräte-Master nur wenn MAC bekannt
|
||||||
|
if (!empty($mac)) {
|
||||||
|
$this->processDevice($host_record, $ip, $mac, $host, $vendor, $netbios, $ttl, $ports, $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processDevice(
|
||||||
|
NetworkHost $hostRecord,
|
||||||
|
string $ip, string $mac, ?string $hostname,
|
||||||
|
?string $vendor, ?string $netbios, ?int $ttl,
|
||||||
|
?string $ports, string $status
|
||||||
|
): void {
|
||||||
|
$device = NetworkDevice::firstOrNew(['mac_address' => $mac]);
|
||||||
|
$isNew = !$device->exists;
|
||||||
|
|
||||||
|
if ($isNew) {
|
||||||
|
$device->fill([
|
||||||
|
'current_ip' => $ip,
|
||||||
|
'hostname' => $hostname,
|
||||||
|
'mac_vendor' => $vendor,
|
||||||
|
'status' => $status,
|
||||||
|
'netbios_name' => $netbios,
|
||||||
|
'ttl' => $ttl,
|
||||||
|
'ports' => $ports,
|
||||||
|
'first_seen_at'=> now(),
|
||||||
|
'last_seen_at' => now(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
NetworkDeviceEvent::create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'scan_id' => $this->scan->id,
|
||||||
|
'event_type' => 'new_device',
|
||||||
|
'new_value' => $ip,
|
||||||
|
'description'=> "Erstes Erscheinen: {$ip} ({$vendor})",
|
||||||
|
]);
|
||||||
|
$this->newDevices++;
|
||||||
|
} else {
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
// IP-Änderung erkennen
|
||||||
|
if ($device->current_ip !== $ip) {
|
||||||
|
$events[] = [
|
||||||
|
'event_type' => 'ip_changed',
|
||||||
|
'old_value' => $device->current_ip,
|
||||||
|
'new_value' => $ip,
|
||||||
|
'description'=> "IP geändert von {$device->current_ip} zu {$ip}",
|
||||||
|
];
|
||||||
|
$this->changedDevices++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online/Offline-Status
|
||||||
|
if ($device->status !== $status) {
|
||||||
|
$events[] = [
|
||||||
|
'event_type' => $status === 'online' ? 'came_online' : 'went_offline',
|
||||||
|
'old_value' => $device->status,
|
||||||
|
'new_value' => $status,
|
||||||
|
'description'=> $status === 'online'
|
||||||
|
? "Gerät wieder online ({$ip})"
|
||||||
|
: "Gerät offline ({$device->current_ip})",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$device->update([
|
||||||
|
'current_ip' => $ip,
|
||||||
|
'hostname' => $hostname ?? $device->hostname,
|
||||||
|
'status' => $status,
|
||||||
|
'last_seen_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
NetworkDeviceEvent::create(array_merge($event, [
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'scan_id' => $this->scan->id,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostRecord->update(['device_id' => $device->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractColumn(array $row, array $keys): string
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (isset($row[$key]) && $row[$key] !== '') {
|
||||||
|
return $row[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeMAC(string $mac): string
|
||||||
|
{
|
||||||
|
// Verschiedene MAC-Formate normalisieren zu XX:XX:XX:XX:XX:XX
|
||||||
|
$clean = preg_replace('/[^a-fA-F0-9]/', '', $mac);
|
||||||
|
if (strlen($clean) !== 12) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return strtoupper(implode(':', str_split($clean, 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class SettingsService
|
||||||
|
{
|
||||||
|
private const CACHE_KEY = 'app_settings';
|
||||||
|
private const CACHE_TTL = 3600;
|
||||||
|
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, fn() => Setting::allAsArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return $this->all()[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
Setting::set($key, $value);
|
||||||
|
Cache::forget(self::CACHE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMany(array $data): void
|
||||||
|
{
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
Setting::set($key, $value);
|
||||||
|
}
|
||||||
|
Cache::forget(self::CACHE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -6,13 +6,20 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withProviders([
|
||||||
|
App\Providers\SettingsServiceProvider::class,
|
||||||
|
])
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
$middleware->alias([
|
||||||
|
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
||||||
|
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
||||||
|
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('key')->unique();
|
||||||
|
$table->text('value')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('network_scans', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('subnet'); // z.B. 192.168.86.0/24
|
||||||
|
$table->string('source')->default('manual'); // manual | import | auto
|
||||||
|
$table->string('scanner')->nullable(); // z.B. "Angry IP Scanner 3.9.3"
|
||||||
|
$table->integer('total_hosts')->default(0);
|
||||||
|
$table->integer('online_hosts')->default(0);
|
||||||
|
$table->integer('new_devices')->default(0);
|
||||||
|
$table->integer('changed_devices')->default(0);
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('network_scans');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('network_devices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('mac_address')->unique(); // Primärschlüssel-Logik
|
||||||
|
$table->string('current_ip')->nullable();
|
||||||
|
$table->string('hostname')->nullable();
|
||||||
|
$table->string('mac_vendor')->nullable(); // Hersteller
|
||||||
|
$table->string('status')->default('unknown'); // online | offline | unknown
|
||||||
|
$table->string('netbios_name')->nullable();
|
||||||
|
$table->integer('ttl')->nullable();
|
||||||
|
$table->text('ports')->nullable();
|
||||||
|
$table->text('notes')->nullable(); // manuelle Notizen
|
||||||
|
$table->string('label')->nullable(); // Benutzerfreundlicher Name
|
||||||
|
$table->timestamp('first_seen_at')->nullable();
|
||||||
|
$table->timestamp('last_seen_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('network_devices');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('network_hosts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('scan_id')->constrained('network_scans')->cascadeOnDelete();
|
||||||
|
$table->foreignId('device_id')->nullable()->constrained('network_devices')->nullOnDelete();
|
||||||
|
$table->string('ip_address');
|
||||||
|
$table->string('mac_address')->nullable();
|
||||||
|
$table->string('hostname')->nullable();
|
||||||
|
$table->string('mac_vendor')->nullable();
|
||||||
|
$table->string('status')->default('offline'); // online | offline | filtered
|
||||||
|
$table->integer('ping_ms')->nullable();
|
||||||
|
$table->string('netbios_info')->nullable();
|
||||||
|
$table->integer('ttl')->nullable();
|
||||||
|
$table->text('ports')->nullable();
|
||||||
|
$table->text('http_sender')->nullable();
|
||||||
|
$table->text('web_detection')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['scan_id', 'ip_address']);
|
||||||
|
$table->index('mac_address');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('network_hosts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('network_device_events', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('device_id')->constrained('network_devices')->cascadeOnDelete();
|
||||||
|
$table->foreignId('scan_id')->nullable()->constrained('network_scans')->nullOnDelete();
|
||||||
|
$table->string('event_type'); // new_device | ip_changed | came_online | went_offline | manual_note | label_changed
|
||||||
|
$table->string('old_value')->nullable();
|
||||||
|
$table->string('new_value')->nullable();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->boolean('documented')->default(false); // manuell bestätigt
|
||||||
|
$table->foreignId('documented_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamp('documented_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['device_id', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('network_device_events');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
RolesAndPermissionsSeeder::class,
|
RolesAndPermissionsSeeder::class,
|
||||||
|
SettingsSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class SettingsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$defaults = [
|
||||||
|
'site_name' => 'Network-MGMT',
|
||||||
|
'site_logo' => '',
|
||||||
|
'button_color' => '#4f46e5', // Indigo-600
|
||||||
|
'theme_mode' => 'light', // light | dark
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($defaults as $key => $value) {
|
||||||
|
Setting::firstOrCreate(['key' => $key], ['value' => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('Default settings seeded.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
Layout-Einstellungen
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="p-4 bg-green-100 text-green-800 rounded-md">
|
||||||
|
{{ session('success') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.layout.update') }}" enctype="multipart/form-data"
|
||||||
|
class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-6 space-y-8">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
{{-- Site-Name --}}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4 pb-2 border-b border-gray-200">
|
||||||
|
Allgemein
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<x-input-label for="site_name" value="Seitenname" />
|
||||||
|
<x-text-input id="site_name" name="site_name" type="text" class="mt-1 block w-full"
|
||||||
|
value="{{ old('site_name', $settings['site_name'] ?? 'Network-MGMT') }}" required />
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Wird im Browser-Tab und in der Navigation angezeigt.</p>
|
||||||
|
<x-input-error :messages="$errors->get('site_name')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Logo --}}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4 pb-2 border-b border-gray-200">
|
||||||
|
Logo
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
@if(!empty($settings['site_logo']))
|
||||||
|
<div class="mb-4 flex items-center space-x-4">
|
||||||
|
<img src="{{ asset('storage/' . $settings['site_logo']) }}"
|
||||||
|
alt="Logo" class="h-12 object-contain border rounded p-1 bg-gray-50">
|
||||||
|
<a href="{{ route('admin.layout.removeLogo') }}"
|
||||||
|
onclick="return confirm('Logo wirklich entfernen?')"
|
||||||
|
class="text-sm text-red-600 hover:text-red-800">Logo entfernen</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="site_logo" value="Logo hochladen (PNG, JPG – max. 2 MB)" />
|
||||||
|
<input id="site_logo" name="site_logo" type="file" accept="image/*"
|
||||||
|
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4
|
||||||
|
file:rounded-md file:border-0 file:text-sm file:font-medium
|
||||||
|
file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
|
||||||
|
<x-input-error :messages="$errors->get('site_logo')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Button-Farbe --}}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4 pb-2 border-b border-gray-200">
|
||||||
|
Button-Farbe
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<input id="button_color" name="button_color" type="color"
|
||||||
|
value="{{ old('button_color', $settings['button_color'] ?? '#4f46e5') }}"
|
||||||
|
class="h-10 w-20 rounded border border-gray-300 cursor-pointer p-1" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">Primärfarbe für Buttons und Akzente</p>
|
||||||
|
<p class="text-xs text-gray-500" id="color_preview_text">
|
||||||
|
{{ old('button_color', $settings['button_color'] ?? '#4f46e5') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
id="preview_btn"
|
||||||
|
style="background-color: {{ old('button_color', $settings['button_color'] ?? '#4f46e5') }}"
|
||||||
|
class="px-4 py-2 text-white text-sm font-medium rounded-md transition">
|
||||||
|
Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<x-input-error :messages="$errors->get('button_color')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Dark / Light Mode --}}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4 pb-2 border-b border-gray-200">
|
||||||
|
Erscheinungsbild
|
||||||
|
</h3>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input type="radio" name="theme_mode" value="light"
|
||||||
|
{{ ($settings['theme_mode'] ?? 'light') === 'light' ? 'checked' : '' }}
|
||||||
|
class="text-indigo-600 focus:ring-indigo-500" />
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
☀️ Hell (Light Mode)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input type="radio" name="theme_mode" value="dark"
|
||||||
|
{{ ($settings['theme_mode'] ?? 'light') === 'dark' ? 'checked' : '' }}
|
||||||
|
class="text-indigo-600 focus:ring-indigo-500" />
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
🌙 Dunkel (Dark Mode)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Speichern --}}
|
||||||
|
<div class="flex justify-end pt-2 border-t border-gray-200">
|
||||||
|
<button type="submit"
|
||||||
|
id="save_btn"
|
||||||
|
style="background-color: {{ $settings['button_color'] ?? '#4f46e5' }}"
|
||||||
|
class="inline-flex items-center px-6 py-2 text-white text-sm font-medium rounded-md hover:opacity-90 transition">
|
||||||
|
Einstellungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const colorInput = document.getElementById('button_color');
|
||||||
|
const previewBtn = document.getElementById('preview_btn');
|
||||||
|
const saveBtn = document.getElementById('save_btn');
|
||||||
|
const colorText = document.getElementById('color_preview_text');
|
||||||
|
|
||||||
|
colorInput.addEventListener('input', function () {
|
||||||
|
previewBtn.style.backgroundColor = this.value;
|
||||||
|
saveBtn.style.backgroundColor = this.value;
|
||||||
|
colorText.textContent = this.value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</x-app-layout>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
<x-app-layout>
|
<x-app-layout>
|
||||||
<x-slot name="header">
|
<x-slot name="header">
|
||||||
<div class="flex items-center justify-between">
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
Benutzerverwaltung
|
||||||
Benutzerverwaltung
|
</h2>
|
||||||
</h2>
|
|
||||||
<a href="{{ route('admin.users.create') }}"
|
|
||||||
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 transition">
|
|
||||||
+ Neuer Benutzer
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{{-- Toolbar --}}
|
||||||
|
<div class="flex justify-end mb-4">
|
||||||
|
<a href="{{ route('admin.users.create') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700 transition">
|
||||||
|
+ Neuer Benutzer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Flash-Meldungen --}}
|
{{-- Flash-Meldungen --}}
|
||||||
@if(session('success'))
|
@if(session('success'))
|
||||||
<div class="mb-4 p-4 bg-green-100 text-green-800 rounded-md">
|
<div class="mb-4 p-4 bg-green-100 text-green-800 rounded-md">
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
@php
|
||||||
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
$logoPath = $appSettings['site_logo'] ?? '';
|
||||||
</svg>
|
@endphp
|
||||||
|
|
||||||
|
@if(!empty($logoPath))
|
||||||
|
<img src="{{ asset('storage/' . $logoPath) }}"
|
||||||
|
alt="{{ $appSettings['site_name'] ?? config('app.name') }}"
|
||||||
|
{{ $attributes->merge(['class' => 'object-contain']) }}
|
||||||
|
style="max-height: 2.25rem;" />
|
||||||
|
@else
|
||||||
|
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||||
|
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
|
||||||
|
</svg>
|
||||||
|
@endif
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
Changelog
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-8">
|
||||||
|
<div class="prose dark:prose-invert max-w-none">
|
||||||
|
@php
|
||||||
|
// Einfaches Markdown-to-HTML für den Changelog
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
$html = '';
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (str_starts_with($line, '## ')) {
|
||||||
|
$html .= '<h2 class="text-xl font-bold mt-6 mb-2 text-gray-900 dark:text-gray-100 border-b pb-1">'
|
||||||
|
. e(substr($line, 3)) . '</h2>';
|
||||||
|
} elseif (str_starts_with($line, '### ')) {
|
||||||
|
$html .= '<h3 class="text-base font-semibold mt-4 mb-1 text-indigo-700 dark:text-indigo-400">'
|
||||||
|
. e(substr($line, 4)) . '</h3>';
|
||||||
|
} elseif (str_starts_with($line, '# ')) {
|
||||||
|
$html .= '<h1 class="text-2xl font-bold mb-4 text-gray-900 dark:text-gray-100">'
|
||||||
|
. e(substr($line, 2)) . '</h1>';
|
||||||
|
} elseif (str_starts_with($line, '- ')) {
|
||||||
|
$html .= '<li class="ml-4 text-sm text-gray-700 dark:text-gray-300 list-disc">'
|
||||||
|
. e(substr($line, 2)) . '</li>';
|
||||||
|
} elseif (trim($line) === '---') {
|
||||||
|
$html .= '<hr class="my-4 border-gray-200 dark:border-gray-700">';
|
||||||
|
} elseif (trim($line) !== '') {
|
||||||
|
$html .= '<p class="text-sm text-gray-600 dark:text-gray-400 my-1">'
|
||||||
|
. e($line) . '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
{!! $html !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
Handbuch
|
||||||
|
</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg p-8 space-y-8">
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-3">Erste Schritte</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
|
Network-MGMT ist eine webbasierte Verwaltungsplattform für Netzwerk-Ressourcen
|
||||||
|
mit rollenbasierter Zugriffskontrolle.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-3">Rollen & Rechte</h2>
|
||||||
|
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<table class="min-w-full text-sm">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300">Rolle</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300">Beschreibung</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-600 dark:text-gray-300">Berechtigungen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2"><span class="px-2 py-0.5 bg-red-100 text-red-800 rounded-full text-xs">admin</span></td>
|
||||||
|
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">Administrator</td>
|
||||||
|
<td class="px-4 py-2 text-gray-600 dark:text-gray-400">Vollzugriff auf alle Funktionen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2"><span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded-full text-xs">manager</span></td>
|
||||||
|
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">Manager</td>
|
||||||
|
<td class="px-4 py-2 text-gray-600 dark:text-gray-400">Netzwerk lesen, anlegen, bearbeiten; Benutzer lesen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2"><span class="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full text-xs">user</span></td>
|
||||||
|
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzer</td>
|
||||||
|
<td class="px-4 py-2 text-gray-600 dark:text-gray-400">Netzwerk lesen</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-3">Benutzerverwaltung</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
|
Unter <strong>Einstellungen → Benutzerverwaltung</strong> können Administratoren
|
||||||
|
neue Benutzer anlegen, bestehende bearbeiten und Rollen zuweisen.
|
||||||
|
Der eigene Account kann nicht gelöscht werden.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-3">Layout-Einstellungen</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
|
Unter <strong>Einstellungen → Layout</strong> kann der Seitenname, das Logo,
|
||||||
|
die Button-Farbe sowie der Dark/Light-Mode konfiguriert werden.
|
||||||
|
Änderungen werden sofort für alle Benutzer wirksam.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Network-MGMT · Version {{ config('app.version', '0.4.0') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
|
||||||
|
class="{{ ($appSettings['theme_mode'] ?? 'light') === 'dark' ? 'dark' : '' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
<title>{{ $appSettings['site_name'] ?? config('app.name', 'Network-MGMT') }}</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
@@ -13,14 +14,28 @@
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<!-- Dynamic Settings -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--color-primary: {{ $appSettings['button_color'] ?? '#4f46e5' }};
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div class="min-h-screen bg-gray-100">
|
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
@include('layouts.navigation')
|
@include('layouts.navigation')
|
||||||
|
|
||||||
<!-- Page Heading -->
|
<!-- Page Heading -->
|
||||||
@isset($header)
|
@isset($header)
|
||||||
<header class="bg-white shadow">
|
<header class="bg-white dark:bg-gray-800 shadow">
|
||||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
{{ $header }}
|
{{ $header }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
||||||
<!-- Primary Navigation Menu -->
|
<!-- Primary Navigation Menu -->
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex justify-between h-16">
|
<div class="flex justify-between h-16">
|
||||||
@@ -15,11 +15,55 @@
|
|||||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-nav-link>
|
</x-nav-link>
|
||||||
|
|
||||||
@role('admin')
|
@role('admin')
|
||||||
<x-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.*')">
|
{{-- Einstellungen-Dropdown --}}
|
||||||
Benutzerverwaltung
|
<x-dropdown align="left" width="48">
|
||||||
</x-nav-link>
|
<x-slot name="trigger">
|
||||||
|
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none
|
||||||
|
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||||
|
Einstellungen
|
||||||
|
<svg class="ms-1 fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</x-slot>
|
||||||
|
<x-slot name="content">
|
||||||
|
<x-dropdown-link :href="route('admin.users.index')">
|
||||||
|
👥 Benutzerverwaltung
|
||||||
|
</x-dropdown-link>
|
||||||
|
<x-dropdown-link :href="route('admin.layout.index')">
|
||||||
|
🎨 Layout
|
||||||
|
</x-dropdown-link>
|
||||||
|
</x-slot>
|
||||||
|
</x-dropdown>
|
||||||
@endrole
|
@endrole
|
||||||
|
|
||||||
|
{{-- Netzwerk-Link --}}
|
||||||
|
<x-nav-link :href="route('network.index')" :active="request()->routeIs('network.*')">
|
||||||
|
🌐 Netzwerk
|
||||||
|
</x-nav-link>
|
||||||
|
|
||||||
|
{{-- Hilfe-Dropdown --}}
|
||||||
|
<x-dropdown align="left" width="48">
|
||||||
|
<x-slot name="trigger">
|
||||||
|
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none
|
||||||
|
{{ request()->routeIs('help.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||||
|
Hilfe
|
||||||
|
<svg class="ms-1 fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</x-slot>
|
||||||
|
<x-slot name="content">
|
||||||
|
<x-dropdown-link :href="route('help.manual')">
|
||||||
|
📖 Handbuch
|
||||||
|
</x-dropdown-link>
|
||||||
|
<x-dropdown-link :href="route('help.changelog')">
|
||||||
|
📋 Changelog
|
||||||
|
</x-dropdown-link>
|
||||||
|
</x-slot>
|
||||||
|
</x-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,11 +119,34 @@
|
|||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
|
|
||||||
@role('admin')
|
@role('admin')
|
||||||
<x-responsive-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.*')">
|
<div class="pt-2 pb-1 border-t border-gray-200">
|
||||||
Benutzerverwaltung
|
<div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">Einstellungen</div>
|
||||||
</x-responsive-nav-link>
|
<x-responsive-nav-link :href="route('admin.users.index')">
|
||||||
|
👥 Benutzerverwaltung
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('admin.layout.index')">
|
||||||
|
🎨 Layout
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
@endrole
|
@endrole
|
||||||
|
|
||||||
|
<div class="pt-2 pb-1 border-t border-gray-200">
|
||||||
|
<x-responsive-nav-link :href="route('network.index')" :active="request()->routeIs('network.*')">
|
||||||
|
🌐 Netzwerk
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 pb-1 border-t border-gray-200">
|
||||||
|
<div class="px-4 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">Hilfe</div>
|
||||||
|
<x-responsive-nav-link :href="route('help.manual')">
|
||||||
|
📖 Handbuch
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
<x-responsive-nav-link :href="route('help.changelog')">
|
||||||
|
📋 Changelog
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Responsive Settings Options -->
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="{{ route('network.devices') }}" class="text-gray-500 hover:text-gray-700">Geräte</a>
|
||||||
|
<span class="text-gray-400">/</span>
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">{{ $device->display_name }}</h2>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8">
|
||||||
|
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="p-4 bg-green-100 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Stammdaten --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center space-x-2 mb-1">
|
||||||
|
<span class="w-3 h-3 rounded-full {{ $device->status === 'online' ? 'bg-green-500' : 'bg-red-400' }}"></span>
|
||||||
|
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ $device->display_name }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Erstmals gesehen: {{ $device->first_seen_at?->format('d.m.Y H:i') }}
|
||||||
|
· Zuletzt: {{ $device->last_seen_at?->format('d.m.Y H:i') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">IP-Adresse</p>
|
||||||
|
<p class="mt-1 font-mono font-semibold text-gray-900 dark:text-gray-100">{{ $device->current_ip ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">MAC-Adresse</p>
|
||||||
|
<p class="mt-1 font-mono font-semibold text-gray-900 dark:text-gray-100">{{ $device->mac_address }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Hersteller</p>
|
||||||
|
<p class="mt-1 text-gray-900 dark:text-gray-100">{{ $device->mac_vendor ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Hostname</p>
|
||||||
|
<p class="mt-1 text-gray-900 dark:text-gray-100">{{ $device->hostname ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">NetBIOS</p>
|
||||||
|
<p class="mt-1 text-gray-900 dark:text-gray-100">{{ $device->netbios_name ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Offene Ports</p>
|
||||||
|
<p class="mt-1 text-gray-900 dark:text-gray-100">{{ $device->ports ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Bezeichnung & Notiz --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">Bezeichnung & Notizen</h3>
|
||||||
|
<form method="POST" action="{{ route('network.device.update', $device) }}" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<div>
|
||||||
|
<x-input-label for="label" value="Bezeichnung (eigener Name)" />
|
||||||
|
<x-text-input id="label" name="label" type="text" class="mt-1 block w-full"
|
||||||
|
value="{{ old('label', $device->label) }}"
|
||||||
|
placeholder="z.B. NAS-Server, Drucker Büro, ..." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-input-label for="notes" value="Notizen" />
|
||||||
|
<textarea id="notes" name="notes" rows="3"
|
||||||
|
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"
|
||||||
|
placeholder="Interne Notizen zu diesem Gerät...">{{ old('notes', $device->notes) }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<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">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Manuelle Notiz --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4">Manuelle Notiz hinzufügen</h3>
|
||||||
|
<form method="POST" action="{{ route('network.device.note', $device) }}" class="flex gap-3">
|
||||||
|
@csrf
|
||||||
|
<input type="text" name="description" required
|
||||||
|
placeholder="Änderung, Beobachtung, Aufgabe..."
|
||||||
|
class="flex-1 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" />
|
||||||
|
<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 whitespace-nowrap">
|
||||||
|
+ Notiz
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- IP-Verlauf --}}
|
||||||
|
@if($ipHistory->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">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100">IP-Verlauf</h3>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Datum</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">IP</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
|
||||||
|
<th class="px-4 py-2 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">
|
||||||
|
@foreach($ipHistory as $h)
|
||||||
|
<tr>
|
||||||
|
<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">
|
||||||
|
<span class="text-xs {{ $h->status === 'online' ? 'text-green-600' : 'text-gray-400' }}">{{ $h->status }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-gray-500 dark:text-gray-400">{{ $h->ping_ms ? $h->ping_ms . ' ms' : '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Ereignis-Protokoll --}}
|
||||||
|
<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">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Ereignis-Protokoll</h3>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
@forelse($device->events as $event)
|
||||||
|
<div class="flex items-start justify-between px-5 py-3">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<span class="mt-1 w-2 h-2 rounded-full flex-shrink-0
|
||||||
|
{{ $event->event_color === 'blue' ? 'bg-blue-500' :
|
||||||
|
($event->event_color === 'yellow' ? 'bg-yellow-500' :
|
||||||
|
($event->event_color === 'green' ? 'bg-green-500' :
|
||||||
|
($event->event_color === 'red' ? 'bg-red-500' :
|
||||||
|
($event->event_color === 'purple' ? 'bg-purple-500' : 'bg-gray-400')))) }}">
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $event->event_label }}</span>
|
||||||
|
@if($event->old_value && $event->new_value)
|
||||||
|
<span class="text-xs text-gray-400">
|
||||||
|
{{ $event->old_value }} → {{ $event->new_value }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $event->description }}
|
||||||
|
· {{ $event->created_at->format('d.m.Y H:i') }}
|
||||||
|
@if($event->documented)
|
||||||
|
· <span class="text-green-600">✓ dokumentiert</span>
|
||||||
|
@if($event->documentedBy) von {{ $event->documentedBy->name }} @endif
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(!$event->documented)
|
||||||
|
<form method="POST" action="{{ route('network.document', $event) }}" class="ml-4 flex-shrink-0">
|
||||||
|
@csrf
|
||||||
|
<button type="submit"
|
||||||
|
class="text-xs px-3 py-1 rounded border border-green-300 text-green-700 hover:bg-green-50 transition">
|
||||||
|
✓ Bestätigen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="px-5 py-8 text-center text-sm text-gray-500">Keine Ereignisse vorhanden.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">Alle Geräte</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{{-- Filter --}}
|
||||||
|
<form method="GET" action="{{ route('network.devices') }}" class="flex gap-3 mb-4">
|
||||||
|
<input type="text" name="search" value="{{ request('search') }}"
|
||||||
|
placeholder="IP, MAC, Hostname, Hersteller..."
|
||||||
|
class="flex-1 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" />
|
||||||
|
<select name="status"
|
||||||
|
class="border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm">
|
||||||
|
<option value="">Alle Status</option>
|
||||||
|
<option value="online" {{ request('status') === 'online' ? 'selected' : '' }}>Online</option>
|
||||||
|
<option value="offline" {{ request('status') === 'offline' ? 'selected' : '' }}>Offline</option>
|
||||||
|
</select>
|
||||||
|
<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">
|
||||||
|
Suchen
|
||||||
|
</button>
|
||||||
|
@if(request()->hasAny(['search', 'status']))
|
||||||
|
<a href="{{ route('network.devices') }}"
|
||||||
|
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">
|
||||||
|
Zurücksetzen
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<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-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 / Bezeichnung</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">Zuletzt gesehen</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ereignisse</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
@forelse($devices as $device)
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<span class="w-2 h-2 rounded-full mr-2
|
||||||
|
{{ $device->status === 'online' ? 'bg-green-500' : 'bg-red-400' }}">
|
||||||
|
</span>
|
||||||
|
<span class="text-xs {{ $device->status === 'online' ? 'text-green-700 dark:text-green-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||||
|
{{ $device->status }}
|
||||||
|
</span>
|
||||||
|
</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-gray-600 dark:text-gray-400 text-xs">
|
||||||
|
{{ $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 ?? '—' }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 text-xs">
|
||||||
|
{{ $device->mac_vendor ?? '—' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-500 dark:text-gray-400 text-xs">
|
||||||
|
{{ $device->last_seen_at?->format('d.m.Y H:i') ?? '—' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
@php $undoc = $device->events->where('documented', false)->count(); @endphp
|
||||||
|
<a href="{{ route('network.device', $device) }}"
|
||||||
|
class="text-indigo-600 hover:text-indigo-800 text-xs">
|
||||||
|
Detail
|
||||||
|
@if($undoc > 0)
|
||||||
|
<span class="ml-1 bg-yellow-100 text-yellow-800 text-xs px-1.5 py-0.5 rounded-full">{{ $undoc }}</span>
|
||||||
|
@endif
|
||||||
|
</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
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<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 importieren</h2>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8">
|
||||||
|
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="mb-4 p-4 bg-green-100 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
Importiere den Textexport von <strong>Angry IP Scanner</strong>.<br>
|
||||||
|
Exportformat: <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">Datei → Speichern als → Komma-getrennte Textdatei (.txt)</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('network.import') }}" enctype="multipart/form-data" class="space-y-5">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="scan_file" value="Angry IP Scanner Export (.txt)" />
|
||||||
|
<input id="scan_file" name="scan_file" type="file" accept=".txt,.csv" required
|
||||||
|
class="mt-1 block w-full text-sm text-gray-500
|
||||||
|
file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0
|
||||||
|
file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700
|
||||||
|
hover:file:bg-indigo-100 dark:file:bg-indigo-900 dark:file:text-indigo-300" />
|
||||||
|
<x-input-error :messages="$errors->get('scan_file')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" style="background-color: var(--color-primary)"
|
||||||
|
class="px-6 py-2 text-white text-sm font-medium rounded-md hover:opacity-90 transition">
|
||||||
|
Import starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<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 leading-tight">Netzwerk-Übersicht</h2>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<a href="{{ route('network.devices') }}"
|
||||||
|
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">
|
||||||
|
Alle Geräte
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('network.import') }}"
|
||||||
|
style="background-color: var(--color-primary)"
|
||||||
|
class="px-3 py-2 text-sm font-medium rounded-md text-white hover:opacity-90 transition">
|
||||||
|
+ Scan importieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="p-4 bg-green-100 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- KPI-Karten --}}
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-5">
|
||||||
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Geräte gesamt</p>
|
||||||
|
<p class="mt-1 text-3xl font-bold text-gray-900 dark:text-gray-100">{{ $totalDevices }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-5">
|
||||||
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Aktuell online</p>
|
||||||
|
<p class="mt-1 text-3xl font-bold text-green-600">{{ $onlineDevices }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-5">
|
||||||
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Scans gesamt</p>
|
||||||
|
<p class="mt-1 text-3xl font-bold text-gray-900 dark:text-gray-100">{{ $scans->count() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-5">
|
||||||
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Undokumentierte Ereignisse</p>
|
||||||
|
<p class="mt-1 text-3xl font-bold text-yellow-600">{{ $recentEvents->count() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{{-- Letzte Scans --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden">
|
||||||
|
<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">Letzte Scans</h3>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
@forelse($scans as $scan)
|
||||||
|
<a href="{{ route('network.scan', $scan) }}"
|
||||||
|
class="flex items-center justify-between px-5 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $scan->subnet }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $scan->created_at->format('d.m.Y H:i') }} · {{ $scan->scanner }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-sm font-semibold text-green-600">{{ $scan->online_hosts }} online</span>
|
||||||
|
@if($scan->new_devices > 0)
|
||||||
|
<span class="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full">{{ $scan->new_devices }} neu</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@empty
|
||||||
|
<p class="px-5 py-8 text-center text-sm text-gray-500">Noch keine Scans vorhanden.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Undokumentierte Ereignisse --}}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm overflow-hidden">
|
||||||
|
<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">Undokumentierte Ereignisse</h3>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
@forelse($recentEvents as $event)
|
||||||
|
<div class="flex items-start justify-between px-5 py-3">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<span class="mt-0.5 inline-block w-2 h-2 rounded-full flex-shrink-0
|
||||||
|
{{ $event->event_color === 'blue' ? 'bg-blue-500' :
|
||||||
|
($event->event_color === 'yellow' ? 'bg-yellow-500' :
|
||||||
|
($event->event_color === 'green' ? 'bg-green-500' :
|
||||||
|
($event->event_color === 'red' ? 'bg-red-500' : 'bg-gray-500'))) }}">
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ $event->event_label }}
|
||||||
|
@if($event->device)
|
||||||
|
<a href="{{ route('network.device', $event->device) }}"
|
||||||
|
class="text-indigo-600 hover:underline">
|
||||||
|
{{ $event->device->display_name }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $event->description }}
|
||||||
|
· {{ $event->created_at->diffForHumans() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('network.document', $event) }}" class="ml-3 flex-shrink-0">
|
||||||
|
@csrf
|
||||||
|
<button type="submit"
|
||||||
|
class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||||
|
✓ OK
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="px-5 py-8 text-center text-sm text-green-600">Alle Ereignisse dokumentiert.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<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>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-8">
|
||||||
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
|
|
||||||
|
{{-- Scan-Info --}}
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
|
@foreach(['Subnetz' => $scan->subnet, 'Quelle' => $scan->scanner, 'Gesamt' => $scan->total_hosts, 'Online' => $scan->online_hosts, 'Neue Geräte' => $scan->new_devices] as $label => $val)
|
||||||
|
<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">{{ $label }}</p>
|
||||||
|
<p class="mt-1 text-xl font-bold text-gray-900 dark:text-gray-100">{{ $val }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Host-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-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">Ping</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ports</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
@foreach($hosts as $host)
|
||||||
|
<tr class="{{ $host->status === 'online' ? '' : 'opacity-50' }} hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<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-2 font-mono font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
@if($host->device)
|
||||||
|
<a href="{{ route('network.device', $host->device) }}" class="text-indigo-600 hover:underline">
|
||||||
|
{{ $host->ip_address }}
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
{{ $host->ip_address }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">{{ $host->mac_address ?? '—' }}</td>
|
||||||
|
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">{{ $host->hostname ?? '—' }}</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"
|
||||||
|
class="{{ ($appSettings['theme_mode'] ?? 'light') === 'dark' ? 'dark' : '' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
<title>{{ $appSettings['site_name'] ?? config('app.name', 'Network-MGMT') }}</title>
|
||||||
|
|
||||||
@fonts
|
@fonts
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
|
use App\Http\Controllers\HelpController;
|
||||||
|
use App\Http\Controllers\NetworkController;
|
||||||
use App\Http\Controllers\Admin\UserController as AdminUserController;
|
use App\Http\Controllers\Admin\UserController as AdminUserController;
|
||||||
|
use App\Http\Controllers\Admin\LayoutController as AdminLayoutController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
@@ -25,6 +28,34 @@ Route::prefix('admin')
|
|||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', fn() => redirect()->route('admin.users.index'));
|
Route::get('/', fn() => redirect()->route('admin.users.index'));
|
||||||
Route::resource('users', AdminUserController::class);
|
Route::resource('users', AdminUserController::class);
|
||||||
|
Route::get('layout', [AdminLayoutController::class, 'index'])->name('layout.index');
|
||||||
|
Route::put('layout', [AdminLayoutController::class, 'update'])->name('layout.update');
|
||||||
|
Route::get('layout/remove-logo', [AdminLayoutController::class, 'removeLogo'])->name('layout.removeLogo');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Netzwerk-Bereich – für alle eingeloggten Benutzer
|
||||||
|
Route::prefix('network')
|
||||||
|
->name('network.')
|
||||||
|
->middleware(['auth'])
|
||||||
|
->group(function () {
|
||||||
|
Route::get('/', [NetworkController::class, 'index'])->name('index');
|
||||||
|
Route::get('/devices', [NetworkController::class, 'devices'])->name('devices');
|
||||||
|
Route::get('/devices/{device}', [NetworkController::class, 'device'])->name('device');
|
||||||
|
Route::put('/devices/{device}', [NetworkController::class, 'updateDevice'])->name('device.update');
|
||||||
|
Route::post('/devices/{device}/note', [NetworkController::class, 'addNote'])->name('device.note');
|
||||||
|
Route::post('/events/{event}/document', [NetworkController::class, 'documentEvent'])->name('document');
|
||||||
|
Route::get('/import', [NetworkController::class, 'showImport'])->name('import');
|
||||||
|
Route::post('/import', [NetworkController::class, 'import'])->name('import');
|
||||||
|
Route::get('/scans/{scan}', [NetworkController::class, 'scan'])->name('scan');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hilfe-Bereich – für alle eingeloggten Benutzer
|
||||||
|
Route::prefix('help')
|
||||||
|
->name('help.')
|
||||||
|
->middleware(['auth'])
|
||||||
|
->group(function () {
|
||||||
|
Route::get('manual', [HelpController::class, 'manual'])->name('manual');
|
||||||
|
Route::get('changelog', [HelpController::class, 'changelog'])->name('changelog');
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export default {
|
|||||||
'./resources/views/**/*.blade.php',
|
'./resources/views/**/*.blade.php',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
darkMode: 'class',
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|||||||
Reference in New Issue
Block a user