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 | | Composant | Rôle | Hosting | Status |
|---|---|---|---| |---|---|---|---|
| `apps/landing` | Landing + blog publics (Astro 6 SSR) | Pod Node K3s (`rubis-landing:4321`) | ✅ Déployé | | `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/web` | SPA React + TanStack Router/Query (SaaS) | nginx pod K3s (build statique + proxy `/api/*` → rubis-api) | ✅ Déployé |
| `apps/api` | API REST AdonisJS — logique métier, jobs, email | Pod Node K3s (`rubis-api:3333`) | À écrire | | `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 communes | workspace local | À écrire | | `packages/shared` | Types TS, schemas Zod, constantes (statuts, tons, plans, etc.) | workspace local | ✅ |
| `packages/ui` | Design system partagé (tokens Tailwind + composants TSX) | workspace local | ✅ | | `packages/ui` | Design system partagé (tokens Tailwind v4 + composants TSX) | workspace local | ✅ |
| PostgreSQL | Base de données métier | LXC Proxmox existant | ✅ En place | | PostgreSQL | Base de données métier | LXC Proxmox existant (`10.10.10.3`) | ✅ En place |
| MinIO | Stockage PDF + pièces jointes (S3-compat) | LXC Proxmox existant | ✅ En place | | MinIO | Stockage PDF + uploads blog (S3-compat, bucket `rubis-prod-invoices`) | namespace K3s `minio` | ✅ En place |
| Provider OCR | Extraction texte des factures | Externe (HTTPS) | ADR-020 à venir | | Redis | Backend BullMQ (queues: relances, checkins, payment-thanks) | Deployment K3s `rubis-redis` + PVC | ✅ Déployé |
| Provider Email | Envoi outbound (relances + check-in) | Externe (HTTPS) | ADR-021 à venir | | 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 ### Pods K3s
```yaml ```yaml
# Namespace: rubis # Namespace: rubis (cf. k3s/app/*.yml)
- Deployment: rubis-api # AdonisJS Node, port 3333 - Deployment: rubis-api # AdonisJS Node, port 3333 (workers BullMQ inline, pas de pod séparé)
- Deployment: rubis-web # nginx, sert le bundle Vite, port 80 - Deployment: rubis-web # nginx, sert le bundle Vite + proxy /api → rubis-api:3333, port 80
- Deployment: rubis-landing # déjà existant - Deployment: rubis-landing # Astro 6 SSR Node, port 4321
- Service: rubis-api-svc # ClusterIP - Deployment: rubis-redis # Redis 7 + PVC pour BullMQ
- Service: rubis-web-svc # ClusterIP - Service: rubis-api # NodePort 30100 → 3333
- Service: postgres-external # ExternalName → IP du LXC postgres - Service: rubis-web # NodePort 30110 → 80
- Service: minio-external # ExternalName → IP du LXC minio - Service: rubis-landing # NodePort 30111 → 4321
- Secret: rubis-config # DB credentials, MinIO credentials, OCR API key, mail API key - Service: rubis-redis # ClusterIP 6379
- IngressRoute (Traefik) : - ConfigMap: rubis-api-config + rubis-landing-config + rubis-web-config
api.rubis.pro → rubis-api-svc:3333 - Secret: rubis-api-secrets # DB credentials, S3 credentials, OCR/Mistral API key, Resend key, Stripe keys
app.rubis.pro → rubis-web-svc:80 - Routing Traefik (config dynamique sur la VM gateway, repo proxmox) :
rubis.pro → rubis-landing-svc:80 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 ### Pipeline CI Gitea
``` ```
@ -554,13 +560,14 @@ healthchecks readinessProbe → service public
À trancher avant fin V1, par ordre de priorité : À 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 | | 019 | **Domain model** (entités, relations, index) | ✅ tranché — modèles dans `apps/api/app/models/` |
| 020 | **Provider OCR** (Mindee, Document AI, Textract, Tesseract) | Avant l'implémentation du job ProcessOcr | | 020 | **Provider OCR** | ✅ tranché — Mistral (`OCR_PROVIDER=mistral`, cf. `.env.example` + `k3s/app/api.yml`). ADR à formaliser |
| 021 | **Provider email** (Resend, Postmark, SendGrid, AWS SES) | Avant l'implémentation des relances | | 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** (Free 5 factures ? Pro 19 €/mois ?) | Avant le payment flow | | 022 | **Pricing exact** | ✅ tranché — Free 5 factures / Pro 19 € / Business 49 € (cf. `apps/api/app/services/billing.ts:34`) |
| 023 | **Endpoint waitlist** (Resend / Formspree / API Adonis) | Au push de la landing en prod | | 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 # Guide d'implémentation — Backend
> Version : 0.1 · Dernière maj : 2026-05-06 > 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). > 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** : **À lire avant** :
- `/CLAUDE.md` — contexte top-level - `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1 - `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/tech/architecture.md` — vue d'ensemble du système - `/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) - `/docs/tech/frontend.md` — guide d'implémentation du SPA
- `/packages/shared/src/` — types et schemas Zod déjà partagés - `/packages/shared/src/` — types et schemas Zod partagés (statuts, tons, plans, billing)
- `/apps/web/src/mocks/handlers/` — **le contrat API tel qu'il est consommé** - `/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é) - **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 - **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 - **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) - **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) - **MinIO** pour les pièces jointes (cf. ADR-018, namespace K3s `minio`, bucket `rubis-prod-invoices`)
- **Provider OCR** externe (à benchmarker, ADR-020) - **OCR** : Mistral (`OCR_PROVIDER=mistral`)
- **Provider Email** externe (à benchmarker, ADR-021) - **Email outbound** : Resend (sub-domaine `send.rubis.pro`)
- **Background jobs** pour l'OCR différé, les relances programmées, les check-ins - **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é : 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) | | 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` | | 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) | | 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 | | Cache / queue backend | Redis | Backend de BullMQ + cache des KPIs dashboard |
| Rate-limit | `@adonisjs/limiter` | 5 req/min sur `/auth/*`, 10/h sur upload | | Rate-limit | `@adonisjs/limiter` | 5 req/min sur `/auth/*`, 10/h sur upload |
| Tests | `@japa/runner` + `@japa/api-client` | Tests d'intégration HTTP | | 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) | — | | HTTP client | `@adonisjs/limiter` + `ky` (côté SPA) | — |
| Storage | `@adonisjs/drive` (S3 driver MinIO) | Abstraction stockage PDFs | | 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 ```jsonc
cd apps/api // apps/api/package.json (extrait)
pnpm add @adonisjs/bouncer @adonisjs/mail @adonisjs/limiter @adonisjs/drive "@adonisjs/auth", "@adonisjs/bouncer", "@adonisjs/mail",
pnpm add @rlanz/bull-queue bullmq ioredis "@adonisjs/limiter", "@adonisjs/drive", "@adonisjs/lucid",
pnpm add @aws-sdk/client-s3 # pour MinIO via le driver S3 d'@adonisjs/drive "bullmq", "ioredis", // queues directes, pas de wrapper Adonis
pnpm add resend # ou postmark / @aws-sdk/client-ses selon ADR-021 "@aws-sdk/client-s3", // MinIO via driver S3
node ace add @adonisjs/bouncer "resend", // email outbound
node ace add @adonisjs/mail --providers=resend "@mistralai/mistralai", // OCR
node ace add @adonisjs/limiter "stripe", // billing
node ace add @adonisjs/drive --services=s3 "@anthropic-ai/sdk", // génération IA blog
node ace add @rlanz/bull-queue "@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) ## 3. Repo layout (apps/api)
@ -185,9 +193,11 @@ apps/api/
│ │ ├── XXXX_create_import_drafts_table.ts │ │ ├── XXXX_create_import_drafts_table.ts
│ │ └── XXXX_create_activity_events_table.ts │ │ └── XXXX_create_activity_events_table.ts
│ ├── seeders/ │ ├── seeders/
│ │ ├── default_plans_seeder.ts # 4 plans pré-fournis (cf. seed.ts MSW) │ │ └── (pas de seeders Adonis : les 4 plans pré-fournis vivent dans
│ │ └── demo_data_seeder.ts # comptes démo (dev seulement) │ │ app/services/default_plans.ts et sont insérés à la création de
│ └── factories/ # Factories Lucid pour les tests │ │ 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/ ├── tests/
│ ├── functional/ # Tests HTTP via @japa/api-client │ ├── functional/ # Tests HTTP via @japa/api-client
│ │ ├── auth.spec.ts │ │ ├── auth.spec.ts
@ -693,12 +703,12 @@ const driveConfig = defineConfig({
services: { services: {
s3: services.s3({ s3: services.s3({
credentials: { credentials: {
accessKeyId: env.get('MINIO_ACCESS_KEY'), accessKeyId: env.get('S3_ACCESS_KEY'),
secretAccessKey: env.get('MINIO_SECRET_KEY'), secretAccessKey: env.get('S3_SECRET_KEY'),
}, },
endpoint: env.get('MINIO_ENDPOINT'), // http://lxc-minio:9000 endpoint: env.get('S3_ENDPOINT'), // http://minio.minio.svc.cluster.local:9000
region: 'fr-par', region: env.get('S3_REGION'),
bucket: 'rubis-invoices', bucket: env.get('S3_BUCKET'), // rubis-prod-invoices en prod
forcePathStyle: true, // requis pour MinIO forcePathStyle: true, // requis pour MinIO
visibility: 'private', visibility: 'private',
}), }),
@ -912,7 +922,7 @@ Inbound : les deux veulent le MX `@`. Plan probable : on garde OVH pour
### 13.1 Stack ### 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 ```ts
// config/queue.ts // config/queue.ts
@ -931,12 +941,17 @@ const queueConfig = defineConfig({
### 13.2 Jobs ### 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 | | `send_relance_job.ts` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× |
| `SendRelanceJob` | RelanceTask.sendAt | oui (status=sent → no-op) | 5× | | `send_checkin_job.ts` | CheckinTask.sendAt | oui | 3× |
| `SendCheckinJob` | CheckinTask.sendAt | oui | 3× | | `send_payment_thanks_job.ts` | confirmation paiement (step 04 du flow) | oui | 3× |
| `RecomputeKpisJob` | nightly cron + post-mutation | oui | 1× |
**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 ### 13.3 Programmation des relances
@ -1111,11 +1126,11 @@ REDIS_HOST=lxc-redis.proxmox.local
REDIS_PORT=6379 REDIS_PORT=6379
# MinIO # MinIO
MINIO_ENDPOINT=http://lxc-minio.proxmox.local:9000 S3_ENDPOINT=http://minio.minio.svc.cluster.local:9000
MINIO_ACCESS_KEY=<secret> S3_ACCESS_KEY=<secret>
MINIO_SECRET_KEY=<secret> S3_SECRET_KEY=<secret>
MINIO_INVOICES_BUCKET=rubis-invoices S3_REGION=us-east-1
MINIO_BACKUPS_BUCKET=rubis-backups S3_BUCKET=rubis-prod-invoices # bucket réel en prod (cf. k3s/app/api.yml)
# Mail (Resend) # Mail (Resend)
MAIL_FROM_ADDRESS=relances@rubis.pro MAIL_FROM_ADDRESS=relances@rubis.pro

View File

@ -2,7 +2,7 @@
> Version : 0.1 · 2026-05-06 > 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 ## Prérequis
@ -68,7 +68,7 @@ Si Docker n'est pas dispo, on peut basculer sur SQLite :
DB_CONNECTION=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) ## 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) - **PG_*** : connexion Postgres (default ports décalés docker-compose)
- **REDIS_*** : BullMQ - **REDIS_*** : BullMQ
- **S3_*** : MinIO (driver S3 d'@adonisjs/drive) - **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 - **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 - **ACCESS_TOKEN_TTL_MINUTES** / **REFRESH_TOKEN_TTL_DAYS** : durées d'auth

View File

@ -1,7 +1,12 @@
# Guide d'implémentation — Frontend # Guide d'implémentation — Frontend
> Version : 0.1 · Dernière maj : 2026-05-05 > Version : 0.2 · Dernière maj : 2026-05-09
> Décisions de référence : ADR-014 (stack), ADR-015 (monorepo), ADR-017 (auth). > 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. 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 - `/CLAUDE.md` — contexte top-level
- `/docs/produit.md` — flows utilisateur, IN/OUT V1 - `/docs/produit.md` — flows utilisateur, IN/OUT V1
- `/docs/marque.md` — palette, typo, voix, do/don't - `/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 - `/docs/tech/architecture.md` — vue d'ensemble du système
--- ---
## 1. Vue d'ensemble ## 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). **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'`. 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
<link rel="preconnect" href="https://fonts.googleapis.com"> pnpm add @fontsource-variable/bricolage-grotesque @fontsource-variable/inter
<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">
``` ```
```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 ### Usage typique
```tsx ```tsx
@ -207,27 +218,31 @@ Référence : les 13 écrans dans `wireframes-mvp.html`.
``` ```
apps/web/src/routes/ apps/web/src/routes/
├── __root.tsx # Layout global, providers, AuthGate ├── __root.tsx # Layout global, providers, AuthGate
├── login.tsx # 1.2 Connexion ├── login.tsx, signup.tsx, reset-password.tsx, accept-invite.tsx
├── signup.tsx # 1.1 Inscription ├── auth/ # callbacks SSO Google + Microsoft
├── _onboarding/ # Layout onboarding (sans sidebar) ├── onboarding.tsx # layout onboarding (segment URL /onboarding)
│ ├── _onboarding.tsx ├── onboarding/ # 3 étapes inscription post-signup
│ ├── compte.tsx # 1.3 step 1 │ ├── compte.tsx
│ ├── entreprise.tsx # 1.3 step 2 │ ├── entreprise.tsx
│ └── signature.tsx # 1.3 step 3 │ ├── signature.tsx
└── _app/ # Layout app authentifiée │ └── index.tsx
├── _app.tsx # Layout : sidebar + topbar + tab bar mobile └── _app/ # Layout app authentifiée (group route, pas de segment)
├── index.tsx # 4.1 Dashboard ├── _app.tsx # sidebar + topbar + tab bar mobile
├── factures.tsx # 2.4 Liste filtrable ├── index.tsx # Dashboard (compteur rubis + KPIs)
├── factures.$id.tsx # 4.2 Détail facture (timeline) ├── factures.tsx # Liste filtrable
├── factures.import.$batchId.tsx # 2.2 Vérification OCR ├── factures_.$id.tsx # Détail facture + timeline relances
├── plans.tsx # 3.1 Bibliothèque ├── factures_.import.tsx # Drag-and-drop OCR (étape 1)
├── plans.$slug.tsx # 3.2 Éditeur (cadence + templates) ├── factures_.import_.$batchId.tsx # Vérification OCR (étape 2)
├── clients.tsx # liste clients ├── clients.tsx, clients_.$id.tsx
└── parametres.tsx # paramètres compte ├── 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 ### 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. [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 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 ```bash
VITE_API_URL=https://api.rubis.pro VITE_API_URL= # vide → fetch en relatif
VITE_PUBLIC_LANDING_URL=https://rubis.pro 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`.
--- ---