feat(0185): an ugly but real portal app served from /api-tokens/ - #218
Open
adamkoot wants to merge 4 commits into
Open
feat(0185): an ugly but real portal app served from /api-tokens/#218adamkoot wants to merge 4 commits into
adamkoot wants to merge 4 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
web/portal— an@nx/reactapp: React 19, Vite, TypeScript 5.9, Vitest 4,react-router-dom7. 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/'andbasenameon the router, in the first commit. Both, not either —basecovers assets,basenamecovers routes, and they differ by a trailing slash on purpose. The builtindex.htmlreferences/api-tokens/assets/….GET /api-tokens/api/configand 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.soroban-block-explorer, with the key injected server-side in the Node config and never in the bundle. One change from the reference: only/v1gets a key, because the portal's routes are keyless by design.PORTAL_ASSET_DIRrepointed off feat(0184): portal hosting skeleton — private S3 behind CloudFront #209's placeholder, and its singleBucketDeploymentsplit 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/healthdoes not exist. The portal backend maps exactly one route,/config(portal/mod.rs), and #207's gate answers an empty404on every other path under the prefix. A/healthprobe 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./configis exempt from the gate and answers200in 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'sverify-openapi-routes.mjsfails 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/v1data; dropped the 973-line emitted file, which nothing imported and would only drift. The one type the app needs,PortalConfig, is hand-written againstportal/mod.rsand 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.ymlsynthesizes only, and every deploy ismake -C infrarun by an operator. What the criterion protects against is a hand-runaws s3 syncthat nobody follows with an invalidation, andBucketDeploymentalready closes that insidecdk deploy. A deploy pipeline is a much larger task and is not smuggled in here.Three things found on the way
typecheckandbuildfought overdist/.tsconfig.app.jsonemitted declarations there and Vite'semptyOutDirdeleted them, sotypecheckpassed the first time and failed the second — the worst version of this bug, because it reads as a flake. Declarations now go toout-tsc/app.cdksynthesizes the whole app whichever stack you name, so an unbuilt frontend failssynth-productionand 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 deletingweb/portal/distand runningmake -C infra synth-production, which rebuilds it and succeeds.web/**was missing from the CI paths filters. No pattern inci.ymlmatchedweb/, 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 therustfilter closes forportal-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_ENABLEDstaysfalse, so what this ships is the closed page.