Skip to content

feat: @stuajnht's feature requests + team-scoped palette, and the mention-rendering bug they uncovered - #496

Merged
enyineer merged 37 commits into
mainfrom
fix/team-scoped-palette-and-team-ux
Jul 30, 2026
Merged

feat: @stuajnht's feature requests + team-scoped palette, and the mention-rendering bug they uncovered#496
enyineer merged 37 commits into
mainfrom
fix/team-scoped-palette-and-team-ux

Conversation

@enyineer

@enyineer enyineer commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Implements the feature requests from @stuajnht, plus the fixes that fell out of reviewing them.

Features (14 requested, all built)

# Feature Notes
1 "Auto" theme option also fixes a live-update bug: the theme was read during render with no matchMedia listener
2 Markdown preview tab + toolbar
3 # mentions between incidents and maintenances see below - the rendering half was broken
4 GitHub release version on About baked in at version time; @checkstack/release is private and absent from an npm install
5 Select/deselect all rules in a category
6 Clone roles
7 Alphabetise rule categories sorted by the RENDERED label, not the key
8 Clone systems / environments shallow: name, description, custom fields
9 HTTP proxy for HTTP checks the proxy becomes the egress policy boundary for that check
10 Preview system custom-field template vars
11 Richer maintenance notifications window + description, UTC and explicitly labelled
12 Maintenance/incident title on the system card
13 Status-coloured timeline dots incidents colour by SEVERITY, maintenance by lifecycle - preserves the "one coloured dimension per row" rule
14 Per-satellite offline threshold + notifications schema column + migration, threaded through every computeStatus reader

Also 14b: a satellite-only check whose satellites are all offline now records a
degraded run instead of going silently unobserved.

The bug worth reading about

Inline mentions never rendered as links, in any surface. react-markdown
blanks any href outside its safe-protocol list before the rehype plugins run,
so checkstack:<type>/<id> reached the anchor renderer as "". The renderer
saw no mention, took the ordinary-link branch, and emitted an <a> with no
href. The label rendered; the link did not. No error, no missing text.

The e2e that appeared to cover it asserted getByRole("link", …).first(),
which matched the Referenced items chip - a plain router link that renders
whether or not the inline mention works. It validated the wrong element and
passed throughout.

Fixing it needed both filters widened (urlTransform and the sanitizer's
protocol list); either alone still drops the href. javascript:/data: stay
blocked.

Mention resolution now differs per context

Surface Resolves to Gate
Admin UI in-app route the owning plugin confirms this viewer may READ the target
Public status page that page's public detail route the page itself publishes the target
Notification bodies nothing - flattened to the label no per-recipient context exists

Backed by new resolveIncidentRefs / resolveMaintenanceRefs, which return
ids only (so an unreadable record is indistinguishable from a deleted one)
and carry the same listKey read post-filter as their list procedures. They are
deliberately not a filter over the authoring search list, which hides resolved
incidents and would silently drop valid references.

Public resolution reuses the exact gate the public detail pages already apply,
so a mention can never lead somewhere the page would refuse to render. Widgets
opt in via a mentionType they declare themselves, so status-page takes no
dependency on any domain plugin.

A leak this closed: notification bodies were emitting the internal scheme.
Slack sent <checkstack:maintenance/9f1c-abc|Database upgrade> straight to
recipients; email left a dead anchor; Discord/Telegram/Teams would have passed
it through too.

Behavioural change

The in-app public status page (/statuspage/view/<slug>) now builds
detail-page hrefs. It previously passed none, so incident and maintenance titles
rendered as plain text there while the same page on a custom domain linked them.
Both now behave identically.

Testing

Four features had shipped on logic-only coverage - the same shape that let
mentions ship inert. Each now has a guard verified to fail when the thing it
guards is broken:

  • HTTP proxy - fetch({ proxy }) had never run. A real proxy server now
    proves the request arrives there, credentials are sent, a 407 is a completed
    request, an unreachable proxy is a transport failure, and an empty templated
    proxy falls back to direct.
  • Timeline dots - the feature was the renderDot forwarding; the colour
    helpers were tested, the pass-through was not.
  • System field preview - SystemPreviewPicker had no render coverage at all.
  • Per-satellite threshold - computeStatus has five call sites and one
    forgetting the per-satellite value silently falls back to the global default,
    making the admin list, entity read and monitor disagree. The design note
    warned about this; nothing enforced it. Now a behavioural drift guard.

E2E flakes: 22 toast assertions across 9 specs each raced their own
auto-dismiss. Replaced with durable outcomes. Two traps found doing it: several
toasts were also the synchronisation barrier absorbing a create round-trip (the
suite had no global expect timeout, so everything fell back to Playwright's
5s default), and one replacement asserted a button that relabels to
"Posting..." on submit - passing before the mutation landed.

Verification: typecheck clean · lint 0 errors · unit 10,175 + 511 pass ·
integration 436 pass / 24 skip · e2e 202 passed, 0 failed · docs index,
SDK, manage-capability and tsconfig-reference guards all in sync.

Migration

core/satellite-backend/drizzle/0004_solid_patriot.sql adds a nullable
offline_threshold_ms. Additive; verified against both a fresh chain and a
populated database at the previous migration.


Earlier on this branch: team-scoped palette + team-access editor

Original description (still accurate for the first three commits)

Addresses 5 of the 6 feedback notes. (The "Manage X / Create X feel duplicated" note was dropped — both commands stay, per feedback that having both is good.)

1. Bug — Command Palette hid create-actions from team members

command-backend filtered commands against the caller's global access rules only, so a user whose team holds a create-capability grant (but no global incident.incident.manage) never saw "Create Incident" / "Create Maintenance" or their shortcuts. The palette hid the actions from exactly the people authorized to run them — the same "global-only gate excludes team-scoped users" class as .claude/rules/rlac.md.

Commands may now declare manageCapability (mirroring routes/nav). filterByAccessRules admits an item on the global rules OR a team grant on the declared type; the backend resolves that per request via hasAnyTypeGrant (with includeCreator, so a would-be creator qualifies before owning an instance) and fails closed.

Also removed useGlobalShortcuts' userAccessRules re-check: it tested global rules only and would have dropped team users' shortcuts — both call sites already defeated it by passing ["*"], so it was dead, misleading code.

2. Team-access editor clarity + safety

  • "Manage" → "Can edit", gear icon removed. It sets the team's grant on this resource, but the label + gear read as "manage the team".
  • Team name is now a link to /teams?team=<id>, opening that team's members dialog. "Go to the team" gets its own affordance instead of being conflated with the checkbox. The Teams page consumes the param once, then clears it.
  • Self-lockout guard: revoking your own team's only edit grant now confirms first — afterwards you could neither change the resource nor restore the permission. Global teams.manage admins are exempt (they can always restore). Extracted as the pure, unit-tested isSelfRevokingChange.
  • Add-member field explained: placeholder "Add a user by name or email" + helper text stating it adds a new member from the directory (not a filter over current members) and that users are only findable after their first sign-in.

Verification

  • typecheck ✓ · lint ✓ · full unit suite 10,315 pass / 0 fail
  • New behavioral tests: filter-access.test.ts (10 cases, pins the palette regression) and selfLockout.test.ts (8 cases)

3. Create an incident / maintenance from the system overview

The system overview listed a system.s incidents and maintenances but offered no way to open one for it - you had to navigate away 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 and clear both params, so a refresh does not 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 that 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 helps most (the same mistake as bug 1).

Editors gain presetSystemIds, and their unsaved-changes baseline accounts for the pre-selection - otherwise opening a pre-scoped form and closing it would falsely prompt to discard.

Verification

  • typecheck OK, lint OK, full unit suite 10,315 pass / 0 fail
  • New behavioral tests: filter-access.test.ts (10 cases, pins the palette regression) and selfLockout.test.ts (8 cases)

Only the "Manage X / Create X feel duplicated" note is intentionally not addressed - per feedback, having both commands is good.

🤖 Generated with Claude Code

https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1

enyineer and others added 2 commits July 26, 2026 04:55
The palette filtered commands by GLOBAL access rules only, so a user whose team
holds a create-capability grant (and no global *.manage rule) never saw "Create
Incident" / "Create Maintenance" or their shortcuts - it hid the actions from
exactly the people authorized to run them.

Commands may now declare `manageCapability`, mirroring the gate routes/nav use.
filterByAccessRules admits an item on the global rules OR a team grant on the
declared type; the backend resolves that per request via hasAnyTypeGrant
(includeCreator, so a would-be creator qualifies before owning an instance) and
fails closed. Incident + maintenance commands declare their types.

useGlobalShortcuts drops its userAccessRules re-check: the server-filtered list
is authoritative, the check tested global rules only, and both call sites already
defeated it by passing ["*"].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
From user feedback on the "Who can change this" editor:

- Relabel the "Manage" checkbox to "Can edit" and drop its gear icon. It sets the
  team's grant on THIS resource, but the wording + gear read as "manage the
  team", so people expected it to open the Teams UI.
- Make the team name a link to /teams?team=<id>, which opens that team's members
  dialog. "Go to the team" now has its own affordance instead of being conflated
  with the grant checkbox. TeamsTab consumes the param once, then clears it.
- Confirm before a user revokes their OWN team's only edit grant: afterwards they
  could neither change the resource nor restore the permission. Global
  teams.manage admins are exempt (they can always restore). The decision is
  extracted as the pure, unit-tested isSelfRevokingChange.
- Explain the add-member field: it adds a NEW member from the directory (not a
  filter over current members), and only users who have signed in at least once
  exist there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a05dcf1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 165 packages
Name Type
@checkstack/backend Minor
@checkstack/about-frontend Minor
@checkstack/auth-frontend Minor
@checkstack/common Minor
@checkstack/ui Minor
@checkstack/theme-frontend Minor
@checkstack/catalog-frontend Minor
@checkstack/command-common Minor
@checkstack/command-backend Minor
@checkstack/command-frontend Minor
@checkstack/incident-backend Minor
@checkstack/maintenance-backend Minor
@checkstack/healthcheck-http-backend Minor
@checkstack/satellite-backend Minor
@checkstack/incident-frontend Minor
@checkstack/maintenance-frontend Minor
@checkstack/frontend-api Minor
@checkstack/healthcheck-frontend Minor
@checkstack/announcement-frontend Minor
@checkstack/incident-common Minor
@checkstack/maintenance-common Minor
@checkstack/status-page-common Minor
@checkstack/status-page-backend Minor
@checkstack/status-page-frontend Minor
@checkstack/notification-common Minor
@checkstack/frontend Minor
@checkstack/ai-backend Patch
@checkstack/satellite-common Minor
@checkstack/satellite-frontend Minor
@checkstack/healthcheck-backend Minor
@checkstack/logstream-frontend Patch
@checkstack/metricstream-frontend Patch
@checkstack/tracestream-frontend Patch
@checkstack/automation-backend Patch
@checkstack/dev-server Patch
@checkstack/logstream-backend Patch
@checkstack/metricstream-backend Patch
@checkstack/secrets-backend-local Patch
@checkstack/secrets-backend-vault Patch
@checkstack/secrets-backend Patch
@checkstack/telemetry-backend Patch
@checkstack/tracestream-backend Patch
@checkstack/automation-frontend Patch
@checkstack/notification-frontend Patch
@checkstack/slo-frontend Patch
@checkstack/telemetry-frontend Patch
@checkstack/tips-frontend Patch
@checkstack/about-common Patch
@checkstack/ai-common Patch
@checkstack/ai-frontend Patch
@checkstack/announcement-backend Patch
@checkstack/announcement-common Patch
@checkstack/anomaly-backend Patch
@checkstack/anomaly-common Patch
@checkstack/anomaly-frontend Patch
@checkstack/api-docs-common Patch
@checkstack/api-docs-frontend Patch
@checkstack/auth-backend Patch
@checkstack/auth-common Patch
@checkstack/automation-common Patch
@checkstack/backend-api Patch
@checkstack/cache-api Patch
@checkstack/cache-backend Patch
@checkstack/cache-common Patch
@checkstack/cache-frontend Patch
@checkstack/catalog-backend Patch
@checkstack/catalog-common Patch
@checkstack/dashboard-frontend Patch
@checkstack/dependency-backend Patch
@checkstack/dependency-common Patch
@checkstack/dependency-frontend Patch
@checkstack/gitops-backend Patch
@checkstack/gitops-common Patch
@checkstack/gitops-frontend Patch
@checkstack/healthcheck-common Patch
@checkstack/healthcheck-execution Patch
@checkstack/infrastructure-common Patch
@checkstack/infrastructure-frontend Patch
@checkstack/integration-backend Patch
@checkstack/integration-common Patch
@checkstack/integration-frontend Patch
@checkstack/k8s-events-common Patch
@checkstack/logstream-common Patch
@checkstack/metricstream-common Patch
@checkstack/notification-backend Patch
@checkstack/pluginmanager-common Patch
@checkstack/pluginmanager-frontend Patch
@checkstack/queue-api Patch
@checkstack/queue-backend Patch
@checkstack/queue-common Patch
@checkstack/queue-frontend Patch
@checkstack/satellite Patch
@checkstack/screenshots Patch
@checkstack/script-packages-backend Patch
@checkstack/script-packages-common Patch
@checkstack/script-packages-frontend Patch
@checkstack/script-packages-store-postgres Patch
@checkstack/script-packages-store-s3 Patch
@checkstack/scripts Patch
@checkstack/sdk Patch
@checkstack/secrets-common Patch
@checkstack/secrets-frontend Patch
@checkstack/signal-backend Patch
@checkstack/signal-common Patch
@checkstack/slo-backend Patch
@checkstack/slo-common Patch
@checkstack/telemetry-common Patch
@checkstack/template-engine Patch
@checkstack/test-utils-backend Patch
@checkstack/theme-backend Patch
@checkstack/theme-common Patch
@checkstack/tips-backend Patch
@checkstack/tips-common Patch
@checkstack/tracestream-common Patch
@checkstack/auth-credential-backend Patch
@checkstack/auth-github-backend Patch
@checkstack/auth-ldap-backend Patch
@checkstack/auth-saml-backend Patch
@checkstack/cache-memory-backend Patch
@checkstack/cache-memory-common Patch
@checkstack/cache-redis-backend Patch
@checkstack/cache-redis-common Patch
@checkstack/collector-hardware-backend Patch
@checkstack/healthcheck-container-backend Patch
@checkstack/healthcheck-container-common Patch
@checkstack/healthcheck-dns-backend Patch
@checkstack/healthcheck-grpc-backend Patch
@checkstack/healthcheck-jenkins-backend Patch
@checkstack/healthcheck-mysql-backend Patch
@checkstack/healthcheck-ping-backend Patch
@checkstack/healthcheck-postgres-backend Patch
@checkstack/healthcheck-rcon-backend Patch
@checkstack/healthcheck-rcon-common Patch
@checkstack/healthcheck-redis-backend Patch
@checkstack/healthcheck-script-backend Patch
@checkstack/healthcheck-snmp-backend Patch
@checkstack/healthcheck-ssh-backend Patch
@checkstack/healthcheck-ssh-common Patch
@checkstack/healthcheck-tcp-backend Patch
@checkstack/healthcheck-tls-backend Patch
@checkstack/integration-jira-backend Patch
@checkstack/integration-jira-common Patch
@checkstack/integration-script-backend Patch
@checkstack/integration-teams-backend Patch
@checkstack/integration-webex-backend Patch
@checkstack/integration-webhook-backend Patch
@checkstack/k8s-events-backend Patch
@checkstack/notification-backstage-backend Patch
@checkstack/notification-discord-backend Patch
@checkstack/notification-gotify-backend Patch
@checkstack/notification-pushover-backend Patch
@checkstack/notification-slack-backend Patch
@checkstack/notification-smtp-backend Patch
@checkstack/notification-teams-backend Patch
@checkstack/notification-telegram-backend Patch
@checkstack/notification-webex-backend Patch
@checkstack/notification-webhook-backend Patch
@checkstack/queue-bullmq-backend Patch
@checkstack/queue-bullmq-common Patch
@checkstack/queue-memory-backend Patch
@checkstack/queue-memory-common Patch
@checkstack/cache-utils Patch
@checkstack/ingest-utils Patch
create-checkstack-plugin Patch
@checkstack/signal-frontend Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ❌ Failed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Security Audit Failures (dependency graph)
🔎 Security Audit (dependency graph, incl. devDependencies)

❌ Fixable vulnerabilities (2) — an upgrade path exists, so these fail the build:
  [MEDIUM] tar 7.5.20 -> fixed in 7.5.21  (GHSA-r292-9mhp-454m)
  [HIGH] brace-expansion 5.0.7 -> fixed in 5.0.8  (CVE-2026-14257)

⚠️  Known unfixed vulnerabilities (0) — surfaced, not gated:
  (none)

How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run bun run audit:security locally to reproduce. Bump the affected direct dependency, or for a transitive package add a documented entry to security/managed-overrides.json plus matching overrides/resolutions in package.json (then bun install). Re-run bun run audit:overrides:check to confirm the override is consistent.

@enyineer The above code quality issues were found in this PR. Please fix them before merging.

The system overview listed a system's incidents/maintenances but gave no way to
open one for it - you had to go to the incidents page and re-pick the system.

Both system panels now offer "Report incident" / "Schedule maintenance", deep
-linking to the editor with the system pre-selected via
?action=create&systemId=<id>. The config pages consume and clear both params.

The action is gated on useProcedureAccess over the CREATE procedure's contract,
so it shows for a global manager AND for someone who manages the system via a
team (create.parent) - the bare global rule would have hidden it from exactly the
team-scoped users it helps most.

Editors gain `presetSystemIds`, and their unsaved-changes baseline accounts for
it so opening a pre-scoped form and closing it does not nag to discard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hccsjzvqr6xfhGicNBjRgB
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ❌ Failed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Security Audit Failures (dependency graph)
🔎 Security Audit (dependency graph, incl. devDependencies)

❌ Fixable vulnerabilities (2) — an upgrade path exists, so these fail the build:
  [MEDIUM] tar 7.5.20 -> fixed in 7.5.21  (GHSA-r292-9mhp-454m)
  [HIGH] brace-expansion 5.0.7 -> fixed in 5.0.8  (CVE-2026-14257)

⚠️  Known unfixed vulnerabilities (0) — surfaced, not gated:
  (none)

How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run bun run audit:security locally to reproduce. Bump the affected direct dependency, or for a transitive package add a documented entry to security/managed-overrides.json plus matching overrides/resolutions in package.json (then bun install). Re-run bun run audit:overrides:check to confirm the override is consistent.

@enyineer The above code quality issues were found in this PR. Please fix them before merging.

enyineer and others added 24 commits July 29, 2026 06:10
Access-rule categories now sort by their rendered label instead of plugin
registration order, at both the category and rule level.

Each category gained Select all / Clear, routed through the same guard the
individual checkboxes use so the bulk action cannot grant a rule the anonymous
role may not hold.

Roles can be cloned. The dialog takes an explicit mode rather than inferring
'editing' from a role being present, which is what made the third state
expressible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A Clone row action opens the editor seeded from the source and saves as a
create.

The clone is deliberately SHALLOW: name (suffixed), description and custom
fields only. Memberships, links, team grants and health-check assignments are
not copied, and the dialog says so - duplicating check assignments would
silently multiply probe volume with every clone.

Gated on CREATE rather than manage of the source, and a GitOps lock on the
source does not block it: the copy is a new, unmanaged record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
With exactly one leading maintenance window or active incident, the system
overview card now shows its title linked to the record instead of the count.
A bare '1' told the reader nothing they could not infer from the card being
there, and forced a second click.

With two or more there is no single thing to name, so the count remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
The theme control is now Light / Dark / Auto. Auto persists 'system' and
follows the operating system.

Fixes two bugs. Auto was a one-way door: the backend, schema and ThemeProvider
had always supported 'system', but both toggles were binary and could only write
light or dark, so touching the control once destroyed the preference with
nothing able to write it back. And Auto did not react: ThemeProvider read
matchMedia during render with no listener, so a live OS switch did not repaint
until something unrelated re-rendered.

Resolution is now a pure function, and a value read from localStorage is
narrowed rather than cast so a hand-edited value cannot reach the html class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
The About page showed only the backend package version, which cannot be matched
to a GitHub release, a Docker tag or a changelog entry - those carry
@checkstack/release's version, which advances on every release while the core
package's does not.

Both are now shown, labelled, with the release version leading and linked to its
tag. It is baked in at version time by a new generate:release-version script
(checked in CI) rather than read at runtime: @checkstack/release is private and
so is absent from node_modules in an npm install, where a relative-path read
would silently fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…fications

A maintenance notification said only that something had been scheduled, which
told a subscriber nothing about what was planned or when - every recipient had
to open the app to learn anything.

The body now carries the scheduled window and the description the operator had
already written. The window renders in UTC with an explicit suffix: the
pipeline has no per-recipient timezone, so a server-local time would be silently
wrong for most subscribers and an unlabelled one unfalsifiable. The description
goes through the same sanitiser as an update message, so authored markdown
survives while control characters and blank-line padding do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A System picker sits beside the environment one, 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 - an environment is no longer required,
since system.metadata.* resolves without one. Both pickers only offer resources
the caller may read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Markdown fields were plain textareas with a hint, so an author found out how
their text rendered only after saving - or, for a notification, after it had
been delivered.

MarkdownEditor adds Write / Preview tabs and a toolbar, 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 as the saved content. A second renderer would be free to
drift, and a preview that disagrees with the real render is worse than none.

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: the component wraps its textarea, so 'required' on it does
nothing - gate submission explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Status-update dots were uniformly grey, so the rail carried no information.

Maintenance dots take the update's own status: maintenance has no severity, so
its lifecycle is the one coloured dimension. Incident dots take the incident's
SEVERITY, keeping status on a neutral pill - incidents carry both an urgency and
a lifecycle, and status-tone.ts gives the hue to the urgency, so colouring both
would put two competing scales on one row. Public status pages now tone the dot
to match the status label already beside it.

An update that changes nothing stays neutral, so a coloured dot always means the
status moved here.

Also fixes the rail: it anchored its left EDGE at left-4, putting its centre at
16.25px while every dot centres at 16px. A new TimelineDot owns the positioning
so the four copies of that maths cannot diverge again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Adds optional proxy URL, username and password, so a network requiring an
outbound proxy can be monitored through the path its users take.

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 we connect to, and
the target is left for the proxy to resolve. A filtering proxy is frequently the
only thing that can resolve the target, 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, a path a proxied request never takes.

The password is a secret field - encrypted, redacted, resolvable as
${{ secrets.NAME }}, delivered to satellites just in time. It is deliberately
NOT templatable: a field marked both secret and templatable is rejected at
plugin load, so the combination would break boot. Tests pin each field's
contract.

A proxy answering with an error is a COMPLETED request: 407 and 502 surface as
an assertable statusCode, not a transport failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…stop silent checks

A satellite going offline was invisible, and so were its checks.

Per-satellite threshold. The 45s global constant becomes a per-satellite
override (2 minutes to 24 hours): tolerance is a property of the link, not the
platform, so a flaky uplink should not force its grace on everything else. The
threshold is carried on every row read by computeStatus, so the entity read, the
admin list and the heartbeat monitor cannot disagree. Additive nullable column.

Connectivity notifications. Satellites are a notification target with a
subscription: a warning when one stops heartbeating, informational when it
returns. A reconnect only notifies if it was actually offline, so a redeploy is
not an event.

BUG FIX: satellite-only checks no longer go silent. A check with includeLocal
false whose satellites were all offline recorded NOTHING, so it displayed its
last known status indefinitely - a dead probe was indistinguishable from a
passing one. The core now records a degraded run. Degraded rather than
unhealthy because the target may be fine; what failed is our ability to observe
it. Liveness that cannot be resolved is treated as executing, so a transient
lookup failure cannot mark the fleet degraded.

Checks also surface staleness, and a RETIRED slice - environment removed or
satellite unassigned - is never stale: warning about something you retired on
purpose trains operators to ignore the badge.

Corrects the user guide, which claimed offline satellites produced failed runs;
they produced nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Typing # in a markdown field opens a picker and inserts a reference.
Referencing another record previously meant pasting a URL, which cannot be right
everywhere: an admin URL is meaningless on a 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 source, parsed unchanged by existing tooling - and only
the href is resolved per 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 update must not become a link
that confirms it exists. A renderer given no resolver links nothing.

Detail pages gained a Referenced items section, derived by scanning the authored
markdown on each render, so nothing is stored twice and an edit that drops a
reference drops it from the list.

The platform owns the contract; each owning plugin registers its own type, so no
plugin imports another. Search only offers records the caller may read.

Scope: wired for the admin UI. Public pages and notification bodies render
mentions as plain text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Generated artefacts for the changes above: the bundled docs index (CI checks it
is in sync with docs/src/content/docs), the lockfile and tsconfig references for
the new workspace dependencies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
New specs for the markdown editor (preview tab, toolbar, # picker, mention
resolution, Referenced items) and catalog cloning. The keyboard test is a
regression guard: trigger detection re-ran on every keyup, including arrow keys
the open picker had already handled, and reset the highlight - so the picker
could not be navigated and Enter always inserted the first suggestion.

Updated theme.spec and announcement.spec for behaviour these changes altered.
The theme spec had been seeding localStorage, which ThemeSynchronizer overwrites
for a signed-in user; it only passed before because the backend default resolved
to the same colour.

Hardened two flakes. The status-page Add button is disabled until a type is
picked, so the real failure was the Select click not landing and the timeout
surfacing one step downstream; the retry only opens the popover when it is not
already open, since the builder's live-preview re-render can leave it open and
blindly clicking would toggle it shut. The team row lags its refetch because the
success toast fires first.

Also updates four rlac specs for a placeholder renamed in an earlier commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
`heartbeat-monitor.it.test.ts` creates its own `satellites` table rather than
running migrations, so a column added to the schema has to be added here too -
`listConnectionLiveness` now selects `offline_threshold_ms` and the query failed
with "column does not exist".

The file's own comment already warned about this trap after `capabilities` did
the same thing. The integration lane is gated behind CHECKSTACK_IT and excluded
from `bun run test`, so this was green locally and would have failed CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…s offline

One offline satellite degrades EVERY check assigned to it in the same tick, and
healthy -> degraded classifies as an escalation, so a satellite hosting 50
checks produced 51 notifications for a single root cause.

persistRunAndReact gains suppressSubscriberNotification, set for the
unobservable-run path. The run, the transition and the health state are still
written - the dashboard and the check's history stay accurate - and only the
per-check alert is withheld. The satellite's own connectivity subscription
already reports the cause once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…orm cache

The health-check executor asks getOnlineSatelliteIds() on every tick of every
satellite-only check, and the read behind it is a full scan of the satellites
table over a cross-plugin RPC. The procedure previously had no live caller, so
this was a newly introduced per-tick cost that scales with the number of such
checks - the pattern .claude/rules/optimization.md calls out.

Cached with a 5s TTL on the SHARED cache, never a pod-local Map: two pods must
not disagree about whether a check is being executed.

A TTL rather than explicit invalidation because liveness is a function of
elapsed time, not of a write - there is no mutation to hang an invalidation off.
5s sits an order of magnitude below the smallest offline threshold the schema
allows, so a cached answer can lag a transition by one tick (which the next tick
corrects) but never span one. A cache failure degrades to the uncached answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Documents that the degraded runs from an offline satellite deliberately do not
alert per check, that a retired slice is never stale, and the liveness cache's
TTL reasoning. Regenerates the bundled docs index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
react-markdown blanks any href outside its safe-protocol list BEFORE the rehype
plugins run, so `checkstack:<type>/<id>` reached the anchor renderer as an
empty string. The renderer then saw no mention, took the ordinary-link branch,
and emitted an <a> with no href - the label rendered, the link did not.

Inline mentions were therefore inert in EVERY context since they shipped: the
admin incident and maintenance detail pages as well as public surfaces. Nothing
threw and no text was missing, so the failure was invisible.

Two filters had to be widened, since either alone still drops the href:
urlTransform now passes the mention scheme through (deferring everything else to
defaultUrlTransform, so javascript:/data: stay blocked), and the sanitizer's
protocol allow-list gains the same scheme.

Markdown.mentions.test.tsx pins the whole path from authored markdown to a
rendered anchor, including that an unresolved mention stays plain text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
useMentionResolution({documents}) collects the references a page is about to
render and asks each owning plugin, in ONE batched request, which of them the
viewer may read. A mention to a deleted or unreadable record now renders as
plain text instead of a link to a not-found page or an access gate.

Fails closed: while the check is in flight, and for any provider that cannot
answer, every mention renders as plain text. The label always shows either way.

The batch is capped at the bound the resolving procedures declare - over it the
request would be rejected wholesale and, because the resolver fails closed,
EVERY mention on the page would silently drop to plain text.

Adds rewriteMentions() for delivery contexts that cannot host a link at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
resolveIncidentRefs / resolveMaintenanceRefs take ids and return only those the
caller may READ, carrying the same listKey post-filter as their list procedures
so they can never be more permissive.

Deliberately NOT a filter over the authoring search list: that list is shaped
for browsing (it hides resolved incidents, and pagination would hide more), so a
reference missing from it is not evidence the caller cannot read it. The IT
tests pin exactly that case.

They return ids and nothing else - no titles, no statuses - so an unreadable
record is indistinguishable from a deleted one. The label already lives in the
authored markdown.

The detail pages and update timelines now resolve through the viewability check
rather than mapping any well-formed reference to a route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
A public page now links a `#` reference when - and only when - the same page
publishes the target, which is exactly the anti-enumeration gate its detail
pages already apply. So "caused by #Database upgrade" becomes a working link,
while a mention of an internal-only incident stays plain text rather than a link
that would confirm it exists.

Widgets opt in by declaring a mentionType, so the status-page packages take no
dependency on any domain plugin. The constant lives in each plugin's *-common
because both halves must agree - drift silently stops public mentions resolving.

The custom-domain allow-list is extracted to public-api-paths.ts and bound to
the contract: a host 404s anything not listed, so an omission breaks that
procedure on customer domains ONLY, invisibly. It is now a compile error.

BREAKING CHANGE (behavioural): the in-app public status page at
/statuspage/view/<slug> now builds detail-page hrefs. It previously passed none,
so item titles rendered as plain text there while the same page on a custom
domain linked them. Both now behave identically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
`checkstack:` is an internal scheme that notification channels do not
understand, and each leaked it differently. Measured against the real
converters, before this change:

  email  -> <a>Database upgrade</a>   (href stripped, dead anchor)
  text   -> Database upgrade          (correct by luck)
  slack  -> <checkstack:maintenance/9f1c-abc|Database upgrade>

Slack showed the internal URI to the recipient; Discord, Telegram and Teams
render markdown natively and would have passed it through too.

sanitizeUpdateMessage now flattens every mention to its label before the body
reaches any channel, so no channel has to know the scheme exists. Flattening
happens BEFORE the length bound, so the excerpt budget is spent on visible text
rather than on an internal URI.

Linking instead would need a per-recipient URL and, to be correct, a
per-recipient permission check inside a fan-out that has neither. The
notification already deep-links to the item it is about.

mention-leak.test.ts asserts across all three converters - a composition test,
because neither the sanitizer's nor the converters' own suites can catch this
alone. The notification tests drive the real notifyAffectedSystems, since a
caller that interpolated the message directly would pass both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
22 assertions across 9 specs waited for a success toast. Toasts auto-dismiss, so
each raced its own display duration: if the round-trip was slow enough that the
first poll landed after the toast had gone, the test failed with no defect
behind it.

17 were pure redundancy - the row appearing, the dialog closing, or the row
disappearing was already asserted on the next line. The rest now assert the
member row, the update form closing, or the mutation's HTTP status.

Two corrections found while doing it:

- The toast was often ALSO the synchronisation barrier absorbing a create
  round-trip. Removing it exposed following assertions to Playwright's 5s
  default, which had never been stressed. There was no global expect timeout at
  all; there is now one of 15s, plus explicit budgets at create sites.
- postUpdate's replacement waited for a button that RELABELS to "Posting..."
  on submit, so it passed the instant the mutation started - strictly weaker
  than the toast. It now asserts the message field, which unmounts only in
  onSuccess.

Retry loops need inner assertions bounded well below their own budget, or the
raised global leaves them ~2 attempts instead of ~6.

Adds coverage for mention rendering on the admin incident AND maintenance detail
pages and on public pages. The admin assertion previously used .first(), which
matched the "Referenced items" chip - a plain router link that renders whether
or not the inline mention works - so it passed while mentions were inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
enyineer and others added 8 commits July 30, 2026 17:44
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 the same shape of coverage. Each guard below was VERIFIED to
fail when the thing it guards is broken.

- HTTP proxy: fetch({proxy}) had NEVER run. Everything around it was tested -
  the URL we build, the SSRF host we guard, the field contracts - but no request
  had gone through a proxy. A real proxy server now proves the request arrives
  there, credentials are sent, a 407 is a COMPLETED request, an unreachable
  proxy IS a transport failure, and an empty templated proxy falls back to a
  direct connection. Removing the proxy option fails 4 of the 6.
- Timeline dots: the feature was StatusUpdateTimeline FORWARDING renderDot; the
  colour helpers were tested but the one-line pass-through was not. Also pins
  the newest-first ordering a dot renderer must not assume away.
- System field preview: SystemPreviewPicker had no render coverage at all. It is
  purely presentational, so there was no excuse.
- Per-satellite offline threshold: computeStatus has five call sites and one
  forgetting the per-satellite value silently falls back to the global default,
  making the admin list, the entity read and the monitor disagree about the same
  satellite. The design note warned about exactly this; nothing enforced it. Now
  a behavioural drift guard, per the project's own rule.

Also covers the public widget feed and Text blocks, which resolve mentions
through context rather than props.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Records what each surface resolves to and what gates it: the admin UI links only
what the viewer may READ, a public page only what that page publishes, and
notification bodies flatten to the label because no per-recipient context
exists.

Replaces the claim that public surfaces render mentions as "plain text - the
safe default", which was untrue for notifications, and the note that the admin
resolver deliberately does not check readability, which it now does.

Regenerates the bundled docs index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
… credential

The new proxy tests wrote an adjacent username/password literal pair and joined
it with a colon to build the expected Basic header. Secret scanners match
exactly that shape, so GitGuardian flagged the PR - a false positive, but a red
security check nobody should have to triage.

Same coverage, named fixtures, and the credential pair is no longer written as a
literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
Two fixable transitive CVEs were failing the dependency-graph gate, and every
typecheck/lint/test/e2e job needs: that gate - so the whole suite was blocked
before any of it ran (this predates the branch's feature work).

  brace-expansion 5.0.7 -> 5.0.8  HIGH    CVE-2026-14257
  tar             7.5.20 -> 7.5.21 MEDIUM  GHSA-r292-9mhp-454m

Pinned via the existing overrides/resolutions blocks - the mechanism already
used here for minimatch, ws, adm-zip and fast-uri.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
CodeQL flagged js/polynomial-redos (HIGH) on the mention link pattern. The label
class allowed a raw `[`, so a run of `[[[[` started a label scan at every
bracket that could only fail at the closing `](` - quadratic in the input
length. The scanned documents are operator-authored update text, so this is
genuinely uncontrolled input.

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 test. A quadratic pattern returns the RIGHT answer, just far
too slowly, so every correctness test kept passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
CodeQL still flagged js/polynomial-redos after the label fix, because the label
was not the ambiguity that mattered. The href class `[^\s)]+` consumed the
whole remaining string on input like `[](checkstack:` repeated, then gave back
one character per failed `)` attempt - O(n) backtracking at O(n) start
positions.

Bounding it to exclude brackets and parens makes each attempt stop at the next
one. A real href is `checkstack:<type>/<id>` with both segments `[\w.-]+`
(SAFE_SEGMENT), so none of those characters is ever valid in one.

Measured on the exact shape CodeQL named: 4x the input costs 2.8x the time
(linear), where quadratic would be ~16x. Pinned by a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
…DoS fix

Two earlier attempts tightened the CHARACTER CLASSES and CodeQL kept flagging
the same lines, because the classes were never the problem. The cost comes from
the UNBOUNDED repetition itself: the label can consume an arbitrarily long run
before discovering there is no `](` after it, and the scan restarts at every
`[`, so an input of '[' followed by many '\\[Z' - exactly the shape CodeQL
named - is O(n) work at O(n) start positions.

Measured A/B on that shape, 4x the input:

  unbounded  n=1000 7.7ms -> n=4000 130.0ms = 16.8x  (quadratic)
  bounded    n=1000 3.0ms -> n=4000  13.9ms =  4.6x  (linear)

The bounds are far above anything real - a label is a record title, an href is
`checkstack:<type>/<id>` with both segments `[\w.-]+`. A mention exceeding
512 characters is simply not listed, the same graceful degradation this
best-effort index already gives malformed input; both directions are pinned by
tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
@gitguardian

gitguardian Bot commented Jul 30, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35578379 Triggered Username Password 478ae64 plugins/healthcheck-http-backend/src/strategy.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@enyineer enyineer changed the title fix(auth/command): team-scoped palette actions + team-access editor clarity feat: @stuajnht's feature requests + team-scoped palette, and the mention-rendering bug they uncovered Jul 30, 2026
Comment thread core/common/src/mention-links.ts Fixed
Comment thread core/common/src/mention-links.ts Fixed
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ❌ Failed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Security Audit Failures (dependency graph)
🔎 Security Audit (dependency graph, incl. devDependencies)

❌ Fixable vulnerabilities (2) — an upgrade path exists, so these fail the build:
  [MEDIUM] tar 7.5.20 -> fixed in 7.5.21  (GHSA-r292-9mhp-454m)
  [HIGH] brace-expansion 5.0.7 -> fixed in 5.0.8  (CVE-2026-14257)

⚠️  Known unfixed vulnerabilities (0) — surfaced, not gated:
  (none)

How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run bun run audit:security locally to reproduce. Bump the affected direct dependency, or for a transitive package add a documented entry to security/managed-overrides.json plus matching overrides/resolutions in package.json (then bun install). Re-run bun run audit:overrides:check to confirm the override is consistent.

@enyineer The above code quality issues were found in this PR. Please fix them before merging.

@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

⚠️ Escalation: Automated fixes have not resolved the issues after 3 attempts. Manual intervention is required.

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ❌ Failed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Security Audit Failures (dependency graph)
🔎 Security Audit (dependency graph, incl. devDependencies)

❌ Fixable vulnerabilities (2) — an upgrade path exists, so these fail the build:
  [MEDIUM] tar 7.5.20 -> fixed in 7.5.21  (GHSA-r292-9mhp-454m)
  [HIGH] brace-expansion 5.0.7 -> fixed in 5.0.8  (CVE-2026-14257)

⚠️  Known unfixed vulnerabilities (0) — surfaced, not gated:
  (none)

How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run bun run audit:security locally to reproduce. Bump the affected direct dependency, or for a transitive package add a documented entry to security/managed-overrides.json plus matching overrides/resolutions in package.json (then bun install). Re-run bun run audit:overrides:check to confirm the override is consistent.

@enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

⚠️ Escalation: Automated fixes have not resolved the issues after 3 attempts. Manual intervention is required.

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ❌ Failed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Security Audit Failures (dependency graph)
🔎 Security Audit (dependency graph, incl. devDependencies)

❌ Fixable vulnerabilities (2) — an upgrade path exists, so these fail the build:
  [MEDIUM] tar 7.5.20 -> fixed in 7.5.21  (GHSA-r292-9mhp-454m)
  [HIGH] brace-expansion 5.0.7 -> fixed in 5.0.8  (CVE-2026-14257)

⚠️  Known unfixed vulnerabilities (0) — surfaced, not gated:
  (none)

How to fix: This gate scans the whole dependency graph (incl. devDependencies) and fails only on findings with an upgrade path (a fixed version exists); unfixed findings are warnings, not gated. Run bun run audit:security locally to reproduce. Bump the affected direct dependency, or for a transitive package add a documented entry to security/managed-overrides.json plus matching overrides/resolutions in package.json (then bun install). Re-run bun run audit:overrides:check to confirm the override is consistent.

@enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required.

enyineer and others added 2 commits July 30, 2026 23:26
CI's Test job failed on four assertions that pass locally:

  TestingLibraryElementError: Found multiple elements with the role "link"
  and name "Database upgrade"

RTL's destructured queries bind to document.body, not to the render's own
container. Several tests in these files render the same accessible name, so the
moment cleanup does not run between them they collide - and whether it runs is a
property of the runner, not of the component under test. Locally it did; on CI
it did not.

Scoping each assertion to its own container with within() removes the
dependency entirely rather than betting on cleanup ordering.

Deliberately NOT applied to SystemPreviewPicker: Radix renders its options in a
PORTAL, outside the container, so container-scoping would make those queries
find nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bdCuhhRWywMnaCMQmjzv1
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR Checks Failed

⚠️ Escalation: Automated fixes have not resolved the issues after 3 attempts. Manual intervention is required.

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ❌ Failed
Integration ✅ Passed
Security (deps) ✅ Passed
Security (container) ✅ Passed
E2E ✅ Passed
❌ Test Failures
... (truncated 16864 lines)

::group::scripts/eslint-rules/no-state-seed-in-effect.test.ts:
(pass) no-state-seed-in-effect > valid > useEffect(() => { if (!open) { setQuery(""); setTab("actions"); } }, [open]); [2.36ms]
(pass) no-state-seed-in-effect > valid > useEffect(() => { if (open) { setValue(""); } }, [open]); [1.61ms]
(pass) no-state-seed-in-effect > valid > useEffect(() => { setName(initialData?.name ?? ""); }, []); [1.67ms]
(pass) no-state-seed-in-effect > valid > useEffect(() => { setName(initialData?.name ?? ""); }); [1.39ms]
(pass) no-state-seed-in-effect > valid > useEffect(() => { const sub = bus.subscribe(fn); setReady(true); return () => sub(); }, [bus]); [1.61ms]
(pass) no-state-seed-in-effect > valid > useSeedFormOnOpen(open, () => { setName(initialData?.name ?? ""); }); [1.14ms]
(pass) no-state-seed-in-effect > valid > useInitOnceForKey(data, data?.id, (d) => setDraft(d)); [1.03ms]
(pass) no-state-seed-in-effect > valid > useEffect(() => { setSelectedIds((prev) => new Set(prune(prev, selectableIds))); }, [selectableIds]); [1.29ms]
(pass) no-state-seed-in-effect > invalid > useEffect(() => { if (open) { setName(initialData?.name ?? ""); setFields(rows(initialData?.metadata)); } }, [open, initialData]); [2.01ms]
(pass) no-state-seed-in-effect > invalid > useEffect(() => { if (data) setDraft(data); }, [data]); [1.45ms]
(pass) no-state-seed-in-effect > invalid > useEffect(() => setDraft(data), [data]); [1.22ms]
(pass) no-state-seed-in-effect > invalid > React.useEffect(() => { setTitle(announcement?.title ?? ""); }, [open, announcement]); [1.56ms]

::endgroup::

::group::scripts/eslint-rules/no-unmanaged-entity-state.test.ts:
(pass) no-unmanaged-entity-state > valid > const h = createHook("notification.delivered"); [3.96ms]
(pass) no-unmanaged-entity-state > valid > const h = createHook("incident.created"); [1.15ms]
(pass) no-unmanaged-entity-state > valid > const h = createHook("auth.user.deleted"); [1.11ms]
(pass) no-unmanaged-entity-state > valid > const h = createHook(buildId()); [1.27ms]
(pass) no-unmanaged-entity-state > valid > db.update(incidents).set({ status: "open" }); [1.20ms]
(pass) no-unmanaged-entity-state > valid > db.update(incidents).set({ status: "open" }); [1.63ms]
(pass) no-unmanaged-entity-state > valid > db.update(incidents).set({ title: "x" }); [1.37ms]
(pass) no-unmanaged-entity-state > valid > cache.update(incidents).set({ status: "open" }); [1.21ms]
(pass) no-unmanaged-entity-state > invalid > const h = createHook("incident.created"); [1.26ms]
(pass) no-unmanaged-entity-state > invalid > const h = createHook("incident.updated"); [1.00ms]
(pass) no-unmanaged-entity-state > invalid > const h = createHook("catalog.system.deleted"); [1.18ms]
(pass) no-unmanaged-entity-state > invalid > const h = createHook("healthcheck.system.health_changed"); [1.08ms]
(pass) no-unmanaged-entity-state > invalid > const h = createHook("incident.resolved"); [1.18ms]
(pass) no-unmanaged-entity-state > invalid > const h = api.createHook("maintenance.updated"); [1.25ms]
(pass) no-unmanaged-entity-state > invalid > db.update(incidents).set({ status: "resolved" }); [1.66ms]
(pass) no-unmanaged-entity-state > invalid > tx.insert(incidents).values({ status: "open", title: "x" }); [1.65ms]

::endgroup::

::group::scripts/eslint-rules/no-direct-role-membership-writes.test.ts:
(pass) no-direct-role-membership-writes > valid > db.select().from(schema.userRole).where(eq(schema.userRole.userId, id)); [2.81ms]
(pass) no-direct-role-membership-writes > valid > db.insert(schema.session).values(row); tx.delete(schema.accessRule).where(c); [1.28ms]
(pass) no-direct-role-membership-writes > valid > myMap.delete(userId); set.delete(roleId); [0.77ms]
(pass) no-direct-role-membership-writes > valid > db.insert(schema.userRole).values(row); [0.81ms]
(pass) no-direct-role-membership-writes > invalid > await db.insert(schema.userRole).values({ userId, roleId }); [1.27ms]
(pass) no-direct-role-membership-writes > invalid > await tx.delete(schema.roleAccessRule).where(eq(schema.roleAccessRule.roleId, id)); [1.26ms]
(pass) no-direct-role-membership-writes > invalid > await db.update(schema.role).set({ name }).where(eq(schema.role.id, id)); [1.49ms]
(pass) no-direct-role-membership-writes > invalid > await db.insert(userRole).values(row); [1.32ms]

::endgroup::

::group::scripts/eslint-rules/prefer-gated-mutation.test.ts:
(pass) prefer-gated-mutation > valid > const m = client.updateAutomation.useGatedMutation({}); [1.39ms]
(pass) prefer-gated-mutation > valid > const q = client.listAutomations.useGatedQuery(); [0.96ms]
(pass) prefer-gated-mutation > valid > const m = useMutation({ mutationFn }); [0.94ms]
(pass) prefer-gated-mutation > valid > const m = foo.useMutation({}); [0.94ms]
(pass) prefer-gated-mutation > valid > const q = client.listAutomations.useQuery(); [0.85ms]
(pass) prefer-gated-mutation > invalid > const m = client.updateAutomation.useMutation({}); [0.89ms]
(pass) prefer-gated-mutation > invalid > const m = api.automation.client.createAutomation.useMutation({}); [1.15ms]
(pass) prefer-gated-mutation > invalid > const m = client.deleteAutomation.useMutation(); [0.79ms]

::endgroup::

::group::scripts/eslint-rules/no-direct-system-status-read.test.ts:
(pass) no-direct-system-status-read > valid > const s = await cache.read(systemId); [2.30ms]
(pass) no-direct-system-status-read > valid > const s = await cache.readBulk(ids); [1.00ms]
(pass) no-direct-system-status-read > valid > os.getSystemHealthStatus.handler(async () => cache.read(id)); [1.12ms]
(pass) no-direct-system-status-read > valid > service.getSystemEnvironmentIds(systemId); [0.85ms]
(pass) no-direct-system-status-read > valid > service.getSystemHealthStatus(systemId); [0.62ms]
(pass) no-direct-system-status-read > invalid > const s = await service.getSystemHealthStatus(systemId); [1.05ms]
(pass) no-direct-system-status-read > invalid > const s = await service.getSystemHealthStatus(systemId, envId); [0.85ms]
(pass) no-direct-system-status-read > invalid > const s = await this.getSystemHealthStatus(systemId); [1.18ms]

::endgroup::

::group::scripts/eslint-rules/no-inline-detail-card-chrome.test.ts:
(pass) no-inline-detail-card-chrome > valid > const el = <DetailCard className="overflow-hidden">x</DetailCard>; [1.55ms]
(pass) no-inline-detail-card-chrome > valid > const cls = cn(detailCardSurface, "overflow-hidden"); [1.03ms]
(pass) no-inline-detail-card-chrome > valid > const el = <div className="rounded-lg border border-border bg-card">x</div>; [0.97ms]
(pass) no-inline-detail-card-chrome > valid > const el = <div className="rounded-md bg-surface-inset p-2">x</div>; [0.92ms]
(pass) no-inline-detail-card-chrome > invalid > const el = <div className="rounded-[var(--d-card-r)] border border-border/70 bg-card">x</div>; [1.12ms]
(pass) no-inline-detail-card-chrome > invalid > const el = <div className="border bg-gradient-to-b from-surface-2 to-surface">x</div>; [1.05ms]
(pass) no-inline-detail-card-chrome > invalid > const SHADOW = "shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_10px_30px_-14px_hsl(var(--foreground)/0.12)]"; [0.91ms]
(pass) no-inline-detail-card-chrome > invalid > const el = <div className={`rounded-[var(--d-card-r)] border ${PANEL_SHADOW}`}>x</div>; [1.36ms]

::endgroup::

::group::scripts/eslint-rules/no-direct-health-run-insert.test.ts:
(pass) no-direct-health-run-insert > valid > db.select().from(schema.healthCheckRuns).where(c); [1.35ms]
(pass) no-direct-health-run-insert > valid > db.insert(schema.healthCheckConfigurations).values(row); [0.95ms]
(pass) no-direct-health-run-insert > valid > map.insert(key, value); [0.79ms]
(pass) no-direct-health-run-insert > valid > tx.insert(schema.healthCheckRuns).values(row); [0.94ms]
(pass) no-direct-health-run-insert > invalid > await tx.insert(schema.healthCheckRuns).values({ systemId }); [1.20ms]
(pass) no-direct-health-run-insert > invalid > await db.insert(healthCheckRuns).values(row); [0.80ms]

::endgroup::

 317 pass
 0 fail
 440 expect() calls
Ran 317 tests across 23 files. [1141.00ms]

How to fix: Run bun test locally to reproduce these failures. Read the failing test assertions carefully and fix the implementation code so the tests pass. Do not modify test expectations unless the test itself is clearly wrong. If a test is testing new functionality you introduced, make sure your implementation satisfies the test's contract.

@enyineer The above code quality issues were found in this PR. Automated fixes have not resolved them after 3 attempts. Manual intervention is required.

@github-actions

Copy link
Copy Markdown
Contributor

✅ All PR Checks Passed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ✅ Passed
Security (container) ✅ Passed
E2E ✅ Passed

@enyineer All quality checks have passed. This PR is ready for your review.

2 similar comments
@github-actions

Copy link
Copy Markdown
Contributor

✅ All PR Checks Passed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ✅ Passed
Security (container) ✅ Passed
E2E ✅ Passed

@enyineer All quality checks have passed. This PR is ready for your review.

@github-actions

Copy link
Copy Markdown
Contributor

✅ All PR Checks Passed

Check Status
Typecheck ✅ Passed
Lint ✅ Passed
Deps ✅ Passed
Test ✅ Passed
Integration ✅ Passed
Security (deps) ✅ Passed
Security (container) ✅ Passed
E2E ✅ Passed

@enyineer All quality checks have passed. This PR is ready for your review.

@enyineer
enyineer merged commit 88f4333 into main Jul 30, 2026
19 of 20 checks passed
@enyineer
enyineer deleted the fix/team-scoped-palette-and-team-ux branch July 30, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants