@checkstack/healthcheck-frontend 0.21.0 → 0.23.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,314 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9dcc848: Redesign the catalog into a group-first browse view and tabbed management tables, with inline health rollups.
8
+
9
+ - Browse view: the catalog home is a real read-only, scale-built experience - collapsible group sections (with member counts) plus a synthetic Ungrouped section, a shared toolbar (search, group/health/tag filters, density toggle), URL-backed view state (shareable deep links), polished empty states, and a manager-only "Manage catalog" link. Per-system status badges render through the existing `SystemStateBadgesSlot`; filtering is client-side over the loaded set.
10
+ - Management: redesigned as tabbed data tables (Systems / Groups / Environments) replacing the two-column drag-to-assign layout. Systems get multi-select + a bulk bar, inline health, and group + environment membership as removable chips with type-ahead pickers (portaled so they are never clipped); Groups get inline rename and member chips; Environments get a name / members / field-count table (CRUD gated by `catalog.environment.manage`). GitOps-locked rows stay read-only. Drag-and-drop (and `@dnd-kit` on this page) is removed; the management page also shares the browse toolbar.
11
+ - Inline health rollups: a new platform contract `CatalogBrowseHealthSlot` (`@checkstack/catalog-common`) - an additive optional slot catalog-frontend only consumes (a headless data boundary feeding group rollups + the health filter), with a catalog-owned `CatalogHealthStatus` vocabulary so catalog gains no health-plugin dependency. Group headers show a rollup pill derived from the reported status DATA (a system absent from the map is `"unknown"`, never healthy); all-healthy groups start collapsed. The health filter is wired on both toolbars and enables once a filler reports. healthcheck-frontend fills the slot by reusing dashboard-frontend's `SystemBadgeDataProvider`. When no health source is installed the slot is unfilled and the catalog stays fully functional.
12
+
13
+ This is a beta minor.
14
+
15
+ - 9dcc848: Redesign the dashboard as an extensible "needs attention" overview, and normalize system state badges.
16
+
17
+ The dashboard now surfaces ONLY systems that need attention (degraded, unhealthy, breaching/at-risk SLO, under an incident or active maintenance, anomalous, or with a dependency problem) and hides everything healthy. A compact header summarises fleet health and filters by severity; each problem renders as an elevated card with one row per issue that deep-links to where the issue originates. A calm "all clear" state shows when nothing needs attention, a live "recent activity" feed sits below, and a "View catalog" link replaces the duplicated system list.
18
+
19
+ New platform contract `SystemSignalsSlot` (`@checkstack/catalog-common`): a headless, render-once slot where any plugin bulk-fetches and reports structured `SystemSignal[]` per system via `onSignals(sourceId, map)`. The dashboard aggregates every source agnostic to which plugins contribute; each core reliability plugin (healthcheck, incident, SLO, maintenance, anomaly, dependency) ships a filler, and third-party plugins add new per-system state the same way with no dashboard change. Signals carry an `iconName` rendered via `DynamicIcon` so the contract stays React-free. The dashboard's old summary tiles and overview sheets are removed, so it no longer depends on those plugins' packages. The group "subscribe" control moved onto the catalog browse page's group headers.
20
+
21
+ System state badges are normalized into one icon-only `@checkstack/ui` `StatusBadge` primitive - a small tinted icon chip with the full label on hover/focus (and via `aria-label`). Each signal uses its feature's navbar icon (health = Activity, incident = AlertTriangle, SLO = Target, maintenance = Wrench, dependency = GitBranch; anomaly = ChartSpline). Badges self-sort by severity via CSS `order` (error -> warn -> info), tooltips are scoped to a named group, and in catalog browse rows the cluster moved to the right edge.
22
+
23
+ This is a beta minor.
24
+
25
+ - 9dcc848: Add environments as a first-class catalog primitive, with per-environment health-check fan-out, config templating, per-environment reactive health, and script run-context exposure.
26
+
27
+ - Catalog primitive: an environment is a sibling of groups - a named, instance-global record carrying free-form custom fields (baseUrl, region, tier, ...) that any system can belong to many-to-many. New `environments` + `systems_environments` tables, `EnvironmentSchema` + create/update schemas, `EntityService` environment CRUD and membership joins, RPC endpoints gated by a new `catalogAccess.environment` access rule, a GitOps `Environment` kind + `System.environments` extension, and frontend management (an `EnvironmentEditor`, an Environments management panel, and a per-system environment picker). The Environments card's Add/Edit/Delete affordances are gated on `catalogAccess.environment.manage`.
28
+ - Per-environment fan-out: run identity becomes `(systemId, configurationId, environmentId)`. Runs, aggregates, and state transitions gain a nullable `environmentId`. The health-check assignment gains an `environmentIds` selector with three modes (All / Specific / None; `null` and `[]` are distinct). The queue executor resolves the effective environment set via the catalog `resolveSystemEnvironments` read and executes one isolated run per environment.
29
+ - Config templating: a new `x-templatable` config-field marker renders a string field through the template engine at execute time, against `{ environment, check, system }`. A shared `renderTemplatableConfig` and a `renderTemplatePreview` helper (re-exported from `@checkstack/template-engine`) keep editor previews identical to the run-time render. The HTTP collector's `url`, `headers[].value`, and `body` are templatable, rendered per environment (the strategy client build moves inside the per-env loop); the `url`'s `.url()` validation moves post-render. Secrets resolve before templating; a field marked both secret and `x-templatable` is rejected at plugin load. `DynamicForm` shows a live "Preview" line, and the catalog `EnvironmentPreviewPicker` ("Preview as: <environment>") drives it in the collector editor (only when the schema has a templatable field).
30
+ - Script run-context: `CollectorRunContext` gains an optional `environment` field (`{ id, name, fields }`, metadata only). Shell collectors receive `CHECKSTACK_ENV_ID` / `_NAME` / `CHECKSTACK_ENV_<FIELD>` vars; inline TS collectors read `globalThis.context.environment`; the editor test panel mirrors both. The env-less path is unchanged.
31
+ - Per-environment reactive health (see BREAKING below), env-keyed read/write paths, env-qualified serialization locks, an optional `trigger.payload.environmentId`, per-environment isolation, and an `ENVIRONMENT_RESOLUTION_FAILED` signal when catalog resolution degrades to a single env-less run.
32
+
33
+ BREAKING CHANGES: the reactive `health` entity's id-shape and cardinality change. It now encodes two views: per-environment (id `"<systemId>::<environmentId>"`) and a system rollup (id `"<systemId>"`, the worst status across environments + env-less runs). The rollup PRESERVES the pre-existing system-level contract - dashboards, status badges, and automations referencing health by `systemId` keep working without re-authoring - but the entity's contract surface changed (new id-shape, higher cardinality, new payload field), so it is flagged breaking. `getBulkHealthState` parses env-qualified ids and keys results by the original id.
34
+
35
+ State and scale: membership and custom fields live only in catalog Postgres and are re-read every tick via the cross-plugin RPC; env-keyed health reads from shared `health_check_runs` / aggregates / transitions (compute-on-read). Every pod resolves the same effective set and the same per-environment health. No pod-local environment state.
36
+
37
+ Also: `unwrapSchema` in `zod-config.ts` loops instead of single-pass-stripping so multi-layer wrappers (`.optional().default()`) still resolve `x-templatable` meta. The env-less `{{ environment.* }}` run notice logs at `debug` (a legitimate recurring configuration), while the post-render HTTP `.url()` check still fails a genuinely-broken empty render with a clear "Rendered URL is invalid" error.
38
+
39
+ This is a beta minor.
40
+
41
+ - 9dcc848: Cut initial-load JS: lazy plugin contributions, a hardened lazy-by-default contribution contract, on-demand Monaco, and a lighter icon/chart load.
42
+
43
+ - Lazy plugin route pages: each plugin's route `element` references a `React.lazy`-wrapped page rendered inside a shared `<Suspense>` boundary. Plugins still register synchronously, so nav, slots, commands, API factories, and `foreignSignals` are available on first paint. This moves ~37 route-page chunks (~600 KB) out of the entry; the entry chunk drops from ~2.4 MB to ~190 KB. Auth flow pages stay eager. The `@checkstack/scripts` scaffold template generates lazy route pages too.
44
+ - Hardened contribution contract (BREAKING, frontend plugin contract): plugins declare contributions lazily and let the framework own code-splitting, Suspense, and per-plugin error isolation. Routes use `load: () => import("./Page").then((m) => ({ default: m.Page }))` instead of `element: <Page />` (`element` is still accepted for the rare page that must paint without a chunk fetch; provide exactly one). Slot extensions accept either an eager `component` or a lazy `load`; new `getLazyContribution` + `ExtensionComponent` exports from `@checkstack/frontend-api` render either kind. This also fixes runtime-installed plugins: `ExtensionSlot` subscribes to the plugin registry, and the API registry rebuilds when the plugin set changes (`getPlugins()` returns an immutable snapshot via `useSyncExternalStore`). A per-plugin error boundary contains a bad contribution.
45
+ - On-demand Monaco: the `@checkstack/ui` barrel no longer pulls the `@codingame/*` / `monaco-languageclient` stack into the initial load. `CodeEditor` lazy-loads its Monaco-backed editor behind `React.lazy` + Suspense, `validateTypeScriptSources` imports the editor API via in-body `await import(...)`, and the "vscode services ready" signal moved to a Monaco-free module. The ~10 MB editor body loads only when a `CodeEditor` mounts. A `react-vendor` `manualChunks` split was added for stable vendor caching.
46
+ - lucide-react 1.x + lighter icons/charts (BREAKING for icon consumers): lucide-react unified from three drifting ranges to `^1.17.0`. lucide v1 removed brand icons, so the GitHub/GitLab marks are vendored in `@checkstack/ui` (`GithubIcon`, `GitlabIcon`, `brandIcons`); a new `IconName` type (`LucideIconName | BrandIconName`) in `@checkstack/common` is canonical, accepted by `AuthStrategy.icon` and the card components, so data-driven brand names keep working. `DynamicIcon` no longer eagerly imports lucide's ~1600-icon map (~1 MB) - it lives in a `React.lazy` `iconRegistry` chunk fetched on first data-driven render, while statically named-imported icons tree-shake normally. The recharts-backed health-check charts (~300 KB) and the `HealthCheckSystemOverview` drawer leave the initial load.
47
+
48
+ BREAKING CHANGES:
49
+
50
+ - Frontend plugin contract: routes/slot contributions are lazy-by-default (`load` instead of `element`/eager elements) as described above.
51
+ - Any external consumer importing a brand icon from `lucide-react` (e.g. `import { Github } from "lucide-react"`) must switch to the vendored `@checkstack/ui` brand icons or a custom SVG.
52
+
53
+ This is a beta minor.
54
+
55
+ - 9dcc848: Add the auto-generated, version-pinned `@checkstack/sdk` package + codegen, and serve its types live to the in-app editor.
56
+
57
+ - A new committed workspace package `@checkstack/sdk`, generated from the platform's source of truth by `scripts/generate-sdk.ts` (`generate:sdk` / `generate:sdk:check`): a fully-typed oRPC client (`createCheckstackClient`) over the REST surface with one `InferClient` per plugin contract, real script-authoring helpers (`@checkstack/sdk/healthcheck`, `@checkstack/sdk/integration`) whose runtime body is the same identity function the in-app runner injects, per-subpath `.d.ts` under the package `exports` map, and an editor-only ambient bundle. A `generate:sdk:check` CI guard fails when the committed SDK files drift from a fresh generation. The `@checkstack/sdk` version is stamped from `@checkstack/release` and MUST NOT appear in a changeset (a guard enforces this); the `@checkstack/release` bump here advances the release version so the generated SDK can be published later. The generated client also normalizes its base URL without a backtracking-prone regex, closing a CodeQL `js/polynomial-redos` finding.
58
+ - Live editor type injection: a new version-keyed route `GET /api/script-packages/sdk-types/:releaseVersion` (raw handler in `@checkstack/script-packages-backend`) serves the generated SDK editor bundle with `Cache-Control: private, max-age=1y, immutable`; the pure path-build/parse module lives in `@checkstack/script-packages-common`, shared by backend and frontend. A mismatched version returns `409` so the editor refetches and never serves stale types after an upgrade. The frontend `useSdkTypeInjection` hook fetches the bundle once per session and mounts it into Monaco via `addExtraLib`. Schema-narrowed `context.config` / `context.event.payload` editor types stay local; the package-resolving module declarations come from the one published `@checkstack/sdk` source.
59
+
60
+ BREAKING CHANGES: the script-authoring import surface moves from the bare `@checkstack/healthcheck` / `@checkstack/integration` virtual modules to the `@checkstack/sdk/healthcheck` / `@checkstack/sdk/integration` subpaths of the published `@checkstack/sdk` package. The old bare-name imports no longer resolve (an old import now errors in the editor, surfacing the migration). Existing scripts must update the module specifier:
61
+
62
+ - import { defineHealthCheck } from "@checkstack/healthcheck";
63
+ + import { defineHealthCheck } from "@checkstack/sdk/healthcheck";
64
+
65
+ - import { defineIntegration } from "@checkstack/integration";
66
+ + import { defineIntegration } from "@checkstack/sdk/integration";
67
+
68
+ The helper names and their runtime behaviour are unchanged - only the module specifier moves. The global (no-import) helper form continues to work unchanged.
69
+
70
+ This is a beta minor.
71
+
72
+ - 9dcc848: Align workspace dependency versions and migrate React Router to v7.
73
+
74
+ BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
75
+
76
+ Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
77
+
78
+ Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
79
+
80
+ Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
81
+
82
+ ### Patch Changes
83
+
84
+ - 9dcc848: Move primary navigation into a left sidebar, and serve the user guide in-app.
85
+
86
+ Feature navigation (a ~20-item user-menu dropdown) now lives in a persistent left sidebar (a slide-over drawer on mobile), grouped by section with the active route highlighted; the user menu keeps only account actions. A route opts into the sidebar with new `nav` metadata (`{ group, icon, label?, order?, accessRule? }`) on its registration, co-located with path + access + title. The sidebar filters entries with the same access check as page guards. `@checkstack/common` gains `isAccessRuleSatisfied` and a centralized set of in-app doc slugs (`APP_DOC_SLUGS` + `docsPath`, with a test asserting each resolves to a real docs page); `@checkstack/auth-frontend` exports `useAccessRules`.
87
+
88
+ The backend now serves the Astro Starlight docs build same-origin at `/checkstack/*` (the same artifact deployed to GitHub Pages), so the user guide is available inside the app including for self-hosted / air-gapped installs (served verbatim, no rebuild, no link rewriting; from `CHECKSTACK_DOCS_DIST`, before the SPA catch-all, degrading gracefully when absent; the Docker image builds and ships `docs/dist`; Vite proxies `/checkstack` in dev). The "Docs" link is a shell-owned external sidebar entry under the Documentation group (book icon), opening `/checkstack/user-guide/` in a new tab; the group renders even when no plugin route contributes to it.
89
+
90
+ BREAKING (plugin authors): `UserMenuItemsSlot` is no longer the way to add navigation - registering a top user-menu item no longer surfaces it anywhere. Add `nav` to the page's route instead. `UserMenuItemsBottomSlot` (account items) is unchanged. All bundled plugins have been migrated.
91
+
92
+ This is a beta minor.
93
+
94
+ - 9dcc848: Guard component animations behind isLowPower, and add a shared inline Spinner.
95
+
96
+ - `@checkstack/ui` shared components (`Tabs`, `ConfirmationModal`, `Accordion`, `CodeEditor` popout-button backdrop blur) now drop their `animate-*` / `backdrop-blur` classes when the device reports the low-power tier, matching `LoadingSpinner` / `Skeleton`. No public API change; normal-power rendering is unchanged.
97
+ - A new shared inline `Spinner` (`@checkstack/ui`) renders a lucide `Loader2` whose `animate-spin` is gated internally behind `usePerformance().isLowPower`, so call sites inherit the low-power guard. Props: `size` (`sm`/`md`/`lg`), `className`, rest spread to the icon; decorative by default (`aria-hidden`), `role="status"` when given `aria-label`. The hand-rolled `Loader2` button/table spinners in `HealthCheckDrawer`, `HealthCheckRunsTable`, `IncidentEditor`, `IncidentUpdateForm`, `ProviderConnectionsPage`, `MaintenanceEditor`, `MaintenanceUpdateForm`, `UserChannelCard`, and `DynamicOptionsField` are migrated onto it.
98
+ - Remaining unguarded `animate-*` / `animate-in` / blur classes across the auth, gitops, healthcheck, incident, integration, maintenance, and notification frontends are gated behind `usePerformance().isLowPower`, so effects degrade gracefully on low-power devices per the performance rule.
99
+
100
+ Normal-power behavior is unchanged; low-power rendering drops the animations.
101
+
102
+ This is a beta minor.
103
+
104
+ - Updated dependencies [9dcc848]
105
+ - Updated dependencies [9dcc848]
106
+ - Updated dependencies [9dcc848]
107
+ - Updated dependencies [9dcc848]
108
+ - Updated dependencies [9dcc848]
109
+ - Updated dependencies [9dcc848]
110
+ - Updated dependencies [9dcc848]
111
+ - Updated dependencies [9dcc848]
112
+ - Updated dependencies [9dcc848]
113
+ - Updated dependencies [9dcc848]
114
+ - Updated dependencies [9dcc848]
115
+ - Updated dependencies [9dcc848]
116
+ - Updated dependencies [9dcc848]
117
+ - Updated dependencies [9dcc848]
118
+ - Updated dependencies [9dcc848]
119
+ - Updated dependencies [9dcc848]
120
+ - Updated dependencies [9dcc848]
121
+ - Updated dependencies [9dcc848]
122
+ - Updated dependencies [9dcc848]
123
+ - @checkstack/ui@1.13.0
124
+ - @checkstack/auth-frontend@0.7.0
125
+ - @checkstack/healthcheck-common@1.5.0
126
+ - @checkstack/anomaly-common@1.3.0
127
+ - @checkstack/catalog-frontend@0.11.0
128
+ - @checkstack/catalog-common@2.3.0
129
+ - @checkstack/common@0.13.0
130
+ - @checkstack/dashboard-frontend@0.8.0
131
+ - @checkstack/frontend-api@0.7.0
132
+ - @checkstack/gitops-frontend@0.5.0
133
+ - @checkstack/script-packages-frontend@0.3.0
134
+ - @checkstack/secrets-frontend@0.2.0
135
+ - @checkstack/tips-frontend@0.3.0
136
+ - @checkstack/satellite-common@0.8.0
137
+ - @checkstack/signal-frontend@0.2.0
138
+
139
+ ## 0.22.0
140
+
141
+ ### Minor Changes
142
+
143
+ - b995afb: Move health-check flapping configuration from the per-assignment notification policy onto the `healthcheck.flapping_detected` automation trigger.
144
+
145
+ Flapping thresholds (`transitions`, `windowMinutes`) are now configured on the trigger itself, next to the automation that reacts to them, instead of on each check assignment. The health-check executor still owns the windowed transition counting (it writes `health_check_unhealthy_transitions` and runs the window query), but it now SOURCES the thresholds from the subscribed automations' trigger config:
146
+
147
+ - On a transition-to-unhealthy it records the transition unconditionally (keeping history warm), then looks up the enabled automations subscribed to `healthcheck.flapping_detected`, collects the distinct set of configured windows, counts transitions once per distinct window, and emits one `healthcheck.flapping_detected` per window. The trigger's exact-window `evaluateConfig` gate then fires each automation only for its own window and transition threshold.
148
+ - A missing or partial flapping trigger config defaults to `{ transitions: 3, windowMinutes: 60 }`, so automations created before the trigger carried config keep working unchanged.
149
+ - `automation-backend` exposes a new backend-only, read-only `automationSubscriptionsRef` service ref (`findEnabledByTriggerEvent`) so a plugin that owns a trigger's underlying event can discover its subscribers' trigger config. It is never browser-exposed.
150
+
151
+ **BREAKING CHANGES**
152
+
153
+ - The per-assignment `notificationPolicy.flappingTrigger` field is removed. `NotificationPolicy` is now `{ suppressDeEscalations }` only. Stored rows that still carry a `flappingTrigger` key parse cleanly - the key is stripped on read - so no data migration is required, but the per-check flapping toggle/threshold in the assignment Notifications tab is gone; configure flapping on the trigger instead.
154
+ - The GitOps `System.healthcheck[].notificationPolicy.flappingTrigger` field is removed. A `flappingTrigger` block in a manifest is ignored. Move the thresholds to the `transitions` / `windowMinutes` config of your `healthcheck.flapping_detected` automation trigger.
155
+ - The standalone `enabled` flag for flapping is gone: flapping is "enabled" precisely when at least one enabled automation subscribes to `healthcheck.flapping_detected`. With no subscriber, the transition is still recorded but nothing is counted or emitted.
156
+
157
+ - b995afb: Remove the legacy per-assignment auto-incident system. Auto-incidents are now built entirely by user-authored automations; nothing is seeded or hardcoded.
158
+
159
+ What was removed:
160
+
161
+ - The one-time migration that auto-seeded "sustained unhealthy" and "flapping" default automations from each assignment's notification policy, plus the `listAutoIncidentPolicies` RPC it consumed.
162
+ - The seeder-only notification-policy settings and their UI: `autoOpenIncidentOnUnhealthy`, `useNotificationSuppression`, `skipDuringMaintenance`, `sustainedUnhealthyTrigger`, and `autoCloseAfterMinutes`. The assignment **Notifications** tab now exposes only the two live settings: **Suppress de-escalation notifications** and the **flapping-detection** thresholds.
163
+ - The dead `health_check_auto_incidents` table (no longer written or read; dropped via migration).
164
+
165
+ What is preserved: flapping detection (`healthcheck.flapping_detected`) and de-escalation suppression are unchanged. The `flappingTrigger` and `suppressDeEscalations` policy fields stay exactly as before.
166
+
167
+ > [!NOTE]
168
+ > One-time cleanup: an automation-backend migration deletes the historically auto-seeded incident automations (`managed_by LIKE 'auto-incident:%'`) from existing databases. This is intentional and destructive - those automations were no longer managed by anything. If you had edited a seeded automation and want to keep it, re-create it as a normal automation before upgrading. See the "Build auto-incident automations" guide for templates.
169
+
170
+ > [!IMPORTANT]
171
+ > NARROWING: `NotificationPolicySchema` is narrowed to `{ suppressDeEscalations, flappingTrigger }`. Stored rows that still carry the removed legacy keys parse cleanly - zod strips the unknown keys on read - so no data migration is required for the `system_health_checks.notification_policy` column. GitOps `notificationPolicy` specs that set the removed fields are no longer accepted for those keys.
172
+
173
+ - 270ef29: Extend in-UI script testing to health-check collectors, and add
174
+ load-from-run replay for automation script tests.
175
+
176
+ - Health-check collectors: a new `testCollectorScript` RPC runs the
177
+ inline-script (TypeScript) collector and the shell `script` collector
178
+ against an editable, auto-seeded sample context using the same
179
+ sandboxed runner the real collector uses. Surfaces beneath the
180
+ collector script fields in the collector editor (both marked
181
+ `x-script-testable`). Gated by `healthcheck.configuration.manage`.
182
+ - Automation replay: a new `getRunScopeForReplay` RPC reconstructs an
183
+ editable test context from a real run (trigger + persisted artifacts,
184
+ plus the durable scope snapshot when the run is still in-flight), and
185
+ the script-test panel gains a "Load from run" picker that seeds the
186
+ sample context from a past run.
187
+
188
+ Note: health-check executions do not persist the script / config /
189
+ check / system that produced a result, so there is no health-check
190
+ replay - auto-seed is the only context source for collector tests. This
191
+ is by design; see the feature plan.
192
+
193
+ - b995afb: Autocomplete the import specifier itself in script editors.
194
+
195
+ Lazy type acquisition only loads a package's types once its name is already in the buffer, so while you were still typing the import specifier (`import {} from "lod"`) there were no suggestions - the lazy-ATA catch-22. Script editors now suggest installed package names directly in import-specifier position; selecting one (e.g. `lodash`) inserts the name, and the existing ATA loop then loads its `@types/lodash` closure so members complete.
196
+
197
+ - `@checkstack/ui`: `CodeEditor`/`TypefoxEditor` gained an injected `importablePackages?: string[]` prop and a dedicated Monaco completion provider (registered once per `typescript`/`javascript` language, scoped to the editor's model, disposed on unmount). It fires ONLY when the cursor is inside an import/require module-specifier string - detected by a new pure, unit-tested helper `importSpecifierCompletionContext(lineUpToCursor)` that handles `from "…"`, bare `import "…"`, `require("…")`, and dynamic `import("…")`, returns the partial specifier + the replace range, and returns null once the string is closed or outside an import. Items are `kind: Module`, insert the bare name without touching the quotes, and coexist with (do not replace) the TS worker's own completions. Trigger characters: `"`, `'`, and `/` (for scoped subpaths); manual invoke (Ctrl+Space) also works. A new pure helper `importablePackageNames` filters a raw manifest name list (excludes `@types/*`, dedupes, sorts).
198
+ - `@checkstack/script-packages-frontend`: `useScriptPackageTypeAcquisition()` now also returns `importablePackages`, derived from the installed manifest (what is actually resolvable at runtime) with `@types/*` companions excluded - you import `lodash`, never `@types/lodash` (the `@types` package still backs the closure types).
199
+ - `@checkstack/automation-frontend` / `@checkstack/healthcheck-frontend`: pass `importablePackages` into `DynamicForm` alongside the existing `acquireTypes` wiring, so both the Run Script action editor and healthcheck collector editors get import-name completion.
200
+
201
+ The completion list is plugin-agnostic in `@checkstack/ui` (the names are injected); it never fires outside import-string positions, so normal completions are unaffected.
202
+
203
+ - b995afb: Fix package IntelliSense in script editors: lazy Automatic Type Acquisition (ATA) with proper `@types/*` resolution.
204
+
205
+ Script editors (automation "Run Script (TypeScript)" and healthcheck collectors) now provide real autocomplete for installed npm packages. Importing a package whose types live in DefinitelyTyped - e.g. `import { debounce } from "lodash"` (lodash ships no own types; `@types/lodash` does) - now yields member completions. Previously no package completions appeared at all.
206
+
207
+ Root cause: the old rollup wrapped each package's raw, multi-file `.d.ts` (with `export =`, `export as namespace`, and triple-slash `/// <reference path>` chains) inside a single `declare module "<name>" { ... }`, which the TypeScript worker silently rejected, and it truncated large type sets (lodash is ~866 KB across ~700 files) at a 256 KB cap.
208
+
209
+ The fix registers the REAL declaration files at their `node_modules/...` virtual paths and lets TypeScript's own NodeJs + `@types` resolution do the work:
210
+
211
+ - `@checkstack/script-packages-backend`: replaced `rollupPackageTypes` with a tree-driven closure extractor (`resolvePackageTypeClosure`). Given a bare specifier, it resolves against the materialized tree - own types via `package.json` `types`/`typings`/`exports` (bundled-types packages like `zod`/`dayjs`), the `@types/<mangled>` companion when it exists (`lodash` -> `@types/lodash`, scoped `@babel/core` -> `@types/babel__core`), or both, or neither (graceful empty, never a throw). It follows `/// <reference path|types>` and relative imports, includes each package's `package.json`, leaves every file UNWRAPPED, and surfaces a `truncated` flag instead of silently capping. Served from a new raw, HTTP-cacheable route `GET /api/script-packages/types/:lockfileHash/:specifier` (`Cache-Control: private, max-age=1y, immutable`), auth-gated by `script-packages.read`.
212
+ - `@checkstack/script-packages-common`: **BREAKING** - replaced the `listPackageTypes` RPC procedure and `PackageTypesSchema { name, version, dts }` with `PackageTypeClosureSchema` (a `{ path, content }` file-map plus `hasOwnTypes`/`hasAtTypes`/`notFound`/`truncated`) served over the cacheable HTTP route. Added a shared `buildTypeAcquisitionPath`/`parseTypeAcquisitionPath` path contract.
213
+ - `@checkstack/ui`: `CodeEditor`/`TypefoxEditor` gained an injected `acquireTypes` resolver + `acquireResetKey`. On debounced buffer change it parses bare `import`/`require` specifiers (pure, unit-tested) and lazily fetches + registers each NEW package's closure via `addExtraLib` at `file:///node_modules/...`, deduped by a shared acquired-set that resets when the install hash changes. Compiler options set `moduleResolution: NodeJs`, `baseUrl: "file:///"`, and `typeRoots` so a bare import resolves to its `@types` companion. The `context` ambient global keeps working unchanged.
214
+ - `@checkstack/script-packages-frontend`: replaced the old `useScriptPackageTypes` (which concatenated the broken `dts`) with `useScriptPackageTypeAcquisition()`, returning the `acquireTypes` resolver (targets the cacheable route, zod-validates the response) and the current `lockfileHash` as `acquireResetKey`.
215
+ - `@checkstack/automation-frontend` / `@checkstack/healthcheck-frontend`: wired the resolver into the Run Script and collector editors.
216
+
217
+ State & scale: the type closure is derived on read from the materialized package tree (no new durable state). The editor's acquired-set is pod-local UI bookkeeping; the route is keyed by the cluster-wide `lockfileHash`, so the browser HTTP cache is correct across pods and only refetches after a new install changes the hash.
218
+
219
+ - 270ef29: Wire up the script-packages RPC router, admin UI, and editor IntelliSense.
220
+
221
+ - `script-packages-backend`: the oRPC router implementing the full
222
+ contract (allowlist CRUD, registry config with encrypted write-only auth
223
+ token, `installNow` via the elected installer, size cap, storage backend
224
+ selection, install state, `getManifest` / `downloadBlob` for reconcilers,
225
+ and `listPackageTypes`), the `installNow` controller (election, size-cap
226
+ enforcement, `script-packages.changed` emit, blocked during migration),
227
+ the `.d.ts` rollup, the singleton config stores, and the full plugin
228
+ wiring (broadcast-hook reconcile + startup backstop).
229
+ - `script-packages-common`: admin route for the settings page.
230
+ - `script-packages-frontend`: the Settings -> Script Packages admin page
231
+ (allowlist, install state + size, registry/storage summary, satellite
232
+ sync) and the `useScriptPackageTypes()` hook.
233
+ - `automation-frontend` / `healthcheck-frontend`: merge installed-package
234
+ `.d.ts` into the script-editor `typeDefinitions` so `import` from an
235
+ allowlisted package autocompletes in every script field.
236
+
237
+ - b995afb: Fix the automation Run Script action's `secretEnv` (secret → env mapping) test wiring and tolerate bare secret names.
238
+
239
+ - `@checkstack/ui` `ScriptTestPanel` now accepts the script field's declared `secretEnv` and renders an optional per-secret test-override input. The `ScriptTestRenderer` callback (DynamicForm) receives the SIBLING `x-secret-env` mapping value, located by annotation (not by field name), so a testable script field forwards it to the panel. Previously the test path never sent `secretEnv`, so `buildTestSecretEnv` got `undefined` and `process.env.<env>` was undefined in an in-UI test. Now an override-less test injects `__SECRET_<NAME>__` placeholders, and any operator override is masked from the output. Real secret values are still NEVER resolved in the test path.
240
+ - `@checkstack/automation-frontend` forwards the action's `secretEnv` and the collected overrides to `testScript`.
241
+ - `@checkstack/secrets-common`: the `secretEnv` mapping VALUE now accepts EITHER a `${{ secrets.NAME }}` template OR a bare secret name, normalizing a bare name to the canonical `${{ secrets.NAME }}` template on parse. This is a forgiving / NARROWING input change (more inputs accepted; stored/output form is unchanged and still the template), not a breaking change. Existing data and YAML shorthand like `secretEnv: { secret: SECRET }` now pass config validation instead of failing with "Must contain a ${{ secrets.NAME }} reference". Partial inline interpolation (e.g. `u:${{ secrets.pw }}@host`) keeps working unchanged; values that are neither a secret reference nor a valid secret name are still rejected.
242
+ - `@checkstack/ui` `parseSecretName` tolerates a legacy bare secret name for display so the picker shows the same name for both the template and the bare form.
243
+
244
+ The healthcheck collector test panel was checked: its config has no `x-secret-env` field, so it needed no secret wiring (only the `onRun` signature change, which is backward compatible).
245
+
246
+ - 270ef29: Secrets platform Phase 2: secret -> env-var mapping with central resolve, inject, and mask.
247
+
248
+ - Script consumers declare a least-privilege `secretEnv` allowlist
249
+ (`{ ENV_NAME: "${{ secrets.NAME }}" }`). The automation `run_script` /
250
+ `run_shell` actions resolve ONLY the declared secrets via
251
+ `secretResolverRef.resolveForRun`, inject them into the runner env for
252
+ that run (memory-only; the ESM runner gained a per-run `env` option), and
253
+ mask their values out of stdout/stderr/result/error via the run-scoped
254
+ masking context. A missing required secret fails the run clearly. No
255
+ ambient secret access.
256
+ - Test panel: `testScript` / `testCollectorScript` inject named
257
+ `__SECRET_<NAME>__` placeholders by default, or user-supplied per-secret
258
+ overrides; real production values are never resolved in the test path,
259
+ and overrides are masked out of the result.
260
+ - Healthcheck collectors carry the `secretEnv` field for authoring +
261
+ the test panel; runtime injection on satellites lands in Phase 3.
262
+ - Editor UX: a new `@checkstack/ui` `SecretEnvEditor` renders `x-secret-env`
263
+ record fields with `${{ secrets.* }}` name autocomplete (from
264
+ `listSecretNames`), wired into the automation action editor and the
265
+ healthcheck collector editor. New `withConfigMeta` helper +
266
+ `x-secret-env` config-meta key in `@checkstack/backend-api`.
267
+
268
+ ### Patch Changes
269
+
270
+ - Updated dependencies [b995afb]
271
+ - Updated dependencies [270ef29]
272
+ - Updated dependencies [270ef29]
273
+ - Updated dependencies [270ef29]
274
+ - Updated dependencies [270ef29]
275
+ - Updated dependencies [b995afb]
276
+ - Updated dependencies [b995afb]
277
+ - Updated dependencies [b995afb]
278
+ - Updated dependencies [b995afb]
279
+ - Updated dependencies [b995afb]
280
+ - Updated dependencies [270ef29]
281
+ - Updated dependencies [270ef29]
282
+ - Updated dependencies [b995afb]
283
+ - Updated dependencies [270ef29]
284
+ - Updated dependencies [270ef29]
285
+ - Updated dependencies [b995afb]
286
+ - Updated dependencies [270ef29]
287
+ - Updated dependencies [270ef29]
288
+ - Updated dependencies [b995afb]
289
+ - Updated dependencies [b995afb]
290
+ - Updated dependencies [b995afb]
291
+ - Updated dependencies [270ef29]
292
+ - Updated dependencies [270ef29]
293
+ - Updated dependencies [b995afb]
294
+ - Updated dependencies [270ef29]
295
+ - Updated dependencies [270ef29]
296
+ - Updated dependencies [270ef29]
297
+ - Updated dependencies [270ef29]
298
+ - Updated dependencies [b995afb]
299
+ - Updated dependencies [b995afb]
300
+ - Updated dependencies [270ef29]
301
+ - Updated dependencies [b995afb]
302
+ - @checkstack/ui@1.12.0
303
+ - @checkstack/healthcheck-common@1.4.0
304
+ - @checkstack/script-packages-frontend@0.2.0
305
+ - @checkstack/satellite-common@0.7.0
306
+ - @checkstack/secrets-frontend@0.1.0
307
+ - @checkstack/auth-frontend@0.6.7
308
+ - @checkstack/dashboard-frontend@0.7.8
309
+ - @checkstack/gitops-frontend@0.4.7
310
+ - @checkstack/tips-frontend@0.2.7
311
+
3
312
  ## 0.21.0
4
313
 
5
314
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,30 +13,33 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/anomaly-common": "1.2.2",
17
- "@checkstack/auth-frontend": "0.6.5",
18
- "@checkstack/catalog-common": "2.2.2",
19
- "@checkstack/common": "0.11.0",
20
- "@checkstack/dashboard-frontend": "0.7.6",
21
- "@checkstack/frontend-api": "0.5.2",
22
- "@checkstack/gitops-frontend": "0.4.5",
23
- "@checkstack/healthcheck-common": "1.2.0",
24
- "@checkstack/satellite-common": "0.5.3",
25
- "@checkstack/signal-frontend": "0.1.4",
26
- "@checkstack/tips-frontend": "0.2.5",
27
- "@checkstack/ui": "1.10.0",
16
+ "@checkstack/anomaly-common": "1.2.3",
17
+ "@checkstack/auth-frontend": "0.6.7",
18
+ "@checkstack/catalog-common": "2.2.3",
19
+ "@checkstack/catalog-frontend": "0.10.7",
20
+ "@checkstack/common": "0.12.0",
21
+ "@checkstack/dashboard-frontend": "0.7.8",
22
+ "@checkstack/frontend-api": "0.6.0",
23
+ "@checkstack/gitops-frontend": "0.4.7",
24
+ "@checkstack/healthcheck-common": "1.4.0",
25
+ "@checkstack/satellite-common": "0.7.0",
26
+ "@checkstack/script-packages-frontend": "0.2.0",
27
+ "@checkstack/secrets-frontend": "0.1.0",
28
+ "@checkstack/signal-frontend": "0.1.5",
29
+ "@checkstack/tips-frontend": "0.2.7",
30
+ "@checkstack/ui": "1.12.0",
28
31
  "ajv": "^8.18.0",
29
32
  "ajv-formats": "^3.0.1",
30
- "date-fns": "^4.1.0",
31
- "lucide-react": "^0.344.0",
32
- "react": "^18.2.0",
33
- "react-router-dom": "^6.20.0",
33
+ "date-fns": "^4.4.0",
34
+ "lucide-react": "^1.17.0",
35
+ "react": "^18.3.1",
36
+ "react-router-dom": "^7.16.0",
34
37
  "recharts": "^3.6.0",
35
38
  "uuid": "^14.0.0",
36
39
  "zod": "^4.2.1"
37
40
  },
38
41
  "devDependencies": {
39
- "@checkstack/scripts": "0.3.3",
42
+ "@checkstack/scripts": "0.3.4",
40
43
  "@checkstack/tsconfig": "0.0.7",
41
44
  "@types/react": "^18.2.0",
42
45
  "typescript": "^5.0.0"
@@ -5,12 +5,21 @@
5
5
  * for all strategies that have schema metadata.
6
6
  */
7
7
 
8
+ import { lazy, Suspense } from "react";
8
9
  import { createSlotExtension } from "@checkstack/frontend-api";
10
+ import { Skeleton } from "@checkstack/ui";
9
11
  import {
10
12
  HealthCheckDiagramSlot,
11
13
  type HealthCheckDiagramSlotContext,
12
14
  } from "../slots";
13
- import { AutoChartGrid } from "./AutoChartGrid";
15
+
16
+ // Lazy-loaded: AutoChartGrid pulls in recharts (~300 KB). This extension is
17
+ // registered eagerly at plugin load, so a static import would ship recharts in
18
+ // the initial bundle even though charts only render inside the (on-demand)
19
+ // HealthCheckDiagramSlot. Deferring it keeps recharts out of the initial load.
20
+ const AutoChartGrid = lazy(() =>
21
+ import("./AutoChartGrid").then((m) => ({ default: m.AutoChartGrid })),
22
+ );
14
23
 
15
24
  /**
16
25
  * Extension that renders auto-generated charts for any strategy.
@@ -22,6 +31,10 @@ import { AutoChartGrid } from "./AutoChartGrid";
22
31
  export const autoChartExtension = createSlotExtension(HealthCheckDiagramSlot, {
23
32
  id: "healthcheck.auto-charts",
24
33
  component: (context: HealthCheckDiagramSlotContext) => {
25
- return <AutoChartGrid context={context} />;
34
+ return (
35
+ <Suspense fallback={<Skeleton className="h-48 w-full" />}>
36
+ <AutoChartGrid context={context} />
37
+ </Suspense>
38
+ );
26
39
  },
27
40
  });
@@ -0,0 +1,60 @@
1
+ import React, { useEffect, useMemo } from "react";
2
+ import type { SlotContext } from "@checkstack/frontend-api";
3
+ import type { CatalogBrowseHealthSlot } from "@checkstack/catalog-common";
4
+ import type { CatalogHealthStatuses } from "@checkstack/catalog-common";
5
+ import {
6
+ SystemBadgeDataProvider,
7
+ useSystemBadgeData,
8
+ } from "@checkstack/dashboard-frontend";
9
+
10
+ type Props = SlotContext<typeof CatalogBrowseHealthSlot>;
11
+
12
+ /**
13
+ * Reads the bulk-fetched health from dashboard-frontend's SystemBadgeDataProvider
14
+ * context and reports the per-system statuses back to the catalog browse view via
15
+ * `onStatuses`. Renders nothing — it is a headless reporter inside the provider.
16
+ *
17
+ * Healthcheck's status enum (`healthy` | `degraded` | `unhealthy`) matches the
18
+ * catalog-owned `CatalogHealthStatus` vocabulary exactly, so the mapping is a
19
+ * direct copy of the resolved statuses; systems with no health are simply omitted
20
+ * (the catalog rollup treats absence as `"unknown"`).
21
+ */
22
+ const HealthStatusReporter: React.FC<Props> = ({ systemIds, onStatuses }) => {
23
+ const { getSystemBadgeData } = useSystemBadgeData();
24
+
25
+ // Stable, serialised status map: `useMemo` keeps the object identity fixed
26
+ // unless `systemIds` or the bulk-data accessor change, so the effect below
27
+ // re-reports only when the resolved statuses actually change.
28
+ const statuses = useMemo<CatalogHealthStatuses>(() => {
29
+ const result: CatalogHealthStatuses = {};
30
+ for (const id of systemIds) {
31
+ const status = getSystemBadgeData(id)?.health?.status;
32
+ if (status) result[id] = status;
33
+ }
34
+ return result;
35
+ }, [systemIds, getSystemBadgeData]);
36
+
37
+ useEffect(() => {
38
+ onStatuses(statuses);
39
+ }, [statuses, onStatuses]);
40
+
41
+ return null;
42
+ };
43
+
44
+ /**
45
+ * Fills the catalog `CatalogBrowseHealthSlot`. Wraps dashboard-frontend's existing
46
+ * `SystemBadgeDataProvider` (already a healthcheck-frontend dependency) over the
47
+ * browse list's system ids to bulk-fetch health, then reports the resolved
48
+ * statuses to catalog. All cross-plugin coupling lives here on the filler side;
49
+ * catalog stays decoupled.
50
+ */
51
+ export const CatalogBrowseHealthFiller: React.FC<Props> = ({
52
+ systemIds,
53
+ onStatuses,
54
+ }) => {
55
+ return (
56
+ <SystemBadgeDataProvider systemIds={systemIds}>
57
+ <HealthStatusReporter systemIds={systemIds} onStatuses={onStatuses} />
58
+ </SystemBadgeDataProvider>
59
+ );
60
+ };
@@ -35,6 +35,14 @@ interface CollectorListProps {
35
35
  loading?: boolean;
36
36
  /** Called when collector form validity changes */
37
37
  onValidChange?: (isValid: boolean) => void;
38
+ /**
39
+ * Sample context used to preview `x-templatable` config fields (e.g. the
40
+ * HTTP URL's `{{ environment.baseUrl }}`). Typically the custom fields of a
41
+ * sample environment the system belongs to, shaped as
42
+ * `{ environment, check, system }`. Omit it and templatable fields render
43
+ * without a preview line.
44
+ */
45
+ templatePreviewContext?: Record<string, unknown>;
38
46
  }
39
47
 
40
48
  /**
@@ -48,6 +56,7 @@ export const CollectorList: React.FC<CollectorListProps> = ({
48
56
  onChange,
49
57
  loading,
50
58
  onValidChange,
59
+ templatePreviewContext,
51
60
  }) => {
52
61
  // Track validity state per collector index
53
62
  const [validityMap, setValidityMap] = useState<Record<number, boolean>>({});
@@ -277,6 +286,7 @@ export const CollectorList: React.FC<CollectorListProps> = ({
277
286
  <DynamicForm
278
287
  schema={collector.configSchema}
279
288
  value={entry.config}
289
+ templatePreviewContext={templatePreviewContext}
280
290
  onChange={(config) =>
281
291
  handleConfigChange(index, config)
282
292
  }
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useCallback, useRef } from "react";
2
- import { Loader2, ExternalLink, Server } from "lucide-react";
2
+ import { ExternalLink, Server, Layers } from "lucide-react";
3
3
  import { Satellite as SatelliteIcon } from "lucide-react";
4
+ import { CatalogApi } from "@checkstack/catalog-common";
4
5
  import {
5
6
  ExtensionSlot,
6
7
  usePluginClient,
@@ -48,6 +49,7 @@ import {
48
49
  AccordionItem,
49
50
  AccordionTrigger,
50
51
  AccordionContent,
52
+ Spinner,
51
53
  } from "@checkstack/ui";
52
54
  import { formatDistanceToNow } from "date-fns";
53
55
  import { useNavigate, Link } from "react-router-dom";
@@ -114,6 +116,18 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
114
116
  }) => {
115
117
  const healthCheckClient = usePluginClient(HealthCheckApi);
116
118
  const satelliteClient = usePluginClient(SatelliteApi);
119
+ const catalogClient = usePluginClient(CatalogApi);
120
+
121
+ // Environments the system belongs to - resolves run.environmentId to a
122
+ // human-readable name in the run-history table (per-environment fan-out).
123
+ const { data: systemEnvironments = [] } =
124
+ catalogClient.getSystemEnvironments.useQuery(
125
+ { systemId },
126
+ { enabled: !!systemId },
127
+ );
128
+ const envNameById = new Map(
129
+ systemEnvironments.map((e) => [e.id, e.name]),
130
+ );
117
131
  const navigate = useNavigate();
118
132
  const accessApi = useApi(accessApiRef);
119
133
  const { allowed: canViewDetails } = accessApi.useAccess(
@@ -339,7 +353,10 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
339
353
  </button>
340
354
  )}
341
355
  {chartFetching && (
342
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground ml-2" />
356
+ <Spinner
357
+ size="sm"
358
+ className="text-muted-foreground ml-2"
359
+ />
343
360
  )}
344
361
  </div>
345
362
 
@@ -453,7 +470,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
453
470
  <span className="text-sm text-muted-foreground flex items-center gap-2">
454
471
  Recent Runs
455
472
  {historyLoading && (
456
- <Loader2 className="h-3 w-3 animate-spin" />
473
+ <Spinner size="sm" className="h-3 w-3" />
457
474
  )}
458
475
  </span>
459
476
  <div className="flex-1 h-px bg-border" />
@@ -475,12 +492,13 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
475
492
  <TableRow>
476
493
  <TableHead className="w-24">Status</TableHead>
477
494
  <TableHead>Time</TableHead>
495
+ <TableHead>Environment</TableHead>
478
496
  <TableHead>Source</TableHead>
479
497
  </TableRow>
480
498
  </TableHeader>
481
499
  <TableBody>
482
500
  {runs.length === 0 && !historyLoading && (
483
- <EmptyRunsTableRow colSpan={3}>
501
+ <EmptyRunsTableRow colSpan={4}>
484
502
  No runs match the{" "}
485
503
  <span className="font-medium">{runsStatusFilter}</span>{" "}
486
504
  filter.
@@ -518,6 +536,19 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
518
536
  addSuffix: true,
519
537
  })}
520
538
  </TableCell>
539
+ <TableCell>
540
+ {run.environmentId ? (
541
+ <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
542
+ <Layers className="h-3 w-3" />
543
+ {envNameById.get(run.environmentId) ??
544
+ run.environmentId}
545
+ </span>
546
+ ) : (
547
+ <span className="text-xs text-muted-foreground">
548
+ None
549
+ </span>
550
+ )}
551
+ </TableCell>
521
552
  <TableCell>
522
553
  {run.sourceId ? (
523
554
  <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">