Skip to content

feat: add the demo UI, built into the binary - #7

Merged
tela merged 19 commits into
mainfrom
feat/ui-v1
Nov 4, 2026
Merged

tela merged 19 commits into
mainfrom
feat/ui-v1

Conversation

@tela

@tela tela commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6, which is stacked on #5 and #4. Merge in order: #4, #5, #6, then this. The base branch is feat/accept-and-fund, and GitHub retargets this once that branch is merged and deleted.

Motivation

The demo needs a UI a presenter can run in two clicks. This adds it, on wondertwin-web's toolchain and LocalStack's brand, and builds it into the Go binary so the demo is still one file that needs no network.

Changes

  • chore(web): set up the workspace like wondertwin-web: pnpm 10.33, Node 22, the same Biome rules, and the supply-chain policy copied unchanged. No exclusion is added to the trust policy.
  • feat(web): add a shadcn theme and variants to the brand package: the shadcn variable names set from the LocalStack tokens, and cva button and badge variants, as wondertwin-web does with its design package. Mint is the one action color. The style guide has no error color, so one is added and labelled as an addition.
  • feat(shared): include the hint in each app emulator's status: the UI shows what to do about an app emulator that is not ready, and the Go code already writes that sentence, so it now travels in the status JSON.
  • feat(acme): serve the UI as a single-page app: a client-side route such as /inspector loads index.html when opened directly, a missing file with an extension stays a 404, hashed assets are cached for good, and the page that names them never is.
  • feat(web): add the ACME Commerce Lenders UI: React 18, Vite, TanStack Router and Query, Tailwind 4 and Vitest. Click 1 applies, connects, reads and prices an offer. Click 2 accepts and funds it. Each step shows as it finishes, the offer lists the vendor call behind every number, and a failed step has a Retry that carries on from that step. A Reset button puts everything back.
  • ci: build the UI and check the binary serves it: a web job lints, type-checks, tests and builds the UI, builds the binary, and checks it serves the app, a client-side route, a hashed script and a real 404.
  • docs: document the UI and how to build it

Dependency decisions

  • @base-ui/react is not used. It pulled in reselect@5.1.1, which lost its provenance attestation and trips the trust policy. A plain <button> does the same job here, so the dependency is gone and the bundle is smaller.
  • tailwind-merge is 3.x, which is what Tailwind 4 pairs with. 2.6.1 also lost its attestation.
  • The lockfile was seeded from wondertwin-web's, which pins versions that pass the policy, and pnpm pruned it to what this app uses.

Tests

On the final tip:

$ make vet && make test && make check-kits && make build    # Go, all exit 0
$ pnpm --dir web install --frozen-lockfile                 # ok
$ pnpm --dir web lint                                      # Biome: no fixes needed
$ pnpm --dir web type-check                                # no errors
$ pnpm --dir web test                                      # 3 files, 15 tests passed
$ make web APP=acme-commerce-lenders                       # built: 300 KB JS (96 KB gzipped), 15 KB CSS

The UI tests cover the readiness display and its hints, an unseeded world, the first click running apply, connect, review and offer in order, the second click running accept and fund and showing the money's path, a declined offer that offers no Accept, a failed step that retries from that step without applying twice, Reset, and a failed Reset. The Go tests cover the single-page-app handler.

Run for real in Chrome, with the binary serving the built UI against Plaid, Xero and Stripe built from WonderTwin-AI/wondertwin origin/main (9a92fbdf):

apply button: enabled, rgb(116, 255, 230) (the brand mint), Exo loaded
click 1 -> the offer on screen in 113 ms
click 2 -> the funding on screen in 48 ms
the browser's POSTs: applications, connect, review, offer, accept, fund, reset (all 2xx, in order)
console problems: none
/inspector loaded directly: the app answers (its own "not found", until the inspector exists)

The CI job's embedding check was also run locally exactly as written, and passes.

Not tested: running with the network off, and a browser other than Chrome.

Related

DOC-581

tela added 19 commits November 3, 2026 22:02
A demo that makes its own vendor calls should record every one without opening a port that could clash with LocalStack's. The client hands each request straight to the proxy and gets the answer back, so the inspector sees the same events as with a listener.
The lender measures the last 30 days from the moment the seeded history ends, not from the wall clock, because seeding moves the app emulators' clocks ahead. Keeping that moment in the facts makes the windows the same on every run.
The lender connects to the merchant's three accounts and reads sales, payouts, the bank account and receivables. Every call goes through a wire proxy so it is recorded, and the windows are measured from the moment the seeded history ends. The Stripe key is exported so the seeder and the lender use the same one.
The advance is the last 30 days of card sales rounded down to $500, for a flat 8% fee. Every number the offer rests on names the vendor call it came from, so a presenter can show where it comes from. Plaid's balance is shown but not used, since the app emulator's fixture is too thin to price against.
An application moves through apply, connect, review and offer, and each step is refused out of order. A step that fails saves nothing and can be tried again. State lives in the store, so the command line and the server see the same applications.
The API tests need the same three-vendor fake the CLI tests use. Moving it to one package keeps a single copy, with its strictness about the Stripe key.
The UI and the inspector need the flow over HTTP: status, reset, apply, connect, review, offer and the recorded vendor calls. A reset waits for in-flight calls and they wait for it, so nothing reads half a world. The server mounts the API under /api/ beside the UI.
Until the UI exists, the API is how the flow is shown. List the routes and the one offer the seeded merchant gets, so a presenter can run it from a terminal and know what to expect.
The inspector shows recorded calls on a projector, and the full account number was readable in the request that registers a bank account and in Plaid's auth answer. Mask account_number fields, including bracketed form names, and Plaid's account field beside its routing. Routing numbers and Stripe account ids stay visible.
Accepting an approved offer registers the bank account the merchant verified with Plaid as the Stripe payout destination, transfers the advance to the merchant's Stripe account, and pays it out. Each step is saved as it finishes and the Stripe calls carry idempotency keys, so a funding that stopped halfway resumes without moving money twice. Only the last four digits of the account number are kept.
The UI's accept click calls both. A declined offer is a conflict, the same as a step taken out of order.
Say what funding does in Stripe, that it can be run again after a failure without paying twice, and what the Stripe app emulator does not show: a platform balance and the payout's destination. Xero settlement is not built yet.
The demo UIs use wondertwin-web's toolchain: pnpm 10.33 with its seven-day release age, trust and no-exotic-subdeps policy copied unchanged, Node 22, and the same Biome rules. No exclusion is added to the trust policy.
UIs written against shadcn's variable names and the cva variant pattern need the brand to arrive in that shape, as wondertwin-web does with its design package. Mint is the one action color. The style guide has no error color, so one is added and labelled as an addition.
The UI shows a presenter what to do about an app emulator that is not ready, and the Go code already writes that sentence. Sending it in the status JSON keeps one source for it, instead of the UI guessing at the same words.
A route inside the app, such as /inspector, has no file, so loading it directly must still give index.html. A missing file with an extension stays a 404 rather than the app. Hashed assets are cached for good and the page that names them never is. With no build yet, the placeholder is served.
Two clicks run the demo: apply, then accept and fund. The page shows each app emulator and who serves it, each step as it finishes, the offer with the vendor call behind every number, and how the advance reached the merchant. It is React, TanStack Router and Query and Tailwind 4 on the LocalStack brand package, with no network needed at run time. make web builds it into the Go binary.
Each demo's UI is built with pnpm and embedded in its Go binary, so CI lints, type-checks, tests and builds the UI, builds the binary, and checks that it serves the app, a client-side route, a hashed script and a real 404. The local gate runs the web checks where pnpm install has been run.
Say what the two clicks show, how to build the UI into the binary with make web, and the rules the web workspace follows, including preferring to remove a dependency over excepting it from the supply-chain policy.
@tela
tela changed the base branch from feat/accept-and-fund to main November 4, 2026 06:06
@tela
tela merged commit d5936a0 into main Nov 4, 2026
2 of 4 checks passed
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.

1 participant