Dashboard: live service health + fault surfacing; declutter sidebar
Addresses user feedback on Phase 1: - Dashboard reoriented from static stack facts to LIVE operational status. New health banner surfaces faults prominently (green when all online; amber/red with a pluralized problem count when a service is down). Service tiles probe DB, Redis, Reverb (socket) and Horizon in real time with latency; versions moved to a small, de-emphasized "System" block. wire:poll.10s + a refresh button keep it live. Verified: stopping Reverb flips the banner to "Ein Dienst mit Störung" and the Echtzeit tile to Offline/nicht erreichbar (screenshotted), healthy state restores clean. - Sidebar decluttered: removed the wrapping "In Kürze" count badges; coming-soon items are now dimmed (still show lock icons where gated). R12 re-verified in headless Chromium: 15/15 assertions, 0 console errors, 0 failed requests, breakpoints 375/768/1280. 7 feature tests still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/phase-1-bootstrap
parent
beac04ec51
commit
d4f667437c
|
|
@ -29,6 +29,6 @@ Thumbs.db
|
||||||
# HomeOS
|
# HomeOS
|
||||||
/docker-compose.override.yml
|
/docker-compose.override.yml
|
||||||
/_bootstrap
|
/_bootstrap
|
||||||
/_verify.mjs
|
/_verify*.mjs
|
||||||
/sidecar/__pycache__
|
/sidecar/__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,117 @@
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Layout('layouts.app')]
|
#[Layout('layouts.app')]
|
||||||
class Dashboard extends Component
|
class Dashboard extends Component
|
||||||
{
|
{
|
||||||
/** Interactive demo toggle — verifies Livewire reactivity in Phase 1. */
|
/** @var array<int, array{key:string,label:string,icon:string,state:string,detail:string}> */
|
||||||
public bool $demoOn = true;
|
public array $services = [];
|
||||||
|
|
||||||
public function toggleDemo(): void
|
public ?string $checkedAt = null;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->demoOn = ! $this->demoOn;
|
$this->refreshHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-run all service health probes (called on mount, wire:poll and the refresh button). */
|
||||||
|
public function refreshHealth(): void
|
||||||
|
{
|
||||||
|
$this->services = [
|
||||||
|
$this->checkDatabase(),
|
||||||
|
$this->checkRedis(),
|
||||||
|
$this->checkReverb(),
|
||||||
|
$this->checkHorizon(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->checkedAt = now()->format('H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Worst state across services (drives the banner): offline > warning > online. */
|
||||||
|
public function worstState(): string
|
||||||
|
{
|
||||||
|
$states = array_column($this->services, 'state');
|
||||||
|
|
||||||
|
return in_array('offline', $states, true) ? 'offline'
|
||||||
|
: (in_array('warning', $states, true) ? 'warning' : 'online');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function problemCount(): int
|
||||||
|
{
|
||||||
|
return count(array_filter($this->services, fn ($s) => $s['state'] !== 'online'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkDatabase(): array
|
||||||
|
{
|
||||||
|
$base = ['key' => 'database', 'label' => __('dashboard.svc_database'), 'icon' => 'network'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$start = microtime(true);
|
||||||
|
DB::connection()->select('select 1');
|
||||||
|
$ms = (int) round((microtime(true) - $start) * 1000);
|
||||||
|
|
||||||
|
return $base + ['state' => 'online', 'detail' => $ms.' ms'];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $base + ['state' => 'offline', 'detail' => __('dashboard.svc_unreachable')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkRedis(): array
|
||||||
|
{
|
||||||
|
$base = ['key' => 'cache', 'label' => __('dashboard.svc_cache'), 'icon' => 'devices'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$start = microtime(true);
|
||||||
|
Redis::connection()->ping();
|
||||||
|
$ms = (int) round((microtime(true) - $start) * 1000);
|
||||||
|
|
||||||
|
return $base + ['state' => 'online', 'detail' => $ms.' ms'];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $base + ['state' => 'offline', 'detail' => __('dashboard.svc_unreachable')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkReverb(): array
|
||||||
|
{
|
||||||
|
$base = ['key' => 'realtime', 'label' => __('dashboard.svc_realtime'), 'icon' => 'activity'];
|
||||||
|
|
||||||
|
$host = config('broadcasting.connections.reverb.options.host', 'reverb');
|
||||||
|
$port = (int) config('broadcasting.connections.reverb.options.port', 8080);
|
||||||
|
|
||||||
|
$conn = @fsockopen($host, $port, $errno, $errstr, 1.0);
|
||||||
|
if ($conn) {
|
||||||
|
fclose($conn);
|
||||||
|
|
||||||
|
return $base + ['state' => 'online', 'detail' => $host.':'.$port];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base + ['state' => 'offline', 'detail' => __('dashboard.svc_unreachable')];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkHorizon(): array
|
||||||
|
{
|
||||||
|
$base = ['key' => 'queue', 'label' => __('dashboard.svc_queue'), 'icon' => 'automation'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$masters = app(\Laravel\Horizon\Contracts\MasterSupervisorRepository::class)->all();
|
||||||
|
|
||||||
|
if (empty($masters)) {
|
||||||
|
return $base + ['state' => 'offline', 'detail' => __('dashboard.svc_stopped')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$running = collect($masters)->contains(fn ($m) => ($m->status ?? null) === 'running');
|
||||||
|
|
||||||
|
return $base + ($running
|
||||||
|
? ['state' => 'online', 'detail' => __('dashboard.svc_running')]
|
||||||
|
: ['state' => 'warning', 'detail' => __('dashboard.svc_stopped')]);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $base + ['state' => 'offline', 'detail' => __('dashboard.svc_unreachable')];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,31 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => 'Dashboard',
|
'title' => 'Dashboard',
|
||||||
'subtitle' => 'Überblick über Haus, Geräte und Anwesenheit.',
|
'subtitle' => 'Live-Status von System und Diensten.',
|
||||||
|
|
||||||
'welcome_title' => 'Willkommen bei HomeOS',
|
// health banner
|
||||||
'welcome_body' => 'Das Grundgerüst steht. Räume, Geräte und Live-Zustände folgen in der nächsten Phase.',
|
'health_ok_title' => 'Alle Dienste online',
|
||||||
|
'health_ok_body' => 'Das System läuft ohne bekannte Störungen.',
|
||||||
|
'health_problem_title' => '{1}Ein Dienst mit Störung|[2,*]:count Dienste mit Störungen',
|
||||||
|
'health_problem_body' => 'Bitte die betroffenen Dienste unten prüfen.',
|
||||||
|
|
||||||
'phase_label' => 'Aufbau',
|
// services
|
||||||
'phase_value' => 'Phase 1',
|
'services_title' => 'Dienste',
|
||||||
'phase_meta' => 'Fundament · Auth · Design-System',
|
'svc_database' => 'Datenbank',
|
||||||
|
'svc_cache' => 'Cache / Redis',
|
||||||
|
'svc_realtime' => 'Echtzeit',
|
||||||
|
'svc_queue' => 'Queue-Worker',
|
||||||
|
'svc_unreachable'=> 'nicht erreichbar',
|
||||||
|
'svc_running' => 'läuft',
|
||||||
|
'svc_stopped' => 'gestoppt',
|
||||||
|
|
||||||
|
'refresh' => 'Aktualisieren',
|
||||||
|
'checked_at' => 'Geprüft um :time',
|
||||||
|
|
||||||
|
// compact system info (versions — belongs to its own area later)
|
||||||
|
'system_title' => 'System',
|
||||||
'stack_framework' => 'Framework',
|
'stack_framework' => 'Framework',
|
||||||
'stack_database' => 'Datenbank',
|
'stack_database' => 'Datenbank',
|
||||||
'stack_realtime' => 'Echtzeit',
|
'stack_realtime' => 'Echtzeit',
|
||||||
'stack_queue' => 'Queue',
|
'stack_queue' => 'Queue',
|
||||||
|
|
||||||
'legend_title' => 'Design-System',
|
|
||||||
'legend_status' => 'Statusfarben',
|
|
||||||
'demo_toggle' => 'Beispiel-Schalter',
|
|
||||||
'demo_chips' => 'Geräte-Chips',
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,31 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => 'Dashboard',
|
'title' => 'Dashboard',
|
||||||
'subtitle' => 'Overview of home, devices and presence.',
|
'subtitle' => 'Live status of system and services.',
|
||||||
|
|
||||||
'welcome_title' => 'Welcome to HomeOS',
|
// health banner
|
||||||
'welcome_body' => 'The foundation is in place. Rooms, devices and live state arrive in the next phase.',
|
'health_ok_title' => 'All services online',
|
||||||
|
'health_ok_body' => 'The system is running with no known issues.',
|
||||||
|
'health_problem_title' => '{1}One service has an issue|[2,*]:count services have issues',
|
||||||
|
'health_problem_body' => 'Please check the affected services below.',
|
||||||
|
|
||||||
'phase_label' => 'Build',
|
// services
|
||||||
'phase_value' => 'Phase 1',
|
'services_title' => 'Services',
|
||||||
'phase_meta' => 'Foundation · Auth · Design system',
|
'svc_database' => 'Database',
|
||||||
|
'svc_cache' => 'Cache / Redis',
|
||||||
|
'svc_realtime' => 'Realtime',
|
||||||
|
'svc_queue' => 'Queue worker',
|
||||||
|
'svc_unreachable'=> 'unreachable',
|
||||||
|
'svc_running' => 'running',
|
||||||
|
'svc_stopped' => 'stopped',
|
||||||
|
|
||||||
|
'refresh' => 'Refresh',
|
||||||
|
'checked_at' => 'Checked at :time',
|
||||||
|
|
||||||
|
// compact system info (versions — belongs to its own area later)
|
||||||
|
'system_title' => 'System',
|
||||||
'stack_framework' => 'Framework',
|
'stack_framework' => 'Framework',
|
||||||
'stack_database' => 'Database',
|
'stack_database' => 'Database',
|
||||||
'stack_realtime' => 'Realtime',
|
'stack_realtime' => 'Realtime',
|
||||||
'stack_queue' => 'Queue',
|
'stack_queue' => 'Queue',
|
||||||
|
|
||||||
'legend_title' => 'Design system',
|
|
||||||
'legend_status' => 'Status colors',
|
|
||||||
'demo_toggle' => 'Example switch',
|
|
||||||
'demo_chips' => 'Device chips',
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -51,16 +51,14 @@
|
||||||
<span>{{ __('nav.'.$item['key']) }}</span>
|
<span>{{ __('nav.'.$item['key']) }}</span>
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
<button type="button" title="{{ __('nav.soon') }}"
|
<span title="{{ __('nav.soon') }}"
|
||||||
class="flex items-center gap-2.5 w-full px-2.5 py-2 rounded-lg text-[13px] font-semibold text-ink-2 hover:bg-raised hover:text-ink transition-colors cursor-default">
|
class="flex items-center gap-2.5 w-full px-2.5 py-2 rounded-lg text-[13px] font-medium text-ink-3 cursor-default select-none">
|
||||||
<x-icon :name="$item['icon']" :size="16" />
|
<x-icon :name="$item['icon']" :size="16" class="opacity-80" />
|
||||||
<span>{{ __('nav.'.$item['key']) }}</span>
|
<span class="leading-tight">{{ __('nav.'.$item['key']) }}</span>
|
||||||
@if ($item['lock'])
|
@if ($item['lock'])
|
||||||
<span class="ml-auto text-ink-3"><x-icon name="lock" :size="13" /></span>
|
<span class="ml-auto shrink-0"><x-icon name="lock" :size="13" /></span>
|
||||||
@else
|
|
||||||
<x-badge class="ml-auto">{{ __('nav.soon') }}</x-badge>
|
|
||||||
@endif
|
@endif
|
||||||
</button>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,93 @@
|
||||||
<div>
|
<div wire:poll.10s="refreshHealth">
|
||||||
<x-topbar :title="__('dashboard.title')" :subtitle="__('dashboard.subtitle')" />
|
<x-topbar :title="__('dashboard.title')" :subtitle="__('dashboard.subtitle')" />
|
||||||
|
|
||||||
<div class="px-5 lg:px-[26px] py-6 flex flex-col gap-5 max-w-[1560px] mx-auto w-full">
|
<div class="px-5 lg:px-[26px] py-6 flex flex-col gap-5 max-w-[1560px] mx-auto w-full">
|
||||||
{{-- stack facts --}}
|
@php
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 reveal">
|
$worst = $this->worstState();
|
||||||
<x-kpi :label="__('dashboard.stack_framework')" value="Laravel 13" icon="bolt" />
|
$problems = $this->problemCount();
|
||||||
<x-kpi :label="__('dashboard.stack_database')" value="PG 17" :meta="'TimescaleDB'" icon="network" />
|
$bannerMap = [
|
||||||
<x-kpi :label="__('dashboard.stack_realtime')" value="Reverb" :meta="'WebSocket'" icon="activity" />
|
'online' => ['bg' => 'bg-online/10', 'bar' => 'bg-online', 'icon' => 'check', 'tint' => 'text-online'],
|
||||||
<x-kpi :label="__('dashboard.stack_queue')" value="Redis" :meta="'Horizon'" icon="devices" />
|
'warning' => ['bg' => 'bg-warning/10', 'bar' => 'bg-warning', 'icon' => 'alert', 'tint' => 'text-warning'],
|
||||||
</div>
|
'offline' => ['bg' => 'bg-offline/10', 'bar' => 'bg-offline', 'icon' => 'alert', 'tint' => 'text-offline'],
|
||||||
|
];
|
||||||
|
$b = $bannerMap[$worst];
|
||||||
|
$stateLabel = [
|
||||||
|
'online' => __('common.status_online'),
|
||||||
|
'warning' => __('common.status_warning'),
|
||||||
|
'offline' => __('common.status_offline'),
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_340px] gap-5 items-start">
|
{{-- health banner — surfaces faults prominently --}}
|
||||||
{{-- welcome --}}
|
<section class="relative overflow-hidden rounded-card border border-line-soft {{ $b['bg'] }} reveal">
|
||||||
<x-panel class="reveal">
|
<span class="absolute inset-y-0 left-0 w-[3px] {{ $b['bar'] }}"></span>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex items-center gap-3.5 p-4 pl-5">
|
||||||
<div class="flex items-start gap-3">
|
<span class="grid place-items-center w-10 h-10 rounded-lg bg-base/40 {{ $b['tint'] }} shrink-0">
|
||||||
<span class="logo-mark grid place-items-center w-11 h-11 rounded-[12px] text-accent shrink-0">
|
<x-icon :name="$b['icon']" :size="20" />
|
||||||
<x-icon name="dashboard" :size="22" />
|
</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="text-[15px] font-bold text-ink">
|
||||||
|
{{ $worst === 'online'
|
||||||
|
? __('dashboard.health_ok_title')
|
||||||
|
: trans_choice('dashboard.health_problem_title', $problems, ['count' => $problems]) }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-[12.5px] text-ink-2">
|
||||||
|
{{ $worst === 'online' ? __('dashboard.health_ok_body') : __('dashboard.health_problem_body') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-3 shrink-0">
|
||||||
|
<span class="hidden sm:block text-[11px] font-mono text-ink-3">{{ __('dashboard.checked_at', ['time' => $checkedAt]) }}</span>
|
||||||
|
<button type="button" wire:click="refreshHealth" wire:loading.attr="disabled" wire:target="refreshHealth"
|
||||||
|
class="grid place-items-center w-9 h-9 rounded-lg bg-base/40 text-ink-2 hover:text-ink transition-colors disabled:opacity-50"
|
||||||
|
aria-label="{{ __('dashboard.refresh') }}" title="{{ __('dashboard.refresh') }}">
|
||||||
|
<span wire:loading.remove wire:target="refreshHealth"><x-icon name="activity" :size="17" /></span>
|
||||||
|
<span wire:loading wire:target="refreshHealth" class="pulse-live"><x-icon name="activity" :size="17" /></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{{-- live service status — "was läuft gerade" --}}
|
||||||
|
<x-panel :title="__('dashboard.services_title')" class="reveal">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
@foreach ($services as $svc)
|
||||||
|
<div class="flex items-center gap-3 rounded-lg border border-line-soft bg-raised p-3">
|
||||||
|
<span class="grid place-items-center w-9 h-9 rounded-lg bg-inset text-ink-2 shrink-0">
|
||||||
|
<x-icon :name="$svc['icon']" :size="18" />
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="min-w-0">
|
||||||
<h2 class="text-base font-bold text-ink">{{ __('dashboard.welcome_title') }}</h2>
|
<div class="text-[13px] font-semibold text-ink">{{ $svc['label'] }}</div>
|
||||||
<p class="text-[13px] text-ink-2 leading-relaxed">{{ __('dashboard.welcome_body') }}</p>
|
<div class="text-[11.5px] text-ink-3 font-mono tabular-nums">{{ $svc['detail'] }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<x-status-pill :state="$svc['state']" class="ml-auto shrink-0">
|
||||||
<div class="flex flex-wrap items-center gap-2 pt-1">
|
<x-status-dot :state="$svc['state']" :pulse="$svc['state'] === 'online'" class="!w-1.5 !h-1.5" />
|
||||||
<x-status-pill state="online">
|
{{ $stateLabel[$svc['state']] }}
|
||||||
<x-status-dot state="online" class="!w-1.5 !h-1.5" /> {{ __('common.system_ok') }}
|
|
||||||
</x-status-pill>
|
</x-status-pill>
|
||||||
<x-status-pill state="neutral">{{ __('dashboard.phase_label') }} · {{ __('dashboard.phase_value') }}</x-status-pill>
|
|
||||||
</div>
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</x-panel>
|
||||||
|
|
||||||
|
{{-- compact system info (versions) — de-emphasized, own area later --}}
|
||||||
|
<x-panel :title="__('dashboard.system_title')" class="reveal">
|
||||||
|
<dl class="grid grid-cols-2 sm:grid-cols-4 gap-x-4 gap-y-3">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<dt class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.stack_framework') }}</dt>
|
||||||
|
<dd class="text-[13px] font-mono text-ink-2">Laravel {{ app()->version() }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<dt class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.stack_database') }}</dt>
|
||||||
{{-- design-system legend --}}
|
<dd class="text-[13px] font-mono text-ink-2">PostgreSQL 17 · TSDB</dd>
|
||||||
<x-panel :title="__('dashboard.legend_title')" class="reveal">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
{{-- status colors --}}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.legend_status') }}</span>
|
|
||||||
<div class="flex flex-col gap-1.5 text-[13px] text-ink-2">
|
|
||||||
<span class="flex items-center gap-2"><x-status-dot state="online" :pulse="true" /> {{ __('common.status_online') }}</span>
|
|
||||||
<span class="flex items-center gap-2"><x-status-dot state="warning" /> {{ __('common.status_warning') }}</span>
|
|
||||||
<span class="flex items-center gap-2"><x-status-dot state="offline" /> {{ __('common.status_offline') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- interactive toggle (verifies Livewire) --}}
|
|
||||||
<div class="flex items-center gap-3 border-t border-line-soft pt-3">
|
|
||||||
<span class="text-[13px] text-ink-2">{{ __('dashboard.demo_toggle') }}</span>
|
|
||||||
<x-toggle :on="$demoOn" wire:click="toggleDemo" :label="__('dashboard.demo_toggle')" class="ml-auto" />
|
|
||||||
<x-status-pill :state="$demoOn ? 'online' : 'neutral'">{{ $demoOn ? __('common.status_online') : __('common.status_offline') }}</x-status-pill>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- device chips --}}
|
|
||||||
<div class="flex flex-col gap-2 border-t border-line-soft pt-3">
|
|
||||||
<span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.demo_chips') }}</span>
|
|
||||||
<div class="flex flex-wrap gap-1.5">
|
|
||||||
<x-device-chip state="online">Shelly Plus 1</x-device-chip>
|
|
||||||
<x-device-chip state="warning">BLU Door</x-device-chip>
|
|
||||||
<x-device-chip state="offline" :off="true">Plug S</x-device-chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
<div class="flex flex-col gap-0.5">
|
||||||
</div>
|
<dt class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.stack_realtime') }}</dt>
|
||||||
|
<dd class="text-[13px] font-mono text-ink-2">Reverb</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
<dt class="text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-3">{{ __('dashboard.stack_queue') }}</dt>
|
||||||
|
<dd class="text-[13px] font-mono text-ink-2">Redis · Horizon</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</x-panel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue