CluPilotCloud/resources/views/livewire/admin/capacity.blade.php

197 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<div class="space-y-6">
<div class="animate-rise">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('capacity.title') }}</h1>
<p class="mt-1 max-w-[70ch] text-sm text-muted">{{ __('capacity.subtitle') }}</p>
</div>
{{-- ── The queue ────────────────────────────────────────────────────── --}}
<x-ui.card class="animate-rise [animation-delay:60ms]">
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
<h2 class="font-semibold text-ink">{{ __('capacity.queue_title') }}</h2>
@if ($demand['count'] > 0)
<x-ui.badge status="warning">{{ trans_choice('capacity.queue_count', $demand['count'], ['count' => $demand['count']]) }}</x-ui.badge>
@else
<x-ui.badge status="active">{{ __('capacity.queue_empty_badge') }}</x-ui.badge>
@endif
<x-ui.button wire:click="recheck" variant="secondary" size="sm" class="ml-auto"
wire:loading.attr="disabled" wire:target="recheck">
<x-ui.icon name="refresh" class="size-4" />{{ __('capacity.recheck') }}
</x-ui.button>
</div>
@if ($demand['count'] === 0)
<p class="mt-4 text-sm text-muted">{{ __('capacity.queue_empty') }}</p>
@else
<table class="mt-5 w-full text-left text-sm">
<thead>
<tr class="border-b border-line">
<th class="lbl pb-2 font-normal">{{ __('capacity.col_customer') }}</th>
<th class="lbl pb-2 font-normal">{{ __('capacity.col_plan') }}</th>
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_needs') }}</th>
<th class="lbl pb-2 font-normal">{{ __('capacity.col_datacenter') }}</th>
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_waiting') }}</th>
<th class="lbl pb-2 font-normal">{{ __('capacity.col_target') }}</th>
</tr>
</thead>
<tbody>
@foreach ($parked as $entry)
@php
// Die Hosts, auf denen GENAU DIESE Bestellung landen
// dürfte — eigenes Rechenzentrum und keine fremde
// Reservierung. Zusammengestellt in Capacity::render()
// über denselben Scope wie die Platzierung selbst,
// statt hier ein zweites Mal formuliert.
$choices = $entry['choices'];
// Der angeheftete Host aus dem ganzen Bestand des
// Rechenzentrums, nicht nur aus der Auswahl: eine
// Maschine, die seit dem Anheften reserviert wurde,
// steht nicht mehr zur Wahl — und ein Auswahlfeld,
// das dann kommentarlos wieder „Automatisch" zeigt,
// verschweigt, warum die Bestellung weiter wartet.
// Ein Pin aus einem anderen Rechenzentrum bleibt
// unbeachtet wie bisher; der Schritt ignoriert ihn
// ebenfalls, und „reserviert" wäre dafür der
// falsche Satz.
$pinnedHost = $entry['pinned_host_id'] === null
? null
: $hosts->where('datacenter', $entry['datacenter'])->firstWhere('id', $entry['pinned_host_id']);
$pinnable = $pinnedHost !== null && $choices->contains('id', $pinnedHost->id);
@endphp
<tr wire:key="parked-{{ $entry['run']->id }}" class="border-b border-line last:border-0">
<td class="py-3 font-medium text-ink">{{ $entry['order']->customer?->name ?? '—' }}</td>
<td class="py-3 text-body">{{ $entry['name'] }}</td>
<td class="py-3 text-right font-mono tabular-nums text-ink">{{ $entry['needs'] }} GB</td>
<td class="py-3 font-mono text-xs text-muted">{{ $entry['datacenter'] }}</td>
<td class="py-3 text-right text-xs text-muted">{{ $entry['waiting_since']->local()->diffForHumans() }}</td>
{{-- One select, one value, no height change: the row
stays a row (R20's exception), and the decision
is the operator's the placement rule only
takes over again when nothing is chosen. --}}
<td class="py-3">
<select wire:change="pin({{ $entry['run']->id }}, $event.target.value)"
class="rounded-md border border-line-strong bg-surface px-2 py-1 text-xs text-body">
<option value="">{{ __('capacity.target_auto') }}</option>
@foreach ($choices as $host)
<option value="{{ $host->id }}" @selected($entry['pinned_host_id'] === $host->id)>
{{ $host->name }} · {{ $host->availableGb() }} GB
</option>
@endforeach
</select>
@if ($pinnedHost !== null && ! $pinnable)
{{-- Die Wahl steht noch im Lauf, die Maschine
gehört inzwischen jemandem: der Schritt
lässt die Bestellung darauf nicht landen,
und ohne diesen Satz sähe die Zeile aus
wie jede andere. --}}
<p class="mt-1 flex items-center gap-1.5 text-xs text-warning">
<x-ui.icon name="alert-triangle" class="size-3.5 shrink-0" />{{ __('capacity.target_reserved') }}
</p>
@elseif ($pinnedHost !== null && ! $pinnedHost->canTake($entry['needs']))
{{-- A pin is honoured or it waits never
quietly redirected. So say so, or the row
sits there looking like every other one
while it is the choice, not the estate,
that is holding it up. --}}
<p class="mt-1 flex items-center gap-1.5 text-xs text-warning">
<x-ui.icon name="alert-triangle" class="size-3.5 shrink-0" />{{ __('capacity.target_too_small') }}
</p>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
{{-- What to buy, in numbers. The largest single package decides the
MINIMUM machine an instance lives on one host, so a server
that cannot take the biggest one leaves it parked however much
total space it has. --}}
<div class="mt-6 rounded-lg border border-accent-border bg-accent-subtle p-5">
<p class="lbl !text-accent-text">{{ __('capacity.needed_title') }}</p>
<p class="mt-3 text-sm leading-relaxed text-body">
{{ __('capacity.needed_body', [
'mix' => collect($demand['by_plan'])->map(fn ($n, $plan) => $n.'× '.$plan)->join(', '),
'largest' => $demand['largest'],
'total' => $demand['total'],
]) }}
</p>
</div>
@endif
</x-ui.card>
{{-- ── What the estate can still take ───────────────────────────────── --}}
<x-ui.card :title="__('capacity.hosts_title')" class="animate-rise [animation-delay:120ms]">
@if ($hosts->isEmpty())
<p class="text-sm text-muted">{{ __('capacity.hosts_empty') }}</p>
@else
<table class="w-full text-left text-sm">
<tbody>
@foreach ($hosts as $host)
<tr class="border-b border-line last:border-0">
<td class="py-3 font-medium text-ink">
{{ $host->name }}
{{-- Erklärt statt versteckt: largestPlaceableGb() unten zählt
diese Zeile nicht mit sie gehört bereits einem Kunden. --}}
@if ($host->reserved_for_customer_id)
<span class="ml-2 inline-flex items-center gap-1 rounded-pill bg-surface-2 px-2 py-0.5 text-xs font-normal text-muted">
<x-ui.icon name="lock" class="size-3.5 shrink-0" />{{ __('hosts.reserved_badge', ['name' => $host->reservedFor?->name ?? __('hosts.unknown')]) }}
</span>
@endif
</td>
<td class="py-3 font-mono text-xs text-muted">{{ $host->datacenter }}</td>
<td class="py-3 text-right font-mono tabular-nums text-body">
{{ $host->availableGb() }} / {{ $host->freeGb() }} GB {{ __('capacity.free') }}
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
@if ($unplaceable !== [])
<x-ui.alert variant="warning" class="mt-5">
{{ __('capacity.unsellable', [
'plans' => collect($unplaceable)->pluck('name')->join(', '),
]) }}
</x-ui.alert>
@endif
</x-ui.card>
{{-- ── What such a machine costs today ──────────────────────────────── --}}
@if ($demand['count'] > 0)
<x-ui.card :title="__('capacity.market_title')" class="animate-rise [animation-delay:180ms]">
<p class="text-sm leading-relaxed text-muted">{{ __('capacity.market_body', ['gb' => $demand['largest']]) }}</p>
@if ($offers === [])
<p class="mt-4 text-sm text-muted">{{ __('capacity.market_none') }}</p>
@else
<table class="mt-4 w-full text-left text-sm">
<thead>
<tr class="border-b border-line">
<th class="lbl pb-2 font-normal">CPU</th>
<th class="lbl pb-2 text-right font-normal">RAM</th>
<th class="lbl pb-2 font-normal">{{ __('capacity.col_disks') }}</th>
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_usable') }}</th>
<th class="lbl pb-2 font-normal">{{ __('capacity.col_location') }}</th>
<th class="lbl pb-2 text-right font-normal">{{ __('capacity.col_price') }}</th>
</tr>
</thead>
<tbody>
@foreach ($offers as $offer)
<tr class="border-b border-line last:border-0">
<td class="py-3 text-body">{{ $offer['cpu'] }}</td>
<td class="py-3 text-right font-mono tabular-nums text-body">{{ $offer['ram'] }} GB</td>
<td class="py-3 font-mono text-xs text-muted">{{ $offer['disks'] }}</td>
<td class="py-3 text-right font-mono tabular-nums text-ink">{{ number_format($offer['flash_gb'], 0, ',', '.') }} GB</td>
<td class="py-3 font-mono text-xs text-muted">{{ $offer['datacenter'] }}</td>
<td class="py-3 text-right font-mono tabular-nums font-semibold text-ink">{{ number_format($offer['price'], 2, ',', '.') }} </td>
</tr>
@endforeach
</tbody>
</table>
<p class="mt-3 text-xs text-muted">{{ __('capacity.market_note') }}</p>
@endif
</x-ui.card>
@endif
</div>