@checkstack/healthcheck-frontend 0.0.2

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 ADDED
@@ -0,0 +1,135 @@
1
+ # @checkstack/healthcheck-frontend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/catalog-common@0.0.2
10
+ - @checkstack/common@0.0.2
11
+ - @checkstack/frontend-api@0.0.2
12
+ - @checkstack/healthcheck-common@0.0.2
13
+ - @checkstack/signal-frontend@0.0.2
14
+ - @checkstack/ui@0.0.2
15
+
16
+ ## 0.2.0
17
+
18
+ ### Minor Changes
19
+
20
+ - a65e002: Add command palette commands and deep-linking support
21
+
22
+ **Backend Changes:**
23
+
24
+ - `healthcheck-backend`: Add "Manage Health Checks" (⇧⌘H) and "Create Health Check" commands
25
+ - `catalog-backend`: Add "Manage Systems" (⇧⌘S) and "Create System" commands
26
+ - `integration-backend`: Add "Manage Integrations" (⇧⌘G), "Create Integration Subscription", and "View Integration Logs" commands
27
+ - `auth-backend`: Add "Manage Users" (⇧⌘U), "Create User", "Manage Roles", and "Manage Applications" commands
28
+ - `command-backend`: Auto-cleanup command registrations when plugins are deregistered
29
+
30
+ **Frontend Changes:**
31
+
32
+ - `HealthCheckConfigPage`: Handle `?action=create` URL parameter
33
+ - `CatalogConfigPage`: Handle `?action=create` URL parameter
34
+ - `IntegrationsPage`: Handle `?action=create` URL parameter
35
+ - `AuthSettingsPage`: Handle `?tab=` and `?action=create` URL parameters
36
+
37
+ ### Patch Changes
38
+
39
+ - 0afa204: Subscribe health check charts and history table to real-time signal updates. Charts now display the full data for the selected time range independently from the paginated history table, and both update automatically when a health check run completes.
40
+ - 32ea706: ### User Menu Loading State Fix
41
+
42
+ Fixed user menu items "popping in" one after another due to independent async permission checks.
43
+
44
+ **Changes:**
45
+
46
+ - Added `UserMenuItemsContext` interface with `permissions` and `hasCredentialAccount` to `@checkstack/frontend-api`
47
+ - `LoginNavbarAction` now pre-fetches all permissions and credential account info before rendering the menu
48
+ - All user menu item components now use the passed context for synchronous permission checks instead of async hooks
49
+ - Uses `qualifyPermissionId` helper for fully-qualified permission IDs
50
+
51
+ **Result:** All menu items appear simultaneously when the user menu opens.
52
+
53
+ - Updated dependencies [52231ef]
54
+ - Updated dependencies [b0124ef]
55
+ - Updated dependencies [54cc787]
56
+ - Updated dependencies [a65e002]
57
+ - Updated dependencies [ae33df2]
58
+ - Updated dependencies [32ea706]
59
+ - @checkstack/ui@0.1.2
60
+ - @checkstack/common@0.2.0
61
+ - @checkstack/frontend-api@0.1.0
62
+ - @checkstack/catalog-common@0.1.2
63
+ - @checkstack/healthcheck-common@0.1.1
64
+ - @checkstack/signal-frontend@0.1.1
65
+
66
+ ## 0.1.1
67
+
68
+ ### Patch Changes
69
+
70
+ - Updated dependencies [0f8cc7d]
71
+ - @checkstack/frontend-api@0.0.3
72
+ - @checkstack/catalog-common@0.1.1
73
+ - @checkstack/ui@0.1.1
74
+
75
+ ## 0.1.0
76
+
77
+ ### Minor Changes
78
+
79
+ - ae19ff6: Add configurable state thresholds for health check evaluation
80
+
81
+ **@checkstack/backend-api:**
82
+
83
+ - Added `VersionedData<T>` generic interface as base for all versioned data structures
84
+ - `VersionedConfig<T>` now extends `VersionedData<T>` and adds `pluginId`
85
+ - Added `migrateVersionedData()` utility function for running migrations on any `VersionedData` subtype
86
+
87
+ **@checkstack/backend:**
88
+
89
+ - Refactored `ConfigMigrationRunner` to use the new `migrateVersionedData` utility
90
+
91
+ **@checkstack/healthcheck-common:**
92
+
93
+ - Added state threshold schemas with two evaluation modes (consecutive, window)
94
+ - Added `stateThresholds` field to `AssociateHealthCheckSchema`
95
+ - Added `getSystemHealthStatus` RPC endpoint contract
96
+
97
+ **@checkstack/healthcheck-backend:**
98
+
99
+ - Added `stateThresholds` column to `system_health_checks` table
100
+ - Added `state-evaluator.ts` with health status evaluation logic
101
+ - Added `state-thresholds-migrations.ts` with migration infrastructure
102
+ - Added `getSystemHealthStatus` RPC handler
103
+
104
+ **@checkstack/healthcheck-frontend:**
105
+
106
+ - Updated `SystemHealthBadge` to use new backend endpoint
107
+
108
+ - 0babb9c: Add public health status access and detailed history for admins
109
+
110
+ **Permission changes:**
111
+
112
+ - Added `healthcheck.status.read` permission with `isPublicDefault: true` for anonymous access
113
+ - `getSystemHealthStatus`, `getSystemHealthOverview`, and `getHistory` now public
114
+ - `getHistory` no longer returns `result` field (security)
115
+
116
+ **New features:**
117
+
118
+ - Added `getDetailedHistory` endpoint with `healthcheck.manage` permission
119
+ - New `/healthcheck/history` page showing paginated run history with expandable result JSON
120
+
121
+ ### Patch Changes
122
+
123
+ - Updated dependencies [eff5b4e]
124
+ - Updated dependencies [ffc28f6]
125
+ - Updated dependencies [4dd644d]
126
+ - Updated dependencies [ae19ff6]
127
+ - Updated dependencies [0babb9c]
128
+ - Updated dependencies [b55fae6]
129
+ - Updated dependencies [b354ab3]
130
+ - @checkstack/ui@0.1.0
131
+ - @checkstack/common@0.1.0
132
+ - @checkstack/catalog-common@0.1.0
133
+ - @checkstack/healthcheck-common@0.1.0
134
+ - @checkstack/signal-frontend@0.1.0
135
+ - @checkstack/frontend-api@0.0.2
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@checkstack/healthcheck-frontend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.tsx",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/catalog-common": "workspace:*",
13
+ "@checkstack/common": "workspace:*",
14
+ "@checkstack/frontend-api": "workspace:*",
15
+ "@checkstack/healthcheck-common": "workspace:*",
16
+ "@checkstack/signal-frontend": "workspace:*",
17
+ "@checkstack/ui": "workspace:*",
18
+ "@types/prismjs": "^1.26.5",
19
+ "ajv": "^8.17.1",
20
+ "ajv-formats": "^3.0.1",
21
+ "date-fns": "^4.1.0",
22
+ "lucide-react": "^0.344.0",
23
+ "prismjs": "^1.30.0",
24
+ "react": "^18.2.0",
25
+ "react-router-dom": "^6.20.0",
26
+ "react-simple-code-editor": "^0.14.1",
27
+ "recharts": "^3.6.0",
28
+ "zod": "^4.2.1"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.0.0",
32
+ "@types/react": "^18.2.0",
33
+ "@checkstack/tsconfig": "workspace:*",
34
+ "@checkstack/scripts": "workspace:*"
35
+ }
36
+ }
package/src/api.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { createApiRef } from "@checkstack/frontend-api";
2
+ import { HealthCheckApi } from "@checkstack/healthcheck-common";
3
+ import type { InferClient } from "@checkstack/common";
4
+
5
+ // Re-export types for convenience
6
+ export type {
7
+ HealthCheckConfiguration,
8
+ HealthCheckStrategyDto,
9
+ HealthCheckRun,
10
+ HealthCheckRunPublic,
11
+ } 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");
@@ -0,0 +1,383 @@
1
+ /**
2
+ * AutoChartGrid - Renders auto-generated charts based on schema metadata.
3
+ *
4
+ * Parses the strategy's result/aggregated schemas to extract chart metadata
5
+ * and renders appropriate visualizations for each annotated field.
6
+ */
7
+
8
+ import type { ChartField } from "./schema-parser";
9
+ import { extractChartFields, getFieldValue } from "./schema-parser";
10
+ import { useStrategySchemas } from "./useStrategySchemas";
11
+ import type { HealthCheckDiagramSlotContext } from "../slots";
12
+ import type { StoredHealthCheckResult } from "@checkstack/healthcheck-common";
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardHeader,
17
+ CardTitle,
18
+ } from "@checkstack/ui";
19
+
20
+ interface AutoChartGridProps {
21
+ context: HealthCheckDiagramSlotContext;
22
+ }
23
+
24
+ /**
25
+ * Main component that renders a grid of auto-generated charts.
26
+ */
27
+ export function AutoChartGrid({ context }: AutoChartGridProps) {
28
+ const { schemas, loading } = useStrategySchemas(context.strategyId);
29
+
30
+ if (loading) {
31
+ return; // Don't show loading state, let custom charts render first
32
+ }
33
+
34
+ if (!schemas) {
35
+ return;
36
+ }
37
+
38
+ // Choose schema based on context type
39
+ const schema =
40
+ context.type === "raw"
41
+ ? schemas.resultSchema
42
+ : schemas.aggregatedResultSchema;
43
+
44
+ if (!schema) {
45
+ return;
46
+ }
47
+
48
+ const fields = extractChartFields(schema);
49
+ if (fields.length === 0) {
50
+ return;
51
+ }
52
+
53
+ return (
54
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-4">
55
+ {fields.map((field) => (
56
+ <AutoChartCard key={field.name} field={field} context={context} />
57
+ ))}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ interface AutoChartCardProps {
63
+ field: ChartField;
64
+ context: HealthCheckDiagramSlotContext;
65
+ }
66
+
67
+ /**
68
+ * Individual chart card that renders based on field type.
69
+ */
70
+ function AutoChartCard({ field, context }: AutoChartCardProps) {
71
+ return (
72
+ <Card>
73
+ <CardHeader className="pb-2">
74
+ <CardTitle className="text-sm font-medium">{field.label}</CardTitle>
75
+ </CardHeader>
76
+ <CardContent>
77
+ <ChartRenderer field={field} context={context} />
78
+ </CardContent>
79
+ </Card>
80
+ );
81
+ }
82
+
83
+ interface ChartRendererProps {
84
+ field: ChartField;
85
+ context: HealthCheckDiagramSlotContext;
86
+ }
87
+
88
+ /**
89
+ * Dispatches to appropriate chart renderer based on chart type.
90
+ */
91
+ function ChartRenderer({ field, context }: ChartRendererProps) {
92
+ switch (field.chartType) {
93
+ case "line": {
94
+ return <LineChartRenderer field={field} context={context} />;
95
+ }
96
+ case "gauge": {
97
+ return <GaugeRenderer field={field} context={context} />;
98
+ }
99
+ case "counter": {
100
+ return <CounterRenderer field={field} context={context} />;
101
+ }
102
+ case "bar": {
103
+ return <BarChartRenderer field={field} context={context} />;
104
+ }
105
+ case "boolean": {
106
+ return <BooleanRenderer field={field} context={context} />;
107
+ }
108
+ case "text": {
109
+ return <TextRenderer field={field} context={context} />;
110
+ }
111
+ case "status": {
112
+ return <StatusRenderer field={field} context={context} />;
113
+ }
114
+ default: {
115
+ return;
116
+ }
117
+ }
118
+ }
119
+
120
+ // =============================================================================
121
+ // CHART RENDERERS
122
+ // =============================================================================
123
+
124
+ /**
125
+ * Renders a large counter value with optional trend.
126
+ */
127
+ function CounterRenderer({ field, context }: ChartRendererProps) {
128
+ const value = getLatestValue(field.name, context);
129
+ const displayValue = typeof value === "number" ? value : "—";
130
+ const unit = field.unit ?? "";
131
+
132
+ return (
133
+ <div className="text-2xl font-bold">
134
+ {displayValue}
135
+ {unit && (
136
+ <span className="text-sm font-normal text-muted-foreground ml-1">
137
+ {unit}
138
+ </span>
139
+ )}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Renders a percentage gauge visualization.
146
+ */
147
+ function GaugeRenderer({ field, context }: ChartRendererProps) {
148
+ const value = getLatestValue(field.name, context);
149
+ const numValue =
150
+ typeof value === "number" ? Math.min(100, Math.max(0, value)) : 0;
151
+ const unit = field.unit ?? "%";
152
+
153
+ // Determine color based on value (for rates: higher is better)
154
+ const colorClass =
155
+ numValue >= 90
156
+ ? "text-green-500"
157
+ : numValue >= 70
158
+ ? "text-yellow-500"
159
+ : "text-red-500";
160
+
161
+ return (
162
+ <div className="flex items-center gap-3">
163
+ <div className="relative w-16 h-16">
164
+ <svg className="w-16 h-16 transform -rotate-90" viewBox="0 0 36 36">
165
+ <circle
166
+ cx="18"
167
+ cy="18"
168
+ r="15.5"
169
+ fill="none"
170
+ className="stroke-muted"
171
+ strokeWidth="3"
172
+ />
173
+ <circle
174
+ cx="18"
175
+ cy="18"
176
+ r="15.5"
177
+ fill="none"
178
+ className={colorClass.replace("text-", "stroke-")}
179
+ strokeWidth="3"
180
+ strokeDasharray={`${numValue} 100`}
181
+ strokeLinecap="round"
182
+ />
183
+ </svg>
184
+ </div>
185
+ <div className={`text-2xl font-bold ${colorClass}`}>
186
+ {numValue.toFixed(1)}
187
+ {unit}
188
+ </div>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Renders a boolean indicator (success/failure).
195
+ */
196
+ function BooleanRenderer({ field, context }: ChartRendererProps) {
197
+ const value = getLatestValue(field.name, context);
198
+ const isTrue = value === true;
199
+
200
+ return (
201
+ <div className="flex items-center gap-2">
202
+ <div
203
+ className={`w-3 h-3 rounded-full ${
204
+ isTrue ? "bg-green-500" : "bg-red-500"
205
+ }`}
206
+ />
207
+ <span className={isTrue ? "text-green-600" : "text-red-600"}>
208
+ {isTrue ? "Yes" : "No"}
209
+ </span>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Renders text value.
216
+ */
217
+ function TextRenderer({ field, context }: ChartRendererProps) {
218
+ const value = getLatestValue(field.name, context);
219
+ const displayValue = formatTextValue(value);
220
+
221
+ return (
222
+ <div
223
+ className="text-sm font-mono text-muted-foreground truncate"
224
+ title={displayValue}
225
+ >
226
+ {displayValue || "—"}
227
+ </div>
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Renders error/status badge.
233
+ */
234
+ function StatusRenderer({ field, context }: ChartRendererProps) {
235
+ const value = getLatestValue(field.name, context);
236
+ const hasValue = value !== undefined && value !== null && value !== "";
237
+
238
+ if (!hasValue) {
239
+ return <div className="text-sm text-muted-foreground">No errors</div>;
240
+ }
241
+
242
+ return (
243
+ <div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded truncate">
244
+ {String(value)}
245
+ </div>
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Renders a simple line chart visualization.
251
+ * For now, shows min/avg/max summary. Full charts can be added later.
252
+ */
253
+ function LineChartRenderer({ field, context }: ChartRendererProps) {
254
+ const values = getAllValues(field.name, context);
255
+ const unit = field.unit ?? "";
256
+
257
+ if (values.length === 0) {
258
+ return <div className="text-muted-foreground">No data</div>;
259
+ }
260
+
261
+ const min = Math.min(...values);
262
+ const max = Math.max(...values);
263
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
264
+
265
+ return (
266
+ <div className="space-y-1">
267
+ <div className="text-2xl font-bold">
268
+ {avg.toFixed(1)}
269
+ {unit && (
270
+ <span className="text-sm font-normal text-muted-foreground ml-1">
271
+ {unit}
272
+ </span>
273
+ )}
274
+ </div>
275
+ <div className="text-xs text-muted-foreground">
276
+ Min: {min.toFixed(1)}
277
+ {unit} · Max: {max.toFixed(1)}
278
+ {unit}
279
+ </div>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Renders a bar chart for record values.
286
+ */
287
+ function BarChartRenderer({ field, context }: ChartRendererProps) {
288
+ const value = getLatestValue(field.name, context);
289
+
290
+ if (!value || typeof value !== "object") {
291
+ return <div className="text-muted-foreground">No data</div>;
292
+ }
293
+
294
+ const entries = Object.entries(value as Record<string, number>).slice(0, 5);
295
+ const maxValue = Math.max(...entries.map(([, v]) => v), 1);
296
+
297
+ return (
298
+ <div className="space-y-2">
299
+ {entries.map(([key, val]) => (
300
+ <div key={key} className="flex items-center gap-2">
301
+ <span className="text-xs w-12 text-right text-muted-foreground">
302
+ {key}
303
+ </span>
304
+ <div className="flex-1 h-4 bg-muted rounded overflow-hidden">
305
+ <div
306
+ className="h-full bg-primary"
307
+ style={{ width: `${(val / maxValue) * 100}%` }}
308
+ />
309
+ </div>
310
+ <span className="text-xs w-8">{val}</span>
311
+ </div>
312
+ ))}
313
+ </div>
314
+ );
315
+ }
316
+
317
+ // =============================================================================
318
+ // HELPER FUNCTIONS
319
+ // =============================================================================
320
+
321
+ /**
322
+ * Get the latest value for a field from the context.
323
+ *
324
+ * For raw runs, the strategy-specific data is inside result.metadata.
325
+ * For aggregated buckets, the data is directly in aggregatedResult.
326
+ */
327
+ function getLatestValue(
328
+ fieldName: string,
329
+ context: HealthCheckDiagramSlotContext
330
+ ): unknown {
331
+ if (context.type === "raw") {
332
+ const runs = context.runs;
333
+ if (runs.length === 0) return undefined;
334
+ // result is typed as StoredHealthCheckResult with { status, latencyMs, message, metadata }
335
+ const result = runs.at(-1)?.result as StoredHealthCheckResult | undefined;
336
+ return getFieldValue(result?.metadata, fieldName);
337
+ } else {
338
+ const buckets = context.buckets;
339
+ if (buckets.length === 0) return undefined;
340
+ return getFieldValue(
341
+ buckets.at(-1)?.aggregatedResult as Record<string, unknown>,
342
+ fieldName
343
+ );
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Get all numeric values for a field from the context.
349
+ *
350
+ * For raw runs, the strategy-specific data is inside result.metadata.
351
+ * For aggregated buckets, the data is directly in aggregatedResult.
352
+ */
353
+ function getAllValues(
354
+ fieldName: string,
355
+ context: HealthCheckDiagramSlotContext
356
+ ): number[] {
357
+ if (context.type === "raw") {
358
+ return context.runs
359
+ .map((run) => {
360
+ // result is typed as StoredHealthCheckResult with { status, latencyMs, message, metadata }
361
+ const result = run.result as StoredHealthCheckResult;
362
+ return getFieldValue(result?.metadata, fieldName);
363
+ })
364
+ .filter((v): v is number => typeof v === "number");
365
+ }
366
+ return context.buckets
367
+ .map((bucket) =>
368
+ getFieldValue(
369
+ bucket.aggregatedResult as Record<string, unknown>,
370
+ fieldName
371
+ )
372
+ )
373
+ .filter((v): v is number => typeof v === "number");
374
+ }
375
+
376
+ /**
377
+ * Format a value for text display.
378
+ */
379
+ function formatTextValue(value: unknown): string {
380
+ if (value === undefined || value === null) return "";
381
+ if (Array.isArray(value)) return value.join(", ");
382
+ return String(value);
383
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auto-chart slot extension registration.
3
+ *
4
+ * Registers the AutoChartGrid as a diagram extension that renders
5
+ * for all strategies that have schema metadata.
6
+ */
7
+
8
+ import { createSlotExtension } from "@checkstack/frontend-api";
9
+ import {
10
+ HealthCheckDiagramSlot,
11
+ type HealthCheckDiagramSlotContext,
12
+ } from "../slots";
13
+ import { AutoChartGrid } from "./AutoChartGrid";
14
+
15
+ /**
16
+ * Extension that renders auto-generated charts for any strategy.
17
+ *
18
+ * Unlike custom chart extensions that filter by strategy ID, this extension
19
+ * renders for all strategies and lets AutoChartGrid decide what to display
20
+ * based on the schema metadata.
21
+ */
22
+ export const autoChartExtension = createSlotExtension(HealthCheckDiagramSlot, {
23
+ id: "healthcheck.auto-charts",
24
+ component: (context: HealthCheckDiagramSlotContext) => {
25
+ return <AutoChartGrid context={context} />;
26
+ },
27
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Auto-chart components for rendering schema-driven visualizations.
3
+ *
4
+ * These components render charts based on x-chart-type metadata in JSON schemas,
5
+ * eliminating the need for custom chart components for standard metrics.
6
+ */
7
+
8
+ export { extractChartFields, getFieldValue } from "./schema-parser";
9
+ export type { ChartField, ChartType } from "./schema-parser";
10
+ export { AutoChartGrid } from "./AutoChartGrid";
11
+ export { useStrategySchemas } from "./useStrategySchemas";
12
+ export { autoChartExtension } from "./extension";