initial commit

This commit is contained in:
TheOnlyMace
2026-05-17 13:26:14 +02:00
commit 75299b723d
176 changed files with 20327 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Konsole Fehler</title>
@vite(['resources/css/app.css'])
</head>
<body class="flex min-h-screen items-center justify-center bg-slate-950 text-slate-100">
<div class="max-w-md rounded-xl border border-red-800 bg-slate-900 p-8 text-center">
<h1 class="text-lg font-semibold text-red-400">Konsole nicht verfügbar</h1>
<p class="mt-4 text-sm text-slate-400">{{ $error }}</p>
<a href="{{ route('vms.show', $vm) }}" class="mt-6 inline-block text-cyan-400 hover:underline"> Zurück</a>
</div>
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Konsole {{ $vm->name }}</title>
@vite(['resources/css/app.css'])
<style>
html, body { height: 100%; margin: 0; background: #0f172a; }
#screen { width: 100%; height: calc(100vh - 48px); }
.toolbar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 1rem; background: #1e293b; color: #e2e8f0; font-family: system-ui, sans-serif; font-size: 14px; }
.toolbar a { color: #22d3ee; text-decoration: none; }
#status { color: #94a3b8; }
</style>
</head>
<body>
<div class="toolbar">
<span><strong>{{ $vm->displayName() }}</strong> noVNC</span>
<span id="status">Verbinde…</span>
<a href="{{ route('vms.show', $vm) }}"> Zurück zur VM</a>
</div>
<div id="screen"></div>
<script type="module">
import RFB from 'https://cdn.jsdelivr.net/npm/@novnc/novnc@1.5.0/core/rfb.js';
const wsUrl = @json($wsUrl);
const statusEl = document.getElementById('status');
const screen = document.getElementById('screen');
try {
const rfb = new RFB(screen, wsUrl, {
wsProtocols: ['binary'],
});
rfb.scaleViewport = true;
rfb.resizeSession = true;
rfb.addEventListener('connect', () => { statusEl.textContent = 'Verbunden'; statusEl.style.color = '#34d399'; });
rfb.addEventListener('disconnect', (e) => {
statusEl.textContent = e.detail.clean ? 'Getrennt' : 'Verbindung verloren';
statusEl.style.color = '#f87171';
});
rfb.addEventListener('credentialsrequired', () => { statusEl.textContent = 'Anmeldung erforderlich'; });
window.addEventListener('beforeunload', () => rfb.disconnect());
} catch (err) {
statusEl.textContent = 'Fehler: ' + err.message;
statusEl.style.color = '#f87171';
}
</script>
<p style="position:fixed;bottom:8px;left:8px;font-size:11px;color:#64748b;max-width:400px">
Hinweis: Der Browser muss Proxmox unter {{ parse_url(config('hosting.proxmox.console_ws_url') ?: config('hosting.proxmox.url'), PHP_URL_HOST) }} erreichen können.
</p>
</body>
</html>

View File

@@ -0,0 +1,98 @@
@extends('layouts.app')
@section('title', 'VM erstellen')
@section('heading', 'Neue VM erstellen')
@section('content')
<form method="POST" action="{{ route('vms.store') }}" class="max-w-3xl space-y-6">
@csrf
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6 space-y-4">
<h2 class="font-semibold">Allgemein</h2>
<label class="block">
<span class="text-sm text-slate-400">VM-Name</span>
<input name="name" value="{{ old('name') }}" required class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2">
</label>
@if($customers->isNotEmpty())
<label class="block">
<span class="text-sm text-slate-400">Zugewiesener Kunde</span>
<select name="user_id" required class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2">
@foreach($customers as $c)
<option value="{{ $c->id }}" @selected(old('user_id') == $c->id)>{{ $c->name }} ({{ $c->email }})</option>
@endforeach
</select>
</label>
@endif
<label class="flex items-center gap-2">
<input type="hidden" name="behind_traefik" value="0">
<input type="checkbox" name="behind_traefik" value="1" @checked(old('behind_traefik', true)) id="behind_traefik" class="rounded border-slate-600 text-cyan-500">
<span class="text-sm">Hinter Traefik (Subdomain + DNS)</span>
</label>
<div id="traefik-fields">
<label class="block">
<span class="text-sm text-slate-400">Subdomain</span>
<div class="mt-1 flex">
<input name="subdomain" value="{{ old('subdomain') }}" class="w-full rounded-l-lg border border-slate-700 bg-slate-800 px-3 py-2">
<span class="flex items-center rounded-r-lg border border-l-0 border-slate-700 bg-slate-800 px-3 text-sm text-slate-500">.{{ config('hosting.plesk.base_domain') }}</span>
</div>
</label>
</div>
<label class="block">
<span class="text-sm text-slate-400">Privater IP-Pool</span>
<select name="ip_pool_id" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2">
<option value="">Standard-Pool</option>
@foreach($privatePools as $pool)
<option value="{{ $pool->id }}" @selected(old('ip_pool_id') == $pool->id)>{{ $pool->name }} ({{ $pool->freeIpsCount() }} frei)</option>
@endforeach
</select>
</label>
<p class="text-xs text-slate-500">Ohne Traefik wird zusätzlich eine öffentliche IP aus dem Public-Pool vergeben.</p>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6 space-y-4">
<h2 class="font-semibold">Ressourcen</h2>
<div class="grid gap-4 sm:grid-cols-3">
<label><span class="text-sm text-slate-400">vCPUs</span>
<input type="number" name="cpu" value="{{ old('cpu', config('hosting.defaults.cpu')) }}" min="1" max="32" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
<label><span class="text-sm text-slate-400">RAM (MB)</span>
<input type="number" name="ram" value="{{ old('ram', config('hosting.defaults.ram')) }}" min="512" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
<label><span class="text-sm text-slate-400">Disk (GB)</span>
<input type="number" name="disk" value="{{ old('disk', config('hosting.defaults.disk')) }}" min="10" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
</div>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6 space-y-4">
<h2 class="font-semibold">Installations-ISO (optional)</h2>
<p class="text-xs text-slate-500">ISO wird beim Provisioning als CD-ROM eingebunden (Boot-Reihenfolge: CD zuerst).</p>
<select name="install_iso" class="w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-sm">
<option value="">Keine ISO / Cloud-Init</option>
@foreach($isos as $iso)
<option value="{{ $iso['volid'] }}" @selected(old('install_iso') === $iso['volid'])>{{ $iso['label'] }}</option>
@endforeach
</select>
@if(empty($isos))
<p class="text-xs text-amber-400">Keine ISOs von Proxmox geladen Storage {{ config('hosting.proxmox.iso_storage') }} prüfen.</p>
@endif
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Zusätzliche Geräte</h2>
@include('partials.vm-device-fields', ['deviceTypes' => $deviceTypes])
</section>
<button type="submit" class="rounded-lg bg-cyan-600 px-6 py-2.5 font-medium hover:bg-cyan-500">VM provisionieren</button>
</form>
@push('scripts')
<script>
const cb = document.getElementById('behind_traefik');
const fields = document.getElementById('traefik-fields');
function toggle() { fields.style.display = cb.checked ? 'block' : 'none'; }
cb?.addEventListener('change', toggle);
toggle();
</script>
@endpush
@endsection

View File

@@ -0,0 +1,33 @@
@extends('layouts.app')
@section('title', 'VM bearbeiten')
@section('heading', 'VM konfigurieren: '.$vm->name)
@section('content')
<form method="POST" action="{{ route('vms.update', $vm) }}" class="max-w-3xl space-y-6">
@csrf @method('PUT')
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6 space-y-4">
<label class="block">
<span class="text-sm text-slate-400">Name</span>
<input name="name" value="{{ old('name', $vm->name) }}" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2">
</label>
<div class="grid gap-4 sm:grid-cols-3">
<label><span class="text-sm text-slate-400">vCPUs</span>
<input type="number" name="cpu" value="{{ old('cpu', $vm->cpu) }}" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
<label><span class="text-sm text-slate-400">RAM (MB)</span>
<input type="number" name="ram" value="{{ old('ram', $vm->ram) }}" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
<label><span class="text-sm text-slate-400">Disk (GB)</span>
<input type="number" name="disk" value="{{ old('disk', $vm->disk) }}" class="mt-1 w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2"></label>
</div>
<p class="text-xs text-slate-500">Änderungen werden bei aktiver VM an Proxmox übertragen.</p>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Zusätzliche Geräte</h2>
@php $devices = old('devices', $vm->devices->map(fn($d) => ['type'=>$d->type,'slot'=>$d->slot,'config'=>$d->config])->all()); @endphp
@include('partials.vm-device-fields', ['deviceTypes' => $deviceTypes, 'devices' => $devices])
</section>
<button type="submit" class="rounded-lg bg-cyan-600 px-6 py-2.5 font-medium hover:bg-cyan-500">Speichern</button>
</form>
@endsection

View File

@@ -0,0 +1,61 @@
@extends('layouts.app')
@section('title', 'VMs')
@section('heading', 'Virtuelle Maschinen')
@section('content')
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
<form method="GET" class="flex gap-2">
<select name="status" onchange="this.form.submit()" class="rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-sm">
<option value="">Alle Status</option>
@foreach(['pending','active','failed'] as $s)
<option value="{{ $s }}" @selected(request('status') === $s)>{{ ucfirst($s) }}</option>
@endforeach
</select>
</form>
<a href="{{ route('vms.create') }}" class="rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium hover:bg-cyan-500">+ VM erstellen</a>
</div>
<div class="overflow-hidden rounded-xl border border-slate-800">
<table class="w-full text-left text-sm">
<thead class="bg-slate-900 text-slate-400">
<tr>
<th class="px-4 py-3">Name</th>
@if(auth()->user()->isAdmin())<th class="px-4 py-3">Kunde</th>@endif
<th class="px-4 py-3">VMID</th>
<th class="px-4 py-3">IP / Public</th>
<th class="px-4 py-3">Ressourcen</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800 bg-slate-900/40">
@forelse($vms as $vm)
<tr class="hover:bg-slate-800/30">
<td class="px-4 py-3 font-medium">{{ $vm->name }}</td>
@if(auth()->user()->isAdmin())
<td class="px-4 py-3 text-slate-400">{{ $vm->owner?->name ?? '—' }}</td>
@endif
<td class="px-4 py-3 font-mono text-xs">{{ $vm->vmid ?? '—' }}</td>
<td class="px-4 py-3 font-mono text-xs">
{{ $vm->ip_address ?? '—' }}
@if($vm->public_ip)<br><span class="text-violet-400">{{ $vm->public_ip }}</span>@endif
</td>
<td class="px-4 py-3 text-slate-400">{{ $vm->cpu }} vCPU · {{ $vm->ram }} MB · {{ $vm->disk }} GB</td>
<td class="px-4 py-3">
@include('partials.status-badge', ['status' => $vm->status])
@if($vm->proxmox_status)
<span class="ml-1 text-xs text-slate-500">({{ $vm->proxmox_status }})</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<a href="{{ route('vms.show', $vm) }}" class="text-cyan-400 hover:underline">Details</a>
</td>
</tr>
@empty
<tr><td colspan="7" class="px-4 py-8 text-center text-slate-500">Keine VMs gefunden.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $vms->links() }}</motionless>
@endsection

View File

@@ -0,0 +1,250 @@
@extends('layouts.app')
@section('title', $vm->name)
@section('heading', $vm->displayName())
@section('content')
@php
$canManage = auth()->user()->can('manage', $vm);
$isRunning = ($liveStatus['status'] ?? $vm->proxmox_status) === 'running';
@endphp
<div class="mb-6 flex flex-wrap gap-3">
@can('update', $vm)
<a href="{{ route('vms.edit', $vm) }}" class="rounded-lg border border-slate-700 px-4 py-2 text-sm hover:bg-slate-800">Konfiguration</a>
@endcan
@if($canManage)
<a href="{{ route('vms.console', $vm) }}" target="_blank" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium hover:bg-violet-500 {{ $isRunning ? '' : 'pointer-events-none opacity-50' }}">Konsole öffnen</a>
@endif
@can('delete', $vm)
<form method="POST" action="{{ route('vms.destroy', $vm) }}" onsubmit="return confirm('VM wirklich löschen?')">
@csrf @method('DELETE')
<button class="rounded-lg border border-red-800 px-4 py-2 text-sm text-red-400 hover:bg-red-950">Löschen</button>
</form>
@endcan
</div>
@if($errors->has('power'))
<div class="mb-4 rounded-lg border border-red-800/50 bg-red-950/50 px-4 py-3 text-sm text-red-300">{{ $errors->first('power') }}</div>
@endif
@if($errors->has('iso'))
<div class="mb-4 rounded-lg border border-red-800/50 bg-red-950/50 px-4 py-3 text-sm text-red-300">{{ $errors->first('iso') }}</div>
@endif
@if($canManage)
<section class="mb-6 rounded-xl border border-slate-800 bg-slate-900/60 p-6" id="vm-management-panel">
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
<h2 class="text-lg font-semibold">VM-Steuerung</h2>
<div class="flex items-center gap-3 text-sm">
<span id="live-status-badge" class="inline-flex items-center gap-2 rounded-full border px-3 py-1 {{ $isRunning ? 'border-emerald-800 bg-emerald-950 text-emerald-400' : 'border-slate-700 bg-slate-800 text-slate-400' }}">
<span class="h-2 w-2 rounded-full {{ $isRunning ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500' }}"></span>
<span id="live-status-text">{{ $liveStatus['status'] ?? $vm->proxmox_status ?? 'unbekannt' }}</span>
</span>
<span class="text-slate-500" id="live-uptime">
@if(($liveStatus['uptime'] ?? $vm->proxmox_uptime ?? 0) > 0)
Uptime: {{ gmdate('H:i:s', $liveStatus['uptime'] ?? $vm->proxmox_uptime) }}
@endif
</span>
</div>
</div>
<form method="POST" action="{{ route('vms.power', $vm) }}" class="flex flex-wrap gap-2">
@csrf
@foreach(\App\Enums\VmPowerAction::cases() as $action)
<button type="submit" name="action" value="{{ $action->value }}"
class="rounded-lg border px-4 py-2 text-sm font-medium transition
{{ $action === \App\Enums\VmPowerAction::Stop || $action === \App\Enums\VmPowerAction::Reset ? 'border-red-800 text-red-400 hover:bg-red-950' : 'border-slate-700 hover:bg-slate-800' }}"
onclick="return confirm('{{ $action->label() }} ausführen?')">
{{ $action->label() }}
</button>
@endforeach
</form>
<div class="mt-6 grid gap-4 border-t border-slate-800 pt-6 lg:grid-cols-2">
<div>
<h3 class="mb-3 font-medium text-slate-300">Installations-ISO</h3>
@if($vm->attached_iso)
<p class="mb-2 font-mono text-sm text-cyan-400">{{ $vm->attached_iso }}</p>
<form method="POST" action="{{ route('vms.iso.unmount', $vm) }}" class="inline">
@csrf @method('DELETE')
<button class="text-sm text-red-400 hover:underline">ISO entfernen</button>
</form>
@else
<p class="mb-2 text-sm text-slate-500">Keine ISO eingebunden.</p>
@endif
<form method="POST" action="{{ route('vms.iso.mount', $vm) }}" class="mt-3 flex flex-wrap gap-2">
@csrf
<select name="iso_volid" required class="min-w-0 flex-1 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-sm">
<option value="">ISO wählen…</option>
@foreach($isos as $iso)
<option value="{{ $iso['volid'] }}">{{ $iso['label'] }} ({{ number_format($iso['size'] / 1024 / 1024 / 1024, 2) }} GB)</option>
@endforeach
</select>
<button type="submit" class="rounded-lg bg-cyan-600 px-4 py-2 text-sm hover:bg-cyan-500">Einbinden</button>
</form>
<p class="mt-2 text-xs text-slate-500">Nach dem Einbinden: VM neu starten und von CD booten.</p>
</div>
<div>
<h3 class="mb-3 font-medium text-slate-300">Live-Ressourcen</h3>
<dl class="space-y-2 text-sm" id="live-resources">
@if($liveStatus)
<div class="flex justify-between"><dt class="text-slate-400">CPU</dt><dd>{{ number_format(($liveStatus['cpu'] ?? 0) * 100, 1) }}%</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">RAM</dt><dd>{{ number_format(($liveStatus['mem'] ?? 0) / 1024 / 1024 / 1024, 2) }} / {{ number_format(($liveStatus['maxmem'] ?? 0) / 1024 / 1024 / 1024, 2) }} GB</dd></div>
@else
<p class="text-slate-500">Status nicht abrufbar (Proxmox-Verbindung prüfen).</p>
@endif
</dl>
</div>
</div>
</section>
@endif
<div class="grid gap-6 lg:grid-cols-2">
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Status</h2>
<dl class="space-y-3 text-sm">
<div class="flex justify-between"><dt class="text-slate-400">Provisioning</dt><dd>@include('partials.status-badge', ['status' => $vm->status])</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">Schritt</dt><dd>{{ $vm->provisioning_step ?? '—' }}</dd></div>
@if($vm->error_message)
<div class="rounded bg-red-950/50 p-3 text-red-300">{{ $vm->error_message }}</div>
@endif
<div class="flex justify-between"><dt class="text-slate-400">VMID</dt><dd class="font-mono">{{ $vm->vmid ?? '—' }}</dd></div>
@if(auth()->user()->isAdmin())
<div class="flex justify-between"><dt class="text-slate-400">Besitzer</dt><dd>{{ $vm->owner?->name ?? '—' }}</dd></div>
@endif
</dl>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Netzwerk</h2>
<dl class="space-y-3 text-sm">
<div class="flex justify-between"><dt class="text-slate-400">Private IP</dt><dd class="font-mono">{{ $vm->ip_address ?? '—' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">Öffentliche IP</dt><dd class="font-mono text-violet-400">{{ $vm->public_ip ?? '—' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">Traefik</dt><dd>{{ $vm->behind_traefik ? 'Ja' : 'Nein' }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">Domain</dt><dd>{{ str_ends_with($vm->domain ?? '', '.internal') ? '—' : $vm->domain }}</dd></div>
</dl>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Ressourcen (Konfiguration)</h2>
<dl class="space-y-3 text-sm">
<div class="flex justify-between"><dt class="text-slate-400">vCPUs</dt><dd>{{ $vm->cpu }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">RAM</dt><dd>{{ $vm->ram }} MB</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">Disk</dt><dd>{{ $vm->disk }} GB</dd></div>
</dl>
</section>
<section class="rounded-xl border border-slate-800 bg-slate-900/60 p-6">
<h2 class="mb-4 font-semibold">Aktivitätsprotokoll</h2>
<div class="max-h-48 space-y-2 overflow-y-auto text-xs">
@forelse($vm->activityLogs as $log)
<div class="flex justify-between gap-2 border-b border-slate-800 pb-2">
<span class="{{ $log->status === 'success' ? 'text-slate-300' : 'text-red-400' }}">{{ $log->action }}</span>
<span class="shrink-0 text-slate-500">{{ $log->created_at->diffForHumans() }}</span>
</div>
@empty
<p class="text-slate-500">Noch keine Aktionen.</p>
@endforelse
</div>
</section>
</div>
@if($canManage && $vm->vmid)
<section class="hexahost-card mb-6 p-6">
<h2 class="mb-4 text-lg font-semibold">Snapshots (48h)</h2>
<form method="POST" action="{{ route('vms.snapshots.store', $vm) }}" class="mb-4 flex gap-2">
@csrf
<input type="text" name="label" placeholder="Optional: Bezeichnung" class="hexahost-input flex-1 px-3 py-2 text-sm">
<button class="hexahost-btn-primary px-4 py-2 text-sm">Snapshot erstellen</button>
</form>
<ul class="space-y-2 text-sm">
@forelse($vm->snapshots as $snap)
<li class="flex flex-wrap items-center justify-between gap-2 border-b border-white/10 pb-2">
<span>{{ $snap->name }} @if($snap->auto_created)<span class="text-xs opacity-60">(auto)</span>@endif</span>
<span class="text-xs opacity-60">bis {{ $snap->expires_at?->format('d.m. H:i') }}</span>
<span class="flex gap-2">
<form method="POST" action="{{ route('vms.snapshots.rollback', [$vm, $snap]) }}">@csrf<button class="text-cyan-400 text-xs hover:underline">Rollback</button></form>
<form method="POST" action="{{ route('vms.snapshots.destroy', [$vm, $snap]) }}">@csrf @method('DELETE')<button class="text-red-400 text-xs hover:underline">Löschen</button></form>
</span>
</li>
@empty
<li class="opacity-60">Keine Snapshots.</li>
@endforelse
</ul>
</section>
<section class="hexahost-card mb-6 p-6">
<h2 class="mb-4 text-lg font-semibold">Backups @if(!config('hosting.backups.enabled'))<span class="text-xs opacity-60">(PBS deaktiviert)</span>@endif</h2>
@if(config('hosting.backups.enabled'))
<form method="POST" action="{{ route('vms.backups.store', $vm) }}">@csrf<button class="hexahost-btn-primary px-4 py-2 text-sm">Backup starten</button></form>
@endif
<ul class="mt-4 space-y-2 text-sm">
@forelse($vm->backups as $backup)
<li>{{ $backup->status }} {{ $backup->created_at->format('d.m.Y H:i') }}</li>
@empty
<li class="opacity-60">Keine Backups.</li>
@endforelse
</ul>
</section>
<section class="hexahost-card mb-6 p-6">
<h2 class="mb-4 text-lg font-semibold">Firewall</h2>
<form method="POST" action="{{ route('vms.firewall.store', $vm) }}" class="mb-4 grid gap-2 sm:grid-cols-5">
@csrf
<select name="direction" class="hexahost-input px-2 py-2 text-sm"><option value="in">Eingehend</option><option value="out">Ausgehend</option></select>
<select name="action" class="hexahost-input px-2 py-2 text-sm"><option value="ACCEPT">ACCEPT</option><option value="DROP">DROP</option></select>
<select name="protocol" class="hexahost-input px-2 py-2 text-sm"><option value="tcp">TCP</option><option value="udp">UDP</option></select>
<input name="port" placeholder="Port" class="hexahost-input px-2 py-2 text-sm">
<button class="hexahost-btn-primary px-3 py-2 text-sm">Hinzufügen</button>
</form>
<ul class="space-y-1 text-sm">
@forelse($vm->firewallRules as $rule)
<li class="flex justify-between"><span>{{ $rule->direction }} {{ $rule->protocol }} {{ $rule->port }} {{ $rule->action }}</span>
<form method="POST" action="{{ route('vms.firewall.destroy', [$vm, $rule->id]) }}">@csrf @method('DELETE')<button class="text-red-400 text-xs">×</button></form></li>
@empty
<li class="opacity-60">Keine Regeln.</li>
@endforelse
</ul>
</section>
<section class="hexahost-card mb-6 p-6">
<h2 class="mb-4 text-lg font-semibold">Neuinstallation</h2>
<form method="POST" action="{{ route('vms.reinstall', $vm) }}" onsubmit="return confirm('VM wird gelöscht und neu provisioniert. Fortfahren?')">
@csrf
<input type="hidden" name="confirm" value="REINSTALL">
<button class="rounded-lg border border-red-500/50 px-4 py-2 text-sm text-red-400 hover:bg-red-950/30">Neuinstallation starten</button>
</form>
</section>
@endif
@if($canManage && $vm->vmid)
@push('scripts')
<script>
(function () {
const statusUrl = @json(route('vms.status', $vm));
const badge = document.getElementById('live-status-badge');
const text = document.getElementById('live-status-text');
const uptime = document.getElementById('live-uptime');
async function refresh() {
try {
const res = await fetch(statusUrl, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } });
const data = await res.json();
if (!data.proxmox) return;
const running = data.proxmox.status === 'running';
text.textContent = data.proxmox.status;
badge.className = 'inline-flex items-center gap-2 rounded-full border px-3 py-1 ' +
(running ? 'border-emerald-800 bg-emerald-950 text-emerald-400' : 'border-slate-700 bg-slate-800 text-slate-400');
if (data.proxmox.uptime > 0) {
const h = Math.floor(data.proxmox.uptime / 3600);
const m = Math.floor((data.proxmox.uptime % 3600) / 60);
const s = data.proxmox.uptime % 60;
uptime.textContent = 'Uptime: ' + [h,m,s].map(v => String(v).padStart(2,'0')).join(':');
}
} catch (e) { /* ignore */ }
}
setInterval(refresh, 10000);
})();
</script>
@endpush
@endif
@endsection