Build the dashboard cards to the template, and name what has no source

Card by card against the approved template: label with a chevron, the figure
and its unit on one line, a line of context underneath, a bar across the foot.
The order matches too — storage, seats, transfer, then the fourth slot.

Three of the template's four visuals have no data behind them, and the code now
says so where someone will read it rather than leaving the next person to
wonder why the charts are missing:

- Storage consumption is never sampled. The card states the contractually
  agreed allowance instead of drawing a ring at an invented level.
- instance_traffic keeps one row per period, not a daily series, so there is
  nothing to draw a sparkline from. The share of the allowance is real, so
  that card carries a bar.
- monitoring_targets records a state and a check time but no uptime figure, so
  there is no availability percentage to show. That slot carries the last
  backup — the thing this product is about and can actually prove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-27 16:21:27 +02:00
parent 676c38643c
commit c2a56382e1
3 changed files with 33 additions and 9 deletions

View File

@ -31,6 +31,7 @@ return [
'label' => 'Stammblatt',
'package' => 'Paket',
'operation' => 'Betriebsform',
'last_backup' => 'Letzte Sicherung',
'location' => 'Serverstandort',
'users' => 'Benutzer',
'address' => 'Adresse',

View File

@ -31,6 +31,7 @@ return [
'label' => 'Master record',
'package' => 'Package',
'operation' => 'Operating mode',
'last_backup' => 'Last backup',
'location' => 'Server location',
'users' => 'Users',
'address' => 'Address',

View File

@ -54,10 +54,16 @@
</section>
@else
{{-- ── What the customer has ─────────────────────────────────────
The template's metric card, and only the visuals there is data
for. Storage consumption is not measured yet, so that card states
the allowance instead of drawing a ring at an invented level
this is the sheet a customer forwards to their auditor. --}}
The template's four cards, in its order and its form: label with a
chevron, the figure and its unit on one line, a line of context
underneath, the visual on the right or a bar across the foot.
Three of the template's visuals have no source yet storage
consumption is not sampled, instance_traffic keeps one row per
period rather than a daily series, and monitoring_targets records
a state but no uptime figure. Those cards carry the form and state
what is true instead of drawing a chart at an invented level. This
is the sheet a customer forwards to their auditor. --}}
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@if ($instance->quota_gb)
<x-ui.metric
@ -65,6 +71,7 @@
:value="$instance->quota_gb"
unit="GB"
:foot="__('dashboard.master.storage_note')"
:href="route('cloud')"
class="animate-rise [animation-delay:60ms]"
/>
@endif
@ -77,9 +84,11 @@
class="animate-rise [animation-delay:100ms]"
>
@if ($seats['total'])
<x-slot:visual>
<x-ui.ring :percent="$seats['used'] / max(1, $seats['total']) * 100" :size="54" />
</x-slot:visual>
<div class="mt-4 h-1.5 overflow-hidden rounded-pill bg-surface-hover">
{{-- R4: width is data, not decoration. --}}
<div class="h-full rounded-pill bg-muted"
style="width: {{ min(100, (int) round($seats['used'] / max(1, $seats['total']) * 100)) }}%"></div>
</div>
@endif
</x-ui.metric>
@ -94,7 +103,6 @@
class="animate-rise [animation-delay:140ms]"
>
<div class="mt-4 h-1.5 overflow-hidden rounded-pill bg-surface-hover">
{{-- R4: width is data, not decoration. --}}
<div @class([
'h-full rounded-pill',
'bg-accent' => $state === 'ok',
@ -114,7 +122,21 @@
</x-ui.metric>
@endif
@if ($location)
{{-- Where the template shows availability. There is no uptime
figure to show, so this card carries the thing this product is
actually about and can prove: when the last backup ran and
whether it was verified. --}}
@php $lastBackup = collect($proofs)->firstWhere('key', 'backup'); @endphp
@if ($lastBackup)
<x-ui.metric
:label="__('dashboard.master.last_backup')"
:value="$lastBackup['at']?->locale($locale)->isoFormat('HH:mm') ?? '—'"
:unit="$lastBackup['at']?->locale($locale)->isoFormat('DD.MM.')"
:foot="__('dashboard.proofs.state.'.$lastBackup['state']).' · '.($lastBackup['note'] ?? '')"
:href="route('backups')"
class="animate-rise [animation-delay:180ms]"
/>
@elseif ($location)
<x-ui.metric
:label="__('dashboard.master.location')"
:value="$location['name']"