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>
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>
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>
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>
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>
L'app est désormais installable sur écran d'accueil (Android via Chrome,
iOS via Safari → Partager → Sur l'écran d'accueil). Identité visuelle
strictement alignée sur le SPA :
- background_color : crème (#FAF7F2)
- theme_color : rubis primaire (#9F1239)
- icon : gem 4-facettes sur canvas crème, padding 15% pour
safe-area maskable Android tout en remplissant
suffisamment iOS qui ne masque pas
3 formats générés depuis le SVG via @resvg/resvg-js :
- icon-192.png + icon-512.png (manifest, splashscreen Android)
- apple-touch-icon.png 180×180 (iOS home screen)
Plus la SVG vectorielle servie en favicon.
Le script `pnpm --filter @rubis/web run icons` re-génère tout depuis
`public/icon.svg` — utile si on retouche le design.
Drop le favicon.svg de 1.1 MB hérité du landing (vestige), remplacé par
notre gem propre à 1.1 KB.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backend
- Service dashboard.ts : computeTimeseries + computeClientTimeseries
(helper fetchPaidByMonth DRY entre les deux). Buckets pré-créés sur
N mois pour pas afficher de "trous" quand un mois n'a aucun paiement.
- GET /dashboard/timeseries?range=3|6|12 (paidByMonth + pipelineByStatus)
- GET /clients/:id/timeseries?range=3|6|12 (paidByMonth filtré)
Frontend — Recharts (43 deps, ~50KB gzip)
- components/charts/theme.ts : palette stricte (rubis + neutres chauds,
pas de bleu/vert), couleurs statuts cohérentes avec les badges côté
liste, format fr-FR pour les axes/tooltips
- ChartTooltip themed : carte cream + bordure rubis-glow, font Inter,
tabular-nums, série label override
- EncaisseChart (area, dégradé rubis-glow → transparent)
- DsoTrendChart (line ink + référence pointillée à 30j = norme LME)
- PipelineChart (donut avec total au centre + PipelineLegend séparée)
- ClientPaidChart (bar chart compact pour fiche client)
Wiring
- Dashboard / : encaissé + DSO côte à côte, pipeline + top retards en dessous
- Fiche client /clients/:id : mini bar chart "encaissés sur 6 mois" entre
les stats et la liste factures
- Page /insights : version pleine largeur des 3 charts + range selector
3m/6m/12m + 3 cards récap (encaissé total, factures payées, DSO moyen).
Lien "Insights" ajouté au sidebar desktop (icône TrendingUp).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Monorepo Turborepo (pnpm workspaces) avec 3 packages :
- apps/web : SPA React 19 + Vite 8 + Tailwind v4 (CSS-first)
• TanStack Router (file-based, auto code-splitting), Query, Form
• Radix primitives bruts + CVA + clsx + tailwind-merge
• MSW pour mocker l'API tant qu'Adonis n'est pas branché
• Polices Bricolage Grotesque + Inter self-hostées via fontsource
• Tokens marque (rubis, cream, ink) exposés via @theme
• Primitives maison : Gem, Brand, Eyebrow, Button, Input, Field
• Route /login full flow : TanStack Form + Zod + mutation Query
- apps/api : Adonis 7 (kit api, scaffold via create-adonisjs)
• Auth access tokens (Bearer) — cf. ADR-017
• Tuyau core déjà câblé pour la génération de types
• Routes /api/v1/auth/{signup,login} + /api/v1/account/{profile,logout}
• Minimal — uniquement le pont front ↔ back
- packages/shared : types TS + schemas Zod + constantes
• Source unique de vérité partagée api ↔ web
• Domaines : User, Org, Auth, Client, Invoice, Plan
Tooling racine : Turbo, ESLint v9 flat, Prettier, husky, lint-staged.
CLAUDE.md et docs/decisions.md mis à jour avec ADR-014 à ADR-018
(stack, monorepo, PG existant, Bearer tokens, MinIO existant)
et le pointeur vers docs/tech/architecture.md.
Logo Rubis déplacé de landing/assets/ vers /assets/ (source unique
réutilisée par la landing et l'app).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>