Skip to content

Phase 5 — UX quality, i18n, errors

Status: Closed 2026-04-26 (started 2026-04-25). See plan.md for the phase overview. Scope: UX-polish cluster — brand config, settings, tooltips, error taxonomy retrofit, i18n infrastructure, XLSX translation workflow, Playwright smoke suite, Python SDK scaffold, impact-function viz, compare view, onboarding. Predecessor: Phase 4 — Map stack. Successor: Phase 6 — Admin + ops + catalog.

Goal

Lift the UI from "functional map tool" to "product people would run a demo against." This phase retrofits the error taxonomy (ADR-028) into real code, wires i18n infrastructure + a translation workflow that doesn't require JSON PRs, lands the missing UX glue (tooltips, settings, brand config, onboarding flow), and introduces the first Playwright smoke suite so regressions are caught before any managed-hosting rollout. Also ships the Python SDK scaffold — Tier 3 but cheap to do alongside the error taxonomy since error classes are shared.

Entry conditions

  • Phase 4 closed: MapLibre migration done, UI redesign ADR + wireframes available to plan against.
  • ADR-028 (error taxonomy) merged in Phase 3; error registry at src/climate_lama/core/errors.py exists but is not yet wired into every exception path.

Out of scope (deferred)

  • Admin panel, datasets browser UI, external polling — Phase 6.
  • Reports (PDF, snapshots, export bundles) — Phase 7.
  • Storybook — Phase 7 (after redesign stabilises enough that stories don't rot immediately).
  • Hosted translation tool (Weblate/Tolgee) — Tier 3 re-evaluation; defer per R4 B5 exit criteria.

Scope

5.1 — Error taxonomy retrofit + error/warning/info UI

Context. ADR-028 pinned the envelope + registry in Phase 3. This item does the ground-level work: retrofit every raise in the backbone to map through a coded error, harmonise the HTTP exception handler, and build the UI presentation layer so error/warning/info surface consistently.

Acceptance criteria. - [ ] Every internal exception in src/climate_lama/core/ and src/climate_lama/worker/ maps to an error code from the ADR-028 registry; ad-hoc raise with string-only messages flagged by a lint rule (custom check in tests/). - [ ] HTTP exception handler returns the standardized envelope consistently. - [ ] UI component <ErrorDisplay code={...} severity={...} /> (Tailwind, no MUI) renders error / warning / info variants with icon + title + message + optional detail expansion. - [ ] Code → localized-message mapping lives in the UI i18n JSONs (ties to 5.4). - [ ] Unit tests: sampling 20 codes, asserting envelope shape + UI renders matching severity. - [ ] Postman collection updated with example error responses. - [ ] scripts/demo.py exercises at least one controlled failure path and prints the coded error.

Cross-stack: backbone (retrofit + handler) + UI (component + i18n entries). Linked ADR: ADR-028. Suggested /analyze tier: 4 (Opus · high) — wide retrofit, touches both repos, lint rule authorship.


5.2 — Python SDK scaffold (#I)

Context. Vision #I Tier 3 but bundled here because it consumes the error envelope from 5.1. Ship climate-lama-python on PyPI with auto-gen primitives + a thin hand-written wrapper (Option B in vision R2 write-up).

Acceptance criteria. - [ ] New repo climate-lama-python (or subtree + independent publish workflow — decide during /build). - [ ] Auto-gen layer via openapi-generator-cli in CI, regenerated on backbone OpenAPI changes. - [ ] Hand-written wrapper client.hazards.*, client.exposures.*, client.compute.impact().wait() matching the vision #I example. - [ ] Error classes derived from ADR-028 codes; SDK raises typed ClimateLamaError subclasses. - [ ] pip install climate-lama publishes to Test PyPI on tag; production PyPI publish gated on user-triggered workflow. - [ ] README + docs-site page sdks/python.md with the quickstart example. - [ ] Smoke test: SDK runs one impact calc against a local backbone in CI.

Cross-stack: new repo / workflow + docs site. Linked ADR: references ADR-028. Suggested /analyze tier: 4 (Opus · high) — new repo, CI publish pipeline, API design.


5.3 — Tooltips + info icons + glossary JSON (#16)

Context. Vision #16 Tier 2. Every form field gets a tooltip; every metric card gets an info icon → popover with definition + doc link. The glossary is a single source of truth reused by the docs site.

Acceptance criteria. - [ ] climate-lama-ui/src/glossary/glossary.en.json: {key: {term, short, long, docs_url}}. Initial keys cover ~30 domain terms (EAD, AAI, return period, exceedance frequency, MDR/MDD/PAA, scenario IDs). - [ ] <InfoIcon helpKey="ead" /> component pulls from the glossary. - [ ] Every MUI TextField in the wizard gains a tooltip via the same helpKey mechanism. - [ ] Docs site pulls glossary for the concepts/glossary.md page — single source. - [ ] Vitest: <InfoIcon /> renders expected tooltip for sampled keys.

Cross-stack: UI + docs site. Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.4 — i18n infrastructure (#15)

Context. Vision #15 Tier 2. All hardcoded strings extracted to keys, EN locale 100% populated, RTL CSS scaffolding wired but empty, date/number formatting via Intl. No translations yet — that's 5.5.

Acceptance criteria. - [ ] i18next + react-i18next pinned; initialized at app boot. - [ ] All hardcoded strings extracted; a lint rule flags new inline strings in JSX. - [ ] climate-lama-ui/src/i18n/locales/en/{common,errors,glossary,...}.json covers 100% of visible strings. - [ ] RTL support scaffolded via CSS logical properties + <html dir> toggle from i18n.dir() (the UI is Tailwind-based, not MUI — no MUI RTL plugin needed). - [ ] Date/number formatting via Intl.DateTimeFormat and Intl.NumberFormat, locale-aware. - [ ] Language switcher component in the settings page (5.6). - [ ] Vitest: snapshot one panel in a second (dummy) locale to prove the infrastructure works.

Cross-stack: UI. Linked ADR: none. Suggested /analyze tier: 4 (Opus · high) — wide-reaching rewrite touching every component.


5.5 — XLSX translation round-trip (#33)

Context. Vision #33 R4 B5. Tier 2: XLSX export + import scripts so partners can translate without JSON PRs. Validate that the JSON format stays compatible with Weblate's git-integration model (so the Tier 3 re-eval is non-destructive).

Acceptance criteria. - [ ] scripts/export_translations.py: reads all locale JSONs, emits a single XLSX with one sheet per language + key | en | <lang> columns. - [ ] scripts/import_translations.py: reads an XLSX back, validates placeholder parity ({count}, {{name}}) and key stability, writes JSON files, fails on regression. - [ ] Makefile targets make translations-export and make translations-import. - [ ] JSON format confirmed compatible with Weblate's default component layout (file-per-language, flat keys under namespace/) — document in docs/i18n/README.md. - [ ] Unit tests for both scripts: round-trip a fixture without data loss. - [ ] Docs site page contributing/translations.md for partner workflow.

Cross-stack: tooling (UI repo) + docs. Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.6 — Brand config + theming (#1)

Context. Vision #1 Tier 2. BrandConfig JSON loaded at boot: env var → mounted file → API endpoint fallback. MUI palette overrides + CSS variables; light, dark, high-contrast (not "12 themes").

Acceptance criteria. - [ ] Schema BrandConfig: {logo_url, favicon_url, app_name, primary_color, secondary_color, default_locale, theme_mode, support_url}. - [ ] Boot-time loader in climate-lama-ui/src/branding/ with the three- source fallback chain. - [ ] Tailwind theme + CSS variables consume BrandConfig (the UI is Tailwind-based, not MUI). Themes swap CSS variables, not classes. - [ ] Three built-in themes: light, dark, high-contrast. - [ ] Backbone endpoint GET /v1/orgs/{id}/branding returns the org-level override (ties to 5.7 org settings). - [ ] Vitest: snapshot one panel in each theme.

Cross-stack: UI (primary) + backbone (org-branding endpoint). Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.7 — Settings page (#2)

Context. Vision #2 Tier 2. User settings (theme, language, default time horizon, default scenario, units, email notifications) + org/admin settings (branding overrides, default impact-function selection, data-source allow-list, default discount rate).

Acceptance criteria. - [ ] Backbone: user_preferences + org_settings tables, JSONB for forward compat. Endpoints /v1/users/{id}/preferences and /v1/orgs/{id}/settings with RBAC (user can read/write own; admin can read/write org). - [ ] UI: /settings route with tabs "My preferences" + "Organization" (the latter visible only to admins per Phase 2 RBAC). - [ ] Settings are applied live (theme change, locale switch do not require reload). - [ ] Tests: RBAC — non-admin gets 403 on org settings; admin gets full access.

Cross-stack: backbone (schema + endpoints) + UI (page + state wiring). Linked ADR: none. Suggested /analyze tier: 4 (Opus · high) — two tables, RBAC-adjacent, two repos.


5.8 — Impact-function read-only viz (#8)

Context. Vision #8 Tier 2. Every impact-function selector shows a sparkline in the dropdown + full chart in a side panel. Source, citation, and original paper displayed. Editor is deferred.

Acceptance criteria. - [ ] Backbone endpoint GET /v1/impact-functions/{id}/curve returns the intensity→MDR/MDD/PAA arrays. - [ ] UI component <ImpactFunctionChart /> renders using recharts (already pinned in climate-lama-ui/package.json). - [ ] Dropdown items show a 20-point sparkline inline. - [ ] Right-side detail panel shows the full curve + attribution (source, citation, paper URL) pulled from the #3.10 attribution fields. - [ ] Mobile-responsive: chart scales down below 600px.

Cross-stack: backbone (endpoint) + UI (chart + selector redesign). Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.9 — Configurable discount rate + growth (#11)

Context. Vision #11 Tier 2 minimal. Surface discount_rate and value_growth_rate as configurable inputs in the cost-benefit screen with Stern 1.4% and Nordhaus 4.3% presets + custom.

Acceptance criteria. - [ ] Verify the engine already accepts these inputs (it should per CLIMADA-inherited semantics; if not, raise a blocker). - [ ] Compute API accepts discount_rate and value_growth_rate as optional fields on POST /v1/compute/cost-benefit. - [ ] UI cost-benefit panel gains two inputs with preset dropdown + custom; defaults surface in settings (5.7). - [ ] Report output (Phase 7 #14) will pick these up — stamp them on the scenario (Phase 3 #3.7 already reserves inputs JSONB). - [ ] Docs-site page concepts/discounting.md explains Stern vs Nordhaus.

Cross-stack: backbone + UI + docs. Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.10 — Scenario compare lite (#B)

Context. Vision #B Tier 2 lite: list-view selectable, open two scenarios in two columns. Full diff map + waterfall chart is Tier 3 / Phase 7.

Acceptance criteria. - [ ] "Compare" button on scenario list selects two; /compare?a=...&b=... route opens them side-by-side. - [ ] Each column renders the scenario view (inputs, summary metrics, map, frequency curve). - [ ] A summary header shows "Δ EAD", "Δ AAI" (simple arithmetic diffs). - [ ] No backend compare endpoint yet (two parallel reads); note perf budget for Phase 7 full compare.

Cross-stack: UI only (for lite). Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.11 — Onboarding / sample-dataset flow (#D)

Context. Vision #D Tier 2 must-have. A 2-click "try the demo" path: new user lands → sees a populated map + result, then can swap inputs.

Acceptance criteria. - [ ] Seed a fully worked Greek-flood scenario on backbone startup (idempotent; only if the sample org has no scenarios). - [ ] First-run detection in UI (user has 0 scenarios) shows a call-out: "Try the demo" → one-click loads the seeded scenario. - [ ] Guided tour via react-joyride walks the user through the wizard, map, result panel, report button (if 5.6-5.9 landed). Tour skippable and dismissable permanently in settings. - [ ] Docs-site page quickstart/first-scenario.md mirrors the in-app flow for self-hosters.

Cross-stack: backbone (seed) + UI (detection + tour) + docs. Linked ADR: none. Suggested /analyze tier: 3 (Sonnet · high).


5.12 — Playwright smoke suite (#28 Tier 2)

Context. Vision #28 Tier 2 split. 5-10 smoke tests covering login, upload, run-scenario, view-result, save-scenario, open-compare, change settings. Runs against docker-compose in CI.

Acceptance criteria. - [ ] Playwright project at climate-lama-ui/tests/e2e/ with config + docker-compose fixture bringup. - [ ] 5-10 tests covering the critical paths listed above. - [ ] CI workflow .github/workflows/e2e.yml runs on PR; artifacts (video, traces) uploaded on failure. - [ ] Tests stable on 3 consecutive runs before merge; flake retry = 2. - [ ] Docs-site page contributing/e2e-tests.md.

Cross-stack: UI + CI. Linked ADR: none. Suggested /analyze tier: 4 (Opus · high) — CI setup + Docker orchestration + flake management.


ADRs authored in this phase

None required by default. Optional short ADRs if /build uncovers non-obvious choices (theming fallback chain in 5.6, preset values in 5.9).

Exit criteria

  • All 12 work items closed.
  • Error taxonomy retrofit complete — zero string-only raises in core/ or worker/.
  • UI renders in at least one dummy non-EN locale end-to-end (infrastructure proven; no real translation yet).
  • XLSX round-trip scripts run green locally.
  • Playwright smoke suite green on 3 consecutive CI runs.
  • Python SDK published to Test PyPI with a smoke test passing.

Risks

  • 5.1 error retrofit is a wide change; merge as several PRs (per-subsystem) rather than one mega-PR. Each PR runs /build --review, not auto-merge.
  • 5.4 i18n risks introducing untranslated strings that slip past the lint rule. Keep the lint strict; better a noisy CI than missing keys discovered post-merge.
  • 5.12 Playwright flake is the usual story. Do not gate merges on e2e until stable for two weeks; rely on Vitest + backbone tests for the merge gate.
  • 5.2 SDK publish to real PyPI is user-gated (explicit workflow trigger). Test PyPI is fine to auto-publish on tag.