@checkstack/healthcheck-frontend 0.12.0 → 0.13.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,108 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 26d8bae: Distributed satellite health checks and Assignment IDE page
8
+
9
+ **Satellite System**
10
+
11
+ - New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
12
+ - WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
13
+ - Satellite management UI with create dialog, status badges, and list page
14
+
15
+ **Live Configuration Updates**
16
+
17
+ - Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
18
+ - `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
19
+
20
+ **Assignment IDE Page**
21
+
22
+ - Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
23
+ - New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
24
+ - Added unassign capability and sorted assignment lists for stable ordering
25
+
26
+ **Shared IDE Primitives**
27
+
28
+ - Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
29
+ - Migrated existing health check IDE editor to use shared primitives
30
+
31
+ **Infrastructure**
32
+
33
+ - Added `Dockerfile.satellite` for containerized satellite deployment
34
+ - WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
35
+
36
+ - 26d8bae: Source attribution and filtering for satellite health checks
37
+
38
+ **Source Attribution**
39
+
40
+ - Fixed satellite result attribution: runs from satellites now correctly display their source instead of defaulting to "Local"
41
+ - Added `sourceId` and `sourceLabel` to both public and detailed history API responses
42
+
43
+ **Source Filtering**
44
+
45
+ - Added `sourceFilter` parameter to `getHistory`, `getDetailedHistory`, and `getDetailedAggregatedHistory` RPC endpoints
46
+ - Source filter supports "local" (core-only), specific satellite UUID, or all sources
47
+ - Filter applies to all three aggregation tiers (raw, hourly, daily)
48
+
49
+ **Frontend**
50
+
51
+ - System detail accordion shows source filter buttons (All / Local / per-satellite) next to date range filter
52
+ - Filter applies to both charts and recent runs table
53
+ - Source column added to the recent runs table with Local/Remote badges
54
+ - Health check history detail page includes per-satellite source filter buttons
55
+
56
+ ### Patch Changes
57
+
58
+ - Updated dependencies [26d8bae]
59
+ - Updated dependencies [26d8bae]
60
+ - @checkstack/ui@1.3.0
61
+ - @checkstack/healthcheck-common@0.11.0
62
+ - @checkstack/satellite-common@0.2.0
63
+ - @checkstack/auth-frontend@0.5.19
64
+ - @checkstack/dashboard-frontend@0.3.27
65
+
66
+ ## 0.12.1
67
+
68
+ ### Patch Changes
69
+
70
+ - d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
71
+
72
+ **New utility**
73
+
74
+ - `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
75
+
76
+ **ESLint rules**
77
+
78
+ - `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
79
+ - `no-console` in frontend packages — forces `toast` over silent `console.error`
80
+ - `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
81
+ - Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
82
+
83
+ **Refactoring**
84
+
85
+ - Replace 141 `instanceof Error` boilerplate patterns across the codebase
86
+ - Replace swallowed `console.error` with user-visible `toast.error()` feedback
87
+ - Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
88
+ - Consolidate 3 identical callback handlers into `handleDialogClose`
89
+ - Fix conditional React hook call in `FormField.tsx`
90
+ - Fix unstable useMemo deps in `Dashboard.tsx`
91
+ - Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
92
+ - Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
93
+ - Delete obvious comments in `encryption.ts` and Teams `provider.ts`
94
+
95
+ - Updated dependencies [d1a2796]
96
+ - Updated dependencies [3c34b07]
97
+ - @checkstack/common@0.6.5
98
+ - @checkstack/ui@1.2.1
99
+ - @checkstack/auth-frontend@0.5.18
100
+ - @checkstack/dashboard-frontend@0.3.26
101
+ - @checkstack/frontend-api@0.3.9
102
+ - @checkstack/catalog-common@1.3.1
103
+ - @checkstack/healthcheck-common@0.10.1
104
+ - @checkstack/signal-frontend@0.0.15
105
+
3
106
  ## 0.12.0
4
107
 
5
108
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -12,14 +12,14 @@
12
12
  "lint:code": "eslint . --max-warnings 0"
13
13
  },
14
14
  "dependencies": {
15
- "@checkstack/auth-frontend": "0.5.17",
16
- "@checkstack/catalog-common": "1.2.11",
17
- "@checkstack/common": "0.6.4",
18
- "@checkstack/dashboard-frontend": "0.3.23",
19
- "@checkstack/frontend-api": "0.3.8",
20
- "@checkstack/healthcheck-common": "0.8.4",
21
- "@checkstack/signal-frontend": "0.0.14",
22
- "@checkstack/ui": "1.2.0",
15
+ "@checkstack/auth-frontend": "0.5.18",
16
+ "@checkstack/catalog-common": "1.3.1",
17
+ "@checkstack/common": "0.6.5",
18
+ "@checkstack/dashboard-frontend": "0.3.26",
19
+ "@checkstack/frontend-api": "0.3.9",
20
+ "@checkstack/healthcheck-common": "0.10.1",
21
+ "@checkstack/signal-frontend": "0.0.15",
22
+ "@checkstack/ui": "1.2.1",
23
23
  "ajv": "^8.18.0",
24
24
  "ajv-formats": "^3.0.1",
25
25
  "date-fns": "^4.1.0",
@@ -28,11 +28,12 @@
28
28
  "react-router-dom": "^6.20.0",
29
29
  "recharts": "^3.6.0",
30
30
  "uuid": "^13.0.0",
31
- "zod": "^4.2.1"
31
+ "zod": "^4.2.1",
32
+ "@checkstack/satellite-common": "0.1.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@checkstack/scripts": "0.1.2",
35
- "@checkstack/tsconfig": "0.0.4",
36
+ "@checkstack/tsconfig": "0.0.5",
36
37
  "@types/react": "^18.2.0",
37
38
  "typescript": "^5.0.0"
38
39
  }
@@ -230,10 +230,6 @@ function extractComputedValue(value: unknown): unknown {
230
230
 
231
231
  // _type is required for all aggregated state objects
232
232
  if (!("_type" in obj)) {
233
- console.error(
234
- "[AutoChart] Missing _type discriminator in aggregated state:",
235
- obj,
236
- );
237
233
  return value;
238
234
  }
239
235
 
@@ -252,9 +248,6 @@ function extractComputedValue(value: unknown): unknown {
252
248
  return obj.max;
253
249
  }
254
250
  default: {
255
- console.error(
256
- `[AutoChart] Unrecognized aggregated state type: ${String(obj._type)}`,
257
- );
258
251
  return value;
259
252
  }
260
253
  }
@@ -10,7 +10,7 @@ import {
10
10
  Pagination,
11
11
  } from "@checkstack/ui";
12
12
  import { formatDistanceToNow, format } from "date-fns";
13
- import { ExternalLink, Loader2 } from "lucide-react";
13
+ import { ExternalLink, Loader2, Satellite, Server } from "lucide-react";
14
14
  import { useNavigate } from "react-router-dom";
15
15
  import { healthcheckRoutes } from "@checkstack/healthcheck-common";
16
16
  import { resolveRoute } from "@checkstack/common";
@@ -22,6 +22,10 @@ export interface HealthCheckRunDetailed {
22
22
  status: "healthy" | "unhealthy" | "degraded";
23
23
  result: Record<string, unknown>;
24
24
  timestamp: Date;
25
+ /** Source ID for result attribution (undefined = local core, UUID = satellite) */
26
+ sourceId?: string;
27
+ /** Human-readable source label (e.g. "Local" or "EU West (eu-west-1)") */
28
+ sourceLabel?: string;
25
29
  }
26
30
 
27
31
  export interface HealthCheckRunsTableProps {
@@ -93,6 +97,7 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
93
97
  </>
94
98
  )}
95
99
  <TableHead>Timestamp</TableHead>
100
+ <TableHead>Source</TableHead>
96
101
  {showFilterColumns && <TableHead className="w-16"></TableHead>}
97
102
  </TableRow>
98
103
  </TableHeader>
@@ -123,6 +128,19 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
123
128
  })}
124
129
  </span>
125
130
  </TableCell>
131
+ <TableCell>
132
+ {run.sourceId ? (
133
+ <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-orange-500/10 text-orange-600">
134
+ <Satellite className="h-3 w-3" />
135
+ {run.sourceLabel ?? "Remote"}
136
+ </span>
137
+ ) : (
138
+ <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
139
+ <Server className="h-3 w-3" />
140
+ {run.sourceLabel ?? "Local"}
141
+ </span>
142
+ )}
143
+ </TableCell>
126
144
  {showFilterColumns && (
127
145
  <TableCell>
128
146
  <ExternalLink className="h-4 w-4 text-muted-foreground" />
@@ -15,6 +15,7 @@ import {
15
15
  healthCheckAccess,
16
16
  healthcheckRoutes,
17
17
  } from "@checkstack/healthcheck-common";
18
+ import { SatelliteApi, satelliteAccess } from "@checkstack/satellite-common";
18
19
  import { resolveRoute } from "@checkstack/common";
19
20
  import {
20
21
  HealthBadge,
@@ -36,10 +37,14 @@ import {
36
37
  CardContent,
37
38
  CardHeader,
38
39
  CardTitle,
39
- AnimatedNumber,
40
40
  } from "@checkstack/ui";
41
41
  import { formatDistanceToNow } from "date-fns";
42
- import { ChevronDown, ChevronRight } from "lucide-react";
42
+ import {
43
+ ChevronDown,
44
+ ChevronRight,
45
+ Satellite as SatelliteIcon,
46
+ Server,
47
+ } from "lucide-react";
43
48
  import { useNavigate } from "react-router-dom";
44
49
  import { HealthCheckSparkline } from "./HealthCheckSparkline";
45
50
  import { HealthCheckLatencyChart } from "./HealthCheckLatencyChart";
@@ -71,30 +76,24 @@ interface ExpandedRowProps {
71
76
  systemId: string;
72
77
  }
73
78
 
74
- // Helper to get color class for availability percentage
75
- const getAvailabilityColorClass = (
76
- value: number | null,
77
- totalRuns: number,
78
- ): string => {
79
- if (value === null || totalRuns === 0) {
80
- return "text-muted-foreground";
81
- }
82
- if (value >= 99.9) {
83
- return "text-green-600 dark:text-green-400";
84
- }
85
- if (value >= 99) {
86
- return "text-yellow-600 dark:text-yellow-400";
87
- }
88
- return "text-red-600 dark:text-red-400";
89
- };
90
-
91
79
  const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
92
80
  const healthCheckClient = usePluginClient(HealthCheckApi);
81
+ const satelliteClient = usePluginClient(SatelliteApi);
93
82
  const navigate = useNavigate();
94
83
  const accessApi = useApi(accessApiRef);
95
84
  const { allowed: canViewDetails } = accessApi.useAccess(
96
85
  healthCheckAccess.details,
97
86
  );
87
+ const { allowed: canReadSatellites } = accessApi.useAccess(
88
+ satelliteAccess.satellite.read,
89
+ );
90
+
91
+ // Fetch satellites for source filter (only if user has access)
92
+ const { data: satellitesData } = satelliteClient.listSatellites.useQuery(
93
+ {},
94
+ { enabled: canReadSatellites },
95
+ );
96
+ const satellites = satellitesData?.satellites ?? [];
98
97
 
99
98
  // Date range state for filtering - default to last 24 hours
100
99
  const [dateRange, setDateRange] = useState(() =>
@@ -102,6 +101,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
102
101
  );
103
102
  // Track if a rolling preset is active (vs custom range)
104
103
  const [isRollingPreset, setIsRollingPreset] = useState(true);
104
+ const [sourceFilter, setSourceFilter] = useState<string | undefined>();
105
105
 
106
106
  // Callback to handle date range changes from the filter
107
107
  const handleDateRangeChange = useCallback(
@@ -155,6 +155,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
155
155
  configurationId: item.configurationId,
156
156
  strategyId: item.strategyId,
157
157
  dateRange,
158
+ sourceFilter,
158
159
  isRollingPreset,
159
160
  // Update endDate to current time when new runs are detected (only for rolling presets)
160
161
  onDateRangeRefresh: (newEndDate) => {
@@ -177,6 +178,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
177
178
  offset: pagination.offset,
178
179
  startDate: dateRange.startDate,
179
180
  // Don't pass endDate - backend defaults to 'now' so new runs are included
181
+ sourceFilter,
180
182
  sortOrder: "desc",
181
183
  });
182
184
 
@@ -193,12 +195,11 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
193
195
  }
194
196
  const runs = displayRuns;
195
197
 
196
- // Listen for realtime health check updates to refresh history table and availability stats
198
+ // Listen for realtime health check updates to refresh history table
197
199
  // Charts are refreshed automatically by useHealthCheckData
198
200
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
199
201
  if (changedId === systemId) {
200
202
  void refetch();
201
- void refetchAvailability();
202
203
  }
203
204
  });
204
205
 
@@ -208,13 +209,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
208
209
  : `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
209
210
  : "Using default thresholds";
210
211
 
211
- // Fetch availability stats
212
- const { data: availabilityData, refetch: refetchAvailability } =
213
- healthCheckClient.getAvailabilityStats.useQuery({
214
- systemId,
215
- configurationId: item.configurationId,
216
- });
217
-
218
212
  // Render charts - charts handle data transformation internally
219
213
  const renderCharts = () => {
220
214
  if (chartLoading) {
@@ -289,49 +283,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
289
283
  </div>
290
284
  </div>
291
285
 
292
- {/* Availability Stats - Prominent Display */}
293
- {availabilityData && (
294
- <div className="grid grid-cols-2 gap-4">
295
- <div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
296
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
297
- 31-Day Availability
298
- </span>
299
- <div className="flex items-baseline gap-2">
300
- <AnimatedNumber
301
- value={availabilityData.availability31Days ?? undefined}
302
- suffix="%"
303
- className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability31Days, availabilityData.totalRuns31Days)}`}
304
- />
305
- {availabilityData.totalRuns31Days > 0 && (
306
- <span className="text-sm text-muted-foreground">
307
- ({availabilityData.totalRuns31Days.toLocaleString()} runs)
308
- </span>
309
- )}
310
- </div>
311
- </div>
312
- <div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
313
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
314
- 365-Day Availability
315
- </span>
316
- <div className="flex items-baseline gap-2">
317
- <AnimatedNumber
318
- value={availabilityData.availability365Days ?? undefined}
319
- suffix="%"
320
- className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability365Days, availabilityData.totalRuns365Days)}`}
321
- />
322
- {availabilityData.totalRuns365Days > 0 && (
323
- <span className="text-sm text-muted-foreground">
324
- ({availabilityData.totalRuns365Days.toLocaleString()} runs)
325
- </span>
326
- )}
327
- </div>
328
- </div>
329
- </div>
330
- )}
331
-
332
- {/* Divider */}
333
- <div className="h-px bg-border" />
334
-
335
286
  {/* Date Range Filter with Loading Spinner */}
336
287
  <div className="flex items-center gap-3 flex-wrap">
337
288
  <DateRangeFilter
@@ -356,6 +307,49 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
356
307
  <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
357
308
  )}
358
309
  </div>
310
+ {/* Source filter (visible when satellites exist and user has read access) */}
311
+ {canReadSatellites && satellites.length > 0 && (
312
+ <div className="flex items-center gap-2">
313
+ <span className="text-xs text-muted-foreground">Source:</span>
314
+ <div className="flex items-center gap-1">
315
+ <button
316
+ onClick={() => setSourceFilter(undefined)}
317
+ className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
318
+ sourceFilter === undefined
319
+ ? "bg-primary text-primary-foreground"
320
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
321
+ }`}
322
+ >
323
+ All
324
+ </button>
325
+ <button
326
+ onClick={() => setSourceFilter("local")}
327
+ className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
328
+ sourceFilter === "local"
329
+ ? "bg-primary text-primary-foreground"
330
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
331
+ }`}
332
+ >
333
+ <Server className="h-3 w-3" />
334
+ Local
335
+ </button>
336
+ {satellites.map((sat) => (
337
+ <button
338
+ key={sat.id}
339
+ onClick={() => setSourceFilter(sat.id)}
340
+ className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
341
+ sourceFilter === sat.id
342
+ ? "bg-orange-500 text-white"
343
+ : "bg-orange-500/10 text-orange-600 hover:bg-orange-500/20"
344
+ }`}
345
+ >
346
+ <SatelliteIcon className="h-3 w-3" />
347
+ {sat.name}
348
+ </button>
349
+ ))}
350
+ </div>
351
+ </div>
352
+ )}
359
353
 
360
354
  {/* Charts Section */}
361
355
  {renderCharts()}
@@ -373,12 +367,14 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
373
367
  </span>
374
368
  <div className="flex-1 h-px bg-border" />
375
369
  </div>
370
+
376
371
  <div className="rounded-md border">
377
372
  <Table>
378
373
  <TableHeader>
379
374
  <TableRow>
380
375
  <TableHead className="w-24">Status</TableHead>
381
376
  <TableHead>Time</TableHead>
377
+ <TableHead>Source</TableHead>
382
378
  </TableRow>
383
379
  </TableHeader>
384
380
  <TableBody>
@@ -412,6 +408,19 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
412
408
  addSuffix: true,
413
409
  })}
414
410
  </TableCell>
411
+ <TableCell>
412
+ {run.sourceId ? (
413
+ <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-orange-500/10 text-orange-600">
414
+ <SatelliteIcon className="h-3 w-3" />
415
+ {run.sourceLabel ?? "Remote"}
416
+ </span>
417
+ ) : (
418
+ <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
419
+ <Server className="h-3 w-3" />
420
+ {run.sourceLabel ?? "Local"}
421
+ </span>
422
+ )}
423
+ </TableCell>
415
424
  </TableRow>
416
425
  ))}
417
426
  </TableBody>