Files
home-automation/frontend/README.md
T
tliu93 6cfeb2b865 M2-T06: scaffold React SPA frontend with typed OpenAPI client
- Vite + React 18 + TypeScript + Mantine + TanStack Query + react-router-dom
- typed client: openapi-typescript -> src/api/schema.d.ts (committed), openapi-fetch
- fetch wrapper middleware: cookies, X-CSRF-Token on writes, 401 -> /login,
  non-401 errors carry parsed JSON body
- SessionProvider/useSession (GET /api/session), ProtectedRoute skeleton
- app shell (Mantine + router) with placeholder login/home/config pages + gear nav
- dev proxy to FastAPI; vitest smoke test; frontend README
- npm scripts: dev/build/preview/lint/typecheck/test/codegen
2026-06-13 15:20:50 +02:00

7.0 KiB
Raw Blame History

Home Automation — Frontend

React SPA for the home-automation backend. Built with Vite + React 18 + TypeScript. Scaffolded in M2-T06; feature pages filled in by T07T10.

Stack

Layer Library Version
Build Vite 6.x
UI framework React 18.x
Language TypeScript 5.x
Component library Mantine 7.x
Data fetching TanStack Query 5.x
Routing react-router-dom 6.x
API client codegen openapi-typescript 7.x
API client runtime openapi-fetch 0.17.x
Testing Vitest + @testing-library/react 4.x / 14.x

npm Scripts

Command What it does
npm run dev Start Vite dev server (with backend proxy — see below)
npm run build tsc -b && vite build — type-check then build to dist/
npm run preview Serve the built dist/ locally
npm run lint ESLint (flat config, React + TypeScript rules)
npm run typecheck tsc --noEmit — type-check without emitting files
npm run test Vitest (run once, no watch)
npm run codegen Regenerate src/api/schema.d.ts from ../openapi/openapi.json

All frontend gates must pass before any task is considered done:

npm run codegen
npm run lint
npm run typecheck
npm run test
npm run build   # must produce dist/

Directory Structure

frontend/
├── index.html                  Vite entry HTML
├── vite.config.ts              Vite + Vitest config; dev proxy
├── tsconfig.json               References tsconfig.app.json + tsconfig.node.json
├── tsconfig.app.json           App source TS config (strict, react-jsx)
├── tsconfig.node.json          Vite config TS config
├── eslint.config.js            Flat ESLint config (React + TypeScript rules)
├── package.json                Dependencies + npm scripts
├── package-lock.json           Lockfile (committed; CI uses npm ci)
└── src/
    ├── main.tsx                Entry point; mounts <App> into #root
    ├── App.tsx                 Provider stack + route tree (MantineProvider → QueryClient → Router → SessionProvider)
    ├── vite-env.d.ts           /// <reference types="vite/client" /> for CSS imports
    ├── test-setup.ts           Vitest global setup (@testing-library/jest-dom)
    ├── api/
    │   ├── schema.d.ts         AUTO-GENERATED from openapi/openapi.json (committed)
    │   ├── client.ts           openapi-fetch client + CSRF/cookie/401 middleware
    │   └── csrf.ts             Module-level CSRF token holder (setCsrfToken / getCsrfToken)
    ├── auth/
    │   ├── SessionProvider.tsx  TanStack Query against GET /api/session; exposes useSession()
    │   └── ProtectedRoute.tsx   Redirects to /login when unauthenticated
    └── pages/
        ├── LoginPage.tsx       Placeholder → T07 builds the real form
        ├── HomePage.tsx        Placeholder → T09 builds the map/heatmap view
        └── ConfigPage.tsx      Placeholder → T08 builds the config editor

Dev Proxy (local development)

npm run dev starts Vite on port 5173. The Vite config proxies API/auth paths to the FastAPI backend running on port 8000:

Proxied path Backend URL
/api/* http://localhost:8000
/login http://localhost:8000
/logout http://localhost:8000
/static/* http://localhost:8000
/docs http://localhost:8000
/openapi.json http://localhost:8000

To develop locally:

  1. Start the backend: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
  2. Start the frontend: cd frontend && npm run dev
  3. Open http://localhost:5173 — the app proxies all API calls to the backend.

Since the dev server proxies the session cookie path, auth flows work exactly as they would in the deployed (same-origin) setup.

Adding a New Page + Typed Query

This is the pattern every task T07T10 follows to wire up a real page:

1. Run codegen (if the OpenAPI contract changed)

npm run codegen

The generated src/api/schema.d.ts is committed to the repo. CI enforces that the file is in sync with openapi/openapi.json via:

npm run codegen && git diff --exit-code frontend/src/api/schema.d.ts

2. Import the typed client

// src/pages/SomePage.tsx
import apiClient from '../api/client'

3. Write a typed TanStack Query

import { useQuery } from '@tanstack/react-query'
import apiClient from '../api/client'

function usePooRecords(limit = 100) {
  return useQuery({
    queryKey: ['poo', { limit }],
    queryFn: async () => {
      const res = await apiClient.GET('/api/poo', { params: { query: { limit } } })
      // res.data is typed as PooResponse | undefined
      // On non-2xx the middleware throws ApiError; TanStack Query catches it.
      return res.data
    },
  })
}

The params.query and params.path objects are fully typed from schema.d.ts. TypeScript will error if you pass unknown query params or mistype a path param.

4. Write a typed mutation (write request)

import { useMutation, useQueryClient } from '@tanstack/react-query'
import apiClient from '../api/client'

function useDeletePoo() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: (timestamp: string) =>
      apiClient.DELETE('/api/poo/{timestamp}', {
        params: { path: { timestamp } },
      }),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['poo'] }),
  })
}

The middleware (src/api/client.ts) automatically injects the X-CSRF-Token header on all non-GET/HEAD requests (sourced from getCsrfToken()). You do not need to handle CSRF manually in page code.

5. Add the route in App.tsx

// App.tsx
import { SomePage } from './pages/SomePage'

// Inside <Routes>:
<Route path="/some-path" element={<SomePage />} />
// or, if protected:
<Route
  element={
    <ProtectedRoute>
      <AppLayout />
    </ProtectedRoute>
  }
>
  <Route path="/some-path" element={<SomePage />} />
</Route>

OpenAPI codegen + CI sync rule

src/api/schema.d.ts is committed to the repository (not gitignored).

Rule: whenever openapi/openapi.json changes (any backend task that modifies a route or schema), CI must run:

cd frontend && npm run codegen
git diff --exit-code frontend/src/api/schema.d.ts

If the file has changed but the new version was not committed, CI fails.

To update manually after a backend change:

cd frontend
npm run codegen
git add src/api/schema.d.ts
git commit -m "M2-Txx: update generated OpenAPI types"

Production Build

The production build (npm run build) writes static files to frontend/dist/. In the deployed setup (M2-T11 onwards), FastAPI serves dist/ as a static directory and falls back to dist/index.html for all non-/api paths, enabling client-side routing with deep links.

The multi-stage Dockerfile (M2-T12) builds the frontend in a Node container and copies only dist/ into the Python image — the production image does not contain Node or npm.