Files
dofus-manager/docs/architecture.md
2026-01-19 08:52:38 +01:00

86 KiB

Dofus Manager - Architecture Document

Table of Contents

  1. Introduction
  2. High-Level Architecture
  3. Technology Stack
  4. Data Models
  5. API Specification
  6. Components Architecture
  7. External APIs
  8. Core Workflows
  9. Database Schema
  10. Frontend Architecture
  11. Backend Architecture
  12. Project Structure
  13. Development Workflow
  14. Deployment Architecture
  15. Security & Performance
  16. Testing Strategy
  17. Coding Standards
  18. Error Handling
  19. Monitoring & Observability
  20. Architecture Checklist

1. Introduction

Project Overview

Dofus Manager est une application web personnelle conçue pour gérer efficacement plus de 60 personnages du MMORPG Dofus, répartis sur plusieurs comptes. L'application centralise le suivi des progressions (quêtes, donjons, succès) et permet une gestion optimisée via des équipes.

Goals

  • Centralisation : Un point unique pour gérer tous les personnages et comptes
  • Suivi des progressions : Tracker l'avancement des quêtes, donjons et succès
  • Gestion par équipes : Organiser les personnages en équipes pour faciliter le suivi
  • Efficacité : Interface rapide avec opérations bulk et filtres avancés

Scope

In Scope Out of Scope
Gestion des personnages (CRUD) Intégration directe avec le jeu
Gestion des comptes Multi-utilisateurs
Suivi des progressions Application mobile native
Gestion des équipes Synchronisation automatique
Import données DofusDB Fonctionnalités sociales
Interface responsive Mode hors-ligne

Non-Functional Requirements

Requirement Target
Performance < 200ms pour les opérations courantes
Disponibilité 99% uptime (usage personnel)
Sécurité Authentification requise, données protégées
Scalabilité Support de 100+ personnages
Maintenabilité Code typé, tests automatisés

2. High-Level Architecture

System Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              DOFUS MANAGER                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                         CLIENT (Browser)                             │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐  │    │
│  │  │   React     │  │  TanStack   │  │  TanStack   │  │  Zustand   │  │    │
│  │  │   Components│  │   Router    │  │   Query     │  │  (UI State)│  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘  │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                    │                                         │
│                                    │ Server Functions (RPC)                  │
│                                    ▼                                         │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                     SERVER (TanStack Start)                          │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐  │    │
│  │  │   Server    │  │    Zod      │  │   Prisma    │  │  node-cache│  │    │
│  │  │  Functions  │  │  Validation │  │    ORM      │  │  (Cache)   │  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘  │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                    │                                         │
│                                    ▼                                         │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                        DATABASE (PostgreSQL)                         │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐  │    │
│  │  │  Characters │  │   Accounts  │  │   Teams     │  │Progressions│  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘  │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                      EXTERNAL SERVICES                               │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │                    DofusDB API                               │    │    │
│  │  │            (Classes, Quêtes, Donjons, Succès)                │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Data Flow

User Action → React Component → TanStack Query → Server Function → Prisma → PostgreSQL
     ↑                                                                           │
     └───────────────────────── Response ────────────────────────────────────────┘

Key Architectural Decisions

Decision Choice Rationale
Full-stack Framework TanStack Start Type-safe, modern React, server functions
Database PostgreSQL Robust, JSON support, full-text search
ORM Prisma Type-safe queries, excellent DX
State Management TanStack Query + Zustand Server state vs UI state separation
Styling Tailwind + shadcn/ui Rapid development, consistent design
Deployment Docker + Traefik Easy SSL, reverse proxy, scalable

3. Technology Stack

Frontend

Technology Version Purpose
React 19.x UI library
TanStack Router 1.x Type-safe routing
TanStack Query 5.x Server state management
Zustand 5.x Client state management
Tailwind CSS 4.x Utility-first styling
shadcn/ui latest Component library
Lucide React latest Icons
React Hook Form 7.x Form management
Zod 3.x Schema validation

Backend

Technology Version Purpose
TanStack Start 1.x Full-stack framework
Prisma 6.x Database ORM
Zod 3.x Input validation
node-cache 5.x In-memory caching
Pino 9.x Structured logging
bcryptjs 2.x Password hashing

Database

Technology Version Purpose
PostgreSQL 16.x Primary database

DevOps

Technology Purpose
Docker Containerization
Docker Compose Local development & deployment
Traefik Reverse proxy, SSL termination
GitLab CI CI/CD pipeline

Development Tools

Tool Purpose
TypeScript Type safety
Biome Linting & formatting
Vitest Unit testing
Playwright E2E testing

4. Data Models

Entity Relationship Diagram

┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│    User     │       │   Account   │       │  Character  │
├─────────────┤       ├─────────────┤       ├─────────────┤
│ id          │       │ id          │       │ id          │
│ email       │──┐    │ name        │──┐    │ name        │
│ password    │  │    │ userId    ◄─┘  │    │ level       │
│ createdAt   │  └───►│ createdAt   │  └───►│ classId     │
│ updatedAt   │       │ updatedAt   │       │ serverId    │
└─────────────┘       └─────────────┘       │ accountId ◄─┘
                                            │ createdAt   │
                                            │ updatedAt   │
                                            └──────┬──────┘
                                                   │
                      ┌────────────────────────────┼────────────────────────────┐
                      │                            │                            │
                      ▼                            ▼                            ▼
              ┌─────────────┐              ┌─────────────┐              ┌─────────────┐
              │    Team     │              │TeamMember   │              │ CharProgress│
              ├─────────────┤              ├─────────────┤              ├─────────────┤
              │ id          │◄─────────────│ teamId      │              │ id          │
              │ name        │              │ characterId │◄─────────────│ characterId │
              │ type        │              │ joinedAt    │              │ progressId  │
              │ userId      │              └─────────────┘              │ completed   │
              │ createdAt   │                                           │ completedAt │
              │ updatedAt   │                                           └─────────────┘
              └─────────────┘                                                  │
                                                                               │
                                                                               ▼
                                                                       ┌─────────────┐
                                                                       │ Progression │
                                                                       ├─────────────┤
                                                                       │ id          │
                                                                       │ name        │
                                                                       │ type        │
                                                                       │ category    │
                                                                       │ dofusDbId   │
                                                                       └─────────────┘

Core Entities

User

interface User {
  id: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
  updatedAt: Date;
}

Account

interface Account {
  id: string;
  name: string;
  userId: string;
  createdAt: Date;
  updatedAt: Date;
}

Character

interface Character {
  id: string;
  name: string;
  level: number;
  classId: number;
  className: string;
  serverId: number;
  serverName: string;
  accountId: string;
  createdAt: Date;
  updatedAt: Date;
}

Team

interface Team {
  id: string;
  name: string;
  type: 'MAIN' | 'SECONDARY' | 'CUSTOM';
  userId: string;
  createdAt: Date;
  updatedAt: Date;
}

Progression

interface Progression {
  id: string;
  name: string;
  type: 'QUEST' | 'DUNGEON' | 'ACHIEVEMENT' | 'DOFUS';
  category: string;
  dofusDbId: number | null;
}

CharacterProgression

interface CharacterProgression {
  id: string;
  characterId: string;
  progressionId: string;
  completed: boolean;
  completedAt: Date | null;
}

5. API Specification

Server Functions Pattern

TanStack Start utilise des "Server Functions" - des fonctions RPC type-safe qui s'exécutent côté serveur.

// Définition
import { createServerFn } from '@tanstack/react-start/server';
import { z } from 'zod';

const inputSchema = z.object({
  name: z.string().min(1),
});

export const createCharacter = createServerFn({ method: 'POST' })
  .validator((data: unknown) => inputSchema.parse(data))
  .handler(async ({ data }) => {
    // Logique serveur avec accès DB
    return await db.character.create({ data });
  });

// Utilisation côté client
const result = await createCharacter({ data: { name: 'MyChar' } });

API Endpoints (Server Functions)

Characters

Function Method Description
getCharacters GET Liste tous les personnages avec filtres
getCharacter GET Récupère un personnage par ID
createCharacter POST Crée un nouveau personnage
updateCharacter POST Met à jour un personnage
deleteCharacter POST Supprime un personnage
bulkUpdateCharacters POST Met à jour plusieurs personnages
bulkDeleteCharacters POST Supprime plusieurs personnages

Accounts

Function Method Description
getAccounts GET Liste tous les comptes
getAccount GET Récupère un compte par ID
createAccount POST Crée un nouveau compte
updateAccount POST Met à jour un compte
deleteAccount POST Supprime un compte

Teams

Function Method Description
getTeams GET Liste toutes les équipes
getTeam GET Récupère une équipe avec ses membres
createTeam POST Crée une nouvelle équipe
updateTeam POST Met à jour une équipe
deleteTeam POST Supprime une équipe
addTeamMembers POST Ajoute des personnages à une équipe
removeTeamMembers POST Retire des personnages d'une équipe

Progressions

Function Method Description
getProgressions GET Liste toutes les progressions
getCharacterProgressions GET Progressions d'un personnage
updateCharacterProgression POST Met à jour une progression
bulkUpdateProgressions POST Met à jour plusieurs progressions
syncFromDofusDb POST Synchronise depuis DofusDB

Auth

Function Method Description
login POST Authentification
logout POST Déconnexion
getSession GET Récupère la session courante

Validation Schemas

// src/lib/schemas/character.ts
import { z } from 'zod';

export const createCharacterSchema = z.object({
  name: z.string().min(2).max(50),
  level: z.number().int().min(1).max(200),
  classId: z.number().int().positive(),
  serverId: z.number().int().positive(),
  accountId: z.string().uuid(),
});

export const updateCharacterSchema = createCharacterSchema.partial();

export const characterFiltersSchema = z.object({
  search: z.string().optional(),
  classIds: z.array(z.number()).optional(),
  serverIds: z.array(z.number()).optional(),
  accountIds: z.array(z.string()).optional(),
  minLevel: z.number().optional(),
  maxLevel: z.number().optional(),
  page: z.number().int().positive().default(1),
  limit: z.number().int().min(1).max(100).default(20),
  sortBy: z.enum(['name', 'level', 'class', 'server']).default('name'),
  sortOrder: z.enum(['asc', 'desc']).default('asc'),
});

6. Components Architecture

Component Hierarchy

src/components/
├── ui/                    # shadcn/ui primitives
│   ├── button.tsx
│   ├── input.tsx
│   ├── dialog.tsx
│   ├── table.tsx
│   ├── select.tsx
│   ├── checkbox.tsx
│   ├── tabs.tsx
│   ├── card.tsx
│   ├── badge.tsx
│   ├── progress.tsx
│   └── ...
│
├── layout/               # Layout components
│   ├── app-shell.tsx     # Main layout wrapper
│   ├── app-sidebar.tsx   # Navigation sidebar
│   └── app-header.tsx    # Top header with breadcrumbs
│
├── characters/           # Character feature components
│   ├── character-list.tsx
│   ├── character-card.tsx
│   ├── character-form.tsx
│   ├── character-filters.tsx
│   └── character-bulk-actions.tsx
│
├── accounts/             # Account feature components
│   ├── account-list.tsx
│   ├── account-card.tsx
│   └── account-form.tsx
│
├── teams/                # Team feature components
│   ├── team-list.tsx
│   ├── team-detail.tsx
│   ├── team-form.tsx
│   └── team-member-selector.tsx
│
├── progressions/         # Progression feature components
│   ├── progression-section.tsx
│   ├── progression-tracker.tsx
│   └── progression-filters.tsx
│
└── shared/               # Shared components
    ├── data-table.tsx
    ├── search-input.tsx
    ├── pagination.tsx
    ├── confirmation-modal.tsx
    ├── loading-spinner.tsx
    └── error-boundary.tsx

Component Design Principles

  1. Composition over inheritance - Prefer composing smaller components
  2. Props interface first - Define TypeScript interfaces for all props
  3. Controlled components - Parent manages state when needed
  4. Accessible by default - Use semantic HTML, ARIA attributes
  5. Responsive design - Mobile-first approach with Tailwind

7. External APIs

DofusDB API Integration

Base URL: https://api.dofusdb.fr

Endpoints Used

Endpoint Purpose
GET /classes Liste des classes de personnages
GET /servers Liste des serveurs
GET /quests Liste des quêtes
GET /dungeons Liste des donjons
GET /achievements Liste des succès

Integration Service

// src/lib/server/dofusdb.ts
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 3600 }); // 1 hour cache
const BASE_URL = 'https://api.dofusdb.fr';

interface DofusDbOptions {
  lang?: 'fr' | 'en';
  limit?: number;
}

export async function fetchFromDofusDb<T>(
  endpoint: string,
  options: DofusDbOptions = {}
): Promise<T> {
  const { lang = 'fr', limit = 100 } = options;
  const cacheKey = `dofusdb:${endpoint}:${lang}:${limit}`;

  // Check cache
  const cached = cache.get<T>(cacheKey);
  if (cached) return cached;

  // Fetch from API
  const url = new URL(endpoint, BASE_URL);
  url.searchParams.set('$lang', lang);
  url.searchParams.set('$limit', limit.toString());

  const response = await fetch(url.toString(), {
    headers: { 'Accept': 'application/json' },
  });

  if (!response.ok) {
    throw new Error(`DofusDB API error: ${response.status}`);
  }

  const data = await response.json();
  cache.set(cacheKey, data);

  return data as T;
}

// Specific fetchers
export const getClasses = () => fetchFromDofusDb<DofusClass[]>('/classes');
export const getServers = () => fetchFromDofusDb<DofusServer[]>('/servers');
export const getQuests = () => fetchFromDofusDb<DofusQuest[]>('/quests');
export const getDungeons = () => fetchFromDofusDb<DofusDungeon[]>('/dungeons');

8. Core Workflows

Character Creation Flow

┌─────────────────────────────────────────────────────────────────┐
│                    CHARACTER CREATION FLOW                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │  Click   │───▶│  Open    │───▶│  Fill    │───▶│ Validate │  │
│  │  "Add"   │    │  Modal   │    │   Form   │    │   (Zod)  │  │
│  └──────────┘    └──────────┘    └──────────┘    └────┬─────┘  │
│                                                       │         │
│                                        ┌──────────────┴───┐     │
│                                        ▼                  ▼     │
│                                  ┌──────────┐      ┌──────────┐ │
│                                  │  Valid   │      │ Invalid  │ │
│                                  └────┬─────┘      └────┬─────┘ │
│                                       │                 │       │
│                                       ▼                 ▼       │
│                                ┌────────────┐    ┌────────────┐ │
│                                │   Server   │    │   Show     │ │
│                                │  Function  │    │   Errors   │ │
│                                └─────┬──────┘    └────────────┘ │
│                                      │                          │
│                                      ▼                          │
│                               ┌────────────┐                    │
│                               │   Prisma   │                    │
│                               │   Create   │                    │
│                               └─────┬──────┘                    │
│                                     │                           │
│                                     ▼                           │
│                              ┌────────────┐                     │
│                              │ Invalidate │                     │
│                              │   Query    │                     │
│                              └─────┬──────┘                     │
│                                    │                            │
│                                    ▼                            │
│                             ┌────────────┐                      │
│                             │   Toast    │                      │
│                             │  Success   │                      │
│                             └────────────┘                      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Progression Update Flow

┌─────────────────────────────────────────────────────────────────┐
│                   PROGRESSION UPDATE FLOW                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐                                               │
│  │ Toggle Check │                                               │
│  └──────┬───────┘                                               │
│         │                                                        │
│         ▼                                                        │
│  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐    │
│  │  Optimistic  │────▶│   Server     │────▶│   Database   │    │
│  │   Update     │     │   Function   │     │    Update    │    │
│  └──────────────┘     └──────────────┘     └──────┬───────┘    │
│         │                                         │             │
│         │                              ┌──────────┴─────┐       │
│         │                              ▼                ▼       │
│         │                       ┌──────────┐     ┌──────────┐  │
│         │                       │ Success  │     │  Error   │  │
│         │                       └────┬─────┘     └────┬─────┘  │
│         │                            │                │        │
│         │                            ▼                ▼        │
│         │                     ┌──────────┐     ┌──────────┐   │
│         └────────────────────▶│  Confirm │     │ Rollback │   │
│                               │   State  │     │  + Toast │   │
│                               └──────────┘     └──────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Bulk Operations Flow

┌─────────────────────────────────────────────────────────────────┐
│                    BULK OPERATIONS FLOW                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │  Select  │───▶│  Choose  │───▶│  Confirm │───▶│ Process  │  │
│  │  Items   │    │  Action  │    │  Modal   │    │  Batch   │  │
│  └──────────┘    └──────────┘    └──────────┘    └────┬─────┘  │
│                                                       │         │
│                                                       ▼         │
│                                                ┌────────────┐   │
│                                                │  Loading   │   │
│                                                │   State    │   │
│                                                └─────┬──────┘   │
│                                                      │          │
│                                        ┌─────────────┴────────┐ │
│                                        ▼                      ▼ │
│                                 ┌────────────┐        ┌────────┐│
│                                 │  Success   │        │ Partial││
│                                 │   Toast    │        │ Failure││
│                                 └─────┬──────┘        └───┬────┘│
│                                       │                   │     │
│                                       ▼                   ▼     │
│                                ┌────────────┐      ┌──────────┐ │
│                                │ Invalidate │      │  Show    │ │
│                                │  Queries   │      │  Errors  │ │
│                                └────────────┘      └──────────┘ │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

9. Database Schema

Prisma Schema

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ============================================
// USER & AUTHENTICATION
// ============================================

model User {
  id           String    @id @default(cuid())
  email        String    @unique
  passwordHash String    @map("password_hash")
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")

  accounts Account[]
  teams    Team[]
  sessions Session[]

  @@map("users")
}

model Session {
  id        String   @id @default(cuid())
  userId    String   @map("user_id")
  expiresAt DateTime @map("expires_at")
  createdAt DateTime @default(now()) @map("created_at")

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([expiresAt])
  @@map("sessions")
}

// ============================================
// CORE ENTITIES
// ============================================

model Account {
  id        String   @id @default(cuid())
  name      String
  userId    String   @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user       User        @relation(fields: [userId], references: [id], onDelete: Cascade)
  characters Character[]

  @@unique([userId, name])
  @@index([userId])
  @@map("accounts")
}

model Character {
  id         String   @id @default(cuid())
  name       String
  level      Int      @default(1)
  classId    Int      @map("class_id")
  className  String   @map("class_name")
  serverId   Int      @map("server_id")
  serverName String   @map("server_name")
  accountId  String   @map("account_id")
  createdAt  DateTime @default(now()) @map("created_at")
  updatedAt  DateTime @updatedAt @map("updated_at")

  account      Account                @relation(fields: [accountId], references: [id], onDelete: Cascade)
  teamMembers  TeamMember[]
  progressions CharacterProgression[]

  @@unique([accountId, name])
  @@index([accountId])
  @@index([classId])
  @@index([serverId])
  @@index([level])
  @@map("characters")
}

// ============================================
// TEAMS
// ============================================

enum TeamType {
  MAIN
  SECONDARY
  CUSTOM
}

model Team {
  id        String   @id @default(cuid())
  name      String
  type      TeamType @default(CUSTOM)
  userId    String   @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user    User         @relation(fields: [userId], references: [id], onDelete: Cascade)
  members TeamMember[]

  @@unique([userId, name])
  @@index([userId])
  @@map("teams")
}

model TeamMember {
  id          String   @id @default(cuid())
  teamId      String   @map("team_id")
  characterId String   @map("character_id")
  joinedAt    DateTime @default(now()) @map("joined_at")

  team      Team      @relation(fields: [teamId], references: [id], onDelete: Cascade)
  character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)

  @@unique([teamId, characterId])
  @@index([teamId])
  @@index([characterId])
  @@map("team_members")
}

// ============================================
// PROGRESSIONS
// ============================================

enum ProgressionType {
  QUEST
  DUNGEON
  ACHIEVEMENT
  DOFUS
}

model Progression {
  id        String          @id @default(cuid())
  name      String
  type      ProgressionType
  category  String
  dofusDbId Int?            @unique @map("dofusdb_id")

  characterProgressions CharacterProgression[]

  @@index([type])
  @@index([category])
  @@map("progressions")
}

model CharacterProgression {
  id            String    @id @default(cuid())
  characterId   String    @map("character_id")
  progressionId String    @map("progression_id")
  completed     Boolean   @default(false)
  completedAt   DateTime? @map("completed_at")

  character   Character   @relation(fields: [characterId], references: [id], onDelete: Cascade)
  progression Progression @relation(fields: [progressionId], references: [id], onDelete: Cascade)

  @@unique([characterId, progressionId])
  @@index([characterId])
  @@index([progressionId])
  @@index([completed])
  @@map("character_progressions")
}

10. Frontend Architecture

State Management Strategy

┌─────────────────────────────────────────────────────────────────┐
│                    STATE MANAGEMENT                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   TanStack Query                         │    │
│  │              (Server State / Cache)                      │    │
│  │                                                          │    │
│  │  • Characters list                                       │    │
│  │  • Accounts data                                         │    │
│  │  • Teams & members                                       │    │
│  │  • Progressions                                          │    │
│  │  • DofusDB reference data                                │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                      Zustand                             │    │
│  │               (Client/UI State)                          │    │
│  │                                                          │    │
│  │  • Selected items (multi-select)                         │    │
│  │  • UI preferences (sidebar collapsed, etc.)              │    │
│  │  • Filter states                                         │    │
│  │  • Modal open/close states                               │    │
│  │  • Theme preference                                      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                   React State                            │    │
│  │              (Component-local State)                     │    │
│  │                                                          │    │
│  │  • Form inputs                                           │    │
│  │  • Hover/focus states                                    │    │
│  │  • Animation states                                      │    │
│  │  • Temporary UI states                                   │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

TanStack Query Setup

// src/lib/client/query-client.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 30,   // 30 minutes
      retry: 1,
      refetchOnWindowFocus: false,
    },
    mutations: {
      onError: (error) => {
        console.error('Mutation error:', error);
      },
    },
  },
});

// Query keys factory
export const queryKeys = {
  characters: {
    all: ['characters'] as const,
    list: (filters: CharacterFilters) => [...queryKeys.characters.all, 'list', filters] as const,
    detail: (id: string) => [...queryKeys.characters.all, 'detail', id] as const,
  },
  accounts: {
    all: ['accounts'] as const,
    list: () => [...queryKeys.accounts.all, 'list'] as const,
    detail: (id: string) => [...queryKeys.accounts.all, 'detail', id] as const,
  },
  teams: {
    all: ['teams'] as const,
    list: () => [...queryKeys.teams.all, 'list'] as const,
    detail: (id: string) => [...queryKeys.teams.all, 'detail', id] as const,
  },
  progressions: {
    all: ['progressions'] as const,
    list: (type?: ProgressionType) => [...queryKeys.progressions.all, 'list', type] as const,
    byCharacter: (characterId: string) => [...queryKeys.progressions.all, 'character', characterId] as const,
  },
} as const;

Zustand Store

// src/lib/client/stores/ui-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UIState {
  // Sidebar
  sidebarCollapsed: boolean;
  toggleSidebar: () => void;

  // Theme
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;

  // Selection (for bulk actions)
  selectedCharacterIds: string[];
  selectCharacter: (id: string) => void;
  deselectCharacter: (id: string) => void;
  selectAllCharacters: (ids: string[]) => void;
  clearSelection: () => void;
}

export const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      // Sidebar
      sidebarCollapsed: false,
      toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),

      // Theme
      theme: 'dark',
      setTheme: (theme) => set({ theme }),

      // Selection
      selectedCharacterIds: [],
      selectCharacter: (id) =>
        set((state) => ({
          selectedCharacterIds: [...state.selectedCharacterIds, id]
        })),
      deselectCharacter: (id) =>
        set((state) => ({
          selectedCharacterIds: state.selectedCharacterIds.filter((i) => i !== id)
        })),
      selectAllCharacters: (ids) => set({ selectedCharacterIds: ids }),
      clearSelection: () => set({ selectedCharacterIds: [] }),
    }),
    {
      name: 'dofus-manager-ui',
      partialize: (state) => ({
        sidebarCollapsed: state.sidebarCollapsed,
        theme: state.theme,
      }),
    }
  )
);

Routing Structure

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { AppShell } from '@/components/layout/app-shell';

export const Route = createRootRoute({
  component: () => (
    <AppShell>
      <Outlet />
    </AppShell>
  ),
});

// Route tree
/*
src/routes/
├── __root.tsx           # Root layout with AppShell
├── index.tsx            # / → Dashboard
├── characters/
│   ├── index.tsx        # /characters → List
│   └── $id.tsx          # /characters/:id → Detail
├── accounts/
│   ├── index.tsx        # /accounts → List
│   └── $id.tsx          # /accounts/:id → Detail
├── teams/
│   ├── index.tsx        # /teams → List
│   └── $id.tsx          # /teams/:id → Detail
├── progressions/
│   └── index.tsx        # /progressions → Tracker
└── settings/
    └── index.tsx        # /settings → Settings
*/

11. Backend Architecture

Server Functions Organization

src/server/
├── functions/
│   ├── auth.ts           # Authentication functions
│   ├── characters.ts     # Character CRUD
│   ├── accounts.ts       # Account CRUD
│   ├── teams.ts          # Team management
│   ├── progressions.ts   # Progression tracking
│   └── dofusdb.ts        # DofusDB sync
│
├── middleware/
│   └── auth.ts           # Authentication middleware
│
└── index.ts              # Export all functions

Server Function Example

// src/server/functions/characters.ts
import { createServerFn } from '@tanstack/react-start/server';
import { z } from 'zod';
import { db } from '@/lib/server/db';
import { requireAuth } from '@/server/middleware/auth';

// Schemas
const createCharacterSchema = z.object({
  name: z.string().min(2).max(50),
  level: z.number().int().min(1).max(200),
  classId: z.number().int().positive(),
  className: z.string(),
  serverId: z.number().int().positive(),
  serverName: z.string(),
  accountId: z.string().cuid(),
});

const characterFiltersSchema = z.object({
  search: z.string().optional(),
  classIds: z.array(z.number()).optional(),
  serverIds: z.array(z.number()).optional(),
  accountIds: z.array(z.string()).optional(),
  page: z.number().int().positive().default(1),
  limit: z.number().int().min(1).max(100).default(20),
});

// Functions
export const getCharacters = createServerFn({ method: 'GET' })
  .validator((data: unknown) => characterFiltersSchema.parse(data))
  .handler(async ({ data }) => {
    const session = await requireAuth();
    const { search, classIds, serverIds, accountIds, page, limit } = data;

    const where = {
      account: { userId: session.userId },
      ...(search && {
        name: { contains: search, mode: 'insensitive' as const },
      }),
      ...(classIds?.length && { classId: { in: classIds } }),
      ...(serverIds?.length && { serverId: { in: serverIds } }),
      ...(accountIds?.length && { accountId: { in: accountIds } }),
    };

    const [characters, total] = await Promise.all([
      db.character.findMany({
        where,
        include: { account: { select: { id: true, name: true } } },
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { name: 'asc' },
      }),
      db.character.count({ where }),
    ]);

    return {
      characters,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  });

export const createCharacter = createServerFn({ method: 'POST' })
  .validator((data: unknown) => createCharacterSchema.parse(data))
  .handler(async ({ data }) => {
    const session = await requireAuth();

    // Verify account belongs to user
    const account = await db.account.findFirst({
      where: { id: data.accountId, userId: session.userId },
    });

    if (!account) {
      throw new Error('Account not found');
    }

    return db.character.create({ data });
  });

export const bulkDeleteCharacters = createServerFn({ method: 'POST' })
  .validator((data: unknown) => z.object({ ids: z.array(z.string().cuid()) }).parse(data))
  .handler(async ({ data }) => {
    const session = await requireAuth();

    const result = await db.character.deleteMany({
      where: {
        id: { in: data.ids },
        account: { userId: session.userId },
      },
    });

    return { deleted: result.count };
  });

Authentication Middleware

// src/server/middleware/auth.ts
import { getWebRequest } from '@tanstack/react-start/server';
import { db } from '@/lib/server/db';

interface Session {
  userId: string;
  sessionId: string;
}

export async function requireAuth(): Promise<Session> {
  const request = getWebRequest();
  const sessionId = request.headers.get('cookie')
    ?.split(';')
    .find(c => c.trim().startsWith('session='))
    ?.split('=')[1];

  if (!sessionId) {
    throw new Error('Unauthorized');
  }

  const session = await db.session.findUnique({
    where: { id: sessionId },
    include: { user: true },
  });

  if (!session || session.expiresAt < new Date()) {
    throw new Error('Session expired');
  }

  return {
    userId: session.userId,
    sessionId: session.id,
  };
}

export async function getOptionalAuth(): Promise<Session | null> {
  try {
    return await requireAuth();
  } catch {
    return null;
  }
}

Caching Strategy

// src/lib/server/cache.ts
import NodeCache from 'node-cache';

// Different TTLs for different data types
const caches = {
  // Reference data (rarely changes)
  reference: new NodeCache({ stdTTL: 3600, checkperiod: 600 }), // 1 hour

  // User data (changes more frequently)
  user: new NodeCache({ stdTTL: 300, checkperiod: 60 }), // 5 minutes
};

export const referenceCache = {
  get: <T>(key: string): T | undefined => caches.reference.get(key),
  set: <T>(key: string, value: T): boolean => caches.reference.set(key, value),
  del: (key: string): number => caches.reference.del(key),
  flush: (): void => caches.reference.flushAll(),
};

export const userCache = {
  get: <T>(key: string): T | undefined => caches.user.get(key),
  set: <T>(key: string, value: T): boolean => caches.user.set(key, value),
  del: (key: string | string[]): number => caches.user.del(key),
  flush: (): void => caches.user.flushAll(),

  // Helper to invalidate all cache for a user
  invalidateUser: (userId: string): void => {
    const keys = caches.user.keys().filter(k => k.startsWith(`user:${userId}:`));
    caches.user.del(keys);
  },
};

12. Project Structure

dofus-manager/
├── .github/
│   └── workflows/
│       └── ci.yml
│
├── docker/
│   ├── Dockerfile
│   └── docker-compose.yml
│
├── docs/
│   ├── prd.md
│   ├── front-end-spec.md
│   └── architecture.md
│
├── prisma/
│   ├── schema.prisma
│   └── migrations/
│
├── public/
│   └── favicon.ico
│
├── src/
│   ├── components/
│   │   ├── ui/                    # shadcn/ui components
│   │   ├── layout/                # Layout components
│   │   ├── characters/            # Character feature
│   │   ├── accounts/              # Account feature
│   │   ├── teams/                 # Team feature
│   │   ├── progressions/          # Progression feature
│   │   └── shared/                # Shared components
│   │
│   ├── lib/
│   │   ├── utils.ts               # Utility functions (cn, etc.)
│   │   ├── errors.ts              # Error types
│   │   ├── schemas/               # Zod schemas
│   │   │   ├── character.ts
│   │   │   ├── account.ts
│   │   │   ├── team.ts
│   │   │   └── progression.ts
│   │   ├── client/                # Client-only code
│   │   │   ├── query-client.ts
│   │   │   └── stores/
│   │   │       └── ui-store.ts
│   │   └── server/                # Server-only code
│   │       ├── db.ts
│   │       ├── cache.ts
│   │       ├── logger.ts
│   │       └── dofusdb.ts
│   │
│   ├── routes/
│   │   ├── __root.tsx
│   │   ├── index.tsx
│   │   ├── characters/
│   │   ├── accounts/
│   │   ├── teams/
│   │   ├── progressions/
│   │   └── settings/
│   │
│   ├── server/
│   │   ├── functions/
│   │   │   ├── auth.ts
│   │   │   ├── characters.ts
│   │   │   ├── accounts.ts
│   │   │   ├── teams.ts
│   │   │   ├── progressions.ts
│   │   │   └── dofusdb.ts
│   │   ├── middleware/
│   │   │   └── auth.ts
│   │   └── index.ts
│   │
│   ├── styles/
│   │   └── globals.css
│   │
│   └── app.tsx                    # App entry point
│
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
│
├── .env.example
├── .gitignore
├── biome.json
├── package.json
├── tsconfig.json
├── app.config.ts                  # TanStack Start config
└── README.md

13. Development Workflow

Local Development Setup

# 1. Clone repository
git clone <repo-url>
cd dofus-manager

# 2. Install dependencies
pnpm install

# 3. Setup environment
cp .env.example .env
# Edit .env with your values

# 4. Start database
docker compose up -d postgres

# 5. Run migrations
pnpm prisma migrate dev

# 6. Seed database (optional)
pnpm prisma db seed

# 7. Start development server
pnpm dev

Environment Variables

# .env.example

# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/dofus_manager?schema=public"

# App
APP_URL="http://localhost:3000"
NODE_ENV="development"

# Session
SESSION_SECRET="your-secret-key-min-32-chars"

# Optional: DofusDB
DOFUSDB_CACHE_TTL="3600"

Git Workflow

main (production)
  │
  └── develop (staging)
        │
        ├── feature/add-character-filters
        ├── feature/team-management
        └── fix/progression-update-bug

Branch Naming

  • feature/* - New features
  • fix/* - Bug fixes
  • refactor/* - Code refactoring
  • docs/* - Documentation updates
  • chore/* - Maintenance tasks

Commit Convention

<type>(<scope>): <description>

Types: feat, fix, docs, style, refactor, test, chore
Scope: characters, accounts, teams, progressions, auth, ui

Examples:
feat(characters): add bulk delete functionality
fix(progressions): correct date formatting
docs(readme): update setup instructions

14. Deployment Architecture

Docker Configuration

# docker/Dockerfile

# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# Copy source and build
COPY . .
RUN pnpm prisma generate
RUN pnpm build

# Production stage
FROM node:20-alpine AS runner

WORKDIR /app

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 app

# Copy built application
COPY --from=builder --chown=app:nodejs /app/.output ./.output
COPY --from=builder --chown=app:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=app:nodejs /app/package.json ./package.json
COPY --from=builder --chown=app:nodejs /app/prisma ./prisma

USER app

EXPOSE 3000

ENV NODE_ENV=production
ENV PORT=3000

CMD ["node", ".output/server/index.mjs"]

Docker Compose (Production)

# docker/docker-compose.yml

services:
  app:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432/dofus_manager
      - SESSION_SECRET=${SESSION_SECRET}
      - NODE_ENV=production
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - internal
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dofus.rule=Host(`dofus.example.com`)"
      - "traefik.http.routers.dofus.entrypoints=websecure"
      - "traefik.http.routers.dofus.tls.certresolver=letsencrypt"
      - "traefik.http.services.dofus.loadbalancer.server.port=3000"

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=dofus_manager
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - internal

  traefik:
    image: traefik:v3.0
    restart: unless-stopped
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    networks:
      - traefik

volumes:
  postgres_data:
  letsencrypt:

networks:
  internal:
  traefik:
    external: true

GitLab CI/CD Pipeline

# .gitlab-ci.yml

stages:
  - test
  - build
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

# Test stage
test:
  stage: test
  image: node:20-alpine
  before_script:
    - corepack enable
    - pnpm install --frozen-lockfile
  script:
    - pnpm lint
    - pnpm typecheck
    - pnpm test
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

# Build stage
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $DOCKER_IMAGE -f docker/Dockerfile .
    - docker push $DOCKER_IMAGE
    - docker tag $DOCKER_IMAGE $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main
    - develop

# Deploy staging
deploy_staging:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
  script:
    - ssh $STAGING_USER@$STAGING_HOST "cd /opt/dofus-manager && docker compose pull && docker compose up -d"
  environment:
    name: staging
    url: https://staging.dofus.example.com
  only:
    - develop

# Deploy production
deploy_production:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
  script:
    - ssh $PROD_USER@$PROD_HOST "cd /opt/dofus-manager && docker compose pull && docker compose up -d && docker compose exec app pnpm prisma migrate deploy"
  environment:
    name: production
    url: https://dofus.example.com
  only:
    - main
  when: manual

15. Security & Performance

Security Measures

Authentication

  • Session-based authentication with secure cookies
  • Password hashing with bcrypt (cost factor 12)
  • Session expiration and rotation

Input Validation

  • All inputs validated with Zod schemas
  • Server-side validation mandatory
  • Prisma parameterized queries (SQL injection prevention)

Headers (via Traefik)

# Security headers middleware
http:
  middlewares:
    security-headers:
      headers:
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        contentTypeNosniff: true
        frameDeny: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"
        contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

Performance Optimizations

Database

  • Indexes on foreign keys and search fields
  • Pagination for all list queries
  • Connection pooling via Prisma

Caching

  • node-cache for server-side caching
  • TanStack Query for client-side caching
  • DofusDB data cached for 1 hour

Frontend

  • Code splitting via TanStack Router
  • Lazy loading for routes
  • Optimistic updates for better UX

Bundle Optimization

// app.config.ts
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vendor-react': ['react', 'react-dom'],
            'vendor-tanstack': ['@tanstack/react-router', '@tanstack/react-query'],
            'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-select'],
          },
        },
      },
    },
  },
});

16. Testing Strategy

Testing Pyramid

         ┌───────┐
         │  E2E  │  ← Few, critical paths
         │ Tests │
        ┌┴───────┴┐
        │Integration│  ← API & DB tests
        │  Tests   │
       ┌┴─────────┴┐
       │   Unit    │  ← Many, fast
       │   Tests   │
       └───────────┘

Unit Tests (Vitest)

// tests/unit/schemas/character.test.ts
import { describe, it, expect } from 'vitest';
import { createCharacterSchema } from '@/lib/schemas/character';

describe('createCharacterSchema', () => {
  it('validates correct input', () => {
    const input = {
      name: 'TestChar',
      level: 200,
      classId: 1,
      className: 'Cra',
      serverId: 1,
      serverName: 'Imagiro',
      accountId: 'clx123abc',
    };

    expect(() => createCharacterSchema.parse(input)).not.toThrow();
  });

  it('rejects invalid level', () => {
    const input = {
      name: 'TestChar',
      level: 250, // Invalid: max is 200
      classId: 1,
      className: 'Cra',
      serverId: 1,
      serverName: 'Imagiro',
      accountId: 'clx123abc',
    };

    expect(() => createCharacterSchema.parse(input)).toThrow();
  });
});

Integration Tests

// tests/integration/characters.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { db } from '@/lib/server/db';

describe('Character API', () => {
  let testUserId: string;
  let testAccountId: string;

  beforeAll(async () => {
    // Setup test data
    const user = await db.user.create({
      data: {
        email: 'test@test.com',
        passwordHash: 'hash',
      },
    });
    testUserId = user.id;

    const account = await db.account.create({
      data: {
        name: 'TestAccount',
        userId: testUserId,
      },
    });
    testAccountId = account.id;
  });

  afterAll(async () => {
    // Cleanup
    await db.user.delete({ where: { id: testUserId } });
  });

  it('creates a character', async () => {
    const character = await db.character.create({
      data: {
        name: 'TestChar',
        level: 200,
        classId: 1,
        className: 'Cra',
        serverId: 1,
        serverName: 'Imagiro',
        accountId: testAccountId,
      },
    });

    expect(character.name).toBe('TestChar');
    expect(character.level).toBe(200);
  });
});

E2E Tests (Playwright)

// tests/e2e/characters.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Characters Page', () => {
  test.beforeEach(async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@test.com');
    await page.fill('[name="password"]', 'password');
    await page.click('button[type="submit"]');
    await page.waitForURL('/');
  });

  test('displays character list', async ({ page }) => {
    await page.goto('/characters');

    await expect(page.locator('h1')).toContainText('Personnages');
    await expect(page.locator('table')).toBeVisible();
  });

  test('can create a new character', async ({ page }) => {
    await page.goto('/characters');

    await page.click('button:has-text("Ajouter")');
    await page.fill('[name="name"]', 'NewChar');
    await page.fill('[name="level"]', '100');
    await page.click('button:has-text("Créer")');

    await expect(page.locator('text=NewChar')).toBeVisible();
  });
});

17. Coding Standards

Cette section définit les standards de code minimaux mais critiques pour le développement. Ces règles sont conçues pour les agents IA et les développeurs.

Critical Fullstack Rules

Rule Description
Type Sharing Toujours définir les types partagés dans src/types/ et les importer dans les composants et server functions. Ne jamais dupliquer les interfaces.
Server Functions Only Ne jamais faire d'appels HTTP directs (fetch, axios). Toujours utiliser les server functions importées depuis @/server/*.
Zod Validation Toute entrée utilisateur doit être validée avec un schéma Zod. Le schéma doit être colocalisé avec la server function.
Prisma Transactions Les opérations multi-tables doivent utiliser prisma.$transaction() pour garantir l'atomicité.
Cache Invalidation Après chaque mutation, invalider les caches affectés via cacheService.invalidate() et queryClient.invalidateQueries().
Error Boundaries Chaque page doit être wrappée dans un ErrorBoundary. Les erreurs serveur doivent utiliser les classes de @/lib/errors.ts.
No Direct State Mutation Ne jamais muter l'état directement. Utiliser les setters de useState ou les mutations TanStack Query.
URL State for Filters Les filtres et la pagination doivent être stockés dans l'URL via TanStack Router, pas dans le state local.
Optimistic Updates Pattern Pour les actions fréquentes (toggle, update), utiliser les optimistic updates de TanStack Query.
Component Colocation Les hooks, types et helpers spécifiques à un composant doivent être dans le même dossier.

Naming Conventions

Element Frontend Backend Example
Components PascalCase CharacterCard.tsx
Component files kebab-case ou PascalCase character-card.tsx
Hooks camelCase + use prefix useCharacters.ts
Server Functions camelCase camelCase getCharacters, createTeam
Server Function files kebab-case kebab-case characters.ts
Types/Interfaces PascalCase PascalCase Character, CreateCharacterInput
Enums PascalCase PascalCase CharacterClass, TeamType
Constants SCREAMING_SNAKE SCREAMING_SNAKE MAX_TEAM_SIZE
Database tables snake_case character_progressions
Database columns snake_case created_at, account_id
URL routes kebab-case kebab-case /characters, /bulk-progressions
Query keys camelCase arrays ['characters', 'list', filters]
CSS classes Tailwind utilities bg-card text-foreground

File Structure Conventions

Component File Structure

// 1. Imports (grouped)
import { useState } from 'react';                    // React
import { useQuery } from '@tanstack/react-query';   // External libs
import { Button } from '@/components/ui/button';     // Internal UI
import { getCharacters } from '@/server/characters'; // Server functions
import type { Character } from '@/types';            // Types (last)

// 2. Types (if not in separate file)
interface CharacterListProps {
  serverId?: string;
}

// 3. Constants (if any)
const PAGE_SIZE = 20;

// 4. Component
export function CharacterList({ serverId }: CharacterListProps) {
  // 4a. Hooks (in order: router, query, state, effects, callbacks)
  const navigate = useNavigate();
  const { data, isLoading } = useQuery({ ... });
  const [selected, setSelected] = useState<string[]>([]);

  // 4b. Early returns
  if (isLoading) return <Loading />;
  if (!data) return <EmptyState />;

  // 4c. Render
  return (
    <div>
      {/* JSX */}
    </div>
  );
}

// 5. Sub-components (if small and only used here)
function CharacterRow({ character }: { character: Character }) {
  return <tr>...</tr>;
}

Server Function File Structure

// 1. Imports
import { createServerFn } from '@tanstack/start';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
import { cacheService } from '@/lib/cache';
import { NotFoundError, ValidationError } from '@/lib/errors';

// 2. Schemas (colocated with functions)
const createCharacterSchema = z.object({
  name: z.string().min(1).max(50),
  class: z.nativeEnum(CharacterClass),
  level: z.number().int().min(1).max(200),
  serverId: z.string().uuid(),
  accountId: z.string().uuid(),
});

// 3. Server Functions (exported)
export const getCharacters = createServerFn('GET', async (filters) => {
  // Implementation
});

export const createCharacter = createServerFn('POST', async (input) => {
  const data = createCharacterSchema.parse(input);
  // Implementation
});

// 4. Helper functions (private, at bottom)
function buildWhereClause(filters: Filters): Prisma.CharacterWhereInput {
  // Implementation
}

Code Quality Rules

TypeScript

// ✅ DO: Explicit return types for exported functions
export function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.amount, 0);
}

// ❌ DON'T: Implicit any
function processData(data) { ... }

// ✅ DO: Use type imports
import type { Character } from '@/types';

// ❌ DON'T: Import types as values
import { Character } from '@/types';

// ✅ DO: Discriminated unions for state
type State =
  | { status: 'loading' }
  | { status: 'error'; error: Error }
  | { status: 'success'; data: Character[] };

// ❌ DON'T: Optional properties for state
type State = {
  loading?: boolean;
  error?: Error;
  data?: Character[];
};

React

// ✅ DO: Named exports for components
export function CharacterCard({ character }: Props) { ... }

// ❌ DON'T: Default exports (except for routes)
export default function CharacterCard() { ... }

// ✅ DO: Destructure props
function CharacterCard({ character, onSelect }: Props) { ... }

// ❌ DON'T: Props object
function CharacterCard(props: Props) {
  const character = props.character;
}

// ✅ DO: useCallback for handlers passed to children
const handleSelect = useCallback((id: string) => {
  setSelected(prev => [...prev, id]);
}, []);

// ✅ DO: useMemo for expensive computations
const filteredCharacters = useMemo(
  () => characters.filter(c => c.level >= 180),
  [characters]
);

// ❌ DON'T: Inline objects/arrays in props (causes re-renders)
<Table columns={[col1, col2]} /> // Creates new array each render

Prisma

// ✅ DO: Select only needed fields
const characters = await prisma.character.findMany({
  select: {
    id: true,
    name: true,
    level: true,
    server: { select: { name: true } },
  },
});

// ❌ DON'T: Fetch everything
const characters = await prisma.character.findMany({
  include: {
    server: true,
    account: true,
    progressions: true,
    currencies: true,
    teamMemberships: true,
  },
});

// ✅ DO: Use transactions for related operations
await prisma.$transaction([
  prisma.teamMember.deleteMany({ where: { teamId } }),
  prisma.team.delete({ where: { id: teamId } }),
]);

// ✅ DO: Count separately from data
const [data, total] = await Promise.all([
  prisma.character.findMany({ take: 20 }),
  prisma.character.count(),
]);

Import Order

ESLint avec eslint-plugin-import applique cet ordre :

// 1. React
import { useState, useCallback } from 'react';

// 2. External packages (alphabetical)
import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { z } from 'zod';

// 3. Internal absolute imports (alphabetical by path)
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/data-table/data-table';
import { useCharacters } from '@/hooks/use-characters';
import { prisma } from '@/lib/prisma';
import { getCharacters } from '@/server/characters';

// 4. Relative imports
import { CharacterRow } from './character-row';

// 5. Type imports (at the end)
import type { Character, CharacterFilters } from '@/types';

ESLint & Prettier Config

.eslintrc.cjs

module.exports = {
  root: true,
  env: { browser: true, es2022: true, node: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: './tsconfig.json',
  },
  plugins: ['@typescript-eslint', 'import'],
  rules: {
    // TypeScript
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/consistent-type-imports': 'error',
    '@typescript-eslint/no-explicit-any': 'warn',

    // Import order
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
          'type',
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc' },
      },
    ],

    // React
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',

    // General
    'no-console': ['warn', { allow: ['warn', 'error'] }],
  },
  ignorePatterns: ['node_modules', '.output', 'dist'],
};

.prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "plugins": ["prettier-plugin-tailwindcss"]
}

Git Commit Standards

# Format
<type>(<scope>): <subject>

# Types
feat     # New feature
fix      # Bug fix
docs     # Documentation
style    # Formatting (no code change)
refactor # Refactoring
test     # Adding tests
chore    # Build, deps, tooling

# Scopes (optional)
characters, teams, accounts, progressions, currencies
ui, api, db, auth, dofusdb

# Examples
feat(characters): add bulk progression update
fix(teams): validate account constraint on member add
refactor(api): extract prisma helpers
test(teams): add integration tests for constraints
chore(deps): upgrade TanStack Query to v5.17

18. Error Handling

Unified Error Types

// src/lib/errors.ts

export enum ErrorCode {
  // Validation (400)
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  INVALID_INPUT = 'INVALID_INPUT',

  // Authentication (401)
  UNAUTHORIZED = 'UNAUTHORIZED',
  SESSION_EXPIRED = 'SESSION_EXPIRED',

  // Authorization (403)
  FORBIDDEN = 'FORBIDDEN',
  INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',

  // Not Found (404)
  NOT_FOUND = 'NOT_FOUND',
  RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',

  // Conflict (409)
  CONFLICT = 'CONFLICT',
  DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',

  // External Service (502)
  EXTERNAL_API_ERROR = 'EXTERNAL_API_ERROR',
  DOFUSDB_ERROR = 'DOFUSDB_ERROR',

  // Server (500)
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  DATABASE_ERROR = 'DATABASE_ERROR',
}

export interface AppErrorData {
  code: ErrorCode;
  message: string;
  details?: Record<string, unknown>;
  field?: string;
}

export class AppError extends Error {
  public readonly code: ErrorCode;
  public readonly statusCode: number;
  public readonly details?: Record<string, unknown>;
  public readonly field?: string;
  public readonly isOperational: boolean;

  constructor(data: AppErrorData & { statusCode?: number }) {
    super(data.message);
    this.name = 'AppError';
    this.code = data.code;
    this.statusCode = data.statusCode ?? this.getDefaultStatusCode(data.code);
    this.details = data.details;
    this.field = data.field;
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }

  private getDefaultStatusCode(code: ErrorCode): number {
    const statusMap: Record<ErrorCode, number> = {
      [ErrorCode.VALIDATION_ERROR]: 400,
      [ErrorCode.INVALID_INPUT]: 400,
      [ErrorCode.UNAUTHORIZED]: 401,
      [ErrorCode.SESSION_EXPIRED]: 401,
      [ErrorCode.FORBIDDEN]: 403,
      [ErrorCode.INSUFFICIENT_PERMISSIONS]: 403,
      [ErrorCode.NOT_FOUND]: 404,
      [ErrorCode.RESOURCE_NOT_FOUND]: 404,
      [ErrorCode.CONFLICT]: 409,
      [ErrorCode.DUPLICATE_ENTRY]: 409,
      [ErrorCode.EXTERNAL_API_ERROR]: 502,
      [ErrorCode.DOFUSDB_ERROR]: 502,
      [ErrorCode.INTERNAL_ERROR]: 500,
      [ErrorCode.DATABASE_ERROR]: 500,
    };
    return statusMap[code] ?? 500;
  }

  toJSON(): AppErrorData & { statusCode: number } {
    return {
      code: this.code,
      message: this.message,
      statusCode: this.statusCode,
      details: this.details,
      field: this.field,
    };
  }
}

Backend Error Handler

// src/lib/server/error-handler.ts

import { Prisma } from '@prisma/client';
import { AppError, ErrorCode } from '@/lib/errors';

export function handlePrismaError(error: unknown): AppError {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    switch (error.code) {
      case 'P2002':
        return new AppError({
          code: ErrorCode.DUPLICATE_ENTRY,
          message: 'Cette entrée existe déjà',
          details: { fields: error.meta?.target },
        });
      case 'P2025':
        return new AppError({
          code: ErrorCode.NOT_FOUND,
          message: 'Ressource non trouvée',
        });
      default:
        return new AppError({
          code: ErrorCode.DATABASE_ERROR,
          message: 'Erreur de base de données',
        });
    }
  }

  return new AppError({
    code: ErrorCode.INTERNAL_ERROR,
    message: 'Une erreur inattendue est survenue',
  });
}

Frontend Error Handler

// src/lib/client/error-handler.ts

import { toast } from 'sonner';
import { AppError, ErrorCode } from '@/lib/errors';

const errorMessages: Record<ErrorCode, string> = {
  [ErrorCode.VALIDATION_ERROR]: 'Données invalides',
  [ErrorCode.UNAUTHORIZED]: 'Veuillez vous connecter',
  [ErrorCode.NOT_FOUND]: 'Ressource non trouvée',
  [ErrorCode.DUPLICATE_ENTRY]: 'Cette entrée existe déjà',
  [ErrorCode.INTERNAL_ERROR]: 'Erreur serveur',
  // ... other codes
};

export function handleError(error: unknown): void {
  if (error instanceof AppError) {
    const message = error.message || errorMessages[error.code];

    if (error.statusCode >= 500) {
      toast.error('Erreur serveur', { description: message });
    } else {
      toast.warning(message);
    }
    return;
  }

  console.error('Unexpected error:', error);
  toast.error('Une erreur inattendue est survenue');
}

19. Monitoring & Observability

Structured Logging (Pino)

// src/lib/server/logger.ts

import pino from 'pino';

const isDev = process.env.NODE_ENV === 'development';

export const logger = pino({
  level: process.env.LOG_LEVEL ?? (isDev ? 'debug' : 'info'),

  transport: isDev
    ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'HH:MM:ss',
          ignore: 'pid,hostname',
        },
      }
    : undefined,

  formatters: {
    level: (label) => ({ level: label }),
  },

  base: {
    env: process.env.NODE_ENV,
    version: process.env.APP_VERSION ?? '0.0.0',
  },

  redact: {
    paths: ['password', 'token', 'authorization'],
    censor: '[REDACTED]',
  },
});

export const dbLogger = logger.child({ module: 'database' });
export const apiLogger = logger.child({ module: 'api' });

Health Check Endpoint

// src/server/functions/health.ts

import { createServerFn } from '@tanstack/react-start/server';
import { db } from '@/lib/server/db';

interface HealthStatus {
  status: 'healthy' | 'degraded' | 'unhealthy';
  timestamp: string;
  version: string;
  checks: {
    database: boolean;
    cache: boolean;
  };
}

export const getHealth = createServerFn({ method: 'GET' }).handler(
  async (): Promise<HealthStatus> => {
    const checks = {
      database: false,
      cache: true,
    };

    try {
      await db.$queryRaw`SELECT 1`;
      checks.database = true;
    } catch {
      // Database check failed
    }

    const allHealthy = Object.values(checks).every(Boolean);

    return {
      status: allHealthy ? 'healthy' : 'degraded',
      timestamp: new Date().toISOString(),
      version: process.env.APP_VERSION ?? '0.0.0',
      checks,
    };
  }
);

Application Metrics

// src/lib/server/metrics.ts

class MetricsCollector {
  private startTime = Date.now();
  private data = {
    requests: { total: 0, errors: 0 },
    database: { queries: 0, slowQueries: 0 },
    cache: { hits: 0, misses: 0 },
  };

  incrementRequest(isError = false): void {
    this.data.requests.total++;
    if (isError) this.data.requests.errors++;
  }

  incrementDbQuery(isSlow = false): void {
    this.data.database.queries++;
    if (isSlow) this.data.database.slowQueries++;
  }

  incrementCacheHit(): void {
    this.data.cache.hits++;
  }

  incrementCacheMiss(): void {
    this.data.cache.misses++;
  }

  getMetrics() {
    return {
      ...this.data,
      uptime: Math.floor((Date.now() - this.startTime) / 1000),
    };
  }
}

export const metrics = new MetricsCollector();

20. Architecture Checklist

Category Item Status
Core Architecture
High-level architecture diagram
Technology stack justified
Data flow documented
Scalability considered
Data Layer
Database schema defined
Relationships documented
Indexes identified
Migration strategy
API Design
API contracts defined
Input validation
Error responses standardized
Authentication/Authorization
Frontend
Component architecture
State management
Routing strategy
Error boundaries
Security
Authentication flow
Input sanitization
CORS/CSP headers
Secrets management
Performance
Caching strategy
Database optimization
Bundle optimization
Loading states
DevOps
CI/CD pipeline
Docker configuration
Environment separation
Deployment strategy
Observability
Logging strategy
Health checks
Metrics collection
Error tracking

Architecture Summary

┌─────────────────────────────────────────────────────────────────────────┐
│                     DOFUS MANAGER - FINAL ARCHITECTURE                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Frontend (React + TanStack)          Backend (TanStack Start)          │
│  ┌─────────────────────────┐          ┌─────────────────────────┐       │
│  │ • TanStack Router       │          │ • Server Functions      │       │
│  │ • TanStack Query        │◀────────▶│ • Prisma ORM           │       │
│  │ • Zustand (UI state)    │   RPC    │ • Zod Validation       │       │
│  │ • shadcn/ui + Tailwind  │          │ • node-cache           │       │
│  └─────────────────────────┘          └───────────┬─────────────┘       │
│                                                   │                      │
│                                                   ▼                      │
│                                       ┌─────────────────────────┐       │
│                                       │   PostgreSQL 16         │       │
│                                       │   • 7 tables            │       │
│                                       │   • Full-text search    │       │
│                                       └─────────────────────────┘       │
│                                                                          │
│  Infrastructure                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ Docker Compose + Traefik (reverse proxy + SSL)                   │    │
│  │ GitLab CI/CD → VPS Deployment                                    │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Document généré avec BMAD Framework Version: 1.0.0 Date: 2026-01-18