docs(audit-2/3): aligner doc tech sur le code livré

Audit cross-doc/code, batch tech : architecture, backend, frontend,
dev-setup. Corrige les claims qui pouvaient induire un dev en erreur
(noms de services K3s, hostnames Traefik, Tuyau, queue wrapper,
seeders, env vars, polices).

architecture.md
- Composants : status « À écrire » → «  Déployé » (apps/web,
  apps/api, packages/shared) ; ajout Redis Deployment K3s ; OCR =
  Mistral choisi ; mail = Resend (sortant) + OVH MX (entrant)
  validés
- §7.5 Pods K3s : noms réels (rubis-api / rubis-web / rubis-landing
  / rubis-redis, pas de *-svc) ; pas d'IngressRoute api.rubis.pro
  (l'API est servie via app.rubis.pro/api/* proxifié par nginx du
  pod web) ; PG/MinIO en URL directe dans la ConfigMap, pas de
  Service ExternalName
- §10 Décisions en attente : ADRs 019-024 mises à jour
  (tranchées / obsolètes), suppression du wording « à venir » pour
  les choix déjà figés dans le code

backend.md
- Note de cohérence en tête : pointe vers start/routes.ts comme
  source de vérité de la surface API (~80 routes — Stripe,
  Demo, AI, Microsoft SSO, admin blog, posts publics, KPIs
  timeseries) que cette doc n'inventorie pas exhaustivement
- §1 Vue d'ensemble : Tuyau marqué « non utilisé en pratique »
  (présent en deps mais zéro import côté SPA), partage de types
  via packages/shared. OCR Mistral choisi. Mail Resend choisi.
  BullMQ direct (workers inline pod API). Sentry ADR-024.
- §2 Stack : queue = BullMQ direct (pas @rlanz/bull-queue, qui
  n'est pas installé) ; type-sharing = packages/shared
- §2 Dépendances : remplacé la todo-list pré-livraison par la
  liste réelle des packages dans apps/api/package.json
- §3 Repo layout : `database/factories/` (dossier) → `factories.ts`
  (mono-fichier) ; `database/seeders/{default_plans,demo_data}` →
  inexistants, services à la place
- §13.2 Jobs : ProcessOcrJob + RecomputeKpisJob retirés
  (n'existent pas — OCR synchrone via services/import_batch.ts,
  KPIs calculés on-the-fly). Liste des jobs réels :
  send_relance, send_checkin, send_payment_thanks
- env vars : MINIO_* → S3_* (cf. .env.example + manifest k3s) ;
  bucket prod = rubis-prod-invoices

frontend.md
- Note de cohérence en tête : Tuyau pas utilisé, tokens dans
  packages/ui (pas inline), polices @fontsource-variable (pas
  Google Fonts via <link>)
- §1 Vue d'ensemble : client API = fetch minimaliste dans
  apps/web/src/lib/api.ts ; périmètre livré = ~15 routes _app/
- §3 Polices : section Google Fonts → @fontsource-variable
  (avec note preload woff2 critique sur la landing Astro)
- §4 Routes : arbo `_onboarding/` (faux) → `onboarding/`
  (réel, segment URL) + ajout admin.blog*, clients_.$id, insights,
  parametres_.abonnement, plans_.nouveau, factures_.import
- §6 Tuyau : section marquée « historique, non utilisé en V1 »
  avec note explicative en tête
- §10 env vars : VITE_API_URL=https://api.rubis.pro → vide
  (proxifié same-origin par nginx) + ajout VITE_USE_MOCKS,
  VITE_SENTRY_DSN_WEB, VITE_APP_VERSION

dev-setup.md
- Mailhog → Mailpit (3 occurrences) — c'est ce qui tourne dans
  docker-compose.dev.yml

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-09 19:13:44 +02:00
parent 801168fc74
commit 9eaac0c7ef
4 changed files with 152 additions and 110 deletions

View File

@ -44,14 +44,16 @@ Ce document est la source de vérité technique. Quand le code et ce fichier div
| Composant | Rôle | Hosting | Status |
|---|---|---|---|
| `apps/landing` | Landing + blog publics (Astro 6 SSR) | Pod Node K3s (`rubis-landing:4321`) | ✅ Déployé |
| `apps/web` | SPA React (SaaS) | nginx pod K3s (build statique) | À écrire |
| `apps/api` | API REST AdonisJS — logique métier, jobs, email | Pod Node K3s (`rubis-api:3333`) | À écrire |
| `packages/shared` | Types TS, schemas Zod, constantes communes | workspace local | À écrire |
| `packages/ui` | Design system partagé (tokens Tailwind + composants TSX) | workspace local | ✅ |
| PostgreSQL | Base de données métier | LXC Proxmox existant | ✅ En place |
| MinIO | Stockage PDF + pièces jointes (S3-compat) | LXC Proxmox existant | ✅ En place |
| Provider OCR | Extraction texte des factures | Externe (HTTPS) | ADR-020 à venir |
| Provider Email | Envoi outbound (relances + check-in) | Externe (HTTPS) | ADR-021 à venir |
| `apps/web` | SPA React + TanStack Router/Query (SaaS) | nginx pod K3s (build statique + proxy `/api/*` → rubis-api) | ✅ Déployé |
| `apps/api` | API REST AdonisJS v7 — logique métier, jobs BullMQ inline, email, billing Stripe | Pod Node K3s (`rubis-api:3333`) | ✅ Déployé |
| `packages/shared` | Types TS, schemas Zod, constantes (statuts, tons, plans, etc.) | workspace local | ✅ |
| `packages/ui` | Design system partagé (tokens Tailwind v4 + composants TSX) | workspace local | ✅ |
| PostgreSQL | Base de données métier | LXC Proxmox existant (`10.10.10.3`) | ✅ En place |
| MinIO | Stockage PDF + uploads blog (S3-compat, bucket `rubis-prod-invoices`) | namespace K3s `minio` | ✅ En place |
| Redis | Backend BullMQ (queues: relances, checkins, payment-thanks) | Deployment K3s `rubis-redis` + PVC | ✅ Déployé |
| Provider OCR | Extraction texte des factures | Mistral (`OCR_PROVIDER=mistral`) | ✅ Choisi |
| Provider Email outbound | Resend (`relances@rubis.pro`, sub-domaine `send.rubis.pro`) | Externe (HTTPS) | ✅ En place |
| Provider Email inbound | OVH MX (`contact@rubis.pro`, `dev@rubis.pro`) | Externe (HTTPS/IMAP) | ✅ En place |
---
@ -487,21 +489,25 @@ SPA retry l'appel original avec nouveau token
### Pods K3s
```yaml
# Namespace: rubis
- Deployment: rubis-api # AdonisJS Node, port 3333
- Deployment: rubis-web # nginx, sert le bundle Vite, port 80
- Deployment: rubis-landing # déjà existant
- Service: rubis-api-svc # ClusterIP
- Service: rubis-web-svc # ClusterIP
- Service: postgres-external # ExternalName → IP du LXC postgres
- Service: minio-external # ExternalName → IP du LXC minio
- Secret: rubis-config # DB credentials, MinIO credentials, OCR API key, mail API key
- IngressRoute (Traefik) :
api.rubis.pro → rubis-api-svc:3333
app.rubis.pro → rubis-web-svc:80
rubis.pro → rubis-landing-svc:80
# Namespace: rubis (cf. k3s/app/*.yml)
- Deployment: rubis-api # AdonisJS Node, port 3333 (workers BullMQ inline, pas de pod séparé)
- Deployment: rubis-web # nginx, sert le bundle Vite + proxy /api → rubis-api:3333, port 80
- Deployment: rubis-landing # Astro 6 SSR Node, port 4321
- Deployment: rubis-redis # Redis 7 + PVC pour BullMQ
- Service: rubis-api # NodePort 30100 → 3333
- Service: rubis-web # NodePort 30110 → 80
- Service: rubis-landing # NodePort 30111 → 4321
- Service: rubis-redis # ClusterIP 6379
- ConfigMap: rubis-api-config + rubis-landing-config + rubis-web-config
- Secret: rubis-api-secrets # DB credentials, S3 credentials, OCR/Mistral API key, Resend key, Stripe keys
- Routing Traefik (config dynamique sur la VM gateway, repo proxmox) :
rubis.pro → 10.10.10.5:30111 (rubis-landing)
app.rubis.pro/* → 10.10.10.5:30110 (rubis-web — qui proxie /api/* vers rubis-api en interne K3s)
# Note : pas de hostname `api.rubis.pro` exposé. L'API est servie via app.rubis.pro/api/*.
```
**Ressources externes (LXC Proxmox, hors K3s)** : PostgreSQL (`PG_HOST=10.10.10.3` en clair dans `rubis-api-config`), MinIO (DNS `minio.minio.svc.cluster.local:9000` via le namespace MinIO du même cluster, bucket prod = `rubis-prod-invoices`). Pas de `Service ExternalName` créé, les URLs sont posées directement dans la ConfigMap.
### Pipeline CI Gitea
```
@ -554,13 +560,14 @@ healthchecks readinessProbe → service public
À trancher avant fin V1, par ordre de priorité :
| # | Sujet | Échéance suggérée |
| # | Sujet | Statut |
|---|---|---|
| 019 | **Domain model** (entités, relations, index) | Avant la 1ère migration |
| 020 | **Provider OCR** (Mindee, Document AI, Textract, Tesseract) | Avant l'implémentation du job ProcessOcr |
| 021 | **Provider email** (Resend, Postmark, SendGrid, AWS SES) | Avant l'implémentation des relances |
| 022 | **Pricing exact** (Free 5 factures ? Pro 19 €/mois ?) | Avant le payment flow |
| 023 | **Endpoint waitlist** (Resend / Formspree / API Adonis) | Au push de la landing en prod |
| 019 | **Domain model** (entités, relations, index) | ✅ tranché — modèles dans `apps/api/app/models/` |
| 020 | **Provider OCR** | ✅ tranché — Mistral (`OCR_PROVIDER=mistral`, cf. `.env.example` + `k3s/app/api.yml`). ADR à formaliser |
| 021 | **Provider email** | ✅ tranché — Resend pour le sortant, OVH MX pour l'entrant (cf. `docs/tech/backend.md` §12.5). ADR à formaliser |
| 022 | **Pricing exact** | ✅ tranché — Free 5 factures / Pro 19 € / Business 49 € (cf. `apps/api/app/services/billing.ts:34`) |
| 023 | **Endpoint waitlist** | ❌ obsolète — la landing pousse directement vers signup, pas de waitlist en prod |
| 024 | **Sentry pour error tracking** | ✅ tranché — voir `docs/decisions.md` ADR-024 |
---

View File

@ -1,18 +1,21 @@
# Guide d'implémentation — Backend
> Version : 0.1 · Dernière maj : 2026-05-06
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-016 (PG), ADR-017 (auth), ADR-018 (storage).
> Version : 0.2 · Dernière maj : 2026-05-09
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-016 (PG), ADR-017 (auth), ADR-018 (storage), ADR-024 (Sentry).
Ce document est le **guide pratique d'implémentation de l'API**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes, snippets, conventions, et — surtout — le **contrat exact** que le SPA attend déjà côté front (les mocks MSW de `apps/web/src/mocks/handlers/` sont la source de vérité du contrat actuel).
> ⚠️ **Note de cohérence (audit 2026-05-09)** : ce guide a été rédigé en phase de planification. Plusieurs claims ont divergé du code livré. Pour la **liste exhaustive des routes**, lire **`apps/api/start/routes.ts`** comme source de vérité (~80 routes : auth, factures, clients, plans, billing Stripe, blog admin/public, demo, AI, uploads, KPIs). Les modèles vivants sont dans `apps/api/app/models/`, les services dans `apps/api/app/services/`. Les sections ci-dessous sont valides **dans leur esprit** mais détaillent parfois des choix pré-livraison qui ont évolué.
Ce document est le **guide pratique d'implémentation de l'API**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes, snippets, conventions.
**À lire avant** :
- `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/tech/architecture.md` — vue d'ensemble du système
- `/docs/tech/frontend.md` — guide d'implémentation du SPA (utile pour comprendre ce que le back doit servir)
- `/packages/shared/src/` — types et schemas Zod déjà partagés
- `/apps/web/src/mocks/handlers/` — **le contrat API tel qu'il est consommé**
- `/docs/tech/frontend.md` — guide d'implémentation du SPA
- `/packages/shared/src/` — types et schemas Zod partagés (statuts, tons, plans, billing)
- `/apps/api/start/routes.ts` — **contrat API actuel (source de vérité)**
- `/apps/api/app/services/` — logique métier (billing, posts, blog_uploads, default_plans, import_batch, etc.)
---
@ -22,12 +25,15 @@ L'API (`apps/api/`) est un **AdonisJS v7** en TypeScript, qui sert :
- **JSON-only** sur `/api/v1/*` (pas de Inertia, pas de Hypermedia — le SPA est un consommateur séparé)
- **Auth Bearer** stateless via `@adonisjs/auth` access_tokens (cf. ADR-017), avec un refresh token cookie httpOnly géré custom par-dessus
- **Tuyau** pour générer le client TS typé consommé par le SPA → contrat API ↔ web verrouillé par le compilateur
- **PostgreSQL** comme base relationnelle (cf. ADR-016, instance LXC Proxmox existante)
- **MinIO** pour les pièces jointes (cf. ADR-018)
- **Provider OCR** externe (à benchmarker, ADR-020)
- **Provider Email** externe (à benchmarker, ADR-021)
- **Background jobs** pour l'OCR différé, les relances programmées, les check-ins
- **Type-sharing** via `packages/shared` (Zod schemas + types TS), consommé en workspace symlink par `apps/web` et `apps/landing`. *Note historique : la doc évoquait Tuyau pour générer un client TS typé — `@tuyau/core` est dans les deps mais n'est pas utilisé en pratique côté SPA, qui consomme l'API via un client `fetch()` minimaliste dans `apps/web/src/lib/api.ts`.*
- **PostgreSQL** comme base relationnelle (cf. ADR-016, instance LXC Proxmox existante, IP `10.10.10.3`)
- **MinIO** pour les pièces jointes (cf. ADR-018, namespace K3s `minio`, bucket `rubis-prod-invoices`)
- **OCR** : Mistral (`OCR_PROVIDER=mistral`)
- **Email outbound** : Resend (sub-domaine `send.rubis.pro`)
- **Email inbound** : OVH MX (`contact@rubis.pro`)
- **Background jobs** : BullMQ (Redis), workers inline dans le pod API (pas de pod worker séparé)
- **Billing** : Stripe (checkout, customer portal, webhook `/billing/webhook`)
- **Error tracking** : Sentry (cf. ADR-024)
Le scaffold initial a été créé via `pnpm create adonisjs@latest -- apps/api --kit=api --pkg=pnpm`, kit `api`. Ça nous a déjà donné :
@ -50,31 +56,33 @@ Tout le reste (org, clients, factures, plans, jobs, OCR, email) est à construir
| Authz | `@adonisjs/bouncer` | Policies pour les permissions (V1 mono-user, prêt V2 multi-user) |
| Validation | `@vinejs/vine` | Validateurs typés natifs Adonis, mappés sur les schemas Zod de `packages/shared` |
| Mail | `@adonisjs/mail` | Templates + provider switchable (Resend / Postmark / SES) |
| Queue | `@rlanz/bull-queue` (BullMQ) | Jobs différés (OCR, envoi email, check-ins) |
| Queue | BullMQ direct + ioredis | Jobs différés (envoi relance, envoi confirmation, envoi remerciement paiement). Pas de wrapper Adonis. Code dans `app/services/queue.ts` |
| Cache / queue backend | Redis | Backend de BullMQ + cache des KPIs dashboard |
| Rate-limit | `@adonisjs/limiter` | 5 req/min sur `/auth/*`, 10/h sur upload |
| Tests | `@japa/runner` + `@japa/api-client` | Tests d'intégration HTTP |
| Type-sharing front | `@tuyau/core` | Génère `.adonisjs/api.ts` consommé par le SPA |
| Type-sharing front | `packages/shared` (workspace) | Zod schemas + types TS (statuts, tons, plans, billing) — consommé par `apps/web` et `apps/landing` |
| HTTP client | `@adonisjs/limiter` + `ky` (côté SPA) | — |
| Storage | `@adonisjs/drive` (S3 driver MinIO) | Abstraction stockage PDFs |
### Dépendances déjà installées par le starter API
### Dépendances installées (cf. `apps/api/package.json`)
Voir `apps/api/package.json`. À ajouter pour V1 :
Stack actuelle (V1 livrée) :
```bash
cd apps/api
pnpm add @adonisjs/bouncer @adonisjs/mail @adonisjs/limiter @adonisjs/drive
pnpm add @rlanz/bull-queue bullmq ioredis
pnpm add @aws-sdk/client-s3 # pour MinIO via le driver S3 d'@adonisjs/drive
pnpm add resend # ou postmark / @aws-sdk/client-ses selon ADR-021
node ace add @adonisjs/bouncer
node ace add @adonisjs/mail --providers=resend
node ace add @adonisjs/limiter
node ace add @adonisjs/drive --services=s3
node ace add @rlanz/bull-queue
```jsonc
// apps/api/package.json (extrait)
"@adonisjs/auth", "@adonisjs/bouncer", "@adonisjs/mail",
"@adonisjs/limiter", "@adonisjs/drive", "@adonisjs/lucid",
"bullmq", "ioredis", // queues directes, pas de wrapper Adonis
"@aws-sdk/client-s3", // MinIO via driver S3
"resend", // email outbound
"@mistralai/mistralai", // OCR
"stripe", // billing
"@anthropic-ai/sdk", // génération IA blog
"@sentry/node", // ADR-024
```
Pas de BullMQ direct (sans wrapper Adonis), pas de `@tuyau/server` (le `@tuyau/core` historique n'est plus utilisé côté SPA).
---
## 3. Repo layout (apps/api)
@ -185,9 +193,11 @@ apps/api/
│ │ ├── XXXX_create_import_drafts_table.ts
│ │ └── XXXX_create_activity_events_table.ts
│ ├── seeders/
│ │ ├── default_plans_seeder.ts # 4 plans pré-fournis (cf. seed.ts MSW)
│ │ └── demo_data_seeder.ts # comptes démo (dev seulement)
│ └── factories/ # Factories Lucid pour les tests
│ │ └── (pas de seeders Adonis : les 4 plans pré-fournis vivent dans
│ │ app/services/default_plans.ts et sont insérés à la création de
│ │ chaque organisation. Le mode démo est servi par
│ │ app/services/demo_simulator.ts, pas par un seeder.)
│ └── factories.ts # Factories Lucid (mono-fichier, pas de dossier — V1)
├── tests/
│ ├── functional/ # Tests HTTP via @japa/api-client
│ │ ├── auth.spec.ts
@ -693,12 +703,12 @@ const driveConfig = defineConfig({
services: {
s3: services.s3({
credentials: {
accessKeyId: env.get('MINIO_ACCESS_KEY'),
secretAccessKey: env.get('MINIO_SECRET_KEY'),
accessKeyId: env.get('S3_ACCESS_KEY'),
secretAccessKey: env.get('S3_SECRET_KEY'),
},
endpoint: env.get('MINIO_ENDPOINT'), // http://lxc-minio:9000
region: 'fr-par',
bucket: 'rubis-invoices',
endpoint: env.get('S3_ENDPOINT'), // http://minio.minio.svc.cluster.local:9000
region: env.get('S3_REGION'),
bucket: env.get('S3_BUCKET'), // rubis-prod-invoices en prod
forcePathStyle: true, // requis pour MinIO
visibility: 'private',
}),
@ -912,7 +922,7 @@ Inbound : les deux veulent le MX `@`. Plan probable : on garde OVH pour
### 13.1 Stack
`@rlanz/bull-queue` (Adonis 7 wrapper de BullMQ) + Redis comme backend.
BullMQ direct (sans wrapper Adonis) (Adonis 7 wrapper de BullMQ) + Redis comme backend.
```ts
// config/queue.ts
@ -931,12 +941,17 @@ const queueConfig = defineConfig({
### 13.2 Jobs
| Job | Trigger | Idempotent | Retry |
Réalité V1 (cf. `apps/api/app/jobs/`) :
| Job (fichier) | Trigger | Idempotent | Retry |
|---|---|---|---|
| `ProcessOcrJob` | POST /invoices/upload | oui (status=validated/skipped → no-op) | 3× exponential backoff |
| `SendRelanceJob` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× |
| `SendCheckinJob` | CheckinTask.sendAt | oui | 3× |
| `RecomputeKpisJob` | nightly cron + post-mutation | oui | 1× |
| `send_relance_job.ts` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× |
| `send_checkin_job.ts` | CheckinTask.sendAt | oui | 3× |
| `send_payment_thanks_job.ts` | confirmation paiement (step 04 du flow) | oui | 3× |
**OCR** : pas de job dédié — l'extraction est synchrone via `app/services/import_batch.ts` (appel Mistral bloquant pendant l'upload). Si le volume monte, basculer en job différé.
**KPIs** : calculés on-the-fly à chaque GET dashboard/timeseries (cf. commentaire dans `start/routes.ts`). Pas de `RecomputeKpisJob` — pas de cache Redis pour les KPIs en V1.
### 13.3 Programmation des relances
@ -1111,11 +1126,11 @@ REDIS_HOST=lxc-redis.proxmox.local
REDIS_PORT=6379
# MinIO
MINIO_ENDPOINT=http://lxc-minio.proxmox.local:9000
MINIO_ACCESS_KEY=<secret>
MINIO_SECRET_KEY=<secret>
MINIO_INVOICES_BUCKET=rubis-invoices
MINIO_BACKUPS_BUCKET=rubis-backups
S3_ENDPOINT=http://minio.minio.svc.cluster.local:9000
S3_ACCESS_KEY=<secret>
S3_SECRET_KEY=<secret>
S3_REGION=us-east-1
S3_BUCKET=rubis-prod-invoices # bucket réel en prod (cf. k3s/app/api.yml)
# Mail (Resend)
MAIL_FROM_ADDRESS=relances@rubis.pro

View File

@ -2,7 +2,7 @@
> Version : 0.1 · 2026-05-06
Tout ce qu'il faut pour faire tourner Rubis en local : services backing (Postgres, Redis, MinIO, Mailhog) + l'API + le SPA.
Tout ce qu'il faut pour faire tourner Rubis en local : services backing (Postgres, Redis, MinIO, Mailpit) + l'API + le SPA.
## Prérequis
@ -68,7 +68,7 @@ Si Docker n'est pas dispo, on peut basculer sur SQLite :
DB_CONNECTION=sqlite
```
Les migrations tournent sur les deux. Pour les jobs/storage/mail, il faut quand même Redis/MinIO/Mailhog — donc Docker reste recommandé.
Les migrations tournent sur les deux. Pour les jobs/storage/mail, il faut quand même Redis/MinIO/Mailpit — donc Docker reste recommandé.
## 3. Web (apps/web)
@ -86,7 +86,7 @@ Voir `apps/api/.env.example` — c'est la source de vérité. Récap :
- **PG_*** : connexion Postgres (default ports décalés docker-compose)
- **REDIS_*** : BullMQ
- **S3_*** : MinIO (driver S3 d'@adonisjs/drive)
- **MAIL_*** + **SMTP_*** : Mailhog en dev, Resend en prod (`MAIL_DRIVER=resend` + `RESEND_API_KEY`)
- **MAIL_*** + **SMTP_*** : Mailpit en dev, Resend en prod (`MAIL_DRIVER=resend` + `RESEND_API_KEY`)
- **OCR_PROVIDER** : `mock` en dev, `mistral` quand l'API key est en place
- **ACCESS_TOKEN_TTL_MINUTES** / **REFRESH_TOKEN_TTL_DAYS** : durées d'auth

View File

@ -1,7 +1,12 @@
# Guide d'implémentation — Frontend
> Version : 0.1 · Dernière maj : 2026-05-05
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-017 (auth).
> Version : 0.2 · Dernière maj : 2026-05-09
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-017 (auth), ADR-024 (Sentry).
> ⚠️ **Note de cohérence (audit 2026-05-09)** : ce guide a été rédigé en phase de planification. Plusieurs choix ont évolué :
> - **Tuyau N'EST PAS utilisé** côté SPA. Le client API vit dans `apps/web/src/lib/api.ts` (fetch + ApiError custom). Le partage de types entre API et SPA passe par `packages/shared` (Zod + types TS), pas par un client typed-RPC. Les sections §6 et §7 ci-dessous, qui décrivent l'install et l'usage de Tuyau, sont **historiques** — gardées pour mémoire mais non applicables au code livré.
> - Les **tokens** de design vivent dans `packages/ui/src/styles/{tokens,base}.css`, pas inline dans `apps/web/src/styles/app.css` comme la doc l'écrit.
> - Les **polices** sont self-hosted via `@fontsource-variable/{bricolage-grotesque,inter}`, pas via `<link>` Google Fonts.
Ce document est le **guide pratique d'implémentation du SPA**. Il complète `architecture.md` (qui décrit le **quoi**) en expliquant le **comment** : commandes exactes, snippets de config, conventions de dossier.
@ -9,16 +14,16 @@ Ce document est le **guide pratique d'implémentation du SPA**. Il complète `ar
- `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/marque.md` — palette, typo, voix, do/don't
- `/docs/wireframes-mvp.html`les 13 écrans MVP avec annotations
- `/docs/wireframes-mvp.html`wireframes low-fi initiaux (13 écrans, à jour partiellement)
- `/docs/tech/architecture.md` — vue d'ensemble du système
---
## 1. Vue d'ensemble
L'app web (`apps/web/`) est un SPA React 19 buildé par Vite, qui consomme l'API AdonisJS `apps/api/` via un client HTTP type-safe (Tuyau). Le routing client est géré par **TanStack Router** (file-based, type-safe), le state serveur par **TanStack Query**, le styling par **Tailwind CSS v4** avec les tokens de marque issus de `marque.md`.
L'app web (`apps/web/`) est un SPA React 19 buildé par Vite, qui consomme l'API AdonisJS `apps/api/` via un client `fetch()` minimaliste dans `apps/web/src/lib/api.ts`. Le routing client est géré par **TanStack Router** (file-based, type-safe), le state serveur par **TanStack Query**, le styling par **Tailwind CSS v4** avec les tokens de marque issus de `packages/ui` (consommé en workspace).
**Périmètre V1** : 13 écrans listés dans `wireframes-mvp.html`. Auth Bearer (cf. ADR-017) avec refresh token httpOnly cookie. Mobile responsive, pas d'app native.
**Périmètre V1 livré** : ~15 routes `_app/` (dashboard, factures, clients, plans, paramètres, abonnement, insights, admin/blog) + onboarding 3 étapes + auth (login, signup, reset-password, callbacks SSO Google/Microsoft). Auth Bearer (cf. ADR-017) avec refresh token httpOnly cookie. Mobile responsive, pas d'app native.
**Hors scope V1** : SSR (pas nécessaire pour un SaaS B2B authentifié), i18n (FR uniquement), PWA offline (nice-to-have V2).
@ -163,16 +168,22 @@ body {
Importé dans `main.tsx` : `import './styles/app.css'`.
### Polices Google Fonts
### Polices self-hosted via @fontsource-variable
`index.html` :
Les fonts sont bundlées au build (pas de fetch Google Fonts au runtime → pas de FOUT, pas de fuite RGPD vers `fonts.googleapis.com`).
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
```bash
pnpm add @fontsource-variable/bricolage-grotesque @fontsource-variable/inter
```
```css
/* apps/web/src/styles/app.css (ou packages/ui/src/styles/base.css selon où tu importes) */
@import "@fontsource-variable/bricolage-grotesque";
@import "@fontsource-variable/inter";
```
**Bonus prod** : sur la landing Astro, on preload les woff2 latin critiques dans le `<head>` pour casser la chaîne HTML→CSS→fonts du critical path (cf. `apps/landing/src/layouts/Layout.astro`).
### Usage typique
```tsx
@ -207,27 +218,31 @@ Référence : les 13 écrans dans `wireframes-mvp.html`.
```
apps/web/src/routes/
├── __root.tsx # Layout global, providers, AuthGate
├── login.tsx # 1.2 Connexion
├── signup.tsx # 1.1 Inscription
├── _onboarding/ # Layout onboarding (sans sidebar)
│ ├── _onboarding.tsx
│ ├── compte.tsx # 1.3 step 1
│ ├── entreprise.tsx # 1.3 step 2
│ └── signature.tsx # 1.3 step 3
└── _app/ # Layout app authentifiée
├── _app.tsx # Layout : sidebar + topbar + tab bar mobile
├── index.tsx # 4.1 Dashboard
├── factures.tsx # 2.4 Liste filtrable
├── factures.$id.tsx # 4.2 Détail facture (timeline)
├── factures.import.$batchId.tsx # 2.2 Vérification OCR
├── plans.tsx # 3.1 Bibliothèque
├── plans.$slug.tsx # 3.2 Éditeur (cadence + templates)
├── clients.tsx # liste clients
└── parametres.tsx # paramètres compte
├── __root.tsx # Layout global, providers, AuthGate
├── login.tsx, signup.tsx, reset-password.tsx, accept-invite.tsx
├── auth/ # callbacks SSO Google + Microsoft
├── onboarding.tsx # layout onboarding (segment URL /onboarding)
├── onboarding/ # 3 étapes inscription post-signup
│ ├── compte.tsx
│ ├── entreprise.tsx
│ ├── signature.tsx
│ └── index.tsx
└── _app/ # Layout app authentifiée (group route, pas de segment)
├── _app.tsx # sidebar + topbar + tab bar mobile
├── index.tsx # Dashboard (compteur rubis + KPIs)
├── factures.tsx # Liste filtrable
├── factures_.$id.tsx # Détail facture + timeline relances
├── factures_.import.tsx # Drag-and-drop OCR (étape 1)
├── factures_.import_.$batchId.tsx # Vérification OCR (étape 2)
├── clients.tsx, clients_.$id.tsx
├── plans.tsx, plans_.$slug.tsx, plans_.nouveau.tsx
├── parametres.tsx, parametres_.abonnement.tsx
├── insights.tsx # KPIs avancés (timeseries)
├── admin.blog.tsx # liste posts (admin only)
└── admin.blog_.$id.tsx # éditeur post + publish workflow
```
Les routes commençant par `_` sont des **layout routes** (n'ajoutent pas de segment URL).
Les routes en `_app/*` sont sous **layout group route** (pas de segment URL ajouté). Le suffixe `_` dans `factures_.$id` empêche l'imbrication visuelle dans `factures.tsx` (TanStack Router file-based convention).
### Configuration root
@ -405,7 +420,9 @@ const markPaidMutation = useMutation({
---
## 6. Tuyau — client HTTP typé pour AdonisJS
## 6. Tuyau — *historique, non utilisé en V1*
> ⚠️ Cette section décrit un choix initial qui n'a pas été retenu côté SPA livré. Le client HTTP réel est un `fetch()` minimaliste dans `apps/web/src/lib/api.ts`, et le partage de types entre l'API et le SPA passe par `packages/shared` (Zod schemas + types TS exportés en workspace). Section gardée pour mémoire et pour un éventuel pivot futur si la surface API grossit suffisamment.
[Tuyau](https://github.com/Julien-R44/tuyau) est l'équivalent tRPC pour AdonisJS. Il génère un client TS qui connaît toutes les routes API, leurs payloads, et leurs réponses — depuis le code Adonis lui-même.
@ -676,14 +693,17 @@ VITE_API_URL=http://localhost:3333
VITE_PUBLIC_LANDING_URL=https://rubis.pro
```
Production via secret K3s injecté dans le build Vite :
Production : le SPA appelle son API via le **même origin** (`/api/v1/*`), proxifié par le nginx du pod `rubis-web` vers `rubis-api:3333` côté K3s. Pas de hostname `api.rubis.pro`. Le `VITE_API_URL` peut donc être laissé vide ou `'/api/v1'` en prod.
```bash
VITE_API_URL=https://api.rubis.pro
VITE_API_URL= # vide → fetch en relatif
VITE_PUBLIC_LANDING_URL=https://rubis.pro
VITE_USE_MOCKS=false
VITE_SENTRY_DSN_WEB=<dsn> # ADR-024
VITE_APP_VERSION=$GIT_SHA # tag image / sha commit
```
Toutes les vars accessibles côté SPA **doivent être préfixées `VITE_`** (sinon Vite ne les expose pas au bundle).
Toutes les vars accessibles côté SPA **doivent être préfixées `VITE_`** (sinon Vite ne les expose pas au bundle). Liste complète : voir `apps/web/src/lib/env.ts`.
---