@checkstack/healthcheck-frontend 0.20.0 → 0.22.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,249 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b995afb: Move health-check flapping configuration from the per-assignment notification policy onto the `healthcheck.flapping_detected` automation trigger.
8
+
9
+ 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:
10
+
11
+ - 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.
12
+ - A missing or partial flapping trigger config defaults to `{ transitions: 3, windowMinutes: 60 }`, so automations created before the trigger carried config keep working unchanged.
13
+ - `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.
14
+
15
+ **BREAKING CHANGES**
16
+
17
+ - 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.
18
+ - 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.
19
+ - 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.
20
+
21
+ - b995afb: Remove the legacy per-assignment auto-incident system. Auto-incidents are now built entirely by user-authored automations; nothing is seeded or hardcoded.
22
+
23
+ What was removed:
24
+
25
+ - 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.
26
+ - 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.
27
+ - The dead `health_check_auto_incidents` table (no longer written or read; dropped via migration).
28
+
29
+ What is preserved: flapping detection (`healthcheck.flapping_detected`) and de-escalation suppression are unchanged. The `flappingTrigger` and `suppressDeEscalations` policy fields stay exactly as before.
30
+
31
+ > [!NOTE]
32
+ > 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.
33
+
34
+ > [!IMPORTANT]
35
+ > 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.
36
+
37
+ - 270ef29: Extend in-UI script testing to health-check collectors, and add
38
+ load-from-run replay for automation script tests.
39
+
40
+ - Health-check collectors: a new `testCollectorScript` RPC runs the
41
+ inline-script (TypeScript) collector and the shell `script` collector
42
+ against an editable, auto-seeded sample context using the same
43
+ sandboxed runner the real collector uses. Surfaces beneath the
44
+ collector script fields in the collector editor (both marked
45
+ `x-script-testable`). Gated by `healthcheck.configuration.manage`.
46
+ - Automation replay: a new `getRunScopeForReplay` RPC reconstructs an
47
+ editable test context from a real run (trigger + persisted artifacts,
48
+ plus the durable scope snapshot when the run is still in-flight), and
49
+ the script-test panel gains a "Load from run" picker that seeds the
50
+ sample context from a past run.
51
+
52
+ Note: health-check executions do not persist the script / config /
53
+ check / system that produced a result, so there is no health-check
54
+ replay - auto-seed is the only context source for collector tests. This
55
+ is by design; see the feature plan.
56
+
57
+ - b995afb: Autocomplete the import specifier itself in script editors.
58
+
59
+ 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.
60
+
61
+ - `@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).
62
+ - `@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).
63
+ - `@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.
64
+
65
+ 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.
66
+
67
+ - b995afb: Fix package IntelliSense in script editors: lazy Automatic Type Acquisition (ATA) with proper `@types/*` resolution.
68
+
69
+ 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.
70
+
71
+ 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.
72
+
73
+ The fix registers the REAL declaration files at their `node_modules/...` virtual paths and lets TypeScript's own NodeJs + `@types` resolution do the work:
74
+
75
+ - `@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`.
76
+ - `@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.
77
+ - `@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.
78
+ - `@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`.
79
+ - `@checkstack/automation-frontend` / `@checkstack/healthcheck-frontend`: wired the resolver into the Run Script and collector editors.
80
+
81
+ 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.
82
+
83
+ - 270ef29: Wire up the script-packages RPC router, admin UI, and editor IntelliSense.
84
+
85
+ - `script-packages-backend`: the oRPC router implementing the full
86
+ contract (allowlist CRUD, registry config with encrypted write-only auth
87
+ token, `installNow` via the elected installer, size cap, storage backend
88
+ selection, install state, `getManifest` / `downloadBlob` for reconcilers,
89
+ and `listPackageTypes`), the `installNow` controller (election, size-cap
90
+ enforcement, `script-packages.changed` emit, blocked during migration),
91
+ the `.d.ts` rollup, the singleton config stores, and the full plugin
92
+ wiring (broadcast-hook reconcile + startup backstop).
93
+ - `script-packages-common`: admin route for the settings page.
94
+ - `script-packages-frontend`: the Settings -> Script Packages admin page
95
+ (allowlist, install state + size, registry/storage summary, satellite
96
+ sync) and the `useScriptPackageTypes()` hook.
97
+ - `automation-frontend` / `healthcheck-frontend`: merge installed-package
98
+ `.d.ts` into the script-editor `typeDefinitions` so `import` from an
99
+ allowlisted package autocompletes in every script field.
100
+
101
+ - b995afb: Fix the automation Run Script action's `secretEnv` (secret → env mapping) test wiring and tolerate bare secret names.
102
+
103
+ - `@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.
104
+ - `@checkstack/automation-frontend` forwards the action's `secretEnv` and the collected overrides to `testScript`.
105
+ - `@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.
106
+ - `@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.
107
+
108
+ 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).
109
+
110
+ - 270ef29: Secrets platform Phase 2: secret -> env-var mapping with central resolve, inject, and mask.
111
+
112
+ - Script consumers declare a least-privilege `secretEnv` allowlist
113
+ (`{ ENV_NAME: "${{ secrets.NAME }}" }`). The automation `run_script` /
114
+ `run_shell` actions resolve ONLY the declared secrets via
115
+ `secretResolverRef.resolveForRun`, inject them into the runner env for
116
+ that run (memory-only; the ESM runner gained a per-run `env` option), and
117
+ mask their values out of stdout/stderr/result/error via the run-scoped
118
+ masking context. A missing required secret fails the run clearly. No
119
+ ambient secret access.
120
+ - Test panel: `testScript` / `testCollectorScript` inject named
121
+ `__SECRET_<NAME>__` placeholders by default, or user-supplied per-secret
122
+ overrides; real production values are never resolved in the test path,
123
+ and overrides are masked out of the result.
124
+ - Healthcheck collectors carry the `secretEnv` field for authoring +
125
+ the test panel; runtime injection on satellites lands in Phase 3.
126
+ - Editor UX: a new `@checkstack/ui` `SecretEnvEditor` renders `x-secret-env`
127
+ record fields with `${{ secrets.* }}` name autocomplete (from
128
+ `listSecretNames`), wired into the automation action editor and the
129
+ healthcheck collector editor. New `withConfigMeta` helper +
130
+ `x-secret-env` config-meta key in `@checkstack/backend-api`.
131
+
132
+ ### Patch Changes
133
+
134
+ - Updated dependencies [b995afb]
135
+ - Updated dependencies [270ef29]
136
+ - Updated dependencies [270ef29]
137
+ - Updated dependencies [270ef29]
138
+ - Updated dependencies [270ef29]
139
+ - Updated dependencies [b995afb]
140
+ - Updated dependencies [b995afb]
141
+ - Updated dependencies [b995afb]
142
+ - Updated dependencies [b995afb]
143
+ - Updated dependencies [b995afb]
144
+ - Updated dependencies [270ef29]
145
+ - Updated dependencies [270ef29]
146
+ - Updated dependencies [b995afb]
147
+ - Updated dependencies [270ef29]
148
+ - Updated dependencies [270ef29]
149
+ - Updated dependencies [b995afb]
150
+ - Updated dependencies [270ef29]
151
+ - Updated dependencies [270ef29]
152
+ - Updated dependencies [b995afb]
153
+ - Updated dependencies [b995afb]
154
+ - Updated dependencies [b995afb]
155
+ - Updated dependencies [270ef29]
156
+ - Updated dependencies [270ef29]
157
+ - Updated dependencies [b995afb]
158
+ - Updated dependencies [270ef29]
159
+ - Updated dependencies [270ef29]
160
+ - Updated dependencies [270ef29]
161
+ - Updated dependencies [270ef29]
162
+ - Updated dependencies [b995afb]
163
+ - Updated dependencies [b995afb]
164
+ - Updated dependencies [270ef29]
165
+ - Updated dependencies [b995afb]
166
+ - @checkstack/ui@1.12.0
167
+ - @checkstack/healthcheck-common@1.4.0
168
+ - @checkstack/script-packages-frontend@0.2.0
169
+ - @checkstack/satellite-common@0.7.0
170
+ - @checkstack/secrets-frontend@0.1.0
171
+ - @checkstack/auth-frontend@0.6.7
172
+ - @checkstack/dashboard-frontend@0.7.8
173
+ - @checkstack/gitops-frontend@0.4.7
174
+ - @checkstack/tips-frontend@0.2.7
175
+
176
+ ## 0.21.0
177
+
178
+ ### Minor Changes
179
+
180
+ - 35bc682: feat(healthcheck): expose check + system run-context to script collectors
181
+
182
+ Script health checks can now read which check and system a run is for.
183
+ Previously shell scripts got only a curated env whitelist and inline
184
+ scripts only `context.config`, so a script had no built-in way to know
185
+ its own check name or the system it was checking.
186
+
187
+ - `@checkstack/backend-api`: new `CollectorRunContext` type
188
+ (`{ check: { id, name, intervalSeconds }, system: { id, name } }`) and
189
+ an optional `runContext` param on `CollectorStrategy.execute`. Optional,
190
+ so existing collector implementations are unaffected.
191
+ - Shell-script collector: injects reserved `CHECKSTACK_CHECK_ID`,
192
+ `CHECKSTACK_CHECK_NAME`, `CHECKSTACK_CHECK_INTERVAL_SECONDS`,
193
+ `CHECKSTACK_SYSTEM_ID`, `CHECKSTACK_SYSTEM_NAME` env vars (user-supplied
194
+ `env` still wins on collision).
195
+ - Inline-script collector: exposes `context.check` and `context.system`
196
+ alongside `context.config`; the inline-script editor now types them for
197
+ autocomplete.
198
+ - Shell editors (health-check collectors and automation shell actions) now
199
+ also suggest the user's own `env` (JSON) keys as `$NAME` completions, via
200
+ the new exported `customShellEnvVars` helper. Keys that aren't valid shell
201
+ identifiers are omitted.
202
+ - Fix: the Typefox `CodeEditor` captured a stale `onChange` at editor start,
203
+ so editing one `DynamicForm` field reverted sibling fields changed since
204
+ mount (e.g. typing in a shell `script` field wiped an unsaved `env` value,
205
+ or deleted a sibling automation action added after mount). The change
206
+ handler now routes through a ref to the current `onChange`.
207
+ - Fix: focusing a JSON editor threw "LanguageStatusService.addStatus is not
208
+ supported" because the standalone service set omitted `ILanguageStatusService`.
209
+ That one service is now registered via `serviceOverrides`.
210
+ - Fix: the automation trigger card nested a `<Badge>` (a `<div>`) inside a
211
+ `<p>`, producing a `validateDOMNesting` warning. Switched the wrapper to a
212
+ `<div>`.
213
+ - Local runs (`queue-executor`) and satellite runs both populate the
214
+ context. `SatelliteAssignment` (and the `getAssignmentsForSatellite`
215
+ RPC output) gained optional `configName` / `systemName` so the metadata
216
+ reaches satellite-side execution; `HealthCheckService` resolves the
217
+ system name via the catalog client.
218
+
219
+ BREAKING CHANGE: `createHealthCheckRouter` now requires a `catalogClient`
220
+ option (used to resolve system names for satellite assignments). Update
221
+ call sites to pass the catalog RPC client.
222
+
223
+ ### Patch Changes
224
+
225
+ - Updated dependencies [e2d6f25]
226
+ - Updated dependencies [41c77f4]
227
+ - Updated dependencies [41c77f4]
228
+ - Updated dependencies [41c77f4]
229
+ - Updated dependencies [41c77f4]
230
+ - Updated dependencies [4832e33]
231
+ - Updated dependencies [6d52276]
232
+ - Updated dependencies [35bc682]
233
+ - Updated dependencies [c39ee69]
234
+ - @checkstack/frontend-api@0.6.0
235
+ - @checkstack/ui@1.11.0
236
+ - @checkstack/common@0.12.0
237
+ - @checkstack/healthcheck-common@1.3.0
238
+ - @checkstack/satellite-common@0.6.0
239
+ - @checkstack/auth-frontend@0.6.6
240
+ - @checkstack/catalog-common@2.2.3
241
+ - @checkstack/dashboard-frontend@0.7.7
242
+ - @checkstack/gitops-frontend@0.4.6
243
+ - @checkstack/tips-frontend@0.2.6
244
+ - @checkstack/anomaly-common@1.2.3
245
+ - @checkstack/signal-frontend@0.1.5
246
+
3
247
  ## 0.20.0
4
248
 
5
249
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,18 +13,20 @@
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.5",
21
- "@checkstack/frontend-api": "0.5.2",
22
- "@checkstack/gitops-frontend": "0.4.5",
23
- "@checkstack/healthcheck-common": "1.1.2",
24
- "@checkstack/satellite-common": "0.5.2",
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.6",
18
+ "@checkstack/catalog-common": "2.2.3",
19
+ "@checkstack/common": "0.12.0",
20
+ "@checkstack/dashboard-frontend": "0.7.7",
21
+ "@checkstack/frontend-api": "0.6.0",
22
+ "@checkstack/gitops-frontend": "0.4.6",
23
+ "@checkstack/healthcheck-common": "1.3.0",
24
+ "@checkstack/satellite-common": "0.6.0",
25
+ "@checkstack/script-packages-frontend": "0.1.0",
26
+ "@checkstack/secrets-frontend": "0.0.1",
27
+ "@checkstack/signal-frontend": "0.1.5",
28
+ "@checkstack/tips-frontend": "0.2.6",
29
+ "@checkstack/ui": "1.11.0",
28
30
  "ajv": "^8.18.0",
29
31
  "ajv-formats": "^3.0.1",
30
32
  "date-fns": "^4.1.0",
@@ -36,7 +38,7 @@
36
38
  "zod": "^4.2.1"
37
39
  },
38
40
  "devDependencies": {
39
- "@checkstack/scripts": "0.3.3",
41
+ "@checkstack/scripts": "0.3.4",
40
42
  "@checkstack/tsconfig": "0.0.7",
41
43
  "@types/react": "^18.2.0",
42
44
  "typescript": "^5.0.0"
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type { NotificationPolicy } from "@checkstack/healthcheck-common";
3
- import { Button, Input, Label, Toggle, Tooltip } from "@checkstack/ui";
3
+ import { Button, Label, Toggle, Tooltip } from "@checkstack/ui";
4
4
 
5
5
  interface NotificationsPanelProps {
6
6
  policy: NotificationPolicy;
@@ -25,6 +25,12 @@ interface NotificationsPanelProps {
25
25
  * Panel for configuring per-association notification behaviour. All
26
26
  * settings are scoped to a single (system, configuration) assignment
27
27
  * — different checks on the same system are independent.
28
+ *
29
+ * Auto-incident opening/closing is no longer configured here: it ships
30
+ * as ordinary user automations. Flapping thresholds likewise moved onto
31
+ * the automation engine's windowed-count gate (the
32
+ * `healthcheck.system_health_changed` trigger's `window` block). What
33
+ * remains is the de-escalation notification preference.
28
34
  */
29
35
  export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
30
36
  policy,
@@ -56,7 +62,7 @@ export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
56
62
  <h3 className="text-sm font-semibold">Notifications</h3>
57
63
  <p className="text-xs text-muted-foreground mt-1">
58
64
  Control which health state transitions notify subscribers for this
59
- check, and when an incident is auto-opened for the system.
65
+ check.
60
66
  </p>
61
67
  </div>
62
68
 
@@ -135,241 +141,6 @@ export const NotificationsPanel: React.FC<NotificationsPanelProps> = ({
135
141
  </div>
136
142
  </div>
137
143
 
138
- {/* Auto-open incident */}
139
- <div className="p-4 bg-muted/50 rounded-lg border space-y-4">
140
- <div className="flex items-start justify-between gap-4">
141
- <div className="flex-1 min-w-0">
142
- <div className="flex items-center gap-2">
143
- <Label className="text-sm font-medium">
144
- Auto-open incident when this check is critical
145
- </Label>
146
- <Tooltip content="When either trigger below fires, an incident is auto-opened on the system. Different checks on the same system are independent — disabling here only affects this check." />
147
- </div>
148
- <p className="text-xs text-muted-foreground mt-1">
149
- One incident per outage instead of one ping per state change.
150
- Especially useful for Jira / Slack / email — the incident's
151
- suppression silences downstream channels for the lifetime of
152
- the incident.
153
- </p>
154
- </div>
155
- <Toggle
156
- checked={policy.autoOpenIncidentOnUnhealthy}
157
- onCheckedChange={(checked: boolean) =>
158
- onChange({ ...policy, autoOpenIncidentOnUnhealthy: checked })
159
- }
160
- disabled={disabled}
161
- aria-label="Auto-open incident when this check is critical"
162
- />
163
- </div>
164
-
165
- {policy.autoOpenIncidentOnUnhealthy && (
166
- <div className="pl-4 border-l-2 border-border space-y-4">
167
- {/* Suppress further notifications */}
168
- <div className="flex items-start justify-between gap-4">
169
- <div className="flex-1 min-w-0">
170
- <Label className="text-sm">
171
- Suppress further notifications while open
172
- </Label>
173
- <p className="text-xs text-muted-foreground mt-1">
174
- Email, Jira, Slack all silenced for this system until the
175
- incident is resolved.
176
- </p>
177
- </div>
178
- <Toggle
179
- checked={policy.useNotificationSuppression}
180
- onCheckedChange={(checked: boolean) =>
181
- onChange({
182
- ...policy,
183
- useNotificationSuppression: checked,
184
- })
185
- }
186
- disabled={disabled}
187
- aria-label="Suppress further notifications while open"
188
- />
189
- </div>
190
-
191
- {/* Skip during maintenance */}
192
- <div className="flex items-start justify-between gap-4">
193
- <div className="flex-1 min-w-0">
194
- <Label className="text-sm">
195
- Skip during active maintenance
196
- </Label>
197
- <p className="text-xs text-muted-foreground mt-1">
198
- No auto-incident is opened while the system has an active
199
- maintenance window with suppression.
200
- </p>
201
- </div>
202
- <Toggle
203
- checked={policy.skipDuringMaintenance}
204
- onCheckedChange={(checked: boolean) =>
205
- onChange({ ...policy, skipDuringMaintenance: checked })
206
- }
207
- disabled={disabled}
208
- aria-label="Skip auto-incident during active maintenance"
209
- />
210
- </div>
211
-
212
- {/* Sustained-duration trigger */}
213
- <div className="space-y-2 pt-2 border-t border-border">
214
- <div className="flex items-center justify-between gap-4">
215
- <div className="flex items-center gap-2">
216
- <Label className="text-sm">
217
- Open when unhealthy continuously
218
- </Label>
219
- <Tooltip content="Catches real outages: the check has stayed unhealthy for at least this long without recovering." />
220
- </div>
221
- <Toggle
222
- checked={policy.sustainedUnhealthyTrigger.enabled}
223
- onCheckedChange={(checked: boolean) =>
224
- onChange({
225
- ...policy,
226
- sustainedUnhealthyTrigger: {
227
- ...policy.sustainedUnhealthyTrigger,
228
- enabled: checked,
229
- },
230
- })
231
- }
232
- disabled={disabled}
233
- aria-label="Enable sustained-unhealthy trigger"
234
- />
235
- </div>
236
- {policy.sustainedUnhealthyTrigger.enabled && (
237
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
238
- <span>Open after</span>
239
- <Input
240
- type="number"
241
- min={1}
242
- className="h-8 w-16 text-center"
243
- value={policy.sustainedUnhealthyTrigger.durationMinutes}
244
- onChange={(e) =>
245
- onChange({
246
- ...policy,
247
- sustainedUnhealthyTrigger: {
248
- ...policy.sustainedUnhealthyTrigger,
249
- durationMinutes:
250
- Number.parseInt(e.target.value, 10) || 1,
251
- },
252
- })
253
- }
254
- disabled={disabled}
255
- />
256
- <span>minutes of continuous unhealthy state</span>
257
- </div>
258
- )}
259
- </div>
260
-
261
- {/* Flapping trigger */}
262
- <div className="space-y-2 pt-2 border-t border-border">
263
- <div className="flex items-center justify-between gap-4">
264
- <div className="flex items-center gap-2">
265
- <Label className="text-sm">Open on flapping</Label>
266
- <Tooltip content="Catches checks that flip in and out of unhealthy too quickly for the sustained trigger to fire." />
267
- </div>
268
- <Toggle
269
- checked={policy.flappingTrigger.enabled}
270
- onCheckedChange={(checked: boolean) =>
271
- onChange({
272
- ...policy,
273
- flappingTrigger: {
274
- ...policy.flappingTrigger,
275
- enabled: checked,
276
- },
277
- })
278
- }
279
- disabled={disabled}
280
- aria-label="Enable flapping trigger"
281
- />
282
- </div>
283
- {policy.flappingTrigger.enabled && (
284
- <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
285
- <span>Open after</span>
286
- <Input
287
- type="number"
288
- min={1}
289
- className="h-8 w-16 text-center"
290
- value={policy.flappingTrigger.transitions}
291
- onChange={(e) =>
292
- onChange({
293
- ...policy,
294
- flappingTrigger: {
295
- ...policy.flappingTrigger,
296
- transitions:
297
- Number.parseInt(e.target.value, 10) || 1,
298
- },
299
- })
300
- }
301
- disabled={disabled}
302
- />
303
- <span>transitions to unhealthy within</span>
304
- <Input
305
- type="number"
306
- min={1}
307
- className="h-8 w-16 text-center"
308
- value={policy.flappingTrigger.windowMinutes}
309
- onChange={(e) =>
310
- onChange({
311
- ...policy,
312
- flappingTrigger: {
313
- ...policy.flappingTrigger,
314
- windowMinutes:
315
- Number.parseInt(e.target.value, 10) || 1,
316
- },
317
- })
318
- }
319
- disabled={disabled}
320
- />
321
- <span>minutes</span>
322
- </div>
323
- )}
324
- </div>
325
-
326
- {/* Auto-close cooldown */}
327
- <div className="space-y-2 pt-2 border-t border-border">
328
- <div className="flex items-center gap-2">
329
- <Label className="text-sm">Auto-close cooldown</Label>
330
- <Tooltip content="Resolve the auto-incident once the system has stayed healthy for this long. Snapshotted per-incident at open time — later policy edits don't change in-flight incidents." />
331
- </div>
332
- <div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
333
- <label className="inline-flex items-center gap-2 cursor-pointer">
334
- <input
335
- type="checkbox"
336
- checked={policy.autoCloseAfterMinutes === null}
337
- onChange={(e) =>
338
- onChange({
339
- ...policy,
340
- autoCloseAfterMinutes: e.target.checked ? null : 30,
341
- })
342
- }
343
- disabled={disabled}
344
- />
345
- <span>Never auto-close (manual resolve only)</span>
346
- </label>
347
- {policy.autoCloseAfterMinutes !== null && (
348
- <div className="flex items-center gap-2">
349
- <span>After</span>
350
- <Input
351
- type="number"
352
- min={1}
353
- className="h-8 w-16 text-center"
354
- value={policy.autoCloseAfterMinutes}
355
- onChange={(e) =>
356
- onChange({
357
- ...policy,
358
- autoCloseAfterMinutes:
359
- Number.parseInt(e.target.value, 10) || 1,
360
- })
361
- }
362
- disabled={disabled}
363
- />
364
- <span>minutes of sustained healthy</span>
365
- </div>
366
- )}
367
- </div>
368
- </div>
369
- </div>
370
- )}
371
- </div>
372
-
373
144
  {/* Save button hides when the assignment is inheriting — there
374
145
  is nothing to save. The Override button drives the transition
375
146
  into edit mode. */}
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import {
4
+ ScriptTestPanel,
5
+ ContextSampleEditor,
6
+ type ScriptTestRenderer,
7
+ type ScriptTestPanelResult,
8
+ } from "@checkstack/ui";
9
+ import { HealthCheckApi } from "@checkstack/healthcheck-common";
10
+ import { extractErrorMessage } from "@checkstack/common";
11
+
12
+ const TIMEOUT_MS = 30_000;
13
+
14
+ /**
15
+ * Placeholder check/system metadata for the auto-seeded sample. A test run
16
+ * has no real check assignment, so we surface stable example values for
17
+ * `context.check` / `context.system` (and the `$CHECKSTACK_*` shell vars).
18
+ */
19
+ const SAMPLE_RUN_CONTEXT = {
20
+ check: { id: "test-check", name: "Test Check", intervalSeconds: 60 },
21
+ system: { id: "test-system", name: "Test System" },
22
+ };
23
+
24
+ /**
25
+ * Build the auto-seeded sample context JSON for a collector script. There
26
+ * is no healthcheck replay (executions don't persist the script/config),
27
+ * so the seed is the collector's live config plus placeholder check/system.
28
+ */
29
+ function seedSample(config: Record<string, unknown>): string {
30
+ return JSON.stringify(
31
+ { config, ...SAMPLE_RUN_CONTEXT },
32
+ null,
33
+ 2,
34
+ );
35
+ }
36
+
37
+ interface CollectorScriptTestPanelProps {
38
+ kind: "typescript" | "shell";
39
+ script: string;
40
+ /** Live collector config, used to auto-seed `context.config`. */
41
+ config: Record<string, unknown>;
42
+ }
43
+
44
+ const CollectorScriptTestPanel: React.FC<CollectorScriptTestPanelProps> = ({
45
+ kind,
46
+ script,
47
+ config,
48
+ }) => {
49
+ const client = usePluginClient(HealthCheckApi);
50
+ const testMutation = client.testCollectorScript.useMutation();
51
+ const [sampleContext, setSampleContext] = React.useState(() =>
52
+ seedSample(config),
53
+ );
54
+
55
+ const handleRun = React.useCallback(async (): Promise<ScriptTestPanelResult> => {
56
+ // Collector config has no `x-secret-env` field, so there are no secret
57
+ // overrides to forward; the panel passes none.
58
+ let parsed: {
59
+ config?: Record<string, unknown>;
60
+ check?: { id: string; name: string; intervalSeconds: number };
61
+ system?: { id: string; name: string };
62
+ } = {};
63
+ if (sampleContext.trim().length > 0) {
64
+ try {
65
+ parsed = JSON.parse(sampleContext) as typeof parsed;
66
+ } catch (error) {
67
+ return {
68
+ stdout: "",
69
+ stderr: "",
70
+ durationMs: 0,
71
+ timedOut: false,
72
+ error: `Sample context is not valid JSON: ${extractErrorMessage(error)}`,
73
+ };
74
+ }
75
+ }
76
+ return testMutation.mutateAsync({
77
+ kind,
78
+ script,
79
+ config: parsed.config,
80
+ runContext: {
81
+ ...(parsed.check ? { check: parsed.check } : {}),
82
+ ...(parsed.system ? { system: parsed.system } : {}),
83
+ },
84
+ timeoutMs: TIMEOUT_MS,
85
+ });
86
+ }, [testMutation, kind, script, sampleContext]);
87
+
88
+ return (
89
+ <ScriptTestPanel
90
+ onRun={handleRun}
91
+ disabled={script.trim().length === 0}
92
+ contextEditor={
93
+ <ContextSampleEditor value={sampleContext} onChange={setSampleContext} />
94
+ }
95
+ />
96
+ );
97
+ };
98
+
99
+ /**
100
+ * Build the {@link ScriptTestRenderer} for the health-check collector
101
+ * editor. Captures the live collector `config` so the auto-seeded sample
102
+ * matches what the operator is editing.
103
+ */
104
+ export function createCollectorScriptTestRenderer(
105
+ config: Record<string, unknown>,
106
+ ): ScriptTestRenderer {
107
+ return ({ fieldId, kind, script }) => (
108
+ <CollectorScriptTestPanel
109
+ key={`${fieldId}-test`}
110
+ kind={kind}
111
+ script={script}
112
+ config={config}
113
+ />
114
+ );
115
+ }
@@ -10,7 +10,10 @@ import {
10
10
  healthcheckScriptContext,
11
11
  } from "@checkstack/ui";
12
12
  import { Trash2 } from "lucide-react";
13
+ import { useScriptPackageTypeAcquisition } from "@checkstack/script-packages-frontend";
14
+ import { useSecretNames } from "@checkstack/secrets-frontend";
13
15
  import { AssertionBuilder, type Assertion } from "../AssertionBuilder";
16
+ import { createCollectorScriptTestRenderer } from "./CollectorScriptTestRenderer";
14
17
 
15
18
  interface CollectorSectionProps {
16
19
  entry: CollectorConfigEntry;
@@ -29,6 +32,18 @@ export const CollectorSection: React.FC<CollectorSectionProps> = ({
29
32
  onValidChange,
30
33
  onRemove,
31
34
  }) => {
35
+ const scriptTestRenderer = React.useMemo(
36
+ () => createCollectorScriptTestRenderer(entry.config),
37
+ [entry.config],
38
+ );
39
+ // Lazy ATA: collector scripts get package IntelliSense (incl. `@types/*`)
40
+ // on demand for whatever npm packages they import. `importablePackages`
41
+ // drives import-specifier name completion before any module is registered.
42
+ const { acquireTypes, acquireResetKey, importablePackages } =
43
+ useScriptPackageTypeAcquisition();
44
+ // Secret names (never values) for the secret -> env mapping editor.
45
+ const { secretNames } = useSecretNames();
46
+
32
47
  return (
33
48
  <div className="space-y-6">
34
49
  {/* Header */}
@@ -63,15 +78,27 @@ export const CollectorSection: React.FC<CollectorSectionProps> = ({
63
78
  Configure how this check item behaves.
64
79
  </p>
65
80
  </div>
66
- <DynamicForm
67
- schema={collectorDef.configSchema}
68
- value={entry.config}
69
- onChange={onConfigChange}
70
- onValidChange={onValidChange}
71
- {...healthcheckScriptContext({
81
+ {(() => {
82
+ const ctx = healthcheckScriptContext({
72
83
  collectorConfigSchema: collectorDef.configSchema,
73
- })}
74
- />
84
+ // Surface the user's own `env` keys as `$`-completions.
85
+ customEnv: entry.config.env,
86
+ });
87
+ return (
88
+ <DynamicForm
89
+ schema={collectorDef.configSchema}
90
+ value={entry.config}
91
+ onChange={onConfigChange}
92
+ onValidChange={onValidChange}
93
+ {...ctx}
94
+ scriptTestRenderer={scriptTestRenderer}
95
+ secretNames={secretNames}
96
+ acquireTypes={acquireTypes}
97
+ acquireResetKey={acquireResetKey}
98
+ importablePackages={importablePackages}
99
+ />
100
+ );
101
+ })()}
75
102
  </div>
76
103
  )}
77
104
 
package/tsconfig.json CHANGED
@@ -31,6 +31,12 @@
31
31
  {
32
32
  "path": "../satellite-common"
33
33
  },
34
+ {
35
+ "path": "../script-packages-frontend"
36
+ },
37
+ {
38
+ "path": "../secrets-frontend"
39
+ },
34
40
  {
35
41
  "path": "../signal-frontend"
36
42
  },