Files
HexaHost-Panel/resources/views/vms/show.blade.php
2026-05-17 13:26:14 +02:00

251 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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