From 5835a89ddb239a25629689916f34fc72284ea793 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 6 Aug 2026 10:28:34 +0100 Subject: [PATCH] Refine status page presentation --- resources/css/cachet.css | 95 +++++++++++++++++++ .../components/component-group.blade.php | 10 +- .../components/component-groups.blade.php | 4 +- .../views/components/component.blade.php | 36 ++++--- resources/views/components/header.blade.php | 5 +- resources/views/components/logo.blade.php | 2 +- .../views/components/status-bar.blade.php | 24 ++++- resources/views/status-page/index.blade.php | 24 +++-- src/Status.php | 12 +++ src/View/Components/About.php | 8 +- src/View/Components/StatusBar.php | 7 +- tests/Feature/StatusPage/StatusPageTest.php | 29 ++++++ tests/Unit/StatusTest.php | 18 ++++ 13 files changed, 231 insertions(+), 43 deletions(-) diff --git a/resources/css/cachet.css b/resources/css/cachet.css index 3ed229f6..2cff5fcd 100644 --- a/resources/css/cachet.css +++ b/resources/css/cachet.css @@ -75,3 +75,98 @@ display: none !important; } } + +@layer components { + .status-header { + border-bottom: 1px solid rgb(24 24 27 / 10%); + background: rgb(255 255 255 / 70%); + backdrop-filter: blur(12px); + } + + .status-overview { + overflow: hidden; + border: 1px solid rgb(24 24 27 / 12%); + border-radius: 0.75rem; + background: rgb(255 255 255 / 92%); + box-shadow: 0 12px 28px -24px rgb(24 39 30 / 36%); + } + + .status-overview__masthead { + display: flex; + min-height: 3.75rem; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.875rem 1.25rem; + border-bottom: 1px solid rgb(24 24 27 / 10%); + } + + .status-overview__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .status-summary { + padding: 1.25rem; + border-bottom: 1px solid rgb(24 24 27 / 10%); + background: linear-gradient( + 115deg, + color-mix(in srgb, var(--color-100) 76%, white), + color-mix(in srgb, var(--color-200) 62%, white) + ); + } + + .status-summary__icon { + display: inline-flex; + width: 2rem; + height: 2rem; + flex: none; + align-items: center; + justify-content: center; + border-radius: 9999px; + background: color-mix(in srgb, currentColor 10%, transparent); + } + + .status-overview .status-components { + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + } + + .status-overview .status-components > ul > :last-child, + .status-overview .status-components > ul > :last-child > *:last-child { + border-bottom: 0; + } + + @media (min-width: 640px) { + .status-overview__masthead, + .status-summary { + padding-right: 1.5rem; + padding-left: 1.5rem; + } + } + + .dark .status-header { + background: rgb(24 24 27 / 66%); + } + + .dark .status-overview { + border-color: rgb(255 255 255 / 14%); + background: rgb(24 24 27 / 76%); + } + + .dark .status-overview__masthead, + .dark .status-summary { + border-color: rgb(255 255 255 / 14%); + } + + .dark .status-summary { + background: linear-gradient( + 115deg, + color-mix(in srgb, var(--color-400) 20%, rgb(24 24 27)), + color-mix(in srgb, var(--color-400) 34%, rgb(24 24 27)) + ); + } +} diff --git a/resources/views/components/component-group.blade.php b/resources/views/components/component-group.blade.php index 7870b023..03e07ce5 100644 --- a/resources/views/components/component-group.blade.php +++ b/resources/views/components/component-group.blade.php @@ -3,16 +3,16 @@ {{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENT_GROUPS_BEFORE) }} @php($groupStatus = $componentGroup->worstComponentStatus())
  • isExpanded(auth()->user())) default-open @endif> - diff --git a/resources/views/components/component-groups.blade.php b/resources/views/components/component-groups.blade.php index b6cb1b19..4e3fe308 100644 --- a/resources/views/components/component-groups.blade.php +++ b/resources/views/components/component-groups.blade.php @@ -1,7 +1,5 @@ @if ($componentGroups->isNotEmpty() || $ungroupedComponents->isNotEmpty()) -
    - - +
      @foreach ($componentGroups as $componentGroup) diff --git a/resources/views/components/component.blade.php b/resources/views/components/component.blade.php index 1c417b10..6b961f31 100644 --- a/resources/views/components/component.blade.php +++ b/resources/views/components/component.blade.php @@ -1,18 +1,24 @@ {{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENTS_BEFORE) }} -
    • -
      +
    • +
      -

      ! ($nested ?? false), - 'text-zinc-600 dark:text-zinc-300' => $nested ?? false, - ])> - @if($component->formattedLink()) - {{ $component->name }} - @else - {{ $component->name }} - @endif -

      + @if ($nested ?? false) +

      + @if($component->formattedLink()) + {{ $component->name }} + @else + {{ $component->name }} + @endif +

      + @else +

      + @if($component->formattedLink()) + {{ $component->name }} + @else + {{ $component->name }} + @endif +

      + @endif @if($component->description)
      @@ -36,11 +42,11 @@ class="relative shrink-0">
      @if ($component->impacting_incident) - + {{ $component->latest_status->getLabel() }} @else - {{ $component->latest_status->getLabel() }} + {{ $component->latest_status->getLabel() }} @endif
      diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php index 1200ae8b..1d3d2afe 100644 --- a/resources/views/components/header.blade.php +++ b/resources/views/components/header.blade.php @@ -1,12 +1,11 @@ {{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_NAVIGATION_BEFORE) }} -
      +
      @if($appBanner) {{ $siteName }} @else - diff --git a/resources/views/components/logo.blade.php b/resources/views/components/logo.blade.php index 71c4d97b..d1e9b801 100644 --- a/resources/views/components/logo.blade.php +++ b/resources/views/components/logo.blade.php @@ -1,5 +1,5 @@ merge(['class']) }}> -
      class(['rounded-lg bg-custom-200 p-4 ring-1 ring-custom-400/60 dark:bg-custom-400/30 dark:ring-custom-400/40'])->style([ +
      class(['status-summary'])->style([ Illuminate\Support\Arr::toCssStyles([ \Filament\Support\get_color_css_variables( $status->getColor(), - shades: [200, 400, 800], + shades: [100, 200, 400, 500, 800], ), ]), ]) }}> -
      - @svg($status->getIcon(), 'size-6 shrink-0 text-custom-800 dark:text-custom-200') -

      {{ $status->getLabel() }}

      +
      +
      + + @svg($status->getIcon(), 'size-5 shrink-0 text-custom-800 dark:text-custom-200') + + @if ($isHeading) +

      {{ $status->getLabel() }}

      + @else +

      {{ $status->getLabel() }}

      + @endif +
      + + @if ($lastUpdated) + + {{ __('cachet::component.last_updated', ['timestamp' => $lastUpdated->diffForHumans()]) }} + + @endif
      diff --git a/resources/views/status-page/index.blade.php b/resources/views/status-page/index.blade.php index db8d5909..0038b7b6 100644 --- a/resources/views/status-page/index.blade.php +++ b/resources/views/status-page/index.blade.php @@ -1,13 +1,25 @@ -
      - + @php($appSettings = app(\Cachet\Settings\AppSettings::class)) + @php($siteName = $appSettings->name ?: config('cachet.title', 'Cachet')) - +
      +
      + + @if ($appSettings->show_site_name) +
      +

      {{ $siteName }}

      +
      + @endif + + + + +
      + + - - @if ($display_graphs) @endif @@ -17,7 +29,7 @@ @endif -
      +
      diff --git a/src/Status.php b/src/Status.php index e73b8b4c..32c0f795 100644 --- a/src/Status.php +++ b/src/Status.php @@ -8,6 +8,7 @@ use Cachet\Models\Component; use Cachet\Models\Incident; use Cachet\Settings\AppSettings; +use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Builder; class Status @@ -102,6 +103,17 @@ public function incidents(): object ->first(); } + /** + * Get the most recent update timestamp across enabled components. + */ + public function lastUpdated(): ?CarbonInterface + { + return Component::query() + ->enabled() + ->latest('updated_at') + ->first(['updated_at'])?->updated_at; + } + /** * Tally the enabled components by their effective status. * diff --git a/src/View/Components/About.php b/src/View/Components/About.php index 7aaf6a5a..b8ec23b5 100644 --- a/src/View/Components/About.php +++ b/src/View/Components/About.php @@ -14,8 +14,10 @@ class About extends Component /** * Create a new component instance. */ - public function __construct(private AppSettings $settings) - { + public function __construct( + private AppSettings $settings, + public readonly ?bool $showSiteName = null, + ) { // } @@ -27,7 +29,7 @@ public function render(): View|Closure|string return view('cachet::components.about', [ 'title' => $this->settings->name ?: config('cachet.title', 'Cachet'), 'about' => Cachet::markdown(Str::of($this->settings->about)->trim()->toString()), - 'showSiteName' => $this->settings->show_site_name, + 'showSiteName' => $this->showSiteName ?? $this->settings->show_site_name, 'showAbout' => $this->settings->show_about, ]); } diff --git a/src/View/Components/StatusBar.php b/src/View/Components/StatusBar.php index d3e60c69..fbb7a500 100644 --- a/src/View/Components/StatusBar.php +++ b/src/View/Components/StatusBar.php @@ -12,8 +12,10 @@ class StatusBar extends ViewComponent /** * Create a new component instance. */ - public function __construct(protected readonly Status $status) - { + public function __construct( + protected readonly Status $status, + public readonly bool $isHeading = false, + ) { // } @@ -24,6 +26,7 @@ public function render(): View|Closure|string { return view('cachet::components.status-bar', [ 'status' => $this->status->current(), + 'lastUpdated' => $this->status->lastUpdated(), ]); } } diff --git a/tests/Feature/StatusPage/StatusPageTest.php b/tests/Feature/StatusPage/StatusPageTest.php index fbbf7279..bef3e25b 100644 --- a/tests/Feature/StatusPage/StatusPageTest.php +++ b/tests/Feature/StatusPage/StatusPageTest.php @@ -2,6 +2,7 @@ use Cachet\Enums\ComponentStatusEnum; use Cachet\Models\Component; +use Cachet\Models\ComponentGroup; use Cachet\Models\Incident; use Cachet\Models\Schedule; use Cachet\Settings\AppSettings; @@ -32,6 +33,34 @@ ->assertSee('', escape: false); }); +it('shows the site name once when it is enabled', function () { + $settings = app(AppSettings::class); + $settings->name = 'Acme Status'; + $settings->show_site_name = true; + $settings->show_about = false; + $settings->save(); + + $response = $this->get(route('cachet.status-page'))->assertOk(); + $body = Str::after($response->getContent(), 'toBe(1); +}); + +it('uses a logical heading hierarchy for components', function () { + $group = ComponentGroup::factory()->create(['name' => 'Core services']); + Component::factory()->create(['name' => 'Public API']); + Component::factory()->create(['name' => 'Core API', 'component_group_id' => $group->id]); + + $page = $this->get(route('cachet.status-page')) + ->assertOk() + ->getContent(); + + expect($page) + ->toMatch('/]*>\s*Core services\s*<\\/h2>/') + ->toMatch('/]*>\s*Core API\s*<\\/h3>/') + ->toMatch('/]*>\s*Public API\s*<\\/h2>/'); +}); + it('renders the status page in the configured locale', function () { $settings = app(AppSettings::class); $settings->locale = 'de'; diff --git a/tests/Unit/StatusTest.php b/tests/Unit/StatusTest.php index 29ab9b8d..fbcd06ce 100644 --- a/tests/Unit/StatusTest.php +++ b/tests/Unit/StatusTest.php @@ -12,6 +12,7 @@ use Cachet\Models\Schedule; use Cachet\Models\Update; use Cachet\Status; +use Carbon\CarbonInterface; use function PHPUnit\Framework\assertFalse; use function PHPUnit\Framework\assertTrue; @@ -103,6 +104,23 @@ ->under_maintenance->toBe(1); }); +it('returns the most recent enabled component update timestamp', function () { + Component::factory()->create([ + 'enabled' => false, + 'updated_at' => now(), + ]); + $component = Component::factory()->create([ + 'enabled' => true, + 'updated_at' => now()->subMinute(), + ]); + + $lastUpdated = (new Status)->lastUpdated(); + + expect($lastUpdated) + ->toBeInstanceOf(CarbonInterface::class) + ->toEqual($component->updated_at); +}); + it('excludes disabled components from component overview', function () { Component::factory()->create([ 'status' => ComponentStatusEnum::operational->value,