Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
243c180
fix(command): show palette actions to team-scoped users
enyineer Jul 26, 2026
3802d09
fix(auth-frontend): clarify team-access editor + guard self-lockout
enyineer Jul 26, 2026
303fcd9
feat(incident,maintenance): create from the system overview
enyineer Jul 26, 2026
c3cdc35
feat(auth-frontend): alphabetise, bulk-select and clone roles
enyineer Jul 29, 2026
8d0e924
feat(catalog-frontend): clone systems and environments
enyineer Jul 29, 2026
e061a27
feat(incident,maintenance): name the single item on the system cards
enyineer Jul 29, 2026
125e0f7
feat(ui,theme-frontend): add an Auto theme option and make it react
enyineer Jul 29, 2026
96c6c22
feat(about): show the platform release version
enyineer Jul 29, 2026
27d0581
feat(maintenance-backend): include the window and description in noti…
enyineer Jul 29, 2026
acb7460
feat(healthcheck-frontend): preview system custom fields in the editor
enyineer Jul 29, 2026
d5bc5cc
feat(ui): add a markdown editor with a live preview tab
enyineer Jul 29, 2026
591db97
feat(ui): colour timeline dots and fix the rail they hang from
enyineer Jul 29, 2026
0546363
feat(healthcheck-http-backend): route HTTP checks through a proxy
enyineer Jul 29, 2026
1ae468e
feat(satellite): per-satellite offline threshold, notifications, and …
enyineer Jul 29, 2026
64768e3
feat(mentions): link incidents and maintenances with #
enyineer Jul 29, 2026
7da509b
chore: regenerate docs index, lockfile and project references
enyineer Jul 29, 2026
c8fbe38
test(e2e): cover the new UI and harden two load-dependent flakes
enyineer Jul 29, 2026
932b994
fix(satellite-backend): add the new column to the hand-mirrored IT table
enyineer Jul 29, 2026
33eec2b
fix(healthcheck-backend): do not alert per check when a satellite goe…
enyineer Jul 29, 2026
32ee736
perf(satellite-backend): cache satellite liveness on the shared platf…
enyineer Jul 29, 2026
7fa71cc
docs(satellite): record the alerting and caching behaviour
enyineer Jul 29, 2026
05db979
fix(ui): render cross-entity mentions as links
enyineer Jul 30, 2026
40a6685
feat(frontend-api): viewability-aware mention resolution
enyineer Jul 30, 2026
9b770ad
feat(incident,maintenance): batch readability for mention resolution
enyineer Jul 30, 2026
16c6422
feat(status-page): resolve mentions on public pages
enyineer Jul 30, 2026
900fa0b
fix(notification): stop update messages leaking the mention scheme
enyineer Jul 30, 2026
00daa1e
test(e2e): assert durable outcomes instead of auto-dismissing toasts
enyineer Jul 30, 2026
478ae64
test: cover the features that shipped on logic-only tests
enyineer Jul 30, 2026
166c21c
docs(mentions): document per-context resolution and its gates
enyineer Jul 30, 2026
5fe6e9f
test(healthcheck-http-backend): stop the proxy fixture looking like a…
enyineer Jul 30, 2026
2f82a7a
chore(deps): pin patched brace-expansion and tar
enyineer Jul 30, 2026
8fc193b
Revert "chore(deps): pin patched brace-expansion and tar"
enyineer Jul 30, 2026
5f4228a
fix(common): polynomial ReDoS in mention scanning
enyineer Jul 30, 2026
0cb26ab
fix(common): bound the mention HREF class to finish the ReDoS fix
enyineer Jul 30, 2026
a05dcf1
fix(common): bound the mention pattern's repetitions to finish the Re…
enyineer Jul 30, 2026
3e74e01
Merge remote-tracking branch 'origin/main' into fix/team-scoped-palet…
enyineer Jul 30, 2026
7759c5d
test(ui,status-page): scope mention queries to their own container
enyineer Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/about-release-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@checkstack/backend": minor
"@checkstack/about-frontend": minor
---

Show the platform release version on the About page

The About page showed only `@checkstack/backend`'s package version, which cannot
be matched to a GitHub release, a Docker tag or a changelog entry - those all
carry `@checkstack/release`'s version, which advances on every release while the
core package's does not.

Both are now shown, explicitly labelled, with the release version leading and
linked to its GitHub tag.

The release version is baked in at version time by a new
`generate:release-version` script (checked in CI, mirroring the docs index)
rather than read at runtime: `@checkstack/release` is private and therefore
absent from `node_modules` in an npm install, so a relative-path read would work
in the monorepo and Docker image and silently fail everywhere else.
23 changes: 23 additions & 0 deletions .changeset/auth-role-editor-ergonomics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@checkstack/auth-frontend": minor
"@checkstack/common": minor
---

Role editor: alphabetised categories, bulk select, and role cloning

Access-rule categories in the role dialog are now sorted alphabetically (by their
rendered label, at both the category and the rule level) instead of following
plugin registration order, so a category can be found by scanning rather than by
reading the whole list.

Each category gained **Select all** / **Clear** actions. They respect the same
guards the individual checkboxes do - the anonymous role still cannot be granted
rules no public endpoint uses, and a locked role stays read-only.

Roles can be **cloned**: a new role seeded from an existing one's access rules,
saved as a create. The dialog now takes an explicit `mode` rather than inferring
"editing" from the presence of a role, which is what made the third state
expressible at all.

Adds a shared `buildClonedName` helper to `@checkstack/common` so every clone
affordance in the product produces the same name shape.
23 changes: 23 additions & 0 deletions .changeset/auto-theme-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@checkstack/ui": minor
"@checkstack/theme-frontend": minor
---

Auto theme option, and fix Auto never updating

The theme control is now a three-way Light / Dark / **Auto** selector. Auto
persists `system` and follows the operating system's preference.

This fixes two related bugs:

- **Auto was a one-way door.** The backend, schema and `ThemeProvider` had always
supported `system`, but both toggles were binary and could only ever write
`light` or `dark`. Touching the control once destroyed a user's Auto
preference permanently, with nothing able to write it back.
- **Auto did not react.** `ThemeProvider` read `matchMedia(...).matches` during
render with no listener, so a live OS light/dark switch did not repaint until
something unrelated re-rendered. It now subscribes and repaints immediately.

Theme resolution is extracted into a pure `resolveTheme` (exported from
`@checkstack/ui`), and a value read from `localStorage` is now narrowed rather
than cast - a hand-edited value can no longer put a bogus class on `<html>`.
17 changes: 17 additions & 0 deletions .changeset/catalog-clone-systems-environments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@checkstack/catalog-frontend": minor
---

Clone systems and environments

Systems and environments gained a **Clone** row action, which opens the editor
pre-seeded from the source record and saves as a create.

The clone is deliberately SHALLOW: name (suffixed), description and custom
fields only. Group and environment memberships, tags, contacts, links, team
grants and health-check assignments are NOT copied, and the dialog says so.
Duplicating health-check assignments in particular would silently multiply probe
volume and notification noise with every clone.

Cloning is gated on CREATE, not on manage of the source, and a GitOps lock on
the source does not block it - the copy is a new, unmanaged record.
28 changes: 28 additions & 0 deletions .changeset/command-palette-team-scoped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@checkstack/command-common": minor
"@checkstack/command-backend": minor
"@checkstack/command-frontend": minor
"@checkstack/incident-backend": patch
"@checkstack/maintenance-backend": patch
---

Show command-palette actions to team-scoped users

The palette filtered commands against the caller's GLOBAL access rules only, so a
user whose team holds a create-capability grant - but who holds no global
`incident.incident.manage` / `maintenance.maintenance.manage` rule - never saw
"Create Incident" or "Create Maintenance", nor their keyboard shortcuts. The
palette hid the actions from exactly the people authorized to run them.

Commands can now declare a `manageCapability` (mirroring the gate routes and nav
already use). `filterByAccessRules` shows an item when the caller holds the
global rules OR can create/manage the declared type through a team grant, and the
command backend resolves that per request via `hasAnyTypeGrant` (with
`includeCreator`, so a team member who may CREATE the type qualifies before
owning an instance). It fails closed: an auth error leaves pure global gating.
The incident and maintenance commands declare their types.

`useGlobalShortcuts` no longer takes `userAccessRules` and no longer re-checks
access: the server-filtered list is authoritative. That re-check tested the
global rules only and would have dropped team-scoped users' shortcuts - both call
sites already defeated it by passing `["*"]`.
35 changes: 35 additions & 0 deletions .changeset/cover-untested-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@checkstack/healthcheck-http-backend": patch
"@checkstack/ui": patch
"@checkstack/catalog-frontend": patch
"@checkstack/satellite-backend": patch
---

Cover the features that shipped on logic-only tests

Inline mentions shipped completely inert while ~90 unit tests passed, because
those tests proved the pure functions and nothing proved the render path. Four
features carried exactly the same shape of coverage. Each now has a guard that
was VERIFIED to fail when the thing it guards is broken.

- **HTTP proxy.** `fetch({ proxy })` had never run: every test covered the URL
we build, the SSRF host we guard and the field contracts, but no test routed a
request through an actual proxy. A real proxy server now proves the request
arrives there, that credentials are sent, that a 407 is a COMPLETED request
(not a transport failure), that an unreachable proxy IS a transport failure,
and that an empty templated proxy falls back to a direct connection.
- **Status-coloured timeline dots.** The feature was `StatusUpdateTimeline`
forwarding a caller's `renderDot`; the colour helpers were tested but the
one-line forward was not. Now pinned, including per-item independence and the
newest-first ordering a dot renderer must not assume away.
- **System custom-field preview.** `SystemPreviewPicker` had no render coverage
at all. Now covers the empty case, that the SELECTION is displayed, and that
"No system" reports `null` rather than leaking the internal sentinel.
- **Per-satellite offline threshold.** `computeStatus` is called from five
places and a site that forgets the per-satellite value silently falls back to
the global default, so the admin list, the entity read and the monitor
disagree about the same satellite. A behavioural drift guard now drives the
real reads with a heartbeat stale by the global default but fresh by the
satellite's own threshold - and the shorter-threshold direction too.

Tests only; no runtime behaviour changes.
24 changes: 24 additions & 0 deletions .changeset/create-from-system-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@checkstack/incident-frontend": minor
"@checkstack/maintenance-frontend": minor
---

Report an incident or schedule maintenance straight from a system

The system overview showed a system's incidents and maintenances but offered no
way to open one for it - you had to navigate to the incidents/maintenance page
and re-pick the system by hand.

Both panels now carry an action ("Report incident" / "Schedule maintenance")
that deep-links to the editor with the system already selected, via
`?action=create&systemId=<id>`. The pages consume both params and clear them, so
a refresh doesn't reopen the dialog.

The action is gated on `useProcedureAccess` over the CREATE procedure's
contract, so it appears for a global manager AND for someone who can manage this
system through a team (the `create.parent` gate) - exactly who the backend
accepts. Gating on the bare global rule would have hidden it from the
team-scoped users it is most useful to.

The editors' unsaved-changes baseline accounts for the pre-selection, so opening
a pre-scoped form and closing it again doesn't falsely prompt to discard.
46 changes: 46 additions & 0 deletions .changeset/cross-entity-mentions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
"@checkstack/common": minor
"@checkstack/frontend-api": minor
"@checkstack/ui": minor
"@checkstack/incident-frontend": minor
"@checkstack/maintenance-frontend": minor
---

Link incidents and maintenances with `#` mentions

Typing `#` in a markdown field now opens a picker over every mentionable record
and inserts a reference. Referencing another record previously meant pasting a
URL, which cannot be right everywhere: an admin URL is meaningless on a public
status page, a status-page URL is meaningless in the admin UI, and neither works
in an email.

A mention therefore stores WHAT it points at, never where:
`[Database upgrade](checkstack:maintenance/<id>)`. That is an ordinary markdown
link - readable in the raw source, parsed unchanged by existing tooling - and
only the href is resolved per render context.

Resolution may REFUSE: a resolver returning nothing renders the label as plain
text rather than a link. That is a confidentiality property, not a nicety - an
internal-only incident referenced from a public status update must not become a
link that confirms it exists. A renderer given no resolver links nothing.

Incident and maintenance detail pages gained a **Referenced items** section,
derived by scanning the authored markdown on each render. Nothing is stored
twice, so an edit that drops a reference drops it from the list too.

The platform owns the contract (`registerMentionRoutes` / `setMentionSearch` in
`@checkstack/frontend-api`); each owning plugin registers its own type, so no
plugin imports another. Search only ever offers records the caller may read.

Scope: resolution is wired for the admin UI. Public status pages and notification
bodies do not resolve mentions yet, so a mention renders there as plain text -
the safe default above, not a broken link.

Precisely: the admin resolver maps a well-formed reference to a route WITHOUT
checking that the target still exists or that this viewer may read it, so a
mention to a deleted or unreadable record links to a not-found or an access gate.
That is deliberate - gating on the provider's fetched list would silently
downgrade valid references to plain text (the incident search excludes resolved
incidents by default), and silently dropping a valid link is worse than one that
lands on a gate the backend already enforces. The confidentiality property is
carried by the public renderers, which resolve nothing.
31 changes: 31 additions & 0 deletions .changeset/fix-inline-mention-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@checkstack/ui": minor
---

Fix cross-entity mentions rendering as dead text everywhere

Inline `#` mentions never became links - not in the admin UI, not on incident or
maintenance detail pages, not anywhere. `react-markdown` blanks any `href` whose
protocol is outside its safe list BEFORE the rehype plugins run, so
`checkstack:maintenance/<id>` reached the anchor renderer as `""`. The renderer
then saw no mention, took the ordinary-link branch, and emitted an `<a>` with no
href.

The failure was invisible: the label still rendered, nothing threw, and the page
looked correct - only the link was missing. Two filters had to be widened, since
either one alone still drops the href:

- `urlTransform` now passes the mention scheme through and defers everything
else to `defaultUrlTransform`, so `javascript:`/`data:` stay blocked.
- The sanitizer's URL-protocol allow-list gains the same scheme, so it does not
strip the href immediately afterwards.

The mention href is never emitted to the DOM either way: the anchor renderer
replaces it with a resolved in-app URL or renders plain text.

`Markdown.mentions.test.tsx` pins the whole path from authored markdown to a
rendered anchor, including that an unresolved mention stays plain text and that
`javascript:` is still refused. The e2e that appeared to cover this asserted
`getByRole("link", …).first()`, which matched the "Referenced items" chip - a
plain router link that renders whether or not the inline mention works - so it
passed throughout. It now asserts both links and their hrefs.
18 changes: 18 additions & 0 deletions .changeset/fix-mention-redos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@checkstack/common": patch
---

Fix polynomial ReDoS in mention scanning

The mention link pattern allowed a raw `[` inside the label, so a run of `[[[[`
started a label scan at every bracket that could only fail at the closing `](` -
quadratic in the input length (CodeQL `js/polynomial-redos`, HIGH). The
documents scanned are operator-authored incident and maintenance update text, so
the input is genuinely uncontrolled.

Excluding a raw `[` costs nothing: `buildMentionMarkdown` escapes brackets, so a
legitimate mention never contains one - a bracketed title arrives as `\[`, which
the escape branch already matches.

Guarded by a timing regression test. A quadratic pattern still returns the right
answer, just far too slowly, so no correctness test could have caught this.
19 changes: 19 additions & 0 deletions .changeset/healthcheck-system-template-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@checkstack/catalog-frontend": minor
"@checkstack/healthcheck-frontend": minor
---

Preview system custom fields in the health-check editor

The editor gained a **System** picker beside the existing "Preview as"
environment picker, so `{{ system.metadata.<key> }}` resolves in the preview line
and offers `{{ }}` autocomplete.

Previously system templating could only be previewed when the editor happened to
be opened FROM a system: a shared-config authoring flow and every edit-mode
session got no preview and no completions at all, because the systems list was
not even fetched in edit mode.

Selecting only a system is now enough to preview - an environment is no longer
required, since `system.metadata.*` is fully resolvable without one. Both pickers
only offer resources the caller may read.
40 changes: 40 additions & 0 deletions .changeset/http-healthcheck-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@checkstack/healthcheck-http-backend": minor
---

Route HTTP health checks through a proxy

HTTP checks gained optional **Proxy URL**, username and password settings, so a
network that requires an outbound proxy (a filtering proxy, an audited egress
gateway) can be monitored through the same path its users take. The proxy URL is
templatable, so one check can use a different proxy per environment, and proxy
credentials are stored as secrets.

Two deliberate consequences, both documented in the field description:

- **The proxy becomes the egress policy boundary for that check.** The SSRF
denylist is applied to the PROXY host, because that is the only host Checkstack
connects to, and the target is left for the proxy to resolve. A filtering proxy
is frequently the only thing that CAN resolve the target (split-horizon DNS),
so pre-resolving locally would reject valid checks while proving nothing about
the real egress. A proxy pointed at a denied range is still refused.
- **Connect/TLS timings are omitted for proxied checks.** The probe that measures
them opens a raw socket to the resolved target, which is a path a proxied
request never takes; missing data is honest, a direct-connection timing
reported for a proxied request is not.

A proxy that answers with an error is a COMPLETED request: 407 and 502 surface as
an assertable `statusCode`, not as a transport failure. Only failing to reach the
proxy at all is a transport failure.

**Credentials.** The proxy password is a secret field - encrypted at rest,
redacted in the UI, resolvable as `${{ secrets.NAME }}`, and delivered to a
satellite just in time per run. It is deliberately NOT `{{ }}`-templatable:
secret and template fields are resolved in separate ordered passes and marking a
field both is rejected at plugin load. So the proxy URL can vary per environment
while the credential cannot - documented, and pinned by tests so the
boot-breaking combination cannot be introduced.

**An empty rendered proxy URL means no proxy**, and the target is guarded as
usual. Worth knowing when templating a mandatory proxy: an environment missing
the field degrades to a direct connection rather than to an error.
18 changes: 18 additions & 0 deletions .changeset/maintenance-notification-detail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@checkstack/maintenance-backend": minor
---

Include the window and description in maintenance notifications

A maintenance notification said only `Maintenance "<title>" has been scheduled`,
which told a subscriber nothing about WHAT was planned or WHEN - every recipient
had to open the app to learn anything at all.

The body now carries the scheduled window and the maintenance description, both
of which the operator had already written.

The window renders in **UTC with an explicit suffix**: the notification pipeline
has no per-recipient timezone, so a server-local time would be silently wrong for
most subscribers and an unlabelled one would be unfalsifiable. The description is
normalised through the same sanitiser as update messages, so authored markdown
survives while control characters and blank-line padding do not.
28 changes: 28 additions & 0 deletions .changeset/markdown-editor-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@checkstack/ui": minor
"@checkstack/incident-frontend": minor
"@checkstack/maintenance-frontend": minor
"@checkstack/announcement-frontend": minor
---

Markdown editor with a live preview tab and formatting toolbar

Markdown fields were plain textareas with a "Markdown supported" hint, so an
author found out how their text rendered only after saving - or, for a
notification, after it had already been delivered.

New `MarkdownEditor` in `@checkstack/ui`: Write / Preview tabs plus a toolbar
(bold, italic, link, code, lists, quote). Adopted by the incident and maintenance
update forms and descriptions, and the announcement message.

The preview renders through `MarkdownBlock` - the same component, remark/rehype
chain and sanitiser used for the saved content. A second renderer here would be
free to drift, and a preview that disagrees with the real render is worse than no
preview.

Toolbar marks toggle rather than only adding, and mark lengths are matched
exactly so italic (`*`) never claims bold's (`**`) delimiters and silently
downgrades an author's emphasis.

Note for adopters: `MarkdownEditor` wraps its textarea, so `required` on it does
nothing - gate submission explicitly instead.
Loading
Loading