osr-product-experience
Design and build the user experience of the OpenSmartRoute web app and dashboard under platform/web - screens, user flows, navigation placement, page layouts, components and design tokens so that anyone can use the product without reading the docs. Redesign or simplify a dashboard page, the playground, the signup and login forms, the pricing page or the operator console; put the right action in the right place; write clear empty, loading and error states; keep the UI clean, consistent, responsive on a phone and accessible; make the interface prove the platform is real by showing live data and traces instead of claims. Understand user behaviour with empathy - what a first-time visitor, a developer wiring the API, a workspace admin and an operator each try to do, where they hesitate and why. Validate every change with vitest, Playwright e2e journeys, the axe accessibility audit and the responsive overflow guard. Use when changing UI, UX, screens, components, flows, copy or usability in platform/web.
- Package
- .claude/skills/osr-product-experience
- Compatibility
- OpenSmartRoute repository, Node >= 22, Next.js 16, React 19, Tailwind 4, Playwright
- License
- Apache-2.0
- Domains
- coding marketing data_analysis general
- Quality prior
- 0.85
- Tags
- opensmartroute ux ui design screens flows components accessibility responsive playground dashboard nextjs
Install by copying .claude/skills/osr-product-experience/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.
The web app (platform/web, Next.js 16 App Router, React 19, Tailwind 4, shadcn/ui primitives, Recharts) is the
place where a person decides whether OpenSmartRoute is real, whether it is for them and whether they can trust
it with traffic. This skill is the playbook for changing what they see. Read osr-platform first for the build,
proxy and deployment mechanics; this skill is about what goes on the screen, where, and how to prove it works.
For onboarding, activation and growth measurement see osr-customer-success; for words and colours see
docs/BRAND.md (voice: precise, calm, evidence-led - "routes", "decides", "learns", never "magically").
Who is on the other side of the screen
Design every page for one of these people first and name them in the PR. Each has a different question, a different first screen and a different definition of "it works".
| Person | Arrives at | Wants to know in 30 s | First success | Where they hesitate |
|---|---|---|---|---|
| Evaluator (CTO, platform lead) | /, /pricing, /compare, /rankings, /status | Is this real, who runs it, what does it save, what does it cost, is it up | Sees live numbers (LiveStats, /rankings, /models), the readiness checks on /status and a trace, not adjectives | Marketing claims without evidence, a pricing page that hides limits, no status |
| Developer (wires the API) | /platform/signup, /docs/QUICKSTART_PLATFORM, /platform/playground | How do I send one request and see why it was routed | POST /api/v1/route answers and the DecisionPanel explains the choice | A key they cannot find again, an unclear error, execute failing because no provider is connected |
| Workspace admin | /platform/dashboard | Are we within quota and budget, who is using it, what did it cost | The overview StatCards, /dashboard/usage, /dashboard/savings, /dashboard/governance | A form that needs the docs, a gated feature with no explanation, a number without a unit |
| Operator (runs the deployment) | /platform/admin | Is the deployment healthy, who signed up, who is stuck, what is the margin | /platform/admin/analytics, /health, /finance, /activity | Pages that need three clicks to answer one question, tables without links to the thing |
Empathy is procedural here, not a mood: before changing a page, write the one question that person brings to it, then check the page answers it above the fold, in their words, with a real number or a real action.
Where things live
src/app/(marketing)/* public site: /, /pricing, /compare, /models, /rankings, /marketplace, /estimate,
/roi, /blog, /providers, /vendors, /support, /privacy, /terms
src/app/platform/(auth)/* /platform/login, /signup, /forgot-password, /reset-password, /verify-email,
/invite/[token], /cli/authorize, /email/preferences
src/app/platform/dashboard/* workspace: overview, activity, handoffs, usage, savings, keys, providers,
integrations, listings, governance, events, learning, health, notifications,
tenants, audit, members, workspace, billing, support, account
src/app/platform/admin/(console) operator console (ADMIN_NAV in components/admin/admin-shell.tsx)
src/app/platform/playground the route tracer (components/playground/{playground,decision-panel,compare-panel}.tsx)
src/components/ui/* primitives: button, card (Card, CardHeader, CardBody, StatCard, Alert, Badge,
Skeleton, EmptyState, Trend), field (Input, Textarea, SearchInput), table
(DataTable), tabs (Tabs, SegmentedControl), dialog, sheet, drawer, toast (Sonner),
tooltip (Hint), empty, code (CodeBlock, CopyButton), progress, switch (SwitchField)
src/components/charts/index.tsx CHART_COLORS / SERIES / seriesColor (CSS vars), TimeBars, TimeArea, Bars,
RankedBars, ShareBar, Sparkline, ChartEmpty, ChartTooltip
src/components/layout/* SiteHeader, SiteFooter, Logo / BrandMarkImage, ThemeToggle, NavIcon, SearchCommand
src/components/dashboard/* DashboardShell (SECTIONS, PageHeader), GettingStarted, UsageChart, ApiError
src/components/observability/* TraceView / TraceLoader (the proof a decision is explainable), StatusPill,
SystemStatusCard, AlertsPanel, series
src/lib/config/site.ts site, seo, install one-liners, headerNav, dashboardNav, footerNav
src/lib/api/{client,use-api,types,proxy}.ts api.get/post, ApiError (status, upgradeRequired, keyScope,
edition, budget, walletBalanceCents, requestId, retryAfter), useApi, useAdminApi
src/lib/auth/{session,operator}.tsx useSession (me, apiKey, signIn/out), useOperator
src/app/globals.css tokens: --ink/--paper/--snow/--mist/--line, --brand-blue/-cyan/-violet,
--status-*, --chart-1..9, text-display-{xl,lg,md,sm}, .dark, .theme-darkpage.tsx files may only export default; shared UI goes into components/. Navigation is declared in one
place each: dashboardNav + SECTIONS (dashboard), ADMIN_NAV (console), headerNav / footerNav (site).
A new dashboard or console page also needs its row in docs/PLATFORM.md section 13 (the docs-claims test
checks every /platform/dashboard/<page> and /platform/admin/<page> documented there exists, and vice versa).
Rules for a clean screen
- One question per page, answered above the fold. The
PageHeadertitle says what the page is, the description says what the numbers mean and over which window ("Usage covers the last seven days"). The first row isStatCards with a unit and a hint (hint="Only executed requests incur provider cost"), never a bare number. - Right action in the right place. The primary action sits in
PageHeader action=(top right); the destructive one at the bottom of its card,variant="destructive", behind aDialogconfirmation. Row actions live in the row. Never two primary buttons on one screen. - Progressive disclosure, not a wall. Defaults first, the advanced block in a
Collapsible/Tabs. The playground showstext+ Route; objective weights, constraints,planandexecutesit behind it. Gate by plan with the sameAlertpattern everywhere (ApiError.upgradeRequired-> "not in your plan" with the link to/platform/dashboard/billing), never by hiding the feature silently. - Every state has a design.
loading && !data->Skeletonwith the final shape;error->Alert tone="danger"witherror.messageand, for 5xx, therequestIdso support can find it;!data.length->EmptyState/ChartEmptythat says what will appear here and offers the action that creates it ("No requests yet - open the playground"). A page must never render a blank card orundefined. - Show the evidence. A routing decision is never a label alone: link to the trace (
TraceLoaderin the activity drawer,DecisionPanelin the playground), show confidence, cost and the alternatives. Savings showbaseline_cost_usdnext torouted_cost_usdand how the baseline was chosen. Status is a liveStatusPill, not a green dot drawn in CSS. - Copy is plain and specific. Sentence case, no exclamation marks, numbers with units (
formatUsd,formatPercent,formatMs,formatCompactfromlib/utils/format.ts), dates throughformatDate. Prefer "Route a first request" to "Get started!". Name the real API call next to UI actions where a developer is the audience (the getting-started list showsPOST /api/v1/route). - Forms.
Field-labelled inputs (a label a screen reader can name), inline validation on blur,aria-invalid, the submit buttonloadingwhile pending, success throughtoast.success, the created secret (API key) shown once in aCodeBlockwithCopyButtonand a sentence saying it will not be shown again. Escape-insidepatterncharacter classes ([A-Za-z0-9._\-], thevflag). NeverDate.now()in render (React Compiler lint) -useState(() => Date.now()). - Tokens only. Colours come from the CSS variables (
text-ink,bg-snow,border-line,text-brand-blue,CHART_COLORS.*), never hex in a component; both themes must work (.darkon<html>, always-dark panes use.theme-dark; the brand mark throughBrandMarkImage, vendor logos inside.logo-tile). Display type istext-display-*(registered intailwind-mergeextendTailwindMerge- keep it there). - Responsive by construction. Grid tracks are
minmax(0,1fr)(a table or<pre>otherwise widens the track;.grid > * { min-width: 0 }inglobals.cssis the safety net); action rows wrap; tables and code panes scroll inside their card (overflow-x-auto), never the page. Two-column detail layouts collapse belowlg. Check 375 px and 768 px. - Accessible by default. Every icon-only button has
aria-label; every imagealt; focus is visible (focus-visible:ring-[3px]); dialogs trap focus (Radix); colour is never the only signal (theBadgetone carries a word too); contrast >= 4.5 on both themes (--brand-cyan-textexists because cyan on white does not pass). Motion respectsprefers-reduced-motion.
Understanding behaviour before redesigning
Do not guess where people struggle - the platform records it, without prompts:
- Funnel and time to value -
GET /api/v1/admin/analytics/activation?days=(console Analytics, card Activation funnel): the share of new workspaces that confirmed the e-mail, routed, got an answer, reported success, returned; the median hours to each. A step with a steep drop is a screen to redesign. - Stuck before the first request - the same payload's
stucklist: who signed up more than a day ago and never routed. Read their console page (/platform/admin/workspaces/{id}): did they create a key, open the playground, hit a 4xx? - Where errors happen -
GET /api/v1/admin/activity?failed=trueand the events page; a 400 onPOST /api/v1/routeright after signup is a form or copy problem, not a user problem. - What people click on the public site - GA4 events through
lib/analytics.tstrack()(sign_up,login,select_content), consent-gated (ConsentBanner), never on/platform/dashboard/*or/platform/admin/*(isTrackedPath). First-touch attribution (lib/attribution.ts) lands on the signup and in the funnel'sby_source. - Support tickets and sales inquiries (
/platform/admin/support,/leads) are the qualitative signal; quote them in the PR when they motivate a change. - Watch someone. For anything on the first-value path (signup -> key -> playground -> activity) sit with one person who has never seen it, say nothing, note every hesitation. Five people find most of what matters.
Write the finding as observation -> hypothesis -> change -> metric that will move, and put the metric in the CHANGELOG entry ("activation funnel: first_route share").
Building a page: the recipe
- Data first: find or add the endpoint (
osr-platform), then the TypeScript type inlib/api/types.tswith a one-line doc comment naming the route. useApi<T>("/api/v1/...")(workspace) oruseAdminApi<T>(console); design the four states before the happy path.- Compose from primitives:
PageHeader->StatCardrow ->Cardgrid (lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]) ->DataTable/RankedBars/TimeBars. Rows link to the thing they describe (Linkto/platform/admin/workspaces/{id},/models/{id}). - Register: nav (
site.ts/SECTIONS/ADMIN_NAV) +NavIconname, thedocs/PLATFORM.mdrow, the CHANGELOG bullet. - Prove it: unit test for any pure helper (
tests/*.test.ts), e2e coverage (below), then the visual pass in both themes at 375 / 768 / 1280.
Validation - what "working" means here
cd platform/web
npm run check # eslint + tsc + vitest (tests/*.test.ts, .tsx)
npm run build # the standalone build the e2e harness serves
$env:OSR_E2E_DATABASE_URL="postgresql://osr:osr@127.0.0.1:55432/osr" # docker start osr-test-pg
npx playwright test e2e/customer-journey.spec.ts # sign up -> dashboard -> playground -> keys -> activity
npx playwright test e2e/accessibility.spec.ts # axe WCAG 2.1 A/AA on the first-value pages (serious+ fails)
npx playwright test e2e/responsive.spec.ts # no sideways scroll at 375 / 768 on every page
npx playwright test e2e/dashboard-pages.spec.ts e2e/console-pages.spec.ts # every page renders real data, no crash
npx playwright test e2e/page-quality.spec.ts # failed requests, console errors, leaked "undefined"/NaN, one h1,
# dead internal links, unknown slugs -> not-found page (not a skeleton)e2e/global-setup.tsspawns a throw-away API on 8111 and the standalone build on 3111 (OSR_E2E_WEB/OSR_E2E_APIpoint at servers you already run - clear them afterwards, they linger in the shell).- Helpers in
e2e/helpers.ts:signUp(tag)(unique e-mail per run - the DB is shared across runs),adoptSession(page, token),operatorToken(),setPlan,field(page, /^Label/),expectNoCrash(page). Locator lessons:Fieldlabels with hints ->getByRole("textbox", { name: /^Code/ });getByRole("alert")also matches Next's route announcer; Card titles areh3; a hidden streamed copy of a form exists indiv#S:0-> use:visible. - Add a new page to
PUBLIC/DASHBOARD/ADMINinaccessibility.spec.tsandresponsive.spec.ts, and to the page-walk indashboard-pages.spec.ts/console-pages.spec.ts. A new journey gets its own spec named after what the person does ("open a ticket, get its reference and e-mail, operator answers..."). - Theme and contrast audit: run against
next devon 3001 (OSR_API_URL=http://localhost:8080), sign in by fetch (POST /api/v1/signup->localStorage osr.platform.apiKey), parse colours through a 1x1 canvas (getImageData; Tailwind 4 emitslab()/oklab()). Screenshots go to%TEMP%, never underplatform/web. - Against the dev stack (
docker compose up -d --build, containers run the image, no watch):python -X utf8 platform/api/scripts/smoke.py --api http://localhost:3000 --web http://localhost:3000 --operator admin:admin-dev-onlywalks every page and API surface. - Live:
platform/api/scripts/live_use_cases.py --key <k>proves routing on production; open the deployed page in both themes on a phone before closing the task.
Anti-patterns seen in this codebase (do not bring them back)
- A
1frgrid track that grew to a<pre>/ install command and made the page scroll sideways on phones. tailwind-mergedroppingtext-display-*as a colour class;shrink-0action rows; fixedw-64inputs.- A cache-only
useApipage that showed stale numbers after a mutation without areload(). - A gated feature that disappeared on the free plan instead of explaining the gate.
- Provider "healthy" badges that were five days old; health must show when it was checked.
- Clicks that "worked" in the integrated browser but not on the deployed site - verify on the real origin.
notFound()thrown by a detail page under a route-grouploading.tsxleft visitors on the skeleton with a 200: every route group that streams needs its ownnot-found.tsx(the public one isapp/(marketing)/not-found.tsx).- A checklist step that was "done" for the wrong reason (
user is Noneon a key session read as verified): a step must be computed from the event it names, for every kind of principal. - "100% succeeded" over zero requests; raw proxy errors (
platform API unavailable: fetch failed) shown to a person.
osr-platform
Work on the hosted OpenSmartRoute platform under platform/ - the FastAPI platform API (osr_platform, accounts, API keys, plans, metered /api/v1/route, OpenAI-compatible /v1 proxy, admin, editions, the committed openapi.json snapshot) and the Next.js 16 web app (landing, models catalogue, rankings, playground, pricing, dashboard, route-handler proxies).
osr-routing-catalogue
Define and tune an OpenSmartRoute routing catalogue.