Skip to content

Commit eefd9ee

Browse files
Michał Fąferekmfaferek93
authored andcommitted
fix: show healed faults in dashboard
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
1 parent 23c0c49 commit eefd9ee

6 files changed

Lines changed: 130 additions & 22 deletions

File tree

src/components/FaultsDashboard.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ function getSeverityBadgeVariant(severity: FaultSeverity): 'default' | 'secondar
5454
}
5555
}
5656

57+
/**
58+
* Get badge variant for fault status
59+
*/
60+
function getStatusBadgeVariant(status: FaultStatus): 'default' | 'secondary' | 'outline' {
61+
switch (status) {
62+
case 'active':
63+
return 'default';
64+
case 'pending':
65+
return 'secondary';
66+
default:
67+
return 'outline';
68+
}
69+
}
70+
5771
/**
5872
* Get icon for fault severity
5973
*/
@@ -128,7 +142,9 @@ function FaultRow({
128142
<Collapsible open={isExpanded} onOpenChange={onToggle}>
129143
<div className="rounded-lg border bg-card">
130144
<CollapsibleTrigger asChild>
131-
<div className="flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/50 transition-colors">
145+
<div
146+
className={`flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/50 transition-colors ${fault.status === 'healed' ? 'opacity-60' : ''}`}
147+
>
132148
{/* Expand/Collapse Icon */}
133149
<div className="shrink-0 mt-0.5">
134150
{isExpanded ? (
@@ -151,14 +167,8 @@ function FaultRow({
151167
{fault.severity}
152168
</Badge>
153169
<Badge
154-
variant={
155-
fault.status === 'active'
156-
? 'default'
157-
: fault.status === 'pending'
158-
? 'secondary'
159-
: 'outline'
160-
}
161-
className="text-xs"
170+
variant={getStatusBadgeVariant(fault.status)}
171+
className={`text-xs ${fault.status === 'healed' ? 'text-green-600 border-green-300 dark:text-green-400 dark:border-green-700' : ''}`}
162172
>
163173
{fault.status}
164174
</Badge>
@@ -528,16 +538,17 @@ export function FaultsDashboard() {
528538
});
529539
}, [filteredFaults]);
530540

531-
// Count by severity
541+
// Count by severity (active faults only — CONFIRMED + PREFAILED)
542+
const activeFaults = useMemo(() => faults.filter((f) => f.status === 'active' || f.status === 'pending'), [faults]);
532543
const counts = useMemo(() => {
533544
return {
534-
critical: faults.filter((f) => f.severity === 'critical').length,
535-
error: faults.filter((f) => f.severity === 'error').length,
536-
warning: faults.filter((f) => f.severity === 'warning').length,
537-
info: faults.filter((f) => f.severity === 'info').length,
538-
total: faults.length,
545+
critical: activeFaults.filter((f) => f.severity === 'critical').length,
546+
error: activeFaults.filter((f) => f.severity === 'error').length,
547+
warning: activeFaults.filter((f) => f.severity === 'warning').length,
548+
info: activeFaults.filter((f) => f.severity === 'info').length,
549+
total: activeFaults.length,
539550
};
540-
}, [faults]);
551+
}, [activeFaults]);
541552

542553
// Toggle severity filter
543554
const toggleSeverity = (severity: FaultSeverity) => {
@@ -745,6 +756,12 @@ export function FaultsDashboard() {
745756
>
746757
Cleared
747758
</DropdownMenuCheckboxItem>
759+
<DropdownMenuCheckboxItem
760+
checked={statusFilters.has('healed')}
761+
onCheckedChange={() => toggleStatus('healed')}
762+
>
763+
Healed
764+
</DropdownMenuCheckboxItem>
748765
</DropdownMenuContent>
749766
</DropdownMenu>
750767

src/components/FaultsPanel.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ function FaultRow({
116116
<Collapsible open={isExpanded} onOpenChange={onToggle}>
117117
<div className="rounded-lg border bg-card">
118118
<CollapsibleTrigger asChild>
119-
<div className="flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/50 transition-colors">
119+
<div
120+
className={`flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/50 transition-colors ${fault.status === 'healed' ? 'opacity-60' : ''}`}
121+
>
120122
{/* Expand/Collapse Icon */}
121123
<div className="shrink-0 mt-0.5">
122124
{isExpanded ? (
@@ -146,7 +148,10 @@ function FaultRow({
146148
<Badge variant={getSeverityBadgeVariant(fault.severity)} className="text-xs">
147149
{fault.severity}
148150
</Badge>
149-
<Badge variant={getStatusBadgeVariant(fault.status)} className="text-xs">
151+
<Badge
152+
variant={getStatusBadgeVariant(fault.status)}
153+
className={`text-xs ${fault.status === 'healed' ? 'text-green-600 border-green-300 dark:text-green-400 dark:border-green-700' : ''}`}
154+
>
150155
{fault.status}
151156
</Badge>
152157
</div>

src/lib/sovd-api.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,87 @@ describe('SovdApiClient', () => {
5959
});
6060
});
6161

62+
describe('listAllFaults', () => {
63+
const makeFaultItem = (overrides: Record<string, unknown> = {}) => ({
64+
fault_code: 'TEST_FAULT',
65+
description: 'A test fault',
66+
severity: 2,
67+
severity_label: 'error',
68+
status: 'CONFIRMED',
69+
first_occurred: 1700000000,
70+
reporting_sources: ['/test/node'],
71+
...overrides,
72+
});
73+
74+
it('passes status query parameter when provided', async () => {
75+
vi.mocked(fetch).mockResolvedValue({
76+
ok: true,
77+
json: () => Promise.resolve({ items: [] }),
78+
} as Response);
79+
80+
await client.listAllFaults('all');
81+
82+
expect(fetch).toHaveBeenCalledWith(
83+
'http://localhost:8080/api/v1/faults?status=all',
84+
expect.objectContaining({ method: 'GET' })
85+
);
86+
});
87+
88+
it('omits status parameter when not provided', async () => {
89+
vi.mocked(fetch).mockResolvedValue({
90+
ok: true,
91+
json: () => Promise.resolve({ items: [] }),
92+
} as Response);
93+
94+
await client.listAllFaults();
95+
96+
expect(fetch).toHaveBeenCalledWith(
97+
'http://localhost:8080/api/v1/faults',
98+
expect.objectContaining({ method: 'GET' })
99+
);
100+
});
101+
102+
it('maps HEALED API status to healed', async () => {
103+
vi.mocked(fetch).mockResolvedValue({
104+
ok: true,
105+
json: () => Promise.resolve({ items: [makeFaultItem({ status: 'HEALED' })] }),
106+
} as Response);
107+
108+
const result = await client.listAllFaults('all');
109+
expect(result.items[0]?.status).toBe('healed');
110+
});
111+
112+
it('maps PREPASSED API status to healed', async () => {
113+
vi.mocked(fetch).mockResolvedValue({
114+
ok: true,
115+
json: () => Promise.resolve({ items: [makeFaultItem({ status: 'PREPASSED' })] }),
116+
} as Response);
117+
118+
const result = await client.listAllFaults('all');
119+
expect(result.items[0]?.status).toBe('healed');
120+
});
121+
122+
it('maps CONFIRMED API status to active', async () => {
123+
vi.mocked(fetch).mockResolvedValue({
124+
ok: true,
125+
json: () => Promise.resolve({ items: [makeFaultItem({ status: 'CONFIRMED' })] }),
126+
} as Response);
127+
128+
const result = await client.listAllFaults();
129+
expect(result.items[0]?.status).toBe('active');
130+
});
131+
132+
it('maps CLEARED API status to cleared', async () => {
133+
vi.mocked(fetch).mockResolvedValue({
134+
ok: true,
135+
json: () => Promise.resolve({ items: [makeFaultItem({ status: 'CLEARED' })] }),
136+
} as Response);
137+
138+
const result = await client.listAllFaults();
139+
expect(result.items[0]?.status).toBe('cleared');
140+
});
141+
});
142+
62143
describe('listBulkDataCategories', () => {
63144
it('returns categories array', async () => {
64145
vi.mocked(fetch).mockResolvedValue({

src/lib/sovd-api.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,8 @@ export class SovdApiClient {
15611561
status = 'pending';
15621562
} else if (apiStatus === 'cleared' || apiStatus === 'resolved') {
15631563
status = 'cleared';
1564+
} else if (apiStatus === 'healed' || apiStatus === 'prepassed') {
1565+
status = 'healed';
15641566
}
15651567

15661568
// Extract entity info from reporting_sources
@@ -1591,9 +1593,12 @@ export class SovdApiClient {
15911593

15921594
/**
15931595
* List all faults across the system
1596+
* @param status Optional status filter (e.g. 'all' to include healed faults)
15941597
*/
1595-
async listAllFaults(): Promise<ListFaultsResponse> {
1596-
const response = await fetchWithTimeout(this.getUrl('faults'), {
1598+
async listAllFaults(status?: FaultStatus | 'all'): Promise<ListFaultsResponse> {
1599+
const baseUrl = this.getUrl('faults');
1600+
const url = status ? `${baseUrl}?status=${encodeURIComponent(status)}` : baseUrl;
1601+
const response = await fetchWithTimeout(url, {
15971602
method: 'GET',
15981603
headers: { Accept: 'application/json' },
15991604
});

src/lib/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,7 @@ export const useAppStore = create<AppState>()(
13671367
}
13681368

13691369
try {
1370-
const result = await client.listAllFaults();
1370+
const result = await client.listAllFaults('all');
13711371
// Skip state update if faults haven't changed to avoid unnecessary re-renders.
13721372
// Compare by serializing fault codes + statuses (cheap and covers all meaningful changes).
13731373
const newKey = result.items.map((f) => `${f.code}:${f.status}:${f.severity}`).join('|');

src/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ export type FaultSeverity = 'info' | 'warning' | 'error' | 'critical';
556556
/**
557557
* Fault status values (legacy)
558558
*/
559-
export type FaultStatusValue = 'active' | 'pending' | 'cleared';
559+
export type FaultStatusValue = 'active' | 'pending' | 'cleared' | 'healed';
560560

561561
/**
562562
* Alias for backwards compatibility

0 commit comments

Comments
 (0)