@checkstack/healthcheck-frontend 0.2.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,176 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7a23261: ## TanStack Query Integration
8
+
9
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
10
+
11
+ ### New Features
12
+
13
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
14
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
15
+ - **Built-in caching**: Configurable stale time and cache duration per query
16
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
17
+ - **Background refetching**: Stale data is automatically refreshed when components mount
18
+
19
+ ### Contract Changes
20
+
21
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
22
+
23
+ ```typescript
24
+ const getItems = proc()
25
+ .meta({ operationType: "query", access: [access.read] })
26
+ .output(z.array(itemSchema))
27
+ .query();
28
+
29
+ const createItem = proc()
30
+ .meta({ operationType: "mutation", access: [access.manage] })
31
+ .input(createItemSchema)
32
+ .output(itemSchema)
33
+ .mutation();
34
+ ```
35
+
36
+ ### Migration
37
+
38
+ ```typescript
39
+ // Before (forPlugin pattern)
40
+ const api = useApi(myPluginApiRef);
41
+ const [items, setItems] = useState<Item[]>([]);
42
+ useEffect(() => {
43
+ api.getItems().then(setItems);
44
+ }, [api]);
45
+
46
+ // After (usePluginClient pattern)
47
+ const client = usePluginClient(MyPluginApi);
48
+ const { data: items, isLoading } = client.getItems.useQuery({});
49
+ ```
50
+
51
+ ### Bug Fixes
52
+
53
+ - Fixed `rpc.test.ts` test setup for middleware type inference
54
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
55
+ - Fixed null→undefined warnings in notification and queue frontends
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [180be38]
60
+ - Updated dependencies [7a23261]
61
+ - @checkstack/dashboard-frontend@0.2.0
62
+ - @checkstack/frontend-api@0.2.0
63
+ - @checkstack/common@0.3.0
64
+ - @checkstack/auth-frontend@0.3.0
65
+ - @checkstack/catalog-common@1.2.0
66
+ - @checkstack/healthcheck-common@0.4.0
67
+ - @checkstack/ui@0.2.1
68
+ - @checkstack/signal-frontend@0.0.7
69
+
70
+ ## 0.3.0
71
+
72
+ ### Minor Changes
73
+
74
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
75
+
76
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
77
+
78
+ ## Changes
79
+
80
+ ### Core Infrastructure (`@checkstack/common`)
81
+
82
+ - Introduced `AccessRule` interface as the primary access control type
83
+ - Added `accessPair()` helper for creating read/manage access rule pairs
84
+ - Added `access()` builder for individual access rules
85
+ - Replaced `Permission` type with `AccessRule` throughout
86
+
87
+ ### API Changes
88
+
89
+ - `env.registerPermissions()` → `env.registerAccessRules()`
90
+ - `meta.permissions` → `meta.access` in RPC contracts
91
+ - `usePermission()` → `useAccess()` in frontend hooks
92
+ - Route `permission:` field → `accessRule:` field
93
+
94
+ ### UI Changes
95
+
96
+ - "Roles & Permissions" tab → "Roles & Access Rules"
97
+ - "You don't have permission..." → "You don't have access..."
98
+ - All permission-related UI text updated
99
+
100
+ ### Documentation & Templates
101
+
102
+ - Updated 18 documentation files with AccessRule terminology
103
+ - Updated 7 scaffolding templates with `accessPair()` pattern
104
+ - All code examples use new AccessRule API
105
+
106
+ ## Migration Guide
107
+
108
+ ### Backend Plugins
109
+
110
+ ```diff
111
+ - import { permissionList } from "./permissions";
112
+ - env.registerPermissions(permissionList);
113
+ + import { accessRules } from "./access";
114
+ + env.registerAccessRules(accessRules);
115
+ ```
116
+
117
+ ### RPC Contracts
118
+
119
+ ```diff
120
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
121
+ + .meta({ userType: "user", access: [access.read] })
122
+ ```
123
+
124
+ ### Frontend Hooks
125
+
126
+ ```diff
127
+ - const canRead = accessApi.usePermission(permissions.read.id);
128
+ + const canRead = accessApi.useAccess(access.read);
129
+ ```
130
+
131
+ ### Routes
132
+
133
+ ```diff
134
+ - permission: permissions.entityRead.id,
135
+ + accessRule: access.read,
136
+ ```
137
+
138
+ - 827b286: Add array assertion operators for string array fields
139
+
140
+ New operators for asserting on array fields (e.g., playerNames in RCON collectors):
141
+
142
+ - **includes** - Check if array contains a specific value
143
+ - **notIncludes** - Check if array does NOT contain a specific value
144
+ - **lengthEquals** - Check if array length equals a value
145
+ - **lengthGreaterThan** - Check if array length is greater than a value
146
+ - **lengthLessThan** - Check if array length is less than a value
147
+ - **isEmpty** - Check if array is empty
148
+ - **isNotEmpty** - Check if array has at least one element
149
+
150
+ Also exports a new `arrayField()` schema factory for creating array assertion schemas.
151
+
152
+ ### Patch Changes
153
+
154
+ - f533141: Enforce health result factory function usage via branded types
155
+
156
+ - Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
157
+ - Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
158
+ - Added branded `HealthResultField<T>` type to mark schemas created by factory functions
159
+ - Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
160
+ - Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
161
+ - Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
162
+
163
+ - Updated dependencies [9faec1f]
164
+ - Updated dependencies [95eeec7]
165
+ - Updated dependencies [f533141]
166
+ - @checkstack/auth-frontend@0.2.0
167
+ - @checkstack/catalog-common@1.1.0
168
+ - @checkstack/common@0.2.0
169
+ - @checkstack/frontend-api@0.1.0
170
+ - @checkstack/healthcheck-common@0.3.0
171
+ - @checkstack/ui@0.2.0
172
+ - @checkstack/signal-frontend@0.0.6
173
+
3
174
  ## 0.2.0
4
175
 
5
176
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -12,6 +12,7 @@
12
12
  "@checkstack/auth-frontend": "workspace:*",
13
13
  "@checkstack/catalog-common": "workspace:*",
14
14
  "@checkstack/common": "workspace:*",
15
+ "@checkstack/dashboard-frontend": "workspace:*",
15
16
  "@checkstack/frontend-api": "workspace:*",
16
17
  "@checkstack/healthcheck-common": "workspace:*",
17
18
  "@checkstack/signal-frontend": "workspace:*",
package/src/api.ts CHANGED
@@ -1,7 +1,3 @@
1
- import { createApiRef } from "@checkstack/frontend-api";
2
- import { HealthCheckApi } from "@checkstack/healthcheck-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
1
  // Re-export types for convenience
6
2
  export type {
7
3
  HealthCheckConfiguration,
@@ -9,9 +5,5 @@ export type {
9
5
  HealthCheckRun,
10
6
  HealthCheckRunPublic,
11
7
  } from "@checkstack/healthcheck-common";
12
-
13
- // HealthCheckApiClient type inferred from the client definition
14
- export type HealthCheckApiClient = InferClient<typeof HealthCheckApi>;
15
-
16
- export const healthCheckApiRef =
17
- createApiRef<HealthCheckApiClient>("healthcheck-api");
8
+ // Client definition is in @checkstack/healthcheck-common - use with usePluginClient
9
+ export { HealthCheckApi } from "@checkstack/healthcheck-common";
@@ -7,8 +7,8 @@
7
7
  * Supports nested schemas under `collectors.*` for per-collector metrics.
8
8
  */
9
9
 
10
- import type { ChartType } from "@checkstack/healthcheck-common";
11
10
  import type {
11
+ ChartType,
12
12
  JsonSchemaPropertyCore,
13
13
  JsonSchemaBase,
14
14
  } from "@checkstack/common";
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  import { useEffect, useState } from "react";
10
- import { useApi } from "@checkstack/frontend-api";
11
- import { healthCheckApiRef } from "../api";
10
+ import { usePluginClient } from "@checkstack/frontend-api";
11
+ import { HealthCheckApi } from "../api";
12
12
 
13
13
  interface StrategySchemas {
14
14
  resultSchema: Record<string, unknown> | undefined;
@@ -28,59 +28,50 @@ export function useStrategySchemas(strategyId: string): {
28
28
  schemas: StrategySchemas | undefined;
29
29
  loading: boolean;
30
30
  } {
31
- const api = useApi(healthCheckApiRef);
31
+ const healthCheckClient = usePluginClient(HealthCheckApi);
32
32
  const [schemas, setSchemas] = useState<StrategySchemas | undefined>();
33
33
  const [loading, setLoading] = useState(true);
34
34
 
35
+ // Fetch strategies with useQuery
36
+ const { data: strategies } = healthCheckClient.getStrategies.useQuery({});
37
+
38
+ // Fetch collectors with useQuery
39
+ const { data: collectors } = healthCheckClient.getCollectors.useQuery(
40
+ { strategyId },
41
+ { enabled: !!strategyId }
42
+ );
43
+
35
44
  useEffect(() => {
36
- let cancelled = false;
37
-
38
- async function fetchSchemas() {
39
- try {
40
- // Fetch strategy and collectors in parallel
41
- const [strategies, collectors] = await Promise.all([
42
- api.getStrategies(),
43
- api.getCollectors({ strategyId }),
44
- ]);
45
-
46
- const strategy = strategies.find((s) => s.id === strategyId);
47
-
48
- if (!cancelled && strategy) {
49
- // Build collector schemas object for nesting under resultSchema.properties.collectors
50
- const collectorProperties: Record<string, unknown> = {};
51
- for (const collector of collectors) {
52
- // Use full ID so it matches stored data keys like "healthcheck-http.request"
53
- collectorProperties[collector.id] = collector.resultSchema;
54
- }
55
-
56
- // Merge collector schemas into strategy result schema
57
- const mergedResultSchema = mergeCollectorSchemas(
58
- strategy.resultSchema as Record<string, unknown> | undefined,
59
- collectorProperties
60
- );
61
-
62
- setSchemas({
63
- resultSchema: mergedResultSchema,
64
- aggregatedResultSchema:
65
- (strategy.aggregatedResultSchema as Record<string, unknown>) ??
66
- undefined,
67
- });
68
- }
69
- } catch (error) {
70
- console.error("Failed to fetch strategy schemas:", error);
71
- } finally {
72
- if (!cancelled) {
73
- setLoading(false);
74
- }
75
- }
45
+ if (!strategies || !collectors) {
46
+ return;
76
47
  }
77
48
 
78
- fetchSchemas();
49
+ const strategy = strategies.find((s) => s.id === strategyId);
79
50
 
80
- return () => {
81
- cancelled = true;
82
- };
83
- }, [api, strategyId]);
51
+ if (strategy) {
52
+ // Build collector schemas object for nesting under resultSchema.properties.collectors
53
+ const collectorProperties: Record<string, unknown> = {};
54
+ for (const collector of collectors) {
55
+ // Use full ID so it matches stored data keys like "healthcheck-http.request"
56
+ collectorProperties[collector.id] = collector.resultSchema;
57
+ }
58
+
59
+ // Merge collector schemas into strategy result schema
60
+ const mergedResultSchema = mergeCollectorSchemas(
61
+ strategy.resultSchema as Record<string, unknown> | undefined,
62
+ collectorProperties
63
+ );
64
+
65
+ setSchemas({
66
+ resultSchema: mergedResultSchema,
67
+ aggregatedResultSchema:
68
+ (strategy.aggregatedResultSchema as Record<string, unknown>) ??
69
+ undefined,
70
+ });
71
+ }
72
+
73
+ setLoading(false);
74
+ }, [strategies, collectors, strategyId]);
84
75
 
85
76
  return { schemas, loading };
86
77
  }
@@ -81,6 +81,13 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
81
81
  ],
82
82
  enum: [{ value: "equals", label: "Equals" }],
83
83
  array: [
84
+ { value: "includes", label: "Includes" },
85
+ { value: "notIncludes", label: "Not Includes" },
86
+ { value: "lengthEquals", label: "Length Equals" },
87
+ { value: "lengthGreaterThan", label: "Length Greater Than" },
88
+ { value: "lengthLessThan", label: "Length Less Than" },
89
+ { value: "isEmpty", label: "Is Empty" },
90
+ { value: "isNotEmpty", label: "Is Not Empty" },
84
91
  { value: "exists", label: "Exists" },
85
92
  { value: "notExists", label: "Not Exists" },
86
93
  ],
@@ -98,6 +105,7 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
98
105
  // Operators that don't need a value input
99
106
  const VALUE_LESS_OPERATORS = new Set([
100
107
  "isEmpty",
108
+ "isNotEmpty",
101
109
  "isTrue",
102
110
  "isFalse",
103
111
  "exists",
@@ -44,20 +44,20 @@ export function HealthCheckDiagram({
44
44
  }
45
45
 
46
46
  /**
47
- * Wrapper that shows permission message when user lacks access.
47
+ * Wrapper that shows access message when user lacks access.
48
48
  */
49
- export function HealthCheckDiagramPermissionGate({
50
- hasPermission,
49
+ export function HealthCheckDiagramAccessGate({
50
+ hasAccess,
51
51
  children,
52
52
  }: {
53
- hasPermission: boolean;
53
+ hasAccess: boolean;
54
54
  children: React.ReactNode;
55
55
  }) {
56
- if (!hasPermission) {
56
+ if (!hasAccess) {
57
57
  return (
58
58
  <InfoBanner variant="info">
59
59
  Additional strategy-specific visualizations are available with the
60
- &quot;Read Health Check Details&quot; permission.
60
+ &quot;Read Health Check Details&quot; access rule.
61
61
  </InfoBanner>
62
62
  );
63
63
  }
@@ -1,6 +1,5 @@
1
- import React, { useEffect, useState } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
3
- import { healthCheckApiRef, HealthCheckRunPublic } from "../api";
1
+ import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
2
+ import { HealthCheckApi } from "../api";
4
3
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
5
4
  import {
6
5
  Table,
@@ -25,18 +24,15 @@ export const HealthCheckHistory: React.FC<SlotProps> = (props) => {
25
24
  const { system, configurationId, limit } = props as Props;
26
25
  const systemId = system?.id;
27
26
 
28
- const healthCheckApi = useApi(healthCheckApiRef);
29
- const [history, setHistory] = useState<HealthCheckRunPublic[]>([]);
30
- const [loading, setLoading] = useState(true);
27
+ const healthCheckClient = usePluginClient(HealthCheckApi);
31
28
 
32
- useEffect(() => {
33
- // If it's used in a context that doesn't provide systemId or configurationId,
34
- // we might want to skip or handle it.
35
- healthCheckApi
36
- .getHistory({ systemId, configurationId, limit })
37
- .then((response) => setHistory(response.runs))
38
- .finally(() => setLoading(false));
39
- }, [healthCheckApi, systemId, configurationId, limit]);
29
+ // Fetch history with useQuery
30
+ const { data, isLoading: loading } = healthCheckClient.getHistory.useQuery(
31
+ { systemId, configurationId, limit },
32
+ { enabled: true }
33
+ );
34
+
35
+ const history = data?.runs ?? [];
40
36
 
41
37
  if (loading) return <LoadingSpinner />;
42
38
 
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
3
3
  import { Activity } from "lucide-react";
4
4
  import type { UserMenuItemsContext } from "@checkstack/frontend-api";
5
5
  import { DropdownMenuItem } from "@checkstack/ui";
6
- import { qualifyPermissionId, resolveRoute } from "@checkstack/common";
6
+ import { resolveRoute } from "@checkstack/common";
7
7
  import {
8
8
  healthcheckRoutes,
9
- permissions,
9
+ healthCheckAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/healthcheck-common";
12
12
 
13
13
  export const HealthCheckMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
17
- pluginMetadata,
18
- permissions.healthCheckRead
19
- );
16
+ const qualifiedId = `${pluginMetadata.pluginId}.${healthCheckAccess.configuration.read.id}`;
20
17
  const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
21
18
 
22
19
  if (!canRead) {
@@ -1,9 +1,11 @@
1
- import React, { useState } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
1
+ import React, { useState, useCallback } from "react";
2
+ import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
3
3
  import { useSignal } from "@checkstack/signal-frontend";
4
- import { healthCheckApiRef } from "../api";
5
4
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
6
- import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
5
+ import {
6
+ HEALTH_CHECK_RUN_COMPLETED,
7
+ HealthCheckApi,
8
+ } from "@checkstack/healthcheck-common";
7
9
  import {
8
10
  HealthBadge,
9
11
  LoadingSpinner,
@@ -16,6 +18,7 @@ import {
16
18
  Tooltip,
17
19
  Pagination,
18
20
  usePagination,
21
+ usePaginationSync,
19
22
  DateRangeFilter,
20
23
  } from "@checkstack/ui";
21
24
  import { formatDistanceToNow } from "date-fns";
@@ -50,7 +53,7 @@ interface ExpandedRowProps {
50
53
  }
51
54
 
52
55
  const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
53
- const api = useApi(healthCheckApiRef);
56
+ const healthCheckClient = usePluginClient(HealthCheckApi);
54
57
 
55
58
  // Date range state for filtering
56
59
  const [dateRange, setDateRange] = useState<{
@@ -79,42 +82,33 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
79
82
  limit: 1000,
80
83
  });
81
84
 
82
- // Paginated history for the table
85
+ // Pagination state for history table
86
+ const pagination = usePagination({ defaultLimit: 10 });
87
+
88
+ // Fetch paginated history with useQuery
83
89
  const {
84
- items: runs,
85
- loading,
86
- pagination,
87
- } = usePagination({
88
- fetchFn: (params: {
89
- limit: number;
90
- offset: number;
91
- systemId: string;
92
- configurationId: string;
93
- startDate?: Date;
94
- }) =>
95
- api.getHistory({
96
- systemId: params.systemId,
97
- configurationId: params.configurationId,
98
- limit: params.limit,
99
- offset: params.offset,
100
- startDate: params.startDate,
101
- // Don't pass endDate - backend defaults to 'now' so new runs are included
102
- }),
103
- getItems: (response) => response.runs,
104
- getTotal: (response) => response.total,
105
- extraParams: {
106
- systemId,
107
- configurationId: item.configurationId,
108
- startDate: dateRange.startDate,
109
- },
110
- defaultLimit: 10,
90
+ data: historyData,
91
+ isLoading: loading,
92
+ refetch,
93
+ } = healthCheckClient.getHistory.useQuery({
94
+ systemId,
95
+ configurationId: item.configurationId,
96
+ limit: pagination.limit,
97
+ offset: pagination.offset,
98
+ startDate: dateRange.startDate,
99
+ // Don't pass endDate - backend defaults to 'now' so new runs are included
111
100
  });
112
101
 
102
+ // Sync total from response
103
+ usePaginationSync(pagination, historyData?.total);
104
+
105
+ const runs = historyData?.runs ?? [];
106
+
113
107
  // Listen for realtime health check updates to refresh history table
114
108
  // Charts are refreshed automatically by useHealthCheckData
115
109
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
116
110
  if (changedId === systemId) {
117
- pagination.silentRefetch();
111
+ void refetch();
118
112
  }
119
113
  });
120
114
 
@@ -248,100 +242,49 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
248
242
 
249
243
  export function HealthCheckSystemOverview(props: SlotProps) {
250
244
  const systemId = props.system.id;
251
- const api = useApi(healthCheckApiRef);
245
+ const healthCheckClient = usePluginClient(HealthCheckApi);
252
246
 
253
- // Fetch health check overview
254
- const [overview, setOverview] = React.useState<HealthCheckOverviewItem[]>([]);
255
- const [initialLoading, setInitialLoading] = React.useState(true);
256
247
  const [expandedRow, setExpandedRow] = React.useState<string | undefined>();
257
248
 
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
- });
276
- }, [api, systemId]);
277
-
278
- React.useEffect(() => {
279
- fetchOverview();
280
- }, [fetchOverview]);
281
-
282
- // Listen for realtime health check updates - merge into existing state to avoid remounting expanded content
283
- useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
284
- if (changedId === systemId) {
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
- });
342
- }
249
+ // Fetch health check overview using useQuery
250
+ const {
251
+ data: overviewData,
252
+ isLoading: initialLoading,
253
+ refetch,
254
+ } = healthCheckClient.getSystemHealthOverview.useQuery({
255
+ systemId,
343
256
  });
344
257
 
258
+ // Transform API response to component format
259
+ const overview: HealthCheckOverviewItem[] = React.useMemo(() => {
260
+ if (!overviewData) return [];
261
+ return overviewData.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
+ }, [overviewData]);
274
+
275
+ // Listen for realtime health check updates to refresh overview
276
+ useSignal(
277
+ HEALTH_CHECK_RUN_COMPLETED,
278
+ useCallback(
279
+ ({ systemId: changedId }) => {
280
+ if (changedId === systemId) {
281
+ void refetch();
282
+ }
283
+ },
284
+ [systemId, refetch]
285
+ )
286
+ );
287
+
345
288
  if (initialLoading) {
346
289
  return <LoadingSpinner />;
347
290
  }