Skip to content

feat(0185): an ugly but real portal app served from /api-tokens/ - #218

Open
adamkoot wants to merge 4 commits into
developfrom
feat/0185_portal-app-ugly-but-real
Open

feat(0185): an ugly but real portal app served from /api-tokens/#218
adamkoot wants to merge 4 commits into
developfrom
feat/0185_portal-app-ugly-but-real

Conversation

@adamkoot

Copy link
Copy Markdown
Collaborator

Summary

  • web/portal — an @nx/react app: React 19, Vite, TypeScript 5.9, Vitest 4, react-router-dom 7. One route, plain HTML elements, no MUI. Ugly is the requirement; styling is docs(lore-0111): add rate-driven discovery as option 5 #193.
  • base: '/api-tokens/' and basename on the router, in the first commit. Both, not either — base covers assets, basename covers routes, and they differ by a trailing slash on purpose. The built index.html references /api-tokens/assets/….
  • The page probes GET /api-tokens/api/config and renders "not yet available" with nothing to click while the flag is off. Four tests cover both gate branches, the failure path, and that the call is relative with no key header — the same-origin property feat(0184): portal hosting skeleton — private S3 behind CloudFront #209 bought, which an absolute base URL would silently undo.
  • Dev proxy ported from soroban-block-explorer, with the key injected server-side in the Node config and never in the bundle. One change from the reference: only /v1 gets a key, because the portal's routes are keyless by design.
  • The bucket now serves the real bundle. PORTAL_ASSET_DIR repointed off feat(0184): portal hosting skeleton — private S3 behind CloudFront #209's placeholder, and its single BucketDeployment split in two, which is what that PR's decision 10 said this slice should do.

Two criteria could not be met as written

Both are recorded in the task, not quietly dropped.

/api-tokens/api/health does not exist. The portal backend maps exactly one route, /config (portal/mod.rs), and #207's gate answers an empty 404 on every other path under the prefix. A /health probe would render a failure whether or not anyone implemented it, and implementing it would mean adding a route whose only job is to be gated. /config is exempt from the gate and answers 200 in both flag states, which is what makes it the honest probe — and it is the route #207 built for this bundle to read.

"Generate API types from /api-docs-json" has nothing to generate from. Every portal endpoint is deliberately absent from that document — #209's verify-openapi-routes.mjs fails CI if one appears in it, because the document describes the public data API to integrators and the portal describes itself to its own bundle. That holds for the later slices too, not just this one. Kept the mechanism (npm run portal:api-types, openapi-typescript) for when a page renders /v1 data; dropped the 973-line emitted file, which nothing imported and would only drift. The one type the app needs, PortalConfig, is hand-written against portal/mod.rs and says so.

"CI deploys on merge" is withdrawn, agreed with Adam rather than built. #209 established there is no infrastructure deploy workflow in this repo at all — ci.yml synthesizes only, and every deploy is make -C infra run by an operator. What the criterion protects against is a hand-run aws s3 sync that nobody follows with an invalidation, and BucketDeployment already closes that inside cdk deploy. A deploy pipeline is a much larger task and is not smuggled in here.

Three things found on the way

  • typecheck and build fought over dist/. tsconfig.app.json emitted declarations there and Vite's emptyOutDir deleted them, so typecheck passed the first time and failed the second — the worst version of this bug, because it reads as a flake. Declarations now go to out-tsc/app.
  • Every production Makefile target now builds the portal, not just the portal's own. cdk synthesizes the whole app whichever stack you name, so an unbuilt frontend fails synth-production and every per-stack deploy alike. It is also docs(0088): pass 1 health check — on rate, floor 27.58M, ETA 07-26/27 #141's footgun arriving for the frontend: the bundle is packaged off disk with no freshness check, so a stale one deploys quietly and reports success. Verified by deleting web/portal/dist and running make -C infra synth-production, which rebuilds it and succeeds.
  • web/** was missing from the CI paths filters. No pattern in ci.yml matched web/, so a frontend-only PR would have run no job at all and merged green having been checked by nothing — the same shape as the gap the rust filter closes for portal-hosting-stack.ts.

Not verified live

The bundle is verified at build, not on the distribution: production still serves #209's intermediate state, and reaching the committed shape takes the three deploys tracked in task 0205. PORTAL_ENABLED stays false, so what this ships is the closed page.

React 19 + Vite + TypeScript 5.9 + Vitest 4, generated by @nx/react
into web/portal. Nothing of the app itself yet — this is the toolchain
and a green build/lint/test/typecheck to build on.

Two notes on the stack the task pinned to soroban-block-explorer:

- react-router-dom came out of the generator at 6.30; bumped to 7 as
  the task specifies. 0195's per-prefix SPA fallback will be written
  against v7, and v6 emits future-flag warnings for exactly the APIs
  this app uses.
- vite resolved to 8, not the explorer's 7. Left alone: the task
  anticipates drift ("Nx here is 22.7.0 against the explorer's 22.6.1
  — close enough to share config shapes"), and the point of mirroring
  is config shape, not version lockstep. Fighting the generator down a
  major to match a sibling repo buys nothing.

@nx/react, @nx/vite and @nx/web are pinned to 22.7.0 rather than ^ —
the caret pulled 22.7.8, which peer-conflicts with @nx/eslint 22.7.0
on @nx/jest and fails npm install mid-generate.

web/* joins the npm workspaces; the app lives at web/portal rather
than web/ because 0184's routing convention has several frontends
sharing the distribution, and the next one should not have to move
this one first.
The base path, the dev proxy and the one page this slice ships.

base: '/api-tokens/' in vite.config.mts and basename on the router.
Two constants that differ by a trailing slash on purpose: vite wants
one (without it assets emit as /api-tokensassets/...), react-router
warns if given one. Dropped <base href="/"> from index.html, which
would have resolved asset URLs a second time on top of vite's base.

Dev proxy ported from soroban-block-explorer: loadEnv(mode, root, '')
so DEV_API_PROXY_TARGET and DEV_API_KEY are read in the Node config
and never reach import.meta.env. One deliberate change from the
reference — the key is injected only on /v1, not on every proxied
path. The portal's routes are keyless by design, so sending a key to
/api-tokens/api/* in dev would exercise a configuration production
never runs.

The page probes GET /api-tokens/api/config and renders "not yet
available" with nothing to click while enabled is false. Four tests
cover both gate branches, the failure path, and that the call is
relative with no key header — the same-origin property 0184 bought,
which an absolute base URL would silently undo.

**The criterion naming /api-tokens/api/health cannot be met as
written.** The portal backend maps exactly one route, /config
(portal/mod.rs), and 0183's gate answers an empty 404 on every other
path under the prefix — so a /health probe would render a failure
whether or not anyone implemented it. /config answers 200 in BOTH flag
states, which is precisely what makes it the honest liveness probe,
and it is the route 0183 built for this bundle to read. Proving the
same reachability, without adding a gated route that exists to be 404.

Also not met as written: "generate API types from /api-docs-json".
Every portal endpoint is deliberately absent from that document —
0184's verify-openapi-routes.mjs fails CI if one appears in it — so
there is nothing there to generate this app's own calls from, now or
in later slices. Kept the mechanism (`npm run portal:api-types`,
openapi-typescript) because the data-route types will be worth having
when a page renders them; dropped the 973-line emitted file, which
nothing imports and would only drift.

tsconfig.app.json emits declarations to out-tsc/app rather than dist:
vite owns dist and empties it on every build, which deleted the .d.ts
that tsconfig.spec.json references and broke typecheck after any
build.
Repoints PORTAL_ASSET_DIR from 0184's throwaway placeholder to
web/portal/dist and retires the placeholder. The app now says the
portal is unavailable on the authority of GET /api-tokens/api/config
rather than a hardcoded sentence nobody would remember to delete.

Splits 0184's single BucketDeployment in two, which is what its
decision 10 said this slice should do. Content-hashed assets get a
year and immutable; the unhashed entry document keeps max-age=0,
must-revalidate. The asset deployment does not prune, deliberately: a
viewer still holding the previous index.html requests the old chunk
names, and deleting them the moment a new build lands turns an open
tab into a blank page. Only the entry-document deployment invalidates
— new hashed assets are new URLs and were never in the cache.

Every production Makefile target now builds the portal, not just the
portal's own. cdk synthesizes the whole app whichever stack you name,
so an unbuilt frontend fails synth-production and every per-stack
deploy alike. It is also 0141's footgun arriving for the frontend: the
bundle is packaged off disk with no freshness check, so a stale one
deploys quietly and reports success. Verified by deleting
web/portal/dist and running make -C infra synth-production, which
rebuilds it and succeeds.

web/** joins the typescript paths filter. Without it no pattern in
that file matches web/, so a frontend-only PR would run no job at all
and merge green having been checked by nothing — the same shape as the
gap the rust filter closes for portal-hosting-stack.ts.

The task record carries the two criteria that could not be met as
written (the /health probe, the generated types), the withdrawn
CI-deploy criterion, and seven emerged decisions.
vite.config.mts claims `web/portal/.env.development` is gitignored.
It was not: .gitignore covered .env, .env.local and .env.*.local, and
none of those match it. Vite loads it by MODE, so it is a real config
file rather than a local override — which is exactly why the pattern
missed it, and exactly why it matters.

Left as it was, the first developer to follow the comment would have
committed the production distribution URL, and later DEV_API_KEY next
to it. Found by creating the file to run the app against the real
distribution and watching git offer to track it.
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