168 Commits

Author SHA1 Message Date
ordinarthur
0ecf8ad40f test(e2e): PR 2 — import OCR + plans + mailing Mailpit
Ajoute 6 scénarios Playwright qui couvrent les surfaces utilisateur
au-delà de l'auth/clients/factures : upload OCR via dropzone, listing
des plans pré-fournis, et la chaîne mailing complète end-to-end.

Import (2 tests) — import.spec.ts :
  - Upload PDF via setInputFiles : MockOcrProvider extrait depuis le
    filename → drafts créés → SPA redirige sur /factures/import/$batchId
    → on voit le filename listé dans les drafts
  - Empty state /factures/import : dropzone + bouton "Parcourir" visibles

Plans (3 tests) — plans.spec.ts :
  - Les 4 plans pré-fournis (Standard, Rapide, Patient, Ferme) sont
    visibles dès le signup (provisionnés par provisionDefaultPlans)
  - Clic "Créer un plan" → arrive sur le wizard /plans/nouveau
  - Clic "Modifier" sur une PlanCard → page détail /plans/{slug}

Mailing (1 test, le plus précieux) — mailing.spec.ts :
  - Helper helpers/mailpit.ts : clearMailpit, waitForMessageTo (poll
    200ms / 10s timeout), getMessage (HTML + texte)
  - Scénario : signup → onboarding → créer client avec email →
    créer facture saisie manuelle → mark-paid → BullMQ worker enqueue
    payment-thanks → email envoyé via SMTP → Mailpit catche →
    test inspecte subject + body (numero, montant)
  - Valide la chaîne complète : SPA → API → DB → BullMQ → Worker →
    Mailpit, en moins de 5 s

Total après cette PR : 26 scénarios Playwright verts en 55 s.

Pré-requis runtime :
  - `pnpm dev:up` (Postgres + Redis + Mailpit + MinIO)
  - `pnpm e2e:setup` (création DB rubis_test_e2e + migrations)
  - Mailpit accessible sur :8025 (clearMailpit l'utilise en beforeEach)

Note check-in flow : pas implémenté en E2E V1 — nécessite un endpoint
test_e2e pour générer un CheckinTask + clear token. Le flow checkin
est déjà couvert par les tests japa functional. À ajouter en PR 3 si
besoin de validation end-to-end UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:45:39 +02:00
ordinarthur
35ffde9d56 chore(gitignore): ignore les rapports OCR bench locaux
Le fichier ocr-baseline-XX.json contient potentiellement des données
client extraites (montants, noms, emails). Pas à versionner — chaque
dev/contributeur lance son propre bench.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:29:20 +02:00
ordinarthur
4d0cab8b33 feat(ocr): retry exponential backoff sur 429 dans ocr:validate
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m19s
La free tier Mistral a un rate limit non-linéaire (parfois 4-5 req/min
acceptées, parfois 1 req/2min selon la charge). Un délai fixe entre
calls ne suffit pas — on retry max 3× avec backoff 30s, 60s, 90s.

Combiné avec --delay-ms (espacement nominal entre calls), ça permet
de tenir tout un bench même si le quota se serre en cours de route.

Bench réel observé sur 10 factures variées (templates Boulangerie,
Mercier moderne, Mercier ancien, retards 5j/30j/90j/180j) :

  - amountTtcCents : 10/10 (100 %)  ← précision financière parfaite
  - clientEmail    : 10/10 (100 %)
  - numero         :  9/10 (90 %)   ← 1 hallucination "FOUT"
  - issueDate      :  9/10 (90 %)   ← même facture, 1970-01-01 fallback
  - dueDate        :  9/10 (90 %)   ← idem
  - clientName     :  8/10 (80 %)   ← 2 fails : Mistral inclut contact
  - Latence moy.   : 9.5 s/facture (avec delay 7s)
  - 8/10 factures 100 % match (80 %)
  - 91.7 % accuracy globale champs

Insights actionnables :
  - amountTtcCents et clientEmail sont fiables → ok pour auto-validate
  - clientName : ajouter au prompt "ne pas inclure le contact (M./Mme)"
  - 1 facture sur 10 fait halluciner Mistral (FOUT + dates 1970) →
    afficher "à vérifier" dans la UI quand confidence < 0.5 sur dates

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:17:56 +02:00
ordinarthur
2f96238efe feat(ocr): throttle --delay-ms + script generate-expected pour ground truth
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m21s
Améliorations sur la commande de bench OCR validée avec 5 factures
réelles via Mistral (100 % accuracy obtenue sur l'échantillon test) :

  - Option `--delay-ms` (default 1500 ms) entre 2 appels provider pour
    éviter le rate limit Mistral (1300 free tier ≈ 1 req/s). Permet de
    benchmark les 27 factures sans HTTP 429.
  - Script `e2e/fixtures/invoices/generate-expected.mjs` qui parse les
    PDFs via `pdftotext -layout` (poppler-utils) et génère
    automatiquement les <name>.expected.json :
      • Numéro F2026-XXXX
      • Dates DD/MM/YYYY ou format long ("21 avril 2026")
      • Montant TTC en cents (gère séparateur milliers "2 775,02")
      • clientName en gérant 3 templates :
          - "DOIT : <Nom>"
          - "Facturé à :" en colonne droite
          - "ADRESSÉE À ... ÉCHÉANCE" côte à côte
    Re-générable, idempotent (skip si .expected.json existe déjà).

Le .gitignore du dossier reste sur `*` exclude pour ne pas commit les
PDFs (cohérent avec assets/test-invoices/ déjà ignoré racine), mais
autorise le script `generate-expected.mjs` (reproductible, sans secret).

Workflow utilisateur :
  1. Pose tes PDFs dans e2e/fixtures/invoices/
  2. `node generate-expected.mjs` génère les ground truth en lot
  3. Vérifie/corrige à la main si besoin (parser pas 100 % parfait sur
     tous les templates exotiques)
  4. `OCR_PROVIDER=mistral pnpm ocr:validate` lance le bench réel

Résultat baseline observé sur 5 factures Mistral en mode réel :
  - clientName     5/5  (100 %)
  - clientEmail    5/5  (100 %)
  - numero         5/5  (100 %)
  - amountTtcCents 5/5  (100 %)
  - issueDate      5/5  (100 %)
  - dueDate        5/5  (100 %)
  - Latence moyenne : 3,1 s / facture

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:05:37 +02:00
ordinarthur
e38aa224e8 feat(api): commande ocr:validate pour bencher l'OCR sur factures réelles
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 1m35s
Build & Deploy API / build-and-deploy (push) Successful in 2m24s
Outil standalone qui mesure la qualité d'extraction du provider OCR
courant (Mock, Mistral, ou autre futur) sur un set de factures avec
ground truth, séparé de la suite Playwright (qui reste sur OCR mock
pour la rapidité CI).

Pourquoi : permet de valider qu'un changement de provider (Mistral
upgrade, ajout Document AI, custom prompt) maintient la précision sur
les factures réelles avant de l'activer en prod.

Architecture :
  - Lit `e2e/fixtures/invoices/<name>.pdf` (ou .png/.jpg)
  - À côté, `<name>.expected.json` avec la ground truth
  - Pour chaque facture : upload temporaire vers le storage courant
    (MinIO en dev), appelle provider.extract(), compare field-by-field
  - Cleanup du fichier temp après extraction
  - Sommaire : accuracy globale, par champ, latence moyenne, exit 1
    si une fixture a échoué (utile CI)

Tolérances par champ :
  - amountTtcCents : exact (la précision financière compte)
  - issueDate / dueDate : jour exact
  - numero : exact (trim, case-insensitive)
  - clientName : Jaccard similarity ≥ 85 % sur les tokens
    (tolère "SARL" final manquant, espaces, etc.)
  - clientEmail : exact (lowercased) ou null

Usage :
  pnpm ocr:validate                                  # provider courant (.env)
  OCR_PROVIDER=mistral MISTRAL_API_KEY=... pnpm ocr:validate
  node ace ocr:validate --fixtures-dir=./other --out=report.json

Sécurité :
  - `.gitignore` exclut tous les fichiers de e2e/fixtures/invoices/
    sauf README + .gitignore eux-mêmes — les vraies factures ne fuitent
    pas dans le repo public

À faire par Arthur :
  1. Dépose 10-20 vraies factures (anonymisées si possible) dans
     e2e/fixtures/invoices/
  2. Pour chaque, écrit le .expected.json (5 min par facture)
  3. Lance `OCR_PROVIDER=mistral pnpm ocr:validate` → ajuste prompt ou
     post-process si l'accuracy descend sous le seuil

Format ground truth + seuils cibles documentés dans le README du dossier.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:23:19 +02:00
ordinarthur
70c851dd0e test(e2e): PR 1 — auth complet + clients CRUD + factures saisie
Étend la suite Playwright avec 14 nouveaux scénarios couvrant les
surfaces critiques que tout user touche au quotidien.

Auth (7 tests) — auth.spec.ts :
  - Signup : email invalide (HTML5), email déjà pris (422 → toast)
  - Login : happy path après signup, mauvais password (401), email
    inconnu (401)
  - Protection des routes : / et /factures redirigent vers /login
    sans session

Clients (5 tests) — clients.spec.ts :
  - Create via dialog : remplir Nom + Email du contact → apparaît
    dans la liste, compteur "1 fiche"
  - Refuse email manquant (422, dialog reste ouvert)
  - 2 clients distincts → compteur "2 fiches"
  - Duplicate par nom case-insensitive → liste reste à 1 fiche
  - Recherche ILIKE par nom → filtre côté liste

Factures (2 tests) — factures.spec.ts :
  - Saisie manuelle complète : créer client puis facture via le
    dialog (combobox client async + numéro + montant + Radix Select
    plan) → apparaît dans /factures
  - Empty state visible si aucune facture

Total Playwright après cette PR : 20 scénarios verts en 38 s.

Stratégie : les edge cases déjà couverts par les couches inférieures
(unit, functional japa, vitest) ne sont PAS re-testés en E2E pour
éviter la duplication. Le E2E garde son rôle : happy path UI + edge
cases produit qui n'apparaissent qu'au niveau navigation/forms.

Prochaines PRs prévues :
  - PR 2 : OCR upload + Plans + Relances + Mailpit (mailing)
  - PR 3 : Billing complet (trial→active/past_due/cancel) + Dashboard
    KPIs + Settings
  - PR 4 : Blog + edge cases globaux + coverage report c8

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:07:55 +02:00
ordinarthur
59f81879d8 test(e2e): tests Playwright multi-stack — vrai navigateur, DB isolée, Stripe mocké
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m39s
Build & Deploy API / build-and-deploy (push) Successful in 2m30s
Build & Deploy Web / build-and-deploy (push) Successful in 1m21s
Ajoute une couche end-to-end où un Chromium drive la SPA + API ensemble
contre une DB Postgres séparée, avec Stripe entièrement mocké au niveau
API. 6 scénarios couverts (signup + onboarding + 4 sur le billing trial).

Architecture :
  - DB `rubis_test_e2e` séparée, TRUNCATE entre tests (~50 ms reset)
  - Routes test-only `/__test__/*` gated par NODE_ENV=test_e2e
    (reset, install Stripe mock, fire webhook, lire org state, last-org)
  - Stripe mocké via __setStripeForTests — pas d'appel réseau
  - Playwright spawn API + SPA automatiquement (webServer config)
  - CORS étendu à test_e2e pour le cross-origin localhost:5173 → :3333

Scénarios :
  - signup.spec.ts : signup → onboarding 3 étapes → dashboard (assert rubis hero)
  - billing-trial.spec.ts :
      • démarrer essai 14j → redirect Stripe Checkout (mock)
      • fallback Free 2 factures continue l'onboarding
      • webhook checkout.completed → org en trialing + trial_ends_at
      • retour ?trial=cancel après abandon
      • inspection DB : stripeCustomerId posé après start-trial

Scripts :
  - pnpm e2e          (headless)
  - pnpm e2e:headed   (Chromium visible)
  - pnpm e2e:ui       (mode interactif Playwright)
  - pnpm e2e:setup    (crée + migre rubis_test_e2e via docker exec)

Documentation : docs/tech/e2e-tests.md — architecture, scénarios,
extensions, CI, troubleshooting.

Limites assumées :
  - L'UI Stripe Checkout (3DS, formulaire CB) n'est pas testée — externe.
    Pour ça : playbook manuel docs/tech/stripe-trial-e2e-playbook.md.
  - Le rendu du banner "Essai Pro" n'est pas asserté en E2E à cause de
    TanStack Query staleTime — couvert par les tests vitest à la place.

État global du chantier billing : 127 tests japa + 6 Playwright + 11
vitest = couverture multi-niveaux.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:58:51 +02:00
ordinarthur
0db7ff877c fix(clients): aligne adresse structurée sur convention Lucid snakeCase
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m17s
La migration 1778800000100_enrich_clients_for_invoicing avait créé les
colonnes `address_line1` et `address_line2` (sans underscore avant le
chiffre), mais Lucid v22 auto-snake-case `addressLine1` → `address_line_1`
côté écriture. Résultat : tous les INSERT dans `clients` cassaient avec
`column "address_line_1" of relation "clients" does not exist`.

Bug latent — surfacé en lançant la suite functional complète après
`node ace migration:run`. Affectait 20 tests Clients/Dashboard/Invoices.

Fix : migration de rename qui aligne `address_line1` → `address_line_1`
et `address_line2` → `address_line_2`. Le `RENAME COLUMN` préserve les
données existantes en prod. Modèle Client simplifié (les déclarations
manuelles ne sont plus nécessaires depuis que `schema.ts` les a régen).

Bonus : fix du test `invoices.spec.ts` "liste paginée + meta total/page"
qui créait 3 factures, ce qui dépasse la nouvelle limite Free 2 (ADR-023).
Posé un `gracePeriodEndsAt` futur sur l'org du test pour bypass le quota.

État après ce commit : 127 tests verts (60 unit + 67 functional).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:10:49 +02:00
ordinarthur
094c26059f test(billing): tests E2E HTTP du tunnel essai 14 j + playbook Stripe test mode
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Ajoute 16 tests E2E qui hit les vraies routes `/api/v1/billing/*` à
travers le middleware auth, les validators et la persistance DB.
Complémentaire des 60 tests unitaires sur les services.

Suites couvertes :
  - POST /start-trial : 200 happy path, customer Stripe réutilisé,
    409 trial déjà consommé (2 garde-fous), 401 sans Bearer
  - GET  /subscription : expose inTrial + trialEndsAt, garde-fou
    trial_ends_at passé
  - POST /webhook : checkout.completed, subscription.updated trialing→active,
    trial_will_end → enqueue recap (avec spy), payment_failed → past_due,
    subscription.deleted → free + trial_ends_at conservé
  - Idempotence : 2× le même event = même état final
  - Event type inconnu → 200 silencieux (pas de DB write)
  - 400 si stripe-signature absent / signature invalide

Helpers de test :
  - installFullStripeMock(opts) → mock complet : customers, prices,
    checkout, billingPortal, subscriptions, webhooks. Avec
    passThroughWebhook qui bypass la vérif signature pour tester
    le routing applicatif sans signer manuellement chaque payload.

env.test : STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET dummy +
WEB_URL/LANDING_URL.

Documentation : docs/tech/stripe-trial-e2e-playbook.md — playbook
manuel pour valider en mode Stripe test (5 scénarios : happy path
3DS, carte refusée au prélèvement, annulation Customer Portal,
re-trial bloqué, fallback Free). Utilise Stripe Test Clocks pour
fast-forward sans attendre 14 jours réels.

Total après ce commit : 76 tests sur la chaîne billing (60 unit + 16 E2E).
Les cas Stripe-side (3DS UI réel, prélèvement effectif J+14) restent
à valider manuellement via le playbook avant le go-live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:01:16 +02:00
ordinarthur
b0e6f83655 feat(billing): essai 14 j Pro avec CB à l'inscription (Stripe trial_period_days)
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m19s
Build & Deploy API / build-and-deploy (push) Successful in 1m44s
Build & Deploy Web / build-and-deploy (push) Successful in 41s
Implémente le chantier #6 de docs/tech/landing-optimisations.md. Le
funnel signup propose maintenant un essai 14 j Pro avec carte demandée
mais non prélevée — prélèvement automatique à J+14 avec rappel à J+11
(webhook customer.subscription.trial_will_end de Stripe).

Couverture tests : 60 tests unitaires sur la couche billing
  - billing.spec.ts          (25) — quota Free, bypass trial, inTrial state
  - stripe_billing.spec.ts   (24) — handlers webhook, idempotence, dispatcher
  - trial_recap_job.spec.ts  (11) — stats aggregation, formatRubisToHoursFr
+ 3 nouveaux tests vitest côté SPA (useTrialDaysRemaining,
useIsAtFreeLimit bypass trial).

Backend :
  - Migration 1779000000000_add_trial_ends_at_to_organizations
  - PLAN_CAPS bypass quand status=trialing AND trial_ends_at futur
  - getOrgSubscriptionState expose inTrial + trialEndsAt
  - Refactor handlers webhook en service stripe_billing.ts (pures,
    testables) — extraction depuis le controller. dispatchWebhookEvent
    routeur typé également extrait pour les tests.
  - createTrialCheckoutSession avec subscription_data.trial_period_days=14,
    garde-fou TrialAlreadyConsumedError contre re-trial.
  - handleTrialWillEnd → enqueue job recap (BullMQ jobId déterministe
    basé sur subscriptionId, idempotent contre re-delivery Stripe).
  - Endpoint POST /api/v1/billing/start-trial.
  - Email template trial_recap (React Email, branding Rubis figé) avec
    stats: factures importées, relances envoyées, € récupérés, rubis +
    heures libérées.

Infra de test :
  - tests/helpers/stripe_mock.ts : __setStripeForTests injection +
    factories fakeSubscription / fakeCheckoutSession / fakeInvoice.
  - __setTrialRecapEnqueueForTests : permet de spy l'enqueue sans Redis.

Frontend :
  - /onboarding/billing.tsx (opt-in, pas encore forcé dans le flow) :
    bouton primaire essai 14j + fallback "Free 2 factures".
  - PlanLimitBanner : nouveau état "Essai Pro · X jours restants" qui
    prime sur les autres bandeaux. Discret rubis-glow, non blocant.
  - useStartTrial hook + useTrialDaysRemaining (arrondi sup).
  - SubscriptionState typé avec inTrial + trialEndsAt.

Landing :
  - Sous-texte CTA réactivé : « CB demandée, non prélevée avant J+14 »
    (Hero + FinalCTA), maintenant promesse véridique.

Notes ouvertes (à décider ultérieurement) :
  - Tunnel /onboarding/billing FORCÉ entre signup et /onboarding/compte :
    guard reste à activer (risque cassage du signup actuel sinon).
    Pour l'instant l'écran est accessible mais opt-in.
  - Cron de redondance trial-recap : pas encore implémenté (le
    jobId déterministe BullMQ couvre déjà la double-livraison Stripe).
    À ajouter si on observe des trial sans recap en prod.
  - Tests E2E avec Stripe test mode à faire avant le go-live (cartes
    3DS 4000 0027 6000 3184, declined 4000 0000 0000 0341).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:04:41 +02:00
ordinarthur
f9cba50b5e feat(billing,landing): plan Free 2 factures + scaffold preuves sociales/SEO
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m30s
Build & Deploy API / build-and-deploy (push) Successful in 1m43s
Build & Deploy Web / build-and-deploy (push) Successful in 33s
Suite des chantiers structurants de landing-optimisations.md.

#5 — Plan Free : 5 → 2 factures actives (cf. ADR-023)
  - PLAN_CAPS.free.activeInvoicesLimit dans apps/api/app/services/billing.ts
  - Tests unitaires alignés (4 → 1, 5 → 2 cap, delta 3 → delta 2)
  - billing:scenario command : commentaires + valeur par défaut
  - PlanLimitBanner : copy dynamique via {limit} au lieu de "5" hardcodé
  - /parametres/abonnement : H1 + tile Free (3 mois → 14 jours, 5 → 2)
  - billing.test.tsx (fixtures + cas test)
  - landing copy : hero feature pill, Pricing tile, FinalCTA, CGV §5
  - CLAUDE.md pricing table

#7 — Scaffold <TrustedBy /> (preuve sociale)
  - Composant qui render null tant que copy.trustedBy.{logos,testimonials}
    sont vides — pas de placeholder bidon.
  - Structure data dans copy.ts avec commentaires sur les prérequis
    avant d'ajouter une entrée (accord signé, photo, citation chiffrée).
  - Section insérée juste avant <Pricing /> (cf. doc §4).

#8 — Plan articles SEO + brouillon article 1
  - docs/marketing/seo-articles.md : 5 articles ciblés, mots-clés,
    structure type, lead magnet, calendrier 5 semaines.
  - Article 1 ("Modèle d'email de relance facture impayée") en
    brouillon complet, prêt à valider via l'admin blog (apps/api).

#6 — Plan détaillé migration Stripe trial 14 j (code reporté)
  - docs/tech/stripe-trial-with-card.md : état actuel vs cible,
    architecture (Stripe Checkout + trial_period_days), modifs DB
    (trial_ends_at), API (start-trial + webhook trial_will_end),
    SPA (onboarding/billing), 3 emails transactionnels avec contenu
    intégral, risques + mitigations, plan d'exécution 2,5 j.
  - Implémentation reportée à une session focus avec accès Stripe
    test mode (cartes 3DS, webhook signing secret).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:38:52 +02:00
ordinarthur
7c0767f45e refactor(web): retire i18n EN, app SPA mono-langue FR
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m43s
Build & Deploy API / build-and-deploy (push) Successful in 2m29s
Build & Deploy Web / build-and-deploy (push) Successful in 1m24s
Pendant de cecbddc (landing). Retire le système react-i18next ajouté
en 254f65b — un seul brief utilisateur en français suffit pour l'audience
TPE-PME française visée en V1.

- Désinstalle i18next + react-i18next (≈ 13 kB gzip économisés).
- Supprime apps/web/src/i18n/ (fr.ts, en.ts, index.ts, types.ts) et
  le LanguageSwitcher de /parametres.
- Inline les chaînes FR dans les composants impactés : main.tsx
  (FallbackError), shell (AppSidebar, MobileTabBar, UserMenu), auth
  (login, signup, onboarding/compte), dashboard (_app/index),
  /parametres.
- Met à jour signup : « 30 jours gratuits » → « 14 jours gratuits »
  pour s'aligner sur la landing.

Côté UX visible : plus de switcher langue, plus de détection
navigator.language, plus de localStorage["rubis:locale"].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 10:05:04 +02:00
ordinarthur
cecbddc496 refactor(landing): mono-langue FR + quick wins optimisations conversion
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m1s
- Retire le système i18n EN (apps/landing/src/i18n/, pages /en/*) ajouté
  en 4f3417f. Source unique de copy dans src/copy.ts (FR uniquement).
  Switcher de langue retiré du header, sitemap nettoyé des hreflang.
- Header : micro-baseline « Logiciel de relance de factures impayées »
  sous le wordmark pour lever l'ambiguïté du nom (§1).
- CTA principal : « Lancer Rubis » → « Démarrer mon essai 14 jours »
  avec sous-texte sur Hero / FinalCTA / Pricing (§5).
- Essai 30 j → 14 j sur landing + CGV §6.2 (§3).
- Blog promu en nav primaire avec label « Ressources » (§6).

Doc d'arbitrage : docs/tech/landing-optimisations.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:46:42 +02:00
ordinarthur
254f65b5d7 feat(web): support i18n EN avec react-i18next, détection auto + switcher
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m38s
Build & Deploy API / build-and-deploy (push) Successful in 2m27s
Build & Deploy Web / build-and-deploy (push) Successful in 1m26s
Stack : `i18next` + `react-i18next` (≈ 42 kB → 13 kB gzip). Init avant
le 1er render dans `main.tsx` pour que les chaînes soient résolues dès
le bootstrap.

Détection de la locale au 1er load :
  1. `localStorage["rubis:locale"]` (préférence explicite de l'user)
  2. `navigator.language` (langue du navigateur)
  3. fallback `fr`

→ un user EN voit l'app en EN dès la 1re visite sans intervention.
La fonction `setLocale()` persiste le choix + synchronise `<html lang>`.

Architecture :
- `src/i18n/{types,fr,en,index}.ts` — même pattern que landing :
  FR fait foi, EN typé par `Dict = typeof fr`.
- `components/settings/LanguageSwitcher.tsx` — radio FR/EN dans
  `/parametres` (section ajoutée en tête).

Surfaces traduites en V1 :
- shell : AppSidebar (nav + compteur rubis), MobileTabBar, UserMenu,
  FallbackError.
- auth : login, signup, onboarding/compte.
- main : dashboard `_app/index`, `_app/parametres` (sections compte,
  entreprise, signature, abonnement, facturation, marque, banque,
  danger zone).

Routes restantes (`factures`, `clients`, `plans`, `insights`,
`admin.blog`, sous-routes parametres) restent en FR inline ; le dico
EN les anticipe déjà via `factures.*`, `clients.*`, `plans.*`,
`insights.*` — il suffira de hooker `useTranslation()` au moment de
traduire ces écrans.

Emails côté API restent FR — à brancher sur une `locale` org plus tard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:35:47 +02:00
ordinarthur
4f3417fcef feat(landing): support i18n EN avec routing /en/* (Astro i18n natif)
Active Astro i18n avec `defaultLocale: fr` et `prefixDefaultLocale: false`
— les URLs FR restent canoniques à la racine, l'EN vit sous `/en/*` pour
ne pas casser le SEO existant.

Architecture :
- `src/i18n/{types,fr,en,index}.ts` — dico FR fait foi (Dict inféré),
  EN doit matcher la shape ; helpers `getTranslations(locale)` et
  `getAlternateUrl()` pour le language switcher.
- `Layout.astro` lit `Astro.currentLocale`, propage `locale` aux
  composants React, set `<html lang>`, og:locale + alt, hreflang.
- `SiteHeader` expose un lien switcher FR↔EN qui préserve la page.
- Toutes les sections (Hero, Stats, Promise, HowItWorks, Gamification,
  AutoBanking, Legal, Pricing, FAQ, FinalCTA, Footnotes, SiteFooter)
  acceptent une prop `locale` et tirent leurs chaînes du dico.

Pages EN créées :
- `/en/` — home complète
- `/en/blog`, `/en/changelog` — chrome traduit, contenu reste dans la
  langue de rédaction (les .md changelog + posts API sont FR)
- `/en/cgv`, `/en/mentions-legales`, `/en/confidentialite` — résumés
  courts ; la version juridiquement contraignante reste la FR (droit
  français, conformité GDPR/LCEN/LME).

Sitemap mis à jour avec entrées FR/EN + `xhtml:link rel="alternate"`.

Pas de détection auto via Accept-Language pour l'instant — le switcher
header suffit en V1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:29:57 +02:00
ordinarthur
cb1195ab73 fix(api): TCP keepalive + pool tuning sur la connexion PG
Le firewall entre pod K3s et LXC PG coupe silencieusement les connexions
idle (« Connection terminated unexpectedly » remonté par Sentry). On
active `keepAlive` sur le socket pg et on recycle les connexions idle
du pool plus vite (idleTimeoutMillis: 30 s).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:29:24 +02:00
ordinarthur
5f88a6411e feat(landing): instrumentation PostHog (Astro client)
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m0s
Setup PostHog côté landing — loader inline dans Layout.astro + tracking
de 5 events business côté browser :
- blog_article_viewed / blog_cta_clicked (funnel blog → app)
- pricing_pro_cta_clicked / pricing_plan_selected (intent upgrade)
- signup_cta_clicked (CTA hero/header/finalCTA, location-aware)

Vars PUBLIC_POSTHOG_* inlinées au build via build-arg CI
(POSTHOG_PROJECT_TOKEN, partagé avec apps/web). Token public phc_*,
safe à bake dans le bundle.

Au passage : supprime posthog-server.ts laissé par le wizard
(dead code, importait posthog-node qui n'est pas dans les deps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:43:11 +02:00
ordinarthur
7919f20a4f feat(web): enrichit invoice_uploaded + ajoute payment_completed (PostHog)
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
- invoice_uploaded : un event par draft (avec invoice_id, filename,
  batch_id, batch_size) au lieu d'un event groupé { count }. Permet de
  lister les uploads individuellement et de cibler une facture précise
  dans les insights. Amount pas dispo à ce stade (OCR pas encore validé).
- payment_completed : nouvel event sur mark-paid, avec amount_cents,
  amount_eur et payment_method (= 'manual' en V1). Alimente le funnel
  facture → paiement et la liste « payeurs » côté PostHog.
- invoice_marked_paid : conservé tel quel pour ne pas casser l'insight
  S5hMZkDE (Invoice paid vs relance launched).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:34:59 +02:00
ordinarthur
cc44ed8ce8 chore(ci): rename deploy.yml → deploy-landing.yml
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 23s
Cohérence avec deploy-web.yml / deploy-api.yml — un fichier par cible.
Met à jour la self-reference dans le path filter + la doc deploy-memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:30:56 +02:00
ordinarthur
b7ad42f3d0 chore(web): commit PostHog wizard skill + report
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 43s
Contexte pour les futures itérations agent : skill PostHog
integration-react-tanstack-router-file-based + rapport du wizard
(événements instrumentés, dashboards créés, next steps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:22:49 +02:00
ordinarthur
3fc3a7456a feat(web): instrumentation PostHog (analytics + nginx proxy)
Some checks failed
Build & Deploy Web / build-and-deploy (push) Has been cancelled
Build & Deploy API / build-and-deploy (push) Successful in 2m30s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m31s
Setup PostHog côté SPA — pageviews TanStack Router + 10 events business
(signup, login SSO, upload facture, émission/brouillon facture native,
marquer payée, lancer relance, plan créé, checkout Stripe). PostHogProvider
dans __root.tsx, identify sur auth, proxy nginx /ingest/* → eu.i.posthog.com
pour contourner les adblockers. Token bake via build-arg CI
(POSTHOG_PROJECT_TOKEN, à ajouter côté Gitea Secrets).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:21:59 +02:00
ordinarthur
e9e0ac6abb update landing
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 57s
2026-05-14 15:14:07 +02:00
ordinarthur
b81bc2609b fix(parametres): teaser banking l'emporte sur upsell + section démo admin-only
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 42s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m26s
Build & Deploy API / build-and-deploy (push) Successful in 2m10s
Deux bugs visibles dans /parametres :

1. **Banque** — un user Free voyait la carte "Plan Pro ou Business
   requis" alors que la feature est gated derrière BANKING_ENABLED=false
   en prod (Powens KYC en cours). L'upgrade n'aurait rien débloqué.
   Fix : la branche `comingSoon` court-circuite l'upsell, et le titre +
   description de la SettingsSection bascule en mode teaser ("Bientôt :
   votre banque connectée à Rubis") pour rester cohérent avec la carte
   "Bientôt disponible" en dessous.

2. **Démonstration** — la section apparaissait pour tous les users,
   alors que c'est un outil de prospection commerciale réservé aux
   admins Rubis (horloge virtuelle + capture des emails). Déroutant
   pour un user lambda.
   Fix : section gated sur `user.isAdmin` côté UI, et split des routes
   /demo côté API :
   - GET /demo/state reste accessible à tous les users authed (sinon le
     DemoClock dans AppLayout spam des 403 sur chaque page). Un user
     normal reçoit `{active: false}` — pas de leak.
   - GET /demo/inbox + POST /demo/start, /end, /tick : auth + admin.
     Mutations et lecture des emails capturés.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:57:37 +02:00
ordinarthur
e449b708f3 docs(invoices): édition native + ADR-025 + roadmap Factur-X (Phase 5)
Documente la feature ajoutée en V1.1 dans toute la doc cadre :

- **CLAUDE.md** : "Pure-player relance" nuancé en "La relance reste
  l'âme du produit", extension douce assumée. Périmètre V1/IN
  enrichi avec l'éditeur de factures. Glossaire enrichi (facture
  native, numéro de séquence, snapshot, Factur-X). Stack : ajout
  @react-pdf/renderer + pointeurs vers pdf-templates et les routes
  /parametres/facturation et /factures/nouvelle.
- **docs/produit.md** : nouvelle section 4.2bis "Édition native des
  factures" — scope V1.1 minimal, snapshots immuables, numérotation
  strict séquentielle, roadmap Factur-X V1.5 / PDP V2.
- **docs/flow.md** : nouvelle section 11bis (3 sources d'une facture,
  flow utilisateur de création, génération PDF, numérotation,
  snapshots, cas limites). Tableau "Ce que Rubis ne fait PAS" mis à
  jour (édition oui mais pas devis/avoirs/Factur-X V1).
- **docs/decisions.md** : ADR-025 "Édition native des factures +
  roadmap Factur-X" (rationale extension douce, choix techniques
  notables, alternatives écartées).
- **docs/tech/architecture.md** : section 6.1bis (flow technique
  édition native, points d'attention numérotation atomique + lazy
  PDF regenerate), ajout @react-pdf à la stack, routes /native +
  /preview-pdf + /invoice-themes + /invoice-settings documentées.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:18:11 +02:00
ordinarthur
aa6468e9a0 feat(web): éditeur de factures /factures/nouvelle (Phase 4)
Page split-view qui permet de composer une facture native dans Rubis
avec preview PDF en live (debounce 500 ms via POST /invoices/preview-pdf
→ Blob → objectURL → iframe).

UI
- Gauche : panneau d'édition organisé en cards (destinataire,
  dates + plan, lignes éditables, thème + accent, notes).
- Droite : iframe sticky qui affiche le PDF rendu côté serveur. Loader
  discret pendant la génération, fallback "sélectionnez un client" tant
  qu'on n'a pas un payload minimal valide.
- Lignes : ajout/suppression, quantité décimale (heures, demi-jours),
  taux TVA selon FRENCH_TVA_RATES, total HT recalculé live.
- Totaux client-side : mêmes règles d'arrondi (Math.round par ligne)
  que invoice_totals.ts côté serveur — feedback instantané, le serveur
  recalcule à la persistance.
- Footer sticky : "Enregistrer en brouillon" / "Émettre la facture",
  avec rappel que l'émission consomme la séquence (irréversible).

API client
- `useCreateNativeInvoice` : POST /invoices/native, invalide les caches
  invoices + counts.
- `previewInvoicePdf(input, signal)` : POST /invoices/preview-pdf qui
  retourne un Blob (annulable via AbortSignal pour les frappes rapides).
- `api.postBlob` : helper générique POST+JSON → Blob (inverse de fetchBlob).

Defaults : les settings résolus de l'org (theme, accent, paymentTermsDays)
sont chargés une fois au mount et appliqués comme valeurs initiales.

Liste factures : remplace le bouton "Nouvelle facture" par deux actions
côte-à-côte — "Importer" (secondaire, mène à /factures/import) et
"Créer une facture" (primaire, mène à /factures/nouvelle).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:07:41 +02:00
ordinarthur
0680bb9f77 feat(web): page /parametres/facturation — paramétrage de la facturation (Phase 3)
Ajoute la page de configuration de l'éditeur de factures natif côté SPA,
plus les hooks React Query pour /organizations/me/invoice-settings et
/invoice-themes.

Sections (chacune avec son propre Save → blast radius clair) :
1. Identité émetteur (raison sociale, forme juridique, adresse structurée,
   SIREN/SIRET/TVA intracom, NAF, RCS, capital, contact). Snapshotée
   à chaque émission dans `invoices.issuer_snapshot` — modifier ces
   champs n'altère pas les factures déjà émises.
2. RIB (IBAN normalisé à l'enregistrement, BIC, nom de banque).
3. Numérotation avec aperçu live "FAC-2026-0042" — préfixe + prochain
   numéro + padding éditables. Une fois la première facture émise, le
   compteur s'auto-incrémente.
4. Mentions & délais — délai de paiement par défaut + textes légaux
   préremplis (pénalités art. L441-10, escompte art. L441-9, libre).
5. Thème par défaut + couleur d'accent — galerie 4 thèmes avec previews
   miniatures CSS (pas de PDF embed pour la galerie : trop lourd).

Ajoute aussi le lien vers /parametres/facturation depuis /parametres
(section "Facturation" placée avant "Marque").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:32:07 +02:00
ordinarthur
ab07cd4a3b feat(invoices): génération PDF native via @react-pdf/renderer (Phase 2)
Implémente les 4 thèmes de factures (Classique, Moderne, Minimal,
Élégant) en composants React PDF et remplace les stubs Phase 1 par la
vraie génération + upload MinIO.

Templates (app/pdf-templates/)
- common.tsx : props partagées, formatters fr-FR (cents → euros,
  dates longues, taux TVA), palette neutre.
- classique : sobre, header texte centré, filets fins. Pour les
  cabinets et professions réglementées.
- moderne : bandeau coloré pleine largeur, logo dans le bandeau.
  Pour les agences et studios.
- minimal : noir et blanc, aéré, accent uniquement sur le numéro.
  Pour les indépendants et les designers.
- elegant : Times Roman, filets fins, titre centré encadré, italique
  sur le pied légal. Pour les boutiques premium.
- index.tsx : dispatcher slug → composant + renderInvoiceToBuffer.

Génération
- media_storage : nouveau scope `invoice-pdf` (`invoices/<orgId>/<uuid>.pdf`)
  et fonction `uploadBuffer(buffer, scope, subPath?)` pour stocker les
  buffers générés en mémoire (vs. uploads multipart existants).
- invoice_pdf : `generateInvoicePdf` rend + upload, `previewInvoicePdf`
  rend en Buffer pour stream HTTP direct.
- InvoicesController.pdf : lazy regenerate si pdf_storage_key est null
  sur une facture native (cas où la génération initiale a échoué).
- InvoicesController.previewPdf : synthétise un clientSnapshot depuis
  les données live, passe dans le pipeline standard.
- InvoicesController.storeNative : appelle la vraie génération en
  post-commit, log + continue si échec.

Conformité Factur-X (V1.5) : la structure de génération est un
point d'extension Buffer → Buffer ; l'injection d'un XML CII en
pièce jointe PDF sera ajoutée sans toucher aux templates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:16:45 +02:00
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