5 Commits

Author SHA1 Message Date
ordinarthur
6dcae6956c feat(blog): admin CRUD + image upload + sidebar link
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m32s
Build & Deploy API / build-and-deploy (push) Successful in 2m20s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m20s
L'éditeur du blog (jusqu'ici limité au seeder) a maintenant une vraie
interface au-dessus de l'API.

Backend (apps/api) :
* Migration users.is_admin (boolean default false).
* Middleware admin (404 si user.is_admin=false après auth).
* Commande ace promote:admin --email=… [--revoke].
* AdminPostsController CRUD complet : list/show/store/update/publish/
  unpublish/destroy + suggest-slug. Au save, contentHtml + wordCount +
  readingTime sont re-calculés via blog_renderer. Au publish, durcit la
  validation SEO (titre ≤60, excerpt 120-160, hero+alt requis, ≥600 mots),
  flippe status='published' + publishedAt, ping Google+Bing pour le sitemap.
* BlogUploadsController :
  - POST /api/v1/admin/uploads (multipart, JPEG/PNG/WebP, max 4MB)
    → MinIO clé uploads/blog/{uuid}.{ext}
    → renvoie URL relative /api/v1/uploads/blog/{filename}
  - GET /api/v1/uploads/blog/:filename (public, cache immutable 1 an)
    → stream depuis MinIO, regex anti-traversal sur le nom.
* UserTransformer expose isAdmin (cf. shared/types/user).
* k3s/app/landing.yml : NodePort 30111 explicite (pour Traefik repo proxmox).

Frontend (apps/web) :
* Lib typée admin-blog (calls API, queryKeys, helpers URL).
* Route /admin/blog : liste filtrable avec status badge, ouverture
  publique, dépublier, supprimer, "+ Nouveau brouillon".
* Route /admin/blog/:id : éditeur 2-colonnes
  - Gauche : @uiw/react-md-editor (lazy import) avec preview live.
  - Droite : hero image (drag&drop + alt), excerpt avec compteur
    120-160, tags, aperçu Google snippet, validations bloquantes.
  - Autosave debounce 2s + bouton Publier qui sauve d'abord.
  - Hero image upload via MinIO (HeroImageUpload component).
* Sidebar : lien "Blog (admin)" si user.isAdmin.
* Gate côté client (beforeLoad redirect si non admin) + côté serveur
  (middleware admin) — defense in depth.

Note : les requirements de publish miroir backend ↔ frontend (cf.
PUBLISH_REQUIREMENTS dans validators/post.ts et VALIDATION_RULES dans
admin.blog_.\$id.tsx). À synchroniser si un seuil bouge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 17:25:34 +02:00
ordinarthur
77fdb6af48 feat: email de remerciement automatique après confirmation de paiement
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 38s
Build & Deploy API / build-and-deploy (push) Successful in 1m43s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m16s
Quand l'utilisateur confirme « Oui, payé » via check-in (lien email ou modale
in-app) ou marque une facture encaissée manuellement, on envoie automatiquement
un email de remerciement chaleureux au client final. Subject + body éditables
par plan (mêmes variables que les relances), avec fallback hardcodé si vide.
Gardé par la transition `* → paid` pour idempotence ; envoi async via BullMQ
avec retry exponentiel ; capture en mode démo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 16:41:26 +02:00
ordinarthur
9e531e32a9 feat(plans): wizard de création de plan custom + génération IA Mistral
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>
2026-05-06 22:55:00 +02:00
ordinarthur
16120ed3e0 feat(web): création client (modale) + email required + SIRET optionnel
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>
2026-05-06 12:25:37 +02:00
ordinarthur
8d3bab6a89 feat: scaffold frontend monorepo + first /login screen
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>
2026-05-06 10:10:48 +02:00