141 Commits

Author SHA1 Message Date
ordinarthur
e0b47ddfdc feat(invoices): éditeur de factures natif — data model + API (Phase 1)
Pose les fondations pour permettre aux utilisateurs de créer leurs
factures directement dans Rubis (en complément de l'upload OCR existant),
avec snapshots immuables, numérotation strict séquentielle (art. 242
nonies A CGI) et 4 thèmes pré-faits paramétrables.

Data model
- organizations.invoice_settings (JSONB) : thème par défaut, accent color,
  préfixe et compteur de numérotation, mentions légales (pénalités,
  escompte), identité émetteur (SIREN/SIRET/TVA intra/RCS/capital), RIB.
- clients enrichi : SIREN, TVA intra, adresse structurée (lines/zip/city
  /country). Le champ address legacy reste pour les clients pré-feature.
- invoices enrichi : lines (JSONB), client_snapshot + issuer_snapshot
  figés à l'émission, amount_ht/tva, tva_breakdown, payment_terms_days,
  theme_slug + theme_accent_color, is_native, sequence_number (unique
  per org), pdf_generated_at.

API
- GET/PATCH /organizations/me/invoice-settings (resolveInvoiceSettings)
- GET /invoice-themes (4 thèmes : classique, moderne, minimal, élégant)
- POST /invoices/native (séquence strict allouée en transaction,
  totaux recalculés serveur, snapshots immuables)
- POST /invoices/preview-pdf (stream PDF sans persister, stub Phase 1)

Le rendu PDF lui-même (@react-pdf/renderer + templates) arrive en
Phase 2 ; le storeNative crée bien la facture mais pdf_storage_key
reste null jusqu'à Phase 2. Conformité Factur-X visée pour V1.5
(Q3-Q4 2026, avant l'échéance d'émission TPE-PME au 1er sept 2027).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:07:45 +02:00
ordinarthur
1200c549a0 feat(landing): section "Mode automatique bientôt" + FAQ + pricing
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m2s
Annonce la connexion bancaire auto sur la landing pendant la fenêtre
KYC Powens. Trois touches cohérentes pour préparer le marché aux
plans Pro / Business :

- Nouvelle section AutoBanking entre Gamification et Legal : badge
  "Bientôt disponible", h2 « Plus jamais besoin de répondre "C'est
  payé" », 4 bénéfices clés (détection temps réel via Powens AISP,
  toutes banques françaises, mode validation ou auto-pilote, lecture
  seule), badge "Inclus sur Pro et Business", mention DSP2 / AISP
  ACPR pour la conformité. Mock visuel d'email "Garage Lemoine a
  payé F2026-0013 · 4 189,40 €" avec halo glow rubis, récap
  client/facture/montant + check-list "facture payée · relances
  annulées · remerciement envoyé · +1 rubis".
- FAQ : enrichit la 1re question (paiement hors plateforme) avec
  une teaser vers la section AutoBanking, et ajoute une nouvelle
  question dédiée « La connexion bancaire, c'est sécurisé ? Vous
  pouvez bouger mon argent ? » avec réponse explicite sur le statut
  AISP (lecture seule, DSP2, révocation 1 clic).
- Pricing : ajoute une feature dans la liste des inclus Pro avec
  mention "à venir" pour la détection bancaire automatique.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 14:21:14 +02:00
ordinarthur
3207f873e9 feat(banking): mode "Bientôt disponible" pendant la fenêtre KYC Powens
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 36s
Build & Deploy API / build-and-deploy (push) Successful in 1m29s
Ajoute un état intermédiaire entre "feature désactivée silencieusement"
et "feature pleinement active" : un teaser visible dans /parametres
pour les Pro/Business qui annonce que la connexion bancaire arrive,
avec une note rassurante sur la lecture seule. Permet d'annoncer la
feature aux users payants pendant le délai d'agrément AISP / KYC
Powens, sans risque de cliquer dans le vide.

- Nouveau flag d'env `BANKING_TEASER_ENABLED` (boolean, default false)
- `GET /banking/status` renvoie désormais `{ enabled, comingSoon }`
  où comingSoon = !enabled && BANKING_TEASER_ENABLED
- `BankingSection` : nouveau composant `ComingSoonCard` (halo glow,
  copy explicite sur l'agrément AISP en cours, rassurance lecture
  seule) affiché quand comingSoon=true et l'org est Pro/Business
- `parametres.tsx` : la section "Banque" apparaît si enabled OU
  comingSoon (au lieu de uniquement enabled)
- ConfigMap K3s : `BANKING_TEASER_ENABLED='true'` en prod pour
  annoncer la feature aux clients payants pendant le KYC

Trois états possibles désormais :
  enabled=true                       → feature active (post-KYC)
  enabled=false + comingSoon=true    → teaser "Bientôt disponible"
  enabled=false + comingSoon=false   → section invisible (kill switch dur)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 14:14:33 +02:00
ordinarthur
51217175ad feat(banking): intégration Powens AISP + auto-réconciliation factures
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 38s
Build & Deploy API / build-and-deploy (push) Successful in 1m36s
Module banking complet en lecture seule via Powens (ex-Budget Insight)
pour détecter automatiquement les paiements clients et arrêter les
relances dès qu'une facture est payée. Réservé plans Pro / Business,
kill switch global BANKING_ENABLED désactivé en prod tant que le KYC
Powens n'est pas validé (cf. .claude/deploy-memory.md).

Backend (apps/api)
- PowensClient bas niveau : init user, code temporaire 30s, build
  webview URL, list/get/delete connections, accounts, transactions,
  vérif HMAC SHA-256 timing-safe pour webhook.
- BankingService : ensurePowensUser (chiffrement token via Adonis
  encryption / APP_KEY), createWebviewUrl avec state HMAC anti-CSRF
  (TTL 10 min), handleCallback (upsert connection + accounts +
  fire-and-forget mail + sync 90j + reconcile), disconnect (DELETE
  Powens + soft-revoke en DB), setReconciliationMode.
- Réconciliation : match transactions ↔ factures sur montant exact
  + label normalisé (numero ou nom client, NFD strip + alphanum).
  Confiance HIGH (label matche) vs LOW (montant seul). Mode auto +
  HIGH → invoice.status=paid + bonus rubis + cancel relances +
  enqueuePaymentThanks (client) + sendInvoiceAutoPaidNotification
  (user). Mode manual ou LOW → match_status='suggested' (UI V2).
- Webhook /webhooks/powens : vérif HMAC, lookup org par
  powens_user_id, dispatch CONNECTION_SYNCED / NEW_TRANSACTIONS /
  USER_SYNC_ENDED → sync incrémental 7j + reconcile, CONNECTION_ERROR
  / SCA_REQUIRED → update state + last_error. Réponse 200 immédiate
  puis processing fire-and-forget pour ne pas timeout côté Powens.
- 4 migrations : bank_connections, bank_accounts, bank_transactions
  + colonnes powens_user_id (chiffré APP_KEY) et reconciliation_mode
  sur organizations.
- 2 templates React Email : BankConnectedEmail (post-connection,
  récap comptes + lien settings) et InvoiceAutoPaidNotificationEmail
  (notif user après match auto, lien direct facture + libellé
  bancaire détecté). Toujours en branding Rubis (notif Rubis → user,
  jamais marque blanche).
- 2 commandes ace : banking:reconcile (rejoue le reconcile sans
  reconnecter la banque) et banking:simulate-payment (injecte une
  bank_transaction synthétique qui matche une facture, pour test E2E
  sans devoir attendre un vrai virement sandbox).
- Kill switch isBankingEnabled() : flag BANKING_ENABLED + check des
  credentials Powens. Endpoint public GET /banking/status renvoie
  { enabled }, /banking/powens/init throw 503 banking_disabled si OFF.
- Fix handler exceptions : UNIQUE violation composite (org, X)
  rapporte désormais la vraie colonne en faute (numero/slug/…) avec
  message lisible « Le numéro de facture "F2026-0013" existe déjà »,
  au lieu d'un message ambigu sur organization_id.

Frontend (apps/web)
- /parametres : nouvelle SettingsSection "Banque" gated par kill
  switch + plan Pro/Business. Si Free → upsell card avec CTA vers
  /parametres/abonnement. Si Pro/Business sans banque → CTA "Connecter
  une banque". Si banque connectée → carte avec accounts (IBAN
  masqué FR76 **** **** **** 1234), solde, last sync, bouton
  Déconnecter. Toggle Manuel/Auto pour reconciliation_mode.
- /parametres/banque/success : nouvelle route dédiée post-callback
  avec badge ✓ animé + halo glow rubis, récap des comptes
  synchronisés, 2 CTAs ("Voir mes paramètres" / "Retour dashboard"),
  note sécurité "lecture seule, aucun déplacement de fonds".
- Hooks : useBankingStatus, useBankConnections (avec opt-out via
  { enabled }), useInitBanking, useDisconnectBank, useBankingSettings,
  useUpdateBankingSettings.

Infrastructure (k3s)
- ConfigMap rubis-api-config : BANKING_ENABLED='false' par défaut,
  BANKING_PROVIDER='powens', POWENS_DOMAIN='rubis',
  POWENS_API_BASE_URL='https://rubis.biapi.pro/2.0/',
  POWENS_REDIRECT_URI='https://app.rubis.pro/api/v1/banking/powens/callback'.
- Secret rubis-app-secrets : 3 nouvelles clés POWENS_CLIENT_ID,
  POWENS_CLIENT_SECRET, POWENS_WEBHOOK_SECRET (valeurs sandbox posées
  via kubectl patch, à remplacer post-KYC).

Sécurité
- Token Powens chiffré au repos via Adonis encryption (AES-256-GCM,
  clé APP_KEY).
- State HMAC SHA-256 signé sur APP_KEY pour le flow webview
  (anti-CSRF + porte l'org_id à travers le redirect).
- Webhook HMAC SHA-256 sur header BI-Signature avec
  POWENS_WEBHOOK_SECRET, comparaison timing-safe.
- IBAN masqué côté API (transformer).
- Scope par org sur tous les endpoints (anti-IDOR).
- Rate limiting via le middleware Adonis existant.
- Idempotence DB : UNIQUE (org, powens_connection_id), (connection,
  powens_account_id), (account, powens_id) → rejouer un event ou un
  callback ne pose pas de problème.

Documentation
- /docs/tech/banking-setup.md : procédure complète setup dev avec
  Cloudflare Quick Tunnel, compte sandbox Powens, whitelist URLs.
- /.claude/deploy-memory.md : section "Banking (Powens) — activation
  prod" avec procédure en 6 étapes (KYC → secrets → ConfigMap →
  flip flag → smoke test), snippet kubectl patch pour rotation
  ciblée de secrets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 14:03:32 +02:00
ordinarthur
c590b489ef change logo
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 58s
2026-05-11 23:29:09 +02:00
ordinarthur
363caf8061 feat(web): UI marque blanche — page /parametres/marque + intégration
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 39s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m6s
Complète la feature marque blanche initiée dans le commit précédent
(919ebfe). L'API backend est désormais consommée par une page
dédiée dans le SPA, et le changelog v1.11.0 décrit la feature complète
plutôt que la "première brique".

Livraison côté SPA :

- `apps/web/src/lib/brand.ts` — types BrandSettings/BrandTokens (miroir
  serveur) + 5 hooks TanStack Query : useBrand (GET cache), useUpdateBrand
  (PATCH), useUploadBrandLogo (multipart), useDeleteBrandLogo, et
  useSendBrandTestEmail. Pas de retry sur le GET pour éviter de bombarder
  /brand quand l'org n'est pas Business (403 définitif).

- `apps/web/src/components/settings/BrandEmailPreview.tsx` — mock fidèle
  d'un email de relance qui réagit en direct aux color pickers. Copie la
  structure HTML/CSS de relance_email.tsx + _layout.tsx (banner, body
  pre-line, card récap, footer Rubis) pour que le user soit confiant que
  son vrai email rendra pareil.

- `apps/web/src/routes/_app/parametres_.marque.tsx` — page éditeur
  complète :
  • Header avec retour
  • Upsell card propre si l'org n'est pas Business (pas d'éditeur du tout
    pour éviter de leak des controls qui throw 403 derrière)
  • Form 2 colonnes desktop : zone upload logo (drop ou click) avec
    preview sur le bandeau effectif, input nom expéditeur, color pickers
    natifs (HTML5 + hex input) groupés en "Principales" (primary + banner)
    et "Avancées" (7 autres, accordéon fermé par défaut)
  • Live preview à droite (sticky desktop) qui se met à jour à chaque
    keystroke / pick
  • Actions : Enregistrer (diff draft → settings → PATCH), Réinitialiser
    (tous les overrides à null), Envoyer un test (qui force l'enregistrement
    préalable parce que le test utilise les tokens sauvegardés)
  • Sémantique null/undefined respectée côté patch — undefined = pas
    touché, null = reset au default Rubis sur ce champ précis

- Carte de navigation ajoutée dans `/parametres` qui linke vers
  `/parametres/marque` avec libellé adaptatif (Business = "Configurer",
  autres = "En savoir plus").

Changelog v1.11.0 réécrit pour décrire la feature complète et non plus
seulement la moitié backend. Un seul concept, une seule entrée changelog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 12:05:36 +02:00
ordinarthur
919ebfe755 feat(release): v1.11.0 — marque blanche pour le plan Business (backend)
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 36s
Build & Deploy API / build-and-deploy (push) Successful in 1m36s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m18s
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>
2026-05-11 11:37:07 +02:00
ordinarthur
7a112d3329 chore(claude-code): bypassPermissions + ask sur git commit/push
Auto-approuve toutes les actions Claude Code par défaut sauf `git commit`
et `git push` qui prompteront toujours — ces deux commandes restent sous
validation manuelle pour ne pas pousser accidentellement du code sans
validation explicite. Le skill /push déclenche naturellement ces deux
prompts à la fin de la release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:58:32 +02:00
ordinarthur
fc66775109 feat(claude-code): skill /push pour release automatisée
Le ritual de release Rubis demande de bumper `apps/web/src/version.ts`
et d'ajouter `apps/landing/src/content/changelog/<x.y.z>.md` dans le
*même* commit — sinon le toast SPA pointe sur une ancre absente côté
`/changelog`. Ce skill orchestre les deux pour éliminer la classe entière
des releases désynchronisées.

Workflow :
 1. `git status` + `git diff` pour inspecter les changements pendants
 2. Détection heuristique du type (feature/improvement/fix) → bump
    semver correspondant (minor pour feature, patch sinon)
 3. AskUserQuestion pour titre + highlights (au ton brand : produit-only,
    pas de jargon tech, une phrase d'attitude max)
 4. Edit `version.ts` + Write nouveau `.md` avec frontmatter Zod-validé
 5. Stage + commit Conventional Commits (scope `release`)
 6. Push gitea/main

Edge cases couverts : aucun changement à committer, branche ≠ main,
build cassé pré-existant (ne bloque pas la release, juste flag), conflits
remote, hook pre-commit échoue (jamais d'amend / no-verify), version
déjà utilisée, versionnage incohérent entre `version.ts` et le dossier
changelog.

Trigger : `/push`, "release ça", "sors une nouvelle version", "push avec
changelog", "deploy" (sauf si l'user dit explicitement "sans bump").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:49:39 +02:00
ordinarthur
77d52ea95c docs: aligner top-level + produit + technique + decisions sur le changelog
Les commits récents ont introduit le changelog public, le toast SPA, et la
convention de release (bump version.ts + ajout du .md dans le même commit).
Les docs reflètent maintenant ce qui est en prod :

- CLAUDE.md : V1 IN gagne la mention `/changelog` avec le mécanisme MD
  versionné + toast SPA. Table "Documents associés" gagne 3 lignes
  (`apps/landing/src/content/changelog/`, `apps/web/src/version.ts`,
  `.claude/skills/push/`).
- produit.md : nouvelle §4.9 "Changelog public et toast de version" qui
  couvre le ton produit-only, le mécanisme du toast, la première visite
  silencieuse, le RSS et le SEO.
- tech/architecture.md : ajoute `/changelog` à la table de stratégie de
  rendu (SSG), met à jour l'arbre de fichiers `apps/landing/` avec
  `content.config.ts` + `content/changelog/` + `pages/changelog/`, et
  ajoute une sous-section "Mécanique du changelog (release workflow)"
  qui décrit le couplage `version.ts` ↔ `.md`. Côté SPA, ajoute la
  sous-section "Versionnage SPA + toast de release" avant la partie auth.
- decisions.md : ADR-022 nouvelle entrée — Changelog en Markdown
  versionné (pas en DB) avec rationale (release-coupled, pas d'admin à
  maintenir, review en PR, SSG = LCP optimal, schéma Zod).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:49:37 +02:00
ordinarthur
c4910889de feat(landing/changelog): glow rubis sur la dernière version, retire les pills sur les anciennes
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 52s
Le contexte "Nouveauté" sur 11 cartes à la suite devenait du bruit visuel
(toutes les anciennes versions ont été des nouveautés à un moment, hein).
Le pill de type est maintenant réservé à la version la plus récente — sur
les autres on garde seulement la chip de version + la date.

Pour compenser et ancrer le regard sur le dernier état du produit, la
carte "latest" gagne un glow rubis multi-couches :
- Ring serré 4 px en couleur rubis-glow (contour soft)
- Drop shadow proche teintée rubis (profondeur)
- Bloom large diffus (halo ambiant)
- Animation "respiration" 5 s ease-in-out infini (variation subtile sur
  l'intensité du bloom)
- Désactivée si `prefers-reduced-motion: reduce` côté user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:16:08 +02:00
ordinarthur
847e7a3fc5 feat(web): toast "Nouvelle version" persistant + lien vers le changelog
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 35s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m6s
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>
2026-05-11 00:06:06 +02:00
ordinarthur
fc0d13e955 feat(landing): changelog public /changelog + flux RSS
Page Astro prerendered qui liste les versions livrées en reverse-chrono.
Contenu géré en MD files versionnés dans le repo via Astro content
collections (`src/content.config.ts`), pas de DB — chaque release =
1 fichier `src/content/changelog/<x.y.z>.md` ajouté en PR à côté du bump
de version applicatif.

11 entrées initiales (v1.0.0 → v1.10.0) couvrant le premier mois public :
lancement (OCR Mistral + plans par défaut + mode démo + Stripe), saisie
manuelle, SSO Google puis Microsoft, plans custom, templates email,
réécriture IA, insights, blog, marque blanche, remerciement automatique.

UI :
- Hero centré aligné DA landing (eyebrow rubis + h1 display + sub muted)
- 2 colonnes desktop : feed cartes (gauche) + sticky rail jump-nav (droite)
- Sur mobile/tablette : pas de rail, juste le feed
- Sticky rail : IntersectionObserver inline qui met en surbrillance la
  version courante quand l'user scrolle
- Anchors `#1.4.0` partageables, cliquables depuis le chip de chaque carte
- Type pills colorés : feature (rubis solid), improvement (cream-2),
  fix (line outline)
- Bullets losanges ◆ rubis cohérents avec le gem brand

SEO :
- `prerender = true` → HTML figé au build, LCP minimum
- JSON-LD WebPage avec mainEntity[TechArticle] par version → rich
  snippets Google
- Flux RSS 2.0 à `/changelog/rss.xml` (prerendered aussi)
- Auto-discovery RSS ajoutée au Layout (à côté de celle du blog)
- Lien Changelog ajouté au SiteFooter à côté de Blog

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:05:45 +02:00
ordinarthur
642747d762 docs(marketing): outils d'export logo + OG image + image LinkedIn
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 37s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m10s
Trois templates HTML autonomes pour générer les assets visuels sans
quitter le repo :

- `docs/logo-export.html` — outil interactif d'export du wordmark
  Rubis.pro en PNG (Bricolage Grotesque). 3 layouts (horizontal,
  vertical, texte seul), 5 fonds, 5 couleurs, taille 120 → 4096 px,
  format carré. Preset 120 px pour le logo de consent screen Google
  OAuth. Le canvas attend `document.fonts.ready` avant rendu pour
  éviter le fallback sans-serif.

- `docs/og-default-source.html` — recréation HTML pixel-perfect du
  `og-default.png` (1200×630) avec le nouveau wordmark `Rubis.pro` et
  `DSO*`. Export via Chrome DevTools "Capture node screenshot" pour
  remplacer `apps/landing/public/og-default.png`.

- `docs/linkedin-launch-source.html` — image carrée 1200×1200 pour
  un post de lancement LinkedIn. Format optimal pour le feed mobile
  (LinkedIn n'crop pas les carrés). Headline + stat-héros + brand.

Tous les gems sont des SVG inline (4 facettes + contour) — identité
visuelle 1:1 avec `<Gem/>` du SPA et `favicon.svg`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:29:44 +02:00
ordinarthur
3052a7e909 feat(landing): brand "Rubis.pro" + corrections copy
- Brand : suffixe `sur l'ongle` → `.pro` (attaché, muted, non italique).
  Propagé partout via `<Brand withSuffix>` — header/footer landing,
  topbar/sidebar SPA, login/signup/onboarding. Renforce l'identification
  brand requise par Google OAuth (gem seul jugé trop générique).

- Hero : dots groupés avec leur label (inline-flex) — ne se baladent plus
  en début de ligne au flex wrap. Ajout d'un topbar mock "Rubis.pro ·
  Tableau de bord" en haut de la carte hero pour identifier le mock comme
  un vrai dashboard. `DSO` → `DSO*`.

- Stats : "Trois chiffres qui devraient vous fâcher" → "Trois chiffres
  exorbitants". Sous-titre remplacé par "Et vous faites sûrement partie
  intégrante de ces enquêtes." (plus direct, moins gratuit).

- Promise : "Votre temps vaut plus que ça" → "Votre temps est plus
  précieux". Réécriture de l'amorce ("votre boîte / lundis soirs" →
  "votre entreprise / journées"). Ajout d'une ligne italique "Parfois
  moins, si votre plan par défaut est bien réglé".

- HowItWorks : Step 01 — suppression de "à la caisse" et de ", RIB"
  (l'OCR ne lit pas le RIB en V1). Step 03 — "La machine fait le reste"
  → "L'algorithme fait le reste".

- Gamification : suppression de "Pas un PDF abscons" (jargon inutile) et
  de "Et oui, on garde un classement amical" (le classement n'est pas en
  V1). `DSO` → `DSO*`.

- Footnotes : ajout de la définition DSO (Days Sales Outstanding) sous
  celle d'OCR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:48:17 +02:00
ordinarthur
c3c9dbb408 add des betises
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
2026-05-09 20:11:33 +02:00
ordinarthur
fb248553a8 docs(audit-3/3): aligner brand + marketing sur le code livré
Audit cross-doc/code, batch brand & marketing : nettoyage de l'or
accent obsolète, correction des chemins cassés, retrait du hashtag
#recouvrement des templates LinkedIn (mot interdit en com publique
par marque.md).

docs/brand-identity.html
- §3 Palette : retrait du swatch « Or discret #B89F6B » et de la
  description « accent or à doser » (CLAUDE.md indiquait le caveat
  depuis le début, mais le HTML lui-même n'avait jamais été
  nettoyé). Reformulation alignée avec marque.md (rubis +
  neutres only).
- §Notes finales : « Or accent : à utiliser très peu » → « 
  retiré de la palette » avec renvoi vers marque.md
- (Variable CSS --gold gardée car référencée dans deux contextes
  inactifs lignes 24/203 — non visible côté affichage. Non
  bloquant.)

docs/marque.md
- §Préambule : lien `/brand-identity.html` → `/docs/brand-identity.html`
  (chemin réel)
- §Typographie : « Source Google Fonts » → « self-hosted via
  @fontsource-variable/{bricolage-grotesque,inter} » (réalité
  package.json)
- Header bumpé 2026-05-05 v0.1 → 2026-05-09 v0.2

docs/munitions-marketing.md
- §Pricing : retrait de la promesse « intégration banking » sur
  le plan Business — explicitement V2+ (CLAUDE.md V1 OUT). Note
  le check-in email comme alternative V1.

docs/marketing/playbook.md
- §Templates LinkedIn : hashtag `#recouvrement` → `#impayés` +
  warning explicite renvoyant à marque.md:151
- §Annexe ressources : chemin obsolète `/landing/index.html` →
  chemin réel `apps/landing/src/pages/index.astro`

docs/marketing/launch-kit.md (NOUVEAU — était untracked)
- Tracked avec correction des 2 hashtags #recouvrement →
  #impayés (variantes A et B des posts LinkedIn). La stat
  AFDCC ligne 39 (« taux de recouvrement ») est gardée — terme
  technique cité dans un contexte factuel, pas un positionnement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 19:17:27 +02:00
ordinarthur
9eaac0c7ef 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>
2026-05-09 19:13:44 +02:00
ordinarthur
801168fc74 docs(audit-1/3): aligner top-level + produit sur le code
Issu d'un audit de cohérence doc-vs-code. Corrige les claims périmés
sur le périmètre V1, la nomenclature des plans, les statuts de
facture et les sources typographiques.

CLAUDE.md
- IN/V1 : ajout Microsoft SSO + Stripe billing + mode démo (livrés)
- IN/V1 : « admin blog (à venir PR3) » → admin blog livré
- IN/V1 : 4 plans nommés concrètement (Standard B2B, Rapide, Patient,
  Ferme) au lieu de la mention vague « 4 plans fournis »
- Glossaire : « Check-in » → « Confirmation » (terme migré dans
  flow.md depuis ADR-008, alignement)
- Brand identité : typo Bricolage + Inter sont self-hosted via
  @fontsource-variable, pas Google Fonts via <link>
- Brand : lien `/brand-identity.html` → `/docs/brand-identity.html`
  (chemin réel) + retrait du caveat « or accent obsolète » (HTML
  sera nettoyé dans le batch brand)
- Bumpé la dernière maj 2026-05-07 → 2026-05-09

docs/produit.md
- §4.1 : ajout Microsoft SSO
- §4.3 : 4 plans réels (Standard B2B / Rapide / Patient / Ferme),
  variables Mustache réelles ({{client.name}}, {{amount}},
  {{dueDate}}), tons réels (amical | courtois | ferme |
  mise_en_demeure)
- §7 : trial commercial 30 jours (vs 14j historique). Note
  l'incohérence avec la « grâce 3 mois » dans billing.ts à fixer
  dans un PR séparé
- §8 : statut `relanced` → `in_relance` (constants/index.ts) +
  « check-in » → « confirmation »
- Header bumpé 2026-05-05 v0.1 → 2026-05-09 v0.2

docs/flow.md
- §6.1 : tons d'étapes corrigés (était amical|ferme|stricte —
  réel : amical|courtois|ferme|mise_en_demeure)
- Slug exemple : `standard-b2b` → `standard-30j` (réel)

docs/wireframes-mvp.html
- Compteur « 13 écrans » nuancé : c'était le scope low-fi initial,
  la SPA livrée en compte ~15 + onboarding multi-étapes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 19:06:59 +02:00
ordinarthur
06a3aaf468 feat(landing): step 04 — remerciement automatique au client
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 58s
Ajoute une 4e étape dans la section « Comment ça marche » qui
matérialise la fin heureuse du cycle : le client paye, Rubis envoie
automatiquement un mot court de remerciement (« Merci, paiement bien
reçu »).

Pourquoi c'est important côté pitch :
- Aligne le produit avec le principe brand « respectueux du client
  final » (cf. CLAUDE.md). On n'est pas qu'un outil de pression — on
  est aussi celui qui sait dire merci.
- Crée une attente positive de fin de cycle, qui s'enchaîne mieux
  vers le compteur de rubis (déplacé du step 03 vers 04 pour servir
  de récompense narrative à la boucle complète).

Modifs :
- En-tête : « Trois étapes → Quatre étapes » + ajustement du
  sous-titre.
- Step 03 : retitré « Vous validez. La machine fait le reste. »
  (le punchline « Et puis c'est tout » migre implicitement sur 04).
- Step 04 (nouveau) : email de remerciement + encart rubis.
- Nouveau ThankYouWidget : email card stylé Apple Mail (sender,
  sujet, corps, badge « Envoyé automatiquement ») en tokens rubis
  uniquement (pas de vert — interdit par brand).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 18:52:15 +02:00
ordinarthur
6993d80089 perf(landing): inline critical CSS + preload latin woff2
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 59s
Élimine ~130 ms du critical path LCP rapportés par l'audit Lighthouse :

- `build.inlineStylesheets: "always"` dans astro.config.mjs : la
  feuille `_astro/Layout.<hash>.css` (~42 KiB) n'est plus une requête
  séparée render-blocking, elle est inlinée dans le HTML. Coût : +42 KiB
  par page prerenderée (≈10 KiB gzippé sur la wire). Gain : 80 ms FCP.
  `"auto"` aurait été ignoré (Layout.css > 4 KiB du seuil interne).

- `<link rel="preload">` sur les 2 woff2 latin (Inter body + Bricolage
  Grotesque display) dans Layout.astro. Casse la chaîne HTML→CSS→fonts
  du network dependency tree (~50 ms gagnés). URLs résolues via Vite
  `?url` import → hashing préservé entre les builds.

On ne preload que latin-wght-normal — les autres subsets (latin-ext,
vietnamese, cyrillic, greek) sont chargés à la demande et ne valent
pas le poids upfront pour un trafic FR/latin.

Vérifié build : `<link rel="stylesheet">` disparu du HTML rendu, 1
seul `<style>` inline présent, 2 `<link rel="preload">` avec les bons
hashes d'asset.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 18:45:06 +02:00
ordinarthur
eda5436d12 fix(seo): smart title suffix + OG image par défaut 1200×630
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m1s
Audit SEO révélait deux pertes de valeur :

1. **Titres trop longs** (86 chars sur la home, 71 chars sur les articles).
   Google tronque à ~60 chars dans le SERP. Le suffixe automatique
   `— Rubis sur l'ongle` (20 chars) écrasait le message-clé.

   Layout.astro fait maintenant un suffix smart :
   * Si title <45 chars ET ne contient pas "Rubis" → suffix `— Rubis` (8 chars)
   * Sinon → titre tel quel (la brand est déjà couverte par og:site_name +
     JSON-LD publisher + le hostname rubis.pro visible dans la SERP).

   Résultat : home 64 chars, article 51 chars, "Mentions légales" → 24 chars
   avec suffix.

2. **Pas d'`og:image` sur les pages sans hero** (home, légal). Sur
   LinkedIn/X/Slack, aucune preview image — perte d'engagement énorme.

   Ajout d'un og-default.png 1200×630 (165 KB optimisé) dans
   apps/landing/public/, monté par fallback par Layout.astro quand la
   page ne fournit pas d'`ogImage` explicite. Twitter:card devient
   toujours `summary_large_image`.

   L'image a été générée via le nouvel outil HTML
   docs/marketing/assets/og-default.html (clone de la mécanique du
   linkedin-banner.html — clic sur "Télécharger PNG" et go).
   Compose : brand + tagline + mock card 124 rubis + CTA rubis.pro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 18:14:28 +02:00
ordinarthur
b2dd991c58 fix(blog/admin): accept upload URLs (absolute + relative /uploads paths)
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m15s
L'upload renvoie maintenant une URL absolue construite depuis APP_URL
(`https://app.rubis.pro/api/v1/uploads/blog/{uuid}.{ext}`), pour que la
landing publique l'affiche directement en <img src> sans absolutize.

Le validator post (createPostValidator + updatePostValidator) accepte :
* Les URLs HTTPS absolues (image externe ou notre upload absolutisé)
* Les paths relatifs `/api/v1/uploads/...` (rétro-compat sécurité — si
  une URL relative arrive d'une autre source, on la laisse passer plutôt
  que 422 sur un champ qui résout côté client)

Bug initial : POST /api/v1/admin/uploads renvoyait `/api/v1/uploads/...`
(relatif), puis le PATCH /admin/posts/:id rejetait ce path en 422 car
`vine.string().url()` exige une URL absolue. Cause = double oubli (path
relatif côté upload + validator strict).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 17:54:06 +02:00
ordinarthur
52bc7507fb fix(blog/admin): expose contentMd dans PostTransformer + nullish guards
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 35s
Build & Deploy API / build-and-deploy (push) Successful in 1m27s
Le PostTransformer ne renvoyait que contentHtml — l'éditeur admin avait
besoin du contentMd source pour permettre l'édition, et plantait avec
"Cannot read properties of undefined (reading 'replace')" dans countWords()
au mount.

* PostTransformer expose maintenant contentMd, status et createdAt en
  plus de l'existant. Surcoût ~quelques KB par requête côté landing
  publique (négligeable). Si volume devient un problème, on splittera
  en PublicPostTransformer + AdminPostTransformer.
* admin.blog_.$id.tsx : nullish coalescing sur tous les champs string
  au moment d'init le draft (defense in depth — si l'API renvoie
  jamais un payload partiel, l'éditeur reste fonctionnel).
* countWords() accepte maintenant string | null | undefined.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 17:40:53 +02:00
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
2b34388723 fix(landing): copie le workspace + node_modules pruned en runner stage
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 48s
Le bundle Astro standalone (dist/server/entry.mjs) référence quand même
certaines deps externes (react, react-dom, lucide-react, sharp). En ne
copiant que dist/ vers le runner, le pod crashait au boot — Node ne
trouvait pas les modules → startupProbe failed → rollout K3s timeout.

Stratégie miroir de Dockerfile.api : on prune les devDeps après build,
puis on copie tout /repo dans /app. Les workspace symlinks (apps/landing
↔ packages/ui ↔ packages/shared) restent fonctionnels, les hoisted deps
(react, etc.) sont résolus depuis /app/node_modules.

CMD ajusté à `node ./dist/server/entry.mjs` (relative depuis le WORKDIR
/app/apps/landing) pour cohérence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:27:45 +02:00
ordinarthur
566febca48 chore: sync pnpm-lock.yaml after removal of react-dom from apps/api
Some checks failed
Build & Deploy Web / build-and-deploy (push) Successful in 1m37s
Build & Deploy API / build-and-deploy (push) Successful in 2m22s
Build & Deploy Landing / build-and-deploy (push) Failing after 3m50s
Le commit précédent retirait `react-dom` + `@types/react-dom` de
apps/api/package.json (deps utilisées uniquement pour le SSR HTML
du blog, désormais dans apps/landing) sans régénérer le lockfile.
La CI deploy-api échouait avec ERR_PNPM_OUTDATED_LOCKFILE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:18:28 +02:00
ordinarthur
e5530930b3 feat: refactor frontend en stack React unifiée (Astro + packages/ui)
Some checks failed
Build & Deploy API / build-and-deploy (push) Failing after 17s
Build & Deploy Web / build-and-deploy (push) Successful in 1m15s
Build & Deploy Landing / build-and-deploy (push) Failing after 3m43s
Trois surfaces partagent désormais le même design system, Tailwind v4
et React 19 — au lieu d'avoir landing en HTML vanilla, app en React, et
blog en Adonis SSR :

* packages/ui — design system partagé (tokens Tailwind v4 + composants
  TSX) extrait depuis apps/web : Brand, Gem, Button, Card, Chip, Eyebrow,
  EmptyState. apps/web migre 41 imports vers @rubis/ui.

* apps/landing — nouvelle app Astro 6 SSR (rubis.pro), remplace l'ancienne
  landing nginx vanilla. Embarque :
  - Landing complète portée en sections React (Hero, Stats, Promise,
    HowItWorks, Gamification, Legal, Pricing, FAQ, FinalCTA, Footnotes)
  - Pages légales (mentions, confidentialité, CGV) via LegalLayout.astro
  - Blog SSR (/blog, /blog/:slug) qui consomme /api/v1/posts
  - sitemap.xml, blog/rss.xml, robots.txt en endpoints Astro
  - SEO complet (canonical, hreflang, OG, Twitter Card, JSON-LD
    Article/BreadcrumbList/Blog/SoftwareApplication)

* apps/api — BlogController réduit à 2 endpoints JSON (GET /api/v1/posts
  + GET /api/v1/posts/:slug). Suppression des templates SSR Adonis
  (apps/api/app/blog/), de l'alias #blog/*, des deps react-dom et
  @types/react-dom. PostTransformer + PostSummaryTransformer ajoutés.
  Le service blog_renderer + le seeder + les 3 articles fondateurs
  restent intacts (réutilisés par futurs admin + cron IA).

* Infra :
  - Dockerfile.landing (multi-stage Node 22 + tini, Astro standalone)
  - k3s/app/landing.yml (Deployment + Service rubis-landing:4321 +
    ConfigMap avec API_URL=http://rubis-api.rubis.svc.cluster.local:3333)
  - .gitea/workflows/deploy.yml mis à jour pour build rubis-landing
  - .gitea/workflows/deploy-web.yml + Dockerfile.web : prennent en
    compte packages/ui/ comme dépendance
  - Suppression du Dockerfile nginx legacy + k3s/{deployment,service}.yml
  - Suppression de landing/ (assets favicons migrés vers
    apps/landing/public/)

* Docs : architecture.md (vue d'ensemble + §4bis apps/landing complet,
  §3 endpoints JSON blog, layout monorepo), CLAUDE.md (stack technique,
  documents associés, déploiement).

Note infra : l'ancien Deployment "rubis" (nginx) et son Service ne sont
PAS supprimés par la CI — à nettoyer manuellement après validation que
Traefik a été repointé sur rubis-landing:4321 dans le repo proxmox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:09:13 +02:00
ordinarthur
7141412174 feat(legal): infos éditeur réelles + OVH comme hébergeur
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 23s
2026-05-08 14:37:06 +02:00
ordinarthur
5127cd2c9e feat(landing): pages légales + CTAs trial 30 jours
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 19s
- CGV B2B SaaS (16 sections, conforme avec Stripe en place)
- Mentions légales allégées au strict minimum LCEN
- Politique de confidentialité resserrée :
  - retrait des détails infra (Proxmox, K3s, etc.)
  - sous-traitants par catégorie (Stripe / Resend / Mistral AI)
  - section sécurité standardisée
  - cookies simplifiés
- Période d'essai harmonisée à 30 jours partout (landing + CGV)
- Insistance sur l'hébergement et les données en France
2026-05-08 14:29:22 +02:00
ordinarthur
f59b11f836 chore(debug): endpoint /api/v1/_debug/sentry-test pour valider Sentry E2E
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m17s
Endpoint qui throw délibérément pour tester :
- Capture côté Sentry (handler.ts:report)
- Release tag (APP_VERSION = sha git)
- Sourcemaps désobfusquées
- Tags { url=pattern, method, status }
- User context (si auth présente)

Gardé par NODE_ENV !== 'production' OU DEBUG_SENTRY_TEST=true.
Sur prod, activer le temps du test via :

  kubectl -n rubis set env deploy/rubis-api DEBUG_SENTRY_TEST=true
  curl -i https://app.rubis.pro/api/v1/_debug/sentry-test
  # → 500, vérifier Sentry
  kubectl -n rubis set env deploy/rubis-api DEBUG_SENTRY_TEST-

À retirer (revert ce commit) une fois l'intégration Sentry validée.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:50:10 +02:00
ordinarthur
f33b2dd319 feat(observability): Sentry monitoring API + Web (ADR-024)
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m9s
Build & Deploy API / build-and-deploy (push) Successful in 2m2s
Intégration Sentry SaaS pour error monitoring + replay sur les 2 apps.

API (apps/api) :
- start/sentry.ts : init au plus tôt dans bin/server.ts (avant Ignitor)
  pour capturer les erreurs de bootstrap. No-op si SENTRY_DSN_API absent.
- app/exceptions/handler.ts:report : captureException sur les 5xx avec
  tags { url, method, status } et user.id (PII minimisée). 4xx filtrés
  par beforeSend dans start/sentry.ts (validation, auth invalide = bruit).
- start/env.ts : SENTRY_DSN_API + APP_VERSION optionnels.
- bin/server.ts : import #start/sentry en 1er.
- @sentry/node + @sentry/profiling-node ajoutés au package.json.

Web (apps/web) :
- src/lib/sentry.ts : init au plus tôt dans main.tsx, BrowserTracing +
  Replay (0% session, 100% sur erreur — économie quota free tier).
  maskAllText + blockAllMedia pour privacy par défaut.
- src/lib/auth.ts : Sentry.setUser({ id }) au login, setUser(null) au
  logout (corrélation cross-stack des erreurs front avec un user).
- src/main.tsx : ErrorBoundary autour de l'app avec FallbackError UX.
- vite.config.ts : @sentry/vite-plugin uploads les sourcemaps + les
  SUPPRIME du dist/ final (filesToDeleteAfterUpload) pour ne pas leak
  le code source via nginx en prod. Helper resolveAppVersion() pour
  injecter le sha git en dev (le shell n'étant pas évaluable dans .env).
- src/lib/env.ts : VITE_SENTRY_DSN_WEB + VITE_APP_VERSION optionnels.
- .env.development : VITE_SENTRY_DSN_WEB (préfixé correctement pour
  être exposé par Vite — l'ancienne SENTRY_DSN ne marchait pas).
- @sentry/react + @sentry/vite-plugin ajoutés au package.json.

CI Gitea :
- deploy-api.yml : kubectl set env APP_VERSION=${{ github.sha }}
  runtime → release Sentry trackable au commit pour l'API.
- deploy-web.yml : build-args VITE_SENTRY_DSN_WEB, VITE_APP_VERSION,
  SENTRY_AUTH_TOKEN, SENTRY_ORG injectés depuis les secrets Gitea.
- Dockerfile.web : ARG correspondants + propagation au stage build.

Privacy / sécurité (cf. ADR-024) :
- captureException tags : ctx.route?.pattern (pas l'URL réelle) →
  les codes OAuth (?code=...) et tokens de check-in n'apparaissent
  jamais dans les tags Sentry indexés.
- Sentry user context = user.id UUID seulement, pas d'email/nom.
- Sourcemaps en prod : uploadées à Sentry, supprimées du bundle.
- 4xx filtrées en amont (beforeSend) ET en aval (handler.ts:report).
- DSN public (by-design) commit-able, AUTH_TOKEN secret CI uniquement.

Sample rates (free tier 5K events / 50 replays par mois) :
- traces : 10% prod, 100% dev
- profiles : 100% (sampled par traces)
- replay session : 0% (économie quota)
- replay sur erreur : 100% (debug post-mortem)

Pré-requis runtime à configurer hors-repo :
- Secret K3s rubis-app-secrets : SENTRY_DSN_API
- Secrets Gitea Actions : SENTRY_DSN_WEB, SENTRY_AUTH_TOKEN, SENTRY_ORG

ADR-024 logué dans docs/decisions.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:38:12 +02:00
ordinarthur
7c45ee4490 add plausible
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 29s
Build & Deploy Landing / build-and-deploy (push) Successful in 17s
Build & Deploy API / build-and-deploy (push) Successful in 1m20s
2026-05-08 13:08:07 +02:00
ordinarthur
05cfa598b2 chore(bruno): mises à jour test data dans la collection
- 00-Auth/02 Login : trailing whitespace
- 03-Clients/04 Create : test data variant (Martin2, siret/phone
  différents) pour éviter le conflit unique sur les exécutions
  successives
- 05-Invoices/04 Create : numéro F-2026-0047, dueDate au 2026-04-01
  (passée → déclenche l'envoi immédiat des RelanceTasks BullMQ pour
  tester le pipeline outbound)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:47:49 +02:00
ordinarthur
06dcf38fee feat(ui): DatePicker brandé Rubis (remplace input type=date)
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 20s
Le natif <input type="date"> affiche le calendrier moche du navigateur
(différent par client OS, pas d'alignement palette). On le remplace par
un composant Radix Popover + grille mensuelle Tailwind aux couleurs
Rubis.

Composant : apps/web/src/components/ui/DatePicker.tsx
- Trigger : bouton style Input (border line, focus rubis-glow,
  data-[state=open]:border-rubis pour hint visuel)
- Popover Radix : focus trap, Escape, click outside, animation
- Grille 7×6 (semaine commence lundi, locale FR via date-fns)
- Sélection : bg-rubis text-white + ombre rubis
- Today : ring-inset rubis-glow
- Hover : bg-cream
- Raccourci "Aujourd'hui" en footer

API alignée avec l'usage existant :
- value: string ISO | Date | null
- onChange(date: Date) — l'appelant fait .toISOString() comme avant

Usages migrés :
- ManualInvoiceDialog.tsx : Date d'émission
- factures_.import_.$batchId.tsx : Date d'échéance (avec préservation
  du className aria-invalid pour les low-confidence OCR)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 23:01:31 +02:00
ordinarthur
eb248c98b8 style(mail): fond crème edge-to-edge (au lieu d'un container blanc)
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
Avant : body crème + container blanc avec border-radius + bordure → effet
"card flottante" qui laissait des marges crème en haut/bas et un fond
blanc pour le contenu. Sur iPhone Mail, cette structure créait un gap
mal rendu entre la frame Mail.app et le header rubis-deep.

Maintenant :
- bodyStyle.padding 24px 0 → 0 (rubis-deep colle au haut de la zone mail)
- containerStyle background blanc → crème (toute la zone email est crème,
  cohérente avec la palette)
- containerStyle borderRadius + border supprimés (edge-to-edge)
- invoiceCardStyle (checkin) + summaryCardStyle (relance) passent en
  blanc pour se détacher du nouveau fond crème
- Dark mode CSS : .rubis-container override aussi mis à crème

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:55:47 +02:00
ordinarthur
1bb0c7166b fix(mail): force light color-scheme pour empêcher l'auto-inversion iOS
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m7s
Sur iPhone Mail.app en dark mode, iOS inverse automatiquement les
couleurs de l'email : header rubis-deep (#771328) devenait rose
pâle, fond crème devenait noir, texte sombre devenait blanc.

Fix appliqué dans le _layout.tsx (donc impacte checkin + relance) :
- Ajoute meta `color-scheme: light only` + `supported-color-schemes`
  → signal aux clients mail (iOS Mail, Gmail mobile, Yahoo)
  qu'on ne souhaite PAS d'auto-dark-mode
- Ajoute style block avec :root color-scheme + overrides
  Outlook.com dark mode ([data-ogsc] / [data-ogsb])
- Ajoute className sur Body / Container / header / footer pour
  permettre le ciblage CSS dark-mode-resistant

Couvre : iOS Mail, Apple Mail macOS, Gmail mobile dark, Outlook.com.
Aucun impact sur les clients qui ne font pas d'inversion (Outlook
desktop, Thunderbird, etc.).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:42:57 +02:00
ordinarthur
d7148525d0 fix(k3s): mail from-address contact@rubis.pro + LANDING_URL
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 25s
- MAIL_FROM_ADDRESS: rubis@arthurbarre.frcontact@rubis.pro
  Les emails Rubis partent désormais du domaine principal,
  cohérent avec la marque et avec la boîte OVH MX Plan créée
  pour recevoir les replies clients.
- MAIL_FROM_NAME: "Rubis Sur l'Ongle" → "Rubis sur l'ongle"
  (alignement casse brand)
- LANDING_URL: ajoute la clé manquante (https://rubis.pro)
  qui n'était pas trackée dans le ConfigMap du repo. Le footer
  des emails utilisait l'ancienne valeur stale du cluster.

Pré-requis : domaine rubis.pro Verified dans Resend (DKIM + SPF).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:36:41 +02:00
ordinarthur
f86b07c444 copy(landing): CTAs pricing → app.rubis.pro + trial 30 jours
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 24s
- "Commencer l'essai 14 jours" → "Commencer l'essai 30 jours" et
  lien vers https://app.rubis.pro (au lieu de #waitlist obsolète)
- "Démarrer Free maintenant" → lien vers app.rubis.pro
- "Démarrer Business maintenant" → lien vers app.rubis.pro

30 jours = sweet spot SMB B2B (couvre 1 cycle de relance complet
J+3/J+10/J+20, conserve l'urgence sans tomber dans l'effet
d'ancrage du gratuit long).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:02:56 +02:00
ordinarthur
aebcb07b88 copy(landing): "huissier" → "commissaire de justice"
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 15s
Mise à jour terminologique : depuis le 1er juillet 2022 (réforme
ordonnance n° 2016-728), les huissiers de justice et commissaires-
priseurs judiciaires ont fusionné en une profession unifiée :
"commissaire de justice".

Garde le terme exact à jour dans le bloc "cadre légal" du landing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:52:46 +02:00
ordinarthur
1acb273c1d docs: email infra rubis.pro (Resend sortant + OVH MX entrant)
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 24s
Documentation post-migration du setup email :

- /docs/tech/backend.md §12.5 : architecture des 2 flux
  (Resend pour le sortant transactionnel via send.rubis.pro,
  OVH MX Plan pour l'entrant humain via @ rubis.pro)
- /CLAUDE.md : tableau récap email infra + maj domaine principal
  rubis.pro / app.rubis.pro, suppression de la question ouverte
  "domaine définitif" (résolue) et "endpoint waitlist" (remplacé
  par CTA app)
- /.claude/deploy-memory.md : section migration rubis.pro marquée
   avec checklist décommissionnement legacy
- /landing/confidentialite.html : remplace privacy@rubis.pro
  par contact@rubis.pro (alignement avec les boîtes OVH créées)

Adresses opérationnelles :
- contact@rubis.pro (général + RGPD)
- dev@rubis.pro (notifs techniques)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:48:35 +02:00
ordinarthur
1c5a58e09a chore(domain): migrate rubis.arthurbarre.fr → rubis.pro
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 26s
Build & Deploy Landing / build-and-deploy (push) Successful in 27s
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Bascule du domaine principal vers rubis.pro / app.rubis.pro :

- K3s ConfigMaps (api.yml, web.yml) : APP_URL, WEB_URL,
  COOKIE_DOMAIN, OAUTH callbacks pointent vers app.rubis.pro
- Dockerfile.web : ARG VITE_API_URL et VITE_PUBLIC_LANDING_URL
- Workflows Gitea : commentaires + build args web → rubis.pro
- Code API (mail_dispatcher, send_test_email, config/mail) :
  defaults env LANDING_URL et MAIL_FROM_ADDRESS migrés
- Templates env (.env.example) idem
- Docs (architecture, backend, frontend, brand-identity) idem
- AGENTS.md / CLAUDE.md / deploy-memory : pointeurs domaine MAJ

Note : MAIL_FROM_ADDRESS dans le secret K3s reste sur
rubis@arthurbarre.fr tant que le domaine rubis.pro n'est pas
Verified dans Resend. À switcher manuellement après vérif Resend.

Compat : un 301 Traefik redirige rubis.arthurbarre.fr → rubis.pro
(et app.X aussi) — config Ansible dans le repo proxmox.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:32:31 +02:00
ordinarthur
6e796a0980 feat(landing): CTAs vers app + pages mentions légales & RGPD
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 25s
- Remplace les formulaires waitlist par des CTA "Lancer Rubis"
  pointant vers app.rubis.pro (nav, hero, section finale)
- Met à jour la trust line ("3 mois gratuits puis Free 5 factures")
- Footer : ajoute liens Mentions légales / Confidentialité, casse
  "Rubis sur l'ongle" + lien Lancer Rubis
- Supprime le script de binding waitlist (plus utilisé)
- Migre les références au domaine vers rubis.pro

Nouvelles pages :
- mentions-legales.html : conformité LCEN (9 sections)
- confidentialite.html : politique RGPD (10 sections, sous-traitants
  Stripe/Resend/Mistral, droits utilisateur, durées de conservation)
- _legal-shell.css : shell graphique partagé (palette rubis,
  Bricolage Grotesque + Inter, header/footer brandés, TOC)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:24:42 +02:00
ordinarthur
ff8fe64be2 feat(mail): templates HTML React Email + brand "Rubis sur l'ongle"
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m1s
Build & Deploy API / build-and-deploy (push) Successful in 2m3s
Templates HTML stylés DA Rubis pour les 2 emails sortants — fini le
plain text moche.

apps/api/app/mails/
  ├── _brand.ts          : tokens couleur + spacing partagés
  ├── _layout.tsx        : squelette commun (header rubis-deep + footer)
  ├── checkin_email.tsx  : email envoyé À L'USER avec 2 boutons CTA
  │                        Oui (rubis primary) / Non (outlined)
  └── relance_email.tsx  : email envoyé AU CLIENT, body texte du plan
                           + card récap (numéro, montant, échéance,
                           badge retard rubis-deep)

Stack :
  - @react-email/components + @react-email/render
  - Tous les styles inline (compatible Gmail / Outlook / Apple Mail)
  - HTML + plain text en fallback (anti-spam, accessibility)

mail_dispatcher.ts :
  - sendRelanceEmail : .html(rendered) + .text(body)
  - sendCheckinEmail : .html(rendered) + .text(body)
  - daysLate calculé via clock.now (démo-aware)

send_test_email :
  - Nouveau flag --template=checkin (default) | relance | plain pour
    tester chaque rendu via Mailpit sans créer de vraie facture.

Brand & landing :
  - "Rubis Sur l'Ongle" → "Rubis sur l'ongle" partout (config, mail,
    PDF, Stripe appInfo)
  - Nouvelle env var LANDING_URL (default https://rubis.arthurbarre.fr)
  - Footer email rend "Rubis sur l'ongle" comme <a> rubis cliquable
    vers la landing — l'user qui reçoit le mail connaît la marque
    derrière l'envoi
  - .env.example mis à jour avec LANDING_URL pour les autres devs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:10:27 +02:00
ordinarthur
87c6f49692 fix(mail): from-name = nom de l'org (pas "Rubis Sur l'Ongle")
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
Le client final qui reçoit la relance voyait "From: Rubis Sur l'Ongle"
alors qu'il connaît "Arthur Barré" (le patron de la TPE qui utilise
Rubis). Confusion garantie côté client → relance perçue comme spam.

Fix : `sendRelanceEmail` utilise maintenant comme display name "From" :
  1. `organization.name` (en priorité — c'est le nom commercial connu
     du client)
  2. `user.fullName` (fallback si l'org n'a pas de nom posé)
  3. `MAIL_FROM_NAME` env (dernier recours, "Rubis Sur l'Ongle" en prod)

L'adresse technique reste sur notre domaine vérifié (relances@arthurbarre.fr
→ SPF/DKIM Resend OK), seul le display name change.

Le mail check-in (envoyé À l'user, pas au client) garde "Rubis Sur l'Ongle"
comme display — c'est nous qui le notifions, c'est cohérent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:45:57 +02:00
ordinarthur
ab75f1f979 fix(checkin): bump invoice.status pending → awaiting_user_confirmation
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m6s
Bug V1 documenté dans flow.md mais jamais corrigé : le job send_checkin_job
envoyait l'email + marquait la CheckinTask `sent`, mais ne touchait pas le
statut de la facture. Conséquence : l'user reçoit le mail check-in dans sa
boîte mais la modale in-app au refresh ne l'affiche pas (la modale liste
uniquement les `awaiting_user_confirmation` côté DB).

Fix : après l'envoi mail OK et le mark CheckinTask=sent, on bump
`Invoice.status = 'awaiting_user_confirmation'` SI elle est encore
en `pending`. Pas de bump si entre temps :
  - mark-paid (status=paid)
  - litigation/cancelled (transitions manuelles)
  - in_relance (impossible mais safe)

Doc flow.md mise à jour pour refléter le nouveau comportement (effets
de la transition pending → awaiting + déprécation de la note "TODO V1.5").

Pour les factures existantes en prod qui ont déjà reçu le mail mais
restent en `pending` (cas pré-fix) : backfill manuel via SQL :

  UPDATE invoices SET status = 'awaiting_user_confirmation'
  WHERE status = 'pending'
    AND id IN (
      SELECT invoice_id FROM checkin_tasks WHERE status = 'sent'
    );

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:42:52 +02:00
ordinarthur
023f08c261 feat(api): commande ace billing:scenario pour tester les états billing
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m7s
Force un état billing sur l'org d'un user pour tester rapidement chaque
comportement UI/enforcement sans passer par Stripe ni attendre 3 mois.

Usage :
  node ace billing:scenario --email <user> --scenario <name>

Scénarios :
  • status         : ne touche rien, affiche juste l'état courant
  • fresh          : reset signup neuf (free + grace 3 mois)
  • grace-expired  : free, grace terminée, ≤ 5 actives → OK
  • limit-reached  : free, grace terminée, force 5 actives → bloqué (402)
  • pro            : pro mensuel actif, fake IDs si pas de vrais
  • pro-cancelling : pro + cancel_at_period_end=true → bandeau ANNULÉ
  • pro-past-due   : pro + status=past_due → warning UI
  • business       : business mensuel actif

Sécurité : préserve les VRAIS Stripe IDs s'ils existent (= l'org a
déjà payé). Génère des fake `cus_test_FAKE_*` / `sub_test_FAKE_*`
seulement si NULL — ne pas écraser une vraie souscription.

Le command affiche un récap compact à chaque exécution :
  - plan / grace / Stripe IDs / status / cancel_at
  - factures actives vs limite
  - création autorisée ou non + raison

Pour tester un comportement côté UI :
  1. Lance le scénario
  2. Reload /parametres/abonnement et /factures
  3. Vérifie le rendu (bandeau cancel, blocage import, etc.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:36:06 +02:00
ordinarthur
3bad1451a9 docs(bruno): collection Billing + endpoints check-in in-app
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 19s
Nouveau dossier `09-Billing/` avec :
  - folder.bru (overview : plans + flows upgrade/cancel/reactivate)
  - 01 Get subscription : state du plan, caps, grace period, cancel flag
  - 02 Start checkout   : crée une Checkout Session Stripe (Pro/Business
                          × monthly/yearly)
  - 03 Open portal      : Customer Portal pour gérer CB/annulation
  - 04 Reactivate       : annule l'annulation programmée (sans paiement
                          immédiat) — gère le conflit Stripe
                          cancel_at vs cancel_at_period_end

Aussi documenté les endpoints in-app check-in qui manquaient dans Bruno :
  - 03 In-app pending           : liste des factures awaiting_user_confirmation
  - 04 In-app respond paid      : équivalent du lien email "C'est payé"
  - 05 In-app respond pending   : équivalent "Toujours en attente"

README mis à jour avec le parcours étendu (signup → … → billing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:25:55 +02:00
ordinarthur
0f1a309be3 fix 2026-05-07 17:20:33 +02:00