diff --git a/database/migrations/2026_08_06_000001_add_component_group_status_setting.php b/database/migrations/2026_08_06_000001_add_component_group_status_setting.php new file mode 100644 index 00000000..1009c223 --- /dev/null +++ b/database/migrations/2026_08_06_000001_add_component_group_status_setting.php @@ -0,0 +1,11 @@ + $this->migrator->add('app.show_component_group_status', true)); + } +}; diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index e8c9168b..d4a3ca69 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -42,6 +42,8 @@ 'api_protected_helper' => 'Für alle API-Anfragen einen API-Token verlangen, auch für rein lesende Endpunkte.', 'dynamic_favicon' => 'Dynamisches Favicon', 'dynamic_favicon_helper' => 'Das Favicon aktualisieren, damit es den aktuellen Status deiner Systeme widerspiegelt.', + 'show_component_group_status' => 'Status der Komponentengruppe anzeigen', + 'show_component_group_status_helper' => 'Den schwerwiegendsten Komponentenstatus jeder Gruppe auf der öffentlichen Statusseite anzeigen.', ], 'display_settings_title' => 'Anzeigeeinstellungen', 'api_settings_title' => 'API-Einstellungen', diff --git a/resources/lang/de_AT/settings.php b/resources/lang/de_AT/settings.php index e8c9168b..d4a3ca69 100644 --- a/resources/lang/de_AT/settings.php +++ b/resources/lang/de_AT/settings.php @@ -42,6 +42,8 @@ 'api_protected_helper' => 'Für alle API-Anfragen einen API-Token verlangen, auch für rein lesende Endpunkte.', 'dynamic_favicon' => 'Dynamisches Favicon', 'dynamic_favicon_helper' => 'Das Favicon aktualisieren, damit es den aktuellen Status deiner Systeme widerspiegelt.', + 'show_component_group_status' => 'Status der Komponentengruppe anzeigen', + 'show_component_group_status_helper' => 'Den schwerwiegendsten Komponentenstatus jeder Gruppe auf der öffentlichen Statusseite anzeigen.', ], 'display_settings_title' => 'Anzeigeeinstellungen', 'api_settings_title' => 'API-Einstellungen', diff --git a/resources/lang/de_CH/settings.php b/resources/lang/de_CH/settings.php index e8c9168b..d4a3ca69 100644 --- a/resources/lang/de_CH/settings.php +++ b/resources/lang/de_CH/settings.php @@ -42,6 +42,8 @@ 'api_protected_helper' => 'Für alle API-Anfragen einen API-Token verlangen, auch für rein lesende Endpunkte.', 'dynamic_favicon' => 'Dynamisches Favicon', 'dynamic_favicon_helper' => 'Das Favicon aktualisieren, damit es den aktuellen Status deiner Systeme widerspiegelt.', + 'show_component_group_status' => 'Status der Komponentengruppe anzeigen', + 'show_component_group_status_helper' => 'Den schwerwiegendsten Komponentenstatus jeder Gruppe auf der öffentlichen Statusseite anzeigen.', ], 'display_settings_title' => 'Anzeigeeinstellungen', 'api_settings_title' => 'API-Einstellungen', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index fa945ec4..4029ee51 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -46,6 +46,8 @@ 'mcp_protected_helper' => 'Require an API token for all MCP connections. When off, read-only tools are public and write tools still require an API token with the matching ability.', 'dynamic_favicon' => 'Dynamic favicon', 'dynamic_favicon_helper' => 'Update the favicon to reflect the current status of your systems.', + 'show_component_group_status' => 'Show component group status', + 'show_component_group_status_helper' => 'Display each group’s most severe component status on the public status page.', ], 'display_settings_title' => 'Display settings', 'api_settings_title' => 'API settings', diff --git a/resources/lang/en_GB/settings.php b/resources/lang/en_GB/settings.php index 9064be67..d2d9a5b8 100644 --- a/resources/lang/en_GB/settings.php +++ b/resources/lang/en_GB/settings.php @@ -7,6 +7,8 @@ 'show_site_name_helper' => 'Display the site name on the public status page.', 'show_about' => 'Show About this site', 'show_about_helper' => 'Display the About this site content on the public status page.', + 'show_component_group_status' => 'Show component group status', + 'show_component_group_status_helper' => 'Display each group’s most severe component status on the public status page.', ], ], 'manage_theme' => [ diff --git a/resources/lang/es_ES/settings.php b/resources/lang/es_ES/settings.php index 0293684a..8edea750 100644 --- a/resources/lang/es_ES/settings.php +++ b/resources/lang/es_ES/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => 'Exigir un token de API para todas las peticiones, incluidos los endpoints de solo lectura.', 'dynamic_favicon' => 'Favicon Dinámico', 'dynamic_favicon_helper' => 'Actualiza el favicon para reflejar el estado actual de tus sistemas.', + 'show_component_group_status' => 'Mostrar el estado del grupo de componentes', + 'show_component_group_status_helper' => 'Muestra el estado más grave de los componentes de cada grupo en la página de estado pública.', ], 'display_settings_title' => 'Configuración de visualización', 'api_settings_title' => 'Configuración de la API', diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php index cb1b0b8d..dfb88073 100644 --- a/resources/lang/fr/settings.php +++ b/resources/lang/fr/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => "Exiger un jeton d\u{2019}API pour toutes les requ\u{00EA}tes, y compris les points de terminaison en lecture seule.", 'dynamic_favicon' => 'Favicon dynamique', 'dynamic_favicon_helper' => "Met \u{00E0} jour le favicon pour refl\u{00E9}ter l\u{2019}\u{00E9}tat actuel de vos syst\u{00E8}mes.", + 'show_component_group_status' => 'Afficher l’état du groupe de composants', + 'show_component_group_status_helper' => 'Affiche l’état le plus grave des composants de chaque groupe sur la page d’état publique.', ], 'display_settings_title' => "Param\u{00E8}tres d\u{2019}affichage", 'api_settings_title' => "Param\u{00E8}tres de l\u{2019}API", diff --git a/resources/lang/ko/settings.php b/resources/lang/ko/settings.php index 2ba61c82..86f5cc8d 100644 --- a/resources/lang/ko/settings.php +++ b/resources/lang/ko/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => '읽기 전용 엔드포인트를 포함한 모든 API 요청에 API 토큰을 요구합니다.', 'dynamic_favicon' => '동적 파비콘', 'dynamic_favicon_helper' => '시스템의 현재 상태를 반영하도록 파비콘을 업데이트합니다.', + 'show_component_group_status' => '구성 요소 그룹 상태 표시', + 'show_component_group_status_helper' => '공개 상태 페이지에 각 그룹에서 가장 심각한 구성 요소 상태를 표시합니다.', ], 'display_settings_title' => '표시 설정', 'api_settings_title' => 'API 설정', diff --git a/resources/lang/nl/settings.php b/resources/lang/nl/settings.php index dd798e1c..ccfc2572 100644 --- a/resources/lang/nl/settings.php +++ b/resources/lang/nl/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => 'Vereis een API-token voor alle API-verzoeken, inclusief alleen-lezen endpoints.', 'dynamic_favicon' => 'Dynamische favicon', 'dynamic_favicon_helper' => 'Werk de favicon bij zodat deze de huidige status van je systemen weergeeft.', + 'show_component_group_status' => 'Status van componentgroep weergeven', + 'show_component_group_status_helper' => 'Toon de ernstigste componentstatus van elke groep op de openbare statuspagina.', ], 'display_settings_title' => 'Weergave-instellingen', 'api_settings_title' => 'API-instellingen', diff --git a/resources/lang/ph/settings.php b/resources/lang/ph/settings.php index c44178bb..357ed08e 100644 --- a/resources/lang/ph/settings.php +++ b/resources/lang/ph/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => 'Mangailangan ng API token para sa lahat ng API request, kabilang ang mga read-only na endpoint.', 'dynamic_favicon' => 'Dynamic na Favicon', 'dynamic_favicon_helper' => 'I-update ang favicon upang ipakita ang kasalukuyang status ng iyong mga sistema.', + 'show_component_group_status' => 'Ipakita ang status ng pangkat ng component', + 'show_component_group_status_helper' => 'Ipakita ang pinakamalubhang status ng component sa bawat pangkat sa pampublikong pahina ng status.', ], 'display_settings_title' => 'Mga Setting ng Display', 'api_settings_title' => 'Mga Setting ng API', diff --git a/resources/lang/pt_BR/settings.php b/resources/lang/pt_BR/settings.php index 71a4b3b9..52df64ac 100644 --- a/resources/lang/pt_BR/settings.php +++ b/resources/lang/pt_BR/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => 'Exigir um token de API para todas as requisições, incluindo endpoints somente leitura.', 'dynamic_favicon' => 'Favicon Dinâmico', 'dynamic_favicon_helper' => 'Atualiza o favicon para refletir o status atual dos seus sistemas.', + 'show_component_group_status' => 'Exibir status do grupo de componentes', + 'show_component_group_status_helper' => 'Exibe o status mais grave dos componentes de cada grupo na página de status pública.', ], 'display_settings_title' => 'Configurações de Exibição', 'api_settings_title' => 'Configurações da API', diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index 9e2e2d28..27c166ed 100644 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => '所有 API 请求(包括只读接口)都需要 API 令牌。', 'dynamic_favicon' => '动态网站图标', 'dynamic_favicon_helper' => '根据系统当前状态更新网站图标。', + 'show_component_group_status' => '显示组件组状态', + 'show_component_group_status_helper' => '在公开状态页面上显示每个组中最严重的组件状态。', ], 'display_settings_title' => '显示设置', 'api_settings_title' => 'API 设置', diff --git a/resources/lang/zh_TW/settings.php b/resources/lang/zh_TW/settings.php index 34e84375..1f134e1c 100644 --- a/resources/lang/zh_TW/settings.php +++ b/resources/lang/zh_TW/settings.php @@ -43,6 +43,8 @@ 'api_protected_helper' => '所有 API 請求(包括唯讀端點)都需要 API 權杖。', 'dynamic_favicon' => '動態網站圖示', 'dynamic_favicon_helper' => '根據系統目前狀態更新網站圖示。', + 'show_component_group_status' => '顯示元件群組狀態', + 'show_component_group_status_helper' => '在公開狀態頁面上顯示每個群組中最嚴重的元件狀態。', ], 'display_settings_title' => '顯示設定', 'api_settings_title' => 'API 設定', diff --git a/resources/views/components/component-group.blade.php b/resources/views/components/component-group.blade.php index 03e07ce5..ddbc52ee 100644 --- a/resources/views/components/component-group.blade.php +++ b/resources/views/components/component-group.blade.php @@ -1,7 +1,7 @@ @props(['componentGroup' => null]) {{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENT_GROUPS_BEFORE) }} -@php($groupStatus = $componentGroup->worstComponentStatus()) +@php($showComponentGroupStatus = app(\Cachet\Settings\AppSettings::class)->show_component_group_status)
  • isExpanded(auth()->user())) default-open @endif>
    diff --git a/src/Filament/Pages/Settings/ManageCachet.php b/src/Filament/Pages/Settings/ManageCachet.php index 01dac91d..85d5267c 100644 --- a/src/Filament/Pages/Settings/ManageCachet.php +++ b/src/Filament/Pages/Settings/ManageCachet.php @@ -115,6 +115,9 @@ public function form(Schema $schema): Schema Toggle::make('dynamic_favicon') ->label(__('cachet::settings.manage_cachet.toggles.dynamic_favicon')) ->helperText(__('cachet::settings.manage_cachet.toggles.dynamic_favicon_helper')), + Toggle::make('show_component_group_status') + ->label(__('cachet::settings.manage_cachet.toggles.show_component_group_status')) + ->helperText(__('cachet::settings.manage_cachet.toggles.show_component_group_status_helper')), ]), Section::make(__('cachet::settings.manage_cachet.api_settings_title')) diff --git a/src/Models/ComponentGroup.php b/src/Models/ComponentGroup.php index 0c795d7a..938e8b48 100644 --- a/src/Models/ComponentGroup.php +++ b/src/Models/ComponentGroup.php @@ -105,6 +105,19 @@ public function worstComponentStatus(): ComponentStatusEnum ->first() ?? ComponentStatusEnum::operational; } + /** + * Count the distinct unresolved incidents affecting this group. + */ + public function openIncidentCount(): int + { + return $this->components + ->flatMap(fn (Component $component) => $component->relationLoaded('unresolvedIncidents') + ? $component->unresolvedIncidents + : $component->unresolvedIncidents()->get()) + ->unique('id') + ->count(); + } + /** * Determine whether an incident the given viewer can see is affecting the group. * diff --git a/src/Settings/AppSettings.php b/src/Settings/AppSettings.php index cb27ef29..b2a5ba85 100644 --- a/src/Settings/AppSettings.php +++ b/src/Settings/AppSettings.php @@ -52,6 +52,8 @@ class AppSettings extends Settings public bool $dynamic_favicon = false; + public bool $show_component_group_status = true; + public static function group(): string { return 'app'; diff --git a/tests/Feature/Filament/Settings/ManageCachetTest.php b/tests/Feature/Filament/Settings/ManageCachetTest.php index b369191a..c44d4446 100644 --- a/tests/Feature/Filament/Settings/ManageCachetTest.php +++ b/tests/Feature/Filament/Settings/ManageCachetTest.php @@ -31,6 +31,17 @@ expect(app(AppSettings::class)->refresh()->dynamic_favicon)->toBeTrue(); }); +it('saves the component group status visibility setting', function () { + expect(app(AppSettings::class)->show_component_group_status)->toBeTrue(); + + livewire(ManageCachet::class) + ->fillForm(['show_component_group_status' => false]) + ->call('save') + ->assertHasNoFormErrors(); + + expect(app(AppSettings::class)->refresh()->show_component_group_status)->toBeFalse(); +}); + it('saves the status page title and about visibility settings', function () { expect(app(AppSettings::class)) ->show_site_name->toBeFalse() diff --git a/tests/Feature/StatusPage/StatusPageTest.php b/tests/Feature/StatusPage/StatusPageTest.php index bef3e25b..1afb1bfc 100644 --- a/tests/Feature/StatusPage/StatusPageTest.php +++ b/tests/Feature/StatusPage/StatusPageTest.php @@ -1,6 +1,7 @@ toMatch('/]*>\s*Public API\s*<\\/h2>/'); }); +it('can hide component group statuses', function () { + $settings = app(AppSettings::class); + $settings->show_component_group_status = false; + $settings->save(); + + $group = ComponentGroup::factory()->create(['name' => 'Core services']); + $firstComponent = Component::factory()->create([ + 'component_group_id' => $group->id, + 'status' => ComponentStatusEnum::major_outage, + ]); + $secondComponent = Component::factory()->create(['component_group_id' => $group->id]); + $incident = Incident::factory()->create(['status' => IncidentStatusEnum::investigating]); + $incident->components()->attach([$firstComponent->id, $secondComponent->id]); + + $page = $this->get(route('cachet.status-page')) + ->assertOk() + ->getContent(); + + expect($page) + ->toContain('Core services') + ->toContain('1 Incident') + ->not->toMatch('/Core services\s*<\\/h2>\s*]*>\s*Major outage\s*<\\/span>/'); +}); + it('renders the status page in the configured locale', function () { $settings = app(AppSettings::class); $settings->locale = 'de'; diff --git a/tests/Unit/Models/ComponentGroupTest.php b/tests/Unit/Models/ComponentGroupTest.php index 24c75887..42d0fea0 100644 --- a/tests/Unit/Models/ComponentGroupTest.php +++ b/tests/Unit/Models/ComponentGroupTest.php @@ -201,6 +201,18 @@ expect($group->worstComponentStatus())->toBe(ComponentStatusEnum::operational); }); +it('counts each unresolved incident once across a group', function () { + $group = ComponentGroup::factory()->hasComponents(2)->create(); + $incident = Incident::factory()->create(['status' => IncidentStatusEnum::investigating]); + $incident->components()->attach($group->components->pluck('id')); + $resolvedIncident = Incident::factory()->create(['status' => IncidentStatusEnum::fixed]); + $resolvedIncident->components()->attach($group->components->first()); + + $group->load('components.unresolvedIncidents'); + + expect($group->openIncidentCount())->toBe(1); +}); + it('has meta', function () { $group = ComponentGroup::factory()->withMeta()->create();