From 230c358b75291545e6408bc0fac4c429ba3fb91b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 19:21:09 +0100 Subject: [PATCH 01/55] feat(webapp): move Pause environment control to the queues filter bar (TRI-12309) Move the environment pause/resume button out of the Queued stat tile into the top-right of the queues filter bar, and give it a visible label ("Pause/Resume {environment} environment") instead of icon-only. --- .../route.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 1a9ef75c27..161f1e5482 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -502,12 +502,18 @@ function QueuesWithMetricsView() { shortcut={{ key: "d" }} /> - +
+ {environment.runsEnabled && + env.pauseSource !== ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT ? ( + + ) : null} + +
) : null} @@ -524,23 +530,17 @@ function QueuesWithMetricsView() { suffix={env.paused ? paused : undefined} animate accessory={ -
- {environment.runsEnabled && - env.pauseSource !== ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT ? ( - - ) : null} - -
+ } valueClassName={env.paused ? "text-warning tabular-nums" : "tabular-nums"} compactThreshold={1000000} @@ -1062,7 +1062,7 @@ function EnvironmentPauseResumeButton({ From d0a34a021ca44f7f7f848c6758ae0d87280a9208 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 19:28:07 +0100 Subject: [PATCH 02/55] feat(webapp): make the whole queues table row clickable (TRI-12311) The "Limited by" cell was the only data cell not linking to the queue detail page, so clicking it did nothing. Make it navigable like the rest of the row (matching the runs-table pattern); the override explainer moves to an info-icon tooltip rendered beside the link so the button never nests inside the row's . --- .../route.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 161f1e5482..2a5a63e8df 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -865,26 +865,35 @@ function QueuesWithMetricsView() { )} , and the label itself stays the link. + trailingContent={ + queue.concurrency?.overriddenAt ? ( + + ) : undefined + } > {queue.concurrency?.overriddenAt ? ( - Override} - content={ - queue.concurrencyLimitOverridePercent !== null - ? `Overridden at ${formatOverridePercent( - queue.concurrencyLimitOverridePercent - )}% of the environment limit.` - : `This queue's concurrency limit has been manually overridden to ${limit}.` - } - className="max-w-xs" - disableHoverableContent - /> + Override ) : queue.concurrencyLimit ? ( "User" ) : ( From e69fd49398c0d7afa4e274ea665736de96f746c5 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 19:30:48 +0100 Subject: [PATCH 03/55] feat(webapp): only the sort arrows toggle table sorting, not the whole header (TRI-12312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In TableHeaderCell, clicking anywhere on a sortable header used to sort (the label + arrows were one full-width button). Now only the sort-arrows icon is the button; the label (and any info tooltip) are plain, non-clickable content. Shared primitive change — affects every sortable table. --- .../app/components/primitives/Table.tsx | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 93372f7f8e..761239e8f1 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -271,40 +271,19 @@ export const TableHeaderCell = forwardRef {sortable ? ( - // Order is always title → info icon → sort arrows. The info trigger is itself a - {tooltip ? ( - <> - {tooltipNode} - - - ) : null} ) : tooltip ? (
From 632df63deb2f0ff967a463e5fdf1c341667da020 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 19:33:54 +0100 Subject: [PATCH 04/55] feat(webapp): tighten spacing between metric-page containers to the 2 scale (TRI-12313) Normalize the gaps between content containers (stat/chart grid gap, the vertical block gap, and the content-region gap) from 3 (0.75rem) to 2 (0.5rem) in the shared MetricsLayout. Page gutters are left at 3 so left edges stay aligned with the filter bar. Affects all metric/dashboard pages using MetricsLayout. --- apps/webapp/app/components/layout/MetricsLayout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 6ae40778af..8f9d1fdfd9 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -178,7 +178,7 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
@@ -306,7 +306,7 @@ function MetricsLayoutGrid({ return (
{children}
; + return
{children}
; } export const MetricsLayout = { From dd04bae65a53c03edce350fd7dbdfd3eadcbaf61 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 19:48:10 +0100 Subject: [PATCH 05/55] feat(webapp): consolidate the queue detail header into one top bar (TRI-12315, TRI-12316) Move the Overview/Concurrency-keys tabs up into the filter bar (tabs left, inset by pl-2), with the date filter and the relocated Override limit + Pause {queue} queue buttons on the right (gap-1.5). Both controls move out of the Concurrency panel and gain visible labels; the override button gets an explanatory tooltip. Adds withQueueName to QueuePauseResumeButton and a labeled+tooltip 'button' trigger to QueueOverrideConcurrencyButton. --- .../app/components/queues/QueueControls.tsx | 57 +++++++++++----- .../route.tsx | 67 +++++++++---------- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 9c4302d06e..0ee2fde929 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -32,6 +32,7 @@ export function QueuePauseResumeButton({ fullWidth = false, showTooltip = true, iconOnly = false, + withQueueName = false, }: { /** The "id" here is a friendlyId */ queue: { id: string; name: string; paused: boolean }; @@ -41,6 +42,8 @@ export function QueuePauseResumeButton({ /** Icon-only trigger (label moves to the tooltip). For compact placements like the detail-page * live blocks. */ iconOnly?: boolean; + /** Render the full "Pause/Resume {name} queue" label instead of the short "Pause"/"Resume". */ + withQueueName?: boolean; }) { const [isOpen, setIsOpen] = useState(false); @@ -64,7 +67,13 @@ export function QueuePauseResumeButton({ textAlignLeft={fullWidth} aria-label={label} > - {iconOnly ? undefined : queue.paused ? "Resume" : "Pause"} + {iconOnly + ? undefined + : withQueueName + ? `${queue.paused ? "Resume" : "Pause"} ${queue.name} queue` + : queue.paused + ? "Resume" + : "Pause"}
@@ -209,24 +218,38 @@ export function QueueOverrideConcurrencyButton({ + ) : trigger === "button" ? ( + + + +
+ + + +
+
+ + Set a custom concurrency limit for this queue, overriding the environment default — as + an absolute number or a percentage of the environment limit. + +
+
) : ( - {trigger === "button" ? ( - - ) : ( - - )} + )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index a2792b4f4d..361228b095 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -245,7 +245,25 @@ export default function Page() { everything, like the Queues list. The time filter scopes the tab charts; search filters the keys table. The bar is pinned by the layout while the page scrolls. */} -
+
+ + replace({ view: undefined, key: undefined })} + > + Overview + + replace({ view: "keys" })} + > + Concurrency keys + + +
+
{view === "keys" && hasKeys ? ( ) : null} @@ -257,6 +275,16 @@ export default function Page() { valueClassName="text-text-bright" shortcut={{ key: "d" }} /> + +
@@ -275,23 +303,6 @@ export default function Page() { {/* Tabs + charts share the padded (inset) column. Both tabs always render; the keys tab shows an empty state when the queue has no concurrency keys. */} - - replace({ view: undefined, key: undefined })} - > - Overview - - replace({ view: "keys" })} - > - Concurrency keys - - - {view === "keys" ? ( hasKeys ? ( - - - -
- } - /> + Date: Wed, 22 Jul 2026 19:53:49 +0100 Subject: [PATCH 06/55] feat(webapp): add inline legends explaining the orange warning line on queue charts (TRI-12317) The header charts turn the line orange as a warning (saturation over the env limit, p95 over 1 min, any throttling) but nothing labelled the colour. Add a small inline legend (rounded colour square + label) below the title on Env saturation, Scheduling delay p95, and Throttled. Backlog has no warning colour so it gets no legend. --- .../route.tsx | 84 +++++++++++++------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 2a5a63e8df..892f23232a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1198,6 +1198,9 @@ type QueueHeaderTile = { /** Info-icon copy explaining what the chart shows, rendered next to the card title. */ description: string; color: string; + /** Optional inline legend rendered below the card title: a fixed set of {colored square, label} + * entries, for charts where a colour (e.g. the orange warning line) needs explaining. */ + legend?: Array<{ color: string; label: string }>; query: string; /** Formats a single bucket's value in the chart tooltip. */ formatValue?: (value: number) => string; @@ -1260,6 +1263,10 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ label: "Env saturation", description: "Running as a share of the environment limit. Yellow over 100% (burst headroom).", color: "var(--color-queues)", + legend: [ + { color: "var(--color-queues)", label: "Saturation" }, + { color: "var(--color-warning)", label: "Over limit" }, + ], // Numerator: running summed across the visible set. Denominator: the env-wide limit (same for // every queue in a bucket), so the line reads as the set's share of the environment capacity. query: `SELECT timeBucket() AS t,\n queue,\n max(max_running) AS running,\n max(max_env_limit) AS env_limit\nFROM queue_metrics\nGROUP BY t, queue\nORDER BY t`, @@ -1297,6 +1304,10 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ description: "p95 wait from eligible to dequeued. Yellow over 1 min.", totalTooltip: "The worst p95 in the selected window.", color: "var(--color-queues)", + legend: [ + { color: "var(--color-queues)", label: "p95" }, + { color: "var(--color-warning)", label: "Over 1 min" }, + ], // quantilesMerge over the set's rows in a bucket is the true p95 across the union of samples // (merging quantile states is valid; averaging per-queue percentiles would not be). query: `SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS p95\nFROM queue_metrics\nGROUP BY t\nORDER BY t`, @@ -1319,6 +1330,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ description: "Times dequeuing was blocked by a limit.", totalTooltip: "The share of the selected window with at least one blocked dequeue.", color: "var(--color-queues)", + legend: [{ color: "var(--color-warning)", label: "Throttled" }], query: `SELECT timeBucket() AS t,\n sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t`, derive: (rows) => { const points = rows.map((r) => ({ @@ -1430,35 +1442,53 @@ function QueueEnvMetricChart({ return ( - - {tile.label} - + + + + {tile.label} + + + {peak != null ? ( + tile.totalTooltip && !showLoading ? ( + + {peak} + + } + content={tile.totalTooltip} + className="max-w-xs" + disableHoverableContent + /> + ) : ( + + {peak} + + ) + ) : null} - {peak != null ? ( - tile.totalTooltip && !showLoading ? ( - + {tile.legend.map((item) => ( + - {peak} - - } - content={tile.totalTooltip} - className="max-w-xs" - disableHoverableContent - /> - ) : ( - - {peak} - - ) + className="size-2.5 rounded-[3px]" + style={{ backgroundColor: item.color }} + /> + {item.label} + + ))} + ) : null} } From 45f3e66cccf2699db09e3a9797749a89c87eeb2d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:01:46 +0100 Subject: [PATCH 07/55] feat(webapp): cap tooltip descriptions at 230px (TRI-12318) Bake a max-w-[230px] default into the shared TooltipContent so no tooltip sprawls too wide, and converge the queue-page tooltips that were pinned at max-w-xs (320px) onto the same 230px cap. --- apps/webapp/app/components/primitives/Tooltip.tsx | 2 +- .../app/components/queues/QueueMetricCards.tsx | 4 ++-- .../route.tsx | 12 ++++++------ .../route.tsx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index cb9eaf0364..6f12e408c7 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -42,7 +42,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden", + "z-50 max-w-[230px] overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden", variantClasses[variant], className )} diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index b7694eade4..7fef23624e 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -206,7 +206,7 @@ export function QueueMetricChartCard({ info || titleAccessory ? ( {title} - {info ? : null} + {info ? : null} {titleAccessory} ) : ( @@ -336,7 +336,7 @@ export function QueueSparklineStat({ ) : null}
} - contentClassName="max-w-xs" + contentClassName="max-w-[230px]" /> ) : null} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 892f23232a..bfabf89d2a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -797,7 +797,7 @@ function QueuesWithMetricsView() { } content="At concurrency limit: this queue is running as many runs as its limit allows; new runs wait in the backlog." - className="max-w-xs" + className="max-w-[230px]" disableHoverableContent /> ) : null @@ -886,7 +886,7 @@ function QueuesWithMetricsView() { )}% of the environment limit.` : `This queue's concurrency limit has been manually overridden to ${limit}.` } - contentClassName="max-w-xs" + contentClassName="max-w-[230px]" disableHoverableContent /> ) : undefined @@ -1446,7 +1446,7 @@ function QueueEnvMetricChart({ {tile.label} - + {peak != null ? ( tile.totalTooltip && !showLoading ? ( @@ -1462,7 +1462,7 @@ function QueueEnvMetricChart({ } content={tile.totalTooltip} - className="max-w-xs" + className="max-w-[230px]" disableHoverableContent /> ) : ( @@ -1828,7 +1828,7 @@ function ClassicQueuesView() { } content="This queue's concurrency limit has been manually overridden from the dashboard or API." - className="max-w-xs" + className="max-w-[230px]" disableHoverableContent /> ) : null} @@ -2012,7 +2012,7 @@ function BurstFactorTooltip({ }, but you can burst up to ${ environment.burstFactor * environment.concurrencyLimit } when across multiple queues/tasks.`} - contentClassName="max-w-xs" + contentClassName="max-w-[230px]" /> ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 361228b095..cb4be417d1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -603,7 +603,7 @@ function chartTitleWithInfo(title: string, info: string) { return ( {title} - + ); } From f98d9e739c1c36db7b976fa1fd7a374f7bcfba36 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:12:06 +0100 Subject: [PATCH 08/55] fix(webapp): cache metric-chart results so tab switches and back-nav don't reload charts (TRI-12321) useMetricResourceQuery reset to rows=null on every mount, so remounting a chart (switching queue-detail tabs, or navigating back to the queues list) flashed a loading skeleton and refetched. Add a bounded module-level cache keyed by the query signature: a remounted chart paints its last rows immediately and revalidates in the background instead of reloading. --- .../app/hooks/useMetricResourceQuery.ts | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/hooks/useMetricResourceQuery.ts b/apps/webapp/app/hooks/useMetricResourceQuery.ts index 2a2ee413af..53b7c03af8 100644 --- a/apps/webapp/app/hooks/useMetricResourceQuery.ts +++ b/apps/webapp/app/hooks/useMetricResourceQuery.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useInterval } from "./useInterval"; export type MetricResourceRow = Record; @@ -24,16 +24,32 @@ export type MetricResourceQueryOptions = { refreshIntervalMs?: number; }; +// Module-level cache of the last successful rows per query signature. Lets a remounted chart +// (switching tabs, or navigating back to the queues list) paint its previous data immediately +// instead of flashing a loading skeleton every time, while it revalidates in the background. +// Bounded so it can't grow without limit over a long session. +const responseCache = new Map(); +const RESPONSE_CACHE_MAX = 200; + +function cacheSet(key: string, rows: MetricResourceRow[]) { + // Re-insert so the key becomes the most-recently-used (Map preserves insertion order). + responseCache.delete(key); + responseCache.set(key, rows); + if (responseCache.size > RESPONSE_CACHE_MAX) { + const oldest = responseCache.keys().next().value; + if (oldest !== undefined) responseCache.delete(oldest); + } +} + /** * Client-fetch a TRQL query from the metric resource route (like the dashboard * widgets): own loading state, interval + on-focus refresh, abort on change/unmount. + * + * Successful results are cached per query signature, so a chart that remounts (tab switch, or + * back-navigation to the queues list) shows its last data immediately and revalidates in the + * background rather than flashing a loading skeleton. */ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryOptions) { - const [rows, setRows] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [failed, setFailed] = useState(false); - const abortRef = useRef(null); - const { organizationId, projectId, @@ -44,11 +60,37 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO } = opts; const { period, from, to } = opts.timeRange; const queuesKey = opts.queues && opts.queues.length > 0 ? opts.queues.join(",") : undefined; + const resolvedPeriod = period ?? (from || to ? null : defaultPeriod); + const cacheKey = useMemo( + () => + [ + organizationId, + projectId, + environmentId, + resolvedPeriod ?? "", + from ?? "", + to ?? "", + fillGaps ? 1 : 0, + queuesKey ?? "", + query, + ].join("|"), + [organizationId, projectId, environmentId, resolvedPeriod, from, to, fillGaps, queuesKey, query] + ); + + const [rows, setRows] = useState( + () => responseCache.get(cacheKey) ?? null + ); + const [isLoading, setIsLoading] = useState(true); + const [failed, setFailed] = useState(false); + const abortRef = useRef(null); const load = useCallback(() => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; + // Paint cached rows immediately (no skeleton) on remount / key change while we revalidate. + const cached = responseCache.get(cacheKey); + if (cached) setRows(cached); setIsLoading(true); fetch("/resources/metric", { method: "POST", @@ -56,7 +98,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO body: JSON.stringify({ query, scope: "environment", - period: period ?? (from || to ? null : defaultPeriod), + period: resolvedPeriod, from, to, fillGaps: !!fillGaps, @@ -71,6 +113,7 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO .then((data) => { if (controller.signal.aborted) return; if (data.success) { + cacheSet(cacheKey, data.data.rows); setRows(data.data.rows); setFailed(false); } else { @@ -86,11 +129,11 @@ export function useMetricResourceQuery(query: string, opts: MetricResourceQueryO } }); }, [ + cacheKey, query, - period, + resolvedPeriod, from, to, - defaultPeriod, fillGaps, organizationId, projectId, From c608266fbcbcb1ef7165f11f07aae01335905c27 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:15:16 +0100 Subject: [PATCH 09/55] fix(webapp): match queue stat-tile containers to the chart-container styling (TRI-12323) The BigNumber stat tiles (Queued/Running/Allocated/Environment limit) and the detail-page ConcurrencyBlock used rounded-sm + border-grid-dimmed, which didn't match the chart cards' rounded-lg + border-grid-bright. Align both to the chart Card styling so the tiles and charts read as one system. --- apps/webapp/app/components/metrics/BigNumber.tsx | 2 +- .../route.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/metrics/BigNumber.tsx b/apps/webapp/app/components/metrics/BigNumber.tsx index f9e0ecdb5c..4a41656f90 100644 --- a/apps/webapp/app/components/metrics/BigNumber.tsx +++ b/apps/webapp/app/components/metrics/BigNumber.tsx @@ -40,7 +40,7 @@ export function BigNumber({ typeof compactThreshold === "number" && v !== undefined && v >= compactThreshold; return ( -
+
{title} {accessory &&
{accessory}
} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index cb4be417d1..38710edd21 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -1050,7 +1050,7 @@ function ConcurrencyBlock({ const atLimit = limit !== null && limit > 0 && running >= limit; const pct = limit && limit > 0 ? Math.min(100, Math.round((running / limit) * 100)) : 0; return ( -
+
Concurrency From 668a133ee3112950bd7e9252817d711ddbe4bbb9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:16:29 +0100 Subject: [PATCH 10/55] fix(webapp): tighten queues filter-bar item gap to 1.5 (TRI-12324) Both queues pages: filter-bar item gap is now gap-1.5. The list page's search/period and pause/pagination clusters move from gap-2 to gap-1.5; the detail page's cluster was already gap-1.5 (TRI-12315). --- .../route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index bfabf89d2a..3bafa2943b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -491,7 +491,7 @@ function QueuesWithMetricsView() { cluster = pagination. */} {success ? ( -
+
-
+
{environment.runsEnabled && env.pauseSource !== ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT ? ( From f665e11bd3f7663f5680b1d2e43df3b9ffb0289d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:17:54 +0100 Subject: [PATCH 11/55] fix(webapp): label the environment-panel Increase limit button, drop its tooltip (TRI-12328) The Increase limit control (Environment limit tile) was an icon-only button with the label only in a hover tooltip. Show "Increase limit" as the button text and remove the tooltip, for both the increase and the upgrade variants. --- .../route.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 3bafa2943b..f58874fdb6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -603,19 +603,21 @@ function QueuesWithMetricsView() { plan?.v3Subscription?.plan?.limits.concurrentRuns.canExceed ? ( + > + Increase limit + ) : ( + > + Increase limit + ) ) : undefined } From f11f240b002ee38678857f51fbc80416328a7f57 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:18:56 +0100 Subject: [PATCH 12/55] fix(webapp): dim the Allocated tile subtext (TRI-12331) The "NN% of the environment limit" caption on the Allocated tile was text-text-bright; switch it to the text-text-dimmed token so it reads as secondary subtext like the other tiles' captions. --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index f58874fdb6..820e4bbcfe 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -591,7 +591,7 @@ function QueuesWithMetricsView() { formattedValue={allocation ? undefined : "–"} valueClassName={cn(allocation && overAllocated && "text-warning")} suffix={allocation ? `${allocationPct}% of the environment limit` : undefined} - suffixClassName="text-text-bright" + suffixClassName="text-text-dimmed" /> Date: Wed, 22 Jul 2026 20:21:26 +0100 Subject: [PATCH 13/55] fix(webapp): keep all line-chart x-axis labels visible on hover (TRI-12337) ChartLine collapsed the x-axis to only the first + last tick while the tooltip was active. Remove that so every x-axis label stays visible at all times; recharts keeps its default preserveStartEnd ticks whether hovering or not. --- .../app/components/primitives/charts/ChartLine.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartLine.tsx b/apps/webapp/app/components/primitives/charts/ChartLine.tsx index 6adcf0484a..17fceee27c 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLine.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLine.tsx @@ -255,18 +255,13 @@ export function ChartLineRenderer({ return ; } - // Get the x-axis ticks based on tooltip state - const xAxisTicks = - highlight.tooltipActive && data.length > 2 - ? [data[0]?.[dataKey], data[data.length - 1]?.[dataKey]] - : undefined; - const xAxisConfig = { dataKey, tickLine: false, axisLine: false, tickMargin: 10, - ticks: xAxisTicks, + // Keep every x-axis label visible at all times, including on hover. Previously the axis + // collapsed to just the first + last tick while the tooltip was active. interval: "preserveStartEnd" as const, tick: { fill: "var(--color-text-dimmed)", From 2adcd10d7f47e9708ab04a541754acdd47a6438b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:26:55 +0100 Subject: [PATCH 14/55] =?UTF-8?q?fix(webapp):=20tidy=20queue=20tooltip=20c?= =?UTF-8?q?opy=20=E2=80=94=20inline=20colour=20swatch,=20colon=20separator?= =?UTF-8?q?s=20(TRI-12330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the word "Yellow" with an inline warning-coloured swatch (matching the chart's over-limit line) in the Env saturation, Scheduling delay p95, and Backlog tooltips. Swap the em dashes in the "Limited by" header tooltip for colons. The tile description type widens to ReactNode to carry the inline swatch. --- .../route.tsx | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 820e4bbcfe..20205f358b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -10,7 +10,7 @@ import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useNavigation, type MetaFunction } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon"; @@ -709,14 +709,14 @@ function QueuesWithMetricsView() { tooltip={

- Environment — the environment's + Environment: the environment's limit of {environment.concurrencyLimit}.

- User — a limit set in your code. + User: a limit set in your code.

- Override — set manually from the + Override: set manually from the dashboard or API.

@@ -737,7 +737,11 @@ function QueuesWithMetricsView() { + Runs waiting over the selected window. where throttled. + + } > Backlog @@ -1194,11 +1198,23 @@ type MetricTileRow = Record; /** One charted point per time bucket, already aggregated across the visible queue set. */ type TilePoint = { bucket: number; value: number }; +// Inline colour swatch matching the chart's warning ("yellow") line — used in tooltip copy that +// refers to that colour instead of naming it, so the swatch always matches the chart. +function WarningSwatch() { + return ( + + ); +} + type QueueHeaderTile = { id: string; label: string; /** Info-icon copy explaining what the chart shows, rendered next to the card title. */ - description: string; + description: ReactNode; color: string; /** Optional inline legend rendered below the card title: a fixed set of {colored square, label} * entries, for charts where a colour (e.g. the orange warning line) needs explaining. */ @@ -1263,7 +1279,12 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ { id: "saturation", label: "Env saturation", - description: "Running as a share of the environment limit. Yellow over 100% (burst headroom).", + description: ( + <> + Running as a share of the environment limit. Turns over 100% (burst + headroom). + + ), color: "var(--color-queues)", legend: [ { color: "var(--color-queues)", label: "Saturation" }, @@ -1303,7 +1324,11 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ { id: "p95", label: "Scheduling delay p95", - description: "p95 wait from eligible to dequeued. Yellow over 1 min.", + description: ( + <> + p95 wait from eligible to dequeued. Turns over 1 min. + + ), totalTooltip: "The worst p95 in the selected window.", color: "var(--color-queues)", legend: [ From 1b3e6802bf1525379861ac8b9924f16da9dca0fc Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Jul 2026 20:32:12 +0100 Subject: [PATCH 15/55] fix(webapp): restyle the override-concurrency modal (TRI-12338) Shrink the description text (variant=small); move the Number/Percent segmented control to the right of the concurrency-limit input (gap-2, matched height) instead of up by the label; show a % suffix in the field when Percent is selected; and let the label sit close above the input (InputGroup's gap-1.5). --- .../app/components/queues/QueueControls.tsx | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 0ee2fde929..388135c1a8 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -258,7 +258,7 @@ export function QueueOverrideConcurrencyButton({
{isOverridden ? ( - + This queue's concurrency limit is currently overridden to {currentLimit}. {typeof queue.concurrency?.base === "number" && ` The original limit set in code was ${queue.concurrency.base}.`}{" "} @@ -269,7 +269,7 @@ export function QueueOverrideConcurrencyButton({ . ) : ( - + Override this queue's concurrency limit. The current limit is {currentLimit}, which is set {queue.concurrencyLimit !== null ? "in code" : "by the environment"}. @@ -278,10 +278,39 @@ export function QueueOverrideConcurrencyButton({ -
- + +
+
+ {mode === "percent" ? ( + setPercent(e.target.value)} + placeholder="100" + autoFocus + accessory={%} + /> + ) : ( + setConcurrencyLimit(e.target.value)} + placeholder={currentLimit.toString()} + autoFocus + /> + )} +
{mode === "percent" ? ( - <> - setPercent(e.target.value)} - placeholder="100" - autoFocus - /> - - {materializedFromPercent !== null - ? `${percentNumber}% = ${materializedFromPercent} concurrent ${ - materializedFromPercent === 1 ? "run" : "runs" - } of the environment's ${environmentConcurrencyLimit}. Recalculates automatically when the environment limit changes.` - : "Enter a percentage between 1 and 100."} - - + + {materializedFromPercent !== null + ? `${percentNumber}% = ${materializedFromPercent} concurrent ${ + materializedFromPercent === 1 ? "run" : "runs" + } of the environment's ${environmentConcurrencyLimit}. Recalculates automatically when the environment limit changes.` + : "Enter a percentage between 1 and 100."} + ) : ( - <> - setConcurrencyLimit(e.target.value)} - placeholder={currentLimit.toString()} - autoFocus - /> - - {limitOverCap - ? `Can't exceed the environment limit of ${environmentConcurrencyLimit}.` - : `Up to the environment limit of ${environmentConcurrencyLimit}.`} - - + + {limitOverCap + ? `Can't exceed the environment limit of ${environmentConcurrencyLimit}.` + : `Up to the environment limit of ${environmentConcurrencyLimit}.`} + )} From e6a7cf4c071c092f07961e3f15f57c95d8cfccc7 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 10:31:48 +0100 Subject: [PATCH 16/55] fix(webapp): set metric-page container gaps and padding to 1.5 (TRI-12068) Both queue pages: the gaps between stat/chart containers and the padding around the metrics area move from 2 to 1.5 in the shared MetricsLayout. --- apps/webapp/app/components/layout/MetricsLayout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 8f9d1fdfd9..85da807ebd 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -178,7 +178,7 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
@@ -306,7 +306,7 @@ function MetricsLayoutGrid({ return (
{children}
; + return
{children}
; } export const MetricsLayout = { From 7d142a1ae319c998e53faa226ec7d72abc44b626 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 10:31:50 +0100 Subject: [PATCH 17/55] =?UTF-8?q?fix(webapp):=20chart=20legend=20tweaks=20?= =?UTF-8?q?=E2=80=94=20tighter=20swatch=20corners,=20more=20title=20gap=20?= =?UTF-8?q?(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce the legend colour-swatch corner radius (rounded-[3px] to rounded-[2px]) and increase the gap between the chart title and the legend row (gap-0.5 to gap-1). --- .../route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 20205f358b..12b1226522 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1469,7 +1469,7 @@ function QueueEnvMetricChart({ return ( + {tile.label} @@ -1509,7 +1509,7 @@ function QueueEnvMetricChart({ className="flex items-center gap-1 text-xs font-normal text-text-dimmed" > {item.label} From 06ed1f76ecc554b0b36b9ef7c5278b0dc3effe5f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 10:36:21 +0100 Subject: [PATCH 18/55] fix(webapp): make the whole Name table cell clickable via a stretched link (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For a TableCell with a link plus leading/trailing adornments, the only wrapped the text. Add an inset ::before overlay so the anchor covers the whole cell; the interactive adornments (type icon, badges) sit above it (relative z-10) and stay clickable. Shared primitive — applies to every such cell (e.g. the queues Name cell). --- apps/webapp/app/components/primitives/Table.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 761239e8f1..07f9dc0e06 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -386,19 +386,27 @@ export const TableCell = forwardRef( // With leading/trailing content, the link is content-sized and the adornments sit beside // it (still inside the td) so interactive triggers never nest inside the . leadingContent || trailingContent ? ( -
- {leadingContent} + // Stretched link: the covers the whole cell via an inset ::before overlay, so the + // entire cell is clickable — not just the text. The interactive adornments (tooltip + // icons, badge buttons) sit above the overlay (relative z-10) and stay clickable, and + // never nest inside the . +
+ {leadingContent ? ( + {leadingContent} + ) : null} {children} - {trailingContent} + {trailingContent ? ( + {trailingContent} + ) : null}
) : ( Date: Thu, 23 Jul 2026 10:45:21 +0100 Subject: [PATCH 19/55] fix(webapp): use the custom Queues icon in the queues-table Name cell (TRI-12068) Swap the generic RectangleStackIcon for the QueuesIcon (the same icon as the Queues side-menu item) on custom queues in the Name cell. --- .../route.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 12b1226522..0c6a93ac5e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -11,6 +11,7 @@ import { Form, useNavigation, type MetaFunction } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { QueuesIcon } from "~/assets/icons/QueuesIcon"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon"; @@ -783,7 +784,7 @@ function QueuesWithMetricsView() { )} /> ) : ( - Date: Thu, 23 Jul 2026 10:50:32 +0100 Subject: [PATCH 20/55] fix(webapp): remove queues table column sorting (TRI-12068) Remove client-side column sorting from the queues table (to be reimplemented server-side separately, per TRI-12322). Drops the useTableSort wiring, the sort-column defs, the header sort props, and the now-dead queueLimitedByLabel helper; the sort arrow icons no longer render. --- .../route.tsx | 65 ++----------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 0c6a93ac5e..6c0a3c3b45 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -43,7 +43,6 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { useTableSort, type SortColumn } from "~/components/primitives/useTableSort"; import { InfoIconTooltip, SimpleTooltip, @@ -434,43 +433,6 @@ function QueuesWithMetricsView() { // Client-side, header-click sorting over the current page's rows. Server pagination and the // default busiest order are unchanged; clearing a sort returns to that server order. const queueRows = queues ?? []; - type QueueRow = (typeof queueRows)[number]; - const sortColumns = useMemo[]>( - () => [ - { key: "name", type: "alpha", value: (q) => q.name }, - { key: "queued", type: "number", value: (q) => q.queued }, - { key: "running", type: "number", value: (q) => q.running }, - { - key: "limit", - type: "number", - value: (q) => q.concurrencyLimit ?? environment.concurrencyLimit, - }, - { key: "limitedBy", type: "alpha", value: (q) => queueLimitedByLabel(q) }, - { - key: "health", - type: "alpha", - value: (q) => - queueHealthLabel({ - paused: q.paused, - running: q.running, - queued: q.queued, - limit: q.concurrencyLimit ?? environment.concurrencyLimit, - }), - }, - { - key: "delayP95", - type: "number", - value: (q) => metricsByQueue[queueMetricsKey(q)]?.p95WaitMs ?? null, - }, - { - key: "backlog", - type: "number", - value: (q) => metricsByQueue[queueMetricsKey(q)]?.peakQueued ?? null, - }, - ], - [environment.concurrencyLimit, metricsByQueue] - ); - const { sortedRows: sortedQueues, getSortProps } = useTableSort(queueRows, sortColumns); return ( @@ -694,19 +656,18 @@ function QueuesWithMetricsView() { - Name - + Name + Queued - + Running - + Limit

@@ -725,19 +686,17 @@ function QueuesWithMetricsView() { > Limited by - + Health Delay p95 Runs waiting over the selected window. where throttled. @@ -752,8 +711,8 @@ function QueuesWithMetricsView() { - {sortedQueues.length > 0 ? ( - sortedQueues.map((queue) => { + {queueRows.length > 0 ? ( + queueRows.map((queue) => { const limit = queue.concurrencyLimit ?? environment.concurrencyLimit; const isAtConcurrencyLimit = queue.running >= limit; const isAtQueueLimit = @@ -1602,16 +1561,6 @@ function QueueHealthBadge(health: QueueHealth) { ); } -// The label rendered in the "Limited by" cell, also used to sort that column. -function queueLimitedByLabel(queue: { - concurrency?: { overriddenAt?: Date | string | null } | null; - concurrencyLimit?: number | null; -}): "Override" | "User" | "Environment" { - if (queue.concurrency?.overriddenAt) return "Override"; - if (queue.concurrencyLimit) return "User"; - return "Environment"; -} - // The `queue_metrics`-prefixed key a queue is stored under (task queues are prefixed `task/`). function queueMetricsKey(queue: { type: string; name: string }): string { return `${queue.type === "task" ? "task/" : ""}${queue.name}`; From 6410a1f2927f998cc7e164da06c95fb575a6b345 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 10:53:47 +0100 Subject: [PATCH 21/55] =?UTF-8?q?fix(webapp):=20rework=20queue=20filter=20?= =?UTF-8?q?bars=20=E2=80=94=20Period=20label,=20date-filter=20placement,?= =?UTF-8?q?=20per-page=20padding=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show the date filter as "Period: {value}" (bright label, dimmed value, matching the Runs page) on both queue pages; move it to the right of the list filter bar (left of Pause); and set per-page filter-bar padding (list px-2, detail pl-1.5 pr-2) via a new optional className on MetricsLayout.Filters. --- .../app/components/layout/MetricsLayout.tsx | 16 ++++++++++++++-- .../route.tsx | 8 +++----- .../route.tsx | 4 +--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 85da807ebd..4e1ac30144 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -275,9 +275,21 @@ function MetricsLayoutRoot({ * standard page insets. Compose left/right clusters as child divs — `justify-between` spreads them * (a single child sits at the start). */ -function MetricsLayoutFilters({ children }: { children: ReactNode }) { +function MetricsLayoutFilters({ + children, + className, +}: { + children: ReactNode; + /** Override the baked horizontal padding (the two queue pages want slightly different insets). */ + className?: string; +}) { return ( -

+
{children}
); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 6c0a3c3b45..8837583648 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -453,19 +453,17 @@ function QueuesWithMetricsView() { {/* Filters — pinned bar directly under the NavBar. Left cluster = search + period; right cluster = pagination. */} {success ? ( - +
+
+
-
-
{environment.runsEnabled && env.pauseSource !== ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT ? ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 38710edd21..dfecedb1a2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -244,7 +244,7 @@ export default function Page() { {/* Filters — search (concurrency keys) + time filter in one left cluster, above everything, like the Queues list. The time filter scopes the tab charts; search filters the keys table. The bar is pinned by the layout while the page scrolls. */} - +
Date: Thu, 23 Jul 2026 10:58:55 +0100 Subject: [PATCH 22/55] fix(webapp): brighten the chart hover cursor to match the synced line (TRI-12068) The hovered chart's cursor was a dim rgba(255,255,255,0.1); point it at SYNC_LINE_COLOR so it matches the dashed synced indicator drawn on the other charts. Bar charts' fill cursor is brightened to match. Global to line + bar charts. --- apps/webapp/app/components/primitives/charts/ChartBar.tsx | 2 +- apps/webapp/app/components/primitives/charts/ChartLine.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartBar.tsx b/apps/webapp/app/components/primitives/charts/ChartBar.tsx index 73ffb35c58..8f18ff7e13 100644 --- a/apps/webapp/app/components/primitives/charts/ChartBar.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartBar.tsx @@ -260,7 +260,7 @@ export function ChartBarRenderer({ {/* When legend is shown below the chart, render tooltip with cursor only (no content popup). Otherwise render the full tooltip with zoom instructions. */} diff --git a/apps/webapp/app/components/primitives/charts/ChartLine.tsx b/apps/webapp/app/components/primitives/charts/ChartLine.tsx index 17fceee27c..570bf4fedc 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLine.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLine.tsx @@ -488,7 +488,7 @@ export function ChartLineRenderer({ {/* When legend is shown below, render tooltip with cursor only (no content popup) */} {/* When legend is shown below, render tooltip with cursor only (no content popup) */} Date: Thu, 23 Jul 2026 10:58:56 +0100 Subject: [PATCH 23/55] fix(webapp): top-align the chart card header so Maximize sits top-right (TRI-12068) Chart card header used items-center, so with a two-line title the Maximize button sat too low. Switch to items-start. --- apps/webapp/app/components/primitives/charts/Card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/charts/Card.tsx b/apps/webapp/app/components/primitives/charts/Card.tsx index f4fb13b51b..d197b109b0 100644 --- a/apps/webapp/app/components/primitives/charts/Card.tsx +++ b/apps/webapp/app/components/primitives/charts/Card.tsx @@ -19,7 +19,7 @@ const CardHeader = ({ children, draggable }: { children: ReactNode; draggable?: return ( From 79bb580ab36ceac8de32abb6ed4606a077e6d43e Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 10:58:58 +0100 Subject: [PATCH 24/55] fix(webapp): make the Throttled detail chart span full width (TRI-12068) The 5th Overview chart (Throttled) sat alone at 50% width; span it across both columns (sm:col-span-2) with a wider aspect so its height matches the others. --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index dfecedb1a2..d1d958003d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -429,7 +429,7 @@ function OverviewCharts({ Date: Thu, 23 Jul 2026 11:09:04 +0100 Subject: [PATCH 25/55] fix(webapp): pause-button styling + override-modal tweaks (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pause buttons (queue + environment): orange border matching the icon, brighter on hover; ellipsis in the label; the queue button reads "Pause this queue…" with the queue name moved into the tooltip (items 11/12/18). Override modal: Cancel is now secondary, description numbers use tabular-nums, the absolute-mode copy is clearer and spans two lines to avoid layout shift, and the Number/Percent toggle height now matches the number field via a new SegmentedControl className (item 13). --- .../primitives/SegmentedControl.tsx | 6 ++++- .../app/components/queues/QueueControls.tsx | 24 +++++++++++++------ .../route.tsx | 9 +++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/primitives/SegmentedControl.tsx b/apps/webapp/app/components/primitives/SegmentedControl.tsx index 2f749215c2..de9a84b5c0 100644 --- a/apps/webapp/app/components/primitives/SegmentedControl.tsx +++ b/apps/webapp/app/components/primitives/SegmentedControl.tsx @@ -76,6 +76,8 @@ type SegmentedControlProps = { variant?: VariantType; fullWidth?: boolean; onChange?: (value: string) => void; + /** Override the control's outer styling (e.g. a height that matches an adjacent input). */ + className?: string; }; export default function SegmentedControl({ @@ -86,6 +88,7 @@ export default function SegmentedControl({ variant = "secondary/medium", fullWidth, onChange, + className, }: SegmentedControlProps) { const variantStyle = variants[variant]; const _isPrimary = variant.startsWith("primary"); @@ -95,7 +98,8 @@ export default function SegmentedControl({ className={cn( "flex rounded text-text-bright", variantStyle.base, - fullWidth ? "w-full" : "w-fit" + fullWidth ? "w-full" : "w-fit", + className )} > @@ -66,11 +66,20 @@ export function QueuePauseResumeButton({ fullWidth={fullWidth} textAlignLeft={fullWidth} aria-label={label} + className={ + withQueueName + ? queue.paused + ? "border-success/60 hover:border-success" + : "border-warning/60 hover:border-warning" + : undefined + } > {iconOnly ? undefined : withQueueName - ? `${queue.paused ? "Resume" : "Pause"} ${queue.name} queue` + ? queue.paused + ? "Resume this queue…" + : "Pause this queue…" : queue.paused ? "Resume" : "Pause"} @@ -314,6 +323,7 @@ export function QueueOverrideConcurrencyButton({
{mode === "percent" ? ( - + {materializedFromPercent !== null ? `${percentNumber}% = ${materializedFromPercent} concurrent ${ materializedFromPercent === 1 ? "run" : "runs" @@ -330,10 +340,10 @@ export function QueueOverrideConcurrencyButton({ : "Enter a percentage between 1 and 100."} ) : ( - + {limitOverCap ? `Can't exceed the environment limit of ${environmentConcurrencyLimit}.` - : `Up to the environment limit of ${environmentConcurrencyLimit}.`} + : `The most concurrent runs this queue can use at once. It can't exceed the environment limit of ${environmentConcurrencyLimit}.`} )} @@ -371,7 +381,7 @@ export function QueueOverrideConcurrencyButton({ )} - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 8837583648..4f6e608420 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1038,6 +1038,11 @@ function EnvironmentPauseResumeButton({ variant="secondary/small" LeadingIcon={env.paused ? PlayIcon : PauseIcon} leadingIconClassName={env.paused ? "text-success" : "text-warning"} + className={ + env.paused + ? "border-success/60 hover:border-success" + : "border-warning/60 hover:border-warning" + } aria-label={ env.paused ? `Resume processing runs in ${environmentFullTitle(env)}` @@ -1045,8 +1050,8 @@ function EnvironmentPauseResumeButton({ } > {env.paused - ? `Resume ${environmentFullTitle(env)} environment` - : `Pause ${environmentFullTitle(env)} environment`} + ? `Resume ${environmentFullTitle(env)} environment…` + : `Pause ${environmentFullTitle(env)} environment…`}
From 4d04c733f0e057fef52704f1ef86079847956237 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 11:12:25 +0100 Subject: [PATCH 26/55] fix(webapp): bottom-align the queue-detail tabs to the filter bar (TRI-12068) The Overview/Concurrency-keys tabs were vertically centered in the filter bar, leaving their underline ~10px above the bar's bottom border. Bottom-align them (self-end) so the tab underline seats on the filter bar's bottom edge. --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index d1d958003d..9f54d61bae 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -245,7 +245,7 @@ export default function Page() { everything, like the Queues list. The time filter scopes the tab charts; search filters the keys table. The bar is pinned by the layout while the page scrolls. */} -
+
Date: Thu, 23 Jul 2026 11:14:27 +0100 Subject: [PATCH 27/55] fix(webapp): tabular-nums on the remaining queue-page numbers (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of numeric displays on the queue pages: BigNumber values, table cells, chart axes, and chart peak readouts already used tabular-nums. Add it to the two that were missing — the BigNumber stat-tile suffix (e.g. "peak 2.7K", "23% of the environment limit") and the ConcurrencyBlock "NN% of limit" caption. --- apps/webapp/app/components/metrics/BigNumber.tsx | 4 ++-- .../route.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/metrics/BigNumber.tsx b/apps/webapp/app/components/metrics/BigNumber.tsx index 4a41656f90..b792c63519 100644 --- a/apps/webapp/app/components/metrics/BigNumber.tsx +++ b/apps/webapp/app/components/metrics/BigNumber.tsx @@ -56,7 +56,7 @@ export function BigNumber({ ) : formattedValue !== undefined ? (
{formattedValue} - {suffix &&
{suffix}
} + {suffix &&
{suffix}
}
) : v !== undefined ? (
@@ -70,7 +70,7 @@ export function BigNumber({ ) : ( formatNumber(v) )} - {suffix &&
{suffix}
} + {suffix &&
{suffix}
}
) : ( "–" diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 9f54d61bae..6cf590561f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -1073,7 +1073,9 @@ function ConcurrencyBlock({ / {limit !== null ? limit.toLocaleString() : "∞"} {limit !== null && limit > 0 && ( - + {/* Separator so the limit and the percentage don't read as one number (e.g. "/ 25" + "44%" mashing into "2544%"). */} · From 49d7107ffb9ac351d8e94f77d60eecc0c6656ea0 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 11:17:53 +0100 Subject: [PATCH 28/55] fix(webapp): plain-language queue tooltip copy (/bro pass) (TRI-12068) Rewrite the queue-page tooltip and description copy to be simpler and jargon-free: chart descriptions, table-header tooltips, the Override and Pause button tooltips, and the detail chart info tooltips. Replaces phrasing like "p95 wait from eligible to dequeued" with "how long runs wait before they start", drops em dashes, etc. --- .../app/components/queues/QueueControls.tsx | 8 +++--- .../route.tsx | 27 ++++++++++--------- .../route.tsx | 10 +++---- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index cff43c2d48..95fa065a83 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -48,8 +48,8 @@ export function QueuePauseResumeButton({ const [isOpen, setIsOpen] = useState(false); const label = queue.paused - ? `Resume the "${queue.name}" queue so its runs can be dequeued again` - : `Pause the "${queue.name}" queue — its runs stay queued until you resume it`; + ? `Resume the "${queue.name}" queue so its runs can start again` + : `Pause the "${queue.name}" queue. Its runs wait until you resume it`; const trigger = showTooltip ? (
@@ -248,8 +248,8 @@ export function QueueOverrideConcurrencyButton({
- Set a custom concurrency limit for this queue, overriding the environment default — as - an absolute number or a percentage of the environment limit. + Give this queue its own concurrency limit instead of the environment default. Set it as + a number or a percentage of the environment limit. diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 4f6e608420..c0c451d189 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -669,15 +669,16 @@ function QueuesWithMetricsView() { tooltip={

- Environment: the environment's - limit of {environment.concurrencyLimit}. + Environment: uses the + environment limit of {environment.concurrencyLimit}.

- User: a limit set in your code. + User: a limit you set in your + code.

- Override: set manually from the - dashboard or API. + Override: a limit you set here or + via the API.

} @@ -689,7 +690,7 @@ function QueuesWithMetricsView() { Delay p95 @@ -697,7 +698,8 @@ function QueuesWithMetricsView() { alignment="right" tooltip={ <> - Runs waiting over the selected window. where throttled. + How many runs were waiting, over the selected time. marks + where the queue was throttled. } > @@ -1244,8 +1246,8 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ label: "Env saturation", description: ( <> - Running as a share of the environment limit. Turns over 100% (burst - headroom). + How much of the environment's concurrency these queues are using. Turns{" "} + above 100%, when they're into burst capacity. ), color: "var(--color-queues)", @@ -1272,7 +1274,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ { id: "backlog", label: "Backlog", - description: "Runs waiting across these queues over time.", + description: "How many runs are waiting across these queues, over time.", color: "var(--color-queues)", query: `SELECT timeBucket() AS t,\n queue,\n max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t, queue\nORDER BY t`, derive: (rows) => { @@ -1289,7 +1291,8 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ label: "Scheduling delay p95", description: ( <> - p95 wait from eligible to dequeued. Turns over 1 min. + How long runs wait before they start (95% start faster than this). Turns{" "} + above 1 minute. ), totalTooltip: "The worst p95 in the selected window.", @@ -1317,7 +1320,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ { id: "throttled", label: "Throttled", - description: "Times dequeuing was blocked by a limit.", + description: "How often runs were held back by a limit.", totalTooltip: "The share of the selected window with at least one blocked dequeue.", color: "var(--color-queues)", legend: [{ color: "var(--color-warning)", label: "Throttled" }], diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 6cf590561f..349b186236 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -364,7 +364,7 @@ function OverviewCharts({
Date: Thu, 23 Jul 2026 14:23:49 +0100 Subject: [PATCH 29/55] =?UTF-8?q?fix(webapp):=20pause=20buttons=20?= =?UTF-8?q?=E2=80=94=20orange=20label=20text=20+=20accurate=20tooltip=20co?= =?UTF-8?q?py=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pause buttons: colour the label text to match the icon (orange when pausing, green when resuming). Rework the tooltips off the modal's accurate wording — "Pauses all runs from being dequeued in the {name} queue/environment. Any executing runs will continue to run." (items 2, 16). --- apps/webapp/app/components/queues/QueueControls.tsx | 8 ++++---- .../route.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 95fa065a83..ae92d57a13 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -48,8 +48,8 @@ export function QueuePauseResumeButton({ const [isOpen, setIsOpen] = useState(false); const label = queue.paused - ? `Resume the "${queue.name}" queue so its runs can start again` - : `Pause the "${queue.name}" queue. Its runs wait until you resume it`; + ? `Resumes the "${queue.name}" queue so its runs can be dequeued again.` + : `Pauses all runs from being dequeued in the "${queue.name}" queue. Any executing runs will continue to run.`; const trigger = showTooltip ? (
@@ -69,8 +69,8 @@ export function QueuePauseResumeButton({ className={ withQueueName ? queue.paused - ? "border-success/60 hover:border-success" - : "border-warning/60 hover:border-warning" + ? "border-success/60 text-success hover:border-success" + : "border-warning/60 text-warning hover:border-warning" : undefined } > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index c0c451d189..b4225609e2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1042,13 +1042,13 @@ function EnvironmentPauseResumeButton({ leadingIconClassName={env.paused ? "text-success" : "text-warning"} className={ env.paused - ? "border-success/60 hover:border-success" - : "border-warning/60 hover:border-warning" + ? "border-success/60 text-success hover:border-success" + : "border-warning/60 text-warning hover:border-warning" } aria-label={ env.paused - ? `Resume processing runs in ${environmentFullTitle(env)}` - : `Pause processing runs in ${environmentFullTitle(env)}` + ? `Resumes ${environmentFullTitle(env)} so its runs can be dequeued again.` + : `Pauses all runs from being dequeued in ${environmentFullTitle(env)}. Any executing runs will continue to run.` } > {env.paused @@ -1060,8 +1060,8 @@ function EnvironmentPauseResumeButton({ {env.paused - ? `Resume processing runs in ${environmentFullTitle(env)}` - : `Pause processing runs in ${environmentFullTitle(env)}`} + ? `Resumes ${environmentFullTitle(env)} so its runs can be dequeued again.` + : `Pauses all runs from being dequeued in ${environmentFullTitle(env)}. Any executing runs will continue to run.`} From e473d7abf83ce3791f016860214fdddf7e4daaf8 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:24:50 +0100 Subject: [PATCH 30/55] =?UTF-8?q?fix(webapp):=20tooltip=20legend=20swatch?= =?UTF-8?q?=20=E2=80=94=20match=20legend=20corners=20+=20nudge=20up=201px?= =?UTF-8?q?=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline colour swatch used in chart tooltips now uses rounded-[2px] to match the chart legend swatches, and is bumped up 1px (-translate-y-px) so it sits on the text baseline better (item 3). --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index b4225609e2..dd4f10dac7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1168,7 +1168,7 @@ type TilePoint = { bucket: number; value: number }; function WarningSwatch() { return ( From 5c83dd128f57e8fe92ef420d24b02966dadef3a0 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:30:55 +0100 Subject: [PATCH 31/55] =?UTF-8?q?fix(webapp):=20big-number=20panels=20?= =?UTF-8?q?=E2=80=94=20tighter=20top/right=20padding=20+=20hover-reveal=20?= =?UTF-8?q?view-runs=20button=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the big-number container padding to pt-3 pr-3 (keeping left/bottom). On the Queued and Running panels the 'view runs' button now only appears on panel hover, matching the chart Maximize button's reveal + dimmed→bright icon colour (items 4, 5). --- .../app/components/metrics/BigNumber.tsx | 2 +- .../route.tsx | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/webapp/app/components/metrics/BigNumber.tsx b/apps/webapp/app/components/metrics/BigNumber.tsx index b792c63519..5843e6229d 100644 --- a/apps/webapp/app/components/metrics/BigNumber.tsx +++ b/apps/webapp/app/components/metrics/BigNumber.tsx @@ -40,7 +40,7 @@ export function BigNumber({ typeof compactThreshold === "number" && v !== undefined && v >= compactThreshold; return ( -
+
{title} {accessory &&
{accessory}
} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index dd4f10dac7..b2be8ecf3b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -491,17 +491,19 @@ function QueuesWithMetricsView() { suffix={env.paused ? paused : undefined} animate accessory={ - + + + } valueClassName={env.paused ? "text-warning tabular-nums" : "tabular-nums"} compactThreshold={1000000} @@ -522,17 +524,19 @@ function QueuesWithMetricsView() { ) : undefined } accessory={ - + + + } compactThreshold={1000000} /> From 5439218552de606cd965e19c4fa21c4527ae4363 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:38:11 +0100 Subject: [PATCH 32/55] fix(webapp): tighten chart title to info-icon gap to gap-1 (TRI-12068) Chart card titles (and the Allocated tile title) now sit gap-1 from their info icon instead of gap-1.5 (item 7). --- apps/webapp/app/components/queues/QueueMetricCards.tsx | 4 ++-- .../route.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 7fef23624e..a536d3adfb 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -204,7 +204,7 @@ export function QueueMetricChartCard({ + {title} {info ? : null} {titleAccessory} @@ -322,7 +322,7 @@ export function QueueSparklineStat({ return (
-
+
{title} {info || (data.length > 0 && peak > 0) ? ( + Allocated {allocation && overAllocated ? ( - + {tile.label} From c42d98e3b32b5c7e977e1d0e38df211a41cb9239 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:39:01 +0100 Subject: [PATCH 33/55] fix(webapp): nudge queue-detail tabs down 1px onto the filter-bar border (TRI-12068) translate-y-px on the self-end tab cluster so the tab underline sits flush on the filter bar's bottom border (item 11). --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 349b186236..4c73959b2b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -245,7 +245,7 @@ export default function Page() { everything, like the Queues list. The time filter scopes the tab charts; search filters the keys table. The bar is pinned by the layout while the page scrolls. */} -
+
Date: Thu, 23 Jul 2026 14:41:55 +0100 Subject: [PATCH 34/55] fix(webapp): brighten the Override-limit button icon (TRI-12068) The AdjustmentsHorizontal icon on the 'Override limit' button is now text-text-bright instead of dimmed (item 14). --- apps/webapp/app/components/queues/QueueControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index ae92d57a13..15b9fba73b 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -237,7 +237,7 @@ export function QueueOverrideConcurrencyButton({ type="button" variant="secondary/small" LeadingIcon={AdjustmentsHorizontalIcon} - leadingIconClassName="text-text-dimmed" + leadingIconClassName="text-text-bright" aria-label={ isOverridden ? "Edit concurrency override" : "Override concurrency limit" } From 5c594693a1698e529b9cddc04927d6bb301fbd21 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:41:56 +0100 Subject: [PATCH 35/55] fix(webapp): let the Limited-by header tooltip grow to max-content (TRI-12068) Add an optional tooltipContentClassName to TableHeaderCell and use max-w-max on the 'Limited by' tooltip so its three lines each sit on one line instead of wrapping at the default 230px (item 17). --- apps/webapp/app/components/primitives/Table.tsx | 5 ++++- .../route.tsx | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 07f9dc0e06..dccb26df45 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -181,6 +181,8 @@ type TableCellBasicProps = { type TableHeaderCellProps = TableCellBasicProps & { hiddenLabel?: boolean; tooltip?: ReactNode; + /** Extra class merged onto the tooltip content — e.g. widen it past the default max-width. */ + tooltipContentClassName?: string; disableTooltipHoverableContent?: boolean; /** * When set (together with `onSort`), the header renders a sort indicator and becomes clickable. @@ -202,6 +204,7 @@ export const TableHeaderCell = forwardRef ) : null; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index b6fe4afe82..75daac93d3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -670,8 +670,9 @@ function QueuesWithMetricsView() { +

Environment: uses the environment limit of {environment.concurrencyLimit}. From 4e77f0e02916d83368f8d97eccb0db702d4e9123 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:42:41 +0100 Subject: [PATCH 36/55] =?UTF-8?q?fix(webapp):=20pause=20modals=20=E2=80=94?= =?UTF-8?q?=20Cancel=20button=20to=20secondary=20variant=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cancel button in the Pause queue? and Pause environment? modals is now secondary/medium (was tertiary), matching the override modal (item 12). --- apps/webapp/app/components/queues/QueueControls.tsx | 2 +- .../route.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 15b9fba73b..8f781dc555 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -134,7 +134,7 @@ export function QueuePauseResumeButton({ } cancelButton={ - diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 75daac93d3..137f0a3dc5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1103,7 +1103,7 @@ function EnvironmentPauseResumeButton({ } cancelButton={ - From e0b57444a079789775a71a757b89ef9cfabae713 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 14:44:47 +0100 Subject: [PATCH 37/55] fix(webapp): use the Queues icon for the row menu's View-queued-runs item (TRI-12068) The queue row triple-dot menu's 'View queued runs' entry now uses the custom Queues icon instead of RectangleStackIcon (item 13). --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 137f0a3dc5..ef95bf9478 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -951,7 +951,7 @@ function QueuesWithMetricsView() { })} /> Date: Thu, 23 Jul 2026 14:44:48 +0100 Subject: [PATCH 38/55] fix(webapp): remove sorting from the Concurrency keys table (TRI-12068) Drop the sort affordance + useTableSort from the Concurrency keys table; rows render in their natural (filtered) order (item 15). --- .../route.tsx | 45 ++++--------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 4c73959b2b..9be77036b3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -48,7 +48,6 @@ import { SearchInput } from "~/components/primitives/SearchInput"; import { engine } from "~/v3/runEngine.server"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { useSearchParams } from "~/hooks/useSearchParam"; -import { useTableSort, type SortColumn } from "~/components/primitives/useTableSort"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server"; import { requireUserId } from "~/services/session.server"; @@ -776,20 +775,6 @@ function KeyStatsTable({ [merged, query] ); - const sortColumns = useMemo[]>( - () => [ - { key: "key", type: "alpha", value: (r) => r.key }, - { key: "queued", type: "number", value: (r) => r.queued }, - { key: "running", type: "number", value: (r) => r.running }, - { key: "oldestWait", type: "number", value: (r) => r.oldestWaitMs }, - { key: "started", type: "number", value: (r) => r.range?.started }, - { key: "peakBacklog", type: "number", value: (r) => r.range?.peakBacklog }, - { key: "meanWait", type: "number", value: (r) => r.range?.meanWaitMs }, - ], - [] - ); - const { sortedRows, getSortProps } = useTableSort(filtered, sortColumns); - if (merged.length === 0) return null; return ( @@ -797,34 +782,22 @@ function KeyStatsTable({

- Key - - Queued now - - - Running now - - - Oldest wait - - - Started - - - Peak backlog - - - Mean delay - + Key + Queued now + Running now + Oldest wait + Started + Peak backlog + Mean delay - {sortedRows.length === 0 ? ( + {filtered.length === 0 ? ( No keys match “{query}” ) : null} - {sortedRows.map((row) => ( + {filtered.map((row) => ( Date: Thu, 23 Jul 2026 14:46:09 +0100 Subject: [PATCH 39/55] fix(webapp): disableHoverableContent on chart info-icon tooltips (TRI-12068) The info tooltips on the chart card titles (detail charts, mini activity charts, and the list saturation tiles) now close when the pointer leaves the icon instead of staying open over the chart (item 9). --- apps/webapp/app/components/queues/QueueMetricCards.tsx | 9 ++++++++- .../route.tsx | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index a536d3adfb..98525d23e3 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -206,7 +206,13 @@ export function QueueMetricChartCard({ info || titleAccessory ? ( {title} - {info ? : null} + {info ? ( + + ) : null} {titleAccessory} ) : ( @@ -337,6 +343,7 @@ export function QueueSparklineStat({ } contentClassName="max-w-[230px]" + disableHoverableContent /> ) : null} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index ef95bf9478..352a937367 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1444,7 +1444,11 @@ function QueueEnvMetricChart({ {tile.label} - + {peak != null ? ( tile.totalTooltip && !showLoading ? ( From 2fa69106ba000e8f5c03412fd89646a67678f07f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 15:18:42 +0100 Subject: [PATCH 40/55] fix(webapp): threshold-stroke gradient split from the line's own range; saturation orange only over 100% (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The threshold gradient offset is now derived from the plotted line's value range (objectBoundingBox maps to the path bbox, not the axis), so the colour change lands exactly at the threshold and the domain no longer needs pinning. thresholdStroke can target a single series. The Env saturation line now uses it — orange only above 100%, not across the whole spike (item 6). --- .../primitives/charts/ChartLine.tsx | 115 +++++++++++------- .../route.tsx | 32 ++--- 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartLine.tsx b/apps/webapp/app/components/primitives/charts/ChartLine.tsx index 570bf4fedc..593f5b706d 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLine.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLine.tsx @@ -118,11 +118,12 @@ export type ChartLineRendererProps = { * pinned so the gradient split lines up exactly with the plotted values and reference lines. * Single-series (non-stacked) line charts only. * - * Prefer {@link warningOverlay} when the threshold sits far below the data maximum: a gradient - * split whose boundary collapses onto the baseline paints zero/near-zero buckets the warning - * colour. The overlay retraces per-bucket instead, so under-threshold buckets always stay blue. + * The gradient offset is derived from the plotted line's own value range (objectBoundingBox maps + * 0..1 to the path's bounding box, not the y-axis), so the colour change lands exactly at the + * threshold value however the domain is padded for reference lines. `series` targets which line + * the gradient applies to (others keep their own colour); defaults to the first series. */ - thresholdStroke?: { value: number; aboveColor: string }; + thresholdStroke?: { value: number; aboveColor: string; series?: string }; /** * Per-bucket warning recolour: a series is retraced in the warning colour only across buckets * where it crosses a limit — either strictly above a constant `threshold` (single-series case, @@ -273,24 +274,36 @@ export function ChartLineRenderer({ // A threshold stroke needs an exact, fixed y-domain so the gradient split aligns with the // plotted values and the reference lines. Compute it from the data + reference/threshold ys. - let thresholdDomain: [number, number] | undefined; + let thresholdActive = false; let thresholdOffset = 0; if (thresholdStroke && !stacked) { - let dataMax = thresholdStroke.value; + thresholdActive = true; + // The gradient is objectBoundingBox — its 0..1 maps to the plotted line's own bounding box + // (lineMax at the top, lineMin at the bottom), NOT the y-axis. So derive the split from the + // target line's value range: offset = (lineMax - threshold) / (lineMax - lineMin) lands the + // colour change exactly at the threshold value's pixel, whatever the axis domain is. That means + // we don't pin the domain (which coarsened the ticks) — it auto-scales as usual. + const gradientKey = thresholdStroke.series ?? visibleSeries[0]; + let lineMin = Infinity; + let lineMax = -Infinity; for (const row of data) { - for (const key of visibleSeries) { - const v = Number(row[key]); - if (Number.isFinite(v) && v > dataMax) dataMax = v; + const v = Number(row[gradientKey]); + if (Number.isFinite(v)) { + if (v < lineMin) lineMin = v; + if (v > lineMax) lineMax = v; } } - for (const line of referenceLines ?? []) { - if (Number.isFinite(line.y) && line.y > dataMax) dataMax = line.y; + if (!Number.isFinite(lineMin)) { + lineMin = 0; + lineMax = thresholdStroke.value; } - const domainMax = dataMax > 0 ? dataMax * 1.1 : 1; - thresholdDomain = [0, domainMax]; - // Gradient runs top (offset 0 = domainMax) to bottom (offset 1 = 0); the split sits where - // the threshold value falls within the domain. - thresholdOffset = Math.min(1, Math.max(0, (domainMax - thresholdStroke.value) / domainMax)); + const range = lineMax - lineMin; + thresholdOffset = + range > 0 + ? Math.min(1, Math.max(0, (lineMax - thresholdStroke.value) / range)) + : lineMax >= thresholdStroke.value + ? 0 + : 1; } // Per-bucket warning overlay: single-series line charts only. Retrace the primary series in the @@ -339,7 +352,6 @@ export function ChartLineRenderer({ style: { fontVariantNumeric: "tabular-nums" }, }, tickFormatter: yAxisTickFormatter, - ...(thresholdDomain ? { domain: thresholdDomain } : {}), ...yAxisPropsProp, }; @@ -523,11 +535,14 @@ export function ChartLineRenderer({ margin={chartMargin} {...sharedMouseHandlers} > - {thresholdStroke && thresholdDomain ? ( + {thresholdActive ? ( - - + + ) : null} @@ -545,33 +560,39 @@ export function ChartLineRenderer({ /> {/* Note: Legend is now rendered by ChartRoot outside the chart container */} {referenceOverlays} - {visibleSeries.map((key) => ( - ( - - ) - : { r: 4, fill: config[key]?.color, strokeWidth: 0 } - } - isAnimationActive={false} - /> - ))} + {visibleSeries.map((key) => { + // The gradient stroke only applies to the threshold's target series (default: the first); + // other series (e.g. the grey limit line) keep their own colour. + const gradientLine = + thresholdActive && (thresholdStroke!.series == null || thresholdStroke!.series === key); + return ( + ( + + ) + : { r: 4, fill: config[key]?.color, strokeWidth: 0 } + } + isAnimationActive={false} + /> + ); + })} {overlayActive && ( // Drawn after the base line so the warning colour sits on top. connectNulls={false} keeps // the mask to over-threshold stretches; excluded from the legend and (above) the tooltip. diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 352a937367..f694b05cd2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -627,23 +627,23 @@ function QueuesWithMetricsView() { ] : undefined } - // Saturation and p95 "step over the line": a per-bucket overlay retraces only - // the over-threshold stretches in warning colour, so under-threshold values stay - // blue. (A gradient split can't do this reliably — an SVG objectBoundingBox - // gradient tracks the line's own bbox, not the y-axis, so a low/flat line reads - // as entirely warning-coloured.) - // All thresholded lines colour warning where they step over the threshold: the - // per-bucket overlay retraces only the over-threshold stretches, so the colour - // change tracks the axis crossing. - warningOverlay={ + // Saturation recolours the line above its 100% limit with a gradient split, so + // only the portion over the line is orange (the offset is derived from the line's + // own value range, so the split lands exactly at 100% regardless of domain + // padding). p95 and throttled use a per-bucket overlay: it retraces only the + // over-threshold stretches, so under-threshold buckets stay blue. + thresholdStroke={ tile.id === "saturation" - ? { threshold: 100 } - : tile.id === "p95" - ? { threshold: 60_000 } - : tile.id === "throttled" - ? // Integer counts: threshold 0 warns once a bucket has ≥1 throttle. - { threshold: 0 } - : undefined + ? { value: 100, aboveColor: "var(--color-warning)" } + : undefined + } + warningOverlay={ + tile.id === "p95" + ? { threshold: 60_000 } + : tile.id === "throttled" + ? // Integer counts: threshold 0 warns once a bucket has ≥1 throttle. + { threshold: 0 } + : undefined } /> ))} From 84f9f5e6eb76064e574ea9645bcb2fdb6581abfb Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 15:18:52 +0100 Subject: [PATCH 41/55] =?UTF-8?q?fix(webapp):=20queue-detail=20charts=20?= =?UTF-8?q?=E2=80=94=20legends,=20at-limit=20concurrency=20colour,=20toolt?= =?UTF-8?q?ip=20colour=20squares=20(TRI-12068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add legends to the Concurrency, Throughput and Scheduling-delay charts (item 8). Recolour the Concurrency running line above the limit with a gradient split so it's orange only at/over the limit, not on the way up (item 18). Rework the chart info tooltips to show inline colour swatches with the word "color" (item 10). --- .../components/queues/QueueMetricCards.tsx | 38 ++++++++++ .../route.tsx | 69 ++++++++++++++++--- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 98525d23e3..9f26894a0c 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -104,6 +104,20 @@ type QueueMetricChartProps = { * are config values that existed all along, so carry the first value backward instead. */ carryBackfill?: string[]; + /** Show the series legend below the chart (use for multi-series charts). */ + showLegend?: boolean; + /** + * Recolour a series' stroke above a threshold with a gradient split (colour only above the + * line). `value` sets a constant threshold; `valueFromSeries` reads a (roughly constant) + * threshold off another series — e.g. the concurrency limit. `series` targets which line is + * recoloured; the others keep their own colour. + */ + thresholdStroke?: { + aboveColor: string; + series?: string; + value?: number; + valueFromSeries?: string; + }; }; // Bare chart (no card chrome) so it can live inside a shared card, e.g. a tabbed panel. @@ -118,6 +132,8 @@ export function QueueMetricChart({ defaultPeriod, warningOverlay, carryBackfill, + showLegend, + thresholdStroke, }: QueueMetricChartProps) { const { rows, showLoading, failed } = useQueueMetric(query, { ids, @@ -163,6 +179,26 @@ export function QueueMetricChart({ [data] ); + // Resolve the threshold value: a constant, or the max of another series (e.g. the limit line, + // which is effectively constant). A gradient split then colours the target series only above it. + // `valueFromSeries` targets integer-count series (concurrency limit), so split half a unit below + // the limit — that way the line renders warning *at or above* the limit (saturated), matching + // "turns yellow at the limit", rather than only when it strictly exceeds it. + const resolvedThresholdStroke = useMemo(() => { + if (!thresholdStroke) return undefined; + let value = thresholdStroke.value; + if (value == null && thresholdStroke.valueFromSeries) { + let max = -Infinity; + for (const p of data) { + const v = Number(p[thresholdStroke.valueFromSeries]); + if (Number.isFinite(v) && v > max) max = v; + } + value = max > 0 ? max - 0.5 : undefined; + } + if (value == null || !Number.isFinite(value)) return undefined; + return { value, aboveColor: thresholdStroke.aboveColor, series: thresholdStroke.series }; + }, [thresholdStroke, data]); + const state: ChartState = showLoading ? "loading" : failed ? "invalid" : undefined; return ( @@ -173,6 +209,7 @@ export function QueueMetricChart({ series={series.map((s) => s.key)} state={state} fillContainer + showLegend={showLegend} > ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 9be77036b3..d390a47210 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -348,6 +348,17 @@ export default function Page() { ); } +// Inline colour swatch for tooltip copy — matches the chart legend swatch (rounded-[2px]) and is +// nudged up 1px so it sits on the text baseline. +function ColorSwatch({ color }: { color: string }) { + return ( + + ); +} + function OverviewCharts({ ids, timeRange, @@ -363,7 +374,14 @@ function OverviewCharts({
+ How many runs are going at once ( color) versus + the queue's limit ( color). Turns{" "} + color when it reaches the limit. + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_running) AS running, max(max_limit) AS limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -375,15 +393,26 @@ function OverviewCharts({ { key: "limit", label: "Limit", color: COLORS.limit }, { key: "running", label: "Running", color: COLORS.running }, ]} - // Recolour Running warning where it reaches the limit (saturated), matching the tooltip. - warningOverlay={{ series: "running", atOrAbove: "limit" }} + // Recolour Running above the limit line with a gradient split, so it's orange only where + // it's actually over the limit — not on the way up. The threshold reads off the (roughly + // constant) limit series. + thresholdStroke={{ + series: "running", + valueFromSeries: "limit", + aboveColor: "var(--color-warning)", + }} // The limit is a config value emitted only while the queue is active; back-fill its // leading zeros so the reference line doesn't start with a false 0→limit step. carryBackfill={["limit"]} /> + How many runs are waiting in this queue over time ( + color). + + } className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -394,7 +423,14 @@ function OverviewCharts({ /> + Runs arriving (Enqueued, color) versus starting + (Started, color). Turns{" "} + color when Started falls behind. + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n deltaSumTimestampMerge(enqueue_delta) AS enqueued,\n deltaSumTimestampMerge(started_delta) AS started\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -411,7 +447,14 @@ function OverviewCharts({ /> + How long runs wait before they start: p50 ( color), + p95 ( color), and p99 ( + color). + + } + showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[1]) AS p50,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS p95,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[4]) AS p99\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -427,7 +470,12 @@ function OverviewCharts({ /> + How often runs were held back by a limit ({" "} + color). + + } className="aspect-[2/1] sm:col-span-2 sm:aspect-[4/1]" query={`SELECT timeBucket() AS t, sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -844,7 +892,12 @@ function KeyDrilldown({
+ This key: waiting (Queued, color) vs running ( + color). + + } className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_queued) AS queued, max(max_running) AS running\nFROM queue_metrics_by_key\nWHERE ${pin}\nGROUP BY t\nORDER BY t`} fillGaps From 7a84e1c777ad953c9c95178f5512bdf18d1de376 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 15:24:09 +0100 Subject: [PATCH 42/55] fix(webapp): actually colour the pause button label text orange (TRI-12068) The earlier text-warning landed on the button content wrapper (colouring the icon) but the label span uses the variant's own text colour. Target it with [&_span]:text-warning/success so the label text matches the icon (follow-up to item 2). --- apps/webapp/app/components/queues/QueueControls.tsx | 4 ++-- .../route.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueControls.tsx b/apps/webapp/app/components/queues/QueueControls.tsx index 8f781dc555..d7c074221b 100644 --- a/apps/webapp/app/components/queues/QueueControls.tsx +++ b/apps/webapp/app/components/queues/QueueControls.tsx @@ -69,8 +69,8 @@ export function QueuePauseResumeButton({ className={ withQueueName ? queue.paused - ? "border-success/60 text-success hover:border-success" - : "border-warning/60 text-warning hover:border-warning" + ? "border-success/60 text-success [&_span]:text-success hover:border-success" + : "border-warning/60 text-warning [&_span]:text-warning hover:border-warning" : undefined } > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index f694b05cd2..9c0cbc93f5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -1047,8 +1047,8 @@ function EnvironmentPauseResumeButton({ leadingIconClassName={env.paused ? "text-success" : "text-warning"} className={ env.paused - ? "border-success/60 text-success hover:border-success" - : "border-warning/60 text-warning hover:border-warning" + ? "border-success/60 text-success [&_span]:text-success hover:border-success" + : "border-warning/60 text-warning [&_span]:text-warning hover:border-warning" } aria-label={ env.paused From aeaf6300ecfed417bf0ea2bb80426275d33d295c Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:16:15 +0100 Subject: [PATCH 43/55] fix(webapp): double the padding + gap around and between the charts (TRI-12068) Bump the MetricsLayout chart-area spacing from 1.5 to 3 (grid gap + gutter, the scroll column's vertical gap/padding, and the content separation), so the list charts match the detail charts' spacing and both pages breathe more (item 2). --- apps/webapp/app/components/layout/MetricsLayout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index 4e1ac30144..df8d725ecf 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -178,7 +178,7 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
@@ -318,7 +318,7 @@ function MetricsLayoutGrid({ return (
{children}
; + return
{children}
; } export const MetricsLayout = { From bb1de46781af7c2b90372707fe5d13ab9417b9b9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:16:16 +0100 Subject: [PATCH 44/55] fix(webapp): inline chart legends below the title instead of the totals legend (TRI-12068) Drop the Chart.Root legend (which showed summed per-series totals) and render a compact swatch+label legend below the chart title from the series config, matching the list-page charts (item 3). --- .../components/queues/QueueMetricCards.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 9f26894a0c..9d32cc55d6 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -132,7 +132,6 @@ export function QueueMetricChart({ defaultPeriod, warningOverlay, carryBackfill, - showLegend, thresholdStroke, }: QueueMetricChartProps) { const { rows, showLoading, failed } = useQueueMetric(query, { @@ -209,7 +208,6 @@ export function QueueMetricChart({ series={series.map((s) => s.key)} state={state} fillContainer - showLegend={showLegend} > {title} {info ? ( @@ -253,9 +251,25 @@ export function QueueMetricChartCard({ ) : null} {titleAccessory} - ) : ( - title - ) + {/* Inline legend below the title (swatch + label per series), matching the list-page + charts — instead of the Chart.Root legend with per-series totals. */} + {chart.showLegend && chart.series.length > 0 ? ( + + {chart.series.map((s) => ( + + + {s.label} + + ))} + + ) : null} + } > From 479a1e9df1429ecec413ce41cd38a54152cb0125 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:25:31 +0100 Subject: [PATCH 45/55] fix(webapp): queue-detail paused banner + minimal square view-runs button (TRI-12068) Show a warning banner at the top of the queue page when the queue is paused, mirroring the environment-paused banner (item 1). Make the Overview 'Queued' panel's view-runs button minimal + square + hover-reveal, matching the Maximize button (item 4). --- .../route.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index d390a47210..4024b44024 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -6,6 +6,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { MainCenteredContainer, PageContainer } from "~/components/layout/AppLayout"; import { MetricsLayout } from "~/components/layout/MetricsLayout"; +import { AnimatedOrgBannerBar } from "~/components/billing/AnimatedOrgBannerBar"; import { BigNumber } from "~/components/metrics/BigNumber"; import { Header3 } from "~/components/primitives/Headers"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; @@ -239,6 +240,11 @@ export default function Page() { + {/* Paused-queue banner — mirrors the environment-paused banner (OrgBanner) at the top of + the page when this individual queue is paused. */} + + {`"${queue.name}" queue paused. No new runs will be dequeued and executed.`} + {/* Filters — search (concurrency keys) + time filter in one left cluster, above everything, like the Queues list. The time filter scopes the tab charts; search filters @@ -1027,13 +1033,16 @@ function QueueStats({ suffix={peakQueued > 0 ? `peak ${formatNumberCompact(peakQueued)}` : undefined} suffixClassName="text-text-dimmed" accessory={ - + + + } /> Date: Thu, 23 Jul 2026 16:25:33 +0100 Subject: [PATCH 46/55] fix(webapp): list panels minimal square view-runs buttons; limit-cell spacing + hover (TRI-12068) Queued/Running panel view-runs buttons are now minimal + square + reveal on hover like Maximize (item 4). In the Limit cell, give the override percentage a clear ml-1 gap (the text space rendered too tight) and brighten it on row hover (item 6). --- .../route.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index 9c0cbc93f5..b85a20f019 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -493,7 +493,8 @@ function QueuesWithMetricsView() { accessory={ {queue.concurrencyLimitOverridePercent !== null ? ( <> - {limit}{" "} - + {limit} + ({formatOverridePercent(queue.concurrencyLimitOverridePercent)}%) From 83ef88f719f7a1230bc444dba941d38df05aa2c9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:44:29 +0100 Subject: [PATCH 47/55] fix(webapp): chart spacing to 2.5 + drop the content top margin (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set the MetricsLayout chart-area gap/gutter/vertical padding to 2.5 (from 3). Remove the Content top margin so the tile→content step is a single gap (was doubled: gap + mt), fixing the extra space between the Overview tiles and charts, and the table's top margin on the Queues page (item 1). --- apps/webapp/app/components/layout/MetricsLayout.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/components/layout/MetricsLayout.tsx b/apps/webapp/app/components/layout/MetricsLayout.tsx index df8d725ecf..12d0fcd743 100644 --- a/apps/webapp/app/components/layout/MetricsLayout.tsx +++ b/apps/webapp/app/components/layout/MetricsLayout.tsx @@ -178,7 +178,7 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
@@ -318,7 +318,7 @@ function MetricsLayoutGrid({ return (
{children}
; + return
{children}
; } export const MetricsLayout = { From 0ecd7682cd9564aae29e3f4f63921abbed3a5fd1 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:44:31 +0100 Subject: [PATCH 48/55] fix(webapp): space a chart's legend further from its title in fullscreen (TRI-12068) In the maximized chart dialog, the title's legend now sits gap-6 below the title instead of the card's gap-1 (item 3). --- apps/webapp/app/components/primitives/charts/ChartCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartCard.tsx b/apps/webapp/app/components/primitives/charts/ChartCard.tsx index c008db8069..69464cf340 100644 --- a/apps/webapp/app/components/primitives/charts/ChartCard.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartCard.tsx @@ -88,7 +88,9 @@ export function ChartCard({ {maximizable && ( - {title} + {/* In fullscreen, space the title's legend (the flex-col title node) further from the + title — gap-6 instead of the card's gap-1. */} + {title}
{parentSync ? ( From e64d9aed8199fc5d4dee0f7154ee87b82a92c9c9 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 16:44:32 +0100 Subject: [PATCH 49/55] fix(webapp): tighten Override info-icon gap; Throttled reads 'of current period' (TRI-12068) Pull the override explainer icon in to a gap-1 spacing from the 'Override' label (item 2). Change the Throttled chart readout from '{X}% of window' to '{X}% of current period' (item 4). --- .../route.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index b85a20f019..f250c88743 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -861,6 +861,9 @@ function QueuesWithMetricsView() { } contentClassName="max-w-[230px]" disableHoverableContent + // Tighten the gap from the "Override" label to gap-1 (the cell's + // trailing adornment gap is gap-2; -ml-1 pulls the icon in 4px). + buttonClassName="-ml-1" /> ) : undefined } @@ -1197,8 +1200,8 @@ type QueueHeaderTile = { /** Formats the y-axis tick labels. Without it the axis shows raw numbers (bad for durations * in ms or percent scales). Passed through to Chart.Line's yAxisProps.tickFormatter. */ formatAxis?: (value: number) => string; - /** Hover tooltip explaining the headline readout next to the title (e.g. what "9% of window" - * means). Without it the readout has no tooltip. */ + /** Hover tooltip explaining the headline readout next to the title (e.g. what "9% of current + * period" means). Without it the readout has no tooltip. */ totalTooltip?: string; // Rows can be one-per-bucket (p95, throttled: aggregated across the set in ClickHouse) or // one-per-(bucket, queue) (saturation, backlog: summed across the set here, since summing a @@ -1346,7 +1349,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [ return { points, total: pct, - formatTotal: (v) => `${v}% of window`, + formatTotal: (v) => `${v}% of current period`, totalClassName: pct > 0 ? "text-warning" : undefined, }; }, From aeaf6bab773ccbb0aad564ff4909496bdc961cd1 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 17:44:20 +0100 Subject: [PATCH 50/55] chore(webapp): consolidate the queue metrics server-change notes into one (TRI-12068) Merge the queue-metrics dashboard, live/clarity and override-limit notes into a single short release note. Leaves the unrelated notes (side menu, agent detail, duplicate-root-environments) from the base branch untouched. --- .server-changes/queue-metrics-dashboard.md | 2 +- .server-changes/queue-override-reject-above-limit.md | 6 ------ .server-changes/queue-pages-live-and-clarity.md | 6 ------ 3 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 .server-changes/queue-override-reject-above-limit.md delete mode 100644 .server-changes/queue-pages-live-and-clarity.md diff --git a/.server-changes/queue-metrics-dashboard.md b/.server-changes/queue-metrics-dashboard.md index 8574983170..74245fefe6 100644 --- a/.server-changes/queue-metrics-dashboard.md +++ b/.server-changes/queue-metrics-dashboard.md @@ -3,4 +3,4 @@ area: webapp type: feature --- -Queue metrics and health on the Queues page: per-queue depth, throughput, concurrency, throttling, and scheduling-delay charts, plus a per-queue detail view, live queue stats and a backlog chart on the task detail page, and a waiting-in-queue explainer in the run inspector for runs that have not started yet. Off by default; enabled per organization. +New Queue metrics & health dashboard (per-org opt-in): per-queue depth, throughput, concurrency, throttling and scheduling-delay charts, a per-queue detail view, and live queue stats. Queue concurrency limits set above the environment limit are now rejected instead of being silently capped. diff --git a/.server-changes/queue-override-reject-above-limit.md b/.server-changes/queue-override-reject-above-limit.md deleted file mode 100644 index 63d38690ad..0000000000 --- a/.server-changes/queue-override-reject-above-limit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -area: webapp -type: breaking ---- - -Setting a queue's concurrency limit above the environment limit is now rejected with a clear error instead of being silently capped. Existing overrides are unaffected. diff --git a/.server-changes/queue-pages-live-and-clarity.md b/.server-changes/queue-pages-live-and-clarity.md deleted file mode 100644 index 97785debe6..0000000000 --- a/.server-changes/queue-pages-live-and-clarity.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -area: webapp -type: improvement ---- - -Queue pages now refresh their live numbers automatically, and the charts are clearer: better axis labels, hover explanations for the headline stats, and the busiest concurrency key named on the chart. From 11d7a30fab763e592bafde70a34d1174ef14bfc6 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 18:31:08 +0100 Subject: [PATCH 51/55] fix(webapp): refine queue chart tooltip copy (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concurrency: 'are going' → 'are executing', drop the redundant 'color' from the two parenthetical swatches. Queue depth: drop the trailing swatch. Scheduling delay: cut the p50/p95/p99 breakdown. Throughput: parentheticals become ({swatch} Enqueued) / ({swatch} Started). --- .../route.tsx | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 4024b44024..777d54a121 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -382,8 +382,8 @@ function OverviewCharts({ title="Concurrency" info={ <> - How many runs are going at once ( color) versus - the queue's limit ( color). Turns{" "} + How many runs are executing at once () versus + the queue's limit (). Turns{" "} color when it reaches the limit. } @@ -413,12 +413,7 @@ function OverviewCharts({ /> - How many runs are waiting in this queue over time ( - color). - - } + info="How many runs are waiting in this queue over time." className="aspect-[2/1]" query={`SELECT timeBucket() AS t, max(max_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps @@ -431,8 +426,8 @@ function OverviewCharts({ title="Throughput" info={ <> - Runs arriving (Enqueued, color) versus starting - (Started, color). Turns{" "} + Runs arriving ( Enqueued) versus starting + ( Started). Turns{" "} color when Started falls behind. } @@ -453,13 +448,7 @@ function OverviewCharts({ /> - How long runs wait before they start: p50 ( color), - p95 ( color), and p99 ( - color). - - } + info="How long runs wait before they start." showLegend className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[1]) AS p50,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[3]) AS p95,\n round(quantilesMerge(0.5, 0.9, 0.95, 0.99)(wait_quantiles)[4]) AS p99\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} From e1127c1bf3ab91b58a6a72f06770694121c1d17b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 18:52:34 +0100 Subject: [PATCH 52/55] fix(webapp): show the series swatch in chart tooltips for gradient lines (TRI-12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tooltip colour indicator preferred the recharts item colour, which for a threshold/gradient line is a url(#…) gradient ref (invalid as a CSS background) — so the Concurrency 'Running' swatch was missing. Prefer the configured series colour. --- apps/webapp/app/components/primitives/charts/Chart.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/charts/Chart.tsx b/apps/webapp/app/components/primitives/charts/Chart.tsx index a742fbbde4..7a773b0e6f 100644 --- a/apps/webapp/app/components/primitives/charts/Chart.tsx +++ b/apps/webapp/app/components/primitives/charts/Chart.tsx @@ -176,7 +176,11 @@ const ChartTooltipContent = React.forwardRef< {payload.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = color || item.payload.fill || item.color; + // Prefer the series' configured colour over item.color: a threshold/gradient line's + // recharts colour is a `url(#…)` gradient ref, which is invalid as a CSS background and + // renders no swatch. The config colour is the intended solid series colour. + const indicatorColor = + color || itemConfig?.color || item.payload.fill || item.color; return (
Date: Thu, 23 Jul 2026 18:52:36 +0100 Subject: [PATCH 53/55] fix(webapp): make the warning-overlay line the same 1px as the base lines (TRI-12068) The orange over-threshold stretch was 2px vs the 1px base lines. It traces the same points, so 1px covers the base exactly and matches the other lines. --- apps/webapp/app/components/primitives/charts/ChartLine.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/charts/ChartLine.tsx b/apps/webapp/app/components/primitives/charts/ChartLine.tsx index 593f5b706d..000d74b91c 100644 --- a/apps/webapp/app/components/primitives/charts/ChartLine.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartLine.tsx @@ -597,14 +597,14 @@ export function ChartLineRenderer({ // Drawn after the base line so the warning colour sits on top. connectNulls={false} keeps // the mask to over-threshold stretches; excluded from the legend and (above) the tooltip. // Its active dot inherits the warning colour so the hover dot is yellow over yellow. - // Slightly wider than the base line so it fully covers it — otherwise the base colour peeks - // out along the edges and the warning stretch reads as an outlined line. + // Same 1px width as the base line so the warning stretch matches the other lines — it traces + // the same points over its over-threshold buckets, so it covers the base exactly. Date: Thu, 23 Jul 2026 18:52:37 +0100 Subject: [PATCH 54/55] fix(webapp): add the orange 'Falling behind' entry to the Throughput legend (TRI-12068) Add an optional extraLegend to QueueMetricChartCard for warning states that aren't their own series, and use it on Throughput so the orange 'Falling behind' colour is explained in the legend. --- .../app/components/queues/QueueMetricCards.tsx | 18 +++++++++++++++++- .../route.tsx | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/queues/QueueMetricCards.tsx b/apps/webapp/app/components/queues/QueueMetricCards.tsx index 9d32cc55d6..89ac7a7c2c 100644 --- a/apps/webapp/app/components/queues/QueueMetricCards.tsx +++ b/apps/webapp/app/components/queues/QueueMetricCards.tsx @@ -227,6 +227,7 @@ export function QueueMetricChartCard({ info, titleAccessory, className, + extraLegend, ...chart }: QueueMetricChartProps & { title: string; @@ -234,6 +235,9 @@ export function QueueMetricChartCard({ /** Extra content rendered after the info icon inside the title row (e.g. a live readout). */ titleAccessory?: ReactNode; className?: string; + /** Extra legend entries appended after the series — e.g. a warning state that isn't its own + * series (the orange "over threshold" colour). */ + extraLegend?: Array<{ color: string; label: string }>; }) { return (
@@ -253,7 +257,7 @@ export function QueueMetricChartCard({ {/* Inline legend below the title (swatch + label per series), matching the list-page charts — instead of the Chart.Root legend with per-series totals. */} - {chart.showLegend && chart.series.length > 0 ? ( + {chart.showLegend && (chart.series.length > 0 || (extraLegend?.length ?? 0) > 0) ? ( {chart.series.map((s) => ( ))} + {extraLegend?.map((e) => ( + + + {e.label} + + ))} ) : null} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 777d54a121..f24803ffa0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -432,6 +432,7 @@ function OverviewCharts({ } showLegend + extraLegend={[{ color: "var(--color-warning)", label: "Falling behind" }]} className="aspect-[2/1]" query={`SELECT timeBucket() AS t,\n deltaSumTimestampMerge(enqueue_delta) AS enqueued,\n deltaSumTimestampMerge(started_delta) AS started\nFROM queue_metrics\nGROUP BY t\nORDER BY t`} fillGaps From 35642d84ba30af48e90b7b6c9c96f8cb937a37ef Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 23 Jul 2026 18:52:38 +0100 Subject: [PATCH 55/55] fix(webapp): disableHoverableContent on the Queues table header tooltips (TRI-12068) Add disableTooltipHoverableContent to the Limited by, Delay p95 and Backlog header tooltips. --- .../route.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index f250c88743..c3eddaa775 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -673,6 +673,7 @@ function QueuesWithMetricsView() {

@@ -697,12 +698,14 @@ function QueuesWithMetricsView() { Delay p95 How many runs were waiting, over the selected time. marks