Remplace la liste verticale par une vraie visu calendrier qui ancre
chaque étape sur une date concrète, ce qui donne du sens au timing.
- Date d'échéance fictive : le 15 du mois prochain (stable, prévisible,
laisse de la marge avant/après pour offsets négatifs comme positifs)
- Cellule échéance = ◆ rubis plein sur fond rubis-glow + shadow rubis,
jour mis en exergue
- Cellule étape = couleur de fond pleine selon la tonalité (Doux =
rubis-glow, Standard = cream-2, Ferme = ink, Strict = rubis-deep)
avec affichage J+X / numéro du jour
- Cellule jour normal = numéro muted, today souligné en rubis-glow
- Click sur cellule étape → sélection, l'éditeur (offset, ton,
supprimer) apparaît directement sous le calendrier
- Légende des tonalités juste sous l'en-tête
- Affiche tous les mois entre la 1re et la dernière étape (échéance
incluse) — typiquement 1 à 2 mois en pratique
- Mêmes raccourcis qu'avant : OffsetInput string-controlled qui accepte
les états intermédiaires "" et "-"
Suppression de CadenceTimeline.tsx (la liste verticale précédente).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Le précédent layout avec ◆ rotatés en timeline causait des collisions
visuelles sur mobile (les coins du diamant débordaient sur les labels et
la ligne de connexion). Inutilisable.
Nouvelle approche, inspirée des éditeurs de séquences éprouvés
(Mailchimp, Klaviyo) : liste verticale de cards de step, identique
sur mobile et desktop. Plus prévisible, plus lisible, mêmes tap targets.
- Chaque step = card cliquable avec : numéro d'ordre, ◆ accent (petit,
coloré par tonalité, signature de marque sans gêner la lecture),
J+X, label de tonalité, bouton retirer aligné dans le flux
- La card sélectionnée (rubis border + shadow) révèle l'éditeur inline
(Décalage + Tonalité) directement sous l'en-tête → pas de panneau
séparé, pas de saut de focus, l'utilisateur édite ce qu'il vient
de taper
- Bouton "Ajouter une étape" en pleine largeur en pied de liste
- L'avertissement mise-en-demeure (validation manuelle) s'affiche dans
la card sélectionnée
- OffsetInput déplacé dans CadenceTimeline avec le reste de l'éditeur ;
duplication supprimée du fichier route
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Variables
- Le clic sur un chip de variable insère désormais au curseur du dernier
champ focus (sujet OU corps), pas seulement dans le corps. On capture
la position via onSelect/onClick/onKeyUp/onBlur et on utilise mousedown
+ preventDefault sur les chips pour que le focus ne quitte pas le champ
ciblé avant l'insertion. Le label sous les chips indique en live
quel champ est ciblé.
- OffsetInput (étape Cadence) : composant string-controlled qui accepte
les états intermédiaires "" et "-" pour ne plus avoir le 0 fantôme
quand on efface pour ressaisir un offset négatif.
Mobile
- Bottom nav (Annuler/Continuer) sticky en bas sur mobile, en flux normal
sur desktop. Safe-area inset respectée.
- Header du wizard : back button compact (icône seule sous sm), compteur
d'étape toujours visible, stepper centré.
- Card padding adaptatif (p-5 sm:p-7 lg:p-9).
- Step 3 — sélecteur d'étape : scroll horizontal sur mobile (au lieu de
wrap), évite l'effet escalier avec 5 étapes.
- Step 3 — body textarea : min-h adaptatif (180px mobile, 260px sm+).
- CadenceTimeline : rail horizontal masqué sous lg ; en mobile, ligne
verticale fine entre les nœuds (cohérent identité ◆) ; bouton retirer
visible en permanence sur mobile (les hover-only ne marchent pas tactile).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backend
- migration : champs contact_first_name / contact_last_name (nullable)
sur clients pour personnaliser les variables de relance
- POST /api/v1/plans : création de plan custom avec slug auto-généré
(suffixé en cas de collision, "nouveau"/"new"/"create" réservés)
- POST /api/v1/ai/generate-relance : génération de subject+body via
mistral-small-latest, avec brief utilisateur et tonalité ciblée
- mail_dispatcher : nouvelles variables {{daysLate}}, {{issueDate}},
{{user.fullName}}, {{user.companyName}}, {{client.contactFirstName}},
{{client.contactLastName}} (helper buildRelanceVars exposé pour preview)
- send_relance_job preload désormais l'organization pour exposer son name
Frontend
- /plans/nouveau : wizard 4 étapes (Identité → Cadence → Messages → Récap)
- Stepper en haut, navigation guidée, validation par étape
- Étape 1 : nom + tonalité globale (4 cards Doux/Standard/Ferme/Strict)
avec aperçu de la cadence par défaut associée
- Étape 2 : timeline horizontale (rail rubis-glow + nœuds ◆ teintés
selon la tonalité), édition décalage/ton de l'étape sélectionnée
- Étape 3 : édition par étape avec preview live à droite, chips de
variables cliquables, bouton "Générer avec l'IA" qui ouvre une modale
Mistral (brief + résultat + régénérer)
- Étape 4 : récap avec preview de chaque email rendu sur un client fictif
- Détection des variables sensibles → warning si X clients existants n'ont
pas le champ contactFirstName/contactLastName rempli (UX informative,
fallback vide à l'envoi)
- "Dupliquer" sur chaque card de plan → /plans/nouveau?from=<slug>
pour pré-remplir le wizard à partir d'un plan existant
- ClientCreateDialog : ajout des champs prénom/nom du contact dédié
- TEMPLATE_VARIABLES étendu, helper renderTemplate côté front en miroir
exact de l'implémentation API
- MSW handlers ai/plans/clients alignés sur le nouveau contrat
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remplace le placeholder par 4 sections fonctionnelles, chacune avec son
form indépendant et son Save (blast radius clair : modifier sa signature
ne sauvegarde pas l'org).
Layout : sections verticales avec gap large, pas de tabs ni sidebar
interne en V1 (mono-utilisateur, peu de surface). Pattern type Linear /
Stripe : eyebrow + titre + description à gauche (280px), Card form à
droite (1fr). Empilé sur mobile.
Sections :
1. Compte — AccountForm : fullName + email. Synchronise authStore
après save → topbar greeting / sidebar avatar se mettent à jour
live. Save désactivé si form.state.isDirty=false.
2. Entreprise — OrganizationForm : nom + SIRET (14 chiffres) + chips
volume mensuel (réutilise le pattern de l'onboarding step 2).
Fetch GET /organizations/me, PATCH au save, setQueryData pour
éviter un refetch.
3. Signature — SignatureForm : Textarea + aperçu live dans Card flat
avec eyebrow + Sparkles (cohérent onboarding step 3). PATCH
/account/profile avec field signature.
4. Zone danger — DangerZone, variant 'danger' sur SettingsSection
(border rubis-deep/30 dashed + bg rubis-glow/20 — sobre, pas
alarmiste). Logout fonctionnel (duplique UserMenu, c'est OK et
attendu dans les paramètres). Suppression compte disabled
(bientôt) avec mention 'RGPD article 17'.
Composants nouveaux :
- SettingsSection : pattern visuel commun, prop tone='default'|'danger'
- AccountForm, OrganizationForm, SignatureForm, DangerZone
MSW : ajout GET /api/v1/organizations/me (on n'avait que le PATCH).
Bundle prod : 116.21 KB gzip core (-1.76 KB grâce au tree-shaking
mutualisé des deps form).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Réflexion produit : email required vs optionnel.
Le coeur de Rubis = relances email automatiques. Sans email client →
aucune relance ne peut partir → la fiche client est inutilisable pour
le coeur du produit. Décision : email REQUIRED partout, plutôt que
laisser créer des fiches mortes.
Type Client (packages/shared) :
- email: string (était string | null)
- siret: string | null ajouté (optionnel mais recommandé pour mises
en demeure formelles + intégrations comptables V2 type Pennylane)
ClientCreateDialog (modale "+ Nouveau client" sur /clients) :
- Email required avec validator Zod min(1).email()
- SIRET ajouté côte-à-côte avec Téléphone (validator 14 chiffres
ou vide, inputMode='numeric', espaces tolérés à la frappe)
- Adresse postale déplacée full-width (lisibilité)
- Hints éducatifs : 'Préférez compta@/facturation@ à une nominative',
'Recommandé pour les mises en demeure', 'Requise pour les mises en
demeure formelles'
Field component aligned :
- Label/hint en haut, input en bas (mt-auto sur le wrapper input)
- Quand 2 Fields sont côte-à-côte avec hints de longueur différente,
les inputs restent alignés au bas — le hint plus long étire le haut
- Erreur reste collée sous l'input (pas en bas de la cellule)
MSW :
- POST /clients schema strict : email required, siret 14 chiffres si fourni
- Détection doublon par nom (409) conservée
- Handlers création de client implicites (saisie facture, OCR review)
refusent maintenant la création quand email manquant : 422 ciblé
'Email du client requis — Rubis en a besoin pour envoyer les relances.'
Si l'user pick un client existant via le combobox → email déjà en
DB, pas demandé.
Migration mockDb :
- Anciens clients sans siret → null
- Anciens clients avec email null (cas test) → placeholder dérivé du
slug du nom (contact@boulangerie-martin.fr) — éditable, juste évite
un crash au load. slugifyClientName() supprime SARL/SAS/EURL et accents.
Détail /clients/$id :
- SIRET ajouté dans la barre meta du header (Hash icon Lucide +
tabular-nums) — affiché seulement si rempli
- Email plus conditionnel (toujours présent maintenant)
Seeds :
- Boulangerie Martin SARL : SIRET 82345678900012
- Cabinet Rousseau : SIRET 53412987600028
- Atelier Durand, Garage Lemoine, Studio Lefèvre : siret null
(pour tester les deux cas dans la liste)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Page /clients (liste) :
- Header dynamique : 'X factures en retard chez Y clients' en rubis-deep
s'il y en a, sinon 'Tout est calme côté clients.'
- Recherche par nom/email (param q côté serveur, debounce naturel via
TanStack Query staleTime: 10s)
- Table desktop / cards mobile (cohérent avec /factures)
- Tri serveur : retards d'abord (actionable), puis activité récente
- Empty state distincts (recherche vide vs jamais de clients)
- Lien depuis 'Voir tout' du panel TopLatePayers du dashboard fonctionne
- '+ Nouveau client' disabled (V2)
Page /clients/$id (détail) :
- Header : eyebrow contextuel, nom h1, infos contact (mail clickable,
phone, address) avec icônes Lucide ink-3
- 4 KPI cards en grille : Factures actives (avec sub-info 'N en retard'
rubis-deep si pertinent), En attente, Encaissé total, Factures payées
- Liste des factures du client (cliquables vers /factures/$id) avec
StatusBadge sans icône (compact)
- Notes internes : Textarea avec autosave on blur via PATCH /clients/:id
MSW :
- GET /clients?withStats=1&q= : enrichit avec compteurs + montants +
lastActivityAt. Tri par retards d'abord
- GET /clients/:id : détail enrichi + invoices triées plus récentes
- PATCH /clients/:id : édition Zod
- mockDb.updateClient(orgId, id, patch) ajouté
Persistance session mock (stay logged in après reload) :
- mocks/sessionStore.ts : helpers localStorage simulant le cookie
httpOnly côté serveur. TTL 30j (= refresh token typique). SPA n'y
accède jamais directement, seul MSW touche cette persistance.
- POST /auth/{login,signup} : sessionStore.set après succès
- POST /auth/logout : sessionStore.clear (clean disconnect)
- POST /auth/refresh : retourne la session stockée + recharge le user
depuis mockDb au cas où il a été modifié (signature post-onboarding etc.)
- main.tsx : bootstrapSession() avant le 1er render (silent refresh).
Évite le flash redirect /login pour les users déjà connectés.
Architecture : le SPA n'accède jamais directement à localStorage —
il passe toujours par HTTP (/auth/refresh). Quand on branchera le vrai
backend Adonis, on supprime juste mocks/sessionStore.ts et le pattern
continue à marcher (cookie httpOnly remplace localStorage côté serveur).
queryKeys.clients.list ajouté pour le param de recherche.
Bundle prod : 117.92 KB gzip core (stable +0.28 vs avant).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
L'écran de review OCR avait un Input texte libre pour le nom du client,
ce qui faisait qu'on créait un nouveau client à chaque validation même
quand le nom matchait un client existant — doublons assurés.
Maintenant l'OCR fait le matching en amont :
- L'extraction côté MSW (fakeOcrExtract) cherche un client existant par
nom case-insensitive et pré-remplit clientId dans extracted/edited.
Confidence clientName = 1 quand match (vs 0.95 sinon).
- DraftFields type ajoute clientId: string | null
- draftFieldsSchema (validation) ajoute clientId nullable
Côté UI :
- L'Input clientName devient un ClientCombobox (le même que pour la
saisie manuelle — chunk mutualisé 26 KB gzip)
- Border rubis quand un client existant est sélectionné
- Hint contextuel sur le Field :
· clientId set → "Lié à un client existant ✓"
· clientId null + nom ≥ 2 chars → "Nouveau client — sera créé à la validation."
· Sinon → "Tapez pour rechercher ou créer un client."
Validate handler MSW (résolution client en cascade) :
1. clientId explicite (combobox) → utilise direct, zéro lookup
2. Match par nom case-insensitive sur les clients existants → utilise si match
3. Création à la volée si rien ne matche
Fallback création si clientId fourni mais introuvable.
Migration mockDb : les batches d'import seedés avant l'ajout du champ
sont patchés à load() avec clientId ?? null (spread des données stockées
d'abord pour ne pas écraser les snapshots récents).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Modale 'Nouvelle facture' (cf. wireframe 2.3) accessible depuis 4 points :
- Topbar '+ Saisir' (était disabled)
- /factures/import bouton 'Saisir manuellement' (header)
- Dropzone empty state sur /factures (variant full)
- (Reachable de partout dans _app/* via le topbar)
Composants ajoutés :
- Dialog : wrapper Radix Dialog stylé (overlay ink/35 + blur, content
bg-cream + border-line + shadow-card, close button discret, animations
fade+zoom). Header / Title / Description / Footer / Close.
- ClientCombobox : autocomplete maison (pas Radix Combobox qui n'existe
pas, pas cmdk overkill). Input + dropdown filtré, click-outside ferme,
Escape ferme, option 'Créer le client « X »' quand pas de match exact.
Border rubis quand un client existant est sélectionné.
- ManualInvoiceDialog : form complet (TanStack Form + validateurs Zod
par champ). Client (combobox), N° + date émission (côte-à-côte), montant
+ échéance relative 15/30/45/60/90j (Select Radix), plan de relance.
Architecture clean :
- ManualInvoiceProvider au sommet d'AppLayout rend la modale une seule
fois (un seul réseau de portals Radix)
- Hook useManualInvoice() expose open()/close()/isOpen, accessible
depuis n'importe quelle route enfant sans plumber des callbacks
- État local de la modale (pas dans l'URL — propre pour V1)
Logique métier MSW :
- GET /api/v1/clients (autocomplete)
- POST /api/v1/invoices : résolution client (clientId fourni → utilise,
sinon match par nom case-insensitive, sinon création à la volée).
+1 rubis bonus saisie.
- Conversion relativeDueDays (15/30/45/60/90) → dueDate absolue à la
soumission
Bug fix montant TTC :
- L'input était contrôlé avec value={(cents/100).toFixed(2)} → reformat
à chaque keystroke écrasait '10000' en '1.00' (impossible de taper
des gros montants)
- Passé en defaultValue (uncontrolled) avec step='any' + inputMode='decimal'
- Accepte virgule FR (1240,50) et point (1240.50)
- DialogContent unmount à la fermeture → defaultValue ré-évalué à
chaque réouverture (reset OK)
Bouton '+ Saisir' du topbar plus disabled, bouton 'Saisir manuellement'
de /factures/import plus disabled. Le bouton dans la dropzone (variant
full) reçoit un onManualEntry prop optionnel.
Bundle prod : 117.62 KB gzip core (+0.06 KB), useManualInvoiceDialog
chunk 6.68 KB gzip, Select chunk 25.14 KB gzip (partagé OCR + plan
editor + manual entry).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Le bouton '+ Importer factures' du topbar avait un Button inerte. Il
ouvre maintenant une vraie page focused dédiée :
- Route /factures/import (factures_.import.tsx) avec breadcrumb,
eyebrow, H1 'Importer *plusieurs* factures.', lede explicatif,
dropzone full-page avec mutation upload câblée
- Drop-catcher de page comme sur /factures (drop n'importe où marche)
- 3 hints discrets en bas (Formats / Confidentiel / Reprenable) pour
rassurer le user au moment décisif de l'upload
Routing nesting fix :
- Renommé factures_.import.\$batchId.tsx → factures_.import_.\$batchId.tsx
- Trailing underscore sur 'import_' escape la nouvelle landing parent
- Les 2 routes sont maintenant siblings sous _app :
· /factures/import → factures_.import.tsx
· /factures/import/\$batchId → factures_.import_.\$batchId.tsx
Topbar AppLayout :
- '+ Importer factures' = Button asChild + Link to /factures/import
(middle-click / cmd-click / right-click ouvrent un nouvel onglet)
- '+ Saisir' reste disabled (placeholder modale 2.3, prochaine étape)
Bundle prod : 117.56 KB gzip core (stable, +0.06 vs avant).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boucle import complète (cf. wireframe 2.2) :
1. Drop PDF/PNG/JPG sur /factures (dropzone full-page si vide, compact en
bas si populée, OU drop n'importe où sur la page grâce au drop-catcher
de route — évite que le browser ouvre le fichier dans un onglet)
2. POST /invoices/upload → MSW génère un batch avec drafts pré-remplis
(OCR simulé : nom client aléatoire depuis 7 entreprises plausibles,
montant random, dates calibrées, confidences variables) + délai 800ms
3. Toast "X factures extraites. Vérifions ensemble." + navigate vers
/factures/import/$batchId
4. Page review step-by-step : PDF preview à gauche + form à droite,
champs douteux (confidence < 0.7) surlignés border-rubis + hint
inline, bandeau warning rubis-glow si plusieurs champs incertains
5. Valider & suivante → POST validate → crée la facture en mockDb
(nouveau client si nom inconnu) + 1 rubis bonus → la suivante
apparaît automatiquement
6. Skip ou Annuler le batch entier disponibles à tout moment
7. Fin de batch → toast bilan ("X validées · Y ignorées") → /factures
Composants ajoutés :
- PdfPreview : placeholder anti-générique (pas un viewer gris) — header
mono filename + "page A4" simulée avec barres bg-ink/15 et bg-rubis-glow
- Select : wrapper Radix Select stylé (Trigger / Content / Item) cohérent
avec Input (1px line, focus rubis-glow ring, item sélectionné rubis + ✓)
Dropzone amélioré :
- Filtre fichier plus tolérant : MIME OU extension (Finder/Explorer
envoient parfois type === ""), erreur dédiée taille vs format
- Mode isUploading : titre devient "On lit vos factures…", spinner
sur le bouton Parcourir
MSW handlers (invoices.ts) :
- POST /invoices/upload : crée batch + drafts avec OCR simulé
- GET /invoices/import-batch/:id
- POST /invoices/import-batch/:id/drafts/:draftId/validate
- POST /invoices/import-batch/:id/drafts/:draftId/skip
- DELETE /invoices/import-batch/:id
mockDb étendu :
- importBatches store + StoredImportDraft type
- createImportBatch / findImportBatch / updateImportDraft / deleteImportBatch
- createInvoice / createClient / listClientsForOrg
Bug fix migration :
- Le sessionStorage stockait des snapshots d'avant l'ajout du champ
importBatches → db.importBatches undefined → push() crashait. Ajout
d'une migration douce dans load() qui patche les champs manquants
avec les défauts du seed (pas de perte de données existantes).
Bundle : 117.50 KB gzip core. Route factures_.import._batchId 10.26 KB
gzip — la plus grosse à cause de Radix Select + state form complexe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bibliothèque /plans (cf. wireframe 3.1) :
- Grid responsive 1/2/3 cols avec PlanCard + CreatePlanCard placeholder
- PlanCard : titre, chip meta (un seul à la fois), aperçu 3 étapes avec
◆ rotated comme bullet, footer usage + lien "Modifier →"
- Le plan le plus utilisé reçoit le badge "✦ Le plus utilisé" (rubis-glow
+ Sparkles), les autres gardent leur label de tonalité (Doux / Standard
/ Ferme / Strict). Pas de "PLAN PAR DÉFAUT" partout — info tautologique
vu que les 4 plans seedés sont des défauts.
- Chips de tonalité adoucis (bg-cream-2 ou rubis-glow, plus de fills lourds)
- Skeleton pulsé pendant le fetch
Éditeur /plans/$slug (cf. wireframe 3.2, route _app/plans_.$slug pour
escape la layout parent) :
- Header : eyebrow humeur + nom + compteur d'usage + boutons Dupliquer
(V2) / Enregistrer (fonctionnel, désactivé tant que pas de changements)
- Layout 2-col responsive (1fr / 1.4fr) :
· Gauche : cadence — list de StepCard cliquables, état sélectionné avec
border-rubis + shadow-rubis, "+ Ajouter une étape" disabled (V2)
· Droite : éditeur d'email — Card avec subject (Input), body (Textarea
mono 10 rows), grille de variables-chips
- Variable insertion fonctionnelle : clic = insertion au curseur via
selectionStart/End du textarea, label FR + token mono ({{numero}})
- Bandeau warning rubis-glow quand l'étape est requiresManualValidation :
"Validation manuelle obligatoire. L'email est généré en brouillon"
- Save fonctionnel : isDirty calculé via JSON.stringify, mutation PATCH
/plans/:slug, invalidate cache plans.all + setQueryData detail, toast
- Sync state local ↔ serveur via useEffect sur plan.id+updatedAt
MSW :
- handlers/plans.ts : GET /plans (avec usageCount), GET /plans/:slug,
PATCH /plans/:slug (validation Zod, recompose ids manquants)
- mockDb : findPlanBySlug, listPlansForOrg, updatePlan
- Calcul usageCount : factures du plan en statut != paid && != cancelled
Lib /plans.ts :
- TONE_LABELS : Amical / Standard / Ferme / Mise en demeure (FR)
- planMoodLabel + planOverallTone (humeur globale = ton de la dernière étape)
- TEMPLATE_VARIABLES : 5 variables avec token + label FR + preview
Bundle prod : 117.31 KB gzip core (stable). plans 2.06 KB gzip,
plans_._slug 3.28 KB gzip — la plus grosse route chunk vu sa complexité
(form + variables + state local).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Monorepo Turborepo (pnpm workspaces) avec 3 packages :
- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
• TanStack Router (file-based, auto code-splitting), Query, Form
• Radix primitives bruts + CVA + clsx + tailwind-merge
• MSW pour mocker l'API tant qu'Adonis n'est pas branché
• Polices Bricolage Grotesque + Inter self-hostées via fontsource
• Tokens marque (rubis, cream, ink) exposés via @theme
• Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
• Route /login full flow : TanStack Form + Zod + mutation Query
- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
• Auth access tokens (Bearer) — cf. ADR-017
• Tuyau core déjà câblé pour la génération de types
• Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
• Minimal — uniquement le pont front ↔ back
- packages/shared : types TS + schemas Zod + constantes
• Source unique de vérité partagée api ↔ web
• Domaines : User, Org, Auth, Client, Invoice, Plan
Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.
CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.
Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>