From 1409df458b72be30b86d22bf564ddc4da2eda737 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 00:29:46 +0300 Subject: [PATCH] Add 1F-layout implementation plan for root layout, error routes, smoke route --- .../plans/2026-04-14-phase-1f-layout.md | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-14-phase-1f-layout.md diff --git a/docs/superpowers/plans/2026-04-14-phase-1f-layout.md b/docs/superpowers/plans/2026-04-14-phase-1f-layout.md new file mode 100644 index 00000000..2617f847 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-phase-1f-layout.md @@ -0,0 +1,262 @@ +# Phase 1F-layout — Root layout + routes + error mapper contracts + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the root layout provider stack, locale-scoped layout, error boundary, error-to-HTTP mapper, error pages, and smoke route — so that all downstream feature routes render inside a fully-wired provider tree (`LoggerProvider` > `ApiClientProvider` > `I18nProvider` > `ErrorBoundary`) and error handling works end-to-end. + +**Architecture:** Modern.js file-based routing. `src/routes/layout.tsx` wraps children with the global providers (Logger, ApiClient, ErrorBoundary). `src/routes/[lang]/layout.tsx` validates the `lang` param, creates a request-scoped i18n instance, and wraps children with ``. Error pages live at `src/routes/error/[code]/page.tsx`. The smoke route at `src/routes/[lang]/smoke/page.tsx` exercises logger, i18n, and locale display. + +**Tech Stack:** React 18, Modern.js SSR, i18next, Vitest. + +**Prerequisites:** 1A-1 (skeleton), 1A-2 (MF builds), 1C (i18n), 1D (API client), 1G-logger (logger). + +--- + +## File structure + +| File | Responsibility | Task | +|---|---|---| +| `src/ui/errors/ErrorBoundary.tsx` | React error boundary with retry | 1 | +| `src/routes/error/map.ts` | `errorToResponse()` mapper | 2 | +| `src/routes/error/map.test.ts` | TDD tests for mapper | 2 | +| `src/routes/layout.tsx` | Root layout with provider stack | 3 | +| `src/routes/[lang]/layout.tsx` | Locale-scoped layout | 3 | +| `src/routes/error/[code]/page.tsx` | Error page (404, 500, 503) | 4 | +| `src/i18n/locales/en/common.json` | Add SMOKE keys | 5 | +| `src/i18n/locales/ru/common.json` | Add SMOKE keys | 5 | +| `src/routes/[lang]/smoke/page.tsx` | Smoke route | 5 | + +--- + +## Task 1 — ErrorBoundary component + +**Files:** +- Create: `src/ui/errors/ErrorBoundary.tsx` + +- [ ] **Step 1: Create the ErrorBoundary class component** + +The ErrorBoundary must be a class component (React requirement for `componentDidCatch`). It catches errors in its subtree, renders a fallback UI with a "Retry" button that resets the boundary state. + +```tsx +// src/ui/errors/ErrorBoundary.tsx +import { Component } from "react"; +import type { ReactNode, ErrorInfo } from "react"; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + state: ErrorBoundaryState = { hasError: false, error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error("[ErrorBoundary]", error, info.componentStack); + } + + handleRetry = (): void => { + this.setState({ hasError: false, error: null }); + }; + + render(): ReactNode { + if (this.state.hasError) { + if (this.props.fallback) return this.props.fallback; + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + return this.props.children; + } +} +``` + +- [ ] **Step 2: Verify typecheck** + +```bash +pnpm typecheck +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ui/errors/ErrorBoundary.tsx +git commit -m "Add ErrorBoundary class component with retry support" +``` + +--- + +## Task 2 — TDD errorToResponse mapper + +**Files:** +- Create: `src/routes/error/map.ts` +- Create: `src/routes/error/map.test.ts` + +- [ ] **Step 1: Write failing tests** + +Tests cover all four mapping rules: +1. `ApiHttpError(404)` -> `{ status: 404, errorCode: "not_found" }` +2. `ApiHttpError(502)` -> `{ status: 500, errorCode: "internal" }` +3. `ApiTimeoutError` -> `{ status: 503, headers: { "Retry-After": "30" }, errorCode: "unavailable" }` +4. Unknown error -> `{ status: 500, errorCode: "internal" }` + +- [ ] **Step 2: Write the implementation** + +```ts +export interface ErrorResponse { + status: 404 | 500 | 503; + headers?: Record; + errorCode: "not_found" | "internal" | "unavailable"; +} + +export function errorToResponse(error: unknown): ErrorResponse; +``` + +- [ ] **Step 3: Run tests — all must pass** + +```bash +pnpm test -- src/routes/error/map.test.ts +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/routes/error/map.ts src/routes/error/map.test.ts +git commit -m "Add errorToResponse mapper with TDD tests" +``` + +--- + +## Task 3 — Root layout + locale-scoped layout + +**Files:** +- Modify: `src/routes/layout.tsx` (replace 1A-2 stub) +- Create: `src/routes/[lang]/layout.tsx` + +- [ ] **Step 1: Update root layout** + +Replace the stub with the real provider stack: +- `` wrapping everything (logger from `createRootLogger()`) +- `` with a default-locale ApiClient +- `` wrapping children + +- [ ] **Step 2: Create locale-scoped layout** + +`src/routes/[lang]/layout.tsx`: +- Validate `params.lang` using `isLanguage()` from `@/i18n/resolver` +- If invalid lang, redirect to `/ru/` (or render 404) +- Create i18n instance via `createI18nInstance({ locale: params.lang })` +- Wrap children with `` + +- [ ] **Step 3: Verify typecheck** + +```bash +pnpm typecheck +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/routes/layout.tsx src/routes/\[lang\]/layout.tsx +git commit -m "Wire root layout provider stack and locale-scoped layout" +``` + +--- + +## Task 4 — Error pages + +**Files:** +- Create: `src/routes/error/[code]/page.tsx` + +- [ ] **Step 1: Create error page component** + +Simple text-based UI for codes 404, 500, 503. Renders heading, description, and a link back to home. No design system dependency. + +- [ ] **Step 2: Verify typecheck** + +```bash +pnpm typecheck +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/routes/error/\[code\]/page.tsx +git commit -m "Add error pages for 404, 500, 503 codes" +``` + +--- + +## Task 5 — Smoke route + i18n keys + +**Files:** +- Modify: `src/i18n/locales/en/common.json` (add SMOKE keys) +- Modify: `src/i18n/locales/ru/common.json` (add SMOKE keys) +- Create: `src/routes/[lang]/smoke/page.tsx` + +- [ ] **Step 1: Add SMOKE i18n keys** + +Add to `en/common.json`: +```json +"SMOKE": { + "HEADING": "Smoke test page" +} +``` + +Add to `ru/common.json`: +```json +"SMOKE": { + "HEADING": "Страница проверки" +} +``` + +- [ ] **Step 2: Create smoke page** + +`src/routes/[lang]/smoke/page.tsx`: +- Uses `useTranslation()` to render `t("SMOKE.HEADING")` +- Uses `useLogger()` to emit an info log on mount (via `useEffect`) +- Displays the current locale from the URL params + +- [ ] **Step 3: Verify typecheck + lint + test** + +```bash +pnpm typecheck && pnpm lint && pnpm test +``` + +- [ ] **Step 4: Build** + +```bash +pnpm build:standalone +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/i18n/locales/en/common.json src/i18n/locales/ru/common.json src/routes/\[lang\]/smoke/page.tsx +git commit -m "Add smoke route exercising logger, i18n, and locale display" +``` + +--- + +## Exit gate + +- [ ] `pnpm typecheck && pnpm lint && pnpm test` — all pass +- [ ] `pnpm build:standalone` — succeeds +- [ ] `src/routes/[lang]/smoke/page.tsx` exists +- [ ] `src/ui/errors/ErrorBoundary.tsx` exists +- [ ] `src/routes/error/map.ts` exists with `errorToResponse()` exported +- [ ] `src/routes/error/[code]/page.tsx` exists +- [ ] `src/routes/layout.tsx` wraps children with LoggerProvider, ApiClientProvider, ErrorBoundary +- [ ] `src/routes/[lang]/layout.tsx` validates lang and provides I18nProvider