@checkstack/healthcheck-frontend 0.1.0 → 0.3.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,207 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
8
+
9
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
10
+
11
+ ## Changes
12
+
13
+ ### Core Infrastructure (`@checkstack/common`)
14
+
15
+ - Introduced `AccessRule` interface as the primary access control type
16
+ - Added `accessPair()` helper for creating read/manage access rule pairs
17
+ - Added `access()` builder for individual access rules
18
+ - Replaced `Permission` type with `AccessRule` throughout
19
+
20
+ ### API Changes
21
+
22
+ - `env.registerPermissions()` → `env.registerAccessRules()`
23
+ - `meta.permissions` → `meta.access` in RPC contracts
24
+ - `usePermission()` → `useAccess()` in frontend hooks
25
+ - Route `permission:` field → `accessRule:` field
26
+
27
+ ### UI Changes
28
+
29
+ - "Roles & Permissions" tab → "Roles & Access Rules"
30
+ - "You don't have permission..." → "You don't have access..."
31
+ - All permission-related UI text updated
32
+
33
+ ### Documentation & Templates
34
+
35
+ - Updated 18 documentation files with AccessRule terminology
36
+ - Updated 7 scaffolding templates with `accessPair()` pattern
37
+ - All code examples use new AccessRule API
38
+
39
+ ## Migration Guide
40
+
41
+ ### Backend Plugins
42
+
43
+ ```diff
44
+ - import { permissionList } from "./permissions";
45
+ - env.registerPermissions(permissionList);
46
+ + import { accessRules } from "./access";
47
+ + env.registerAccessRules(accessRules);
48
+ ```
49
+
50
+ ### RPC Contracts
51
+
52
+ ```diff
53
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
54
+ + .meta({ userType: "user", access: [access.read] })
55
+ ```
56
+
57
+ ### Frontend Hooks
58
+
59
+ ```diff
60
+ - const canRead = accessApi.usePermission(permissions.read.id);
61
+ + const canRead = accessApi.useAccess(access.read);
62
+ ```
63
+
64
+ ### Routes
65
+
66
+ ```diff
67
+ - permission: permissions.entityRead.id,
68
+ + accessRule: access.read,
69
+ ```
70
+
71
+ - 827b286: Add array assertion operators for string array fields
72
+
73
+ New operators for asserting on array fields (e.g., playerNames in RCON collectors):
74
+
75
+ - **includes** - Check if array contains a specific value
76
+ - **notIncludes** - Check if array does NOT contain a specific value
77
+ - **lengthEquals** - Check if array length equals a value
78
+ - **lengthGreaterThan** - Check if array length is greater than a value
79
+ - **lengthLessThan** - Check if array length is less than a value
80
+ - **isEmpty** - Check if array is empty
81
+ - **isNotEmpty** - Check if array has at least one element
82
+
83
+ Also exports a new `arrayField()` schema factory for creating array assertion schemas.
84
+
85
+ ### Patch Changes
86
+
87
+ - f533141: Enforce health result factory function usage via branded types
88
+
89
+ - Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
90
+ - Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
91
+ - Added branded `HealthResultField<T>` type to mark schemas created by factory functions
92
+ - Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
93
+ - Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
94
+ - Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
95
+
96
+ - Updated dependencies [9faec1f]
97
+ - Updated dependencies [95eeec7]
98
+ - Updated dependencies [f533141]
99
+ - @checkstack/auth-frontend@0.2.0
100
+ - @checkstack/catalog-common@1.1.0
101
+ - @checkstack/common@0.2.0
102
+ - @checkstack/frontend-api@0.1.0
103
+ - @checkstack/healthcheck-common@0.3.0
104
+ - @checkstack/ui@0.2.0
105
+ - @checkstack/signal-frontend@0.0.6
106
+
107
+ ## 0.2.0
108
+
109
+ ### Minor Changes
110
+
111
+ - 8e43507: # Teams and Resource-Level Access Control
112
+
113
+ This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
114
+
115
+ ## Features
116
+
117
+ ### Team Management
118
+
119
+ - Create, update, and delete teams with name and description
120
+ - Add/remove users from teams
121
+ - Designate team managers with elevated privileges
122
+ - View team membership and manager status
123
+
124
+ ### Resource-Level Access Control
125
+
126
+ - Grant teams access to specific resources (systems, health checks, incidents, maintenances)
127
+ - Configure read-only or manage permissions per team
128
+ - Resource-level "Team Only" mode that restricts access exclusively to team members
129
+ - Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
130
+ - Automatic cleanup of grants when teams are deleted (database cascade)
131
+
132
+ ### Middleware Integration
133
+
134
+ - Extended `autoAuthMiddleware` to support resource access checks
135
+ - Single-resource pre-handler validation for detail endpoints
136
+ - Automatic list filtering for collection endpoints
137
+ - S2S endpoints for access verification
138
+
139
+ ### Frontend Components
140
+
141
+ - `TeamsTab` component for managing teams in Auth Settings
142
+ - `TeamAccessEditor` component for assigning team access to resources
143
+ - Resource-level "Team Only" toggle in `TeamAccessEditor`
144
+ - Integration into System, Health Check, Incident, and Maintenance editors
145
+
146
+ ## Breaking Changes
147
+
148
+ ### API Response Format Changes
149
+
150
+ List endpoints now return objects with named keys instead of arrays directly:
151
+
152
+ ```typescript
153
+ // Before
154
+ const systems = await catalogApi.getSystems();
155
+
156
+ // After
157
+ const { systems } = await catalogApi.getSystems();
158
+ ```
159
+
160
+ Affected endpoints:
161
+
162
+ - `catalog.getSystems` → `{ systems: [...] }`
163
+ - `healthcheck.getConfigurations` → `{ configurations: [...] }`
164
+ - `incident.listIncidents` → `{ incidents: [...] }`
165
+ - `maintenance.listMaintenances` → `{ maintenances: [...] }`
166
+
167
+ ### User Identity Enrichment
168
+
169
+ `RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
170
+
171
+ ## Documentation
172
+
173
+ See `docs/backend/teams.md` for complete API reference and integration guide.
174
+
175
+ - 97c5a6b: Add UUID-based collector identification for better multiple collector support
176
+
177
+ **Breaking Change**: Existing health check configurations with collectors need to be recreated.
178
+
179
+ - Each collector instance now has a unique UUID assigned on creation
180
+ - Collector results are stored under the UUID key with `_collectorId` and `_assertionFailed` metadata
181
+ - Auto-charts correctly display separate charts for each collector instance
182
+ - Charts are now grouped by collector instance with clear headings
183
+ - Assertion status card shows pass/fail for each collector
184
+ - Renamed "Success" to "HTTP Success" to clarify it's about HTTP request success
185
+ - Fixed deletion of collectors not persisting to database
186
+ - Fixed duplicate React key warnings in auto-chart grid
187
+
188
+ ### Patch Changes
189
+
190
+ - 97c5a6b: Fix Radix UI accessibility warning in dialog components by adding visually hidden DialogDescription components
191
+ - Updated dependencies [8e43507]
192
+ - Updated dependencies [97c5a6b]
193
+ - Updated dependencies [97c5a6b]
194
+ - Updated dependencies [8e43507]
195
+ - Updated dependencies [8e43507]
196
+ - Updated dependencies [97c5a6b]
197
+ - @checkstack/ui@0.1.0
198
+ - @checkstack/auth-frontend@0.1.0
199
+ - @checkstack/catalog-common@1.0.0
200
+ - @checkstack/common@0.1.0
201
+ - @checkstack/healthcheck-common@0.2.0
202
+ - @checkstack/frontend-api@0.0.4
203
+ - @checkstack/signal-frontend@0.0.5
204
+
3
205
  ## 0.1.0
4
206
 
5
207
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -9,13 +9,13 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
+ "@checkstack/auth-frontend": "workspace:*",
12
13
  "@checkstack/catalog-common": "workspace:*",
13
14
  "@checkstack/common": "workspace:*",
14
15
  "@checkstack/frontend-api": "workspace:*",
15
16
  "@checkstack/healthcheck-common": "workspace:*",
16
17
  "@checkstack/signal-frontend": "workspace:*",
17
18
  "@checkstack/ui": "workspace:*",
18
- "@types/prismjs": "^1.26.5",
19
19
  "ajv": "^8.17.1",
20
20
  "ajv-formats": "^3.0.1",
21
21
  "date-fns": "^4.1.0",
@@ -28,6 +28,7 @@
28
28
  "zod": "^4.2.1"
29
29
  },
30
30
  "devDependencies": {
31
+ "@types/prismjs": "^1.26.5",
31
32
  "typescript": "^5.0.0",
32
33
  "@types/react": "^18.2.0",
33
34
  "@checkstack/tsconfig": "workspace:*",
@@ -33,6 +33,9 @@ interface AutoChartGridProps {
33
33
 
34
34
  /**
35
35
  * Main component that renders a grid of auto-generated charts.
36
+ *
37
+ * Discovers actual collector instances from run data and creates
38
+ * separate charts for each instance, grouped with headings.
36
39
  */
37
40
  export function AutoChartGrid({ context }: AutoChartGridProps) {
38
41
  const { schemas, loading } = useStrategySchemas(context.strategyId);
@@ -55,22 +58,247 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
55
58
  return;
56
59
  }
57
60
 
58
- const fields = extractChartFields(schema);
59
- if (fields.length === 0) {
61
+ const schemaFields = extractChartFields(schema);
62
+ if (schemaFields.length === 0) {
60
63
  return;
61
64
  }
62
65
 
66
+ // Discover actual collector instances from run data
67
+ const instanceMap = discoverCollectorInstances(context);
68
+
69
+ // Separate strategy-level fields from collector fields
70
+ const strategyFields = schemaFields.filter((f) => !f.collectorId);
71
+
72
+ // Build grouped collector fields
73
+ const collectorGroups = buildCollectorGroups(schemaFields, instanceMap);
74
+
63
75
  return (
64
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-4">
65
- {fields.map((field) => (
66
- <AutoChartCard key={field.name} field={field} context={context} />
76
+ <div className="space-y-6 mt-4">
77
+ {/* Strategy-level fields */}
78
+ {strategyFields.length > 0 && (
79
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
80
+ {strategyFields.map((field) => (
81
+ <AutoChartCard key={field.name} field={field} context={context} />
82
+ ))}
83
+ </div>
84
+ )}
85
+
86
+ {/* Collector groups */}
87
+ {collectorGroups.map((group) => (
88
+ <CollectorGroup
89
+ key={group.instanceKey}
90
+ group={group}
91
+ context={context}
92
+ />
67
93
  ))}
68
94
  </div>
69
95
  );
70
96
  }
71
97
 
98
+ /**
99
+ * A group of fields for a single collector instance.
100
+ */
101
+ interface CollectorGroupData {
102
+ instanceKey: string;
103
+ collectorId: string;
104
+ displayName: string;
105
+ fields: ExpandedChartField[];
106
+ }
107
+
108
+ /**
109
+ * Build grouped collector data from schema fields and instance map.
110
+ */
111
+ function buildCollectorGroups(
112
+ schemaFields: ChartField[],
113
+ instanceMap: Record<string, string[]>
114
+ ): CollectorGroupData[] {
115
+ const groups: CollectorGroupData[] = [];
116
+
117
+ // Process each collector type
118
+ for (const [collectorId, instanceKeys] of Object.entries(instanceMap)) {
119
+ // Get fields for this collector type
120
+ const collectorFields = schemaFields.filter(
121
+ (f) => f.collectorId === collectorId
122
+ );
123
+ if (collectorFields.length === 0) continue;
124
+
125
+ // Create a group for each instance
126
+ for (const [index, instanceKey] of instanceKeys.entries()) {
127
+ const displayName =
128
+ instanceKeys.length === 1
129
+ ? collectorId.split(".").pop() || collectorId
130
+ : `${collectorId.split(".").pop() || collectorId} #${index + 1}`;
131
+
132
+ groups.push({
133
+ instanceKey,
134
+ collectorId,
135
+ displayName,
136
+ fields: collectorFields.map((field) => ({
137
+ ...field,
138
+ instanceKey,
139
+ })),
140
+ });
141
+ }
142
+ }
143
+
144
+ return groups;
145
+ }
146
+
147
+ /**
148
+ * Renders a collector group with heading, assertion status, and field cards.
149
+ */
150
+ function CollectorGroup({
151
+ group,
152
+ context,
153
+ }: {
154
+ group: CollectorGroupData;
155
+ context: HealthCheckDiagramSlotContext;
156
+ }) {
157
+ // Get assertion status for this collector instance
158
+ const assertionFailed = getAssertionFailed(context, group.instanceKey);
159
+
160
+ return (
161
+ <div>
162
+ <h4 className="text-sm font-medium text-muted-foreground mb-3 uppercase tracking-wide">
163
+ {group.displayName}
164
+ </h4>
165
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
166
+ {/* Assertion status card */}
167
+ <AssertionStatusCard assertionFailed={assertionFailed} />
168
+
169
+ {/* Field cards */}
170
+ {group.fields.map((field) => (
171
+ <AutoChartCard
172
+ key={`${field.instanceKey}-${field.name}`}
173
+ field={field}
174
+ context={context}
175
+ />
176
+ ))}
177
+ </div>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Get the _assertionFailed value for a specific collector instance.
184
+ */
185
+ function getAssertionFailed(
186
+ context: HealthCheckDiagramSlotContext,
187
+ instanceKey: string
188
+ ): string | undefined {
189
+ if (context.type === "raw" && context.runs.length > 0) {
190
+ const latestRun = context.runs[0];
191
+ const result = latestRun.result as StoredHealthCheckResult | undefined;
192
+ const collectors = result?.metadata?.collectors as
193
+ | Record<string, Record<string, unknown>>
194
+ | undefined;
195
+ const collectorData = collectors?.[instanceKey];
196
+ return collectorData?._assertionFailed as string | undefined;
197
+ }
198
+ return undefined;
199
+ }
200
+
201
+ /**
202
+ * Card showing assertion pass/fail status.
203
+ */
204
+ function AssertionStatusCard({
205
+ assertionFailed,
206
+ }: {
207
+ assertionFailed: string | undefined;
208
+ }) {
209
+ if (!assertionFailed) {
210
+ return (
211
+ <Card>
212
+ <CardHeader className="pb-2">
213
+ <CardTitle className="text-sm font-medium">Assertion</CardTitle>
214
+ </CardHeader>
215
+ <CardContent>
216
+ <div className="flex items-center gap-2 text-green-600">
217
+ <div className="w-3 h-3 rounded-full bg-green-500" />
218
+ <span>Passed</span>
219
+ </div>
220
+ </CardContent>
221
+ </Card>
222
+ );
223
+ }
224
+
225
+ return (
226
+ <Card className="border-red-200 dark:border-red-900">
227
+ <CardHeader className="pb-2">
228
+ <CardTitle className="text-sm font-medium text-red-600">
229
+ Assertion Failed
230
+ </CardTitle>
231
+ </CardHeader>
232
+ <CardContent>
233
+ <div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded">
234
+ {assertionFailed}
235
+ </div>
236
+ </CardContent>
237
+ </Card>
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Discover collector instances from actual run data.
243
+ * Returns a map from base collector ID (type) to array of instance UUIDs.
244
+ */
245
+ function discoverCollectorInstances(
246
+ context: HealthCheckDiagramSlotContext
247
+ ): Record<string, string[]> {
248
+ const instanceMap: Record<string, Set<string>> = {};
249
+
250
+ const addInstances = (collectors: Record<string, unknown> | undefined) => {
251
+ if (!collectors || typeof collectors !== "object") return;
252
+
253
+ for (const [uuid, data] of Object.entries(collectors)) {
254
+ // Read the collector type from the stored _collectorId metadata
255
+ const collectorData = data as Record<string, unknown> | undefined;
256
+ const collectorId = collectorData?._collectorId as string | undefined;
257
+
258
+ if (collectorId) {
259
+ if (!instanceMap[collectorId]) {
260
+ instanceMap[collectorId] = new Set();
261
+ }
262
+ instanceMap[collectorId].add(uuid);
263
+ }
264
+ }
265
+ };
266
+
267
+ if (context.type === "raw") {
268
+ for (const run of context.runs) {
269
+ const result = run.result as StoredHealthCheckResult | undefined;
270
+ const collectors = result?.metadata?.collectors as
271
+ | Record<string, unknown>
272
+ | undefined;
273
+ addInstances(collectors);
274
+ }
275
+ } else {
276
+ for (const bucket of context.buckets) {
277
+ const collectors = (
278
+ bucket.aggregatedResult as Record<string, unknown> | undefined
279
+ )?.collectors as Record<string, unknown> | undefined;
280
+ addInstances(collectors);
281
+ }
282
+ }
283
+
284
+ // Convert sets to arrays
285
+ const result: Record<string, string[]> = {};
286
+ for (const [collectorId, uuids] of Object.entries(instanceMap)) {
287
+ result[collectorId] = [...uuids].toSorted();
288
+ }
289
+ return result;
290
+ }
291
+
292
+ /**
293
+ * Extended ChartField with instance UUID for data lookup.
294
+ */
295
+ interface ExpandedChartField extends ChartField {
296
+ /** Instance UUID for data lookup */
297
+ instanceKey?: string;
298
+ }
299
+
72
300
  interface AutoChartCardProps {
73
- field: ChartField;
301
+ field: ExpandedChartField;
74
302
  context: HealthCheckDiagramSlotContext;
75
303
  }
76
304
 
@@ -91,7 +319,7 @@ function AutoChartCard({ field, context }: AutoChartCardProps) {
91
319
  }
92
320
 
93
321
  interface ChartRendererProps {
94
- field: ChartField;
322
+ field: ExpandedChartField;
95
323
  context: HealthCheckDiagramSlotContext;
96
324
  }
97
325
 
@@ -139,7 +367,7 @@ function ChartRenderer({ field, context }: ChartRendererProps) {
139
367
  * Counts how many times each value appears across all runs/buckets.
140
368
  */
141
369
  function CounterRenderer({ field, context }: ChartRendererProps) {
142
- const counts = getValueCounts(field.name, context);
370
+ const counts = getValueCounts(field.name, context, field.instanceKey);
143
371
  const entries = Object.entries(counts);
144
372
 
145
373
  if (entries.length === 0) {
@@ -184,7 +412,7 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
184
412
  * Renders a percentage gauge visualization using Recharts RadialBarChart.
185
413
  */
186
414
  function GaugeRenderer({ field, context }: ChartRendererProps) {
187
- const value = getLatestValue(field.name, context);
415
+ const value = getLatestValue(field.name, context, field.instanceKey);
188
416
  const numValue =
189
417
  typeof value === "number" ? Math.min(100, Math.max(0, value)) : 0;
190
418
  const unit = field.unit ?? "%";
@@ -231,7 +459,7 @@ function GaugeRenderer({ field, context }: ChartRendererProps) {
231
459
  * Renders a boolean indicator (success/failure).
232
460
  */
233
461
  function BooleanRenderer({ field, context }: ChartRendererProps) {
234
- const value = getLatestValue(field.name, context);
462
+ const value = getLatestValue(field.name, context, field.instanceKey);
235
463
  const isTrue = value === true;
236
464
 
237
465
  return (
@@ -252,7 +480,7 @@ function BooleanRenderer({ field, context }: ChartRendererProps) {
252
480
  * Renders text value.
253
481
  */
254
482
  function TextRenderer({ field, context }: ChartRendererProps) {
255
- const value = getLatestValue(field.name, context);
483
+ const value = getLatestValue(field.name, context, field.instanceKey);
256
484
  const displayValue = formatTextValue(value);
257
485
 
258
486
  return (
@@ -269,7 +497,7 @@ function TextRenderer({ field, context }: ChartRendererProps) {
269
497
  * Renders error/status badge.
270
498
  */
271
499
  function StatusRenderer({ field, context }: ChartRendererProps) {
272
- const value = getLatestValue(field.name, context);
500
+ const value = getLatestValue(field.name, context, field.instanceKey);
273
501
  const hasValue = value !== undefined && value !== null && value !== "";
274
502
 
275
503
  if (!hasValue) {
@@ -287,7 +515,7 @@ function StatusRenderer({ field, context }: ChartRendererProps) {
287
515
  * Renders an area chart for time series data using Recharts AreaChart.
288
516
  */
289
517
  function LineChartRenderer({ field, context }: ChartRendererProps) {
290
- const values = getAllValues(field.name, context);
518
+ const values = getAllValues(field.name, context, field.instanceKey);
291
519
  const unit = field.unit ?? "";
292
520
 
293
521
  if (values.length === 0) {
@@ -443,7 +671,7 @@ const CHART_COLORS = [
443
671
  */
444
672
  function PieChartRenderer({ field, context }: ChartRendererProps) {
445
673
  // First, try to get a pre-aggregated object value
446
- const value = getLatestValue(field.name, context);
674
+ const value = getLatestValue(field.name, context, field.instanceKey);
447
675
 
448
676
  // Determine the data source: use pre-aggregated object or count simple values
449
677
  let dataRecord: Record<string, number>;
@@ -453,7 +681,7 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
453
681
  dataRecord = value as Record<string, number>;
454
682
  } else {
455
683
  // Simple values (like statusCode) - count occurrences
456
- dataRecord = getValueCounts(field.name, context);
684
+ dataRecord = getValueCounts(field.name, context, field.instanceKey);
457
685
  }
458
686
 
459
687
  const entries = Object.entries(dataRecord).slice(0, 8);
@@ -542,7 +770,8 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
542
770
  */
543
771
  function getLatestValue(
544
772
  fieldName: string,
545
- context: HealthCheckDiagramSlotContext
773
+ context: HealthCheckDiagramSlotContext,
774
+ collectorId?: string
546
775
  ): unknown {
547
776
  if (context.type === "raw") {
548
777
  const runs = context.runs;
@@ -550,7 +779,7 @@ function getLatestValue(
550
779
  // For raw runs, aggregate across all runs for record types
551
780
  const allValues = runs.map((run) => {
552
781
  const result = run.result as StoredHealthCheckResult | undefined;
553
- return getFieldValue(result?.metadata, fieldName);
782
+ return getFieldValue(result?.metadata, fieldName, collectorId);
554
783
  });
555
784
 
556
785
  // If the values are record types (like statusCodeCounts), combine them
@@ -613,14 +842,15 @@ function combineRecordValues(
613
842
  */
614
843
  function getValueCounts(
615
844
  fieldName: string,
616
- context: HealthCheckDiagramSlotContext
845
+ context: HealthCheckDiagramSlotContext,
846
+ collectorId?: string
617
847
  ): Record<string, number> {
618
848
  const counts: Record<string, number> = {};
619
849
 
620
850
  if (context.type === "raw") {
621
851
  for (const run of context.runs) {
622
852
  const result = run.result as StoredHealthCheckResult | undefined;
623
- const value = getFieldValue(result?.metadata, fieldName);
853
+ const value = getFieldValue(result?.metadata, fieldName, collectorId);
624
854
  if (value !== undefined && value !== null) {
625
855
  const key = String(value);
626
856
  counts[key] = (counts[key] || 0) + 1;
@@ -631,7 +861,8 @@ function getValueCounts(
631
861
  for (const bucket of context.buckets) {
632
862
  const value = getFieldValue(
633
863
  bucket.aggregatedResult as Record<string, unknown>,
634
- fieldName
864
+ fieldName,
865
+ collectorId
635
866
  );
636
867
  if (value !== undefined && value !== null) {
637
868
  const key = String(value);
@@ -651,14 +882,15 @@ function getValueCounts(
651
882
  */
652
883
  function getAllValues(
653
884
  fieldName: string,
654
- context: HealthCheckDiagramSlotContext
885
+ context: HealthCheckDiagramSlotContext,
886
+ collectorId?: string
655
887
  ): number[] {
656
888
  if (context.type === "raw") {
657
889
  return context.runs
658
890
  .map((run) => {
659
891
  // result is typed as StoredHealthCheckResult with { status, latencyMs, message, metadata }
660
892
  const result = run.result as StoredHealthCheckResult;
661
- return getFieldValue(result?.metadata, fieldName);
893
+ return getFieldValue(result?.metadata, fieldName, collectorId);
662
894
  })
663
895
  .filter((v): v is number => typeof v === "number");
664
896
  }
@@ -666,7 +898,8 @@ function getAllValues(
666
898
  .map((bucket) =>
667
899
  getFieldValue(
668
900
  bucket.aggregatedResult as Record<string, unknown>,
669
- fieldName
901
+ fieldName,
902
+ collectorId
670
903
  )
671
904
  )
672
905
  .filter((v): v is number => typeof v === "number");