From e6197d63ffc17872d0a593ef6b8972b5a7c451c8 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 4 Jul 2026 21:14:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20dashboard=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20real=20health=20status=20contract,=20dedupe=20versi?= =?UTF-8?q?on=20fetch,=20refresh=20guard=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Match the backend's exact health status contract ('pass'|'fail'|'warn'|'info') instead of fictional strings; 'info' now renders as a neutral/idle presentation and is excluded from failing/warning counts in summarizeHealth. - Drop the redundant /api/version fetch from getDashboardData/DashboardData; SidebarVersion's useDashboardVersionQuery remains the single source. - Disable "Refresh health" (via Button's loading prop) while a health request is in flight to prevent concurrent double-click requests. - Add the active-flag unmount guard to useDashboardHealthQuery for consistency with useDashboardQuery/useChannelsQuery. - Remove dead .ctv-activity-*/.ctv-release-* CSS left over from the removed activity feed/release notes UI. Co-Authored-By: Claude Fable 5 --- web/src/App.test.tsx | 38 ++++++++++++++++++++-- web/src/App.tsx | 19 +++++++++-- web/src/api/dashboard.ts | 32 +++++++++++++----- web/src/shell.css | 70 ++-------------------------------------- 4 files changed, 77 insertions(+), 82 deletions(-) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 70c8c0f5c..d0b4a55ef 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -207,13 +207,13 @@ describe('ChicoryTV SPA scaffold', () => { { detail: 'SQLite is reachable', link: null, - status: 'Healthy', + status: 'pass', title: 'Database' }, { detail: 'FFmpeg path is missing', link: null, - status: 'Warning', + status: 'warn', title: 'FFmpeg' } ], @@ -290,13 +290,45 @@ describe('ChicoryTV SPA scaffold', () => { expect(await screen.findByText('No on-air channels reported')).toBeInTheDocument(); }); + it('shows failing health checks with error styling, distinct from neutral info checks', async () => { + mockDashboardApi({ + health: [ + { + detail: 'SQLite is reachable', + link: null, + status: 'pass', + title: 'Database' + }, + { + detail: 'FFmpeg path is missing', + link: null, + status: 'fail', + title: 'FFmpeg' + }, + { + detail: 'Scheduled maintenance window active', + link: null, + status: 'info', + title: 'Maintenance' + } + ] + }); + + const { container } = render(); + + expect(await screen.findByText('FFmpeg path is missing')).toBeInTheDocument(); + expect(screen.getAllByText('1 failing')).toHaveLength(2); + expect(container.querySelectorAll('.ctv-health-icon-error')).toHaveLength(1); + expect(container.querySelectorAll('.ctv-health-icon-idle').length).toBeGreaterThanOrEqual(1); + }); + it('refreshes health on demand without polling it', async () => { mockDashboardApi({ health: [ { detail: 'All checks passed', link: null, - status: 'Healthy', + status: 'pass', title: 'System' } ] diff --git a/web/src/App.tsx b/web/src/App.tsx index dc042174f..83c503882 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -495,14 +495,18 @@ function summarizeHealth(healthState: DashboardHealthQueryState): { label: strin return { label: 'Healthy', status: 'ok' }; } +// The backend serializes health check status as exactly 'pass' | 'fail' | 'warn' | 'info' +// (see ErsatzTV.Application/Health/Mapper.cs GetStatus). function isWarningHealthStatus(status: string): boolean { - return status.toLowerCase() === 'warning' || status.toLowerCase() === 'degraded'; + return status.toLowerCase() === 'warn'; } function isErrorHealthStatus(status: string): boolean { - const normalized = status.toLowerCase(); + return status.toLowerCase() === 'fail'; +} - return normalized === 'error' || normalized === 'failed' || normalized === 'unhealthy'; +function isInfoHealthStatus(status: string): boolean { + return status.toLowerCase() === 'info'; } function healthIconStatus(status: string): HealthStatus { @@ -514,6 +518,10 @@ function healthIconStatus(status: string): HealthStatus { return 'warn'; } + if (isInfoHealthStatus(status)) { + return 'idle'; + } + return 'ok'; } @@ -525,6 +533,10 @@ function healthIcon(status: string): ReactNode { } if (iconStatus === 'warn') { + return