Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
ee88a8e
feat(webapp): switch themes from the account menu
samejr Aug 10, 2026
54939fe
feat(webapp): more options entry, theme select and a finer contrast s…
samejr Aug 10, 2026
41a9789
fix(webapp): stop the contrast label sticking, tighten the theme picker
samejr Aug 10, 2026
47d3246
feat(webapp): mark the default contrast on the slider
samejr Aug 10, 2026
4d5645d
fix(webapp): widen the theme select and correct the tooltip arrow
samejr Aug 10, 2026
4c6ddfb
fix(webapp): notch the contrast track at the default mark
samejr Aug 10, 2026
5c9de3d
fix(webapp): notch the contrast mark against the real page colour
samejr Aug 10, 2026
2a1e291
fix(webapp): widen the gap either side of the contrast mark
samejr Aug 10, 2026
74802cf
fix(webapp): thicken the contrast mark line to 2px
samejr Aug 10, 2026
e0a58ca
feat(webapp): click the contrast mark to reset to the default
samejr Aug 10, 2026
0aeee45
feat(webapp): split icon and badge accents out into an Icon contrast …
samejr Aug 10, 2026
34ad34f
feat(webapp): add an App sidebar row and descriptions to the profile …
samejr Aug 10, 2026
ade0691
fix(webapp): tighten settings row descriptions
samejr Aug 10, 2026
07d62a1
feat(webapp): open the customize sidebar modal from the profile page
samejr Aug 10, 2026
f1b7413
fix(webapp): apply the theme on click, not on the loader's next pass
samejr Aug 11, 2026
23533dd
fix(webapp): brighten the secondary button on hover in the dark themes
samejr Aug 11, 2026
77ed110
fix(webapp): brighten the secondary select on hover in the dark themes
samejr Aug 11, 2026
b80cc8c
feat(webapp): add a surround-free switch variant and move it to indigo
samejr Aug 11, 2026
e0b7a8a
feat(webapp): drop the switch surround on the onboarding emails row
samejr Aug 11, 2026
3037a21
fix(webapp): dim the contrast handle and brighten it on hover
samejr Aug 11, 2026
d9866d8
fix(webapp): step the Customize button and theme select down a size
samejr Aug 11, 2026
641dc25
feat(webapp): floor the contrast slider at 15% and mark the default a…
samejr Aug 11, 2026
bb48233
feat(webapp): add an Underline links preference
samejr Aug 11, 2026
f2adfeb
chore(webapp): temporary underline audit story
samejr Aug 11, 2026
c9e6590
refactor(webapp): route hand-rolled links through TextLink
samejr Aug 11, 2026
1afd029
Merge remote-tracking branch 'origin/main' into samejr/appearance-the…
samejr Aug 12, 2026
5175479
feat(webapp): add Black and White themes
samejr Aug 12, 2026
948db74
feat(webapp): swap the Black and White icons with the active theme
samejr Aug 12, 2026
38dc6a5
feat(webapp): use a custom monitor icon for the System theme
samejr Aug 12, 2026
ab1f24e
feat(webapp): split Theme from Interface and let System pick its two …
samejr Aug 12, 2026
f4ef1b3
fix(webapp): rename the Theme section to Appearance
samejr Aug 12, 2026
f44e574
feat(webapp): slide the system Light and Dark rows open and shut
samejr Aug 12, 2026
b4cfb15
chore(webapp): remove the temporary underline audit story
samejr Aug 12, 2026
1570540
Merge remote-tracking branch 'origin/main' into samejr/appearance-the…
samejr Aug 12, 2026
3b65be0
fix(webapp): treat the White theme as light, not dark
samejr Aug 12, 2026
9cb6fc1
fix(webapp): keep the button text color when the label isn't plain text
samejr Aug 12, 2026
e9080b2
fix(webapp): give loading spinners a color that suits the theme
samejr Aug 12, 2026
aec652d
fix(webapp): swap the last hard-coded greys for theme tokens
samejr Aug 12, 2026
e09b00e
fix(webapp): list White above Black in the theme picker
samejr Aug 12, 2026
9ac591f
fix(webapp): stop CopyButton nesting a button inside its tooltip trigger
samejr Aug 13, 2026
b9a30e7
feat(webapp): give the storybook a theme switcher and a categorised s…
samejr Aug 13, 2026
b443b2d
feat(webapp): cover the remaining components in the storybook
samejr Aug 13, 2026
7040f45
fix(webapp): stop a ClickHouse outage crashing the dev server
samejr Aug 13, 2026
267ab71
feat(webapp): add theme shortcuts, an icon contrast toggle and an H2 …
samejr Aug 13, 2026
305c8b3
feat(webapp): show a story's component file names, copyable on hover
samejr Aug 13, 2026
10e1fde
feat(webapp): complete the storybook's variant coverage
samejr Aug 13, 2026
a385493
feat(webapp): make Dark the default theme and retire Classic
samejr Aug 13, 2026
4a6e9d3
feat(webapp): move the light themes onto ramp travel too
samejr Aug 13, 2026
cef20b9
fix(webapp): let light's whole surface stack respond to contrast
samejr Aug 13, 2026
59dbe5b
fix(webapp): keep light's surfaces distinct and soften its grid lines
samejr Aug 13, 2026
cbe3788
fix(webapp): hover text links by color, never by adding an underline
samejr Aug 14, 2026
bcb48e7
fix(webapp): move hand-rolled text links onto the shared treatment
samejr Aug 14, 2026
1ddf734
fix(webapp): let markdown prose links follow the underline preference
samejr Aug 14, 2026
bc1d745
fix(webapp): make the text link hover big enough to see
samejr Aug 14, 2026
9d7fc59
refactor(webapp): give every text link one theme-aware color
samejr Aug 14, 2026
638649c
refactor(webapp): move the last hand-rolled links onto TextLink
samejr Aug 14, 2026
838c10f
fix(webapp): let the switch's checked fill follow the theme
samejr Aug 14, 2026
e67da93
fix(webapp): give the primary button and the switch one shared accent
samejr Aug 14, 2026
3faf71c
fix(webapp): drop the stray full stop from the Dark system setting row
samejr Aug 14, 2026
abf3f19
feat(webapp): update each profile field on its own instead of one big…
samejr Aug 14, 2026
2cf0672
fix(webapp): let log level chips and a few stray accents follow the t…
samejr Aug 14, 2026
6682a3d
fix(webapp): show the Stronger colors example chip on hover, not in a…
samejr Aug 14, 2026
781fb4a
feat(webapp): move the profile name and email onto the row, behind an…
samejr Aug 14, 2026
e0bdb56
fix(webapp): even up the switch handle's padding at medium size
samejr Aug 15, 2026
0eb7c31
fix(webapp): show a Stronger colors example that changes on the light…
samejr Aug 15, 2026
1cf2f57
feat(webapp): give settings sections more room between them
samejr Aug 15, 2026
8928841
refactor(webapp): rename the accessibility storybook page to Stronger…
samejr Aug 15, 2026
257d8d4
feat(webapp): add line and stacked bar examples to the Stronger color…
samejr Aug 15, 2026
af50664
fix(webapp): keep the pulsing dot centred in its own ring
samejr Aug 18, 2026
c6310fa
fix(webapp): keep the rounded badge's label readable on the light themes
samejr Aug 18, 2026
54933e9
feat(webapp): give the theme a primary accent, and make preview blue …
samejr Aug 18, 2026
42b1217
refactor(webapp): fold Theme tokens into a Colors page showing every …
samejr Aug 18, 2026
81bacb1
TEMP: force the SSO variant of the email modal for review
samejr Aug 18, 2026
3f728e8
fix(webapp): say single sign-on in the email modal, not identity prov…
samejr Aug 18, 2026
e86b2ae
Revert "TEMP: force the SSO variant of the email modal for review"
samejr Aug 18, 2026
5715d88
fix(webapp): tighten the spacing above the SSO email message
samejr Aug 18, 2026
b3a87a6
docs(server-changes): note the new themes and appearance options
samejr Aug 19, 2026
59b084a
fix(webapp): show Debug in the log level filter, using the same chip …
samejr Aug 19, 2026
10a1a0a
fix(webapp): pick the avatar letter colour that reads on its own tile
samejr Aug 19, 2026
7dae4cc
fix(webapp): stop the Colors tables repeating every theme inside ever…
samejr Aug 19, 2026
8d80632
Merge remote-tracking branch 'origin/main' into samejr/appearance-the…
samejr Aug 19, 2026
90f0495
feat(webapp): restore seven primitives so the storybook can document …
samejr Aug 19, 2026
61b390a
Revert "feat(webapp): restore seven primitives so the storybook can d…
samejr Aug 19, 2026
6cc233b
fix(webapp): stop the Colors page pulling Prisma into the client bundle
samejr Aug 19, 2026
af69dbb
feat(webapp): name the resolved colour on every swatch in the Colors …
samejr Aug 19, 2026
4884327
fix(webapp): lift the neutral run statuses to 3:1 on the light themes
samejr Aug 19, 2026
6cfe2aa
feat(webapp): fill tinted badges solid under Stronger colors
samejr Aug 19, 2026
5cde277
fix(webapp): darken the added-line green in feature flag diffs
samejr Aug 19, 2026
117282e
fix(webapp): give the TRACE log chip a colour to render in
samejr Aug 20, 2026
3ba5184
fix(webapp): soften the contrast handle's hover on the light themes
samejr Aug 20, 2026
263a365
fix(webapp): drop the inset ring from the Trigger Agent badges
samejr Aug 20, 2026
dc61767
fix(webapp): halve the contrast handle's hover step again on the ligh…
samejr Aug 20, 2026
9e3df84
fix(webapp): drop the chip ring on the light themes where the chip fi…
samejr Aug 20, 2026
0e89f7e
fix(webapp): keep the contrast ring off every chip that fills solid
samejr Aug 20, 2026
8bbfee2
fix(webapp): give the secondary segmented control a track in every theme
samejr Aug 20, 2026
7df6f71
fix(webapp): stop the slider's value label sticking after a drag ends…
samejr Aug 20, 2026
a344664
feat(webapp): let the Black theme take the contrast slider below its …
samejr Aug 20, 2026
2110993
fix(webapp): make the contrast slider's negative half actually apply
samejr Aug 20, 2026
4360b63
fix(webapp): stop a negative contrast being thrown away on read, and …
samejr Aug 21, 2026
9cfaf36
refactor(webapp): make the contrast percentage mean the same thing in…
samejr Aug 21, 2026
674559e
Merge branch 'main' into samejr/appearance-theme-toggle
samejr Aug 21, 2026
deb7165
test(webapp): update the theme preference tests for the new theme set
samejr Aug 21, 2026
47fe9c6
docs(webapp): cut the branch's comments back to the load-bearing ones
samejr Aug 21, 2026
cd9e7af
fix(webapp): only lock the email field when an IdP really owns the ad…
samejr Aug 21, 2026
c8d7ec6
fix(webapp): address the review findings on the appearance work
samejr Aug 21, 2026
94c527f
fix(webapp): keep the query-only nav items behind query access
samejr Aug 21, 2026
54636f9
Merge remote-tracking branch 'origin/main' into samejr/appearance-the…
samejr Aug 21, 2026
837fafd
fix(webapp): drop the shadow from the tertiary slider handle
samejr Aug 21, 2026
8c3a8c9
fix(webapp): put the task activity chart's tabs on the shared title bar
samejr Aug 21, 2026
a37995a
style(webapp): line the chart tab bar up with the plain card title
samejr Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(webapp): stop a ClickHouse outage crashing the dev server
The env dashboard defers its analytics queries: the presenters create the
promises eagerly and the route only subscribes via typeddefer after further
awaits. With ClickHouse unreachable the queries reject instantly — inside
that gap — and Node treats the not-yet-subscribed rejection as unhandled and
kills the whole server. The agent sparkline promises aren't consumed by the
unified path at all, so with agents present those reject unhandled every
time.

Each deliberately un-awaited promise now carries a no-op backstop catch at
creation. Consumers still receive rejections (the stats chips keep their
error state); the process just no longer dies. With ClickHouse down the
dashboard now renders with failed-stats placeholders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  • Loading branch information
samejr and claude committed Aug 13, 2026
commit 7040f45d275404170e8fa3cefca773e013ebef74
21 changes: 13 additions & 8 deletions apps/webapp/app/presenters/v3/AgentListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type ClickHouse } from "@internal/clickhouse";
import { z } from "zod";
import { $replica } from "~/db.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { backstopPromise } from "~/utils/backstopPromise";
import { singleton } from "~/utils/singleton";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";

Expand Down Expand Up @@ -97,15 +98,19 @@ export class AgentListPresenter {
};
}

// All queries are deferred for streaming
const activeStates = this.#getActiveStates(clickhouse, environmentId, slugs);
const conversationSparklines = this.#getConversationSparklines(
clickhouse,
environmentId,
slugs
// All queries are deferred for streaming. Backstopped: consumers subscribe
// late (or, for some callers, not at all), and an unhandled rejection in
// the gap kills the server.
const activeStates = backstopPromise(this.#getActiveStates(clickhouse, environmentId, slugs));
const conversationSparklines = backstopPromise(
this.#getConversationSparklines(clickhouse, environmentId, slugs)
);
const costSparklines = backstopPromise(
this.#getCostSparklines(clickhouse, environmentId, slugs)
);
const tokenSparklines = backstopPromise(
this.#getTokenSparklines(clickhouse, environmentId, slugs)
);
const costSparklines = this.#getCostSparklines(clickhouse, environmentId, slugs);
const tokenSparklines = this.#getTokenSparklines(clickhouse, environmentId, slugs);

return { agents, activeStates, conversationSparklines, costSparklines, tokenSparklines };
}
Expand Down
23 changes: 13 additions & 10 deletions apps/webapp/app/presenters/v3/TaskListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ClickHouseEnvironmentMetricsRepository,
type CurrentRunningStats,
} from "~/services/environmentMetricsRepository.server";
import { backstopPromise } from "~/utils/backstopPromise";
import { singleton } from "~/utils/singleton";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";

Expand Down Expand Up @@ -84,16 +85,18 @@ export class TaskListPresenter {
});

// IMPORTANT: Don't await this, we want to return the promise
// so we can defer the loading of the data. The caller is responsible for
// consuming it — an unconsumed promise here would become an unhandled
// rejection if the underlying query fails.
const runningStats = environmentMetricsRepository.getCurrentRunningStats({
organizationId,
projectId,
environmentId,
days: 6,
tasks: slugs,
});
// so we can defer the loading of the data. Backstopped because the caller
// only subscribes after further awaits — with ClickHouse down this rejects
// instantly, and an unhandled rejection kills the whole server.
const runningStats = backstopPromise(
environmentMetricsRepository.getCurrentRunningStats({
organizationId,
projectId,
environmentId,
days: 6,
tasks: slugs,
})
);

return { tasks, runningStats };
}
Expand Down
41 changes: 25 additions & 16 deletions apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { z } from "zod";
import { $replica } from "~/db.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { backstopPromise } from "~/utils/backstopPromise";
import { singleton } from "~/utils/singleton";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { agentListPresenter, type AgentActiveState } from "./AgentListPresenter.server";
Expand Down Expand Up @@ -69,26 +70,34 @@ export class UnifiedTaskListPresenter {
const items = toUnifiedItems(taskResult.tasks, agentResult.agents);
const allSlugs = items.map((item) => item.slug);

// Both deferred promises below are backstopped: the route only subscribes
// (via typeddefer) after further awaits, so a rejection landing in that
// gap — e.g. ClickHouse refusing connections — would otherwise be an
// unhandled rejection and take the server down. Awaiting them still
// rejects into the route's error elements.
const hourlyActivity: Promise<HourlyTaskActivity> =
allSlugs.length === 0
? Promise.resolve({})
: (async () => {
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
args.organizationId,
"standard"
);
return getHourlyTaskActivity(clickhouse, {
organizationId: args.organizationId,
projectId: args.projectId,
environmentId: args.environmentId,
slugs: allSlugs,
});
})();
: backstopPromise(
(async () => {
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
args.organizationId,
"standard"
);
return getHourlyTaskActivity(clickhouse, {
organizationId: args.organizationId,
projectId: args.projectId,
environmentId: args.environmentId,
slugs: allSlugs,
});
})()
);

const runningStates: Promise<UnifiedRunningStates> = Promise.all([
taskResult.runningStats,
agentResult.activeStates,
]).then(([runningStats, activeStates]) => mergeRunningStates(runningStats, activeStates));
const runningStates: Promise<UnifiedRunningStates> = backstopPromise(
Promise.all([taskResult.runningStats, agentResult.activeStates]).then(
([runningStats, activeStates]) => mergeRunningStates(runningStats, activeStates)
)
);

return { items, hourlyActivity, runningStates };
}
Expand Down
15 changes: 15 additions & 0 deletions apps/webapp/app/utils/backstopPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Marks a deliberately un-awaited promise as handled without consuming it.
*
* For promises created eagerly and consumed later — handed to Remix `defer()`,
* or composed into another promise after further awaits — a rejection that
* lands before the consumer subscribes counts as an unhandled rejection, and
* Node brings the whole process down on those. The no-op catch here is a
* separate branch: awaiting the returned promise still rejects as normal
* (e.g. into a `<TypedAwait errorElement>`), and a branch nobody ever awaits
* simply logs nothing instead of crashing the server.
*/
export function backstopPromise<T>(promise: Promise<T>): Promise<T> {
promise.catch(() => {});
return promise;
}
Loading