@checkstack/healthcheck-frontend 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState, useCallback } from "react";
1
+ import React, { useState } from "react";
2
2
  import { useApi, type SlotContext } from "@checkstack/frontend-api";
3
3
  import { useSignal } from "@checkstack/signal-frontend";
4
4
  import { healthCheckApiRef } from "../api";
@@ -23,28 +23,25 @@ import { ChevronDown, ChevronRight } from "lucide-react";
23
23
  import { HealthCheckSparkline } from "./HealthCheckSparkline";
24
24
  import { HealthCheckLatencyChart } from "./HealthCheckLatencyChart";
25
25
  import { HealthCheckStatusTimeline } from "./HealthCheckStatusTimeline";
26
+ import { HealthCheckDiagram } from "./HealthCheckDiagram";
27
+ import { useHealthCheckData } from "../hooks/useHealthCheckData";
26
28
 
27
29
  import type {
28
30
  StateThresholds,
29
31
  HealthCheckStatus,
30
32
  } from "@checkstack/healthcheck-common";
31
- import { HealthCheckDiagram } from "./HealthCheckDiagram";
32
33
 
33
34
  type SlotProps = SlotContext<typeof SystemDetailsSlot>;
34
35
 
35
36
  interface HealthCheckOverviewItem {
36
37
  configurationId: string;
37
- configurationName: string;
38
38
  strategyId: string;
39
+ name: string;
40
+ state: HealthCheckStatus;
39
41
  intervalSeconds: number;
40
- enabled: boolean;
41
- status: HealthCheckStatus;
42
+ lastRunAt?: Date;
42
43
  stateThresholds?: StateThresholds;
43
- recentRuns: Array<{
44
- id: string;
45
- status: HealthCheckStatus;
46
- timestamp: Date;
47
- }>;
44
+ recentStatusHistory: HealthCheckStatus[];
48
45
  }
49
46
 
50
47
  interface ExpandedRowProps {
@@ -67,47 +64,20 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
67
64
  return { startDate: start, endDate: end };
68
65
  });
69
66
 
70
- // Chart data - fetches all runs for the time range (unpaginated)
71
- const [chartData, setChartData] = useState<
72
- Array<{
73
- id: string;
74
- status: HealthCheckStatus;
75
- timestamp: Date;
76
- latencyMs?: number;
77
- }>
78
- >([]);
79
- const [chartLoading, setChartLoading] = useState(true);
80
-
81
- const fetchChartData = useCallback(() => {
82
- setChartLoading(true);
83
- api
84
- .getHistory({
85
- systemId,
86
- configurationId: item.configurationId,
87
- startDate: dateRange.startDate,
88
- endDate: dateRange.endDate,
89
- // Fetch up to 1000 data points for charts - enough for most time ranges
90
- limit: 1000,
91
- offset: 0,
92
- })
93
- .then((response) => {
94
- setChartData(response.runs);
95
- })
96
- .finally(() => {
97
- setChartLoading(false);
98
- });
99
- }, [
100
- api,
67
+ // Use shared hook for chart data - handles both raw and aggregated modes
68
+ // and includes signal handling for automatic refresh
69
+ const {
70
+ context: chartContext,
71
+ loading: chartLoading,
72
+ isAggregated,
73
+ retentionConfig,
74
+ } = useHealthCheckData({
101
75
  systemId,
102
- item.configurationId,
103
- dateRange.startDate,
104
- dateRange.endDate,
105
- ]);
106
-
107
- // Fetch chart data when date range changes
108
- useEffect(() => {
109
- fetchChartData();
110
- }, [fetchChartData]);
76
+ configurationId: item.configurationId,
77
+ strategyId: item.strategyId,
78
+ dateRange,
79
+ limit: 1000,
80
+ });
111
81
 
112
82
  // Paginated history for the table
113
83
  const {
@@ -121,7 +91,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
121
91
  systemId: string;
122
92
  configurationId: string;
123
93
  startDate?: Date;
124
- endDate?: Date;
125
94
  }) =>
126
95
  api.getHistory({
127
96
  systemId: params.systemId,
@@ -129,7 +98,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
129
98
  limit: params.limit,
130
99
  offset: params.offset,
131
100
  startDate: params.startDate,
132
- endDate: params.endDate,
101
+ // Don't pass endDate - backend defaults to 'now' so new runs are included
133
102
  }),
134
103
  getItems: (response) => response.runs,
135
104
  getTotal: (response) => response.total,
@@ -137,16 +106,15 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
137
106
  systemId,
138
107
  configurationId: item.configurationId,
139
108
  startDate: dateRange.startDate,
140
- endDate: dateRange.endDate,
141
109
  },
142
110
  defaultLimit: 10,
143
111
  });
144
112
 
145
- // Listen for realtime health check updates to refresh charts and history
113
+ // Listen for realtime health check updates to refresh history table
114
+ // Charts are refreshed automatically by useHealthCheckData
146
115
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
147
116
  if (changedId === systemId) {
148
- fetchChartData();
149
- pagination.refetch();
117
+ pagination.silentRefetch();
150
118
  }
151
119
  });
152
120
 
@@ -156,6 +124,54 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
156
124
  : `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
157
125
  : "Using default thresholds";
158
126
 
127
+ // Render charts - charts handle data transformation internally
128
+ const renderCharts = () => {
129
+ if (chartLoading) {
130
+ return <LoadingSpinner />;
131
+ }
132
+
133
+ if (!chartContext) {
134
+ return;
135
+ }
136
+
137
+ // Check if we have data to show
138
+ const hasData =
139
+ chartContext.type === "raw"
140
+ ? chartContext.runs.length > 0
141
+ : chartContext.buckets.length > 0;
142
+
143
+ if (!hasData) {
144
+ return;
145
+ }
146
+
147
+ return (
148
+ <div className="space-y-4">
149
+ {/* Status Timeline */}
150
+ <div>
151
+ <h4 className="text-sm font-medium mb-2">Status Timeline</h4>
152
+ <HealthCheckStatusTimeline context={chartContext} height={50} />
153
+ </div>
154
+
155
+ {/* Latency Chart */}
156
+ <div>
157
+ <h4 className="text-sm font-medium mb-2">Response Latency</h4>
158
+ <HealthCheckLatencyChart
159
+ context={chartContext}
160
+ height={150}
161
+ showAverage
162
+ />
163
+ </div>
164
+
165
+ {/* Extension Slot for custom strategy-specific diagrams */}
166
+ <HealthCheckDiagram
167
+ context={chartContext}
168
+ isAggregated={isAggregated}
169
+ rawRetentionDays={retentionConfig.rawRetentionDays}
170
+ />
171
+ </div>
172
+ );
173
+ };
174
+
159
175
  return (
160
176
  <div className="p-4 bg-muted/30 border-t space-y-4">
161
177
  <div className="flex flex-wrap gap-4 text-sm">
@@ -179,54 +195,8 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
179
195
  <DateRangeFilter value={dateRange} onChange={setDateRange} />
180
196
  </div>
181
197
 
182
- {/* Charts Section - uses full date range data, not paginated */}
183
- {chartLoading ? (
184
- <LoadingSpinner />
185
- ) : chartData.length > 0 ? (
186
- <div className="space-y-4">
187
- {/* Status Timeline */}
188
- <div>
189
- <h4 className="text-sm font-medium mb-2">Status Timeline</h4>
190
- <HealthCheckStatusTimeline
191
- type="raw"
192
- data={chartData.map((r) => ({
193
- timestamp: new Date(r.timestamp),
194
- status: r.status,
195
- }))}
196
- height={50}
197
- />
198
- </div>
199
-
200
- {/* Latency Chart - only if any run has latency data */}
201
- {chartData.some((r) => r.latencyMs !== undefined) && (
202
- <div>
203
- <h4 className="text-sm font-medium mb-2">Response Latency</h4>
204
- <HealthCheckLatencyChart
205
- type="raw"
206
- data={chartData
207
- .filter((r) => r.latencyMs !== undefined)
208
- .map((r) => ({
209
- timestamp: new Date(r.timestamp),
210
- latencyMs: r.latencyMs!,
211
- status: r.status,
212
- }))}
213
- height={150}
214
- showAverage
215
- />
216
- </div>
217
- )}
218
-
219
- {/* Extension Slot for custom strategy-specific diagrams */}
220
- <HealthCheckDiagram
221
- systemId={systemId}
222
- configurationId={item.configurationId}
223
- strategyId={item.strategyId}
224
- dateRange={dateRange}
225
- limit={1000}
226
- offset={0}
227
- />
228
- </div>
229
- ) : undefined}
198
+ {/* Charts Section */}
199
+ {renderCharts()}
230
200
 
231
201
  {loading ? (
232
202
  <LoadingSpinner />
@@ -246,7 +216,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
246
216
  <TableCell>
247
217
  <HealthBadge status={run.status} />
248
218
  </TableCell>
249
- <TableCell className="text-sm text-muted-foreground">
219
+ <TableCell className="text-muted-foreground">
250
220
  {formatDistanceToNow(new Date(run.timestamp), {
251
221
  addSuffix: true,
252
222
  })}
@@ -256,125 +226,178 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
256
226
  </TableBody>
257
227
  </Table>
258
228
  </div>
259
- {pagination.totalPages > 1 && (
260
- <Pagination
261
- page={pagination.page}
262
- totalPages={pagination.totalPages}
263
- onPageChange={pagination.setPage}
264
- total={pagination.total}
265
- limit={pagination.limit}
266
- onPageSizeChange={pagination.setLimit}
267
- showTotal
268
- />
269
- )}
229
+ <Pagination
230
+ page={pagination.page}
231
+ totalPages={pagination.totalPages}
232
+ onPageChange={pagination.setPage}
233
+ total={pagination.total}
234
+ limit={pagination.limit}
235
+ onPageSizeChange={pagination.setLimit}
236
+ showPageSize
237
+ showTotal
238
+ />
270
239
  </>
271
240
  ) : (
272
- <p className="text-sm text-muted-foreground italic">No runs yet</p>
241
+ <div className="text-center text-muted-foreground py-4">
242
+ No runs recorded yet
243
+ </div>
273
244
  )}
274
245
  </div>
275
246
  );
276
247
  };
277
248
 
278
- export const HealthCheckSystemOverview: React.FC<SlotProps> = (props) => {
279
- const { system } = props;
280
- const systemId = system?.id;
281
-
249
+ export function HealthCheckSystemOverview(props: SlotProps) {
250
+ const systemId = props.system.id;
282
251
  const api = useApi(healthCheckApiRef);
283
- const [overview, setOverview] = useState<HealthCheckOverviewItem[]>([]);
284
- const [loading, setLoading] = useState(true);
285
- const [expandedId, setExpandedId] = useState<string>();
286
252
 
287
- const refetch = useCallback(() => {
288
- if (!systemId) return;
289
-
290
- api
291
- .getSystemHealthOverview({ systemId })
292
- .then((data) => setOverview(data.checks))
293
- .finally(() => setLoading(false));
253
+ // Fetch health check overview
254
+ const [overview, setOverview] = React.useState<HealthCheckOverviewItem[]>([]);
255
+ const [initialLoading, setInitialLoading] = React.useState(true);
256
+ const [expandedRow, setExpandedRow] = React.useState<string | undefined>();
257
+
258
+ const fetchOverview = React.useCallback(() => {
259
+ api.getSystemHealthOverview({ systemId }).then((data) => {
260
+ setOverview(
261
+ data.checks.map((check) => ({
262
+ configurationId: check.configurationId,
263
+ strategyId: check.strategyId,
264
+ name: check.configurationName,
265
+ state: check.status,
266
+ intervalSeconds: check.intervalSeconds,
267
+ lastRunAt: check.recentRuns[0]?.timestamp
268
+ ? new Date(check.recentRuns[0].timestamp)
269
+ : undefined,
270
+ stateThresholds: check.stateThresholds,
271
+ recentStatusHistory: check.recentRuns.map((r) => r.status),
272
+ }))
273
+ );
274
+ setInitialLoading(false);
275
+ });
294
276
  }, [api, systemId]);
295
277
 
296
- // Initial fetch
297
- useEffect(() => {
298
- refetch();
299
- }, [refetch]);
278
+ React.useEffect(() => {
279
+ fetchOverview();
280
+ }, [fetchOverview]);
300
281
 
301
- // Listen for realtime health check updates
282
+ // Listen for realtime health check updates - merge into existing state to avoid remounting expanded content
302
283
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
303
284
  if (changedId === systemId) {
304
- refetch();
285
+ // Fetch fresh data but merge it into existing state to preserve object identity
286
+ // for unchanged items, preventing unnecessary re-renders of expanded content
287
+ api.getSystemHealthOverview({ systemId }).then((data) => {
288
+ setOverview((prev) => {
289
+ // Create a map of new items for quick lookup
290
+ const newItemsMap = new Map(
291
+ data.checks.map((item) => [item.configurationId, item])
292
+ );
293
+
294
+ // Update existing items in place, add new ones
295
+ const merged = prev.map((existing) => {
296
+ const updated = newItemsMap.get(existing.configurationId);
297
+ if (updated) {
298
+ newItemsMap.delete(existing.configurationId);
299
+ // Map API response to our internal format
300
+ const mappedItem: HealthCheckOverviewItem = {
301
+ configurationId: updated.configurationId,
302
+ strategyId: updated.strategyId,
303
+ name: updated.configurationName,
304
+ state: updated.status,
305
+ intervalSeconds: updated.intervalSeconds,
306
+ lastRunAt: updated.recentRuns[0]?.timestamp
307
+ ? new Date(updated.recentRuns[0].timestamp)
308
+ : undefined,
309
+ stateThresholds: updated.stateThresholds,
310
+ recentStatusHistory: updated.recentRuns.map((r) => r.status),
311
+ };
312
+ // Return updated data but preserve reference if nothing changed
313
+ return JSON.stringify(existing) === JSON.stringify(mappedItem)
314
+ ? existing
315
+ : mappedItem;
316
+ }
317
+ return existing;
318
+ });
319
+
320
+ // Add any new items that weren't in the previous list
321
+ for (const newItem of newItemsMap.values()) {
322
+ merged.push({
323
+ configurationId: newItem.configurationId,
324
+ strategyId: newItem.strategyId,
325
+ name: newItem.configurationName,
326
+ state: newItem.status,
327
+ intervalSeconds: newItem.intervalSeconds,
328
+ lastRunAt: newItem.recentRuns[0]?.timestamp
329
+ ? new Date(newItem.recentRuns[0].timestamp)
330
+ : undefined,
331
+ stateThresholds: newItem.stateThresholds,
332
+ recentStatusHistory: newItem.recentRuns.map((r) => r.status),
333
+ });
334
+ }
335
+
336
+ // Remove items that no longer exist
337
+ return merged.filter((item) =>
338
+ data.checks.some((c) => c.configurationId === item.configurationId)
339
+ );
340
+ });
341
+ });
305
342
  }
306
343
  });
307
344
 
308
- if (loading) return <LoadingSpinner />;
345
+ if (initialLoading) {
346
+ return <LoadingSpinner />;
347
+ }
309
348
 
310
349
  if (overview.length === 0) {
311
350
  return (
312
- <p className="text-muted-foreground text-sm">
313
- No health checks assigned to this system.
314
- </p>
351
+ <div className="text-center text-muted-foreground py-4">
352
+ No health checks configured
353
+ </div>
315
354
  );
316
355
  }
317
356
 
318
357
  return (
319
358
  <div className="space-y-2">
320
359
  {overview.map((item) => {
321
- const isExpanded = expandedId === item.configurationId;
322
- const lastRun = item.recentRuns[0];
360
+ const isExpanded = expandedRow === item.configurationId;
323
361
 
324
362
  return (
325
- <div
326
- key={item.configurationId}
327
- className="rounded-lg border bg-card transition-shadow hover:shadow-sm"
328
- >
329
- <div
330
- className="flex items-center gap-4 p-3 cursor-pointer"
363
+ <div key={item.configurationId} className="rounded-md border bg-card">
364
+ <button
365
+ className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
331
366
  onClick={() =>
332
- setExpandedId(isExpanded ? undefined : item.configurationId)
367
+ setExpandedRow(isExpanded ? undefined : item.configurationId)
333
368
  }
334
369
  >
335
- <div className="text-muted-foreground">
370
+ <div className="flex items-center gap-3">
336
371
  {isExpanded ? (
337
- <ChevronDown className="h-4 w-4" />
372
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
338
373
  ) : (
339
- <ChevronRight className="h-4 w-4" />
374
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
340
375
  )}
341
- </div>
342
-
343
- <div className="flex-1 min-w-0">
344
- <div className="flex items-center gap-2">
345
- <span className="font-medium text-sm truncate">
346
- {item.configurationName}
347
- </span>
348
- {!item.enabled && (
349
- <span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
350
- Disabled
351
- </span>
352
- )}
353
- </div>
354
- {lastRun && (
355
- <span className="text-xs text-muted-foreground">
376
+ <div>
377
+ <div className="font-medium">{item.name}</div>
378
+ <div className="text-sm text-muted-foreground">
356
379
  Last run:{" "}
357
- {formatDistanceToNow(new Date(lastRun.timestamp), {
358
- addSuffix: true,
359
- })}
360
- </span>
380
+ {item.lastRunAt
381
+ ? formatDistanceToNow(item.lastRunAt, { addSuffix: true })
382
+ : "never"}
383
+ </div>
384
+ </div>
385
+ </div>
386
+ <div className="flex items-center gap-4">
387
+ {item.recentStatusHistory.length > 0 && (
388
+ <HealthCheckSparkline
389
+ runs={item.recentStatusHistory.map((status) => ({
390
+ status,
391
+ }))}
392
+ />
361
393
  )}
394
+ <HealthBadge status={item.state} />
362
395
  </div>
363
-
364
- <HealthCheckSparkline
365
- runs={item.recentRuns}
366
- className="hidden sm:flex"
367
- />
368
-
369
- <HealthBadge status={item.status} />
370
- </div>
371
-
372
- {isExpanded && systemId && (
373
- <ExpandedDetails item={item} systemId={systemId} />
374
- )}
396
+ </button>
397
+ {isExpanded && <ExpandedDetails item={item} systemId={systemId} />}
375
398
  </div>
376
399
  );
377
400
  })}
378
401
  </div>
379
402
  );
380
- };
403
+ }
@@ -0,0 +1,63 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { useApi } from "@checkstack/frontend-api";
3
+ import { healthCheckApiRef } from "../api";
4
+ import { CollectorDto } from "@checkstack/healthcheck-common";
5
+
6
+ interface UseCollectorsResult {
7
+ collectors: CollectorDto[];
8
+ loading: boolean;
9
+ error: Error | undefined;
10
+ refetch: () => Promise<void>;
11
+ }
12
+
13
+ /**
14
+ * Hook to fetch collectors for a given strategy.
15
+ * @param strategyId - The strategy ID to fetch collectors for
16
+ */
17
+ export function useCollectors(strategyId: string): UseCollectorsResult {
18
+ const api = useApi(healthCheckApiRef);
19
+ const [collectors, setCollectors] = useState<CollectorDto[]>([]);
20
+ const [loading, setLoading] = useState(false);
21
+ const [error, setError] = useState<Error>();
22
+
23
+ const refetch = useCallback(async () => {
24
+ if (!strategyId) {
25
+ setCollectors([]);
26
+ return;
27
+ }
28
+
29
+ setLoading(true);
30
+ setError(undefined);
31
+ try {
32
+ const result = await api.getCollectors({ strategyId });
33
+ setCollectors(result);
34
+ } catch (error_) {
35
+ setError(
36
+ error_ instanceof Error
37
+ ? error_
38
+ : new Error("Failed to fetch collectors")
39
+ );
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ }, [api, strategyId]);
44
+
45
+ useEffect(() => {
46
+ refetch();
47
+ }, [refetch]);
48
+
49
+ return { collectors, loading, error, refetch };
50
+ }
51
+
52
+ /**
53
+ * Check if a collector is built-in for a given strategy.
54
+ * Built-in collectors are those registered by the same plugin as the strategy.
55
+ */
56
+ export function isBuiltInCollector(
57
+ collectorId: string,
58
+ strategyId: string
59
+ ): boolean {
60
+ // Collector ID format: ownerPluginId.collectorId
61
+ // Strategy ID typically equals its plugin ID
62
+ return collectorId.startsWith(`${strategyId}.`);
63
+ }
@@ -1,11 +1,13 @@
1
- import { useEffect, useState, useMemo } from "react";
1
+ import { useEffect, useState, useMemo, useCallback } from "react";
2
2
  import { useApi, permissionApiRef } from "@checkstack/frontend-api";
3
3
  import { healthCheckApiRef } from "../api";
4
4
  import {
5
5
  permissions,
6
6
  DEFAULT_RETENTION_CONFIG,
7
7
  type RetentionConfig,
8
+ HEALTH_CHECK_RUN_COMPLETED,
8
9
  } from "@checkstack/healthcheck-common";
10
+ import { useSignal } from "@checkstack/signal-frontend";
9
11
  import type {
10
12
  HealthCheckDiagramSlotContext,
11
13
  TypedHealthCheckRun,
@@ -123,49 +125,66 @@ export function useHealthCheckData({
123
125
  .finally(() => setRetentionLoading(false));
124
126
  }, [api, systemId, configurationId]);
125
127
 
128
+ // Fetch raw data function - extracted for reuse by signal handler
129
+ const fetchRawData = useCallback(
130
+ (showLoading = true) => {
131
+ if (showLoading) {
132
+ setRawLoading(true);
133
+ }
134
+ api
135
+ .getDetailedHistory({
136
+ systemId,
137
+ configurationId,
138
+ startDate: dateRange.startDate,
139
+ // Don't pass endDate for live updates - backend defaults to 'now'
140
+ limit,
141
+ offset,
142
+ })
143
+ .then((response) => {
144
+ setRawRuns(
145
+ response.runs.map((r) => ({
146
+ id: r.id,
147
+ configurationId,
148
+ systemId,
149
+ status: r.status,
150
+ timestamp: r.timestamp,
151
+ latencyMs: r.latencyMs,
152
+ result: r.result,
153
+ }))
154
+ );
155
+ })
156
+ .finally(() => setRawLoading(false));
157
+ },
158
+ [api, systemId, configurationId, dateRange.startDate, limit, offset]
159
+ );
160
+
126
161
  // Fetch raw data when in raw mode
127
162
  useEffect(() => {
128
163
  if (!hasPermission || permissionLoading || retentionLoading || isAggregated)
129
164
  return;
130
-
131
- setRawLoading(true);
132
- api
133
- .getDetailedHistory({
134
- systemId,
135
- configurationId,
136
- startDate: dateRange.startDate,
137
- endDate: dateRange.endDate,
138
- limit,
139
- offset,
140
- })
141
- .then((response) => {
142
- setRawRuns(
143
- response.runs.map((r) => ({
144
- id: r.id,
145
- configurationId,
146
- systemId,
147
- status: r.status,
148
- timestamp: r.timestamp,
149
- latencyMs: r.latencyMs,
150
- result: r.result,
151
- }))
152
- );
153
- })
154
- .finally(() => setRawLoading(false));
165
+ fetchRawData(true);
155
166
  }, [
156
- api,
157
- systemId,
158
- configurationId,
167
+ fetchRawData,
159
168
  hasPermission,
160
169
  permissionLoading,
161
170
  retentionLoading,
162
171
  isAggregated,
163
- dateRange.startDate,
164
- dateRange.endDate,
165
- limit,
166
- offset,
167
172
  ]);
168
173
 
174
+ // Listen for realtime health check updates to refresh data silently
175
+ useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
176
+ // Only refresh if we're in raw mode (not aggregated) and have permission
177
+ if (
178
+ changedId === systemId &&
179
+ hasPermission &&
180
+ !permissionLoading &&
181
+ !retentionLoading &&
182
+ !isAggregated
183
+ ) {
184
+ fetchRawData(false);
185
+ }
186
+ });
187
+
169
188
  // Fetch aggregated data when in aggregated mode
170
189
  useEffect(() => {
171
190
  if (