Première moitié de la feature marque blanche : la machinerie complète qui
permet à un compte Business d'envoyer ses emails de relance avec son
propre logo, ses propres couleurs et son nom comme expéditeur, à la place
du branding Rubis.
Architecture :
- Nouvelle colonne JSONB `organizations.brand_settings` (12 tokens
customisables : logo, senderName, et 10 couleurs — primary, banner,
body bg, card bg, text, text muted, border, link, button text).
Null = palette Rubis intacte. Validation hex stricte (#RRGGBB).
- Service `#services/brand` avec `resolveBrandTokens(org)` qui merge
defaults + overrides en respectant le plan (couleurs/logo = Business
only ; senderName = cascade pour tous les plans). Mergeurs avec
sémantique "null = reset au default sur ce champ précis" pour les
patches partiels.
- Service mutualisé `#services/media_storage` qui remplace l'ancien
`blog_uploads.ts`. Scopes `blog` (4 MB, jpg/png/webp) et `brand-logo`
(1 MB, + svg accepté). Cleanup automatique du logo précédent lors
d'un remplacement (pas de versioning — la conv produit est "on écrase").
- Controller `BrandController` (5 endpoints) + middleware
`AssertBusinessPlanMiddleware` qui throw 403 `business_plan_required`
(code matché par le SPA pour l'upsell card).
- Refactor des 3 templates mail (relance, payment thanks, checkin) +
layout commun pour accepter `tokens: BrandTokens` en prop. Le
dispatcher résout les tokens per-org pour relance + remerciement
(= user → client, branded), et passe `DEFAULT_BRAND` au checkin
(= Rubis → user, toujours Rubis-branded).
- Routes publiques pour le logo : `/api/v1/uploads/brand-logos/:filename`
(sans auth, cache immutable, X-Content-Type-Options: nosniff pour les SVG).
UI self-service arrive dans la prochaine version (v1.12.0). En attendant,
un compte Business peut être configuré via Bruno / API directe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Composant `<VersionToast/>` monté au root de la SPA qui s'affiche quand
l'utilisateur ouvre l'app sur une version plus récente que la dernière
qu'il a vue.
Mécanique :
- Source de vérité = `apps/web/src/version.ts` (constante semver, à bumper
manuellement à chaque release, en même temps que l'ajout du .md
correspondant dans `apps/landing/src/content/changelog/`).
- Comparaison à `localStorage["rubis:last-seen-version"]` au mount.
- 1re visite (clé absente) → on enregistre la version courante en silence,
pas de toast (sinon spam d'onboarding).
- Version identique à la dernière vue → rien.
- Version différente → toast Sonner persistant (`duration: Infinity`)
avec icône Sparkles rubis et action "Voir les nouveautés ↗" qui ouvre
`rubis.pro/changelog#<version>` dans un nouvel onglet. Au moment de
l'affichage on enregistre la version comme vue — donc même si l'user
ferme sans cliquer, plus de toast pour cette version (il a été informé).
- localStorage indisponible (private mode) → fail silent.
Version initiale : `1.10.0` (remerciement automatique au client, dernière
entrée du changelog).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>