Skip to content

DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523

Open
github-actions[bot] wants to merge 119 commits into
mainfrom
develop
Open

DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523
github-actions[bot] wants to merge 119 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

If this change should result in new package versions please add a changeset before merging. You can do so by clicking the link provided by changeset bot below.

## Summary

Brings Origin's `Pagination` compound component up to parity with the
Base UI idioms used by the rest of Origin (matching the style of DES-18
#26842 and DES-19 #26829), and softens the API so callers without a
known total are no longer blocked.

[DES-21](https://lightspark.atlassian.net/browse/DES-21) (parent epic:
[DES-20](https://lightspark.atlassian.net/browse/DES-20)).

## Changes

- **`render` prop on every part.** Each part now goes through
`useRender`, gaining a `render` prop so consumers can swap the rendered
element. The motivating case is rendering `Pagination.Previous` /
`Pagination.Next` as `<a>` for shareable per-page URLs and
middle-click-to-new-tab.
- **`data-*` state attributes.** Component state surfaces via
`useRender`'s `state` + `stateAttributesMapping`:
  - Root: `data-page`, `data-first-page`, `data-last-page`
- Prev/Next: `data-disabled` mirrors the resolved disabled state (so
anchor renders pick up the disabled visual treatment uniformly with
`<button>` renders)
- **`aria-disabled` on every render path.** Anchors can't carry the
native `disabled` attribute, so `aria-disabled` is set whenever the part
is in its disabled state regardless of the rendered element.
- **`totalItems` is now optional.** When omitted:
- `Pagination.Next` no longer auto-disables — consumers control via the
`disabled` prop
- `Pagination.Range` requires a custom children render fn or no-ops with
a `devWarn`
  - `data-last-page` is absent (never present-and-empty)
Prefer the forthcoming `Pager` primitive (DES-22) for fully
unknown-total flows; this escape hatch unblocks consumers with partial
knowledge.
- **`usePaginationContext` is exported** (both as a named export and on
the compound) so consumers can build custom parts on top of context,
matching the `Combobox.useFilter` dual-surface pattern.
- **CSS gains `[data-disabled]` selectors** alongside the existing
`:disabled` selectors so anchor renders pick up the disabled visual
treatment.

## Out of scope

- Shared SCSS extraction for DES-22's `Pager` — DES-22 will duplicate
the styles. Pagination's button SCSS is unchanged in location and
structure.
- Analytics call surface stays as `useTrackedCallback` with format
`component.interaction`. Direction (`"next"` / `"previous"`) is added to
metadata.
- No page clamping, no new parts, no visual changes for existing
callers.

## Stories

- `URLBased`: anchor renders for Prev/Next
- `WithoutTotals`: optional-totals usage with custom Range children

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 436 passed (15
new for Pagination)
- [x] `yarn workspace @lightsparkdev/origin lint` — clean (only 2
pre-existing warnings outside this PR)
- [x] `yarn workspace @lightsparkdev/origin format` — clean
- [x] `yarn workspace @lightsparkdev/origin types` — clean
- [ ] Reviewer: skim Storybook `URLBased` and `WithoutTotals` stories
- [ ] Reviewer: confirm the `Pagination.Next` no-auto-disable behaviour
matches the intent for unknown-totals flows

Made with [Cursor](https://cursor.com)

[DES-21]:
https://lightspark.atlassian.net/browse/DES-21?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-20]:
https://lightspark.atlassian.net/browse/DES-20?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: 646153fe7d4023be440f9b0572580ae1e6e5671e
## Summary
- expose Origin Button's existing Base UI `render` / `nativeButton`
support in its TypeScript props so product wrappers can render it as a
typed router link without reimplementing visuals
- add a focused Grid `NageButton` wrapper that only owns typed routing
props (`to`, `toParams`, `hash`) around Origin Button
- keep the branch intentionally narrow: no legacy shared UI Button
import, no Emotion compatibility layer, no legacy prop mapping, and no
consumer migration yet
- add a Vitest contract test for routing and transparent Origin prop
pass-through

## Validation
- `yarn vitest run src/uma-nage/components/NageButton.test.tsx
--environment jsdom`
- `yarn tsc --noEmit --pretty false` in `js/apps/private/site`
- `yarn types` in `js/packages/origin`
- `yarn vite build` in `js/apps/private/site`

GitOrigin-RevId: 633ace9159779598d69b44177bbfd3ba9ffe233a
…6920)

## Summary

Ships a new Origin compound primitive `LoadMore` and a
transport-agnostic
companion hook `useLoadMore` for forward-only infinite scroll. Third of
three
sibling pagination primitives under epic
[DES-20](https://lightspark.atlassian.net/browse/DES-20)
(after [DES-21](https://lightspark.atlassian.net/browse/DES-21)
`Pagination` and
[DES-22](https://lightspark.atlassian.net/browse/DES-22) `Pager`).

Resolves [DES-23](https://lightspark.atlassian.net/browse/DES-23).

## Component API

`LoadMore` follows the new Origin idiom standard — `forwardRef`
everywhere,
exported context hook (`useLoadMoreContext`), `data-*` state attributes,
Base
UI `useRender` `render` escape hatch on every overridable part.

- **`Root`** — headless context provider over `{ hasMore, loading,
onLoadMore, analyticsName }`. Renders only its children.
- **`Trigger`** — composes Origin's `Button` by default; swap with
`render={<Button variant=\"ghost\" />}` (or anything else).
Auto-disables when `!hasMore || loading`, forwards `aria-busy`, exposes
`data-loading` / `data-has-more` / `data-disabled`.
- **`Sentinel`** — `IntersectionObserver`-backed invisible trigger with
stable refs so the observer effect doesn't re-subscribe on every state
change. SSR-safe; includes a post-load re-evaluation pass for cases
where the new page didn't grow tall enough to scroll the sentinel out of
view. \`disabled\` renders no DOM at all.
- **`Status`** — \`aria-live=\"polite\"\` + \`aria-atomic\` SR-only slot
with render-prop children: \`{({ loading, hasMore }) => loading ?
\"Loading more results\" : !hasMore ? \"End of results\" : \"\"}\`.

## Hook API

\`useLoadMore\` is a generalisation of nage's
\`useGridApiPaginatedQuery\`:
same request-id race guard, same item accumulation, same
\`JSON.stringify(resetOn)\` reset semantics — but accepts a generic
\`fetchPage(cursor)\` callback instead of being hard-coded to the Grid
API.

\`\`\`ts
const { items, hasMore, loading, loadingMore, loadMore, refetch, error }
=
  useLoadMore({ fetchPage: (cursor) => …, resetOn: [filter] });
\`\`\`

- Maintains an internal \`requestIdRef\` so a slow first response cannot
clobber state set by a later \`refetch\` / \`resetOn\` change.
- \`fetchPage\` is read from a ref, so consumers don't need
\`useCallback\`.
- Rejected \`fetchPage\` lands in \`result.error\` (coerced to
\`Error\`); \`loading\` / \`loadingMore\` clear; existing \`items\` are
preserved. Error clears on the next fetch start.

## Analytics

Following the \`component.interaction\` convention:
- Trigger: \`\${name}.click\` with \`metadata: { part: \"trigger\" }\`.
- Sentinel: \`\${name}.intersect\` with \`metadata: { part: \"sentinel\"
}\`.

The part is in metadata, not the event name. Adds \`\"intersect\"\` to
\`InteractionType\`.

## Tests

- **Vitest** (\`useLoadMore.unit.test.ts\`, 10 tests): initial fetch,
\`enabled: false\` toggle, accumulation across pages, \`hasMore: false\`
gates \`loadMore\`, concurrent-loadMore is a no-op, race-guard against a
slow initial response, \`resetOn\` change resets and refetches,
\`refetch\` clears items, error capture preserves prior items, error
clears on next fetch.
- **Playwright CT** (\`LoadMore.test.tsx\`): Trigger enabled /
disabled-when-no-more / disabled-while-loading / custom render; Sentinel
scroll-in / no-refire-while-loading / disabled-renders-nothing; Status
default / loading / end variants; throws when used outside Root;
analytics emit; end-to-end pagination through \`useLoadMore\`.

## Verification

\`\`\`
yarn workspace @lightsparkdev/origin types        # clean
yarn workspace @lightsparkdev/origin test:unit    # 431 pass (10 new)
yarn workspace @lightsparkdev/origin lint # clean (2 pre-existing
warnings unchanged)
yarn workspace @lightsparkdev/origin format       # clean
\`\`\`

## Files

- New: \`js/packages/origin/src/components/LoadMore/\` (component, hook,
scss, stories, test stories, CT tests, hook unit tests, index)
- Modified: \`js/packages/origin/src/index.ts\` (barrel adds),
\`js/packages/origin/src/components/Analytics/AnalyticsContext.tsx\`
(\`\"intersect\"\` interaction type)

## Out of scope

- Migrating existing \`useGridApiPaginatedQuery\` consumers — follow-up.
- Visual loading skeletons — consumers compose \`Skeleton\` themselves.
- Bidirectional infinite scroll — DES-23 is forward-only.

[DES-23]: https://lightspark.atlassian.net/browse/DES-23

Made with [Cursor](https://cursor.com)

[DES-20]:
https://lightspark.atlassian.net/browse/DES-20?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-21]:
https://lightspark.atlassian.net/browse/DES-21?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-22]:
https://lightspark.atlassian.net/browse/DES-22?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: c12f0de6e4763ee0584785572614e9d54705d282
@github-actions
github-actions Bot requested a review from a team as a code owner April 30, 2026 21:58
@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

The following public packages have changed files:

Changed Current version
@lightsparkdev/lightspark-cli 0.1.18
@lightsparkdev/lightspark-sdk 1.9.18
@lightsparkdev/eslint-config 0.0.1
@lightsparkdev/crypto-wasm 0.1.25
@lightsparkdev/origin 0.14.2
@lightsparkdev/static 0.0.0
@lightsparkdev/oauth 0.1.67
@lightsparkdev/core 1.5.1
@lightsparkdev/vite 0.0.1
@lightsparkdev/ui 1.1.19

There are no existing changesets for this branch. If the changes in this PR should result in new published versions for the packages above please add a changeset. Any packages that depend on the planned releases will be updated and released automatically in a separate PR.

Each changeset corresponds to an update in the CHANGELOG for the packages listed in the changeset. Therefore, you should add a changeset for each noteable package change that this PR contains. For example, if a PR adds two features - one feature for packages A and B and one feature for package C - you should add two changesets. One changeset for packages A and B and one changeset for package C, with a description of each feature. The feature description will end up being the CHANGELOG entry for the packages in the changeset.

No releases planned.

Last updated by commit 89fc07e

jaymantri and others added 25 commits May 1, 2026 18:18
…de conflict (TS2430) (#26931)

## What's broken

`LoadMoreTriggerProps` in
`js/packages/origin/src/components/LoadMore/LoadMore.tsx` extends
`Omit<ButtonProps, "onClick" | "disabled" | "loading">` and then
redeclares `render` with a wider state type (`TriggerRenderState` adds
`hasMore` and `loading` on top of `ButtonState`). Because `Omit` doesn't
drop `render`, TypeScript flags the override as incompatible:

```
TS2430: Interface 'LoadMoreTriggerProps' incorrectly extends interface 'Omit<ButtonProps, "onClick" | "loading" | "disabled">'.
  Types of property 'render' are incompatible.
  Type 'ButtonState' is missing the following properties from type 'TriggerRenderState': hasMore, loading
```

This is currently failing the site app's `tsc` (run during `yarn build`)
on every open PR.

## The fix

Add `"render"` to the `Omit` clause so the trigger's wider render-state
declaration is the only one on `LoadMoreTriggerProps`:

```ts
export interface LoadMoreTriggerProps
  extends Omit<ButtonProps, "onClick" | "disabled" | "loading" | "render"> {
```

One-token change.

## Why it slipped past origin's tests

DES-23 (#26920) introduced the regression. Origin's `test:unit` runs
vitest but does not type-check the site app, so the conflict only
surfaces when `apps/private/site` runs `tsc` as part of `yarn build`.

## Verification

- `yarn workspace @lightsparkdev/origin test:unit` → 447 tests pass
- `yarn workspace @lightsparkdev/origin lint && … format` → clean (only
pre-existing warnings)
- `cd apps/private/site && find . -maxdepth 3 -name
'tsconfig.tsbuildinfo' -delete && yarn tsc` → passes cleanly, no
`LoadMore` errors

## Urgency

Blocking the site build on all open PRs — please land ASAP.

Made with [Cursor](https://cursor.com)

GitOrigin-RevId: c77577a1f91e3e9c6f2e86b31124021c29175e29
## Reason

A standalone browser-based example app is needed to demonstrate and
manually exercise the full Grid Global Accounts API lifecycle, including
credential creation, verification, session management, and wallet
operations across all three supported authentication types (EMAIL_OTP,
OAUTH, and PASSKEY).

## Overview

Adds a new Vite + TypeScript single-page example app at
`js/apps/examples/grid-global-accounts-example-app` that covers:

- **Platform auth**: API client ID/secret input with sandbox and
production mode selection. Sandbox uses magic string constants
(`sandbox-valid-signature`, `000000`, `sandbox-valid-oidc-token`,
`sandbox-valid-passkey-signature`). Production mode generates a
client-side P-256 keypair, HPKE-decrypts the
`encryptedSessionSigningKey` returned by Verify using `@turnkey/crypto`,
and stamps `payloadToSign` values via `@turnkey/api-key-stamper`.
- **Customer setup**: Create customer and fetch internal account
balance, with auto-propagation of account/credential/session IDs into a
shared wallet context used across all tabs.
- **Per-type lifecycle tabs** for EMAIL_OTP, OAUTH, and PASSKEY, each
covering: wallet creation, credential verification → session,
rechallenge, and two-step signed-retry flows for adding a second
credential, deleting a credential, deleting a session, and exporting the
wallet.
- **External account creation** for both `SPARK_WALLET` and
`USD_ACCOUNT` types, quote creation with `payloadToSign` extraction,
payload signing (sandbox magic or real Turnkey stamp), and quote
execution.
- A Vite dev server proxy that rewrites `/api` requests to
`https://api.lightspark.com/grid/2025-10-13`.

The app is registered on port `3106` in `settings.json`.

## Test Plan

Run `yarn dev` from the app directory and manually exercise each tab's
lifecycle against the sandbox environment using the pre-filled magic
values. Verify that signed-retry flows correctly populate `requestId`
from step 1 and forward it with `Grid-Wallet-Signature` in step 2. For
production mode, generate a P-256 key, run a Verify step, then use "Sign
payload" before executing a quote to confirm HPKE decryption and Turnkey
stamping work end-to-end.

GitOrigin-RevId: fe887c117e70114303ebf6de67b9449fc8059c7b
## Summary
- lowers Origin reset/global selectors with `:where(...)` so component
and app styles can override Origin defaults without separate overrides
- splits Origin's public stylesheet into root/document/scopable
internals and adds `@lightsparkdev/origin/scope.scss`
- scopes reusable Origin global rules under `html.origin` while keeping
token/font root setup available at document level
- switches the private site to import the scoped Origin stylesheet,
toggling `html.origin` for auth and Grid/Nage routes while preserving
Emotion globals on legacy routes
- preserves the `--doc-height` viewport resize sync for both paths:
Emotion `GlobalStyles` keeps its updater for other apps, while
Origin-scoped site routes mount a small equivalent because they
intentionally skip `GlobalStyles`
- adds legacy `SuisseIntl` / `SuisseIntl-Mono` font-family aliases for
existing UI typography consumers when Origin globals are active
- removes unused `pretty-scrollbar` globals from both Origin and Emotion
global styles
- updates Origin package exports/files/package checks so SCSS
entrypoints are published and package validation ignores non-JS style
entrypoints in `attw`
- fixes the Origin `LoadMore` trigger type conflict exposed once the
private site imports Origin styles

## Validation
- `git diff --check`
- `yarn workspace @lightsparkdev/origin package:checks`
- `yarn workspace @lightsparkdev/origin lint:styles`
- `yarn workspace @lightsparkdev/origin build:styles`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Button/Button.test.tsx`
- `yarn workspace @lightsparkdev/site exec eslint src/Root.tsx`
- `yarn workspace @lightsparkdev/ui exec eslint src/styles/global.tsx`
- `yarn turbo run types --filter=@lightsparkdev/site`
- pre-commit hook passed earlier for the global stylesheet split (`yarn
install`, `yarn format`)
- Playwright spot checks on local `start:dev`:
- `/login` has `html.origin`, Origin body styles (`14px / 20px "Suisse
Intl"`), Origin background/text tokens, and the body breakpoint marker
- RSK `/dashboard` has no `html.origin`, keeps Emotion globals (`12px /
14.52px Montserrat`), and keeps the breakpoint marker
- RSK `/transactions/sent` keeps Emotion globals and restored
transaction empty-state/card spacing (`320x128`, `32px` padding)

## Notes
- This PR is now the base of the button-render work; #26933 stacks on
top of it.
- `scope.scss` intentionally prefixes Origin global rules with
`html.origin`; non-Origin routes continue to use the existing Emotion
global stylesheet.
- Storybook-only local changes used for visual testing remain
uncommitted.

GitOrigin-RevId: d6ae738f069fe1daffb41301762dd50bc553cab4
…n domain (#26977)

## What

Small change to BarChart so signed-value bars anchor at the zero line —
negatives hang down, positives grow up — instead of all rendering from
the plot bottom. Sheets, Looker, d3 defaults, and recharts all do this;
Origin was the odd one out.

For each non-stacked bar we compute `anchor = clamp(0, yMin, yMax)` and
draw between `anchor` and the value:

- **All-positive data** — anchor lands at `yMin` (bottom). Visual
identical to before.
- **Mixed signs** — anchor is `0`. Positives grow up, negatives hang
down.
- **All-negative data** — anchor lands at `yMax` (top). Bars hang down
to their value.

Same treatment applied to the horizontal orientation. Stacked path is
intentionally untouched — cumulative semantics already differ from the
simple value→height mapping.

## Why

Came up while building a daily net inflow/outflow bar chart in
lighthouse — the chart's domain spanned negative values, but every red
bar was rendered from the bottom of the plot area up to the value, which
made small negative days look as severe as the worst negative day.

## Not a breaking change

- No API change — no props added, removed, or retyped.
- All-positive data renders pixel-identical (`clamp(0, yMin, yMax) =
yMin` when yMin is 0).
- Only diffs are mixed-sign and all-negative charts, which were arguably
broken before this.

## Notes

- Originally proposed against the old origin repo at
lightsparkdev/origin#129; moved here per @coreymartin.
- Lighthouse currently has a small recharts-based bar chart bridging the
signed-data case
([lighthouse#383](lightsparkdev/lighthouse#383)).
Plan is to drop that bridge and use Origin directly once this lands.

## Test plan
- [ ] Existing storybook bar charts (all-positive) render identically —
visual diff is a no-op.
- [ ] Mixed-sign story: bars cross the zero line cleanly.
- [ ] Horizontal orientation: bars extend left of the zero column for
negatives.
- [ ] Stacked path unchanged.

GitOrigin-RevId: 807866e8c7aa64e986b8b31f370630e162cabb6c
## Reason

The Nage login flow is starting to adopt Origin buttons, and the auth
page needs the Origin-backed actions to render with the same visual
treatment and spacing as the existing SSO action.

## Overview

- Builds on the scoped Origin globals that landed in #26900, now that
this PR targets `main` directly.
- Adds `fullWidth` support to Origin `Button` and covers it in
tests/stories.
- Bridges the app theme to Origin's `data-theme` tokens for Origin
components rendered in the private site.
- Updates login email and SSO actions to use `NageButton` with the
previous 10px button spacing preserved at the auth form layout level.
- Adds the Origin mono font asset needed by the scoped Origin
stylesheet.

## Test Plan

- `git diff --check`
- `yarn workspace @lightsparkdev/origin package:checks`
- `yarn workspace @lightsparkdev/origin lint:styles`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Button/Button.test.tsx`
- `yarn workspace @lightsparkdev/site exec eslint src/Root.tsx
src/components/AuthForm.tsx src/pages/login/Login.tsx
src/uma-nage/components/NageButton.test.tsx`
- `yarn turbo run types --filter=@lightsparkdev/site`

GitOrigin-RevId: 5ea673b4ae149244197416c602ad5c936e116c1b
## Summary
- add `checks-tests` as the JS check command that keeps `gql-codegen`,
lint, format, circular dependency checks, and package checks in the same
Turbo task flow while also running `test`
- run `yarn checks-tests` from the JS workflow check job, keeping
`.lightsparkapienv` for tests and removing the obsolete `.lightsparkenv`
setup
- make Origin package `test` run Vitest unit tests only, with Playwright
component tests under `test:ct` / `test:all`
- make Origin package `build` run TypeScript checks before emitting
styles

## Notes
- Measured recent `ui-test-hermetic` Chromium install steps at 25s, 26s,
33s, 37s, and 42s, so this PR intentionally avoids adding Playwright
browser installation to JS CI for now.

## Test Plan
- `yarn install --immutable`
- `yarn checks-tests`
- `git diff --check`
- pre-commit JS format hook

GitOrigin-RevId: 64f9c2942a870c40540faaeb9d19f119736cf171
## Summary
- Add an Origin Storybook main-branch deploy workflow that publishes to
`s3://lightspark-dev-web/app/origin-storybook/` for
`https://dev.dev.sparkinfra.net/app/origin-storybook/`.
- Switch Origin Storybook static builds from `@storybook/nextjs` to
React Vite, matching the package as a static SPA bundle.
- Fix Vite CSS Modules handling for Combobox chips and add the missing
Suisse font assets used by Origin tokens.
- No ops PR is needed: the existing webdev `github-actions` dev role
already allows writes to `lightspark-dev-web/*`.

## Test Plan
- `yarn turbo run build-sb --filter=@lightsparkdev/origin --force`
- `rg -n "@use|@include|url\\(/fonts/SuisseIntl|src=\\\"/|href=\\\"/"
js/packages/origin/storybook-static/index.html
js/packages/origin/storybook-static/iframe.html
js/packages/origin/storybook-static/assets -g "*.css"` (no matches)
- `ruby -e "require \"yaml\"; YAML.load_file(ARGV.fetch(0)); puts
\"ok\"" .github/workflows/deploy-origin-storybook.yaml`
- `git diff --check`
- Local Playwright smoke against `http://127.0.0.1:8081/`: Storybook
loaded, 0 console errors; one Storybook 11 ariaLabel warning from
manager UI.
- `yarn workspace @lightsparkdev/origin lint` (0 errors; 2 pre-existing
accessibility warnings)
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Combobox/Combobox.test.tsx` (21 passed)

GitOrigin-RevId: 1f59c53078e04db5a741a90a85789ab5cf491845
## Summary
- add Origin Storybook to the existing PR UI preview deployment matrix
- deploy Storybook previews under `/app/origin-storybook-pr-<PR>/`,
using the existing generic `/app/*` routing instead of changing
`/preview/*` behavior
- pass the preview base path through Turbo for `build-sb`
- delete `/app/origin-storybook-pr-<PR>/` during existing PR preview
cleanup
- include a small Origin finalizer comment change so this PR triggers a
real preview deployment

## Validation
- `bash -n scripts/gha/detect-ui-preview-apps.sh`
- `node --check
js/packages/origin/scripts/finalize-storybook-static.mjs`
- `git diff --check`
- YAML parse for preview deploy and cleanup workflows
- detector matrix test for
`js/packages/origin/scripts/finalize-storybook-static.mjs`
- `ORIGIN_STORYBOOK_BASE_PATH=/app/origin-storybook-pr-27001/ yarn turbo
run build-sb --filter=@lightsparkdev/origin --force`
- generated HTML check: expected `/app/origin-storybook-pr-27001/`
`<base>` paths and no inline scripts in `index.html` / `iframe.html`
- PR UI Preview Deploy passed, including Origin Storybook upload to
`s3://lightspark-dev-web/app/origin-storybook-pr-27001/`

GitOrigin-RevId: 203ef90b92ae10f85a4df0b5e3c4a60dc58c3c71
## Reason

Scoped Origin globals were adding `html.origin` specificity to reset
selectors. That let resets like a transparent button background outrank
component classes until hover, so components consuming
`@lightsparkdev/origin/scope.scss` could render with reset styling
instead of their intended variant styles.

## Overview

Wrap the scoped entrypoint in zero-specificity `:where(...)` selectors.
This keeps scoped globals limited to `.origin` routes while allowing
Origin component classes to win over resets normally.

## Test Plan

- `git diff --check`
- `yarn workspace @lightsparkdev/origin lint:styles`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin playwright test -c
playwright-ct.config.ts src/components/Button/Button.test.tsx`
- Confirmed `html.origin :where(button)` is gone and the scoped selector
uses `:where(html.origin)`

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: f42b15ebe010458ab483d7ab19904e7de85fdce8
…#26924)

## Summary

Aligns Origin's Combobox with [Base UI's documented multi-select
pattern](https://base-ui.com/react/components/combobox#multiple-select)
and deletes the workarounds DES-18 layered on top of the original
divergence.

The audit traced the workarounds back to one architectural choice:
`Combobox.Value` always wrapped `BaseCombobox.Value` in an extra
`<span>`, even though Base UI's Value is renderless. To keep that span
from breaking the flex layout in `Combobox.Chips`, the file carried
`valueWithChildren { display: contents }`. From there, a `:has(.chip)`
rule (which itself needed a synthetic marker class), and a `.chip +
.input` adjacent-sibling margin all stacked up to recover spacing. None
of these appear in Base UI's documented pattern.

Pulling that thread further surfaced two more layers of accidental
complexity (an `AnchorContext` that the InputGroup primitive made
redundant, and a single-select branch on `Combobox.Value` that no
consumer used). Both are dropped here.

## What changed

1. **`Combobox.Value` is renderless when `children` is a render
function.** Chips and Input become direct flex children of
`Combobox.Chips` with no intermediate `<span>`. Single-select path
unchanged.
2. **Drop the synthetic `.chip` marker class and the `:has(.chip)`
rule.** `Combobox.Chip` applies `Chip.module.scss`'s `root` + `sm`
directly. Wrapper `padding-left` is reduced via `:has(.chips)` (which
always matches in multi-select).
3. **Replace `.chip + .input { margin-inline-start }` with `.chips
.input { padding-inline-start }`.** Same visual outcome, no
adjacent-sibling dependency, mirrors Base UI's demo (Input owns its own
left padding).
4. **`Combobox.ItemCheckbox` no longer overwrites consumer `render`.**
Default `render` is applied before `{...props}`, so a consumer-supplied
`render` wins.
5. **`Combobox.Separator` wraps `BaseCombobox.Separator`** instead of a
hand-rolled `<div role="separator">`, matching the rest of the file and
exposing the `render` escape hatch.
6. **`Combobox.InputWrapper` now wraps `BaseCombobox.InputGroup`.** The
exported `Combobox.InputWrapper` name is preserved (consumers don't
migrate). Migration retires the `&:has(.input:disabled)` style hack in
favor of `&[data-disabled]` (always available), and adds
`&[data-focused]` alongside `:focus-within` so the focus ring fires
under `Field.Root` via the data attr and outside `Field.Root` via the
legacy selector. The existing `&[data-invalid]` rule now actually fires
under `Field.Root` (previously the raw `<div>` carried no such attr).
7. **Replace the broken `Multiple` story with the chips pattern.** The
previous story used the single-select layout with `multiple` —
selections were made but not surfaced anywhere (no `Combobox.Value`
render, no chips). New `Multiple` story matches Base UI's canonical
multi-select example, with `aria-label` on `Combobox.ChipRemove`
("Remove Apple") rather than `Combobox.Chip` (the chip's value is its
own visible label; the remove button needs disambiguation).
8. **Drop `AnchorContext`.** Post item 6, `BaseCombobox.InputGroup`
self-registers as `inputGroupElement` in the combobox store
([`ComboboxInputGroup.js:55`](https://github.com/mui/base-ui/blob/master/packages/react/src/combobox/input-group/ComboboxInputGroup.tsx)),
and `BaseCombobox.Positioner` resolves its anchor as `anchor ??
(inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement)`
([`ComboboxPositioner.js:59`](https://github.com/mui/base-ui/blob/master/packages/react/src/combobox/positioner/ComboboxPositioner.tsx)).
Origin's manual ref-forwarding through context was pointing at the same
DOM node Base UI auto-resolves to, so the entire context plumbing is
dead. Dropping it also unblocks Base UI's input-inside-popup pattern
(which the hardcoded override silently closed off).
9. **Collapse `Combobox.Value` to a `BaseCombobox.Value` pass-through.**
The remaining single-select branch (wrapping in `<span class="value">`)
had zero call sites — every story and the playground page render the
selected value via `Combobox.Input`, not `Combobox.Value`. The `.value`
SCSS rule, dual-mode forwardRef wrapper, dev-mode `console.warn`, and
the `ConformanceValue` test fixture (already skipped) are all gone.
`ValueProps` is now `BaseCombobox.Value.Props`.

## Deferred to follow-up

- **`Combobox.Chip` child-splitting.** Currently splits children by
type-equality with `ChipRemove` and wraps the rest in `<span
class={chipStyles.label}>`. Load-bearing for label-text styling reuse
with standalone `Chip` — removal requires a SCSS shuffle and parallel
cleanup of standalone `Chip`. Filed as a separate ticket.

Resolves [DES-24](https://lightspark.atlassian.net/browse/DES-24).
Follows up on [DES-18](https://lightspark.atlassian.net/browse/DES-18)
(#26842).

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 447 pass
- [x] `yarn workspace @lightsparkdev/origin lint` — clean (only 2
pre-existing unrelated warnings in `DatePicker` and `Sidebar`)
- [x] `yarn workspace @lightsparkdev/origin format` — clean
- [x] `apps/private/site` `yarn tsc` — clean
- [x] Visual: `Default` story unchanged (border, chevron, focus ring on
input click, popup open/close)
- [x] Visual: `Disabled` story renders with reduced opacity via
`&[data-disabled]`
- [x] Visual: `Multiple` story (empty / one chip / many-chip overflow)
renders identically; cursor still has breathing room when typing after a
chip; selecting items live now adds chips with `Remove <fruit>`
accessible names on the dismiss buttons.
- [x] Visual: `WithField` story — `data-focused` fires on focus and
applies `--input-focus` ring; `data-invalid` fires on
blur-with-empty-value and applies `--border-critical` +
`--input-focus-critical`. Verified via runtime inspection of computed
styles on the live `[role=group]` element.
- [x] Anchor resolution: with `AnchorContext` removed, runtime check on
`Default`, `Multiple`, `WithGroups`, `WithField` confirms popup
`--anchor-width` matches the `[role=group]` element's
`getBoundingClientRect().width` to within 1px sub-pixel rounding. Base
UI's auto-resolve from `inputGroupElement` is working.

## Consumer impact

None expected. The single-select `Value` path is unchanged at the
call-site level (consumers don't pass `<Combobox.Value />` for
single-select today). Multi-select `Value` no longer renders the outer
`<span>`, but consumers don't reach into that DOM.
`Combobox.InputWrapper`'s exported name is preserved; the underlying
primitive change to `BaseCombobox.InputGroup` is transparent.
`Combobox.Value` is now a direct re-export of `BaseCombobox.Value`;
props are forwarded natively. Grid `AddCustomerPanel.tsx`
`FieldMultiCombobox` continues to work without changes.

Made with [Cursor](https://cursor.com)

[DES-24]:
https://lightspark.atlassian.net/browse/DES-24?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: 1479d119d378b69bcce7cd9fd92fdac0969482dd
## Summary
- make dev proxy cookie-file ALB cookies override stale localhost
browser ALB cookies
- detect Cognito redirects and ALB-generated HTML 500s as stale dev
proxy session signals
- launch the existing Cognito cookie refresh flow and return explicit
retry guidance
- let the cookie refresh script honor the configured cookie file path

## Testing
- node --check js/packages/vite/index.js
- node --check js/apps/private/scripts/dev-proxy-cookies.mjs
- prettier --check js/packages/vite/index.js
js/apps/private/scripts/dev-proxy-cookies.mjs
- git diff --check -- js/packages/vite/index.js
js/apps/private/scripts/dev-proxy-cookies.mjs
- pre-commit hook: js yarn install + yarn format
- manual Playwright repro: LoginWithPassword on stale localhost ALB
cookies returned GraphQL Invalid credentials instead of ALB HTML 500
after fix

GitOrigin-RevId: 4bd794889f9b52b5f51ea73c2595100d95fc1eb0
## Summary
- migrate the ops DLQ page to DataManagerTable pagination and task-name
filtering
- add shared table row selection so retry/delete only uses selected DLQ
messages
- add DLQ GraphQL cursor/page_info support for internal and paycore
schemas with stable SQS snapshot pagination

## Validation
- yarn eslint src/pages/ops/dead-letter-queue/DeadLetterQueue.tsx
- yarn eslint src/components/Table/Table.tsx
- yarn types in js/apps/private/ops
- yarn types in js/packages/ui
- uv run python -m py_compile
sparklib/graphql/objects/root_to_dead_letter_queue_messages_connection.py
sparklib/graphql/queries/ops/dead_letter_queue_messages_query.py
sparklib/graphql/mutations/ops/manage_dead_letter_queue_message.py
- git diff --check

GitOrigin-RevId: 1d44acfc14220c2d7717fe90cc12faa8697bb7f7
## Reason

Field.Root should expose the same Base UI composition surface that
Origin consumers expect from other primitives. Allowing the Base UI
`render` prop avoids product-side workarounds when a product needs to
render the field root as a semantic or framework-specific element.

Jira: https://lightspark.atlassian.net/browse/DES-26

## Overview

This exposes the inherited Base UI `render` prop on `Field.Root` and
keeps Origin root styling merged for both string and stateful
`className` callbacks. The component test stories and CT coverage
exercise a custom rendered root, invalid state propagation, class
merging, and stateful root class names.

Separate visual Storybook review passed. The unrelated focus/error
contrast issue found during review was filed as DES-27.

## Test Plan

- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Field/Field.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- No app Playwright run: direct Origin component CT changed; no app
Playwright overlap.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 6f8fd21ba4b04b1747777d098808e983a8a7a98a
## Reason

DES-25 needs `Combobox.Chip` composition to match Base UI pass-through
behavior instead of relying on child introspection/type checks. This
keeps wrapped `ChipRemove` usage working and brings standalone `Chip`
into the same direct-children rendering model.

## Overview

- Remove the `Combobox.Chip` child split between label content and
`ChipRemove`; children now pass directly through to Base UI.
- Render standalone `Chip` children directly for parity, while moving
default chip typography/color styling to the root and preserving dismiss
spacing.
- Add Combobox and Chip coverage for arbitrary child content and wrapped
chip removal.

Refs DES-25.

## Test Plan

- User visually reviewed and approved Storybook.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Combobox/Combobox.test.tsx
src/components/Chip/Chip.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: ceeef4f54667f17f2a0666fb3bafabad5e8050fb
## Reason

Field.Label is a flex container, so relying on JSX whitespace between
label text and suffix children is unreliable. DES-30 needs a consistent
tokenized gap so suffix content such as "(optional)" does not visually
run into the label.

https://lightspark.atlassian.net/browse/DES-30

## Overview

Adds the Origin Field label gap using `var(--spacing-3xs)` and covers
the suffix case with Storybook and component-test fixtures.

## Test Plan

- User visually reviewed the Field Storybook state and confirmed it
looks good
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Field/Field.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: a5eea0aa22374e07e8102ff6ca15003cfd00cd0b
## Reason

Select popups should consume Base UI's available-height and
available-width variables like Combobox and Autocomplete, so long option
lists stay bounded by the viewport instead of extending offscreen.

Refs [DES-29](https://lightspark.atlassian.net/browse/DES-29).

## Overview

- Bounds the Select popup and list with Base UI sizing variables.
- Lets long Select lists scroll within the popup.
- Adds a long-list Storybook example and component-test regression
coverage.

## Test Plan

- User visually reviewed the LongList Storybook story and approved.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Select/Select.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

Made with [Cursor](https://cursor.com)

[DES-29]:
https://lightspark.atlassian.net/browse/DES-29?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 5ba77d455fdefa102a307d57d6a2694f5ea77d97
## Reason

Base UI Button should keep button semantics and not be used as the
link-rendering path. This adds a generic Origin `ButtonLink` so
anchor-based actions can preserve link semantics while sharing Button
visuals.

DES-31: https://lightspark.atlassian.net/browse/DES-31

## Overview

- Add `ButtonLink` as an anchor-rendering visual button API in Origin,
including `href` and render-composition support.
- Keep `NageButton` action mode on Origin `Button`, and route mode on
`ButtonLink` composed with the typed router `Link` so routed Nage
buttons remain `role=link`.
- Add ButtonLink stories/tests and NageButton routed-link semantics
coverage.

## Test Plan

- User visually reviewed Storybook and approved.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Button/Button.test.tsx`
- `yarn workspace @lightsparkdev/site vitest run
src/uma-nage/components/NageButton.test.tsx --environment jsdom`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/site types`
- Skipped app Playwright: this change has focused Origin component and
NageButton jsdom coverage, with no router or route-level changes.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 990f3a61857ce0f06361b8057b082c75b9bee1a0
## Reason

DES-28: Align `Tabs.Panel` with the underlying Base UI primitive content
slot so product layouts can own their own panel presentation instead of
inheriting card-like styling from Origin.

https://lightspark.atlassian.net/browse/DES-28

## Overview

This removes layout, padding, border, overflow, and corner styling from
`Tabs.Panel` while preserving the hidden-state behavior needed for
inactive panels. Storybook examples now apply their card presentation at
the story level so the component stays thin over Base UI without adding
a public `variant` or `unstyled` API.

## Test Plan

- User visually reviewed Storybook and approved
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Tabs/Tabs.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: e773751e727a25590db98d79ca5876496a40274e
## Summary

Introduces `Pager`, a new compound primitive in `@lightsparkdev/origin`
for cursor / keyset / time-window pagination. Tracks
[DES-22](https://lightspark.atlassian.net/browse/DES-22).

`Pagination` requires `page` + `totalItems`. Cursor-based APIs don't
have totals (counting is expensive and often meaningless), so consumers
today fake totals or maintain a cursor↔page mapping just to use the
visual button group. `Pager` absorbs that friction: it accepts only
`hasPrevious` / `hasNext` plus `onPrevious` / `onNext`. Visually it is
intentionally a copy of `Pagination.Navigation`.

## API

- `Pager.Root` — `<nav aria-label="Pager">`, owns context and analytics
wrapping. Carries `data-no-previous` / `data-no-next` boundary attrs.
- `Pager.Navigation` — `<div role="group" aria-label="Page
navigation">`. The joined-pill container.
- `Pager.Previous` / `Pager.Next` — `<button>` with auto-derived
`disabled` (overridable), `data-direction`, `data-disabled`, default
chevron icon. Composes consumer `onClick`; `event.preventDefault()`
suppresses the context handler.
- `Pager.Status` — `<span role="status" aria-live="polite"
aria-atomic="true">`. Always mounted so the live region survives empty
renders.
- `usePagerContext` / `PagerContext` — exported for advanced
composition.

Every part forwards refs and accepts `render` via Base UI's `useRender`,
so `render={<a href="?after=…" />}` swaps the underlying element while
preserving handlers, refs, ARIA, and class names.

## Decisions

- **Visual parity with `Pagination` is byte-identical and inlined.** No
`composes:`, no shared SCSS partial. Pager owns its own complete CSS. If
DES-21 later extracts the shared rules into a Pagination-namespaced
mixin, it can DRY both modules in a follow-up.
- **Analytics follow `component.interaction`.** With `analyticsName` set
on Root, clicks fire `Pager.click` with `metadata: { direction:
"previous" | "next" }` via `useTrackedCallback`.
- **Status renders the `<span>` even when children are empty** so the
live region stays mounted across renders.

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` (20 new vitest
specs, 441 total passing)
- [x] `yarn workspace @lightsparkdev/origin lint` (0 errors)
- [x] `yarn workspace @lightsparkdev/origin format` (clean)
- [x] `yarn workspace @lightsparkdev/origin types` (clean)
- [ ] Playwright CT specs in `Pager.test.tsx` cover structure, derived
disabled, click composition (incl. `preventDefault`), keyboard
activation, render-prop swap to `<a>`, data-attrs, and visual parity
with `Pagination` via `boundingBox` + computed `border-radius` /
`box-shadow` / `background-color`. Ready to run via `test:ct` when CI
executes the suite.
- [ ] Storybook covers Default, NoPreviousCursor, NoNextCursor,
BothEdges, WithoutStatus, WithRenderPropAsLink,
SideBySideWithPagination.

## Coordination

DES-21 owns any future extraction of the shared Prev/Next button styles
into a Pagination-namespaced SCSS partial. This PR inlines the rules as
a deliberate duplicate per the explicit decision recorded on DES-22.

Made with [Cursor](https://cursor.com)

[DES-22]:
https://lightspark.atlassian.net/browse/DES-22?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: 0b175dbcd4355bdec285074bc9498c570568d5d2
## Summary

- Increase Yarn npm minimal age gate from 1 day to 3 days for the JS
workspace.
- Keep @lightsparkdev packages preapproved through the existing wildcard
exemption.

## Impact

New npm package versions must now be at least 4320 minutes old before
Yarn considers them for installation, reducing exposure to freshly
published compromised packages.

## Validation

- yarn config get npmMinimalAgeGate
- pre-commit hook: yarn install
- pre-commit hook: yarn format

GitOrigin-RevId: bdc27882e2e466827479bcd3828674bc6521af81
## Reason

The backend stack (#27196 → #27206) wires USDT-on-Tron all the way through paycore, the LSP grid switch, and the rental layer — but the Grid dashboard frontend has no way to actually pick USDT or Tron in the payout flow. This PR adds the four missing pieces in `js/apps/private/site/src/uma-nage` so platforms with USDT in their backend config see it as a funding source and Tron as the network.

## Overview

**Currency-and-network plumbing** (`currencyFields.ts`):
- `USDT` registered as a crypto currency with `TRON` as its only supported network (matches our backend: USDT-Tron is the only USDT corridor we support).
- `TRON → TRON_WALLET` added to `CRYPTO_NETWORK_TO_ACCOUNT_TYPE`.
- Tron Base58Check address validator (`^T[1-9A-HJ-NP-Za-km-z]{33}$`) added to `CRYPTO_ADDRESS_VALIDATORS`. **Not EVM-style** — Tron uses its own base58check encoding, not 0x hex; getting this wrong was the most likely subtle bug.
- USDT entry in `CURRENCY_ACCOUNT_FIELDS` with the same single "Wallet address" field that USDC has.

**Quote-flow network selector** (`EnterAmountPanel.tsx::getCryptoNetworkOptions`):
- When the realtime-funding currency is USDT, return only the Tron option (`TRON_TESTNET` in sandbox / local-dev, `TRON_MAINNET` in prod).
- USDC keeps its current Base / Polygon / Solana options unchanged.

**Platform-config gating**: works automatically. `realtimeFundingOptions` is built from `platformConfig.supportedCurrencies` (lines 172–180), so USDT only appears in the funding-source dropdown if the platform has USDT enabled server-side. No new feature flag needed on the frontend; no risk of USDT appearing for platforms that don't have it.

**Tron chain icon** (`packages/ui/src/icons/chains/`):
- New `Tron.tsx` component (Tron-brand red circle + angular logo).
- Added to `ChainIcon`'s `Chain` type and `CHAIN_COMPONENTS` map.

## Test Plan

- 8 new test cases in `currencyFields.test.ts` for the Tron Base58Check validator: canonical addresses, T-prefix requirement, length, disallowed base58 chars (`0`, `O`, `I`, `l`), EVM-style address rejection.
- 17/17 `currencyFields.test.ts` cases pass total (8 new + 9 existing for Solana / Base / Polygon).
- `yarn lint && yarn format && yarn tsc --noEmit` all clean across `@lightsparkdev/site` and `@lightsparkdev/ui`.

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

GitOrigin-RevId: 50ac10aa773bb53f4cf5107811ff95059d7ae1f6
## Summary

Makes the local site Playwright E2E workflow Kind-first and
self-contained. The normal path is now:

```bash
cd js/apps/private/site
./playwright/run-tests.sh
```

The runner creates or repairs the Kind cluster if needed, starts Tilt
with the lightning profile, waits for core backend resources,
verifies/restarts `sparkcore-app` when Kubernetes readiness disagrees
with Tilt, seeds Nage/Grid prerequisites, deploys the built site as a
static nginx pod, and runs Playwright against
`https://app.minikube.local`.

### Local runner and environment management

- **`run-tests.sh`**: Single entry point for local E2E runs. Handles
Kind setup/repair/recreate, Tilt startup, backend readiness, stale
Sparkcore recovery, static-site deploy, ingress guarding, Nage/Grid seed
checks, and local retries defaulting to single-pass.
- **Stripe billing**: The Kind/static-deploy path now runs the real
Stripe PaymentElement billing flow locally. The old local Stripe
skip/exclusion flags, minikube auto-exclusion, and direct GraphQL
billing fallback have been removed.
- **`--clean`**: Runs and waits for `playwright/destroy-test-env.sh`
before proceeding, giving a fresh local app state in one command.
- **`destroy-test-env.sh`**: Tears down Tilt, Kind/minikube runtime
state, managed hosts entries, Playwright auth/results, `dist`, and turbo
daemons.
- **`setup-kind.sh` / `k8s/` manifests**: Fallback Kind setup and static
nginx deployment resources for serving the site inside the local
cluster.

### Test and fixture fixes

- **RSK/Nage setup projects**: Split setup state so RSK tests can run
even if Nage setup fails; Nage onboarding now verifies Grid readiness
and USDB support.
- **Go-live/billing flow**: Runs the real Stripe HTTPS redirect path on
Kind and keeps only a small real-UI retry for Stripe PaymentElement
submit timing.
- **Payments and UI flakes**: Adds targeted waits/recovery for local
backend latency and two-worker concurrency races.
- **Local seeding**: Ensures billing plans, UI test gatekeepers, and
Nage/Grid switch data exist before tests run.

### Docs

- **README.md**: Updated for the current Kind-first flow, `--clean`,
local troubleshooting, two-worker project execution, current trace/video
behavior, and the fact that Kind runs the full billing flow with Stripe.

## Test plan

- [x] `bash -n js/apps/private/site/playwright/run-tests.sh
js/apps/private/site/playwright/destroy-test-env.sh
js/apps/private/site/playwright/setup-kind.sh`
- [x] `git diff --check`
- [x] `cd js/apps/private/site && yarn lint`
- [x] `cd js/apps/private/site && ./playwright/run-tests.sh
--test-filter=playwright/tests/00-go-live-billing.spec.ts` (Kind/static
deploy, Stripe included; 8 passed)
- [x] Local Kind setup path exercised after `destroy-test-env.sh`:
recreated Kind, configured DNS/registry, started Tilt, and reached
backend resource waits
- [ ] `cd js/apps/private/site && yarn types` currently fails in
unrelated existing app source files with broad
`CurrencyUnit`/`CurrencyUnitType` mismatches outside this PR's changed
files
- [ ] CI hermetic Playwright run

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ec87440e08a583e495329f28e0a83651d011459c
k15z and others added 30 commits June 23, 2026 21:21
## Summary
- add opt-in sortable headers to the shared UI table component
- enable sorting on treasury flow columns using raw numeric/date values
while preserving formatted cells
- move treasury action buttons into a single top-right row and remove
the flows caption

## Testing
- yarn workspace @lightsparkdev/ui types
- yarn workspace @lightsparkdev/ui build
- ./node_modules/.bin/prettier --check
packages/ui/src/components/Table/Table.tsx
apps/private/ops/src/pages/ops/treasury/OpsTreasury.tsx
- ../../node_modules/.bin/eslint src/components/Table/Table.tsx
- ../../../node_modules/.bin/eslint
src/pages/ops/treasury/OpsTreasury.tsx

## Notes
- yarn workspace @lightsparkdev/ops types currently fails on existing
CurrencyUnit/CurrencyUnitType mismatches outside this change path,
including the pre-existing treasury amount cell.

GitOrigin-RevId: 1246dad76c3c9aaf796f90f89b415ced3e5acd30
## Reason

The ops DLQ page showed blank **Task Args** / **Task Kwargs** on the
message detail. The snapshot rewrite (#27848) made the page read from
persisted snapshots, which intentionally store only metadata (no
args/kwargs) for data-at-rest safety — so the payload was never
surfaced.

Fixes the originating Slack report.

## Overview

Keep snapshots metadata-only, but surface the live payload transiently:

- **sparkcore** `OPS_create_dead_letter_queue_snapshot` now reads the
live queue **with** payload (`include_task_payload=True`) and returns
the live tasks (real `task_args`/`task_kwargs`) on a new `messages`
field, plus `is_current`. The persisted snapshot row stays metadata-only
(`to_snapshot_message()`). The recent-snapshot window now only dedups
the snapshot **DB write** — every refresh still reads the live queue, so
args/kwargs are always returned (no stale-cache path).
- **ops UI** shows the live data as **\"Current · N messages\"**
(args/kwargs populated on the detail page) as the primary view, and
falls back to the latest persisted snapshot (metadata-only,
**read-only**) only when the live queue is empty.
- Detail navigation passes the message via **in-memory router state**
(never the URL) so payloads don't land in history/logs; `actionable`
state gates retry/delete so historical snapshot rows are read-only
end-to-end.
- Shared `Table` gains optional `onClickRow().state` pass-through
(additive) to support the above — this restores the intended
router-state navigation the detail page already reads.

### Reviewer notes (accepted, documented)
- The live response is bounded by the existing `MAX_DLQ_MESSAGES_TO_READ
= 200` cap and matches the pre-snapshot live page behavior; no separate
payload byte-cap was added (a fail-the-refresh cap would be a worse
failure mode).
- cmd/ctrl-click opens the detail in a new tab without the in-memory
payload and shows \"Message not found\" — deliberate, since the payload
is kept out of the URL; normal click works.

## Test Plan

- New `test_create_dead_letter_queue_snapshot.py` (GraphQL boundary, SQS
mocked): a fresh read returns `messages` with real args/kwargs while the
**persisted** snapshot stays metadata-only; empty queue returns no
current messages / no snapshot; repeated refreshes within the window
both return live payload and persist only one snapshot row.
- `uv run ruff check`, `uv run ty check`, `uv run
scripts/export-graphql.py`, `yarn gql-codegen`, eslint on changed FE
files, `@lightsparkdev/ui` build (typechecks `Table.tsx`).
- Rendered the page in a local harness (mocked GraphQL): live
\"Current\" list with populated args/kwargs on the detail; read-only
snapshot fallback (no checkboxes, no retry/delete); state-nav verified
to keep the payload out of the URL.

## Private

[Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/primal-plasma/plan.md)
(S3, internal only)

## Public

Ops DLQ page now shows Celery task args/kwargs again, read live from the
queue.

---
🤖
[primal-plasma](https://zeus.dev.dev.sparkinfra.net/#/arc?id=primal-plasma)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=primal-plasma)
| [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

Original PR: lightsparkdev/webdev#29470

---------

Co-authored-by: Bolt Agent <bolt@lightspark.com>
Co-authored-by: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
GitOrigin-RevId: 9b6276580be285ebaffea6121db2442291be78a0
## Reason

Replace #28877 with equivalent dependency changes opened by
@coreymartin, since the dependency-maintainer gate needs a JS dependency
maintainer to author this bump. Per follow-up, this PR now targets
`main` directly and includes the Origin Base UI utility cleanup plus the
React 1.6 retarget in one PR.

## Overview

- Removes Origin's vendored Base UI utility shim and direct Base UI
utility imports.
- Updates Origin's Base UI utility dependency path so `@base-ui/react`
owns `@base-ui/utils@0.3.1` transitively.
- Updates Origin's `@base-ui/react` dependency to `^1.6.0`.
- Leaves `date-fns` and `@date-fns/tz` uninstalled; they remain optional
peer metadata from Base UI only.

## Test Plan

- [x] `cd js && yarn install` -- passed with existing peer warnings
- [x] `git diff --check`
- [x] `cd js && yarn workspace @lightsparkdev/origin types`
- [x] `cd js && yarn workspace @lightsparkdev/origin build`
- [x] `cd js && yarn workspace @lightsparkdev/origin test:unit
src/components/Pagination/Pagination.unit.test.tsx
src/components/Pager/Pager.unit.test.tsx
src/components/LoadMore/LoadMore.unit.test.tsx
src/components/LoadMore/useLoadMore.unit.test.ts` -- 48 tests passed
- [x] `cd js && yarn why @base-ui/react && yarn why @base-ui/utils &&
yarn why date-fns && yarn why @date-fns/tz` -- confirmed Base UI React
1.6.0, transitive Base UI utils 0.3.1, and no installed date packages
- [x] Commit hook: `yarn install` and JS `yarn format` passed

---------

Co-authored-by: jaymantri <jay@lightspark.com>
GitOrigin-RevId: a9ea280c60ab8cca9337fe2e1c669f1e30b85ed3
## Summary
Polish pass on the Grid Billing reconciliation ops UI (from a Slack
request with screenshots). Seven fixes:

1. **Titles** → `Grid Billing — Reconciliation` on both the detail view
and the table view (were `— Transaction` / `— Transactions`).
2. **Payment id** — the external Grid transaction id at the top of the
detail view now renders in neutral gray instead of link-blue (it's
Grid's own id for the incoming payment, not an LSID and not a link).
3. **Transaction ID column** added to the table, linking each row's ent
id to its ops inspector page (`OpsInspectorDetails`). The cell link
`stopPropagation`s so it doesn't also trigger the row's detail-page
navigation.
4. **`Attribute` button** — dropped the ellipsis (`Attribute…` →
`Attribute`).
5. **Attribution search** now accepts **either a platform LSID/UUID**
(resolved directly via `EntUmaaasPlatform.gen_nullable`) **or a
case-insensitive name prefix**. Previously name-prefix only, so pasting
an LSID returned nothing. Unparseable input falls back to the name
search (returns empty rather than erroring).
6. **Modal width** — attribute / reject / match modal contents used
`minWidth: 420` inside a 460px modal (420 + 56px inner padding
overflowed); switched to `width: 100%` so they fit.
7. **`Pending review` badge** → yellow, in both the table and the detail
view. Added an additive `warning` `BadgeKind` to the shared
`@lightsparkdev/ui` Badge (light-yellow bg + amber text, using existing
`warningBackground`/`warningText` tokens). `Rejected`/`Unattributed`
stay red.

## Test plan
- New pytest cases in `test_grid_billing_queries.py` for the attribution
search: by LSID, by raw UUID, an LSID for a non-billing platform →
empty, and a garbage string → empty. Full file: **18 passing**.
- `uv run ty check` + `ruff check`/`format` clean; schema regenerated
(`export-graphql.py` + `gql-codegen` — description-only diff).
- ops app `tsc --noEmit` clean; eslint + prettier clean;
`@lightsparkdev/ui` builds with the new badge kind.
- Frontend rendered in a screenshot harness (the ops app has no
unit-test runner) — all seven fixes verified visually; posted to the
Slack thread.
- `bolt-codex-review`: no P0/P1/P2 findings.

## Private

[Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/covert-vertex/plan.md)
(S3, internal only)

## Public
Ops-only Grid Billing reconciliation page polish.

---
🤖
[covert-vertex](https://zeus.dev.dev.sparkinfra.net/#/arc?id=covert-vertex)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=covert-vertex)
| [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

Original PR: lightsparkdev/webdev#29672

---------

Co-authored-by: Bolt Agent <bolt@lightspark.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: kphurley7 <kphurley7@users.noreply.github.com>
GitOrigin-RevId: 880f0b6ecc4a4306c5cd47bccaa11defe0d09e87
…abel restructure, Table separate borders (DES-77) (#29805)

## Reason

The Grid dashboard table-system work (stacked PR to follow) needs
several Origin primitives extended, and a Figma token-alignment sweep
plus the DES-77 Table hairline fix were due regardless of that consumer.

## Overview

- **ChipFilter**: values can now be `ReactNode`; numeric values are
coerced into the dismiss label and an explicit empty `valueLabel` is
honored as an a11y fallback; interactive-value styling is gated behind a
new typed `ChipFilter.Trigger` compound part (promoted from a
`data-chip-trigger` opt-in), with a dev warning when `valueLabel` is
missing.
- **CentralIcon** no longer emits an inline `currentcolor` style, so
stylesheet rules can reach icon svgs directly.
- Icon, border, and surface tokens aligned with the Figma spec across
Menu, Combobox, Breadcrumb, Chart, DatePicker, NavigationMenu, and
Shortcut (dead `data-icon` selectors dropped; DatePicker nav hover
re-gated on hover capability).
- **Field**: label gains a structured `Field.LabelSuffix` slot;
description and error flow as block text instead of flex.
- **Table** moves to `border-collapse: separate` so hairlines render at
token width (DES-77).
- `devWarnOnce` dedupes dev warnings to once per session; token-probe
test helper shared via test-utils; changeset added covering the
consumer-facing changes.

## Test Plan

- 481 Origin unit + component tests passing
- `tsc` clean
- `stylelint` clean

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: c4ca500464d3557d299d857f57ddcdcc2d4c8456
## Summary

Moves the remaining site Vitest wiring from #29808 into this
maintainer-owned PR so the test/package changes can land together:

- replaces the placeholder site test scripts with `vitest run` /
`vitest`
- adds `jsdom@^29.1.1` for the site and Origin workspaces
- adds the site Vitest config, shared setup file, and React date picker
stubs
- applies the test fixes needed for the site suite to run under Vitest

## Validation

- `cd js && yarn install --immutable`
- `cd js && yarn run build --filter=@lightsparkdev/site`
- `cd js && yarn workspace @lightsparkdev/site test` (93 files / 842
tests passed)
- `cd js && yarn deps:check && yarn turbo run test gql-codegen lint
format circular-deps package:checks --filter=@lightsparkdev/site
--filter=@lightsparkdev/origin`
GitOrigin-RevId: 923519379eff40c7d3a7ef6c81fe315a1a173d2b
…846)

## What was broken

Submitting a form that contains `PhoneInput` and reading
`FormData.get("phoneNumber")` returned the selected country code (e.g.
`"US"`) instead of the phone number the user typed.

Why: Base UI's Select renders a hidden `<input>` purely for form
serialization. When a consumer wraps the whole phone input in one
`Field.Root` — the natural composition for a labeled field — the phone
`<Input name="phoneNumber">` registers its name on the field, and the
Select's hidden input inherits that same name. Because the hidden input
sits earlier in the DOM, `FormData.get("phoneNumber")` returns its value
— the country — instead of the phone number.

## The fix

`CountrySelect` now establishes its own field boundary: a nested
`Field.Root` styled `display: contents`, so it adds no visual box. A
nested `Field.Root` is Base UI's only mechanism to stop field-context
inheritance — there is no opt-out prop. Inside the boundary the hidden
input no longer sees the outer field's name: it is nameless by default
(excluded from serialization), and passing `name` to `CountrySelect`
serializes the country under its own key.

## State propagation

The boundary also blocks the outer field's disabled/invalid context, so
`CountrySelect` re-threads PhoneInput's own `disabled`/`invalid` props
(the documented way to drive PhoneInput state) into the nested field —
the country trigger keeps its disabled behavior and `aria-invalid`.
**Intentional behavior change:** state set only on the outer
`Field.Root` and not mirrored on `PhoneInput.Root` no longer reaches the
country trigger; Base UI has no public API to read across a field
boundary, and Origin's styling never depended on it.

## Consumer test change

`NagePhoneInputField.test.tsx` (site app) previously asserted *two*
controls named `phoneNumber` and told them apart with an `aria-hidden`
filter — a workaround that encoded the buggy behavior. Post-fix those
assertions would fail, so this PR reverts them to the strict form:
exactly one control named `phoneNumber`, and the hidden serialization
input carries no name.

## Validation

- Origin unit suite: 486 tests / 13 files passed (includes new
PhoneInput serialization and state-propagation tests).
- PhoneInput Playwright CT suite: 17/17 passed (invalid-state test now
also asserts the trigger is marked invalid).
- Site vitest suite: 849 tests / 93 files passed, including the restored
strict test.
- `tsc`, ESLint + stylelint, and Prettier clean in both workspaces.

## Context

- Raised by @kphurley7 on #29808
([thread](lightsparkdev/webdev#29808 (comment))).
- Base UI supports one control per `Field.Root` (mui/base-ui#3928).
- DES-51 tracks the consumer workaround this PR removes.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 88e29422990ece6b3dd400ffb6df19dafe138205
## Reason

The hermetic UI workflow now runs Playwright, but the repo still carried
legacy Cypress tests, scripts, dependencies, and CI path filters.
Keeping both made the active UI test path harder to reason about and
kept Cypress in the JS dependency graph.

## Overview

- Remove the private site Cypress test tree, config, runner script, and
package dependencies.
- Remove dead Cypress scripts/dependencies from `uma-bridge`.
- Map `@lightsparkdev/site` `test:ui` to Playwright and rename `pw-*`
aliases to `test:ui:*` scripts.
- Update `ui-test-hermetic` to run `yarn test:ui`.
- Replace the old `window.Cypress` automation flag with
`window.lightsparkTestAutomation` for Playwright fixtures.

## Test Plan

- `yarn install --immutable`
- `yarn run --top-level turbo run build:deps
--filter=@lightsparkdev/site`
- `yarn workspace @lightsparkdev/site lint`
- `yarn workspace @lightsparkdev/site types`
- `yarn workspace @lightsparkdev/site test:ui --list`
- `yarn why cypress` returns no resolved package
GitOrigin-RevId: 5b5097e9da696b30131915f49203c02b8771ad78
## Reason

Origin needs a one-time password input for verification-code flows. Base
UI 1.6.0 (already Origin's pinned version) ships an `otp-field`
primitive; this wraps it in Origin styling and conventions rather than
hand-rolling slot management, paste distribution, or keyboard
navigation.

Figma spec: [Origin design system — OTP
field](https://www.figma.com/design/3JvbUyTqbbPL8cCpwSX0j4/Origin-design-system?node-id=7171-2883).
Design and code are token/geometry parity-verified.

## Overview

- New `OTPField` compound component (`Root` / `Input` / `Separator`)
wrapping `@base-ui/react/otp-field`, exported from the package root.
- `Root` defaults to 6 numeric slots with `length` configurable,
supports controlled/uncontrolled `value`, `onValueChange`,
`onValueComplete`, `validationType`, `disabled`, `readOnly`, `mask`, and
`autoSubmit` (all Base UI Root props pass through).
- A childless `Root` auto-renders its slots with per-slot `aria-label`s
("Character N of M"); explicit children support grouped layouts
(`123-456`) with `Separator`.
- Field integration follows the house pattern: compose inside
`Field.Root` with `Field.Label` / `Field.Error`; the label associates
with the first slot and invalid state cascades to every slot via Base
UI's Field context.
- Styling uses the shared `input` / `input-focus` / `input-critical`
mixins and tokens only; 8px cell gap (`--spacing-xs`) so critical halos
on adjacent invalid cells don't overlap.
- Storybook stories follow the Input/Select convention (bare component
stories + one `WithField` story carrying the Field/invalid composition),
plus a minor changeset and SKILL.md entry.

### Thermos review fixes applied

- Deleted the `aria-label` render-shim in `Input`; the auto-render
branch passes labels as plain props and Base UI natively handles the
`aria-labelledby` swap and first-slot guard. This also stops the shim
from silently stripping a consumer's custom `aria-labelledby`.
- Deduplicated CT vs unit tests: static attribute contracts live only in
the vitest suite; Playwright CT keeps browser-dependent behavior
(typing/focus, paste, keyboard nav, accessible-name resolution, invalid
styling, axe, computed-style checks).
- `Separator` now merges function-form `className` like its siblings and
types props via the Base UI `Separator.Props` idiom.
- `OTPField` exports moved to their alphabetical slot in `src/index.ts`.
- `children ?? autoSlots` so `children={null}` renders the default
slots.

### Known follow-ups (deliberately out of scope)

- Shared `mergeClassName` helper for the function-form `className` merge
pattern (touches Field/Combobox too).
- Input-chrome SCSS mixin for the border/focus/critical block
(pre-existing package-wide duplication).
- Dev warning on grouped child-count vs `length` mismatch.
- Bare-usage first-slot labelling (`WithField` is the documented
composition).

## Test Plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 495 passed (17
OTPField unit tests)
- [x] `yarn workspace @lightsparkdev/origin types`
- [x] `yarn workspace @lightsparkdev/origin lint` — eslint + stylelint
clean (2 pre-existing warnings in DatePicker/Sidebar untouched)
- [x] OTPField Playwright CT suite — 14 passed (typing, paste, keyboard
nav, a11y, invalid styling, axe, gap/size checks)
- [x] Full origin CT suite run earlier in the branch — 953 passed; 7
failures reproduced identically on clean origin/main (pre-existing)
- [x] Visual QA in Storybook against the Figma spec

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 5ddc7fdf97bfde48150f69e1f49094c21f0664ab
## Reason

Turbo's local-only cache makes the JS CI jobs repeat the same work
across runners. We want a remote Turbo cache for CI, but backed by
GitHub Actions cache rather than Vercel-hosted cache infrastructure.

## Overview

- Upgrade `turbo` to `^2.9.9`.
- Add a shared `setup-turbo-remote-cache` action using
`rharkor/caching-for-turbo` with the GitHub cache provider.
- Invoke the shared cache setup from the common Yarn install action and
the dev-cli Spark JS build path.
- Expand workflow path filters so changes to the shared cache action
exercise the affected CI flows.

## Test Plan

- `yarn install --immutable --mode=skip-build`
- `ruby -e 'require "yaml"; ARGV.each { |p| YAML.load_file(p); puts "OK
#{p}" }' .github/actions/setup-turbo-remote-cache/action.yml
.github/actions/yarn-nm-install/action.yml
.github/actions/setup-dev-cli/action.yml .github/workflows/ci.yaml
.github/workflows/deploy-origin-storybook.yaml
.github/workflows/js-lightspark-sdk-hermetic.yaml
.github/workflows/pr-ui-preview-deploy.yaml
.github/workflows/ssp-spark-hermetic-dev-cli.yaml`
- `git diff --check`
- `yarn turbo run build --dry=json`
- Pre-commit hook: `yarn install`, `yarn format`

GitOrigin-RevId: c550e188d273df57f8cb1d0dcb4f6995537dcb34
## Reason

Origin's `Loader` only ships the 3-dot pulse, which is sized and weighted for inline/button contexts. The uma-nage transfer status badge (stacked follow-up PR) needs a compact circular spinner that reads as "in progress" at small sizes. The Figma Origin Loader component set now has a `Style=Ring` option; this implements it as a variant rather than a new component.

Figma: [Loader component set (`Style=Ring`)](https://www.figma.com/design/3JvbUyTqbbPL8cCpwSX0j4/Origin-design-system?node-id=7217-14)

## Overview

- Adds `variant?: "dots" | "ring"` to `Loader` (default `"dots"`, so Button loading and all existing consumers are unchanged). `LoaderVariant` is exported alongside `LoaderProps`.
- The ring matches the Figma `Style=Ring` spec: circular track stroked with `--border-secondary`, rotating quarter-arc indicator on `--stroke-primary`, 2px stroke (`--stroke-lg`), round caps, 24x24 reference geometry.
- Optional `size` prop (ring only) scales the whole svg — strokes scale proportionally like `CentralIcon`, so a 12px ring renders a ~1px stroke. Rotation is `0.5s linear infinite` and respects `prefers-reduced-motion`.
- Accessibility contract preserved: `label` → `role="status"` / `aria-label` + visually hidden text; the svg is `aria-hidden`.
- Tests split per the OTPField convention: static attribute contracts in `Loader.unit.test.tsx` (vitest), browser-dependent behavior in a new Playwright CT suite (`Loader.test.tsx` + `Loader.test-stories.tsx`) covering rendered geometry, computed token strokes, rotation animation, reduced motion, and axe scans for both variants.
- `Ring` / `RingSmall` Storybook stories plus a patch changeset.

Intended consumer: the uma-nage transfer status badge (stacked follow-up PR — not touched here).

## Test Plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 515 passed (9 Loader unit tests)
- [x] Loader Playwright CT suite — 8 passed (geometry at 24px/12px, token strokes, rotation, reduced motion, axe on both variants, accessible name)
- [x] `yarn workspace @lightsparkdev/origin types` — clean
- [x] `yarn workspace @lightsparkdev/origin lint` — eslint + stylelint, no new findings (2 pre-existing warnings in DatePicker/Sidebar)
- [x] `yarn workspace @lightsparkdev/origin format` — clean
- [x] Visual check of `Ring` / `RingSmall` stories in Storybook — QA'd locally on port 6006

Made with [Cursor](https://cursor.com)

GitOrigin-RevId: d5b072f466611641686b34dc1a1e84c9af3a8323
## Reason

Locked-country phone contexts (e.g. KES mobile money flows) need a locked, non-interactive country cap on the phone input — the country is fixed by the product, so a country selector is wrong UX. This adds `PhoneInput.LockedCountry`, a static "leading cap" part (flag + dial code, no chevron, no Select machinery) matching the Figma `_Trigger` `Locked` variant spec.

Origin stays data-agnostic: consumers supply the flag and dial code. Product-side country-selection logic (which country to lock and when) lands separately in Nage.

> Note: this part was originally named `PinnedCountry`; it was renamed to `LockedCountry` to align terminology with the Figma spec's `Locked` axis and Nage's `countrySelection` `mode: "locked"`. The branch name keeps the old `pinned` wording.

## Overview

- `PhoneInput.LockedCountry`: static, non-focusable `div` cap rendered in place of the country Select trigger — no combobox semantics, skipped in tab order.
- Locked-specific padding/border-separator styles matching the Figma spec; root keeps the 36px height.
- Story, Playwright CT tests (axe, focus order, layout/computed styles), Vitest unit tests (DOM shape/semantics), and a patch changeset.

## Storybook preview

- PhoneInput / Locked: https://dev.dev.sparkinfra.net/app/origin-storybook-pr-30094/?path=/story/components-phoneinput--locked

## Test Plan

- `yarn test:unit` in `js/packages/origin` — 519/519 passing (includes new LockedCountry unit tests).
- `yarn test:ct PhoneInput` — 21/21 passing (axe on locked variant, no-combobox/Tab focus-order check, locked padding + border separator, 36px height).
- `yarn types` and `yarn lint` clean.

Made with [Cursor](https://cursor.com)

GitOrigin-RevId: bc565498475fdf16c6958bc3c94654861ff74f64
## Why

The `copy-public-js` job spends most of its time initializing Copybara
even when a push contains no files that can sync to
`lightsparkdev/js-sdk`. Recent runs show this is usually a no-op path,
so the workflow should cheaply skip those runs while keeping the skip
criteria tied to the Copybara config.

## Changes

- Download the pinned official Copybara release from GitHub instead of
the Lightspark CDN, with SHA256 verification.
- Use the latest preinstalled Java available on the GitHub runner,
preferring Java 25 with Java 21 fallback.
- Add a preflight helper that parses `origin_files` from
`js/copy.bara.sky` and checks the pushed git range for eligible file
changes.
- Run Copybara with shallow origin/destination fetches first, then retry
the full fetch path if the shallow run cannot complete.

## Validation

- `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/js.yaml");
puts "yaml ok"'`
- Embedded workflow `run:` scripts passed `bash -n`.
- `python3 -m py_compile scripts/gha/copybara_eligible_changes.py`
- `git diff --check -- .github/workflows/js.yaml js/copy.bara.sky
scripts/gha/copybara_eligible_changes.py`
- `java -jar /tmp/copybara-latest.jar validate --validate-starlark
STRICT js/copy.bara.sky`
- Negative helper check on the current private-only range returned no
eligible files.
- Positive helper check on recent public JS commit `d5b072f...` returned
the public `js/packages/origin/**` files and ignored the excluded
changeset.
- Pre-commit hook passed, including JS install and format.

Known existing caveat: `actionlint` still reports pre-existing issues in
`.github/workflows/js.yaml` unrelated to this Copybara job, including
merge-group path filtering, custom runner labels, and older shellcheck
findings outside the changed block.

GitOrigin-RevId: ff830574cefab41d26f8e951156f45e4bd8a1999
## Summary
- generate `tn` typename constants from each consuming app schema
instead of sharing runtime typenames from `@lightsparkdev/gql`
- move the node/type guard helpers into site and ops so they reference
app-local generated typenames
- keep app codegen cache-safe by declaring `@lightsparkdev/gql` as the
generator package dependency and hashing the shared
`generate-typenames.mjs` script in each app `gql-codegen` task
- move the node password modal into site and remove the remaining
private-ui dependency on site-only gql/store code
- remove unused private gql outputs/fragments/schema JSONs and trim
`@lightsparkdev/gql` to introspection/tooling scripts
- keep generated ops Ent query literals stable by printing generated
GraphQL documents before writing them

## Validation
- `yarn install --immutable`
- `yarn turbo run gql-codegen --filter=@lightsparkdev/site
--filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge
--filter=@lightsparkdev/private-ui`
- `yarn turbo run gql-codegen --filter=@lightsparkdev/site
--filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge`
- `yarn run --top-level turbo run build:deps
--filter=@lightsparkdev/site --filter=@lightsparkdev/ops
--filter=@lightsparkdev/uma-bridge`
- `yarn turbo run types --filter=@lightsparkdev/site
--filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge
--filter=@lightsparkdev/private-ui`
- `yarn turbo run lint --filter=@lightsparkdev/site
--filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge
--filter=@lightsparkdev/private-ui`
- `node packages/private/gql/scripts/generate-typenames.mjs --schema
--output /tmp/typenames.tsx` rejects the missing `--schema` value as
expected
- `yarn workspace @lightsparkdev/site test
src/uma-nage/billing/GridBilling.test.tsx
src/uma-nage/billing/GridBillingInvoiceDetails.test.tsx
src/uma-nage/components/CommandCenter.test.tsx
src/uma-nage/customers/customerDetailsPresentation.test.ts`
- `yarn playwright test playwright/tests/10-payments.spec.ts --list`
- `git diff --check && git diff --cached --check`

## Notes
- Full local Playwright was not run because the local suite expects the
full backend test environment; the payment spec list check passed, and
Corey manually verified the node password modal on mainnet after the
site-local move.

GitOrigin-RevId: e90fbadc215286b09f579b5de0c01cf57ecc23ee
## Summary

Adds a `label` slot to Origin's `Item` component, rendered inline after
the title in the title row (per the Origin Figma spec, node 7269-385 —
intended for badges like "Coming soon" or "Beta").

- New `label?: React.ReactNode` prop on `Item`, rendered in the title
row after the title text
- Styles in `Item.module.scss` for inline alignment and spacing
- Storybook story, Playwright CT test stories + tests, and a patch
changeset included

## Stacking

PR #30043 stacks on this — its badge commit consumes the new `label`
prop. That branch will be rebased to drop the duplicated origin commit
once this merges.

## Verification

- `tsc --noEmit` clean in `js/packages/origin`
- Item Playwright CT suite: 15/15 passing

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 32444a221e2d4dea5a23924b6fc799e8c520b82d
## Reason

Real public-JS Copybara runs still spend about 100 seconds initializing
the webdev origin, even after non-public changes were taught to skip the
job. The pinned Copybara release supports partial origin fetches, which
directly avoid hydrating unrelated monorepo blobs.

## Overview

Enable `partial_fetch` only for the webdev-to-js-sdk origin. The reverse
workflow still fetches the whole repository because its `origin_files`
glob is `**`, which Copybara does not support with partial fetches.

## Test Plan

- Pinned Copybara v20260706 strict config validation passes.
- The Copybara eligibility helper still recognizes `js/copy.bara.sky`.
- Fresh-output-root no-op dry run: 55.44s baseline, 15.67s with partial
fetch (~72% faster), with both returning Copybara's expected no-change
status.
- Remote-branch eligible-change dry run completed successfully in
24.74s, transformed the config commit, created a local destination
commit, and skipped the remote push under `--dry-run`.

GitOrigin-RevId: 398a2855b3856e7c50944e82e6529d841c2e961c
Pagination was passing `useRender` a props array force-cast to
`Record<string, unknown>`, bypassing Base UI's public contract. This
applies the same fix as Breadcrumb in #30178: each call now passes a
single merged object (className joined via clsx, defaults spread before
element props), with no casts and no behavior change.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: c504ae51ccfa777924b930a3c89bc6a8cf38801a
## Summary

- Adds a `render` prop to `Breadcrumb.Link`, implemented with Base UI's
`useRender` (`@base-ui/react/use-render`), so consumers can pass
router-aware link components — e.g. `render={<LinkBase to={...} />}` —
instead of hand-rolling `useHref` + click interception.
- Default tag stays `a`; props merge per Base UI semantics (classNames
join, event handlers compose, refs merge). `href` remains required when
no `render` element is provided.
- Adds a Storybook story, a Playwright CT case, and Vitest unit tests
covering render replacement, className merging, event-handler
composition, and ref merging, plus a patch changeset.

The NAGE settings branch (`ajay/nage-settings-origin`) will consume this
for the settings breadcrumb.

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: d4d57aa0b0bfb3d93aa307f4d68197f0503b97a0
## Summary
- invoke custom error `toJSON` methods with their owning object as the
receiver
- add regression coverage for serializers that read instance state
- add a patch changeset for `@lightsparkdev/core`

## Why
Marker SDK errors expose a receiver-dependent `toJSON` method. Calling
it unbound masked a handled Marker configuration error with an uncaught
TypeError, causing Nage Playwright setup to fail and blocking 68
dependent tests.

## Validation
- `yarn workspace @lightsparkdev/core test` — 70 passed
- core format, lint, and build passed
- filtered site production build passed
- full local UI hermetic rerun: Nage setup passed and previously blocked
Nage suites ran; 110 passed, 3 failed, 3 skipped, 10 serial dependents
did not run
- remaining UI failures are unrelated: Stripe redirect landed on
dashboard, RSK API-token delete control timeout, and Nage command-center
shortcut timeout
- `bolt-codex-review` — no P0/P1/P2 findings

## Local hook note
The repository-wide pre-commit format hook was bypassed after package
checks passed because unrelated ops GraphQL generation fails at
`ent-queries.tsx:35456` with a pre-existing syntax error.

Requested by @coreymartin

Slack thread:
https://lightsparkgroup.slack.com/archives/D0BF4GR03PA/p1783969354352799

---
🤖
[magnetic-summit](https://zeus.dev.dev.sparkinfra.net/#/arc?id=magnetic-summit)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=magnetic-summit)
| [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

Original PR: lightsparkdev/webdev#30279

GitOrigin-RevId: c10c39f5afbc9c24319982fa7c9c25f55ef2f250
## Reason

PR previews use a unique `VITE_BASENAME` for each pull request.
Declaring frontend environment variables on the root `build` task,
together with Turbo's Vite framework inference, made that PR-specific
value part of shared package and GraphQL codegen hashes. This prevented
otherwise identical work from being restored from the GitHub-backed
Turbo cache across previews.

## Overview

- Scope output-affecting frontend environment variables to the app
builds that consume them.
- Disable framework inference for the preview Turbo command now that its
environment inputs are explicit.
- Keep the preview path in each final app build hash while allowing
shared dependencies and codegen tasks to retain stable hashes.

## Test Plan

- `yarn install --immutable`
- Prettier check for all changed Turbo JSON and workflow files
- `git diff --check`
- Focused `site`, `ops`, and `uma-bridge` Turbo build: 14/14 tasks
succeeded
- Identical preview-path rerun: 14/14 tasks cached in 171ms
- Different preview path: 11/14 tasks cached; only the three final app
builds reran
- Pre-commit JS formatting hook: 28/28 tasks succeeded

GitOrigin-RevId: 20466a07062183e1b8f0dbb74afd5ffcc4bf1b21
## Reason

We need a way to exercise the real Grid REST API against a
`StrigaGridSwitch` platform locally — driving quotes, deposits,
withdrawals, and onboarding — to validate the switch end-to-end during
development. There was no local harness for this.

## Overview

Adds a local, **sandbox-only** e2e harness for the Striga Grid switch
under `sparkcore/scripts/striga_harness/`, plus its web UI:

- **`seed.py`** — seeds a local SQLite DB into a fully-configured Striga
Grid platform: grid switch, `EntGridStrigaPlatformConfig` (Striga
creds), Grid API token, a customer linked to a funded sandbox user, and
EUR/BTC/USDC internal accounts. Writes `.grid-creds.json`.
- **`proxy.py`** — stdlib HTTP proxy on `:8787`. Serves the built UI,
injects HTTP Basic auth from `.grid-creds.json`, forwards `/grid/*` to
the local server (CORS bypass), and serves `/harness/creds`.
- **`ngrok.py`** — prints the Striga webhook URL for the platform.
- **UI** (`js/apps/examples/striga-grid-harness/`) — a Vite + React app
built on the Origin design system
(Card/Field/Input/Textarea/Button/Select/Switch/Table/Badge/Alert).
List/create customers, poll internal-account balances, create + execute
quotes, sandbox-fund deposits, transfer-out withdrawals, onboarding (KYC
link, email/phone verification), and a full request/response log. Builds
to `dist/`, which `proxy.py` serves.

Dev-only tooling; not wired into CI. `harness.config.json` and
`.grid-creds.json` are gitignored (sandbox secrets / per-run creds).

## Test Plan

Seeded a local platform against the Striga sandbox and drove flows
through the UI:

- Listed + selected the seeded customer; internal-account balances
render.
- **Sandbox fund** → EUR balance moved 10000 → 15000.
- **Live EUR→BTC quote** (real sandbox rate ~54,612 EUR/BTC) →
**execute** → `COMPLETED`; EUR 15000→10000, BTC 0→90,226 sats.
- **Create customer** creates the Striga user (phone + address
accepted); the immediately-following wallet fetch surfaces the known
pre-KYC wallet-provisioning gap tracked in AT-5682 (out of scope here).

Lefthook pre-commit (ruff / ty / pylint + `yarn format`) passes; `yarn
workspace @lightsparkdev/striga-grid-harness build` (tsc + vite) passes.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: d492f2b53afd2ed9ed5f60aba62a0b47d8a50384
## Reason

Bolt exposes a frontend at a URL such as:

`https://zeus.dev.dev.sparkinfra.net/agent/<job-id>:<port>/`

The app is therefore mounted under a public path prefix, but Zeus
removes that prefix before forwarding the request into the agent pod and
records it in `X-Forwarded-Prefix`. A normal Vite development server
assumes that the browser-facing URL and the URL received by Vite have
the same path and host. That mismatch caused several distinct failures:

1. Vite rejected the forwarded Zeus `Host` header with “Blocked
request.”
2. Vite was configured with the public base path, but received stripped
SPA, asset, `/@fs`, and HMR paths from Zeus.
3. Browser code generated root-relative backend URLs such as
`/graphql/frontend`, which went to the Zeus dashboard instead of
`/agent/<job>:<port>/graphql/frontend` and returned dashboard HTML.
4. Backend paths that did reach Vite had to remain unprefixed internally
so Vite's `server.proxy` rules could match them and forward them to
Tilt.

This PR makes the shared Vite configuration understand that complete
browser → Zeus → Vite → backend boundary. The behavior is enabled only
for Bolt sessions with a valid `BOLT_ZEUS_BASE_URL` and a non-root Vite
base.

This also revives and completes the intent of #27948, which was closed
by stale automation.

## Request flow

For a frontend base of `/agent/test-job:3000`:

### Frontend document and asset request

1. The browser requests `/agent/test-job:3000/@fs/...` or an SPA route.
2. Zeus selects the job and port, removes `/agent/test-job:3000`, and
forwards `/@fs/...` with `X-Forwarded-Prefix: /agent/test-job:3000`.
3. The Vite plugin validates that the header exactly equals its
configured base and restores the prefix before Vite's base-path
middleware runs.
4. Vite serves the correct module, static asset, or SPA fallback.

### Backend request

1. The injected browser bootstrap changes a same-host request such as
`/graphql/frontend` to `/agent/test-job:3000/graphql/frontend` before it
leaves the page.
2. Zeus routes it to the agent and forwards the stripped
`/graphql/frontend` path with `X-Forwarded-Prefix`.
3. The Vite middleware recognizes that `/graphql/frontend` belongs to
`server.proxy` and deliberately leaves it stripped.
4. Vite's proxy matches the path and forwards it to `VITE_PROXY_TARGET`
(for the hermetic Site environment, `http://app.minikube.local`).

The same client-side behavior covers Fetch, `Request` objects,
XMLHttpRequest/axios, and WebSockets. It preserves external URLs and
URLs that already contain the Bolt prefix.

### HMR WebSocket

Vite handles WebSocket upgrades before normal Connect middleware. The
plugin therefore installs an early `upgrade` listener that restores the
validated prefix only for the `vite-hmr` and `vite-ping` subprotocols.
Application GraphQL WebSockets remain bare after Zeus stripping so
Vite's WebSocket proxy can match them.

## Changes

### Exact Zeus host allowlisting

- Parse the hostname from `BOLT_ZEUS_BASE_URL`.
- Add only that hostname to Vite's `server.allowedHosts`.
- Retain Vite's normal DNS-rebinding protection for every other host and
for all non-Bolt development.
- Ignore malformed `BOLT_ZEUS_BASE_URL` values instead of weakening host
protection.

### Browser-side backend routing

- Inject a `head-prepend` bootstrap before application modules run.
- Prefix same-host HTTP(S) and WS(S) destinations for:
  - `window.fetch`, including `Request` inputs
- `XMLHttpRequest.prototype.open` and therefore axios's browser
transport
  - `window.WebSocket`
- Do not modify external destinations or already-prefixed paths.

This prevents Site GraphQL, GraphQL subscriptions, Grid REST calls, UI
logging, Ops GraphiQL, UMA Bridge, and future root-relative proxy routes
from escaping to the Zeus dashboard origin.

### Server-side prefix restoration

- Validate `X-Forwarded-Prefix` against the configured Vite base.
- Restore it for SPA routes, static assets, `/@fs` modules, and other
Vite-served resources.
- Avoid double-prefixing paths.
- Read Vite's final merged `server.proxy` table so both shared and
app-specific backend routes remain bare and continue matching their
proxy rules.

### HMR upgrade handling

- Restore the prefix for stripped `vite-hmr` and `vite-ping` upgrade
requests before Vite's own upgrade listener runs.
- Leave other WebSocket protocols untouched so application WebSocket
proxying still works.

### Turbo and tests

- Pass `BOLT_ZEUS_BASE_URL` through the persistent Turbo `start` task so
it reaches Vite when apps are launched through Turbo.
- Add a package test command using Node's built-in test runner.
- Add six live-server/boundary tests covering:
  - unchanged host protection outside Bolt
  - accepted Zeus host and rejected untrusted host
  - early client bootstrap injection
- Fetch, Request, XHR, external URL, already-prefixed URL, and WebSocket
rewriting
  - HMR WebSocket upgrade returning `101 Switching Protocols`
- full browser prefix → Zeus strip → Vite proxy → backend path handling
  - consumer-added proxy routes from final merged Vite configuration

## Scope and limitations

- This supports Bolt's current path-based Zeus proxy; it does not
introduce dedicated preview hostnames.
- The app still needs to start Vite with its public Bolt path as the
base, for example through `VITE_BASENAME=/agent/<job>:<port>/`.
- `VITE_PROXY_TARGET` remains responsible for selecting the backend
environment. Hermetic Site development should use
`http://app.minikube.local` to reach Tilt without local TLS-certificate
issues.
- When `BOLT_ZEUS_BASE_URL` is absent or the base is `/`, none of the
Bolt plugin, host allowlist, or browser networking changes are
activated.

## Test plan

- `yarn workspace @lightsparkdev/vite test` — 6/6 pass
- `yarn workspace @lightsparkdev/site turbo:build` — pass
- Turbo dry-run confirms `BOLT_ZEUS_BASE_URL` reaches the Site start
task
- Real Site through the Zeus-style boundary:
  - exact Zeus host plus validated `X-Forwarded-Prefix`: `200`
  - untrusted host: `403`
  - SPA HTML/bootstrap: `200 text/html`
  - `/@fs` SCSS module: `200 text/javascript`
- named GraphQL request through Tilt: backend `application/json`, not
Zeus HTML
  - stripped HMR WebSocket request: `101 Switching Protocols`
- Repository pre-commit install and formatting checks

---
🤖
[resolute-reactor-2](https://zeus.dev.dev.sparkinfra.net/#/arc?id=resolute-reactor)[(#2)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=resolute-reactor-2)
| [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

---------

Co-authored-by: Aaron Kanter <aaron@lightspark.com>
GitOrigin-RevId: ecc2672d17d55f53a09c04c0456a1bf09db006ac
## Reason

Make backend target setup discoverable and reusable for engineers and
agents working across private JS frontends, while making the canonical
hermetic UI workflow recoverable and diagnosable.

## Overview

- Adds shared `js/apps/private` agent guidance with `AGENTS.md ->
CLAUDE.md`.
- Keeps `docs/local-backends.md` as the stable router and
common-instructions entry point, with mode and OS details split under
`docs/local-backends/`.
- Documents the three supported private frontend backend modes:
- `start:dev` against hosted dev through Cognito-authenticated local
Vite proxying.
- `mise sparkcore-dev` for a local Sparkcore process using hosted dev
data.
  - a seeded hermetic backend through Tilt and Kind.
- Moves reusable hermetic setup into
`js/apps/private/scripts/hermetic-backend.sh`; Site retains its
Playwright-specific seeding, gatekeepers, static deploy, and test
execution.
- Keeps app READMEs and Site's Playwright README as thin links to the
authoritative shared docs.
- Enables the existing hosted-dev proxy flow for Ops and UMA Bridge and
documents the app commands.
- Makes routing-node initialization fail fast and recover from
mismatches between LND wallet state and DB secrets:
  - stale DB secrets for a missing wallet are cleared;
- an initialized wallet with missing/incomplete secrets resets only the
verified routing-node Helm release and PVC;
- node status and secrets are committed transactionally with SQL error
handling.
- Captures the full hermetic run log and structured phase timings under
`test-results/hermetic-run/`, including setup failures and the first
terminal Tilt error.
- Makes `--clean` preserve the managed hosts block while rebuilding
Kind, avoiding an unnecessary sudo prompt; standalone teardown keeps its
existing host cleanup behavior.
- Resolves runner-owned manifest paths absolutely so the runner is safe
when invoked outside the Site directory.
- Disables Marker, GTM, and Segment when Site is running under
Playwright automation.
- Fixes the Stripe billing flow by forcing persisted app state out of
test mode before submission and asserting the successful billing-plan
mutation.
- Stabilizes Nage command-center coverage and production behavior by
giving asynchronous sidebar sections stable IDs, so late gatekeeper
results cannot remount and close an active command center.
- Keeps unfiltered RSK and Nage projects scheduled in parallel while
leaving stateful tests single-pass by default.

## Test Plan

Latest local verification on head `a00cbd5296`:

- `./playwright/run-tests.sh --clean` -> `126 passed` in 15.2m,
including Stripe billing and all command-center cases on a newly created
Kind cluster.
- `./playwright/run-tests.sh
--test-filter='28-nage-command-center.spec.ts'` -> `10 passed` in 2.5m.
- Routing recovery fault injection: removed the active routing node's DB
secret row, reran initialization, and verified only the expected Helm
release/PVC was replaced; the node returned `READY` with one complete
secret row and a funded wallet.
- `bash js/apps/private/scripts/tilt-resource.test.sh`
- `bash js/apps/private/scripts/hermetic-backend.test.sh`
- Shellcheck on all changed shell scripts and helper tests.
- `yarn workspace @lightsparkdev/site test
src/uma-nage/components/SideNav.test.tsx`
- `yarn workspace @lightsparkdev/site types`
- `yarn workspace @lightsparkdev/site format`
- `yarn workspace @lightsparkdev/site lint`
- `yarn workspace @lightsparkdev/private-ui format`
- `yarn workspace @lightsparkdev/private-ui lint`
- `git diff --check`
- Pre-commit Python, JS install, formatting, lint, and shellcheck hooks
passed.
GitOrigin-RevId: 7765e33b48232259e9634ae83c176a77a5203e42
The shellcheck filter's awk pattern only matched shebangs with the
interpreter glued to `#!`, but POSIX allows whitespace there - so the
six tracked scripts written as `#! /bin/bash` (both `js/` test/start
scripts and `scripts/gha/find-image-tags.sh`) were never linted. Noticed
while adding the release scripts in #30380. The filter now allows
optional whitespace after `#!`; zsh stays excluded.

Bringing the six files under lint surfaced real findings in two of them.
`js/start.sh` built command strings and `eval`'d them; it now builds
arrays, which hand turbo the same argv (including the filter globs,
unexpanded) without the eval. `find-image-tags.sh` gets `SC2016`
directives - the backticks in its `--query` strings are JMESPath
literals, and switching to double quotes would turn them into command
substitution.

Testing: `./scripts/shellcheck.sh` checks 78 files (up from 72) and
passes; checked the new pattern against shebang variants (`#!/bin/bash`,
`#! /bin/bash`, `#!/usr/bin/env bash`, zsh forms still excluded);
confirmed the array form of `start.sh` passes
`--filter='./apps/private/*'` through literally even when the glob
matches files, same as the eval did.
GitOrigin-RevId: d968e3de6314e70a003d9980a43d6e10dda81d43
## Reason

Webdev Turbo artifacts currently live only in GitHub Actions cache, so
Bolt and local clients cannot restore them. The Yarn download cache also
lets ARM64 publish an archive that leaves x64 redownloading optional
packages on every run.

## Overview

- Moves trusted Webdev CI publishing and same-repository PR restores to
the private S3-backed Turbo cache, with local-only fallback when OIDC or
S3 is unavailable.
- Restricts publishing to trusted main workflows; forks,
`pull_request_target`, merge queues, arbitrary-ref dev-cli workflows,
and Spark checkouts do not use the shared namespace.
- Adds indexed-Git workflow/action hashing and the exact Node runtime to
Turbo global inputs.
- Partitions the Yarn download cache by runner OS and architecture and
pins Node 20 callers to 20.19.6.
- Adds a SHA-512-pinned, read-only local portability canary; default
local restores remain gated on a successful macOS artifact canary.

Infrastructure dependency:
lightsparkdev/ops#3412

## Test Plan

- Parsed every changed workflow/action YAML file with `yq`.
- Passed ShellCheck and Bash syntax checks for both Turbo helper
scripts.
- Verified the access matrix for trusted main, same-repo PR, fork PR,
`pull_request_target`, merge-group, hotfix, and foreign-repository
contexts.
- Verified a clean indexed-input hash and distinct Turbo hashes under
Node 20 versus Node 24.
- Verified Spark/dev-cli paths cannot initialize the Webdev shared
cache.
- `git diff --check`.

GitOrigin-RevId: 33ecd14d37c2a15a267d80a1bcc12434db829011
## Summary

- Adds 8 destination currencies from Daya Tech Amendment No. 1 — **BGN,
CZK, HUF, NOK, PLN, RON, SEK, CHF** — to the `CurrencyUnit` enum,
mirroring the merged ARS addition (PR #26191).
- Unblocks the ops corridor-fees CSV importer (Fees Manager → Import
Corridor Fees CSV): it rejected these as unknown currencies, and the
import is all-or-nothing, so the full 14-row amendment CSV couldn't
load.
- All 8 are standard 2-decimal ISO fiats, so `is_fiat()` yields
`decimal_precision=2` via the fallback — no `CurrencyConfig`
registration needed. No importer change: `normalizeCurrency` validates
against the generated `CurrencyUnit` enum.

## Changes

Hand-edited source of truth:
- `sparkcore/sparklib/money/currency_unit.py` — 8 entries added to
`FiatCurrencyUnit` and the GraphQL `CurrencyUnit` enum
- `js/packages/core/src/utils/currency.ts` — 8 entries at every ARS
anchor (enum variants, conversion/format/type maps, `abbrCurrencyUnit`,
2-decimal list)

Regenerated (not hand-edited):
- `sparkcore/graphql_schemas/*.graphql` via `scripts/export-graphql.py`
- `js/**/generated/graphql-schema.ts`, introspection JSONs,
`ent-queries.tsx` via `yarn gql-codegen`

Test:
- `sparkcore/sparklib/money/__tests__/test_currency_unit.py` — asserts
each new currency is a valid `CurrencyUnit`, is fiat (not crypto), and
has `decimal_precision() == 2`

## Test plan

- `uv run pytest sparklib/money/__tests__/` — 50 passed (incl. 8 new)
- `uv run ruff check` + `ruff format --check` — clean
- `yarn turbo run types format --filter=@lightsparkdev/core` — clean
- Diff is 826 insertions, 0 deletions (purely additive, additive enum
values keep existing SDL stable)

## Private

[Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/turbulent-sentinel-2/plan.md)
(S3, internal only)

## Public

Add BGN, CZK, HUF, NOK, PLN, RON, SEK, and CHF to the supported currency
unit set.

Fixes ENG-10916

---
🤖
[turbulent-sentinel-2](https://zeus.dev.dev.sparkinfra.net/#/arc?id=turbulent-sentinel)[(#2)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=turbulent-sentinel-2)
| [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

Co-authored-by: Jacky <jacky@lightspark.com>
GitOrigin-RevId: a7e05772bb28db90d568372434dcba1186b2ac8b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.