fix: apply v1 UI review (a11y, contrast, live-chart robustness)
- Focus-visible token ring + prefers-reduced-motion guard (app.css).
- Lift ink-3/ink-4 to WCAG-AA text contrast.
- Touch targets >=44px (topbar, sidebar, dashboard buttons).
- Off-canvas drawer removed from tab order when closed (breakpoint-aware inert)
+ sidebar aria-label.
- Live chart: NaN guard on incoming cpu; Echo.leave('metrics') + unbind on
destroy (no subscription leak under wire:navigate).
- KPI progress bar follows the tone (status triad), not always accent.
- Resource rings bound to real server data with threshold-based tone.
- docs/v1-ui-review.md: full adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
parent
8df4456108
commit
47eae61371
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Clusev v1 — Konsolidierte Fix-Liste
|
||||||
|
|
||||||
|
Alle Punkte gegen die echten Dateien geprüft. Duplikate (Focus-States, Touch-Targets, KPI-Balken, Ringe, mobile CPU/RAM-Anzeige) sind zusammengeführt. Reine Bereitschafts-/Konventionshinweise ohne Datei-Defekt sind unter „Bewusst weggelassen" gelistet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MUST-FIX (Regelverstoß / echter Bug / A11y-Blocker)
|
||||||
|
|
||||||
|
### A11y — Tastatur-Fokus unsichtbar
|
||||||
|
- **Dateien:** `nav-item.blade.php`, `topbar.blade.php` (Z. 3, 16), `sidebar.blade.php` (Z. 10, 19, 48), `server-item.blade.php` (Z. 5), `livewire/dashboard.blade.php` (Z. 69)
|
||||||
|
- **Änderung:** Auf jeder interaktiven Basisklasse einen Token-Fokusring ergänzen: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface`. Alternativ eine Base-Layer-Regel in `app.css`: `*:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px }`.
|
||||||
|
- **Grund:** Null Fokus-Styles im gesamten View-Layer (per grep bestätigt) — Tastaturbedienung auf der dunklen UI ist unmöglich.
|
||||||
|
|
||||||
|
### A11y — Kontrast `ink-3` unter WCAG AA
|
||||||
|
- **Datei:** `resources/css/app.css:36` (`--color-ink-3: #69757F`)
|
||||||
|
- **Änderung:** Auf ca. `#7E8A95` aufhellen (≈4,6:1 auf surface).
|
||||||
|
- **Grund:** 3,95:1 auf surface — `ink-3` ist Default für fast jeden Sekundärtext (IPs, Subtitles, Unit-Labels, systemd-Beschreibungen) bei `text-[11px]`/`text-xs`, also gilt 4,5:1.
|
||||||
|
|
||||||
|
### A11y — Kontrast `ink-4` unter WCAG AA (für Text)
|
||||||
|
- **Datei:** `resources/css/app.css:37` (`--color-ink-4: #495159`)
|
||||||
|
- **Änderung:** Für Textnutzung auf ca. `#707B85` (≈4,5:1) aufhellen, oder `ink-4` strikt auf Hairlines/Nicht-Text beschränken und betroffene Textstellen (`dashboard.blade.php:107,109`, `server-item.blade.php:13,19`) auf `text-ink-3` umstellen.
|
||||||
|
- **Grund:** 2,31:1 — fällt sogar unter AA-large (3:1), wird aber für echten Lesetext (Audit-Timestamps/Targets, CPU/RAM-Spaltenlabels) genutzt.
|
||||||
|
|
||||||
|
### A11y — Off-Canvas-Drawer nicht aus Tab-Order entfernt
|
||||||
|
- **Datei:** `resources/views/components/sidebar.blade.php:2` (`<aside>`)
|
||||||
|
- **Änderung:** Geschlossenen Zustand mit `x-bind:inert="!nav"` (plus `:aria-hidden`) binden, auf `lg` immer interaktiv halten; Fokus beim Öffnen auf den Schließen-Button setzen.
|
||||||
|
- **Grund:** Nur `-translate-x-full` schiebt den Drawer aus dem Bild — Tastatur/Screenreader tabben weiter in unsichtbare Off-Canvas-Links.
|
||||||
|
|
||||||
|
### R7 — Touch-Targets unter 44px
|
||||||
|
- **Dateien/Änderungen:**
|
||||||
|
- `topbar.blade.php:3` und `:16` — Menü- und Glocken-Button `h-9 w-9` → `min-h-11 min-w-11` (grid `place-items-center` behalten).
|
||||||
|
- `sidebar.blade.php:10` (Schließen) und `:48` (Abmelden-Link `<a>`) — `h-9 w-9` → `min-h-11 min-w-11`.
|
||||||
|
- `livewire/dashboard.blade.php:69` — „Hinzufügen"-Button `min-h-9` → `min-h-11`.
|
||||||
|
- **Grund:** R7 fordert ≥44px; diese realen Tap-Ziele sind 36px (Switcher/Nav-Items nutzen bereits korrekt `min-h-11`).
|
||||||
|
|
||||||
|
### Bug — NaN-Guard greift an der falschen Stelle
|
||||||
|
- **Datei:** `resources/js/app.js:34-37` (`push()`)
|
||||||
|
- **Änderung:**
|
||||||
|
```js
|
||||||
|
push(v) { const n = Number(v); if (!Number.isFinite(n)) return; this.last = n; this.points.push(n); if (this.points.length > this.max) this.points.shift(); }
|
||||||
|
```
|
||||||
|
- **Grund:** `Number(e.cpu)` bei fehlendem/falschem Key liefert `NaN`; die Klammer in `coords()` lässt `NaN` durch → `<polyline>` wird ungültig (Sparkline verschwindet) und der Zähler zeigt `NaN%`.
|
||||||
|
|
||||||
|
### Bug/Leak — Reverb-Channel wird nie abgebaut
|
||||||
|
- **Datei:** `resources/js/app.js:26-32` (`init()`)
|
||||||
|
- **Änderung:** Channel-Referenz halten und `destroy() { window.Echo?.leave('metrics'); }` zum Alpine-Objekt hinzufügen; `state_change`-Handler ebenfalls per Referenz unbinden.
|
||||||
|
- **Grund:** Kein `Echo.leave`/`unbind` vorhanden (grep bestätigt). Heute durch Full-Reload maskiert, aber sobald `wire:navigate` für die geplanten Server/Dienste/Dateien-Seiten kommt, stapeln sich Subscriptions auf dem Singleton `window.Echo`.
|
||||||
|
|
||||||
|
### R10 — KPI-Fortschrittsbalken ignoriert den Status-Triad
|
||||||
|
- **Datei:** `resources/views/components/kpi.blade.php:23`
|
||||||
|
- **Änderung:** Balkenfarbe aus `$tone` ableiten, z. B. `$barColor = ['ink'=>'bg-accent','online'=>'bg-online','warning'=>'bg-warning','offline'=>'bg-offline','accent'=>'bg-accent','cyan'=>'bg-cyan'][$tone] ?? 'bg-accent';` und `bg-accent` ersetzen. Width-Inline-Style bleibt (R4).
|
||||||
|
- **Grund:** Balken ist hart `bg-accent`; die `tone`-Prop färbt nur die Zahl — eine getonte KPI mit `pct` zeigt orangen Balken unter grüner/roter Zahl.
|
||||||
|
|
||||||
|
### Datenintegrität — Ressourcen-Ringe zeigen hartkodierte Konstanten
|
||||||
|
- **Datei:** `resources/views/livewire/dashboard.blade.php:57-60`
|
||||||
|
- **Änderung:** Ringe an echte Daten binden (mind. CPU/RAM aus denselben `$avgCpu`/`$avgMem` wie die KPIs) und Ton schwellenwertgesteuert in `x-ring` setzen (grün <75, amber 75–90, rot >90) statt fixem `tone` je Metrik.
|
||||||
|
- **Grund:** `:value="34/61/48"` mit statischen Tönen — „Disk" rendert amber bei 48% nur wegen `tone="warning"`, irreführend in einem Monitoring-View und entkoppelt vom Datenstrom.
|
||||||
|
|
||||||
|
### R7/UX — Keine Empty-/Loading-States in den Listen
|
||||||
|
- **Datei:** `resources/views/livewire/dashboard.blade.php` (Server `73-78`, systemd `82-93`, Audit `98-112`)
|
||||||
|
- **Änderung:** Jedes `@foreach` zu `@forelse … @empty` mit zentrierter, gedämpfter DE-Empty-Zeile (z. B. `<div class="px-5 py-8 text-center font-mono text-[11px] text-ink-3">Keine Server im Cluster</div>`); `wire:loading`-Skeleton für die per SSH/Queue hydrierten Panels.
|
||||||
|
- **Grund:** Bare `@foreach` ohne Fallback (grep: 0 forelse/@empty/wire:loading) — leere Daten rendern blanke Boxen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NICE-TO-HAVE (Polish)
|
||||||
|
|
||||||
|
### A11y — `prefers-reduced-motion` fehlt
|
||||||
|
- **Datei:** `resources/views/components/status-dot.blade.php:7`
|
||||||
|
- **Änderung:** `animate-ping` → `motion-safe:animate-ping`, oder Base-Layer-Regel in `app.css`, die `.animate-ping` bei reduzierter Bewegung deaktiviert.
|
||||||
|
- **Grund:** Dauerhafte Ping-Animation auf jedem Online-Dot ohne Reduced-Motion-Guard (grep: 0).
|
||||||
|
|
||||||
|
### A11y — `<nav>` ohne zugänglichen Namen
|
||||||
|
- **Datei:** `resources/views/components/sidebar.blade.php:31`
|
||||||
|
- **Änderung:** `<nav aria-label="Hauptnavigation" …>`.
|
||||||
|
- **Grund:** Generisches „navigation"-Landmark für Screenreader.
|
||||||
|
|
||||||
|
### A11y — Live-Chart Disconnected-Zustand kaum wahrnehmbar
|
||||||
|
- **Datei:** `resources/views/livewire/dashboard.blade.php:46`
|
||||||
|
- **Änderung:** Dot `aria-hidden="true"` (Text daneben trägt die Info) und Disconnected-Farbe `bg-ink-4` → `bg-warning`: `:class="connected ? 'bg-online' : 'bg-warning'"`.
|
||||||
|
- **Grund:** `bg-ink-4` (2,3:1) ist als Statusindikator praktisch unsichtbar.
|
||||||
|
|
||||||
|
### R7/Responsive — Per-Server CPU/RAM auf Mobile komplett ausgeblendet
|
||||||
|
- **Datei:** `resources/views/components/server-item.blade.php:11`
|
||||||
|
- **Änderung:** Kompakte Mono-Zeile nur `<sm` einblenden (z. B. `CPU 34% · RAM 61%`), ab `sm` auf die Balken umschalten.
|
||||||
|
- **Grund:** `hidden sm:flex` lässt unter 375px die wichtigste At-a-glance-Metrik ersatzlos verschwinden.
|
||||||
|
|
||||||
|
### R7 — Server-Item-Zeile ohne Mindesthöhe
|
||||||
|
- **Datei:** `resources/views/components/server-item.blade.php:5`
|
||||||
|
- **Änderung:** `min-h-11` zur `<a>`-Klasse ergänzen.
|
||||||
|
- **Grund:** Nur `py-2.5`, keine Mindesthöhe — in der einzeiligen `<sm`-Variante kann die Tap-Höhe unter 44px fallen.
|
||||||
|
|
||||||
|
### R10 — Drei verschiedene Gauge-Track-Tokens
|
||||||
|
- **Dateien:** `kpi.blade.php:22` (`bg-inset`), `server-item.blade.php:14,20` (`bg-line`), `ring.blade.php:16` (`text-line`)
|
||||||
|
- **Änderung:** Ein einheitliches Track-Treatment wählen (`bg-inset` liest als vertiefter Kanal am besten) für alle Progress-/Gauge-Hintergründe; Ring-Track ggf. mit kräftigerem `line-strong`.
|
||||||
|
- **Grund:** Gleicher „Gauge-Hintergrund"-Begriff, drei verschiedene Tokens — `bg-line` (rgba 0.10) ist als Track fast unsichtbar.
|
||||||
|
|
||||||
|
### Polish — Ring-Cap bei 0% und kein Übergang
|
||||||
|
- **Datei:** `resources/views/components/ring.blade.php:17-18`
|
||||||
|
- **Änderung:** `stroke-linecap="butt"` bzw. den Wert-Kreis per `@if($v > 0)` schützen, Track-Stroke verstärken, `transition-[stroke-dashoffset] duration-500` ergänzen.
|
||||||
|
- **Grund:** Bei Wert 0 malt `stroke-linecap="round"` einen Streu-Punkt; Live-Updates springen ohne Transition.
|
||||||
|
|
||||||
|
### Polish — Sparkline ohne Baseline/Marker, verzerrter Stroke
|
||||||
|
- **Datei:** `resources/views/livewire/dashboard.blade.php:36-45`
|
||||||
|
- **Änderung:** `vector-effect="non-scaling-stroke"` auf die Polylines (oder `preserveAspectRatio="none"` entfernen), dezente Baseline/Gridline via `border-line`, Accent-Punkt am letzten Wert.
|
||||||
|
- **Grund:** `preserveAspectRatio="none"` streckt den 1,5px-Stroke ungleichmäßig; kein Bezugsrahmen/aktueller Wert auf der Linie.
|
||||||
|
|
||||||
|
### Polish — Inkonsistentes Eyebrow-Tracking
|
||||||
|
- **Dateien:** `dashboard.blade.php:14` (`tracking-[0.2em]`), `kpi.blade.php:10` (`tracking-wider`), `sidebar.blade.php:32` (`tracking-widest`); `server-item.blade.php:13,19` (kein Tracking)
|
||||||
|
- **Änderung:** Alle uppercase-Mono-Mikrolabels auf einen Wert vereinheitlichen (`tracking-widest`), arbitrary `tracking-[0.2em]` ersetzen; ggf. `x-eyebrow`-Helper.
|
||||||
|
- **Grund:** Gleiche Rolle (Mono-Eyebrow), vier verschiedene Letter-Spacings.
|
||||||
|
|
||||||
|
### Polish — Notifications-Dot kollidiert mit Brand-Accent
|
||||||
|
- **Datei:** `resources/views/components/topbar.blade.php:20`
|
||||||
|
- **Änderung:** Unread-Dot von `bg-accent` (orange) auf `bg-cyan` oder `bg-warning`.
|
||||||
|
- **Grund:** Orange liest als „aktiv/Brand", nicht als „Aufmerksamkeit"; Token-Intent für Accent wahren.
|
||||||
|
|
||||||
|
### API/R11 — `server-item` und Abmelden-Link hart `href="#"`
|
||||||
|
- **Dateien:** `server-item.blade.php:5`, `sidebar.blade.php:48`
|
||||||
|
- **Änderung:** `$href`-Prop ergänzen (`@props([… 'href' => '#'])`, `<a href="{{ $href }}">`), später `:href="route('servers.show', $s['id'])"` übergeben.
|
||||||
|
- **Grund:** Server-Liste nicht navigierbar und kein Slot für den von R11 geforderten UUID-Route-Key.
|
||||||
|
|
||||||
|
### API — `kpi`-`tone` themt Widget nur halb
|
||||||
|
- **Datei:** `resources/views/components/kpi.blade.php:22-24`
|
||||||
|
- **Änderung:** Identisch zum Must-Fix oben (Balkenfarbe aus `$tone`). Hier separat als API-Konsistenz notiert.
|
||||||
|
- **Grund:** `tone` impliziert das ganze Widget, färbt aber nur die Zahl.
|
||||||
|
|
||||||
|
### Datenform — Mock divergiert vom MetricsTicked/SSH-Kontrakt
|
||||||
|
- **Datei:** `app/Livewire/Dashboard.php:28-50`
|
||||||
|
- **Änderung:** Jedem Server eine `id` (UUID) und ein verschachteltes `metrics => ['cpu','mem','disk','series']` geben, damit Panel und Ringe aus `$servers[$active]` lesen und `MetricsTicked.server` 1:1 auf eine Zeile mappt.
|
||||||
|
- **Grund:** Globale `$metrics`-Serie ohne Server-Key und Rows ohne stabile id überleben den Broadcast/SSH-Kontrakt nicht.
|
||||||
|
|
||||||
|
### Security-Readiness — öffentlicher `metrics`-Channel
|
||||||
|
- **Datei:** `resources/js/app.js:20` (+ `app/Events/MetricsTicked.php`)
|
||||||
|
- **Änderung:** TODO setzen; mit Auth auf `PrivateChannel('fleet.metrics')` + `routes/channels.php`-Gate migrieren, in `app.js` auf `window.Echo.private(...)` umstellen.
|
||||||
|
- **Grund:** Öffentlicher Channel (kein `routes/channels.php`) — jeder Browser kann Flotten-CPU abonnieren; akzeptabel in der Mock-Phase, vor echten Daten zu flaggen.
|
||||||
|
|
||||||
|
### R9-Klarheit — englische systemd-Beschreibungen im Mock
|
||||||
|
- **Datei:** `app/Livewire/Dashboard.php:36-40`
|
||||||
|
- **Änderung:** Entweder Code-Kommentar „spiegelt reales Unit-`Description=`" (zulässig unter §6) oder bis zur echten SSH-Schicht auf knappe deutsche Beschreibungen umstellen.
|
||||||
|
- **Grund:** Handgeschriebene Mock-Copy rendert als sichtbarer Sekundärtext (`dashboard.blade.php:88`) und kann als englische UI-Copy gelesen werden.
|
||||||
|
|
||||||
|
### R3/R6-Sauberkeit — totes `welcome.blade.php`
|
||||||
|
- **Datei:** `resources/views/welcome.blade.php`
|
||||||
|
- **Änderung:** Datei löschen.
|
||||||
|
- **Grund:** Einzige Quelle roher Hex-Farben in `resources/views`, nicht geroutet/referenziert — verschmutzt jedes R3-Audit.
|
||||||
|
|
||||||
|
### Defensive Formatierung — `x-text="last + '%'"`
|
||||||
|
- **Datei:** `resources/views/livewire/dashboard.blade.php:51`
|
||||||
|
- **Änderung:** `x-text="Math.round(last) + '%'"` (nach dem `push()`-Guard).
|
||||||
|
- **Grund:** Verhindert lange Dezimalstellen aus dem künftigen SSH-Poller; sichtbare Oberfläche des NaN-Bugs oben.
|
||||||
|
|
||||||
|
### Optional — Progressbar-Semantik
|
||||||
|
- **Dateien:** `kpi.blade.php:22`, `server-item.blade.php` (Balken)
|
||||||
|
- **Änderung:** `role="progressbar" aria-valuenow="{{ $pct }}" aria-valuemin="0" aria-valuemax="100"` am Balken-Wrapper.
|
||||||
|
- **Grund:** Server-Item-Balken haben sichtbar keinen zugeordneten Zahlenwert; niedrige Prio, da KPI die Zahl textlich zeigt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bewusst weggelassen (kein Datei-Defekt)
|
||||||
|
- **R1–R11 Architektur:** Keine echten Verstöße gefunden (Routing, Livewire-3-Klassen, Token-only Farben, German Copy, Component-Kit alle verifiziert).
|
||||||
|
- **`config/livewire.php` fehlt:** Reiner Explizitheits-/Bootstrap-Hinweis — die Livewire-3-Defaults matchen die §4-Konvention; kein funktionaler Defekt.
|
||||||
|
- **`server-item` aria-valuenow / KPI-Progress-Semantik:** Als optional behalten, nicht als Blocker (siehe oben).
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
/* text ramp → text-ink / text-ink-2 ... */
|
/* text ramp → text-ink / text-ink-2 ... */
|
||||||
--color-ink: #E9EEF3;
|
--color-ink: #E9EEF3;
|
||||||
--color-ink-2: #9DAAB6;
|
--color-ink-2: #9DAAB6;
|
||||||
--color-ink-3: #69757F;
|
--color-ink-3: #7E8A95;
|
||||||
--color-ink-4: #495159;
|
--color-ink-4: #707B85;
|
||||||
|
|
||||||
/* hairline borders → border-line ... */
|
/* hairline borders → border-line ... */
|
||||||
--color-line: rgba(255, 255, 255, 0.10);
|
--color-line: rgba(255, 255, 255, 0.10);
|
||||||
|
|
@ -64,6 +64,22 @@
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* keyboard focus — visible token ring on the dark UI (a11y) */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* honor reduced-motion (disables the status-dot ping, transitions, …) */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.001ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.001ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--color-base);
|
background-color: var(--color-base);
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,25 @@ document.addEventListener('alpine:init', () => {
|
||||||
connected: false,
|
connected: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// reflect the REAL websocket state, not just "Echo exists"
|
|
||||||
const conn = window.Echo?.connector?.pusher?.connection;
|
const conn = window.Echo?.connector?.pusher?.connection;
|
||||||
this.connected = conn?.state === 'connected';
|
this.connected = conn?.state === 'connected';
|
||||||
conn?.bind('state_change', (s) => { this.connected = s.current === 'connected'; });
|
this._onState = (s) => { this.connected = s.current === 'connected'; };
|
||||||
window.Echo?.channel('metrics').listen('.tick', (e) => this.push(Number(e.cpu)));
|
conn?.bind('state_change', this._onState);
|
||||||
|
this._channel = window.Echo?.channel('metrics');
|
||||||
|
this._channel?.listen('.tick', (e) => this.push(e.cpu));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Alpine calls this when the island is removed (e.g. wire:navigate) — avoid leaking subscriptions.
|
||||||
|
destroy() {
|
||||||
|
window.Echo?.leave('metrics');
|
||||||
|
window.Echo?.connector?.pusher?.connection?.unbind('state_change', this._onState);
|
||||||
},
|
},
|
||||||
|
|
||||||
push(v) {
|
push(v) {
|
||||||
this.last = v;
|
const n = Number(v);
|
||||||
this.points.push(v);
|
if (! Number.isFinite(n)) return;
|
||||||
|
this.last = n;
|
||||||
|
this.points.push(n);
|
||||||
if (this.points.length > this.max) this.points.shift();
|
if (this.points.length > this.max) this.points.shift();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
'ink' => 'text-ink', 'online' => 'text-online', 'warning' => 'text-warning',
|
'ink' => 'text-ink', 'online' => 'text-online', 'warning' => 'text-warning',
|
||||||
'offline' => 'text-offline', 'accent' => 'text-accent-text', 'cyan' => 'text-cyan-bright',
|
'offline' => 'text-offline', 'accent' => 'text-accent-text', 'cyan' => 'text-cyan-bright',
|
||||||
][$tone] ?? 'text-ink';
|
][$tone] ?? 'text-ink';
|
||||||
|
$barColor = [
|
||||||
|
'ink' => 'bg-accent', 'online' => 'bg-online', 'warning' => 'bg-warning',
|
||||||
|
'offline' => 'bg-offline', 'accent' => 'bg-accent', 'cyan' => 'bg-cyan',
|
||||||
|
][$tone] ?? 'bg-accent';
|
||||||
@endphp
|
@endphp
|
||||||
<div {{ $attributes->merge(['class' => 'rounded-lg border border-line bg-surface p-4']) }}>
|
<div {{ $attributes->merge(['class' => 'rounded-lg border border-line bg-surface p-4']) }}>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
|
@ -20,7 +24,7 @@
|
||||||
</p>
|
</p>
|
||||||
@if (! is_null($pct))
|
@if (! is_null($pct))
|
||||||
<div class="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-inset">
|
<div class="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-inset">
|
||||||
<div class="h-full rounded-full bg-accent" style="width: {{ $pct }}%"></div>
|
<div class="h-full rounded-full {{ $barColor }}" style="width: {{ $pct }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
{{-- Fixed on desktop, off-canvas drawer on mobile/tablet (toggled by `nav` in the layout). --}}
|
{{-- Fixed on desktop, off-canvas drawer on mobile/tablet (toggled by `nav` in the layout). --}}
|
||||||
<aside class="fixed inset-y-0 left-0 z-40 flex w-[272px] -translate-x-full flex-col border-r border-line bg-surface transition-transform duration-200 lg:translate-x-0"
|
<aside class="fixed inset-y-0 left-0 z-40 flex w-[272px] -translate-x-full flex-col border-r border-line bg-surface transition-transform duration-200 lg:translate-x-0"
|
||||||
:class="{ '!translate-x-0': nav }">
|
:class="{ '!translate-x-0': nav }"
|
||||||
|
x-bind:inert="! nav && ! desktop"
|
||||||
|
aria-label="Seitennavigation">
|
||||||
{{-- Brand --}}
|
{{-- Brand --}}
|
||||||
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-line px-4">
|
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-line px-4">
|
||||||
<span class="grid h-7 w-7 place-items-center rounded-sm bg-accent/15 text-accent">
|
<span class="grid h-7 w-7 place-items-center rounded-sm bg-accent/15 text-accent">
|
||||||
|
|
@ -8,7 +10,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="font-display text-lg font-semibold tracking-wide text-ink">Clus<span class="text-accent">e</span>v</span>
|
<span class="font-display text-lg font-semibold tracking-wide text-ink">Clus<span class="text-accent">e</span>v</span>
|
||||||
<button type="button" @click="nav = false"
|
<button type="button" @click="nav = false"
|
||||||
class="ml-auto grid h-9 w-9 place-items-center rounded-md text-ink-3 hover:bg-raised hover:text-ink lg:hidden"
|
class="ml-auto grid min-h-11 min-w-11 place-items-center rounded-md text-ink-3 hover:bg-raised hover:text-ink lg:hidden"
|
||||||
aria-label="Menü schließen">
|
aria-label="Menü schließen">
|
||||||
<x-icon name="x" class="h-5 w-5" />
|
<x-icon name="x" class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -45,7 +47,7 @@
|
||||||
<span class="block truncate text-sm text-ink">admin</span>
|
<span class="block truncate text-sm text-ink">admin</span>
|
||||||
<span class="block truncate font-mono text-[11px] text-online">2FA aktiv</span>
|
<span class="block truncate font-mono text-[11px] text-online">2FA aktiv</span>
|
||||||
</span>
|
</span>
|
||||||
<a href="#" class="grid h-9 w-9 shrink-0 place-items-center rounded-md text-ink-3 hover:bg-raised hover:text-ink" aria-label="Abmelden">
|
<a href="#" class="grid min-h-11 min-w-11 shrink-0 place-items-center rounded-md text-ink-3 hover:bg-raised hover:text-ink" aria-label="Abmelden">
|
||||||
<x-icon name="logout" class="h-[18px] w-[18px]" />
|
<x-icon name="logout" class="h-[18px] w-[18px]" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
@props(['title' => 'Übersicht'])
|
@props(['title' => 'Übersicht'])
|
||||||
<header class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-line bg-base/90 px-4 backdrop-blur sm:px-6 lg:px-8">
|
<header class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-line bg-base/90 px-4 backdrop-blur sm:px-6 lg:px-8">
|
||||||
<button type="button" @click="nav = true"
|
<button type="button" @click="nav = true"
|
||||||
class="grid h-9 w-9 shrink-0 place-items-center rounded-md text-ink-2 hover:bg-raised hover:text-ink lg:hidden"
|
class="grid min-h-11 min-w-11 shrink-0 place-items-center rounded-md text-ink-2 hover:bg-raised hover:text-ink lg:hidden"
|
||||||
aria-label="Menü öffnen">
|
aria-label="Menü öffnen">
|
||||||
<x-icon name="menu" class="h-5 w-5" />
|
<x-icon name="menu" class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="hidden font-mono text-xs text-ink-3 md:inline">Uptime 42d</span>
|
<span class="hidden font-mono text-xs text-ink-3 md:inline">Uptime 42d</span>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="relative grid h-9 w-9 place-items-center rounded-md text-ink-2 hover:bg-raised hover:text-ink"
|
class="relative grid min-h-11 min-w-11 place-items-center rounded-md text-ink-2 hover:bg-raised hover:text-ink"
|
||||||
aria-label="Benachrichtigungen">
|
aria-label="Benachrichtigungen">
|
||||||
<x-icon name="bell" class="h-[18px] w-[18px]" />
|
<x-icon name="bell" class="h-[18px] w-[18px]" />
|
||||||
<span class="absolute right-2 top-2 h-1.5 w-1.5 rounded-full bg-accent"></span>
|
<span class="absolute right-2 top-2 h-1.5 w-1.5 rounded-full bg-accent"></span>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen overflow-x-clip bg-base font-sans text-ink antialiased selection:bg-accent/25">
|
<body class="min-h-screen overflow-x-clip bg-base font-sans text-ink antialiased selection:bg-accent/25">
|
||||||
<div x-data="{ nav: false }">
|
<div x-data="{ nav: false, desktop: window.matchMedia('(min-width: 1024px)').matches }"
|
||||||
|
@resize.window.debounce.200ms="desktop = window.matchMedia('(min-width: 1024px)').matches">
|
||||||
{{-- mobile/tablet backdrop --}}
|
{{-- mobile/tablet backdrop --}}
|
||||||
<div x-show="nav" x-cloak x-transition.opacity @click="nav = false"
|
<div x-show="nav" x-cloak x-transition.opacity @click="nav = false"
|
||||||
class="fixed inset-0 z-30 bg-void/70 lg:hidden"
|
class="fixed inset-0 z-30 bg-void/70 lg:hidden"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
$avgMem = $total ? (int) round(collect($servers)->avg('mem')) : 0;
|
$avgMem = $total ? (int) round(collect($servers)->avg('mem')) : 0;
|
||||||
|
|
||||||
$svcLabel = ['online' => 'aktiv', 'warning' => 'degradiert', 'offline' => 'gestoppt'];
|
$svcLabel = ['online' => 'aktiv', 'warning' => 'degradiert', 'offline' => 'gestoppt'];
|
||||||
|
|
||||||
|
$web = collect($servers)->firstWhere('name', 'web-01') ?? ($servers[0] ?? ['cpu' => 0, 'mem' => 0]);
|
||||||
|
$disk = 71; // mock until the SSH layer reports real disk usage
|
||||||
|
$ringTone = fn (int $v): string => $v >= 90 ? 'offline' : ($v >= 75 ? 'warning' : 'online');
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
@ -55,9 +59,9 @@
|
||||||
|
|
||||||
<x-panel title="Ressourcen" subtitle="web-01">
|
<x-panel title="Ressourcen" subtitle="web-01">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<x-ring :value="34" label="CPU" tone="accent" />
|
<x-ring :value="$web['cpu']" label="CPU" :tone="$ringTone($web['cpu'])" />
|
||||||
<x-ring :value="61" label="RAM" tone="cyan" />
|
<x-ring :value="$web['mem']" label="RAM" :tone="$ringTone($web['mem'])" />
|
||||||
<x-ring :value="48" label="Disk" tone="warning" />
|
<x-ring :value="$disk" label="Disk" :tone="$ringTone($disk)" />
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,7 +70,7 @@
|
||||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||||
<x-panel title="Server" :subtitle="$total . ' im Cluster'" :padded="false">
|
<x-panel title="Server" :subtitle="$total . ' im Cluster'" :padded="false">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
<button type="button" class="inline-flex min-h-9 items-center gap-1.5 rounded-md border border-accent/25 bg-accent/10 px-2.5 py-1 text-xs text-accent-text hover:bg-accent/15">
|
<button type="button" class="inline-flex min-h-11 items-center gap-1.5 rounded-md border border-accent/25 bg-accent/10 px-3 py-1.5 text-xs text-accent-text hover:bg-accent/15">
|
||||||
<x-icon name="plus" class="h-3.5 w-3.5" /> Hinzufügen
|
<x-icon name="plus" class="h-3.5 w-3.5" /> Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue