Commit Graph

7947 Commits

Author SHA1 Message Date
James Ritchie 5cde277979 fix(webapp): darken the added-line green in feature flag diffs
green-400 measures 1.78:1 on white, so an added line in the flag diff was barely
there on the light themes. Under Stronger colors it drops to green-700, the first
stop clearing the 4.5:1 that body text needs - green-600 only reaches 3.22:1,
which is a fill's floor, not text's.

Admin-only, and the +/- prefix still carries the meaning either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:46:06 +00:00
James Ritchie 6cfe2aac8b feat(webapp): fill tinted badges solid under Stronger colors
Queue health, log levels, the Badge primitive and the Trigger Agent tones were all
soft tints - the accent at 10% behind the accent as text - and none of them moved
when the preference went on. A tint that low is carrying no contrast of its own,
so the label was doing all the work at whatever ratio the accent happened to have.

They now do what the Resolved error badge already did: fill with the accent and
put white on it, so the chip reads as a solid block rather than a wash. Off, they
keep the tints.

Same treatment across all four, so a chip doesn't change character depending on
which surface it came from. Idle, DEBUG and the neutral tone take charcoal-500,
which is the neutral the rest of the set is drawn against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:46:06 +00:00
James Ritchie 4884327ea7 fix(webapp): lift the neutral run statuses to 3:1 on the light themes
Queued, Delayed, Waiting, Canceled and Expired are pale greys picked to sit
quietly on a white plot, which put all five between 1.32:1 and 1.66:1 - well
under the 3:1 a chart series needs. With Stronger colors on they now take
charcoal-400, the first stop on the ramp that clears it at 3.36:1. charcoal-300
is still 1.98:1, so there was no gentler step available.

All five land on the same stop. That sounds worse than it is: they were already
within 1.05:1 of each other, so there was nothing distinguishable between them to
lose. Telling them apart is a separate question, and it can't be answered without
pushing four of them darker than the floor requires.

The dark themes keep their greys, which already clear 3:1 against a dark plot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:45:49 +00:00
James Ritchie af69dbba58 feat(webapp): name the resolved colour on every swatch in the Colors tables
Each swatch now carries a tooltip with its token (or raw utility class) and what
that resolves to in the column it's sitting in - the point of the layout being
that one token is five different values across a row, and until now you had to
guess which.

A plain `title` rather than the tooltip primitive: there are five swatches per row
across sixty-odd rows, and mounting a few hundred Radix tooltips to name a
variable isn't a fair trade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:45:49 +00:00
James Ritchie 6cc233bbfb fix(webapp): stop the Colors page pulling Prisma into the client bundle
The page imported TaskRunAttemptStatus from @trigger.dev/database as a value, to
derive the attempt-status list. That package wraps the Prisma client, so it can't
load in a browser: the server render resolved it fine, but the route module failed
client-side, which took the whole page's JavaScript with it.

Three symptoms, one cause. Clicking Colors in the storybook sidebar fell back to a
full document load instead of navigating. The measured ratios stayed blank,
because they're read in an effect that never ran. And the theme switcher in the
storybook layout was dead on that page, since a failed route module leaves the
tree unhydrated.

The enum's six members are written out under a type-only import instead, which is
erased at build time. Every other storybook page already imports this package as
types only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:06:07 +00:00
James Ritchie 61b390a15a Revert "feat(webapp): restore seven primitives so the storybook can document them"
This reverts commit 90f04957a0e60c0e4d7a51e2c0b2c1e3f4d8b7a1.

#4654's unused-code cleanup stands as written. Every file it touched is now
byte-identical to main again: the two modules it deleted stay deleted, the five
exports it removed stay removed, and the three it merely unexported
(AlertAction, SheetDescription, SheetFooter) are private again too - that last
part went beyond the reverted commit, since those re-exports came in with the
merge itself.

The storybook stories for all of them are gone with them. This branch keeps less
primitive coverage than it had, which is the cost of not carrying components the
product doesn't call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:52:38 +00:00
James Ritchie 90f04957a1 feat(webapp): restore seven primitives so the storybook can document them
#4654's unused-code pass removed these, correctly by its own measure: each had
no importer at all, and the storybook pages that document them exist only on this
branch, so the pass couldn't see them.

Restored deliberately, because a design-system storybook that silently omits
primitives is worse than carrying a few components the product doesn't currently
call. Two whole modules come back (AnimatingArrow, PrettyDuration) plus five
exports whose surrounding components stayed (SmartDateTime, LiveCountUp,
InputOTPSeparator, PopoverSideMenuTrigger, TableCellChevron), and their stories
along with them.

Every one carries a comment saying why it's kept and pointing here, so the next
unused-code pass doesn't quietly take them out again - knip is satisfied either
way, since a storybook route is an entry point and anything it imports counts as
used.

LabelValueStack is the exception and stays deleted: its only historical consumer
was a test, not a page.

Collateral from reverting those hunks wholesale - useLocalTimeZone,
useUserTimeZone and the PopoverArrowTriggerVariant type - is kept but no longer
exported, since nothing outside their own files uses them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:36:26 +00:00
James Ritchie 8d806321e3 Merge remote-tracking branch 'origin/main' into samejr/appearance-theme-toggle
Eight conflicts, resolved as follows.

themePreference.ts: kept this branch's contrast default of 0 and its two new
preference normalizers, but left the constant unexported as main had it - nothing
imports it, and main's new unused-code gate would reject the export.

user.server.ts: kept the three per-field profile writes; dropped
grantUserCloudAccess, which main deleted as dead code and which has no callers on
either side.

tailwind.css: kept this branch's ramp-travel contrast ramps and added main's new
radio-card tokens on top - surface-hover-subtle on the dark ramp, mirroring
background-hover as it does at contrast 0, and border-selected on the light ramp,
a stop steeper than border-bright so the selected card stays the loudest edge.

SideMenu.tsx: kept the extracted buildSideMenuSections call over main's inline
definition, after checking the two section lists are item-for-item identical.
Main's Webhooks item and Upgrade-to-v4 link live outside the sections array and
merged untouched.

dashboardPreferences.server.ts, storybook.popover, private-connections: took the
union where both sides added imports, and this branch's richer popover story.

LabelValueStack: accepted main's deletion. This branch had added a storybook
section for it, which is gone too - the component was unused in the app, which is
why main removed it.

Main's unused-code pass also deleted exports that only this branch's storybook
pages consumed. Where the code still exists it is re-exported (AlertAction,
SheetDescription, SheetFooter); where main removed a small list, it now lives in
the page that needs it (log levels, attempt statuses); and where the component
itself is gone, the story went with it - AnimatingArrow, PrettyDuration,
SmartDateTime, LiveCountUp, InputOTPSeparator, PopoverSideMenuTrigger,
TableCellChevron.

That same gate flagged exports of this branch's own that nothing imports. They're
now module-local, and AppearanceIcon - added by the first commit here and
superseded by MonitorIcon - is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 09:59:07 +00:00
James Ritchie 7dae4cc9ff fix(webapp): stop the Colors tables repeating every theme inside every theme
The measured tables sit inside an Audit, which renders its children once per
theme - and the table was building the five themes a second time internally. The
result was twenty-five mode blocks per table, each one a full Token / Fill /
Ratio / Verdict set, which made the section unreadable.

The table is back to one mode: token details, then Fill, Ratio and Verdict. The
themes come only from the Audit columns around it. That works without the table
knowing anything about themes, because each column is already a `data-theme`
context and the ratios are measured off the DOM, so the same table reports that
column's answer.

Audit takes a column width now, since a block holding four sub-columns needs more
room than one holding a single sample.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 09:33:07 +00:00
James Ritchie 10a1a0aaf4 fix(webapp): pick the avatar letter colour that reads on its own tile
The initial was always light, which is fine on the darker tiles in the set and
poor on the pale ones - and the pale tiles are the ones the light themes lean on.

It now measures the tile's own colour and picks whichever of the two letter
colours has the better ratio against it, using WCAG relative luminance so the
comparison matches what a contrast checker would report rather than a
gamma-encoded average.

Recovered from an interrupted session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 08:55:04 +00:00
James Ritchie 59b084a8f6 fix(webapp): show Debug in the log level filter, using the same chip as the rows
Debug was missing from the filter list entirely, so there was no way to filter
for it. The dropdown also hand-rolled its own coloured labels, which meant two
places deciding what a level looks like; it now renders the same LogLevel chip
the log rows use, with the label alongside for screen readers, so the two can't
drift.

The level type comes from the presenter rather than being redeclared, which is
what turned up the missing entry.

Recovered from an interrupted session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 08:55:04 +00:00
James Ritchie b3a87a678b docs(server-changes): note the new themes and appearance options
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 08:19:49 +00:00
nicktrn b93904526c test(testcontainers): hoist container boot off the test timer (#4686)
## What

The one-off worker container boot is billed to whichever test resolves
the fixture first. This moves it into a `beforeAll` with its own
timeout.

## Why

vitest runs the fixture chain *inside* the test timer:

```js
// @vitest/runner 4.1.7
setFn(task, withTimeout(...withFixtures(handler)..., timeout, ...))
```

There is no `fixtureTimeout`. So booting Postgres (plus `CREATE
DATABASE`, schema push, ClickHouse and Redis) lands on the first test
and consumes a budget sized for test work.

That is why losing the image pre-pull on fork PRs was fatal rather than
merely slower: the extra ~10s crossed the 60s cap. Since fork time is
roughly internal + 10s and forks exceed 60s, internal runs were already
clearing that cap by under 10s — a latent flake regardless of forks.

## How

`withWarmup` wraps each fixture family and lazily registers a
`beforeAll` on first touch, with its own generous timeout. Registration
is lazy so only files that actually use a family pay for it —
`@internal/testcontainers` is imported by hundreds of test files, many
of which only need Redis. It registers once per file, since `isolate`
gives each file a fresh module registry.

Eight families are wrapped. `isolatedRedisTest`,
`replicationContainerTest` and `postgresAndRedisTest` are deliberately
untouched: they use per-test containers by design, so there is no
one-off boot to hoist.

No test file or CI changes, and it applies to every package using these
fixtures.

## Verification

Proven by mutation. `src/warmup.test.ts` runs container tests under a
deliberately tight cap:

| | Result |
| --- | --- |
| with the warm-up | passes |
| warm-up neutered | fails, `Test timed out` |

It is kept as a regression test — without it, unwrapping a fixture would
break nothing visibly.

`triggerFailedTask.call.test.ts`, one of the five shard casualties,
passes locally in 20.4s.

## Also here

`@internal/testcontainers` had no `test` script, so `turbo run test
--filter "@internal/*"` skipped the package and its existing
`heteroDedicated.test.ts` never ran in CI. Adding the script (matching
the sibling packages') runs both files; verified green through turbo
exactly as CI invokes it.
2026-08-19 08:40:28 +01:00
nicktrn 7529c33a5e ci: correct testcontainer pre-pull image lists (#4685)
## What

Three corrections to the pre-pull lists, each verified against what the
suites actually use.

## Changes

**`ryuk:0.11.0` -> `0.14.0`** in `e2e-webapp.yml` and
`e2e-webapp-auth-full.yml`. The installed testcontainers hardcodes the
image it starts:

```js
// testcontainers@11.14.0 build/reaper/reaper.js
: ImageName.fromString("testcontainers/ryuk:0.14.0").string;
```

So those two lines were pre-pulling an image nothing starts, and the one
actually used was never pre-pulled. The other three workflows already
say 0.14.0.

**`postgres:17` added** to `unit-tests-webapp.yml`. The webapp suite
references `docker.io/postgres:17` across 10 files but only
`postgres:14` was pre-pulled. `unit-tests-internal.yml` already pulls
both.

**Electric pinned to its digest** in `unit-tests-webapp.yml`. The tests
run `electricsql/electric:1.2.4@sha256:20da...` while the pre-pull asked
for the bare tag, so the pre-pull did not necessarily populate the
manifest the tests then request.

## Not changed

The otel collector and s2 images are pulled by other workflows but are
not used by the webapp suite, so they are deliberately not added here.
`postgresAndRedisTest` uses per-test containers by design and needs
nothing pre-pulled.
2026-08-19 08:40:28 +01:00
nicktrn 9de90f7bed ci: pre-pull testcontainer images on fork PRs (#4684)
## What

The `Pre-pull testcontainer images` step is gated on
`env.DOCKERHUB_USERNAME`. Fork PRs receive no repository secrets, so
that variable is empty and the step is skipped along with the DockerHub
login it was grouped with.

## Why

With the pre-pull skipped, testcontainers pulls images lazily — inside
the first test that resolves the fixture, against that test's
`testTimeout`. On PR #4534 that pushed five webapp shards past their 60s
cap across three runs, each failing as `Test timed out in 60000ms` while
42 of 43 files in the shard passed.

Measured cost of the missing pre-pull, comparing the delta from vitest
start to the first container fixture on the same runner class:

| Run | Delta |
| --- | --- |
| internal x2 | +139.9s, +139.4s |
| fork x2 | +149.7s, +149.4s |

A 10.0s penalty, bimodal to within 0.3s.

Note the pulls themselves succeed anonymously — there are no rate-limit
errors in any of the failing logs. Only the login needs credentials, so
the pre-pull can run unconditionally.

## Scope

Removes the `if:` from the pre-pull step in all five workflows that have
one. The DockerHub login stays gated, since it genuinely needs secrets.
2026-08-19 08:40:27 +01:00
Chris Arderne 97461c08af refactor(webapp): remove redundant React fragments (#4683)
## Summary

Remove redundant React fragments from dashboard components, leaving
their rendered output unchanged while simplifying component trees.

Base: [#4682](https://github.com/triggerdotdev/trigger.dev/pull/4682)
2026-08-19 08:29:01 +01:00
Chris Arderne 219bc09d5f perf(webapp): stabilize chart loading line renderer (#4682)
## Summary

Keep the chart loading line renderer stable across parent renders so its
animated SVG paths retain their component identity.

Base: [#4681](https://github.com/triggerdotdev/trigger.dev/pull/4681)
2026-08-19 08:29:01 +01:00
Chris Arderne 1aeb356b9e fix(webapp): preserve React hook order (#4681)
## Summary

Call dashboard hooks unconditionally so components keep a stable hook
order when their props change.

Base: [#4680](https://github.com/triggerdotdev/trigger.dev/pull/4680)
2026-08-19 08:29:00 +01:00
Chris Arderne c3016eb9e4 chore: enable accessibility lint safeguards (#4680)
## Summary

Enable accessibility rules that catch invalid ARIA usage, inaccessible
media, and invalid focus behavior before they reach users.

Base: [#4679](https://github.com/triggerdotdev/trigger.dev/pull/4679)
2026-08-19 08:29:00 +01:00
Chris Arderne 7fca39c91d chore: enable React correctness safeguards (#4679)
## Summary

Enable React correctness rules that catch invalid DOM attributes, unsafe
legacy APIs, and malformed component contracts before they reach users.

Base: [#4678](https://github.com/triggerdotdev/trigger.dev/pull/4678)
2026-08-19 08:28:59 +01:00
Chris Arderne e0d96c3991 perf(webapp): memoize shared context values (#4678)
## Summary

Memoize shared context values so provider renders do not unnecessarily
rerender every consumer. Oxlint now enforces this pattern for the rest
of the dashboard.

Base: [#4677](https://github.com/triggerdotdev/trigger.dev/pull/4677)
2026-08-19 08:28:59 +01:00
Chris Arderne f4320937c5 chore: prefer direct iteration and function callback types (#4677)
## Summary

Enable lint rules that prefer direct iteration and concise function
callback types.

The existing code now uses direct iteration where no index is needed,
and callback contracts use function types consistently.

Base: [#4675](https://github.com/triggerdotdev/trigger.dev/pull/4675)
2026-08-19 08:28:58 +01:00
Chris Arderne 8572e8edbf chore: reject redundant standalone blocks (#4675)
## Summary

Enable the rule that rejects unnecessary standalone blocks.

The existing empty branches are removed so future control flow remains
purposeful.

Base: [#4674](https://github.com/triggerdotdev/trigger.dev/pull/4674)
2026-08-19 08:28:58 +01:00
Chris Arderne b2afff252c chore: enable JSX cleanup rules (#4674)
## Summary

Enable JSX cleanup rules for shorthand fragments and self-closing
components.

The existing JSX is automatically simplified, and future components will
follow the same concise form.

Base: [#4673](https://github.com/triggerdotdev/trigger.dev/pull/4673)
2026-08-19 08:28:57 +01:00
Chris Arderne 0f725cf2ba chore: enable lint cleanup rules (#4673)
## Summary

Enable small cleanup rules for redundant boolean expressions, object
ownership checks, assignments, and object construction.

The existing call sites now use the simpler equivalent forms, keeping
future code consistent without changing behavior.

Base: [#4672](https://github.com/triggerdotdev/trigger.dev/pull/4672)
2026-08-19 08:28:57 +01:00
Chris Arderne fe1d5f6961 chore: enable additional correctness lint rules (#4672)
## Summary

Enable additional lint rules that catch unsafe optional-chain
assertions, inherited-property iteration, anonymous symbols, and unsafe
external links.

The existing violations now use explicit values and own-property checks,
so the rules can prevent those patterns from returning.
2026-08-19 08:28:56 +01:00
nicktrn cffaa05517 feat(supervisor): optional priority class for run pods (#4671)
Adds an optional priority class for run pods.

```
KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME
```

When set, the value is applied as `priorityClassName` on the run pod
spec. When unset, pods are created exactly as before.

Off by default, and inert unless set. It sits beside the existing
`KUBERNETES_SCHEDULER_NAME` option and follows the same conditional
shape:

```ts
...(env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME
  ? { priorityClassName: env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME }
  : {}),
```

## Verification

`typecheck --filter supervisor`, `format` and `lint` clean. No changeset
or `.server-changes/` note: off by default, no user-visible behaviour
change.
re2-prod-supervisor-cffaa05 re2-test-supervisor-main-cffaa05
2026-08-18 19:34:40 +01:00
Chris Arderne 12ec4667cb feat(webapp): enable development branches for all organizations (#4670) 2026-08-18 19:30:21 +01:00
Marcus Nerløe b83cf671de fix(core): mint the fallback external trace id per run (#4534)
## What

Runs that carry no external trace context (schedules, task-to-task
triggers) fall back to a trace id generated once in the [`TracingSDK`
constructor](https://github.com/triggerdotdev/trigger.dev/blob/main/packages/core/src/v3/otel/tracingSDK.ts#L165).
With `experimental_processKeepAlive` the SDK outlives the run, so every
run on a warm process is exported to the external OTLP endpoint under
that one id.

Across our production traces, 80.3% contained spans from more than one
run, worst case 25. Per-run cost and latency attribution is unusable as
a result. This is the same warm-start hazard c043c4a6a fixed for the
external-context path, which left the fallback captured at construction.

## How

`FallbackExternalTraceIds` hands out one id per internal trace, shared
by the span and log wrappers so a run's spans and logs agree.

The id is keyed off the record's own internal trace id rather than
ambient state at export time, because batch processors drain
asynchronously and a run's records routinely export after the next run
has started. The map is bounded and evicts least-recently-used, so a run
that is still exporting can't lose its id.

Granularity follows the internal trace, so a run and the runs it
triggers stay on one trace.

**Risk:** the wrappers only exist when `exporters` / `logExporters` are
configured, so deployments that don't export externally are untouched.
Nothing outside `tracingSDK.ts` changes.

**Known gap (pre-existing):** sampling and id selection still branch on
ambient `getExternalTraceContext()`, so records draining across a run
boundary in mixed mode are misplaced in both directions. It can't use
the approach here — the external id comes from the run's incoming
`traceparent`, which isn't carried on the record — so closing it means
capturing `internalTraceId -> external context` in a span processor.
Happy to follow up separately.

---

## Testing

`packages/core` suite passes. `pnpm run format` and `pnpm run lint:fix`
produce no diff.

Six cases in `externalSpanExporterWrapper.test.ts`, each
mutation-checked rather than just observed passing: one id per run,
stability within a run, correct id when records drain after the next run
started (spans and logs together), external export stays off when
unconfigured, retention of a run still exporting while the map churns,
and the bound itself.

**CI:** the five failing `webapp` shards are the ones containing
`containerTest` suites. Fork PRs receive no repository secrets, so
`unit-tests-webapp.yml` skips the DockerHub login and the image pre-pull
(both gated on `env.DOCKERHUB_USERNAME`) and the container tests time
out at 60s. Same five shards across five runs, every failure a 60s
timeout, and those shards pass on internal PRs. Happy to be corrected if
you can run them with secrets available.

---

## Changelog

Unrelated runs are no longer merged into a single trace in your external
observability tool when they happen to execute on the same warm worker
process. A run and the runs it triggers still share one trace, so a run
tree stays together.

---

## Screenshots

_n/a_

---

_Supersedes #4526 (auto-closed before I was vouched) and #4533 (opened
ready rather than as a draft). GitHub won't reopen either._

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Iss <74388823+isshaddad@users.noreply.github.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2026-08-18 18:59:00 +01:00
claude[bot] d7056a9c67 chore(webapp): reword the no-billing-limit banner copy (#4656)
<!-- ccr-slack-attribution -->
_Requested by **Matt Aitken** · [Slack
thread](https://triggerdotdev.slack.com/archives/C0BKB98B84W/p1787045331358929)_

Copy-only reword of the banner shown to org admins who have not set a
billing limit yet.

**Before** — the banner read "Protect your organization from unexpected
usage spikes." with a button labelled "Configure billing limit".

**After** — it reads "Add a billing limit to your account to prevent
overspending" with a button labelled "Billing limit settings".

The new wording names the action up front and matches the destination it
sends you to, so the banner reads as a settings link rather than a
one-off setup step.

##  Checklist

- [x] I have followed every step in the [contributing
guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
- [x] The PR title follows the convention.
- [x] I ran and tested the code works

---

## Testing

Formatting and linting pass (`oxfmt --check`, `oxlint`). No tests or
snapshots assert this copy. The change is two string literals in one
component, with no behaviour attached.

---

## Changelog

Reworded the billing-limit banner for organizations without a limit
configured, and relabelled its button to "Billing limit settings".

---

## How

Both strings live in `NoLimitConfiguredBanner` in
`apps/webapp/app/components/billing/OrgBanner.tsx`: the heading is the
`canManageBillingLimits` branch of the banner's children, and the label
is the `<span>` inside the `LinkButton`. Only those two literals
changed. The button still points at `v3BillingLimitsPath(organization)`
(`/orgs/{slug}/settings/billing-limits`), so routing, permissions and
the non-admin variant of the message are untouched.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-18 18:50:32 +01:00
nicktrn e91fb746f7 feat(supervisor): configurable security context for run pods
Adds KUBERNETES_RUNNER_SECURITY_CONTEXT (off | baseline | restricted), selecting how constrained the run container is.

baseline drops the capability bounding set and blocks privilege escalation. restricted additionally pins the container to a non-root uid, chosen by runtime so bun images get their own.

Default is off, so this is inert on merge.
re2-test-supervisor-runtime-uid
2026-08-18 18:40:18 +01:00
Matt Aitken 444c2215ca fix(run-engine): stop requeued runs with a lapsed ttl being orphaned in the queue (#4669)
## Summary

A run triggered with a `ttl` could get permanently stuck showing as
queued. If the run started executing and was then requeued after a
failure (a stalled heartbeat, a worker dying mid-run) once its TTL had
already elapsed, the next dequeue pass silently dropped it from every
queue structure. The run stayed QUEUED in the database forever, and
nothing (dequeue, the TTL consumer, queue repair) could ever see it
again.

## Root cause

Enqueue registers a TTL entry for the TTL consumer, and the first
dequeue removes it ("the run is executing, not expired"). A nack rewrote
the message preserving the original `ttlExpiresAt` without
re-registering that entry. The next dequeue pass then took the
expired-TTL branch: remove the run from the queue sorted sets and leave
the message for the TTL consumer to finalize. But the consumer's entry
was gone, so nothing ever finalized the run.

The fix has two halves:

- `nackMessage` drops `ttlExpiresAt` from the rewritten message. TTL
only applies to runs that have never been dequeued (the same contract as
`includeTtl` on re-enqueues), so a requeued run stays dequeuable and is
never expired by its original deadline.
- The dequeue expired-TTL branches now (re-)register the TTL entry
instead of assuming it exists, so any message still carrying a lapsed
`ttlExpiresAt` with no TTL entry (including ones written before this
fix) finalizes as EXPIRED instead of orphaning.

## Verification

New engine test suite `ttlNackRequeue.test.ts` (testcontainers, real
Redis and Postgres). All four tests fail before the fix and pass after:

- a heartbeat-stalled EXECUTING run with a lapsed TTL is requeued and
dequeued again instead of orphaned (the full production failure chain)
- requeue-after-failure strips `ttlExpiresAt` so later dequeues do not
treat the run as expired
- a lapsed-TTL message whose TTL entry is missing is re-registered by
dequeue and finalized as EXPIRED, for both plain and concurrency-key
queues

Also ran the existing ttl, heartbeats, dequeuing and attemptFailures
engine suites plus the full run-queue suite (149 tests) against the
change.
2026-08-18 19:32:41 +02:00
nicktrn 2496a8a863 feat(supervisor): optional image registry rewrite for run pods
Adds two optional env vars that rewrite the registry host of run pod images at pod creation, so a supervisor can pull from a registry in its own region. Off by default and inert unless both are set. Exact host-prefix matching, so look-alike hosts pass through untouched.
re2-prod-supervisor-2026-08-18 re2-test-supervisor-2026-08-18 re2-test-supervisor-main-2496a8a8
2026-08-18 15:03:12 +01:00
Chris Arderne b4313c8199 feat: logs search v2 (#4615) 2026-08-18 14:59:46 +01:00
nicktrn 158f6957e4 feat(supervisor): make the runner seccomp profile configurable
Replaces the hardcoded runner seccomp profile path with KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH, and the node-24-only condition with KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES (none | node-24-plus | all).

Both defaults reproduce current behaviour, so this is inert on merge. Widening the scope or turning attachment off becomes a config change rather than a deploy.
2026-08-18 14:47:00 +01:00
James Ritchie 5715d88d77 fix(webapp): tighten the spacing above the SSO email message
The paragraph carried py-4, inherited from the form it replaces. A form needs
that room below it before the footer; a line of explanatory text with no footer
under it doesn't, and the modal read bottom-heavy. pt-2 keeps it clear of the
title and lets the dialog's own padding close the gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:05:44 +00:00
Chris Arderne 74db5a3f58 docs: document cron schedule windows (#4657) 2026-08-18 13:23:29 +01:00
Saadi Myftija 7e677008ed feat(supervisor): per-org placement overrides for run pods (#4655)
The supervisor now supports routing an organization's runs to specific
nodes. `KUBERNETES_ORG_PLACEMENT_OVERRIDES` takes JSON keyed by the
internal org ID, adding node selector entries and tolerations to that
org's run pods, e.g. to route an org onto a dedicated, tainted node
pool:

```json
{"<orgId>": {"nodeSelector": {"pool": "dedicated"}, "tolerations": "dedicated=runs:NoSchedule"}}
```

The node selector merges over the defaults (the override wins on key
collision, with a warning logged). Tolerations append to the existing
runner and scheduled-run sets. Overrides are validated at startup
similar to `KUBERNETES_RUNNER_TOLERATIONS`.

Exposed in the Helm chart as
`supervisor.config.kubernetes.orgPlacementOverrides`, where tolerations
can also be given as a list.
2026-08-18 12:16:23 +00:00
Chris Arderne 53ca44dd2d chore: cache and clean up Knip analysis (#4658) 2026-08-18 12:58:47 +01:00
James Ritchie e86b2aefc3 Revert "TEMP: force the SSO variant of the email modal for review"
This reverts commit 81bacb12e2.

The copy is signed off, so the modal goes back to branching on the real
isSsoManaged value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:23:02 +00:00
James Ritchie 3f728e8aba fix(webapp): say single sign-on in the email modal, not identity provider
"Signs you in through its own identity provider" led with the jargon. Single
sign-on is the term someone will have been given by whoever set it up, so the
sentence now opens with that and leaves the identity provider as the place the
address actually lives.

"An admin on your account" was also ambiguous - it's an organization admin, and
"your account" could be read as the reader's own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:22:51 +00:00
Katia Bulatova e768d0a724 feat(webapp): run the dashboard agent through AWS Bedrock behind an env switch (#4609)
## What & why

The dashboard agent can now run its model calls through AWS Bedrock
instead of the direct Anthropic API, chosen by a single env switch. It's
**off by default** (`DASHBOARD_AGENT_MODEL_PROVIDER` unset ⇒
`anthropic`), so merging changes nothing at runtime — the Bedrock path
is a dormant branch until an operator sets the switch and AWS config.
The default Anthropic path is byte-for-byte unchanged.

This also carries a related tenant-isolation hardening for the agent's
delegated token (kept together deliberately — both land the agent on
Bedrock for HIPAA readiness). Refs: TRI-13251, TRI-11032.

## What's inside

**Provider seam** —
`internal-packages/dashboard-agent/src/model-provider.ts`: the registry
now holds both `anthropic` and `bedrock`; `resolveDashboardAgentModel()`
maps the canonical `"anthropic:<id>"` strings the managed prompts carry
to the active provider, and the cache-breakpoint helpers emit the active
provider's shape — Anthropic `cacheControl` vs Bedrock `cachePoint`.
Managed prompt strings stay canonical, so stored prompts don't change
meaning. Unmapped model ids throw rather than shipping a guaranteed-404
profile. All agent, watch, compaction and title callsites route through
the resolver; the `dashboardAgentModelKey` locals override (test mock
injection) is preserved.

**Cache telemetry** — `step-cache.ts`: cache token usage is read from
the active provider (Anthropic reports it on provider metadata; Bedrock
reports the write on metadata and the read via standard usage), so
`gen_ai.usage.cache_*` is populated on both. This also fixes a latent
ordering bug where step attributes could null-overwrite the prompt-cache
read count.

**Webapp callsites** — `dashboardAgentHeadStart.server.ts` and the
head-start route resolve the model and the cache breakpoint through the
same seam, so the warm-up prefix and the following turn share one
provider. The head-start firing gate is provider-aware: on Bedrock it
gates on `AWS_REGION` and lets the SDK resolve credentials (IAM role /
static keys / session token / bearer), so a role-based deploy still
warms; on Anthropic it stays `Boolean(ANTHROPIC_API_KEY)`.
`app/env.server.ts` gains the optional AWS vars and validates
`DASHBOARD_AGENT_MODEL_PROVIDER`. `ANTHROPIC_API_KEY` is untouched and
not required on a Bedrock deploy.

**Tenant-isolation hardening** —
`internal-packages/rbac/src/fallback.ts`: for a **scoped** context, the
OSS `authenticateUserActor` now applies the same membership floor as the
session path — a delegated user-actor token whose user is not a member
of the scoped org/project is denied (403). Unscoped tokens keep their
prior behavior (no tenant claim, no lookup). The user lookup falls back
replica→primary so replication lag can't spuriously 401 a just-joined
member. Members and admins are unaffected. Previously this invariant
held only through per-route discipline; this makes it structural.

## Enabling Bedrock (later, ops)

- Set `DASHBOARD_AGENT_MODEL_PROVIDER=bedrock` **identically** in both
the webapp and the agent task container — the webapp warms the cache
prefix and the task reads it, so a split would silently miss the cache.
- Set `AWS_REGION` and provide credentials the Bedrock SDK can resolve
(IAM role preferred). For v1 this runs **without** an Anthropic API key.
Note: with no Anthropic key set, rollback is "turn the agent off", not
"unset the switch" (unsetting falls back to the Anthropic provider,
which then has no key).
- Two things to confirm before rollout: the Sonnet inference-profile id
is validated against the SDK's own model-id union but still warrants a
live smoke test; and Bedrock prompt caching for Sonnet is a 5-minute
window (not Anthropic's 1h), so input-token cost rises when flipped.

## Testing

Unit tests cover both provider paths: the provider switch and
per-provider cache shapes, a structural regex asserting Bedrock ids are
real inference profiles (not an echo of the table), the split-metadata
cache telemetry, and real-Postgres RBAC tests — member allowed, scoped
non-member denied (org-only and project-only), missing user → 401, admin
non-member exempt, unscoped success. `typecheck --filter webapp` and the
dashboard-agent + rbac suites pass.
2026-08-18 13:14:01 +02:00
James Ritchie 81bacb12e2 TEMP: force the SSO variant of the email modal for review
Not for merge. Hard-codes isSsoManaged so the identity-provider copy can be read
in the browser without an SSO org to test against. Revert once the copy is
signed off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:55:32 +00:00
James Ritchie 42b1217810 refactor(webapp): fold Theme tokens into a Colors page showing every theme at once
The Stronger colors page compared one theme against its own high-contrast
treatment, two columns at a time, which meant four passes with the theme switcher
to see whether an accent held up everywhere. Theme tokens sat separately as a
plain swatch gallery of the layer underneath it.

They're now one page, "Colors", under Foundations - the Accessibility group is
gone, and so is the separate Theme tokens page. Every block renders five times:
once per theme, then once under Stronger colors, which follows the switcher so
any theme can sit beside its own high-contrast treatment.

The measured tables keep the Fill / Ratio / Verdict readings and repeat them per
theme, sixteen columns in one flat grid so a sub-column stays under its header
all the way down. Ratios are still measured live off the DOM, which is what makes
this work at all: pinning data-theme on a wrapper is enough to get that theme's
answer, with no table of hard-coded values to fall out of date.

Two things that made it possible: every theme block is written as a plain
attribute selector rather than being anchored to <html>, and the dark: variant is
descendant-capable - so a nested column inherits the whole token set. The theme
groups use display:contents so their three cells stay direct grid items of the
row. As a bonus the --chart-2/--chart-3 override now resolves here too, since it
wants both attributes on one element and the last column restates the theme it
inherited; the note saying it couldn't is gone.

The charts drop to three bars, since each one renders five times across a row now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:55:24 +00:00
James Ritchie 54933e9012 feat(webapp): give the theme a primary accent, and make preview blue everywhere
Two token changes.

The app had no primary color worth the name: --color-primary was an apple green
that four call sites used and nothing else, while the indigo link accent quietly
did the job a primary usually does. --color-primary now points at
--color-text-link rather than carrying its own value, so it's the same hue by
definition and can't drift. The light themes needed no separate entry once it
became an alias.

Preview environments were yellow-400 on the dark themes and yellow-700 on the
light ones - the darker end reading closer to brown - and blue only under
Stronger colors. Preview and its branch labels are now blue in every theme, so
an environment looks like itself at both ends of the `system` setting. Their
per-mode and Stronger colors entries are gone, since the base value now matches
what those blocks were reaching for.

Worth knowing: because the base and high-contrast values now agree, Stronger
colors no longer moves either preview token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:55:07 +00:00
James Ritchie c6310fa242 fix(webapp): keep the rounded badge's label readable on the light themes
The rounded variant put text-text-bright on a blue-600 fill. That token is
near-black on Light and White, so the label all but disappeared into the fill
while reading correctly on the dark themes.

The fill is the same in every theme, so the label is now plain white in every
theme too - which is what the variant's own system: override was already doing
under Stronger colors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:55:07 +00:00
James Ritchie af50664eab fix(webapp): keep the pulsing dot centred in its own ring
The dot was a fixed size-2 next to a ring stretched to the container, and
nothing centred either. At the default size those agree, so it looked right -
but any call site that resized the whole thing got a small dot sitting in the
corner of an oversized halo, flex-start in both axes.

The dot now fills the container, so a size on the wrapper scales the dot and the
ring together, and both are centred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:55:07 +00:00
Chris Arderne b33197691b chore: enforce no unused deps or code in ci (#4654) 2026-08-18 11:35:51 +01:00
Matt Aitken 40c4064f96 fix(webapp): show errors on AI tool call and embed spans in the run inspector (#4653)
## Summary

When an AI SDK tool call failed inside a run, the span showed up under
the "Errors only" filter but the span inspector gave no hint of what
went wrong. The exception was recorded on the span all along; the
`ai.toolCall` and `ai.embed` inspector views just never rendered span
events. Failed tool call and embedding spans now show the standard error
block (message plus stack trace) below the Input section.

## Root cause

Generic spans render exception span events via the `SpanEvents`
component, but the AI-specific span entities replace the whole panel
with their own layout and dropped the events entirely. The span's events
are now passed into `AIToolCallSpanDetails` and `AIEmbedSpanDetails` and
rendered with the same `SpanEvents` component the generic view uses.

Errored generation spans (`ai.generateText` and friends) use a tabbed
view and still don't surface errors; that needs its own design pass and
is left for a follow-up.
2026-08-18 11:54:07 +02:00
Wes Mason a55f7cdf4d fix(run-engine): stop a '*' concurrency key stranding its whole base queue (#4628)
## The bug

A concurrency key is an unrestricted client string
(`ConcurrencyKeySchema` is `z.union([z.string(),
z.number()]).transform(String)`), and `concurrencyKeySection` does no
escaping, so `*` reaches the queue raw. `queueKey` then renders it as
`...:queue:<q>:ck:*`, which is byte-identical to the wildcard member the
CK scripts keep in the master queue to mean "this base queue has
concurrency-key work".

Every CK script ends with the same pair:

```lua
-- Rebalance master queue with ck:* member
redis.call('ZADD', masterQueueKey, earliestIdx[2], ckWildcardName)

-- Remove old-format entry from master queue (transition cleanup)
redis.call('ZREM', masterQueueKey, queueName)
```

`ckWildcardName` is `toCkWildcard(message.queue)`, and for a `*`-keyed
run that returns the identical string, so the cleanup on the second line
deletes what the rebalance on the first line just wrote.

The master queue then has no entry for that base queue, while `ckIndex`
and the variant queues still hold the work. **Every concurrency key on
the queue stops being dequeued**, not just the `*` one. It is silent,
and it only recovers if some later write happens to re-add the member.

Reproduced before the fix:

```
master queue AFTER normal ck enqueue: ["{org:...}:queue:task/my-task:ck:*"]
master queue AFTER ck='*' enqueue:    []
ckIndex members (work still queued):  [":ck:user-1", ":ck:*"]
dequeued:                             []
```

Blast radius is bounded to the environment that triggers it, so it is
self-inflicted rather than cross-tenant, but a single trigger stalls the
queue for everything on it.

## The fix

Guard the cleanup so it never removes the wildcard member:

```lua
if queueName ~= ckWildcardName then
  redis.call('ZREM', masterQueueKey, queueName)
end
```

Applied to all 10 CK scripts (4 enqueue, 6 ack/nack/dead-letter). No
key-format change and no migration: a queue already stranded in Redis is
repaired by its next write.

I considered rejecting `*` at the API boundary instead and rejected it.
Existing Redis state and `TaskRun.concurrencyKey` rows already hold raw
`:`-bearing and `*` keys, so changing key construction would orphan
in-flight messages and split concurrency accounting mid-deploy. Boundary
validation would still be reasonable as belt-and-braces later, but the
Lua guard alone fixes it including for state already out there.

## Testing

`ckWildcardKey.test.ts` covers the enqueue, ack and nack paths. All
three pass with the guard and **all three fail without it**, verified by
reverting. Full `src/run-queue/` suite is green (166 tests).

## Note for #4367

The virtual-time branch adds three more CK scripts with the same pattern
(`enqueueMessageCkVtimeTracked`, `enqueueMessageWithTtlCkVtimeTracked`,
`nackMessageCkVtimeTracked`). They do not exist on main so they are not
in this PR; the same guard needs applying there, and I will do that on
that branch.
2026-08-18 09:36:48 +01:00