1595 Commits

Author SHA1 Message Date
Oskar Otwinowski 4c16387426 fix(webapp): project integrations page — Staging gating, unreachable code, and follow-ups (#4784)
Three bugs on the project integrations page, one commit each for the two
reported ones and four for the follow-ups found while fixing them.

## `chore`: remove unreachable code on the integrations page (TRI-12645)

Two notification panels in `VercelSettingsPanel` could never render:

1. The **"Failed to load Vercel settings"** panel was gated on a
`hasError` state whose setter is never called anywhere, so it was
permanently `false`.
2. The **"connection expired"** banner *inside* the `connectedProject`
branch was unreachable: `VercelSettingsPresenter` only populates
`connectedProject` on its success exit, which hardcodes `authInvalid:
false`, while both `authInvalid: true` exits return `connectedProject:
undefined`.

Removing them makes the surrounding `!showAuthInvalid` guards vacuous,
and the `onboardingData?.authInvalid` disjunct redundant — the loader
already folds onboarding auth state into `authInvalid` before it reaches
the component.

**No behaviour change.** An org with a connected project and an expired
token still gets the banner, from the branch below (untouched).

## `fix`: gate Staging settings on plans without a Staging environment
(TRI-12646)

The ticket's premise was inverted, and I've corrected it there. In Git
settings, **Preview** is the row that's correctly gated; **Staging** is
the one with no gate at all:

- Preview swaps its switch for an Upgrade button, and
`projectSettings.server.ts` neutralises a forged
`previewDeploymentsEnabled=on`.
- Staging was a plain always-editable `Input`, and
`validateStagingBranch` only checked the branch existed on GitHub. An
org without a staging environment could type a tracking branch, hit
Save, get a success toast, and have it silently do nothing.

Staging and Preview environments are created together for projects on a
plan that includes them, so gating one and not the other was an
oversight.

The Staging row now mirrors the Preview row. Server-side it ignores the
submitted branch when there's no staging environment, but **preserves
the stored branch rather than clearing it** — deliberately different
from the Preview handling. Forcing a boolean off is harmless; forcing a
*string* off would wipe a tracking branch the org had already configured
the first time they saved after losing the environment.

The Vercel write path had the same gap: `update-config` /
`complete-onboarding` / `update-env-mapping` never re-derived available
env slugs server-side, so `["stg","preview"]` could be persisted for a
project with neither environment, and
`createDefaultVercelIntegrationData` turned preview on unconditionally.
Both now filter against the project's actual environments, via a pure
`restrictConfigToAvailableEnvSlugs` helper that only touches keys
present on the input.

## `fix`: show build settings when the GitHub app is disabled
(TRI-13488)

The page wrapped Git settings, the Vercel section **and** build settings
in one `githubAppEnabled` guard, so with the GitHub app off it rendered
an empty container.

The Vercel section genuinely depends on GitHub — it can't sync
environment variables or link deployments without a connected repo — so
it stays gated. Build settings don't: they also apply to CLI deploys run
with `--native-build-server`, exactly as the section's own description
states. They now render regardless.

## `fix`: stop the Vercel onboarding modal spinning forever (TRI-13488)

`computeInitialState` starts in `loading-projects` whenever the org has
a Vercel integration but no onboarding data yet, and the effect that
escapes it waits for `availableProjects !== undefined`. When
`getOnboardingData` returns `null` — it does that on any thrown error,
and when the org integration row is missing — nothing ever arrives.

The empty-array case self-resolves (`[] !== undefined`), so this is
specifically the null case. The route can tell "still loading" from
"loaded nothing" because its fetcher always requests
`?vercelOnboarding=true`; it now passes that down and the modal explains
the failure with a retry and a link to check the integration's access on
Vercel.

## `fix`: match staging and preview environments consistently
(TRI-13488)

The four places that ask "does this project have a staging / preview
environment?" disagreed. `VercelSettingsPresenter` matched on type with
no parent filter, so any preview *branch* row satisfied it — branches
are `PREVIEW` rows too. `GitHubSettingsPresenter` and
`ProjectSettingsService` matched on slug instead.

Slug is the weaker key: it's derived at creation time and legacy rows
can carry something else, which is why
`memberDevelopmentEnvironmentWhere` deliberately avoids it. All four now
match on `type` plus `parentEnvironmentId: null`, which excludes
branches without depending on the slug being canonical.

## `fix`: explain when no Vercel environment can be mapped to Staging
(TRI-13488)

Reported while reviewing the branch. The Staging build settings show
*"Set a Vercel environment for Staging first."* whenever the project has
a staging environment and no mapping — but the control that sets the
mapping only rendered when the Vercel project had at least one custom
environment:

```
hint:     hasStagingEnvironment && !configValues.vercelStagingEnvironment
control:  hasStagingEnvironment && customEnvironments.length > 0
```

So a Vercel project with no custom environments, or one whose custom
environments failed to fetch (the presenter swallows that error to
`[]`), got an instruction with nothing to act on. Both conditions
predate this PR.

The mapping row now always renders alongside the hint and explains what
to do when there's nothing to choose from, and the build-settings hint
says the same thing.

## `chore`: remove the remaining dead code (TRI-13488)

- The `"installing"` `OnboardingState` is unproducible — no `setState`
call yields it — so its redirect effect, switch arm, `isLoadingState`
conjunct and the `vercelAppInstallPath` import it was the only user of
are all dead.
- `(state as string) !== "completed"` sits in a branch where TypeScript
has already narrowed `"completed"` out; the cast is what let it compile.
- `hideSectionToggles` was only ever passed alongside
`layout="settings"` but only read inside `layout="card"` blocks, so it
could never take effect. Removed the prop entirely.
- Unused bindings and the helpers only they referenced: `envSlugLabel`,
`_formatSelectedEnvs`, `_CompleteOnboardingForm`,
`_handleFinishOnboarding`, and the rest.

No behaviour change in that commit.

## Not included

The three overlapping modal-open effects in
`settings.integrations/route.tsx` are left alone — they're defensive
against a close-then-reopen race, and untangling them is a behavioural
risk with no user-visible payoff.

## Verification

`pnpm run typecheck --filter webapp`, `pnpm run lint` and `pnpm run
knip` are clean. New `apps/webapp/test/vercelIntegrationConfig.test.ts`
covers the slug restriction and the default-config seeding (both pure
functions); 39 tests pass across it and the three existing
Vercel/project-settings files.

The new `projectId` + `slug` query is served by the existing
`@@unique([projectId, slug, orgMemberId])` prefix — same access pattern
as the preview check it mirrors.

refs TRI-12645, TRI-12646, TRI-13488
2026-08-26 13:26:44 +00:00
Saadi Myftija ee29393862 perf(webapp): cache deployment logs across navigations (#4775)
Switching between deployments in the dashboard re-fetched the whole
build log stream from record zero and re-rendered the list line by line
every time. Logs are now cached per deployment for the lifetime of the
tab: revisiting a deployment shows its logs immediately, and the stream
is resumed from the next unread record rather than restarted. Finished
deployments whose stream has been read through the `finalized` event are
served entirely from the cache.

### Changes

The stream/cache logic moved out of the route into a `useDeploymentLogs`
hook. On each deployment switch it seeds state from the cache, resumes
the S2 read session at `nextSeqNum`, and writes back on cleanup or
natural session end. Completion is derived from the stream's own
`finalized` event (plus a terminal deployment status), not from the
session closing, so a session cut short by token expiry or a proxy
cannot pin a truncated log in the cache.

Memory is bounded by a small LRU (`deploymentLogsCache`): at most 20
deployments and 20,000 log lines in total, least recently viewed evicted
first. The most recently viewed deployment is always kept, so a single
very large log can temporarily exceed the line budget on its own.
Records are batched into one state update per tick instead of one per
line.
2026-08-25 15:54:22 +02:00
Eric Allam 47ff76d727 feat(webapp,clickhouse): return an actionable error instead of a 500 when a runs list query is too expensive (#4773)
## Summary

When a runs list query is too expensive to complete, it now fails with a
clear, actionable error instead of a generic 500.

Previously, a runs list query that exceeded ClickHouse resource limits
threw an opaque error. On the public `runs.list` API that surfaced as a
retryable 500, so a customer task calling it would keep retrying a query
that could never succeed. On the dashboard it rendered as a generic
error page with no hint about what to do.

## Fix

The ClickHouse client now tags resource-limit failures (memory, time,
rows, bytes) with their error type, and the runs repository maps those
to a dedicated `RunsListQueryError` (HTTP 422).

- `runs.list` API returns 422 with a message telling the user to narrow
their `created_at` range, plus an `x-should-retry: false` header so the
SDK does not retry it.
- The dashboard runs list (and the errors, scheduled, standard-task,
agents, and webhooks list views) render a shared error state with the
same guidance, so a too-broad time filter is recoverable by the user.
2026-08-25 14:49:10 +01:00
Daniel Sutton f98e303292 feat(webapp): resolve which shard an environment mints run roots into (#4755)
## Summary

Adds the shard-selection stage of run-id minting.
`resolveMintShard(env)` returns which run-ops database an environment
mints its new run roots into: the active shard list, then a fleet-wide
override, then a per-environment or per-organization pin, then a
rendezvous hash of the environment id.

That half is inert. Nothing calls `resolveMintShard`, no deployment has
any of the new flags set, and an empty active list returns the current
answer without reading anything.

**The other half is not inert, and it is where review effort belongs.**
To stamp a grace window this needs a read-then-write under a lock, so it
rewrites the global feature-flag write path that `runOpsMintKind`
already depends on in production. See below.

## Placement

Resolution reads the active list from a global flag, applies the grace
window, and then picks:

- a fleet-wide override if one is set, which is how a cutover completes
without visiting each organization. `new` holds the whole fleet on the
current id format.
- otherwise a per-environment or per-organization pin. `new` holds one
organization back while the rest move, which is how a canary works.
- otherwise a rendezvous hash, so adding a shard moves only about
1/(N+1) of environments and removing one moves only its own.

Two hash details are load-bearing. Scores are 64-bit `sha256(envId \0
key)`, because a 32-bit score collides at our environment count and an
undetected tie would resolve by iteration order. The parsed key list is
sorted, because otherwise two deployments listing the same shards in a
different CSV order would place environments differently.

A pin or override naming a shard that has left the active list falls
through to the hash and reports once. Honouring it would leak the drain
the active list exists to perform, and throwing would fail triggers
whenever a pinned shard drains.

## Why the active list is a flag and not an environment variable

A deploy rolls for hours, so two pods hold two different environment
values at the same time. A list held in the environment therefore splits
the fleet for the length of the rollout, with new pods placing an
environment on one shard and old pods on another. A grace window
measured in seconds cannot cover that, and the same knob times the
existing mint-kind flip so it cannot simply be lengthened. An
environment variable also cannot record its own flip time, and an
operator cannot know a rollout's end in advance.

So the list, its grace stamp and the override are global flags, written
server-side against the control-plane clock under an advisory lock. This
branch adds no environment variables.

## The write path, which is live

Stamping generalises to any number of graced flag groups in one
transaction under one lock. That has three consequences a reviewer
should look at directly:

- It closes a real bug. `runOpsMintKind` is an editable control on the
global flags page, and that page previously wrote it with a bare upsert:
no lock, no stamp. An operator flipping mint kind through the UI got an
ungraced flip, so every pod crossed the cutover at a different moment.
Verified against a running instance, before and after.
- A graced group is all-or-nothing. Submitting its primary writes the
group with a fresh stamp; omitting it deletes the primary and its stamp
together, because a stamp left without its primary keeps being served
and would mint into a shard just removed.
- The advisory lock takes the previous id as well as the current one, in
a fixed order, so writers on an older release still serialise during a
rollout. The legacy id can be dropped one release after this ships.

This folds with #4751 rather than replacing it: its `unlockLockedFlags`
rule decides what the sweep may delete, and the graced groups keep their
stamp under the lock. Both sets of tests pass.

## Notes for review

Determinism is a property of the pure core for fixed inputs. The wrapper
supplies the clock, the same split `effectiveMintKind` already uses. A
failed read of the list falls back to the current id format rather than
guessing.

Six flags appear in the admin pages immediately. The two pins are
per-organization, so they render read-only on the global page. The list,
its stamp and the override are deployment-wide, so they render read-only
in the organization dialog.

Nothing bounds the active list against shards that actually exist. That
is safe while nothing mints, but the change that carries a shard key
into an id must land after the shard descriptors bound the list, or
bound it itself.
2026-08-24 15:23:58 +01:00
nicktrn b082e44389 fix(webapp): write-path and appearance-control fixes for the theme work (#4756)
Fixes found while reviewing #4547, stacked on that branch so they can be
reviewed on their own and merged into it. One commit per fix.

## Write-path correctness

**Refuse account writes while impersonating.** The five
`dashboardPreferences` writers already no-op for an impersonating admin,
but the three profile writers added next to them did not, and
`requireUserId` returns the impersonated user's id. Both gates now
refuse up front and say so, rather than the preference writers silently
no-opping while the page reports success.

**Preserve unknown keys on a full-blob write.**
`mutateDashboardPreferences` parses the JSON column, hands the result to
a mutator and persists the whole object back. zod strips keys it does
not declare, so a deploy that predates a preference field drops it on
the next write through that path — and
`updateCurrentProjectEnvironmentId` sits on the navigation hot path.
`preserveUnknownKeys` re-attaches them at the write. Note this cannot
help deploys already running, so it makes this the last release able to
strip rather than retroactively protecting the fields added in #4547.

**Scope hidden-sidebar writes to what was shown.** The customize dialog
builds its hidden map from the sections it can see and the write
replaced `hiddenItems` wholesale. The profile page has no org in scope,
so it resolves sections from the most-recently-updated project's org:
confirming there dropped hidden ids belonging to sections that org's
flags exclude. The payload now carries the ids the dialog rendered and
the write only replaces those. Submissions without the list stay
authoritative.

**Consider both addresses when checking email ownership.** The check
only looked at the address the user already had; it now considers the
current and submitted address together, so an org managing either one
governs the change. Validation moved ahead of the check, and
`emailDomainOf` splits on the last `@`.

## Interaction

**Revert unsaved themes, debounce contrast saves.** The theme and
system-theme selects stamp `data-theme` before the write lands. When it
fails, the loader returns the value it always had — so
`useSystemThemeSync`'s effect deps are unchanged and React's vdom diff
sees no change either, and nothing rewrites the attribute. The page kept
rendering a theme that was never stored while the select showed the
stored one. The stored pair is now re-applied explicitly, as the side
menu's switcher already did. The contrast slider is debounced because
Radix commits on every arrow keypress, so a keyboard user crossing the
range fired one write per step.

**Tick More options for themes outside the short list.** The appearance
submenu offers System, Light and Dark; Black and White live on the
profile page. With one of those stored, every row read as unselected.

## Subtraction

**Drop the profile update rate limiter.** It covered one of four paths
that write the same column — `resources.preferences.sidemenu` and
`.favorites` take unlimited authenticated writes and go through the
locked read-modify-write, which is more expensive than the single narrow
`jsonb_set` this capped. It was also what made the contrast slider
unusable by keyboard. If preference writes want limiting, it belongs in
one place covering all of them.

**Resolve email ownership when the dialog opens.** It fans out one SSO
status lookup per organization the user belongs to and ran in the
profile loader on every page view, purely to pick which body the dialog
renders. The action re-derives it before writing either way, so the
check that guards the write now has one call site instead of two.

## Testing

`typecheck --filter webapp` and `lint` clean. New unit tests for
`preserveUnknownKeys`, `mergeHiddenItems` and `emailDomainOf`;
`themePreference`, `mergeHiddenItems` and `ssoManagedIdentity` suites
pass locally (26 tests). The rest of the webapp suite needs
testcontainers and is left to CI.

No changeset or `.server-changes` entry: everything here fixes code on
the parent branch that has not shipped. The one exception worth a
maintainer's call is `mergeHiddenItems`, which also touches the side
menu's own customize path.
2026-08-21 19:27:52 +01:00
James Ritchie 4c5237ca4a feat(webapp): themes refinement, new black & white themes, 2 accessibility toggles (#4547)
## What this does

Rounds out the theme work behind the existing `hasThemeSwitcher` flag.

**Two new themes.** Black and White sit alongside Dark and Light. They
inherit their neighbour's whole token set and only pin their surfaces
flat, so sections are separated by grid lines rather than layered fills.

**`System` is now configurable at both ends.** You choose which theme
the OS light setting lands on (Light or White) and which the dark
setting lands on (Dark or Black).

**Two accessibility toggles.**
- *Stronger colors* — swaps tinted status chips for solid fills, drops
decorative icon accents to monochrome, and darkens chart series that
didn't clear 3:1 on a white plot.
- *Underline links* — underlines body-text links, so an underline always
means the preference is on rather than being a hover style.

**Contrast slider.** Stores a 0–100 position within the active theme's
own range rather than a shared scale, so 35% stays 35% when you switch
themes. Each theme maps it in CSS, which keeps `system` working before
hydration.

**Appearance in the account popover.** A submenu listing the themes with
a check against the current one, plus a link through to the full set on
your profile. Picking one applies immediately rather than waiting for
the write to round-trip.

**Profile page.** Each row now saves on its own — no submit button. Name
and email show their value inline with an edit button; the email row is
read-only when an identity provider owns the address.

**A `/storybook/colors` audit page.** Renders every colour-carrying
pattern in the app once per theme plus once under Stronger colors, and
measures contrast ratios off the live DOM rather than a hard-coded
table, so it can't go stale.

---

## Demo


https://github.com/user-attachments/assets/d56cd4d8-719f-4ec5-a990-e04cdb98def1


---


## Compatibility

The stored preference shape is unchanged (`version: "1"`), and the four
new fields are all optional. The retired `classic` theme falls back to
Dark, whose palette at contrast 0 is what Classic shipped.

One deliberate change worth knowing: the default contrast moves from 50
to 0, so existing users who never touched the slider will see slightly
less contrast than before. That's what makes 0 mean "the base palette".

---

## Testing

Switched between every theme from both the account popover and the
profile page, in the expanded and collapsed rail, checking `data-theme`
follows and survives a reload. Dragged the contrast slider in each theme
and confirmed the percentage label tracks the handle and resnaps if a
save fails. Checked both accessibility toggles across the
`/storybook/colors` page, which is also where the contrast ratios were
read from. Confirmed the Appearance entry stays hidden for a non-admin
while the flag is off.

<!-- conductor-workspace-link -->

---

[Open workspace in
Conductor](https://app.conductor.build/workspace/fee50611-7623-4422-bada-ed1cba317ed1)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:27:52 +01:00
Oskar Otwinowski 910011d44e feat(vercel): automatic version skew protection at connect + atomic deployments deprecation (#4741)
Connecting a Vercel project now writes
TRIGGER_AUTOMATIC_SKEW_VERSION_PROTECTION=1
(plain, create-if-absent only - an existing value, including "0", is
never
touched; presence is target-containment aware, branch-scoped records do
not
count, a truncated env listing skips the write). The onboarding wizard
no
longer offers automatic atomic deployments (default off); the settings
row is
labelled Deprecated and enabling it requires confirming a dialog that
points
to task version skew protection and the docs (TRI-13001).
2026-08-21 18:09:27 +02:00
Eric Allam 32bf745c02 feat(webapp): customizable runs list with columns and smart columns (#4652)
## Summary

Makes the runs list customizable. A new **Display** control lets you
show, hide, and reorder columns, and add **smart columns** that pull a
single value out of a run's payload, metadata, or output by JSON path
(e.g. `$.failed`, `$.order.total`). Column choices live in the page URL,
so a view can be bookmarked or shared. Applies to the global runs list
and every per-task / scheduled / agent / webhook / error list, which all
share one table.

ID, Task, and Status can be reordered but not hidden. Smart columns are
display-only (no sort or filter, which would defeat the ClickHouse sort
key and cursor).

## How it works

Columns come from a shared registry; the Postgres `select` is derived
from the visible columns, so a run's large payload/output are only
hydrated when a smart column actually references them. All JSON parsing
for smart columns happens client-side, respecting the packet content
type, parsed once per source per row. Offloaded (too-large) values and
paths that aren't present render distinct placeholders rather than
fetching per row. The live poll carries the same sources so smart-column
values update in place.

Scalar columns stay always-selected for now: the shared list presenter
has a fixed output shape consumed by several routes and the live poll,
and narrowing individual scalar fields would add no real query cost
benefit on a single-row read. The select derivation is already
column-driven, so tightening this later is a one-line change.

## Screenshots

<img width="590" height="1028" alt="CleanShot 2026-08-21 at 16 48 17@2x"
src="https://github.com/user-attachments/assets/86b39856-bfcc-47c0-85ed-ee6ccddc3590"
/>
<img width="1924" height="1528" alt="CleanShot 2026-08-21 at 16 48
27@2x"
src="https://github.com/user-attachments/assets/6c766249-6d5b-45be-9330-c6caa75af7f7"
/>


<!-- conductor-workspace-link -->

---

[Open workspace in
Conductor](https://app.conductor.build/workspace/d6911080-2140-4de1-b88a-1b0623593caa)

---------

Co-authored-by: James Ritchie <james@trigger.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:08:12 +01:00
James Ritchie 2efb07e0b1 Toggle switch feels nicer to toggle (#4749)
Two small tweaks to the `Switch` primitive, so every variant and call
site picks them up:

1. **Track is 2px shorter.** `large` 44 → 42px, `medium` 32 → 30px,
`small` 24 → 22px. The checked thumb travel drops by the same 2px so the
thumb stays flush at both ends.
2. **Holding the switch down stretches the thumb into an oval** pointing
the way it's about to travel — rightwards when off, leftwards when on.
Pure CSS via `group-active:`, no new state or handlers.

The thumb's `transition` shorthand doesn't cover `width`, so it's now
`transition-[translate,width,background-color]` (same 150ms
duration/easing as before). `size-N` on the thumb became `h-N w-N` so
the press rule overrides the same `width` utility.

Verified in headless Chrome across all five variants in both states:
correct widths at rest, thumb flush at both ends, stretch grows the
right direction, and no overflow of the track.

<img width="266" height="108" alt="CleanShot 2026-08-21 at 10 16 14"
src="https://github.com/user-attachments/assets/ee95a399-0a40-48c4-a325-a1166b3bd88a"
/>


🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- conductor-workspace-link -->

---

[Open workspace in
Conductor](https://app.conductor.build/workspace/c1ce8d0f-9ed2-4fbc-8084-a3989484cc53)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:24:29 +01:00
Eric Allam 56f875680c fix(webapp): let the dashboard agent use a configurable base URL (#4738)
## Summary

Lets the dashboard agent point at a specific Trigger instance instead of
assuming it runs on the same instance as the webapp. Adds an optional
`DASHBOARD_AGENT_BASE_URL`; when unset it falls back to the SDK default.

## Root cause

The agent's session start, token mint, head start, in-proxy and the
client transport all built the agent's base URL from the webapp's own
origin (`API_ORIGIN ?? APP_ORIGIN`). That only holds when the agent
project runs on the same instance as the webapp. When it runs elsewhere,
`DASHBOARD_AGENT_SECRET_KEY` belongs to that other instance, so the
webapp's own API rejects it with an "Invalid API key" and the chat can't
start.

## Fix

`dashboardAgentApiOrigin()` now returns `DASHBOARD_AGENT_BASE_URL` or
the SDK default, never the webapp origin. A concrete default (rather
than an unset value) keeps it independent of `TRIGGER_API_URL`, which a
webapp may point at a different host. Every server call site already
routes through that helper; the client transport reads the value from
the root loader via a new `useDashboardAgentBaseUrl` hook.
2026-08-20 14:14:14 +01:00
claude[bot] 19eae515fd fix: rename the Projects org settings URL to /settings/projects (#4739) 2026-08-20 13:08:28 +00:00
Chris Arderne 4392e79ce2 chore: adopt stable React Compiler lint rules (#4737) 2026-08-20 14:17:40 +02:00
Chris Arderne 4d040e13be chore(webapp): scope imperative component refs (#4730)
## Summary

Scopes React Compiler diagnostics to component and hook statements where
refs intentionally coordinate editors, animations, polling, deferred
callbacks, and other imperative integrations. Other compiler diagnostics
remain active in those components.
2026-08-20 09:59:45 +01:00
Chris Arderne a89ce5a709 refactor(webapp): replace render-time ref initialization (#4729)
## Summary

Replaces render-time ref initialization with lazy state for frozen form
defaults, the tooltip's virtual positioning element, and the side menu's
first-paint visuals. Editable alert fields now update immutable state
snapshots.
2026-08-20 09:59:44 +01:00
Chris Arderne 7673c46a02 chore(webapp): scope component effect synchronization (#4727)
## Summary

Scopes React Compiler diagnostics for component and hook effects that
intentionally synchronize with navigation, submissions, browser APIs,
streams, timers, or authoritative server values. Each suppression stays
on the reported synchronization call rather than disabling analysis for
the component.
2026-08-20 09:59:43 +01:00
Chris Arderne 101883c41c refactor(webapp): derive controlled UI state during render (#4726)
## Summary

Derives controlled tab, tag, and checkbox values directly during render
instead of copying them through effects. Modal drafts now reset from
their open event, and the route-backed alert dialog renders open
immediately without a mount-time state update.
2026-08-20 09:59:43 +01:00
Chris Arderne 00149675ac chore(webapp): scope intentional draft synchronization (#4725)
## Summary

Scopes state synchronization that intentionally resets editable drafts
from authoritative server values, deployment state, or programmatic
filter changes. These values cannot be derived during render without
removing user control between resets.
2026-08-20 09:59:42 +01:00
Chris Arderne f723e5a1b8 refactor(webapp): simplify manual memoization (#4722)
## Summary

Removes manual memoization where derived values are already rebuilt each
render, narrows the dashboard watch callback to a stable chat
identifier, and scopes two intentional memoization patterns that protect
local edits and serialized synchronization.
2026-08-20 09:59:42 +01:00
Chris Arderne cf96204c7f chore(webapp): scope memo dependency diagnostics (#4721)
## Summary

Makes stable dashboard history refs explicit memo inputs and scopes the
remaining compiler diagnostics to callbacks whose local handlers or
lifetime-stable values cannot be represented accurately in dependency
arrays.
2026-08-20 09:59:41 +01:00
Chris Arderne 7682a215db fix(webapp): stabilize time-sensitive UI renders (#4720)
## Summary

Captures chat-history age when the menu opens so rerenders cannot change
labels mid-view. The waitpoint deadline form also reuses one intentional
wall-clock snapshot for all calculations in a render.
2026-08-20 09:59:41 +01:00
Chris Arderne 11ea1f8ba9 fix(webapp): use stable chart bucket timestamps (#4717)
## Summary

Uses explicit bucket timestamps when rendering usage charts instead of
anchoring missing timestamps to the current render time. Tooltips now
remain stable across rerenders, and examples use a deterministic
timestamp.
2026-08-20 09:59:40 +01:00
Chris Arderne 7ea02716fc fix(webapp): avoid mutating render inputs (#4716)
## Summary

Keeps render inputs and shared regular expressions immutable. Grouped
selects now compute each section's shortcut offset directly from
preceding sections, which also makes numeric shortcuts follow the
displayed item order reliably.
2026-08-20 09:59:39 +01:00
Chris Arderne 176fb6daf4 fix(webapp): call hooks directly and unconditionally (#4715)
## Summary

Calls dashboard hooks directly instead of passing them as ordinary
callback values, and subscribes to optional Ariakit stores through an
unconditional hook. This keeps hook ordering stable while preserving the
existing behavior when a provider is absent.
2026-08-20 09:59:39 +01:00
Chris Arderne 6dfc54b75b chore(webapp): scope unsupported React Compiler diagnostics (#4713)
## Summary

Adds targeted lint suppressions for components built around libraries
that React Compiler intentionally declines to memoize, plus one
unsupported function-reference pattern. Each suppression is scoped to
the affected component so other compiler diagnostics remain actionable.
2026-08-20 09:59:39 +01:00
Chris Arderne 9dca03f682 chore: enforce exhaustive React hook dependencies (#4712)
## Summary

Enables exhaustive React Hook dependency checking and resolves the
existing violations across the dashboard and React hooks package.
Effects and callbacks now track current values without introducing
request, subscription, or render loops.

## Design

Dependencies are included directly when the hook lifecycle should follow
them. Timers, Remix fetchers, and realtime subscriptions use stable
callbacks or latest-value refs where restarting work would change
behavior.

Unnecessary memoization was removed where ordinary derivation is
clearer. Full lint and typechecks for the webapp and React hooks package
pass.
2026-08-20 09:59:38 +01:00
Chris Arderne 23c5619dd1 fix(webapp): enforce keyboard interaction safeguards (#4702)
## Summary

Enable keyboard-event and static-element interaction safeguards across
the dashboard.

Earlier stack changes move actionable behavior to native controls. This
final enforcement keeps narrowly documented exceptions for focus
forwarding, scoped Escape handling, CodeMirror focus, and pointer-driven
table column resizing.

`jsx-a11y/no-autofocus` remains disabled.

Base: [#4701](https://github.com/triggerdotdev/trigger.dev/pull/4701)
2026-08-19 16:35:47 +01:00
Chris Arderne 5ae24710e4 fix(webapp): use native selectable row controls (#4700)
## Summary

Use native controls for sortable columns and selectable prompt versions.

Table headers keep filter actions separate from sort buttons, prompt
version rows expose pressed state, and a redundant deployment click
interceptor is removed.

Base: [#4699](https://github.com/triggerdotdev/trigger.dev/pull/4699)
2026-08-19 16:35:46 +01:00
Chris Arderne 3d650248fb fix(webapp): use native time filter mode controls (#4699)
## Summary

Make time-filter mode selection keyboard accessible.

Duration and exact-range modes now use native pressed buttons. Nested
date, duration, and quick-select controls no longer depend on click
propagation blockers.

Base: [#4698](https://github.com/triggerdotdev/trigger.dev/pull/4698)
2026-08-19 16:35:46 +01:00
Chris Arderne 73c8a4d975 fix(webapp): use native controls for inline actions (#4698)
## Summary

Replace mouse-only dashboard actions with native buttons.

Copy, remove, and stop-generation controls now expose keyboard focus and
accessible names. Hover-revealed actions remain mounted so keyboard
users can discover them, and a decorative clipboard icon no longer
captures clicks.

Base: [#4697](https://github.com/triggerdotdev/trigger.dev/pull/4697)
2026-08-19 16:35:45 +01:00
Chris Arderne 3d156dfd75 fix(webapp): use native checkbox label semantics (#4697)
## Summary

Use native label and checkbox behavior for `CheckboxWithLabel` and
enforce `jsx-a11y/no-noninteractive-element-interactions`.

The component no longer simulates checkbox activation with click
handlers on non-interactive wrappers. Native change events now drive the
controlled checked state.

Base: [#4696](https://github.com/triggerdotdev/trigger.dev/pull/4696)
2026-08-19 16:35:45 +01:00
Chris Arderne 646141199e fix(webapp): enforce accessible control names (#4696)
## Summary

Require accessible names for dashboard controls.

Filter menu action items and chart color controls now expose explicit
names. The chart legend action uses a native button, while lint depth
and spacer-cell configuration match the rendered control structure.

Base: [#4695](https://github.com/triggerdotdev/trigger.dev/pull/4695)
2026-08-19 16:35:44 +01:00
Chris Arderne 4592fdf4d6 fix(webapp): enforce accessible image and role semantics (#4693)
## Summary

Enable foundational JSX accessibility checks for image text alternatives
and valid ARIA roles.

The avatar color picker now has an explicit accessible name and
decorative image alternative. Dashboard chat styling props no longer
reuse the reserved DOM `role` name.

Base: [#4692](https://github.com/triggerdotdev/trigger.dev/pull/4692)
2026-08-19 16:35:43 +01:00
Chris Arderne dda9504bdd fix(webapp): require explicit native button types (#4692)
## Summary

Add explicit types to native dashboard buttons and enforce
`react/button-has-type`.

This prevents action buttons from accidentally submitting a surrounding
form. Shared button primitives retain their caller-selected submit and
reset semantics with documented lint exceptions.

Base: [#4691](https://github.com/triggerdotdev/trigger.dev/pull/4691)
2026-08-19 16:35:42 +01:00
Chris Arderne a7a1e74fcb refactor(webapp): remove redundant React fragments (#4691)
## Summary

Remove redundant React fragments from the dashboard and enforce
`react/jsx-no-useless-fragment`.

The cleanup returns existing nodes, arrays, and empty states directly
without adding wrapper elements.

Base: [#4689](https://github.com/triggerdotdev/trigger.dev/pull/4689)
2026-08-19 16:35:42 +01:00
Chris Arderne a2cc315f40 perf(webapp): stabilize nested component identities (#4689)
## Summary

Keep component and renderer identities stable across dashboard renders.

Inline icon components, chart renderers, table cells, and select render
callbacks now use module-level implementations. Oxlint enforces the
pattern across the dashboard.

Base: [#4688](https://github.com/triggerdotdev/trigger.dev/pull/4688)
2026-08-19 16:35:41 +01:00
Chris Arderne 108f43ee9b fix(webapp,react-hooks): enforce stable hook ordering (#4688)
## Summary

Enforce stable React hook ordering in the dashboard and React hooks
package.

Conditional hook calls now keep a consistent order, and overloaded
realtime stream arguments are resolved before entering the shared hook
implementation.

Base: `main`
2026-08-19 16:35:41 +01:00
Chris Arderne 4dabfca1d5 feat(webapp,cli,core): list production project runtime updates (#4659) 2026-08-19 13:44:55 +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 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 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
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
Chris Arderne b4313c8199 feat: logs search v2 (#4615) 2026-08-18 14:59:46 +01: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
nicktrn 512a619ea8 fix(webapp): back to app returns to the current org (#4632)
## Summary

Following a link straight into an organization's settings (for example
the usage limit link in a billing email) and then clicking "Back to app"
took you to `/`, which resolves to whichever organization you last had
selected, not the one whose settings you were looking at. The button now
links to the organization in the URL, so you land back in the org you
came from.

The org index route already redirects to the best project in that org,
so the destination is unchanged apart from being the right org.

Account settings still links to `/`, since that page is not org scoped
and has no org to return to.
2026-08-16 19:16:39 +01:00
Eric Allam c0b84595a3 feat(webapp): hosted webhook ingress, delivery pipeline, and dashboard (#4344)
## Summary

The server half of hosted webhooks: the public ingress endpoint,
signature verification, the delivery pipeline (Postgres partitioned
storage + ClickHouse for ordering), the in-app partition manager, the
HTTP API, and the dashboard (Deliveries, Endpoints, and the in-app test
console).

The public SDK and docs half is #4537. That PR carries the user-facing
API (`webhook()`, `chat.event` / `chat.channels`, the
`@trigger.dev/slack` connector) and builds on the shared
`@trigger.dev/core` schemas that ship here.

## Shipping behind a flag

A `WEBHOOK_ENABLED` env var (default off) gates the public ingress route
and the engine worker plus partition cron, so merging and deploying this
changes nothing in production until it is flipped on per environment.
The dashboard is separately gated per org by the `hasWebhooksAccess`
feature flag.

## Note on packages

This PR includes the `@trigger.dev/core` schema additions the server
compiles against, but carries no changeset. Core is not consumed
independently of the SDK, so it is released together with the SDK via
#4537. Keeping its changeset off `main` means no release cut from `main`
publishes it early.
2026-08-16 14:33:42 +01:00