feat(0193): the portal's UI pass — landing, dashboard, quick start - #249
Open
adamkoot wants to merge 9 commits into
Open
feat(0193): the portal's UI pass — landing, dashboard, quick start#249adamkoot wants to merge 9 commits into
adamkoot wants to merge 9 commits into
Conversation
MUI 7 and Emotion arrive — the half of the stack task 0185 shipped without — along with the landing page, the login screen and a dashboard that no longer looks like the debug harness it was deliberately left as. Design system Every colour, face and radius is transcribed from the Figma variables into theme/tokens.ts under Figma's own names, so a designer's "the tertiary text is too dim" maps to one line. theme.ts is the interpretation and keeps the two apart. Three self-hosted webfonts, ~130 KB: a link to fonts.googleapis or cdn.fontshare would be a third-party request from a page that renders a credential, and the CSP stays default-src 'self' only while nothing external is loaded. JetBrains Mono is one variable file, not two static weights. Routes / is the landing page and the junction the OAuth callback lands on — portal/auth/mod.rs redirects there in every outcome and says why: "when the portal grows a second page, the page it lands on decides where to go next". It forwards a signed-in visitor to /dashboard and a ?signin= landing to /login, carrying location.search so the one-shot refusals tasks 0186 and 0189 own are not swallowed. /dashboard sends a visitor with no session back to /api-tokens/, but only once /auth/me has answered — redirecting while it is in flight would bounce every arrival from the callback. Sign-in The Discord round-trip now opens in a second window and the page waits on it, as the mock shows. The control is still an <a> with a real href and preventDefault is called only after a window actually opened, so a blocked popup falls through to the navigation that has always worked. Three signals end the wait — the popup's message, a poll of /auth/me, and the window closing — because each covers a case the others cannot. Message origin is checked: without it any page could end the wait and make this card claim an outcome that never happened. Copy This slice re-decides none. The prerequisites, the eligibility refusals, the cancelled and failed banners, the usage figures and the lag line are tasks 0186 to 0189's and are rendered verbatim; the dashboard is styled through descendant rules rather than rewritten, so no wording moves and no testid is touched. What is new is the card chrome, the four "What you get" lines and the marketing sections, which belong to no other slice. The FAQ answers are written here and each restates a decision made elsewhere — two of them want a product read before release, and say so in the file. Task 0185's "Reached /api-tokens/api/config successfully — same-origin, no API key, no CORS" is gone. It was that slice's evidence that the bundle could reach its own backend, written when the page had nothing else to show for it; the page now has plenty, and a diagnostic sentence was the last thing on it that read as scaffolding. The test that guarded it now guards the control that acts on the answer instead. Deployment: DirectoryIndexFn rewrites the two client routes to the portal's index.html. Without it a hard refresh on either resolves against S3 and returns 403 AccessDenied, because the bucket grants s3:GetObject and not s3:ListBucket. An allow-list of literals, not a catch-all — a catch-all would answer 200-with-index.html for genuinely missing objects and turn a broken deploy into an app that renders the wrong thing. Task 0195 replaces it with the per-prefix SPA fallback; until then a route added to landing/links.ts must be added there. Known gaps, all flagged in the files: the two logos are rasters recovered from the Figma export because the seat had no MCP calls left for download_assets and should be replaced with SVGs; the Endpoints paths are the design's, not this repo's; and the login card's legal line names two documents that do not exist, so it is plain text rather than links to a 404.
The Figma seat became a Dev seat, so the dashboard frames (852:1499) could
finally be read rather than guessed at. /dashboard now has its own navbar —
SorobanScan, where you are, the two places you go next, who you are signed in
as, the way out — a page heading, and three cards: API Key, Monthly Usage and
Rate Limit.
The logos are real SVGs from download_assets, replacing the rasters an earlier
pass recovered from a screenshot when there were no tool calls left. The header
lockup is two nodes in the design and stays two here.
What the design shows and this does NOT, each flagged in the file that would
have carried it:
- The daily-requests bar chart. GET /usage returns used, remaining, limit,
the period bounds and as_of — no daily series exists, and drawing those
bars would mean inventing traffic on somebody's key.
- Regenerate, and the "rotation is limited to once per calendar month" note.
Both are task 0191's, which is not on this branch; the button would lead
nowhere and the sentence should arrive from the slice that decided it.
- "Issued" and "Last rotated". GET /key carries an id, a name and the value
and no timestamps at all. A dashboard that makes up the date a credential
was created is worse than one that leaves the field out. "Key name", which
the backend does return, takes the space.
- A link behind "Contact us" — there is no commercial-plans destination.
Three copy decisions worth knowing:
- The card titles are the design's ("API Key", "Monthly Usage", "Rate
Limit"), replacing 0187's and 0188's "Your API key" and "Usage this
period". They are panel labels; those slices' STATEMENTS — the reset rule,
the AWS lag line, the prerequisites — are rendered verbatim.
- "Used: / Remaining: / Monthly limit:" is gone and the figures stay, in the
design's arrangement. 0188 wrote those labels when the panel was three
unstyled paragraphs, and its own brief asked for the limits "as numbers,
not prose". Every data-testid and every raw value its tests read is
unchanged — the grouped form beside them is aria-hidden.
- "Rate limit: N request(s) per second." survives as an abbreviation
expansion. The design shows "1 req/s", which is right for the eye and
wrong for a screen reader, so 0188's sentence is in the DOM and read only
by assistive technology.
The Discord ID stays too, and now renders whatever the key state is: it is task
0186's acceptance criterion and the account key (ADR 0010), it belongs to the
session rather than to the key, and it has to be on screen on the day issuance
failed as much as on any other.
Fixes a latent layout bug while here: `visuallyHidden` used unitless `width: 1`
and `height: 1`, which MUI's sx treats as 100%, not one pixel. `clip` still hid
the element, but it pushed the document 900px past the viewport and gave the
dashboard a horizontal scrollbar. Every such value is now an explicit unit.
Task 0193's screens were read straight out of the Figma file rather than from screenshots, and this is the one line of configuration that makes that possible for the next person. Without it every session starts by wiring the server up by hand, or by asking somebody to export PNGs — which is what the first half of that task actually did. Committed rather than left local because it holds no credentials: the URL is Figma's public MCP endpoint and authentication happens per-user through an OAuth flow the client runs, so the file is safe to share and useless to anyone who has not signed in. Two things it implies, both of them fine but worth stating. Every session in this repo will now offer the Figma tools, and reading a design counts against the seat's tool-call budget — a View seat on the Professional plan gets six calls a MONTH, a Dev seat two hundred a day. Anyone hitting the low ceiling should ask for the seat rather than burn the calls discovering the limit.
One conflict, in the dashboard: 0191's revoke landed on develop while this branch rebuilt the same screens from the Figma design. Resolved as the union, not a pick. The layout is this branch's — the MUI Stack, the 5:3 grid and the Rate Limit card — and 0191's revoke wiring is kept inside it: ApiKey takes both `session` (the design shows the account on the key card) and `onRevoked`, and Usage takes `revokedCount`, so an in-page revoke still clears the key and re-asks for usage. portal: 110 tests pass, typecheck, lint, build and prettier clean.
Nine changes off a review against the Figma frames, most of them one property. The two that are not: The hero and the trust band become ONE section sized to the viewport (`HeroSection`). They stay two components — two frames in Figma, two backgrounds — but the fold has to fall below both, and sizing the hero alone left the band cut in half. Two earlier attempts sized the wrong thing: the dashboard's key panel, then the key card without its heading. `SectionLabel` grows a `neutral` tone for "Free Tier Limits", which the design draws grey rather than yellow: it labels a panel, not a section, and two brand chips side by side read as two headings. That chip also needed wrapping — it is a flex item in a column Stack, so `inline-block` does not stop it being stretched as wide as the cards below it. The rest: Docs → Quick Start in the navbar, pointing at `#get-started` rather than the OpenAPI document; the status badge takes the chip radius instead of a pill; the hero glow is two stacked radials (one strong enough to see banded across the fold) and Endpoints, Self-Service and Fair Access get the same light through a new `glow` prop on `Section`; feature and dashboard icons become discs with glyphs that say what the card says, while the Fair Access ticks stay square because Figma draws a checklist marker differently from a category icon; the Free Tier cards lose 4px of padding and their labels go grey. Values are measured off the exported PNGs, not guessed — #432205 for the warm card, #a3a3a3 for the labels, #f5f5f5 for the units.
The mobile frame (node 922-11966), section by section, plus the real exported icons for the three sections Adam sent as zips. The one that was broken rather than unstyled: below `sm` the navbar hid its three links and offered no replacement, so a phone could reach a section only by scrolling. There is now a menu button and a panel that drops from the top with the links and the call to action. The frame draws only the closed state; the panel is the plainest reading of it. Six grids become `CardRail` — a grid from `sm` up, a scroll-snapped rail on a phone with the next card peeking past the screen edge. The rail bleeds out by the container's gutter on purpose: a card clipped by the screen says "keep going", a card clipped by a padding box says broken. The rest of the frame: the hero glow moves to the top third where the copy is, the trust band's chips scroll in one row, the dashboard preview drops below the claims it illustrates, the endpoint summaries stop hiding, both closing buttons go full width, and the footer centres. Icons: 15 exported SVGs replace Material stand-ins. Each file is the whole 32×32 badge — background path, then glyph — so the hand-built tiles, the accent pairing and the hue constants are gone with them. That also corrects an inversion: the design fills the disc with the accent's 100 shade and draws the glyph in its 900, and the code had it the other way round. The dashboard badges are rounded squares again, which is what the export contains. Two bugs with one cause, both found by measuring rather than reading: `Stack` spaces with margins on every child but the first IN DOM ORDER, through a selector that beats a child's own `sx`. In the dashboard section, where `order` swaps the halves on a phone, that put 80px above the heading and nothing between the text and the preview; in an endpoint row it overrode the summary's `margin-left: auto`, so "All asset prices" never reached the right edge. Both take `useFlexGap`, which lays out in visual order and leaves margins alone. Also measured: the `Get` chip is a 29×24 rounded rectangle whose edge runs straight after ~3px of arc, not the pill it was rendering as.
Two screens: the first-login card (843:2356) and the returning view (844:9434), measured off the 2x exports rather than eyeballed. `GET /key` grows `created_at` and `last_updated_at`. Both were already on `KeyRecord`, read off the listing the reveal makes anyway, so the two instants the metadata row states cost no extra control-plane call — and "Issued" stops being a date computed from the browser's clock. What the frames changed, colour by measured colour: the page floor is #212121 and not #0f0f0f (the cards were reading as lighter than their surroundings), a card is a #1a1a1a title band over a #272727 body, its hairline is solid #535353 rather than 45% of it, the key's ring is #fdda24, the usage bar is a white pill with a #ffe945 → #cc9302 fill, and the status pill's fills are solid. Card titles and the page heading are equal on the frame — both 24px — where the code had 28 and 20. Columns are 7fr:5fr with a 16px gutter, measured at 740 and 524. The first-login card: "Your API Key is ready", the welcome sentence, the key UNMASKED (the one place task 0187's mask lifts — the visitor finished the round-trip seconds ago and the card says "copy it below"), Copy key beside View quick start, and Issued · Monthly quota · Rate limit. The quota rides up from the usage panel through `onUsage` rather than costing a second GetUsage. The returning view: Key ID · Issued · Last rotated · Discord account, the yellow rotation strip under it, and the two lower cards. Adam's review, in his words and against my objections where they differ: - The status pill says "Just issued" for any working key. A 24-hour rule read "Active" on his own days-old key, which is what he was looking at. It is a wrong adjective on an old key, not a wrong instruction; the honest condition is written at the render site. - The button is "Regenerate", not 0191's "Replace my key…", and the strip says "Key rotation is limited to once per calendar month" — both describe the swap model 0191 built and reversed. Nothing rotates: the dialog behind the button still says the key is deactivated and nothing is issued, and where the two disagree the dialog is the one telling the truth. - "Last rotated" labels `lastUpdatedDate`, which a console edit bumps. - The Discord numeric id leaves the screen for the column's `title`, so task 0186's "the username and the ID are on screen" no longer holds. - The usage card loses task 0188's lag line, its reset-rule sentence and its Refresh button — the frame has none of the three and task 0222's chart takes the space. What goes unsaid now is that a figure can trail the last request by minutes. The Rate Limit card stops disappearing. `/config` without `PORTAL_RATE_LIMIT` — every local run — was dropping a third of the dashboard; it now falls back to the free plan's documented 1 req/s, the same figure the landing page states, and a deployment's value still wins. Three CSS-specificity bugs with one cause, all found by measuring rather than reading: a `Stack`'s margin-based spacing and a chrome descendant rule (`.chrome code`, `.chrome button`) each outrank the single class Emotion puts on an element's own `sx`. That is why 80px of gap landed above the dashboard heading instead of between the columns, why "All asset prices" never reached the right edge, and why the key's value kept its grey chip through two attempts to remove it. Fixed by narrowing the rules, not by escalating specificity. Broken/modified tests, all intentional: two deleted (the lag line, the Refresh button) with a note in their place saying what went and how to restore it, and seven rewritten — the account row now finds the id by `title`, the notice pins the date rather than the sentence, and the rate-limit test asserts the fallback it used to forbid. Spawns 0222 for the daily-requests chart: the backend already reads the per-day series in `Gateway::usage_of` and throws it away in `summarize_days`, so the chart needs no new AWS call — only the contract.
The `Quick start` frame (`918:644`) as a third route, reachable from the signed-in header where the link previously pointed at the raw OpenAPI document — a JSON file answering a question nobody asked. The page is public: it is documentation, and a developer deciding whether to authorise the app is exactly who should read it. Signed in it wears the dashboard's bar with `Quick start` underlined, signed out the landing bar. Everything on it is a still. No request runs from here — a "try it" control needing the visitor's real key is what Swagger UI (0195) is for — so the one interactive thing is the copy button beside each snippet. The base URL and the paths are the DESIGN's, not this repo's OpenAPI document's, and the gap is the one `landing/Endpoints.tsx` already flags. Both live in a single constant so reconciling them is a two-line diff rather than a hunt. `/api-tokens/quick-start` joins `APP_ROUTES` in the hosting stack: without it a hard refresh resolves against S3 and comes back as 403 AccessDenied.
Five things, four of them the frame's and one a link that was wrong long before this page existed. The footer's dashboard link was a bare `href="/dashboard"`, which the browser resolves against the domain root — a path the deployment does not serve, since the bundle lives under `/api-tokens/`. It is a `RouterLink` now, like every other in-app destination on the page. The test that pins it renders WITH a basename, unlike its neighbours: without one the broken form and the fixed one produce the same string. The glow was positioned from the left edge of the page, where the frame measures it — but the frame's content column is not centred and ours is, so on a wide window the light sat out in the margin instead of on the word `Get`. It hangs off the headline itself now and needs no phone rule. The rail did not stay put: `main` carried `overflow: hidden`, which makes it a scroll container, and a scroll container is what `position: sticky` sticks to. `overflow-x: clip` still cuts the glow without creating one. Its active entry came from an `IntersectionObserver`, which is handed only the headings whose visibility just changed — so mid-section, or after landing on a `#hash`, it had nothing to say and the rail kept whatever it last knew. Measured on scroll instead, it always has an answer. `Contact us` is underlined, as the frame draws it, and still not a link: there is no commercial-plans destination, and the dashboard's Rate Limit card refuses the same 404 for the same reason. The three `What's next` badges are the real exports from Adam's `Designs.zip` rather than the nearest MUI glyph in a hand-drawn circle.
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
theme/tokens.ts(the file's variables, verbatim) andtheme/theme.ts(the interpretation), with the three families self-hosted — no CDN fonts, no third-party scripts on a page that renders a credential./quick-start— prerequisites through SDK examples, every snippet copyable, reachable from the signed-in header where the link used to point at the raw OpenAPI document./api-tokens/quick-startjoins the hosting stack's route list so a hard refresh does not 403.No copy owned by another slice was changed here — the eligibility refusals are 0189's, the delete-key and revoke wording 0191's, the usage lag line 0188's.