@checkstack/healthcheck-frontend 0.7.1 → 0.8.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,31 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d6f7449: Add availability statistics display to HealthCheckSystemOverview
8
+
9
+ - New `getAvailabilityStats` RPC endpoint that calculates availability percentages for 31-day and 365-day periods
10
+ - Availability is calculated as `(healthyRuns / totalRuns) * 100`
11
+ - Data is sourced from both daily aggregates and recent raw runs to include the most up-to-date information
12
+ - Frontend displays availability stats with color-coded badges (green ≥99.9%, yellow ≥99%, red <99%)
13
+ - Shows total run counts for each period
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [d6f7449]
18
+ - @checkstack/healthcheck-common@0.8.0
19
+ - @checkstack/dashboard-frontend@0.3.12
20
+
21
+ ## 0.7.2
22
+
23
+ ### Patch Changes
24
+
25
+ - e58e994: Fix runtime error in AutoChartGrid when mapping over values with undefined elements
26
+
27
+ The filter functions `getAllBooleanValuesWithTime` and `getAllStringValuesWithTime` incorrectly checked `v !== null` instead of `v !== undefined`, allowing undefined elements to pass through and crash when accessing `.value`.
28
+
3
29
  ## 0.7.1
4
30
 
5
31
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -1202,7 +1202,9 @@ function getAllBooleanValuesWithTime(
1202
1202
  timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
1203
1203
  };
1204
1204
  })
1205
- .filter((v): v is { value: boolean; timeLabel: string } => v !== null);
1205
+ .filter(
1206
+ (v): v is { value: boolean; timeLabel: string } => v !== undefined,
1207
+ );
1206
1208
  }
1207
1209
  return context.buckets
1208
1210
  .map((bucket) => {
@@ -1219,7 +1221,7 @@ function getAllBooleanValuesWithTime(
1219
1221
  timeLabel: `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`,
1220
1222
  };
1221
1223
  })
1222
- .filter((v): v is { value: boolean; timeLabel: string } => v !== null);
1224
+ .filter((v): v is { value: boolean; timeLabel: string } => v !== undefined);
1223
1225
  }
1224
1226
 
1225
1227
  /**
@@ -1242,7 +1244,9 @@ function getAllStringValuesWithTime(
1242
1244
  timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
1243
1245
  };
1244
1246
  })
1245
- .filter((v): v is { value: string; timeLabel: string } => v !== null);
1247
+ .filter(
1248
+ (v): v is { value: string; timeLabel: string } => v !== undefined,
1249
+ );
1246
1250
  }
1247
1251
  return context.buckets
1248
1252
  .map((bucket) => {
@@ -1259,5 +1263,5 @@ function getAllStringValuesWithTime(
1259
1263
  timeLabel: `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`,
1260
1264
  };
1261
1265
  })
1262
- .filter((v): v is { value: string; timeLabel: string } => v !== null);
1266
+ .filter((v): v is { value: string; timeLabel: string } => v !== undefined);
1263
1267
  }
@@ -70,6 +70,30 @@ interface ExpandedRowProps {
70
70
  systemId: string;
71
71
  }
72
72
 
73
+ // Helper to format availability percentage with color
74
+ const formatAvailability = (
75
+ value: number | null,
76
+ totalRuns: number,
77
+ ): { text: string; className: string } => {
78
+ if (value === null || totalRuns === 0) {
79
+ return { text: "N/A", className: "text-muted-foreground" };
80
+ }
81
+ const formatted = value.toFixed(2) + "%";
82
+ if (value >= 99.9) {
83
+ return {
84
+ text: formatted,
85
+ className: "text-green-600 dark:text-green-400",
86
+ };
87
+ }
88
+ if (value >= 99) {
89
+ return {
90
+ text: formatted,
91
+ className: "text-yellow-600 dark:text-yellow-400",
92
+ };
93
+ }
94
+ return { text: formatted, className: "text-red-600 dark:text-red-400" };
95
+ };
96
+
73
97
  const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
74
98
  const healthCheckClient = usePluginClient(HealthCheckApi);
75
99
  const navigate = useNavigate();
@@ -191,6 +215,13 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
191
215
  : `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
192
216
  : "Using default thresholds";
193
217
 
218
+ // Fetch availability stats
219
+ const { data: availabilityData } =
220
+ healthCheckClient.getAvailabilityStats.useQuery({
221
+ systemId,
222
+ configurationId: item.configurationId,
223
+ });
224
+
194
225
  // Render charts - charts handle data transformation internally
195
226
  const renderCharts = () => {
196
227
  if (chartLoading) {
@@ -270,6 +301,59 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
270
301
  </div>
271
302
  </div>
272
303
 
304
+ {/* Availability Stats - Prominent Display */}
305
+ {availabilityData && (
306
+ <div className="grid grid-cols-2 gap-4">
307
+ <div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
308
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
309
+ 31-Day Availability
310
+ </span>
311
+ <div className="flex items-baseline gap-2">
312
+ <span
313
+ className={`text-2xl font-bold ${formatAvailability(availabilityData.availability31Days, availabilityData.totalRuns31Days).className}`}
314
+ >
315
+ {
316
+ formatAvailability(
317
+ availabilityData.availability31Days,
318
+ availabilityData.totalRuns31Days,
319
+ ).text
320
+ }
321
+ </span>
322
+ {availabilityData.totalRuns31Days > 0 && (
323
+ <span className="text-sm text-muted-foreground">
324
+ ({availabilityData.totalRuns31Days.toLocaleString()} runs)
325
+ </span>
326
+ )}
327
+ </div>
328
+ </div>
329
+ <div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
330
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
331
+ 365-Day Availability
332
+ </span>
333
+ <div className="flex items-baseline gap-2">
334
+ <span
335
+ className={`text-2xl font-bold ${formatAvailability(availabilityData.availability365Days, availabilityData.totalRuns365Days).className}`}
336
+ >
337
+ {
338
+ formatAvailability(
339
+ availabilityData.availability365Days,
340
+ availabilityData.totalRuns365Days,
341
+ ).text
342
+ }
343
+ </span>
344
+ {availabilityData.totalRuns365Days > 0 && (
345
+ <span className="text-sm text-muted-foreground">
346
+ ({availabilityData.totalRuns365Days.toLocaleString()} runs)
347
+ </span>
348
+ )}
349
+ </div>
350
+ </div>
351
+ </div>
352
+ )}
353
+
354
+ {/* Divider */}
355
+ <div className="h-px bg-border" />
356
+
273
357
  {/* Date Range Filter with Loading Spinner */}
274
358
  <div className="flex items-center gap-3 flex-wrap">
275
359
  <DateRangeFilter