innercontext/frontend/AGENTS.md
Piotr Oleszczyk e29d62f949 feat(frontend): auto-generate TypeScript types from backend OpenAPI schema
Replace manually maintained types in src/lib/types.ts with auto-generated
types from FastAPI's OpenAPI schema using @hey-api/openapi-ts. The bridge
file re-exports generated types with renames, Require<> augmentations for
fields that are optional in the schema but always present in responses, and
manually added relationship fields excluded from OpenAPI.

- Add openapi-ts.config.ts and generate:api npm script
- Generate types into src/lib/api/generated/types.gen.ts
- Rewrite src/lib/types.ts as bridge with re-exports and augmentations
- Fix null vs undefined mismatches in consumer components
- Remove unused manual type definitions from api.ts
- Update AGENTS.md docs with type generation workflow
2026-03-12 09:17:40 +01:00

8 KiB

Frontend

SvelteKit 2 + Svelte 5 (Runes) web UI. Adapter: @sveltejs/adapter-node (required for form actions).

Structure

frontend/src/
├── app.css              # Tailwind v4 theme + editorial design system (1420 lines)
├── app.html             # HTML shell (Cormorant Infant + Manrope fonts)
├── hooks.server.ts      # Paraglide i18n middleware
├── routes/              # SvelteKit file-based routing
│   ├── +layout.svelte   # App shell, sidebar, mobile drawer, domain-based theming
│   ├── +page.svelte     # Dashboard (routines, snapshots, lab results)
│   ├── products/        # List, [id] detail/edit, new, suggest (AI)
│   ├── routines/        # List, [id] detail/edit, new, suggest (AI), grooming-schedule/
│   ├── health/          # medications/ (list, new), lab-results/ (list, new)
│   ├── skin/            # Snapshots list, new (with photo analysis)
│   └── profile/         # User profile
└── lib/
    ├── api.ts           # Typed fetch wrappers (server: PUBLIC_API_BASE, browser: /api)
    ├── types.ts         # Type bridge — re-exports from generated OpenAPI types with augmentations
    ├── api/generated/   # Auto-generated types from backend OpenAPI schema — DO NOT EDIT
    ├── utils.ts         # cn() class merger, bits-ui types
    ├── utils/           # forms.ts (preventIfNotConfirmed), skin-display.ts (label helpers)
    ├── paraglide/       # Generated i18n runtime — DO NOT EDIT
    └── components/
        ├── ui/          # bits-ui primitives: button, card, badge, input, label, select, tabs, table, separator
        ├── forms/       # DRY helpers: SimpleSelect, GroupedSelect, HintCheckbox, LabeledInputField, FormSectionCard, form-classes.ts
        ├── product-form/ # Sectioned form: Basic, Details, Classification, Assessment, Notes
        ├── PageHeader.svelte              # Reusable page header (kicker, title, subtitle, backlink, actions via snippets)
        ├── ProductForm.svelte             # Main product form (tabbed, 737 lines)
        ├── ProductFormAiModal.svelte      # AI text-to-product parsing modal
        ├── FlashMessages.svelte           # Error/success/warning/info alerts
        ├── StructuredErrorDisplay.svelte   # Parses semicolon-separated backend errors into list
        ├── ValidationWarningsAlert.svelte  # LLM validation warnings display
        ├── ReasoningChainViewer.svelte    # AI reasoning chain viewer (collapsible)
        ├── MetadataDebugPanel.svelte      # Token metrics, model info (collapsible)
        ├── AutoFixBadge.svelte            # Auto-fix indicator
        └── LanguageSwitcher.svelte        # i18n locale toggle

Design System

MUST READ: docs/frontend-design-cookbook.md — update when introducing new UI patterns.

  • Typography: Cormorant Infant (display/headings), Manrope (body/UI).
  • Colors: CSS variables in app.css. Domain accents per route: products (green), routines (cyan), skin (orange), profile (blue), health-labs (purple), health-meds (teal).
  • Layout wrappers: .editorial-page, .editorial-hero, .editorial-panel, .editorial-toolbar, .editorial-backlink, .editorial-alert.
  • Page header: Use PageHeader.svelte for consistent title hierarchy, backlinks, and actions.
  • Accent rule: ~10-15% of visual area. Never full backgrounds or body text.
  • Motion: Short purposeful reveals. Always respect prefers-reduced-motion.

Route Patterns

Every page: +page.svelte (UI) + +page.server.ts (load + actions).

Load functions fetch from API, return data:

export const load: PageServerLoad = async () => {
  const data = await getProducts();
  return { products: data };
};

Form actions parse FormData, call API, return result or fail():

export const actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    try {
      const result = await createProduct(payload);
      return { success: true, product: result };
    } catch (e) {
      return fail(500, { error: (e as Error).message });
    }
  }
};

Component Conventions

  • Prefer SimpleSelect / GroupedSelect over bits-ui ui/select unless search/rich popup UX needed.
  • Use form-classes.ts tokens (baseSelectClass, baseTextareaClass) for consistent form styling.
  • Svelte 5 runes: $props(), $state(), $derived(), $effect(), $bindable().
  • Snippet-based composition in PageHeader (actions, meta, children snippets).
  • Compound components: Card → CardHeader, CardContent, CardFooter, etc.

i18n

  • Source messages: frontend/messages/{en,pl}.json.
  • Generated runtime: src/lib/paraglide/ (via Vite plugin).
  • Import: import * as m from '$lib/paraglide/messages.js'.
  • No hardcoded English labels. Use m.* keys. Add new keys to message files if needed.
  • Fallback display: use m.common_unknown() not hardcoded n/a.

API Client

src/lib/api.ts — typed fetch wrappers.

const base = browser ? "/api" : PUBLIC_API_BASE;
// Browser: /api (nginx proxies, strips prefix to backend)
// Server-side (SSR): PUBLIC_API_BASE (http://localhost:8000)

Methods: api.get<T>(), api.post<T>(), api.patch<T>(), api.del(). File upload: analyzeSkinPhotos() uses FormData (not JSON). Error handling: throws Error with .detail from backend response.

Environment

Variable Default Set at
PUBLIC_API_BASE http://localhost:8000 Build time

Production: PUBLIC_API_BASE=http://innercontext.lan/api pnpm build.

Commands

pnpm dev          # Dev server (API proxied to :8000)
pnpm check        # Type check + Svelte validation
pnpm lint         # ESLint
pnpm format       # Prettier
pnpm build        # Production build → build/
pnpm generate:api # Regenerate TypeScript types from backend OpenAPI schema

Anti-Patterns

  • No frontend tests exist. Only linting + type checking.
  • ESLint svelte/no-navigation-without-resolve has ignoreGoto: true workaround (upstream bug sveltejs/eslint-plugin-svelte#1327).
  • src/paraglide/ is a legacy output path — active i18n output is in src/lib/paraglide/.

Type Generation

TypeScript types are auto-generated from the FastAPI backend's OpenAPI schema using @hey-api/openapi-ts.

Workflow

  1. Generate openapi.json from backend: cd backend && uv run python -c "import json; from main import app; print(json.dumps(app.openapi(), indent=2))" > ../frontend/openapi.json
  2. Generate types: cd frontend && pnpm generate:api
  3. Output lands in src/lib/api/generated/types.gen.tsnever edit this file directly.

Architecture

  • Generated types: src/lib/api/generated/types.gen.ts — raw OpenAPI types, auto-generated.
  • Bridge file: src/lib/types.ts — re-exports from generated types with:
    • Renames: ProductWithInventoryProduct, ProductListItemProductSummary, UserProfilePublicUserProfile, SkinConditionSnapshotPublicSkinConditionSnapshot.
    • Require<T, K> augmentations: Fields with default_factory in SQLModel are optional in OpenAPI but always present in API responses (e.g. id, created_at, updated_at, targets, inventory).
    • Relationship fields: SQLModel Relationship() fields are excluded from OpenAPI schema. Added manually: MedicationEntry.usage_history, Routine.steps, RoutineStep.product, ProductInventory.product, Product.inventory (with augmented ProductInventory).
    • Manual types: PriceTierSource, ShoppingPriority — inline literals in backend, not named in OpenAPI.
  • Canonical import: Always import type { ... } from '$lib/types' — never import from $lib/api/generated directly.

When to regenerate

  • After adding/modifying backend models or response schemas.
  • After adding/modifying API endpoints that change the OpenAPI spec.
  • After updating the bridge file, run pnpm check to verify type compatibility.