53 Commits

Author SHA1 Message Date
ordinarthur
21cd789a62 fix: prefer preview-compatible audio recording formats
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s
2026-04-11 16:49:39 +02:00
ordinarthur
800214976f fix: preserve recorder blob without object-url refetch
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s
2026-04-11 16:45:23 +02:00
ordinarthur
6dd7d2c4e5 fix: validate recording length using audio duration
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 35s
2026-04-11 14:37:23 +02:00
ordinarthur
dfde1c4995 fix: make backend image build in CI
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 1m43s
2026-04-11 14:20:07 +02:00
ordinarthur
21c92abc9c feat: implement Kubernetes deployment infrastructure, migrate database to PostgreSQL, and add CI/CD pipeline
Some checks failed
Build & Deploy to K3s / build-and-deploy (push) Failing after 24s
2026-04-11 14:09:16 +02:00
3bff3c8600 feat: MinIO storage, native audio recording, production Docker config
- Replace vmsg WASM encoder with native MediaRecorder API (WebM/Opus)
  to fix empty MP3 files causing OpenAI Whisper 400 errors
- Add minimum recording duration (2s) and file size (5KB) guards
- Add MinIO S3 storage integration for recipe images and audio
- Add /uploads/* API route that proxies files from MinIO with local fallback
- Save audio locally first for transcription, then upload to MinIO
  (fixes ECONNREFUSED when backend tried to fetch its own public URL)
- Add docker-compose.prod.yml, nginx-prod.conf, frontend Dockerfile
- Frontend Dockerfile: no-cache headers on index.html, long cache on hashed assets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 12:01:26 +02:00
ordinarthur
1b3f53c086 fix(profile): force Stripe sync on mount + on window focus
Problem reported: user cancelled their subscription via the generic
'Gérer' portal, but the backend still showed the old subscription.
Root cause: stripe listen wasn't running, so the
customer.subscription.updated webhook never reached the server. The
Profile page was only reading from the DB (getSubscription), so it
stayed stale forever.

Fix: call stripeService.syncSubscription() alongside getSubscription()
at mount time. The fast DB read still happens first (instant display),
then the Stripe API call updates the state if anything has drifted.
Also add a window.addEventListener('focus', ...) listener that re-syncs
every time the user tabs back to the app — handles the common pattern
of opening Stripe portal in a new tab, doing something, then coming
back.

This makes the Profile self-healing even without a webhook setup in dev.
Production should still run the webhook for other apps/users, but this
fallback ensures individual users see the truth on their next visit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:18:12 +02:00
ordinarthur
b783627890 feat(stripe): deep-linked cancel flow with auto-redirect
Before: 'Gérer l'abonnement' opened the generic Customer Portal. If the
user cancelled, the portal's 'return_url' was just a button label —
nothing auto-redirected back to Freedge, so the user was stranded on
billing.stripe.com after clicking 'Cancel'.

Now: dedicated 'Annuler' button on the Profile SubscriptionCard that
calls a new backend endpoint POST /stripe/portal/cancel. This creates
a portal session with flow_data.type = 'subscription_cancel' deep-linked
to the user's active subscription, plus after_completion.type = 'redirect'
so Stripe automatically redirects to /subscription/cancelled when the
cancellation is confirmed.

New page /subscription/cancelled:
- Animated heart badge (spring + pulsing halo)
- 'À bientôt, on l'espère' title
- Info box showing the period-end date (fetched via sync on mount)
  so the user knows they still have access until the end of the
  already-paid period
- Re-engagement message + 'Retour aux recettes' / 'Voir les plans' CTAs
- On mount: calls /stripe/sync so the DB is updated immediately
  (doesn't wait for the customer.subscription.updated webhook)

Profile SubscriptionCard paid-state footer now has two buttons side by
side: 'Gérer' (outline) and 'Annuler' (ghost with red hover).

Backend verified: Stripe SDK v12 supports flow_data.after_completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:10:57 +02:00
ordinarthur
b0e9425ed5 feat(stripe): on-demand sync fallback when webhooks are missed
Problem: if stripe listen is not running (dev) or the webhook secret is
misconfigured, a successful checkout leaves the user stuck on the free
plan in the DB even though Stripe knows they're subscribed.

Solution: 3 recovery mechanisms.

1. Backend: POST /stripe/sync (auth required)
   Fetches the current user's subscriptions from Stripe by customer ID,
   picks the most recent active/trialing/past_due one, and applies it to
   the User row via the same applySubscriptionToUser helper used by the
   webhook. If no active sub exists, downgrades to free. Returns the
   current plan state.

2. Frontend: CheckoutSuccess now calls /stripe/sync first (instant,
   reliable) before falling back to polling /stripe/subscription. This
   fixes the 'just paid but still free' bug even with no webhook setup.

3. Frontend: 'Rafraîchir' button on the Profile free-plan upgrade banner
   (ghost style with RefreshCw spinning icon). Tooltip hints at its
   purpose. Users who paid but see the free state can click it to
   self-heal in one click.

4. Backend script: scripts/sync-subscription.ts
   - npm run stripe:sync -- user@example.com  (sync one user by email)
   - npm run stripe:sync -- --all             (sync every user with a
                                                stripeId, useful after
                                                a prod webhook outage)
   Colored output with ✓ / ✗ / ↷ status per user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:07:02 +02:00
ordinarthur
8d1202ca34 fix(profile): remove nested Checkbox inside equipment tile buttons
Radix Checkbox renders a <button>, so wrapping it inside our own tile
<button> violated HTML nesting rules and blocked the click from reaching
the tile handler. The visual state (orange border + bg) already
indicates selection — drop the Checkbox entirely and add aria-pressed
for accessibility. Also bump vertical padding and add an active scale
for tactile feedback.
2026-04-08 14:02:27 +02:00
ordinarthur
64d2bf4506 feat(stripe): idempotent setup script that provisions products + prices
New script backend/scripts/setup-stripe.ts that:
- Reads STRIPE_SECRET_KEY from .env
- Detects test vs live mode and warns + 5s delay for live
- For each plan (Essentiel 3EUR/mo, Premium 5EUR/mo):
  - Looks up existing price by lookup_key (freedge_essential_monthly,
    freedge_premium_monthly) — idempotent, safe to re-run
  - If missing, creates the product then the recurring price with the
    lookup_key and nickname for clarity
- Prints the resulting price IDs with their env var names
- With --write-env flag, automatically upserts the values into
  backend/.env preserving other lines
- Points to Customer Portal settings and stripe listen command as
  next steps

npm scripts added:
- npm run stripe:setup        # dry run, just print IDs
- npm run stripe:setup:write  # update .env automatically
- npm run stripe:listen       # shortcut for stripe CLI webhook forward

Updated README to show the script as the recommended path for step 1,
keeping the manual dashboard instructions as a fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:58:26 +02:00
ordinarthur
339de4c44c feat(stripe): full subscription flow (checkout, portal, webhooks)
Backend:
- Prisma: add stripeSubscriptionId, subscriptionStatus, priceId,
  currentPeriodEnd to User + migration SQL
- plugins/stripe.ts: getPlans catalog with env-based price IDs
- server.ts: raw body JSON parser for webhook signature verification,
  skip rate limit on /stripe/webhook
- types/fastify.d.ts: declare rawBody on FastifyRequest
- routes/stripe.ts (new):
  - GET  /stripe/plans        public
  - GET  /stripe/subscription user status
  - POST /stripe/checkout     hosted Checkout Session, lazy-creates
    customer, dynamic payment methods, promo codes enabled
  - POST /stripe/portal       Billing Portal session
  - POST /stripe/webhook      signature verified, handles
    checkout.session.completed, customer.subscription.*,
    invoice.payment_failed. Resolves user by clientReferenceId,
    metadata.userId, or stripeId fallback
- .env.example + README: Stripe setup, stripe CLI, test cards

Frontend:
- api/stripe.ts typed client (getPlans, getSubscription,
  startCheckout, openPortal)
- pages/Pricing.tsx: 3-card grid (free/essentiel/premium) with
  popular badge, current plan indicator, gradient popular card
- pages/CheckoutSuccess.tsx: animated confirmation with polling on
  /stripe/subscription until webhook activates plan
- pages/Profile.tsx: SubscriptionCard above tabs — free users see an
  upgrade banner, paid users see plan + status + next billing date
  + 'Gérer l'abonnement' button opening Customer Portal
- components/header.tsx: 'Tarifs' link in nav
- App.tsx: /pricing (public) and /checkout/success (protected) routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:54:27 +02:00
ordinarthur
0c4e0035c2 feat(ui): redesign Profile page (hero + tabs + danger zone)
Full rewrite of /profile with a cleaner layout and nicer polish.

Layout:
- Removed the sidebar 'user info' card; replaced with a horizontal hero
  at the top: avatar (with premium badge overlay), name, email, logout
  button, and 3 badges (plan, member since, recipe count)
- Tabs stretched full-width (no more 1/3 sidebar + 2/3 content split)
- Removed the 'Mes recettes' section from the bottom (redundant with
  /recipes list page)

Hero:
- 24-28 Avatar with gradient fallback and ring-4 separator
- Premium crown icon overlay (Sparkles badge) if subscription != 'free'
- Badges: plan tier, member since (localized French date), recipe count
- Logout button moved up here, styled as ghost with red hover

Cuisine tab (the main improvement):
- Diet is now a 4-button segmented control instead of a native <select>
- Max time and servings inputs have suffix labels ('min', 'pers.')
- Equipment checkboxes replaced by visual tile buttons with emoji
  icons, 2-col mobile / 4-col desktop, selected state = orange border +
  bg. Hidden actual checkbox for accessibility.
- 'Actif' badge in the card header when any pref is set
- Rounded xl inputs (h-11), brand gradient submit button

Other tabs (Profil/Email/Sécurité):
- Consistent visual language: h-11 rounded-xl inputs, h-12 brand
  gradient buttons, bolder labels
- Email tab shows current email in the description
- Improved placeholders

Alerts:
- Custom inline alerts with AnimatePresence, auto-dismiss after 4s for
  success, dismissible X button, CheckCircle2/AlertCircle icons
- No more shadcn Alert component (simpler + branded colors)

Danger zone:
- Dedicated bordered section at the bottom with icon + explanation
- Delete button opens a proper Dialog (replaces prompt() hack)
- Dialog asks for password, has Cancel/Delete buttons

Mobile polish:
- Hero stacks avatar on top, centered
- Tab triggers show icon-only on mobile, icon+label on sm+
- All 4 diet buttons fit in 2 cols on mobile
- Equipment tiles also 2 cols on mobile
- Logout button full-width on mobile

Removed all unused imports: AvatarImage, Separator, CardFooter, Alert,
AlertTitle, AlertDescription. Cleaned up handlers that weren't needed
(inline setState in onChange).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:42:54 +02:00
ordinarthur
7a3cbb9385 fix(ui): beef up review state action buttons (h-14, bold) 2026-04-08 13:39:06 +02:00
ordinarthur
f4b3339fe4 feat(ui): redesign /recipes/new from scratch
The page had 2 stacked headers (its own + MainLayout), was capped at
max-w-md which wasted desktop space, and had cheap-looking dashed amber
borders. Full rewrite with 4 clean states.

State machine: idle -> recording -> review -> processing

IDLE state:
- Brand pill 'Nouvelle recette' with Wand2 icon
- Big H1 with 'ton frigo' in warm-gradient
- Subtitle explaining the chef Antoine
- User preferences rendered as chips (vegan, allergies, max time,
  cuisine) loaded from /users/profile. Each chip is a Badge with
  backdrop-blur, + a 'Modifier' chip linking to /profile. Chips only
  appear if the user actually set preferences.
- Giant centered mic button (112-128px) with:
  - 3 concentric pulsing rings (border-2, staggered 0.6s delays)
  - Gradient from-orange-500 to-amber-500
  - shadow-2xl + orange glow
  - ring-4 ring-white for separation from background
  - Scale 1.04/0.94 on hover/tap
- Keyboard shortcut hint (Space) shown on md+
- Rotating tip card below with Lightbulb icon (4 tips cycling every 4.5s)

RECORDING state:
- Pulsing red dot + 'ENREGISTREMENT' label
- 7xl monospaced tabular timer
- Synthetic 20-bar waveform animating (each bar randomized
  height/duration for organic feel)
- Large red stop button (80px) with Square icon
- Keyboard shortcut hint

REVIEW state:
- Spring-animated green checkmark badge
- H2 'Tout est bon ?' + subtitle
- Audio player card with:
  - Circular play/pause button (brand gradient)
  - 32-bar static waveform (deterministic sin/cos pattern)
  - Trash button
  - Hidden <audio> controlled via id
  - File size in KB
- Two buttons: 'Recommencer' (outline) + 'Créer ma recette' (brand
  gradient, ChefHat icon that rotates -8deg on hover)

PROCESSING state: (kept from previous version, slightly enlarged to
60-72 and using the new gradient halo)

Bonus:
- Space bar shortcut: starts/stops recording when not focused on input
- Remove the duplicate sticky header
- 'Mes recettes' back button + 'Préférences' link in top bar
- max-w-3xl centered layout, works from mobile to desktop
- All imports cleaned up (no more KitchenIllustration, Sheet, Card,
  useMobile, cn, Info, showTips, isRecorderLoading)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:36:16 +02:00
ordinarthur
e7872df156 feat(ui): polish across the app with new loaders, skeletons & animations
- ui/shimmer.tsx: reusable shimmer placeholder
- recipe-card-skeleton.tsx: skeleton grid for loading states
- CookingLoader: rebuilt as floating chef hat with orbiting sparkles
- RecipeDetailLoader: now a proper skeleton of the detail page
- PageTransition: smooth fade+lift on route change
- index.css: custom keyframes (shimmer, float, glow-pulse), thin
  scrollbars, :focus-visible, safe-area utilities
- RecipeList: skeleton grid, header with count, polished tabs,
  hover lift on cards, spring FAB on mobile
- Header: scroll-aware blur/shadow, animated active underline,
  auto-close mobile menu on navigation
- MainLayout: ambient blurred blobs in background, warm gradient
- Home hero: gradient pill badge with wobbling Sparkles,
  CTA with sliding sheen
- Login/Register buttons: brand gradient, inline spinners
- Profile: skeleton loading state instead of plain spinner
- RecipeForm streaming: glow halo behind image, blur-to-sharp reveal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:31:49 +02:00
ordinarthur
d926ad89c5 fix(sse): hijack reply + manual CORS headers on SSE stream
- reply.hijack() so Fastify doesn't send default 404 after handler returns
- Set Access-Control-Allow-Origin manually (onSend hooks don't fire on raw)
- Initial ': ok' comment line to flush headers immediately
- Guard send('error') in case stream already closed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:57:19 +02:00
ordinarthur
460f7d334c feat: streaming recipe generation + user preferences + parallel image gen
Backend:
- Prisma: add user preferences (dietaryPreference, allergies, maxCookingTime,
  equipment, cuisinePreference, servingsDefault) + migration SQL.
  Also make stripeId nullable so signup works without Stripe.
- prompts.ts: buildUserPrompt now takes a BuildPromptOptions with preferences.
  Injects strong, explicit constraints in the user message (vegan rules,
  allergy warnings, time limits, equipment availability, cuisine hints).
- recipe-generator.ts: new streamRecipe() async generator. Streams OpenAI
  chat completion with json_schema strict mode, parses the growing buffer
  to detect 'titre' and 'description' early, yields typed events:
    { type: 'delta' | 'title' | 'description' | 'complete' }
  Final event includes the parsed StructuredRecipe + cost log.
- recipes.ts route: new POST /recipes/create-stream returning SSE:
    event: progress     { step }
    event: transcription{ text }
    event: title        { title }     <- triggers parallel image gen
    event: description  { description }
    event: recipe       { recipe }
    event: image        { url }
    event: saved        { id }
    event: done
  Heartbeat every 15s to prevent proxy timeouts. Image generation is
  kicked off the moment the title is extracted, running in parallel with
  the rest of the recipe stream. Legacy POST /recipes/create still works
  and now also passes user preferences.
- users.ts route: GET /profile now returns preferences (equipment
  deserialized from JSON). New PUT /users/preferences with validation
  (diet enum, time 5-600, servings 1-20, equipment array -> JSON).
- ai.ts plugin: generateRecipe signature extended to accept preferences.

Frontend:
- api/recipe.ts: createRecipeStream() async generator that consumes SSE
  via fetch + ReadableStream + TextDecoder (EventSource can't do POST).
  Parses 'event:' and 'data:' lines, yields typed StreamEvent union.
- api/auth.ts: User interface extended with preferences; new
  UserPreferences type exported.
- api/user.ts: updatePreferences() method.
- RecipeForm.tsx: handleSubmit now consumes the stream. Live UI displays:
    1. Initial cooking loader with step label
    2. Transcription appears as soon as it's ready
    3. Title fades in the moment it's extracted (before the rest of the
       recipe finishes generating)
    4. Description appears right after
    5. Image replaces the loader when ready
    6. Small delay before navigating to the saved recipe detail page
- Profile.tsx: new 'Cuisine' tab with form for diet, allergies, max time,
  servings, cuisine preference, and equipment checkboxes (8 options).

UX improvement: perceived latency is dramatically reduced. Instead of
waiting 40s staring at a spinner, the user sees the title ~3-4s in and
can start reading, while the image finishes generating in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:53:55 +02:00
ordinarthur
98a2f9d5a1 chore: gitignore frontend/package-lock.json (pnpm project) 2026-04-08 12:42:12 +02:00
ordinarthur
e80341bc0c fix(server): allow cross-origin resource loading for /uploads
Helmet's default 'Cross-Origin-Resource-Policy: same-origin' header was
blocking the frontend (http://localhost:5173) from loading images and
audio served by the backend at /uploads/*. Set policy to 'cross-origin'
so images can be embedded in the frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:41:36 +02:00
ordinarthur
64df5db077 fix(ai): robust image pipeline + local MinIO stack + TS errors fix
Image generation:
- Automatic model fallback: try gpt-image-1 first, fall back to
  dall-e-3 if it fails (e.g. org not verified on OpenAI)
- Local filesystem fallback: if MinIO upload fails, write the image
  to backend/uploads/recipes/ and return a URL served by fastify-static
- Unified handling of base64 vs URL responses from the Images API
- DALL-E quality mapped automatically (low/medium/high -> standard)

Local MinIO stack:
- docker-compose.yml at repo root with minio + minio-init service
  that auto-creates the bucket and makes it publicly readable
- Default credentials: freedge / freedge123 (configurable)
- Console at :9001, API at :9000
- .env.example now points to the local stack by default

Static file serving:
- Register @fastify/static to serve ./uploads at /uploads/*
- Enables local fallback to return usable URLs to the frontend
- New PUBLIC_BASE_URL env var to build absolute URLs

TypeScript errors (21 -> 0):
- JWT typing via '@fastify/jwt' module augmentation (FastifyJWT
  interface with payload + user) fixes all request.user.id errors
- Stripe constructor now passes required StripeConfig
- fastify.createCustomer guards checked on the helper itself for
  proper TS narrowing (not on fastify.stripe)
- Remove 'done' arg from async onClose hook
- MinIO transport.agent + listFiles return type cast ignored

README:
- Add 'Stockage des fichiers' section explaining the two modes
- Updated setup instructions to start with docker compose up

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:30:47 +02:00
ordinarthur
9dbd7e0ba9 feat(ai): quality-first recipe pipeline with structured outputs
Overhaul of the backend AI module to produce better recipes, better
images, more reliably, and cheaper.

New src/ai/ module:
- prompts.ts: long 'Chef Antoine' system prompt (~1500 tokens) with
  explicit originality rules, technical precision requirements, vocal
  transcription handling, and 3 few-shot style examples. Long enough
  to benefit from OpenAI's automatic prompt caching (-50% on cached
  portion from the 2nd call onward).
- recipe-generator.ts: uses Structured Outputs (json_schema strict).
  Rich schema: titre, description, origine_inspiration, ingredients
  with quantity/notes/complement flag, numbered etapes with per-step
  duration, conseils array, accord_boisson. No more JSON.parse crashes.
- image-generator.ts: switched from dall-e-3 to gpt-image-1 (medium
  quality by default). Much better photographic realism. Dedicated
  magazine-style prompt (editorial food photography, 45-deg overhead,
  natural light, stoneware). Slugify preserves extended Latin chars
  (cote-de-boeuf not c-te-de-b-uf).
- transcriber.ts: migrated from whisper-1 to gpt-4o-mini-transcribe
  (50% cheaper, better on French). Includes a context prompt to bias
  toward culinary vocabulary.
- cost.ts: centralized pricing table + helpers. Every OpenAI call now
  emits a structured log with model, durationMs, costUsd, usage, and
  cacheHit flag.

Plugin refactor:
- plugins/ai.ts now delegates to src/ai/* and only keeps the Fastify
  decoration glue + storage fallback for audio.
- OpenAI client configured with maxRetries=3, timeout=60s.
- Image generation runs in parallel with the recipe flatten/serialize
  step (minor speedup, ~0.5s).
- flattenRecipe() converts the rich structured recipe into the legacy
  flat RecipeData shape (for Prisma columns) while preserving the
  structured form in recipeData.structured.

Routes:
- recipes.ts stores the structured JSON in generatedRecipe (instead
  of the aplatissement lossy), enabling future frontends to render
  rich recipes with per-ingredient notes and step timers.

Env vars:
- OPENAI_TRANSCRIBE_MODEL, OPENAI_IMAGE_MODEL, OPENAI_IMAGE_QUALITY,
  OPENAI_IMAGE_SIZE, OPENAI_MAX_RETRIES, OPENAI_TIMEOUT_MS

Cost per recipe (estimated):
- Before: ~$0.044 (whisper $0.003 + 4o-mini $0.0004 + dall-e-3 $0.04)
- After : ~$0.018 (4o-mini-transcribe $0.0015 + 4o-mini $0.0004
  + gpt-image-1 medium $0.0165), ~-59%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:46:18 +02:00
ordinarthur
fc3dfe83c9 refactor(backend): migrate from JavaScript to TypeScript
- tsconfig.json strict mode (noImplicitAny, strictNullChecks, noUnused*)
- Replace nodemon with tsx (watch + run TS directly)
- Build script (tsc -> dist/) and typecheck script
- Fastify decorator types in types/fastify.d.ts (prisma, openai,
  stripe, googleClient, auth helpers, ai helpers, request.user)
- Typed route handlers with generic Body/Params
- Strict null checks on Prisma results and env vars
- Stripe plugin now optional (no-op if STRIPE_SECRET_KEY missing)
- Delete dead utils/errors.js (empty) and utils/resend.js
  (contained a hardcoded Resend API key, unused)
- Add @types/bcrypt, @types/nodemailer, typescript-eslint
- ESLint upgraded to typescript-eslint flat config
- deploy.sh: run prisma generate, migrate deploy, backend build

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:36:21 +02:00
ordinarthur
0134390f5e refactor: cleanup, security hardening & frontend dedup
Backend:
- Remove malicious crypto dep; use node:crypto
- Add helmet + rate-limit (100 req/min)
- CORS whitelist via CORS_ORIGINS env
- Validate required env vars on boot (fail fast)
- Health endpoint + clean shutdown (SIGINT/SIGTERM)
- Multipart limits (15MB / 1 file)
- Fix findUnique composite where bug (use findFirst)
- Wrap JSON.parse(generatedRecipe) in try/catch
- Isolate DALL-E best-effort; ENABLE_IMAGE_GENERATION toggle
- Lazy MinIO client, safe TLS handling
- Uniform fastify.hashPassword/comparePassword
- Proper audio cleanup on delete
- ESLint flat config, Prettier, .env.example, .editorconfig

Frontend:
- Delete 10 orphan/duplicate components
- Remove orphan pages/recipe/, data/recipes.ts, root src/
- Fix /reset-password route order (was unreachable)
- Remove unused ky dep

Docs:
- README rewritten to match real routes and env vars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:00:54 +02:00
Arthur Barre
9b98b3c0c6 fix minio2 2025-03-16 19:44:18 +01:00
Arthur Barre
fe493fe053 fix minio 2025-03-16 19:43:11 +01:00
Arthur Barre
8b6d76fdea add minio storage 2025-03-15 13:11:24 +01:00
Arthur Barre
bb7a083ae8 corret google analytics 2025-03-14 17:53:26 +01:00
Arthur Barre
adfc73ffa7 add google analytics + favicon 2025-03-14 17:42:41 +01:00
Arthur Barre
81f3900ac8 change UI 2025-03-14 00:58:18 +01:00
Arthur Barre
b311e76bf3 add reset password from mail 2025-03-13 23:03:29 +01:00
Arthur Barre
7c2374f0b3 change mail + change password ok ! 2025-03-13 22:31:10 +01:00
Arthur Barre
223b593495 add more data + image on recipe 2025-03-13 22:17:39 +01:00
Arthur Barre
bd5891ff1b update frontend routes 2025-03-13 22:04:04 +01:00
Arthur Barre
45a26e269d new build 2025-03-13 21:36:42 +01:00
Arthur Barre
20b9b17493 fix 2025-03-13 21:26:24 +01:00
Arthur Barre
de70929a62 use new audio recorder 2025-03-13 21:18:21 +01:00
Arthur Barre
20c345f05a change background color 2025-03-11 13:59:32 +01:00
Arthur Barre
c1456eec18 add manifest 2025-03-11 13:57:07 +01:00
Arthur Barre
8228fe865f update UI 2025-03-11 13:47:39 +01:00
Arthur Barre
46f50e3054 use pnpm for frontend 2025-03-11 13:35:25 +01:00
Arthur Barre
e3e6031a6c use npm instead pnpm 2025-03-11 13:34:23 +01:00
Arthur Barre
f1aa2246ea update deploy 2025-03-11 13:28:36 +01:00
Arthur Barre
81c23109c2 add deploy sh build front 2025-03-11 13:22:35 +01:00
Arthur Barre
dcb3a5c0a8 add deploy sh 2025-03-11 13:20:48 +01:00
Arthur Barre
f07767aa81 add deploy sh 2025-03-11 13:19:06 +01:00
Arthur Barre
851b8ee89c add deploy script 2025-03-11 13:16:29 +01:00
Arthur Barre
36f65152f5 correct auth forms 2025-03-11 13:14:13 +01:00
Arthur Barre
191b8a4da2 add google sso 2025-03-11 13:05:13 +01:00
Arthur Barre
3e85733f4a correct ui 2025-03-11 12:45:52 +01:00