From 9eaac0c7efe9e2825fc200bf8c6dec67881c666b Mon Sep 17 00:00:00 2001
From: ordinarthur <@arthurbarre.js@gmail.com>
Date: Sat, 9 May 2026 19:13:44 +0200
Subject: [PATCH] =?UTF-8?q?docs(audit-2/3):=20aligner=20doc=20tech=20sur?=
=?UTF-8?q?=20le=20code=20livr=C3=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 )
- §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
---
docs/tech/architecture.md | 61 ++++++++++++----------
docs/tech/backend.md | 107 ++++++++++++++++++++++----------------
docs/tech/dev-setup.md | 6 +--
docs/tech/frontend.md | 88 +++++++++++++++++++------------
4 files changed, 152 insertions(+), 110 deletions(-)
diff --git a/docs/tech/architecture.md b/docs/tech/architecture.md
index 5f2cf8e..759ce15 100644
--- a/docs/tech/architecture.md
+++ b/docs/tech/architecture.md
@@ -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 |
---
diff --git a/docs/tech/backend.md b/docs/tech/backend.md
index 56f29d7..3a67bf5 100644
--- a/docs/tech/backend.md
+++ b/docs/tech/backend.md
@@ -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=
-MINIO_SECRET_KEY=
-MINIO_INVOICES_BUCKET=rubis-invoices
-MINIO_BACKUPS_BUCKET=rubis-backups
+S3_ENDPOINT=http://minio.minio.svc.cluster.local:9000
+S3_ACCESS_KEY=
+S3_SECRET_KEY=
+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
diff --git a/docs/tech/dev-setup.md b/docs/tech/dev-setup.md
index bf683ac..5a12795 100644
--- a/docs/tech/dev-setup.md
+++ b/docs/tech/dev-setup.md
@@ -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
diff --git a/docs/tech/frontend.md b/docs/tech/frontend.md
index e6c6a49..616ce2d 100644
--- a/docs/tech/frontend.md
+++ b/docs/tech/frontend.md
@@ -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 `` 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
-
-
-
+```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 `` 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= # 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`.
---