Commit Graph

7941 Commits

Author SHA1 Message Date
Saadi Myftija b334fcdd97 fix(cli): correct the artifact guard comment and skip-sync notice condition
Older servers reject the bundle artifact type outright, so the prefix
guard is defense in depth rather than primary detection; the skip-sync
notice now considers secret and parent vars like the classic path.
2026-08-21 20:23:59 +02:00
Saadi Myftija 0706c7979d fix(cli): review nits for the isolated bundle paths
Restore the skip-sync notice in local-bundle deploys and filter
undefined build env values on the from-bundle fresh-init fallback.
2026-08-21 17:12:43 +02:00
Saadi Myftija c16bbcb176 refactor(cli): isolate local-bundle and from-bundle from the classic deploy flow
The classic deploy and native build server paths are restored to their
original inline shape; local-bundle and from-bundle now live in their
own functions with a duplicated tail instead of a shared extraction, so
the existing flows carry zero behavioral risk from the new modes.
2026-08-21 17:04:29 +02:00
Saadi Myftija bbeea391f2 chore(cli): tighten the local-bundle flag descriptions 2026-08-21 17:00:00 +02:00
Saadi Myftija 4a1ebb1725 refactor(core,cli,webapp): drop the build env vars stored ack
An older server without build env var support fails the build on the
server side with a clear error, so the client-side ack guard, the
courtesy cancel, and the client-side limit pre-checks are unnecessary.
2026-08-21 16:59:46 +02:00
Saadi Myftija 140e4b3d30 feat(webapp): make deployment artifact and build env var limits configurable
Adds DEPLOYMENT_CONTEXT_ARTIFACT_SIZE_LIMIT_BYTES,
DEPLOYMENT_BUNDLE_ARTIFACT_SIZE_LIMIT_BYTES,
DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES (128 KB default) and
DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS (400 default). The limit error
messages now match the artifact size error wording.
2026-08-21 16:59:12 +02:00
Saadi Myftija 9ca53a074c fix(webapp): let build env var decrypt failures fall through to the generic error 2026-08-21 16:58:22 +02:00
Saadi Myftija a21baf018a chore(cli): reword the local-bundle changeset 2026-08-21 16:58:06 +02:00
Saadi Myftija c1d03d6d5d chore: trim inline comments to essential constraints 2026-08-21 11:37:38 +02:00
Saadi Myftija 03f3cf04d2 fix(cli): honor --dry-run with --from-bundle and guard bundle artifact type
Review feedback:

- --from-bundle now exits after validating the bundle manifest when
  --dry-run is set, before any server calls, matching the other deploy
  paths
- --local-bundle hard-errors when the created artifact key lacks the
  bundle-specific prefix, catching older servers that silently store the
  upload as a plain source context even when no build env vars are sent
2026-08-21 10:37:54 +02:00
Saadi Myftija bf9b894da9 fix(deploy): adapt build env vars flow to reused deployments and scoped auth
Two adaptations to changes that landed on main:

- the version-skew ack guard now runs after the reused-deployment early
  return, since an externally reused deployment builds nothing and never
  stores build env vars
- the build-env-vars endpoint uses the same scoped api key auth as the
  sibling deployment route, which migrated to authenticateApiKeyWithScope
2026-08-21 10:01:06 +02:00
Saadi Myftija 8abbe0571a fix(webapp): tolerate missing LOGIN_ORIGIN in isCloud
Test suites mock the env module with partial objects and import this
module transitively, so the preview-host check must not assume the
variable is set even though the schema gives it a default.
2026-08-21 09:54:37 +02:00
Saadi Myftija f3594dc47b fix(deploy): fail loud when stored build env vars cannot be read
Review feedback: a corrupt or undecryptable envelope previously returned an
empty record, indistinguishable from no vars at all, letting the remote
build run without its build-time secrets. The endpoint now returns an error
so the build aborts with an actionable message. An empty record remains the
response only for deployments that genuinely have none or are terminal.

Also cancel the deployment best-effort when the version-skew guard aborts,
instead of leaving it pending until the queue timeout reaps it.
2026-08-21 09:54:37 +02:00
Saadi Myftija 4a0eb683ab fix(deploy): drop undefined build env var values before sending
Extensions can set undefined values at runtime (e.g. env?.MISSING_VAR)
despite the manifest type. JSON serialization strips them, so the client
side emptiness check disagreed with what the server received and the
version-skew guard misfired.
2026-08-21 09:54:37 +02:00
Saadi Myftija 1e226e1970 feat(deploy): store local-bundle build env vars encrypted on the deployment
Replaces the trigger-build-args.json file and generated .dockerignore: the
bundle artifact is now secret-free. Build-arg values are sent with the init
request, stored aes-256-gcm encrypted in a new WorkerDeployment.buildEnvVars
column, and cleared on every terminal status transition.

- new dedicated GET /api/v1/deployments/:id/build-env-vars endpoint, used by
  the from-bundle build in attach mode; returns an empty record for terminal
  deployments and never 500s on a bad envelope
- size limits enforced server-side and pre-checked client-side (128 KiB
  serialized, 200 keys)
- version-skew guard: the CLI hard-errors when it sent vars and the server
  did not ack storing them
2026-08-21 09:54:36 +02:00
Saadi Myftija 08ce247f06 fix(cli): stop the bundle archiver dropping the indexer entry points
The bundler emits the controller entry points at paths mirroring the
CLI's install location, which contains a node_modules segment when the
CLI runs via npx. The archiver's blanket node_modules exclusion silently
stripped them from the uploaded bundle, so the image build failed at the
indexer stage with MODULE_NOT_FOUND. Only .DS_Store is excluded now.
2026-08-21 09:51:34 +02:00
Saadi Myftija 5c373ad643 fix(webapp): treat preview environments as cloud installs
isCloud() only matched the hardcoded cloud origins, so PR preview
environments never initialized the billing client and anything gated on
it silently no-op'd. Most visibly: remote builds were never enqueued and
deployments sat queued forever. Preview origins now count as cloud.
2026-08-21 09:51:34 +02:00
Saadi Myftija 332549f6db fix(cli): review feedback for --from-bundle error handling
- wrap the bundle JSON parses in real try/catch so a corrupt build.json
  or trigger-build-args.json surfaces the intended error message instead
  of a raw SyntaxError (the eager JSON.parse threw before tryCatch could
  see it)
- upsert the preview branch on fresh-init from-bundle deploys, matching
  the main deploy path (attach mode already has the branch env)
2026-08-21 09:51:34 +02:00
Saadi Myftija c564578090 test(cli): unit tests for the bundle archiver
Covers the cross-path contract: contents at archive root (extracted
without stripping), dotfiles and nested .trigger/skills included,
node_modules/.DS_Store excluded, dist-like names NOT excluded (the
bundle is build output), empty-dir error.
2026-08-21 09:51:34 +02:00
Saadi Myftija 45f2f6f5c9 fix(cli): warn when build-tuning flags are ignored with --local-bundle
The container build runs on the build server with fixed settings, so
local build-tuning flags (--compression, --no-cache, --builder, --network,
--push, --load, ...) are parsed but not forwarded. Warn instead of
silently dropping them, depot honored these flags, so local-bundle users
migrating from depot would otherwise assume they took effect.
2026-08-21 09:51:34 +02:00
Saadi Myftija 978bbba26b fix(webapp): allow host.docker.internal on the Vite dev server
Local docker builds and the dev build-server harness reach the dev webapp
as host.docker.internal (e.g. the in-build indexer fetching env vars);
Vite's default host blocking rejected those requests since the Vite
migration.
2026-08-21 09:51:34 +02:00
Saadi Myftija d7fb9ba6d7 fix(cli): round-2 review refinements for local-bundle
- sync env vars BEFORE initializing the deployment: initialization
  enqueues the remote build synchronously, so a post-init sync raced a
  fast build, a run triggered right after promotion could execute
  without the synced vars
- keep the bundle dir on --dry-run so the printed path is inspectable
- always append the build-args exclusions as the LAST .dockerignore
  lines so a pre-existing negation cannot re-include them
- warn when --from-bundle initializes a fresh deployment (attach mode
  is the supported flow)
2026-08-21 09:51:34 +02:00
Saadi Myftija 18c44a5a46 fix(cli): review fixes for the local-bundle paths
- --local-bundle now respects --dry-run (build the bundle, print its
  path, stop before any upload or deployment initialization)
- append to an existing .dockerignore in the bundle output instead of
  clobbering one a build extension may have written
- send config.runtime on initialization, identical to classic native
  deploys, instead of the resolved manifest runtime
- bundle artifacts get a distinct 'bundles/' key prefix so the build
  server can recognize a bundle even if the fromBundle flag is stripped
  by schema skew along the enqueue chain
2026-08-21 09:51:34 +02:00
Saadi Myftija 645a4a806c feat(webapp): accept deployment_bundle artifacts and thread fromBundle to build enqueue
The artifacts endpoint accepts the new bundle type (same key prefix and
size limit as deployment contexts), and initializeDeployment persists
fromBundle in the build server metadata and passes it through the
enqueue-build options so the build job can skip install/bundle.
2026-08-21 09:51:34 +02:00
Saadi Myftija 66101016c1 feat(cli): add --local-bundle and --from-bundle deploy modes
--local-bundle: bundle locally (install + esbuild, same as the classic
path), upload only the resulting build context as a deployment_bundle
artifact, and let the build server run just the container build. Env-var
sync happens client-side since the build server never sees the unscrubbed
manifest. The build-arg values (scrubbed from build.json) travel in
trigger-build-args.json, excluded from the image COPY context via a
generated .dockerignore.

--from-bundle <dir> (hidden): build the deployment image from a pre-built
bundle directory, skipping config loading and bundling entirely, used by
the build server's nested deploy for pre-bundled artifacts. Reuses the
extracted buildAndFinalizeDeployment tail shared with the classic path.

Bundle archives use a dedicated archiver: the source-context ignores
(dist, build, .trigger) would strip the bundle output itself.
2026-08-21 09:51:34 +02:00
Saadi Myftija 81c5d4ed74 feat(core): add deployment_bundle artifact type and fromBundle deployment flag
Schema groundwork for pre-bundled deploys: the CLI bundles locally and
uploads the build context as a deployment_bundle artifact; fromBundle on
the initialize request and BuildServerMetadata signals that the build
server should skip install/bundle and only run the container build.
2026-08-21 09:48:46 +02:00
DKP d04467018e feat(webapp,database): save platform notifications as drafts and publish later (#4743)
## Summary

The platform notifications admin page can now save a notification as a
draft without committing to a schedule, then publish it later by
entering start and end dates. Drafts stay hidden from the webapp panel,
the CLI, and the "What's new" changelog until they are published.

## Design

A draft is an `isDraft` flag on `PlatformNotification`, not nullable
dates, so the existing index and every read query stay intact. All three
reader queries filter on the flag, so a draft can never surface
regardless of its placeholder dates. Publishing writes the real start
and end dates and clears the flag; the publish dialog validates the
range and shows inline errors. Editing a draft keeps it a draft, with
the schedule fields hidden until publish.

Also folds in a small tweak: the "Send preview to me" test button now
appears when editing a notification, not just when creating one.
2026-08-20 22:16:32 +01:00
Eric Allam 1034b618a4 fix(webapp): let an impersonating admin preview the Queue Metrics UI (#4736)
## Summary

The Queue Metrics dashboard UI is gated by a per-org feature flag, so
there was no way to look at it for a real org without turning it on for
every member of that org. An admin impersonating into an org now sees
the metrics UI there regardless of the flag, so it can be checked
against real data before anyone else in the org sees it.

Nothing changes for a normal session: a member of an org whose flag is
off still gets the classic Queues page, and the gated sub-routes still
404.

## Design

The gate had no request and only resolved the org flag. It now takes the
request and resolves impersonation itself, rather than each caller
computing a boolean and passing it in, so the rule lives in one place
and a new call site cannot forget it. Seven call sites gate on this,
which is exactly why.

Two things narrow the bypass:

- It keys on **impersonation**, not `user.admin`. Impersonation is
scoped to one org and is deliberate; keying on admin status would
silently hand every admin the preview in their own day-to-day orgs.
- It yields to the **view-as-user** toggle. That toggle exists so an
impersonating admin can see what the member sees, and unreleased UI
leaking through it would make it lie. Suppressing a read-only view there
stays inside the display-only contract in `hasAdminDisplayAccess` (added
in #4421).

The bypass also stays behind the gate's existing org-membership lookup.
Since the acting user id is the impersonation target, that lookup is
what keeps the preview confined to the org actually being impersonated
into.

Verified end-to-end against a running instance across the matrix: member
with the flag off gets the classic view and 404s; the same org under
impersonation gets the metrics view and a 200; flipping view-as-user
returns it to the member's exact experience and back; and the flag-on
path is unchanged. An admin who is merely a member, not impersonating,
still gets the classic view.

One thing worth flagging: a few route comments say that with the flag
off no metrics reads fire. That remains true for every member session
and for the org as a whole, but an admin actively previewing does
exercise that org's real Redis and ClickHouse reads. That is inherent to
previewing, and bounded to one admin session.
2026-08-20 15:46:52 +01:00
Eric Allam 9baebbd1a6 fix(webapp): keep the dashboard agent's tool calls on the user's instance (#4740)
## Summary

Follow-up to #4738. Splits the dashboard agent's base URL into two: the
instance that hosts the agent project (used for sessions), and the
instance the agent acts against as the user (used by its read-tools).
#4738 only needed the first, but moved the second along with it, which
breaks the tools when the agent runs on a different instance than the
webapp.

## Root cause

The agent's read-tools call the API as the logged-in user via a
delegated user-actor token. The webapp signs that token with its own
`SESSION_SECRET`, scoped to its own `userId` and `environmentId`, so it
can only be verified by, and only resolves the user's data on, that same
instance. #4738 routed the injected `apiOrigin` those tools use to the
agent's host instance, so the token no longer verifies and the data
isn't there.

## Fix

`dashboardAgentApiOrigin()` stays the agent's host instance (sessions,
task triggers, realtime, the `in` forward). A new
`dashboardAgentUserApiOrigin()` returns the webapp's own origin
(`API_ORIGIN ?? APP_ORIGIN`) and is injected into the run metadata the
tools use. Same-instance deployments resolve both to the same host, so
behavior is unchanged there.
2026-08-20 15:27:27 +01:00
Eric Allam 56f875680c fix(webapp): let the dashboard agent use a configurable base URL (#4738)
## Summary

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

## Root cause

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

## Fix

`dashboardAgentApiOrigin()` now returns `DASHBOARD_AGENT_BASE_URL` or
the SDK default, never the webapp origin. A concrete default (rather
than an unset value) keeps it independent of `TRIGGER_API_URL`, which a
webapp may point at a different host. Every server call site already
routes through that helper; the client transport reads the value from
the root loader via a new `useDashboardAgentBaseUrl` hook.
2026-08-20 14:14:14 +01:00
claude[bot] 19eae515fd fix: rename the Projects org settings URL to /settings/projects (#4739) 2026-08-20 13:08:28 +00:00
Chris Arderne 4392e79ce2 chore: adopt stable React Compiler lint rules (#4737) 2026-08-20 14:17:40 +02:00
github-actions[bot] ce40d0259f chore: release v4.5.12 (#4610) helm-v4.5.12 v.docker.4.5.12 v4.5.12 2026-08-20 12:47:22 +01:00
Chris Arderne 06f99aeb31 fix: security release 2026-08-12 (#4735) 2026-08-20 12:34:33 +01:00
claude[bot] 518978bc52 fix(core): don't assume a 64-character idempotency key is pre-hashed on reset (#4626)
<!-- ccr-slack-attribution -->
_Requested by **Matt Aitken** · [Slack
thread](https://triggerdotdev.slack.com/archives/C045W9WM3E1/p1786741966214949?thread_ts=1786741966.214949&cid=C045W9WM3E1)_

`idempotencyKeys.reset()` now honours an explicitly passed `scope` even
when the key material happens to be 64 characters long.

**Before:** `resetIdempotencyKey` treated *any* 64-character string as
an already-computed hash and sent it to the API verbatim. That
short-circuit ran before the scope logic, so if your key material is
itself a 64-character digest (a common pattern when you hash your own
dedup identity) the `scope` you passed was silently discarded and the
un-hashed material went on the wire. The server stores the hash, so the
reset matched no run and returned 404 every single time. Key material of
any other length worked fine, which made this look arbitrary.

**After:** a 64-character key with an explicit `scope` is sent verbatim
first and, only when that attempt comes back a definitive not-found,
retried as the derived scope hash. Every call that worked before behaves
identically, and the previously impossible case now resolves on the
fallback.

## How

A 64-character string is forwarded unchanged, exactly as before, when:

- the idempotency key catalog recognises it (it came from
`idempotencyKeys.create()` in this process), or
- no `scope` was passed, so there is nothing to derive a hash from, or
- the scope hash cannot be derived (e.g. `scope: "run"` outside a task
context with no `parentRunId`).

Otherwise the key is ambiguous: it may be raw material the caller wants
hashed with the scope, or it may already be the stored hash. Reset sends
the verbatim value first because that is what every previous version
sent, so anything that resolved before still resolves with the same
single request, the same target run, and the same errors. The derived
hash is the new behaviour, so it only runs once the verbatim attempt has
failed with a 404, a definitive "no run under this key". Any other error
(a 503, a connection error) leaves the verbatim key's state unknown, and
resetting a different key on unknown state would be an untargeted write
the caller never asked for, so those errors surface unchanged. That has
an honest cost: when the endpoint answers 503 for a miss it cannot
confirm, the caller sees the 503 and retries rather than silently
falling through to the derived key. When both attempts miss, the
verbatim attempt's 404 is surfaced, again matching what previous
versions threw.

A side benefit of this order: a key from `idempotencyKeys.create()`
reset with a `scope` from a cold process resolves in a single request,
because the created key is itself the stored value.

`isIdempotencyKey` is deliberately left alone: it applies the same
length rule on the trigger path, but it is self-consistent there, and
changing it would invalidate already-stored keys.

The `attachedOptions?.key` / `attachedOptions?.scope` fallbacks below
the old guard were unreachable (every catalog entry is a 64-character
digest, so it always hit the short-circuit first) and re-deriving from
them produces the identical hash anyway. They are removed rather than
left as dead code.

---

##  Checklist

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

---

## Testing

Tests in `packages/core/src/v3/idempotencyKeys.test.ts` drive the real
`resetIdempotencyKey` against a local HTTP server and assert on the
exact values that reach the wire, in order. Nothing is mocked. They
cover:

- 64-character material + explicit `scope` derives the global- and
run-scoped hash once the verbatim key misses (fails without this change)
- the verbatim key wins when runs exist under both the verbatim value
and the derived hash, so the pre-existing target is preserved
- keys from `idempotencyKeys.create()` are forwarded unchanged: catalog
hit, no scope, and scope with a cold catalog (the last now a single
request)
- a transient failure of the verbatim attempt surfaces its error without
ever touching the derived key
- error surfacing: a double miss reports the key the caller passed, and
a non-404 from the fallback is not swallowed
- ordinary short material is still hashed, and underivable run/attempt
scopes still send a 64-character key verbatim while still throwing for
shorter material

```
pnpm run test ./src/v3/idempotencyKeys.test.ts --run   # 18 passed
pnpm run build --filter @trigger.dev/core              # clean
pnpm run format && pnpm run lint                       # clean
```

---

## Changelog

`idempotencyKeys.reset()` now works when your idempotency key is itself
64 characters long. Previously any 64-character key was assumed to be
already hashed, so passing one along with a `scope` silently ignored the
scope and the reset never found a matching run.

---

## Follow-ups (not in this PR)

- `docs/idempotency.mdx` describes the `idempotencyKey` parameter of
`reset()` as "the 64-character hash string" in one place while showing
raw material plus `{ scope: "global" }` a few lines later. Worth
reconciling.
- No surface currently exposes the stored hash that the reset endpoint
matches on: `ctx.run.idempotencyKey`, the run page and the
`idempotency_key` query column all show the user-provided key. That is
what leads people to send a value reset cannot match.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2026-08-20 12:14:51 +01:00
Chris Arderne c668b72c3f chore(webapp): enforce React Compiler lint (#4732)
## Summary

Enforces `react/react-compiler` as an error for the webapp now that all
reported compiler diagnostics are fixed or narrowly scoped. Removes the
unused lazy-ref helper made obsolete by the ref initialization cleanup.
2026-08-20 09:59:45 +01:00
Chris Arderne 2b2b047089 chore(webapp): scope imperative route refs (#4731)
## Summary

Scopes React Compiler diagnostics to route statements where refs
intentionally coordinate virtualized views, live reload state, transport
lifecycles, and deferred callbacks. Other compiler diagnostics remain
active in those routes.
2026-08-20 09:59:45 +01:00
Chris Arderne 4d040e13be chore(webapp): scope imperative component refs (#4730)
## Summary

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

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

Scopes React Compiler diagnostics for route effects that intentionally
synchronize loader data, navigation, submissions, polling, streams, and
transient UI state. Each suppression remains attached to the reported
synchronization call.
2026-08-20 09:59:44 +01:00
Chris Arderne 7673c46a02 chore(webapp): scope component effect synchronization (#4727)
## Summary

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

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

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

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

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

Captures chat-history age when the menu opens so rerenders cannot change
labels mid-view. The waitpoint deadline form also reuses one intentional
wall-clock snapshot for all calculations in a render.
2026-08-20 09:59:41 +01:00
Chris Arderne e394b5acf5 fix(webapp): timestamp live metric responses (#4719)
## Summary

Records when live metric responses arrive and uses that timestamp to
evaluate gauge freshness and waiting duration. Cached or failed
responses remain untrusted until revalidated, while rendered values stay
stable between polling updates.
2026-08-20 09:59:41 +01:00
Chris Arderne 34211e6649 fix(webapp): derive expiry status from loader time (#4718)
## Summary

Derives session and API key expiry states from a timestamp captured by
each route loader. Every status on a page now uses one consistent point
in time instead of changing according to when an individual component
rerenders.
2026-08-20 09:59:40 +01:00
Chris Arderne 11ea1f8ba9 fix(webapp): use stable chart bucket timestamps (#4717)
## Summary

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

Keeps render inputs and shared regular expressions immutable. Grouped
selects now compute each section's shortcut offset directly from
preceding sections, which also makes numeric shortcuts follow the
displayed item order reliably.
2026-08-20 09:59:39 +01:00