From 6760ae857f3b6a4f4a7444956e01bf6afe166ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20F=C4=85ferek?= Date: Sun, 15 Feb 2026 22:04:19 +0100 Subject: [PATCH] fix: show healed faults in dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Faults transitioning through PREPASSED → HEALED were invisible in the UI because the store fetched without status=all and the type system had no 'healed' status. - Add 'healed' to FaultStatusValue type - Map 'healed'/'prepassed' API status in transformFault() - Fetch with ?status=all so healed faults are available in store - Add "Healed" checkbox to status filter dropdown (default OFF) - Healed fault rows render at 60% opacity with green status badge - Clear button already hidden for non-active statuses (no change needed) Closes #34 --- src/components/FaultsDashboard.tsx | 49 ++++++++++++------ src/components/FaultsPanel.tsx | 9 +++- src/lib/sovd-api.test.ts | 81 ++++++++++++++++++++++++++++++ src/lib/sovd-api.ts | 9 +++- src/lib/store.ts | 2 +- src/lib/types.ts | 2 +- 6 files changed, 130 insertions(+), 22 deletions(-) diff --git a/src/components/FaultsDashboard.tsx b/src/components/FaultsDashboard.tsx index e73428d..5cab03b 100644 --- a/src/components/FaultsDashboard.tsx +++ b/src/components/FaultsDashboard.tsx @@ -54,6 +54,20 @@ function getSeverityBadgeVariant(severity: FaultSeverity): 'default' | 'secondar } } +/** + * Get badge variant for fault status + */ +function getStatusBadgeVariant(status: FaultStatus): 'default' | 'secondary' | 'outline' { + switch (status) { + case 'active': + return 'default'; + case 'pending': + return 'secondary'; + default: + return 'outline'; + } +} + /** * Get icon for fault severity */ @@ -128,7 +142,9 @@ function FaultRow({
-
+
{/* Expand/Collapse Icon */}
{isExpanded ? ( @@ -151,14 +167,8 @@ function FaultRow({ {fault.severity} {fault.status} @@ -528,16 +538,17 @@ export function FaultsDashboard() { }); }, [filteredFaults]); - // Count by severity + // Count by severity (active faults only — CONFIRMED + PREFAILED) + const activeFaults = useMemo(() => faults.filter((f) => f.status === 'active' || f.status === 'pending'), [faults]); const counts = useMemo(() => { return { - critical: faults.filter((f) => f.severity === 'critical').length, - error: faults.filter((f) => f.severity === 'error').length, - warning: faults.filter((f) => f.severity === 'warning').length, - info: faults.filter((f) => f.severity === 'info').length, - total: faults.length, + critical: activeFaults.filter((f) => f.severity === 'critical').length, + error: activeFaults.filter((f) => f.severity === 'error').length, + warning: activeFaults.filter((f) => f.severity === 'warning').length, + info: activeFaults.filter((f) => f.severity === 'info').length, + total: activeFaults.length, }; - }, [faults]); + }, [activeFaults]); // Toggle severity filter const toggleSeverity = (severity: FaultSeverity) => { @@ -745,6 +756,12 @@ export function FaultsDashboard() { > Cleared + toggleStatus('healed')} + > + Healed + diff --git a/src/components/FaultsPanel.tsx b/src/components/FaultsPanel.tsx index 83995a5..ae6196d 100644 --- a/src/components/FaultsPanel.tsx +++ b/src/components/FaultsPanel.tsx @@ -116,7 +116,9 @@ function FaultRow({
-
+
{/* Expand/Collapse Icon */}
{isExpanded ? ( @@ -146,7 +148,10 @@ function FaultRow({ {fault.severity} - + {fault.status}
diff --git a/src/lib/sovd-api.test.ts b/src/lib/sovd-api.test.ts index a6df2b9..f97e1d7 100644 --- a/src/lib/sovd-api.test.ts +++ b/src/lib/sovd-api.test.ts @@ -59,6 +59,87 @@ describe('SovdApiClient', () => { }); }); + describe('listAllFaults', () => { + const makeFaultItem = (overrides: Record = {}) => ({ + fault_code: 'TEST_FAULT', + description: 'A test fault', + severity: 2, + severity_label: 'error', + status: 'CONFIRMED', + first_occurred: 1700000000, + reporting_sources: ['/test/node'], + ...overrides, + }); + + it('passes status query parameter when provided', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [] }), + } as Response); + + await client.listAllFaults('all'); + + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:8080/api/v1/faults?status=all', + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('omits status parameter when not provided', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [] }), + } as Response); + + await client.listAllFaults(); + + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:8080/api/v1/faults', + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('maps HEALED API status to healed', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [makeFaultItem({ status: 'HEALED' })] }), + } as Response); + + const result = await client.listAllFaults('all'); + expect(result.items[0]?.status).toBe('healed'); + }); + + it('maps PREPASSED API status to healed', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [makeFaultItem({ status: 'PREPASSED' })] }), + } as Response); + + const result = await client.listAllFaults('all'); + expect(result.items[0]?.status).toBe('healed'); + }); + + it('maps CONFIRMED API status to active', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [makeFaultItem({ status: 'CONFIRMED' })] }), + } as Response); + + const result = await client.listAllFaults(); + expect(result.items[0]?.status).toBe('active'); + }); + + it('maps CLEARED API status to cleared', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ items: [makeFaultItem({ status: 'CLEARED' })] }), + } as Response); + + const result = await client.listAllFaults(); + expect(result.items[0]?.status).toBe('cleared'); + }); + }); + describe('listBulkDataCategories', () => { it('returns categories array', async () => { vi.mocked(fetch).mockResolvedValue({ diff --git a/src/lib/sovd-api.ts b/src/lib/sovd-api.ts index 9bf1d2e..2248726 100644 --- a/src/lib/sovd-api.ts +++ b/src/lib/sovd-api.ts @@ -1561,6 +1561,8 @@ export class SovdApiClient { status = 'pending'; } else if (apiStatus === 'cleared' || apiStatus === 'resolved') { status = 'cleared'; + } else if (apiStatus === 'healed' || apiStatus === 'prepassed') { + status = 'healed'; } // Extract entity info from reporting_sources @@ -1591,9 +1593,12 @@ export class SovdApiClient { /** * List all faults across the system + * @param status Optional status filter (e.g. 'all' to include healed faults) */ - async listAllFaults(): Promise { - const response = await fetchWithTimeout(this.getUrl('faults'), { + async listAllFaults(status?: FaultStatus | 'all'): Promise { + const baseUrl = this.getUrl('faults'); + const url = status ? `${baseUrl}?status=${encodeURIComponent(status)}` : baseUrl; + const response = await fetchWithTimeout(url, { method: 'GET', headers: { Accept: 'application/json' }, }); diff --git a/src/lib/store.ts b/src/lib/store.ts index ad257d4..0ed573f 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -1367,7 +1367,7 @@ export const useAppStore = create()( } try { - const result = await client.listAllFaults(); + const result = await client.listAllFaults('all'); // Skip state update if faults haven't changed to avoid unnecessary re-renders. // Compare by serializing fault codes + statuses (cheap and covers all meaningful changes). const newKey = result.items.map((f) => `${f.code}:${f.status}:${f.severity}`).join('|'); diff --git a/src/lib/types.ts b/src/lib/types.ts index 9bca2b9..0e9f864 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -556,7 +556,7 @@ export type FaultSeverity = 'info' | 'warning' | 'error' | 'critical'; /** * Fault status values (legacy) */ -export type FaultStatusValue = 'active' | 'pending' | 'cleared'; +export type FaultStatusValue = 'active' | 'pending' | 'cleared' | 'healed'; /** * Alias for backwards compatibility