🔧 CTO PMG — IMPLÉMENTATION PAR TAILLE

🔧 BIBLE CTO — IMPLÉMENTATION PAR TAILLE DE PROJET

Petit · Moyen · Grand

Code: [NOVA:CTO-PMG-V1-20260219]

Stack imposée Dream Nova : Next.js App Router, Python async, Supabase, Tailwind CSS 4, Vercel, TypeScript strict.

JAMAIS changer la stack sans raison technique impérative. La cohérence est une arme compétitive.


TABLE DES MATIÈRES

  1. Classification Technique
  2. GOD-BOILERPLATE — Template Universel DreamNova
  3. PARTIE A — Petit Projet (< 24h)

- A1. Pipeline Voice → Image ([Client] Case Study)

- A2. Landing Page Rapide (< 30 min)

- A3. Script Python Automation Template

  1. PARTIE B — Moyen Projet (2-7 jours)

- B1. Architecture Type

- B2. Schema Supabase Standard

- B3. Stripe Integration Standard

- B4. Performance Checklist Lighthouse >90

- B5. Deployment Checklist (20 items)

  1. PARTIE C — Grand Projet (> 1 semaine)

- C1. Architecture Multi-Services

- C2. CI/CD Pipeline Complet

- C3. Monitoring & Auto-Repair

- C4. Database Migration Strategy

- C5. Security Grand Projet

  1. Règles d'Or CTO (10 Non-Négociables)

CLASSIFICATION TECHNIQUE

Le premier acte du CTO est de classifier le projet avant d'écrire une seule ligne de code. Une mauvaise classification = surengineering ou dette technique immédiate.

CritèrePETITMOYENGRAND
Durée< 24h2–7 jours> 1 semaine
Revenu cible0–2 000 ₪/mois2 000–15 000 ₪/mois15 000 ₪/mois+
FrontendHTML/CSS ou Next.js simpleNext.js App Router completNext.js + Mobile React Native
BackendAPI route ou Python scriptNext.js API routes + SupabaseNode.js/Python séparé + Supabase
AuthAucune ou Supabase Magic LinkSupabase Auth completSupabase Auth + RLS policies exhaustives
PaiementStripe Payment Link (zéro code)Stripe Checkout SessionStripe Subscriptions + LemonSqueezy
DBAucune ou Supabase simpleSupabase PostgreSQL versionnéSupabase + migrations versionnées + seeds
DeployVercel en 1 commandeVercel + env vars + domaineVercel + GitHub Actions CI/CD complet
TestsManuel seulPlaywright QA basiquePlaywright E2E complet + Sentry + alertes
MonitoringAucunVercel AnalyticsSentry + Upstash + dashboards
Exemple DreamNovaScript [Client], Landing ArielKeren Rabbi Yisrael, Esther IfrahDreamNova Platform, DreamOS

Règle de Classification Cash-First

Avant TOUT projet, évaluer silencieusement :


SI feature → paiement &lt; 7 jours   → code production-grade, max effort
SINON                              → MVP &lt;15 lignes, log l'idée, revenir au revenue work

Cette règle est intégrée dans l'identité CTO DreamNova. Elle n'est pas optionnelle.


GOD-BOILERPLATE

Le Template Universel DreamNova

Tout projet DreamNova démarre depuis ce template. Le dupliquer, jamais le réécrire.

Structure de dossiers exacte


project-name/
├── app/
│   ├── (marketing)/              # Pages publiques : /, /about, /pricing, /contact
│   │   ├── page.tsx              # Homepage
│   │   ├── pricing/page.tsx      # Page tarifs
│   │   └── contact/page.tsx      # Formulaire contact
│   ├── (shop)/                   # E-commerce
│   │   ├── checkout/page.tsx     # Checkout Stripe
│   │   └── success/page.tsx      # Confirmation commande
│   ├── (auth)/                   # Authentification
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── (dashboard)/              # Zone connectée
│   │   ├── layout.tsx            # Layout avec sidebar
│   │   ├── overview/page.tsx     # Dashboard principal
│   │   ├── orders/page.tsx       # Commandes
│   │   └── settings/page.tsx     # Paramètres
│   ├── api/
│   │   ├── stripe/
│   │   │   ├── checkout/route.ts # POST → session Stripe
│   │   │   └── webhook/route.ts  # POST → events Stripe
│   │   ├── contact/route.ts      # POST → email Resend
│   │   └── waitlist/route.ts     # POST → inscription waitlist
│   ├── globals.css               # Variables CSS globales + Tailwind
│   └── layout.tsx                # Root layout (fonts, metadata)
├── components/
│   ├── ui/                       # Composants atomiques (Shadcn/ui base)
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   └── input.tsx
│   ├── sections/                 # Sections de pages réutilisables
│   │   ├── Hero.tsx
│   │   ├── Features.tsx
│   │   ├── Testimonials.tsx
│   │   └── PricingTable.tsx
│   └── layout/                   # Composants layout
│       ├── Header.tsx
│       ├── Footer.tsx
│       └── Sidebar.tsx
├── lib/
│   ├── supabase/
│   │   ├── client.ts             # createBrowserClient()
│   │   ├── server.ts             # createServerClient() (RSC/API)
│   │   └── middleware.ts         # Session refresh middleware
│   ├── stripe.ts                 # Instance Stripe configurée
│   ├── resend.ts                 # Instance Resend configurée
│   ├── validations.ts            # Schemas Zod partagés
│   └── utils.ts                  # cn(), formatPrice(), etc.
├── public/
│   ├── images/                   # Images optimisées WebP
│   ├── icons/                    # SVG icons
│   └── og-image.png              # Open Graph image (1200x630)
├── supabase/
│   ├── migrations/               # Migrations SQL numérotées
│   │   └── 001_initial_schema.sql
│   └── seed.sql                  # Données de test
├── .env.local.example            # Template variables d'env
├── .env.local                    # Variables réelles (JAMAIS commitées)
├── next.config.js                # Config Next.js + security headers
├── tailwind.config.ts            # Tokens design DreamNova
├── tsconfig.json                 # TypeScript strict
├── middleware.ts                  # Supabase session + rate limiting
└── package.json

Packages inclus par défaut


{
  "dependencies": {
    "next": "^15.0.0",
    "@supabase/supabase-js": "^2.0.0",
    "@supabase/ssr": "^0.5.0",
    "stripe": "^17.0.0",
    "resend": "^4.0.0",
    "framer-motion": "^11.0.0",
    "zod": "^3.0.0",
    "clsx": "^2.0.0",
    "tailwind-merge": "^2.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0",
    "tailwindcss": "^4.0.0",
    "@playwright/test": "^1.40.0"
  }
}

Variables .env.local requises


# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://XXXX.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...

# Resend
RESEND_API_KEY=re_...

# Site
NEXT_PUBLIC_SITE_URL=https://projet.vercel.app

# Gemini (pour scripts Python)
GEMINI_API_KEY=AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts

# Optionnel: Sentry (grands projets)
SENTRY_DSN=https://...@sentry.io/...

Commande setup alias


# Ajouter dans ~/.zshrc
alias dn-init='function _dn_init() {
  git clone https://github.com/CodeNoLimits/dreamnova-boilerplate "$1"
  cd "$1"
  npm install
  cp .env.local.example .env.local
  echo "Projet $1 pret. Edit .env.local puis npm run dev"
}; _dn_init'

# Premier deploy (token permanent DreamNova)
alias dn-deploy='vercel --prod --token=VCP_TOKEN_REDACTED'

next.config.js obligatoire (headers sécurité)


/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/webp', 'image/avif'],
    minimumCacheTTL: 31536000,
  },
  async headers() {
    return [{
      source: '/(.*)',
      headers: [
        { key: 'X-Frame-Options', value: 'DENY' },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
        {
          key: 'Content-Security-Policy',
          value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' *.stripe.com; frame-src *.stripe.com;"
        }
      ]
    }]
  }
}
module.exports = nextConfig

PARTIE A — Petit Projet (< 24h)

A1. Pipeline Voice → Image ([Client] Case Study — Template Réutilisable)

Contexte réel : Client [Client] envoie un message vocal en hébreu avec des instructions de retouche photo pour ses produits bijoux. Pipeline complet : audio → texte → prompt structuré → image éditée → validation auto.

Durée d'implémentation : 4 heures. Valeur client : 200-500 ₪ par session.


# ~/scripts/workflows/voice_to_image_pipeline.py
"""
Pipeline Voice -&gt; Image Pipeline (DreamNova Template)
Usage: python3 voice_to_image_pipeline.py audio.ogg photo1.jpg photo2.jpg
"""
import os
import sys
import json
import time
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional

import requests
from groq import Groq
import google.generativeai as genai
from PIL import Image

# Configuration
GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
NANO_BANANA_KEY = os.environ.get("NANO_BANANA_PRO_KEY", "")
GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts")
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = "7269582214"  # David @nerostats

# Logging standard DreamNova
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(f"/tmp/voice_to_image_{int(time.time())}.log"),
        logging.StreamHandler()
    ]
)
log = logging.getLogger(__name__)


def send_telegram_alert(message: str) -&gt; None:
    """Alerte Telegram David en cas d'erreur critique."""
    if not TELEGRAM_TOKEN:
        return
    try:
        requests.post(
            f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
            json={"chat_id": TELEGRAM_CHAT_ID, "text": f"[VoiceImage] {message}"},
            timeout=5
        )
    except Exception:
        pass


def transcribe_audio(audio_path: str, language: str = "he") -&gt; str:
    """
    Etape 1: Groq Whisper large-v3-turbo -&gt; texte exact.
    Supporte: OGG, MP3, MP4, WAV, M4A, WebM.
    """
    log.info(f"Transcription de {audio_path} (langue: {language})")
    client = Groq(api_key=GROQ_KEY)
    with open(audio_path, "rb") as f:
        transcription = client.audio.transcriptions.create(
            file=(Path(audio_path).name, f),
            model="whisper-large-v3-turbo",
            language=language,
            response_format="text"
        )
    log.info(f"Transcription: {transcription[:200]}...")
    return transcription


def parse_instructions_to_prompts(raw_text: str, image_paths: list[str]) -&gt; list[dict]:
    """
    Etape 2: Gemini parse les instructions vocales en prompts structurés.
    Retourne: [{image_path, prompt_english, style_notes, priority}]
    """
    genai.configure(api_key=GEMINI_KEY)
    model = genai.GenerativeModel("gemini-2.0-flash")

    system_prompt = f"""
    Tu es un expert retouche photo bijouterie luxe.
    Instructions vocales du client (hébreu/français/anglais melange):
    "{raw_text}"

    Images disponibles: {[Path(p).name for p in image_paths]}

    Genere un JSON valide avec cette structure exacte:
    [
      {{
        "image_filename": "nom_exact_du_fichier.jpg",
        "prompt_english": "Professional product photography: [instruction en anglais ultra-precis]",
        "style_notes": "luxury jewelry, white background, golden ratio, 8K",
        "priority": 1
      }}
    ]

    Regles:
    - prompt_english doit etre en anglais uniquement (les APIs image n'acceptent que l'anglais)
    - Sois TRES specifique: couleurs, eclairage, angles, corrections demandees
    - Si une instruction concerne plusieurs photos, duplique l'entree
    - Trie par priorite (1 = plus important)
    """

    response = model.generate_content(system_prompt)
    # Extraire le JSON de la reponse
    text = response.text.strip()
    start = text.find("[")
    end = text.rfind("]") + 1
    parsed = json.loads(text[start:end])

    # Mapper nom fichier -&gt; chemin complet
    name_to_path = {Path(p).name: p for p in image_paths}
    for item in parsed:
        item["image_path"] = name_to_path.get(item.get("image_filename", ""), image_paths[0])

    log.info(f"Parsed {len(parsed)} instructions depuis le vocal")
    return parsed


def edit_image_nanobananaPro(image_path: str, prompt: str, output_dir: str = "/tmp") -&gt; Optional[str]:
    """
    Etape 3: Nano Banana Pro API -&gt; image editee.
    Retourne: chemin local du fichier resultat.
    """
    log.info(f"Edition image: {Path(image_path).name} | Prompt: {prompt[:80]}...")
    output_path = Path(output_dir) / f"edited_{Path(image_path).stem}_{int(time.time())}.webp"

    try:
        with open(image_path, "rb") as img:
            resp = requests.post(
                "https://api.nanobananapro.com/v1/edit",
                headers={"Authorization": f"Bearer {NANO_BANANA_KEY}"},
                files={"image": (Path(image_path).name, img, "image/jpeg")},
                data={
                    "prompt": prompt,
                    "output_format": "webp",
                    "quality": 95,
                    "style": "professional product photography"
                },
                timeout=120
            )
        resp.raise_for_status()
        result = resp.json()

        # Telecharger l'image resultante
        img_url = result.get("output_url") or result.get("url")
        if img_url:
            img_data = requests.get(img_url, timeout=30).content
            output_path.write_bytes(img_data)
            log.info(f"Image sauvegardee: {output_path}")
            return str(output_path)

    except requests.exceptions.RequestException as e:
        log.error(f"Erreur API Nano Banana: {e}")
        send_telegram_alert(f"Erreur edition image: {e}")
        return None


def auto_evaluate_gemini_vision(image_path: str, original_prompt: str) -&gt; tuple[bool, str]:
    """
    Etape 3.5: Gemini Vision verifie PASS/FAIL et donne raison.
    Retourne: (bool passed, str reason)
    """
    genai.configure(api_key=GEMINI_KEY)
    model = genai.GenerativeModel("gemini-2.0-flash")

    img = Image.open(image_path)
    response = model.generate_content([
        img,
        f"""Analyse cette image de produit bijouterie.
        Prompt demande: "{original_prompt}"

        Verifie:
        1. L'image correspond-elle exactement au prompt?
        2. Qualite professionnelle bijouterie luxe?
        3. Problemes visibles (flou, artefacts, deformations)?

        Reponds UNIQUEMENT: PASS ou FAIL, puis une ligne d'explication."""
    ])

    text = response.text.strip()
    passed = text.upper().startswith("PASS")
    log.info(f"Evaluation: {'PASS' if passed else 'FAIL'} | {text[:100]}")
    return passed, text


def process_single_image(instruction: dict, output_dir: str, max_retries: int = 3) -&gt; Optional[str]:
    """
    Traitement d'une image avec retry automatique et prompt refinement.
    """
    prompt = instruction["prompt_english"]
    image_path = instruction["image_path"]
    style_notes = instruction.get("style_notes", "professional product photography, 8K quality")

    full_prompt = f"{prompt}, {style_notes}"

    for attempt in range(max_retries):
        result_path = edit_image_nanobananaPro(image_path, full_prompt, output_dir)
        if not result_path:
            continue

        passed, reason = auto_evaluate_gemini_vision(result_path, prompt)
        if passed:
            log.info(f"Image validee en {attempt + 1} tentative(s)")
            return result_path

        # Raffiner le prompt a chaque echec
        refinements = [
            f"{prompt}, ultra-sharp details, perfect lighting, no artifacts",
            f"{prompt}, studio lighting, clean background, professional retouching, photorealistic",
            f"High-end jewelry photography: {prompt}, masterpiece quality, award-winning composition"
        ]
        full_prompt = f"{refinements[min(attempt, len(refinements)-1)]}, {style_notes}"
        log.warning(f"Tentative {attempt + 1} echouee: {reason[:80]}. Nouveau prompt applique.")
        time.sleep(2)

    log.error(f"Echec apres {max_retries} tentatives pour {Path(image_path).name}")
    return None


def process_batch(audio_path: str, image_paths: list[str], output_dir: str = "/tmp/voice_image_output") -&gt; list[str]:
    """
    Pipeline complet: audio -&gt; texte -&gt; prompts -&gt; images editees (batch parallele).
    """
    Path(output_dir).mkdir(parents=True, exist_ok=True)

    # Etape 1: Transcription
    raw_text = transcribe_audio(audio_path)

    # Etape 2: Parse instructions
    instructions = parse_instructions_to_prompts(raw_text, image_paths)
    log.info(f"Pipeline: {len(instructions)} images a traiter en parallele")

    # Etape 3 + 3.5: Edition + validation (4 workers max)
    results = []
    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = {
            executor.submit(process_single_image, inst, output_dir): inst
            for inst in sorted(instructions, key=lambda x: x.get("priority", 99))
        }
        for future in as_completed(futures):
            inst = futures[future]
            result_path = future.result()
            if result_path:
                results.append(result_path)
                log.info(f"Traite: {Path(inst['image_path']).name} -&gt; {result_path}")
            else:
                log.error(f"Echec: {Path(inst['image_path']).name}")

    log.info(f"Pipeline termine: {len(results)}/{len(instructions)} images reussies")
    send_telegram_alert(f"Pipeline termine: {len(results)}/{len(instructions)} images. Output: {output_dir}")
    return results


if __name__ == "__main__":
    if len(sys.argv) &lt; 3:
        print("Usage: python3 voice_to_image_pipeline.py audio.ogg photo1.jpg [photo2.jpg ...]")
        sys.exit(1)

    audio = sys.argv[1]
    photos = sys.argv[2:]

    if not Path(audio).exists():
        log.error(f"Fichier audio introuvable: {audio}")
        sys.exit(1)

    results = process_batch(audio, photos)
    print(f"Resultats ({len(results)} images):")
    for r in results:
        print(f"  -&gt; {r}")

A2. Landing Page Rapide (< 30 minutes)

Template Next.js minimal, Lighthouse >90 garanti, CTA Stripe Payment Link sans auth.


// app/(marketing)/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: '[Nom Service] — [Promesse principale]',
  description: '[Bénéfice en 155 caractères max. Pas de blabla.]',
  openGraph: { images: ['/og-image.png'] }
}

// Design tokens: adapter selon projet
const DESIGN = {
  primary: '#D4AF37',   // Luxury
  bg: '#000000',
  text: '#FFFFFF',
  cta: 'https://buy.stripe.com/XXXXXX'  // Payment Link Stripe
}

const FEATURES = [
  { icon: '⚡', title: 'Rapide', desc: 'Livraison en 24h, pas en 2 semaines.' },
  { icon: '🎯', title: 'Précis', desc: 'Résultat exact selon vos instructions.' },
  { icon: '🔒', title: 'Garanti', desc: 'Satisfait ou remboursé sans question.' }
]

export default function LandingPage() {
  return (
    &lt;main className="min-h-screen bg-black text-white font-sans"&gt;
      {/* Hero */}
      &lt;section className="flex flex-col items-center justify-center min-h-screen px-4 text-center"&gt;
        &lt;h1 className="text-4xl md:text-6xl font-bold mb-6 leading-tight"
            style={{ color: DESIGN.primary }}&gt;
          [Problème Client&lt;br/&gt;En Gras Ici]
        &lt;/h1&gt;
        &lt;p className="text-xl md:text-2xl text-gray-300 mb-8 max-w-2xl"&gt;
          [Solution en 1-2 phrases. Concret. Pas de jargon.]
        &lt;/p&gt;
        &lt;a
          href={DESIGN.cta}
          className="px-8 py-4 rounded-xl text-black text-xl font-bold
                     hover:scale-105 transition-transform duration-200"
          style={{ backgroundColor: DESIGN.primary }}
        &gt;
          Commencer — [Prix] ₪
        &lt;/a&gt;
        &lt;p className="mt-4 text-sm text-gray-500"&gt;Paiement sécurisé · Satisfait ou remboursé&lt;/p&gt;
      &lt;/section&gt;

      {/* Features */}
      &lt;section className="py-20 px-4"&gt;
        &lt;div className="max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8"&gt;
          {FEATURES.map((f) =&gt; (
            &lt;div key={f.title}
                 className="p-6 rounded-2xl border border-gray-800 bg-gray-950
                            hover:border-yellow-600 transition-colors duration-200"&gt;
              &lt;div className="text-4xl mb-4"&gt;{f.icon}&lt;/div&gt;
              &lt;h3 className="text-xl font-bold mb-2" style={{ color: DESIGN.primary }}&gt;
                {f.title}
              &lt;/h3&gt;
              &lt;p className="text-gray-400"&gt;{f.desc}&lt;/p&gt;
            &lt;/div&gt;
          ))}
        &lt;/div&gt;
      &lt;/section&gt;

      {/* Testimonial */}
      &lt;section className="py-16 px-4 bg-gray-950"&gt;
        &lt;blockquote className="max-w-2xl mx-auto text-center"&gt;
          &lt;p className="text-2xl italic text-gray-200 mb-4"&gt;
            "[Résultat concret obtenu par un vrai client. Chiffres si possible.]"
          &lt;/p&gt;
          &lt;cite className="text-gray-500"&gt;— [Prénom], [Ville]&lt;/cite&gt;
        &lt;/blockquote&gt;
      &lt;/section&gt;

      {/* CTA Final */}
      &lt;section className="py-20 px-4 text-center"&gt;
        &lt;h2 className="text-3xl font-bold mb-6"&gt;Prêt à [résultat désiré] ?&lt;/h2&gt;
        &lt;a
          href={DESIGN.cta}
          className="inline-block px-10 py-5 rounded-xl text-black text-xl font-bold
                     hover:scale-105 transition-transform duration-200"
          style={{ backgroundColor: DESIGN.primary }}
        &gt;
          Oui, je veux [résultat] — [Prix] ₪
        &lt;/a&gt;
      &lt;/section&gt;
    &lt;/main&gt;
  )
}

A3. Script Python Automation Template

Template standard réutilisable pour TOUT script d'automatisation ponctuel DreamNova.


#!/usr/bin/env python3
"""
[Nom du script] — [Description en une ligne]
Usage: python3 nom_script.py [args]
Auteur: DreamNova CTO (David Amor)
"""
import os
import sys
import time
import logging
import json
import traceback
from pathlib import Path
from functools import wraps
from typing import Any, Optional

import requests
from dotenv import load_dotenv

load_dotenv(Path.home() / ".env.local")

# === CONFIGURATION ===
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = "7269582214"
MAX_RETRIES = 3
RETRY_DELAY = 2  # secondes

# === LOGGING STANDARD ===
log_file = Path("/tmp") / f"{Path(__file__).stem}_{int(time.time())}.log"
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(funcName)s: %(message)s",
    handlers=[logging.FileHandler(log_file), logging.StreamHandler()]
)
log = logging.getLogger(__name__)


# === DECORATEUR RETRY ===
def retry(max_attempts: int = MAX_RETRIES, delay: float = RETRY_DELAY):
    """Retry automatique avec backoff exponentiel."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    wait = delay * (2 ** attempt)
                    log.warning(f"Tentative {attempt+1}/{max_attempts} echouee: {e}. Retry dans {wait}s")
                    time.sleep(wait)
        return wrapper
    return decorator


# === ALERTES TELEGRAM ===
def alert(message: str, level: str = "INFO") -&gt; None:
    """Envoie alerte Telegram. Utiliser pour erreurs critiques uniquement."""
    if not TELEGRAM_BOT_TOKEN:
        return
    emoji = {"INFO": "", "WARNING": "[!]", "ERROR": "[ERREUR]", "SUCCESS": "[OK]"}.get(level, "")
    try:
        requests.post(
            f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
            json={"chat_id": TELEGRAM_CHAT_ID, "text": f"{emoji} [{Path(__file__).stem}] {message}"},
            timeout=5
        )
    except Exception:
        pass


# === OUTPUT VERS FICHIER ===
def save_output(data: Any, filename: Optional[str] = None) -&gt; Path:
    """Sauvegarde output JSON + retourne chemin."""
    output_dir = Path("/tmp/dreamnova_outputs")
    output_dir.mkdir(exist_ok=True)
    outfile = output_dir / (filename or f"{Path(__file__).stem}_output_{int(time.time())}.json")
    outfile.write_text(json.dumps(data, ensure_ascii=False, indent=2))
    log.info(f"Output sauvegarde: {outfile}")
    return outfile


# === FONCTIONS METIER ===
@retry()
def fetch_data(url: str) -&gt; dict:
    """Exemple: fetch avec retry automatique."""
    response = requests.get(url, timeout=30)
    response.raise_for_status()
    return response.json()


def process_item(item: dict) -&gt; dict:
    """Traitement unitaire. Isoler chaque transformation ici."""
    # Logique metier ici
    return item


def run(args: list[str]) -&gt; int:
    """Point d'entree principal. Retourne 0=succes, 1=erreur."""
    log.info(f"Demarrage {Path(__file__).name} avec args: {args}")
    start = time.time()

    try:
        # 1. Validation des args
        if not args:
            log.error("Aucun argument fourni")
            print("Usage: python3 nom_script.py [input]")
            return 1

        # 2. Traitement principal
        results = []
        for arg in args:
            result = process_item({"input": arg})
            results.append(result)
            log.info(f"Traite: {arg}")

        # 3. Sauvegarder output
        outfile = save_output(results)

        # 4. Rapport final
        duration = time.time() - start
        summary = f"{len(results)} elements traites en {duration:.1f}s. Output: {outfile}"
        log.info(summary)
        alert(summary, "SUCCESS")
        return 0

    except KeyboardInterrupt:
        log.info("Interrompu par utilisateur")
        return 0
    except Exception as e:
        error_msg = f"Erreur fatale: {e}\n{traceback.format_exc()}"
        log.error(error_msg)
        alert(f"ERREUR CRITIQUE: {e}", "ERROR")
        return 1


if __name__ == "__main__":
    sys.exit(run(sys.argv[1:]))

PARTIE B — Moyen Projet (2-7 jours)

B1. Architecture Type Moyen Projet

La structure App Router est NON-NÉGOCIABLE pour tous les projets moyens DreamNova. Route groups = séparation claire des responsabilités = CI/CD plus simple.

Règle clé : Chaque route group (nom) peut avoir son propre layout.tsx. Le dashboard a un layout différent du marketing. Ne jamais mélanger.


project-name/
├── app/
│   ├── (marketing)/          # Pages publiques - pas d'auth
│   ├── (shop)/               # E-commerce - auth optionnelle
│   ├── (auth)/               # Login/Register - redirect si déjà connecté
│   ├── (dashboard)/          # Zone protégée - middleware vérifie session
│   └── api/                  # API routes - JAMAIS de logique UI ici
├── components/               # Réutilisables UNIQUEMENT si utilisés 2+ fois
├── lib/                      # Utilitaires purs (pas de JSX)
├── public/                   # Assets statiques WebP/SVG uniquement
└── supabase/migrations/      # UN fichier par migration, numéroté

B2. Schema Supabase Standard

Ce schema couvre 80% des projets moyens DreamNova. L'adapter, jamais repartir de zéro.


-- ============================================================
-- SCHEMA STANDARD DREAMNOVA v2.0
-- Testé sur: Keren Rabbi Yisrael, Esther Ifrah, Baroukh Sagit
-- ============================================================

-- Extension UUID (activée par défaut sur Supabase)
create extension if not exists "uuid-ossp";

-- ============================================================
-- TABLE: profiles (extension de auth.users)
-- ============================================================
create table public.profiles (
  id            uuid references auth.users on delete cascade not null,
  email         text unique not null,
  full_name     text,
  phone         text,
  role          text default 'customer' check (role in ('customer', 'admin', 'staff')),
  locale        text default 'fr' check (locale in ('fr', 'he', 'en', 'es', 'ru', 'ar')),
  created_at    timestamptz default now() not null,
  updated_at    timestamptz default now() not null,
  primary key (id)
);

create index idx_profiles_email on public.profiles(email);

-- ============================================================
-- TABLE: products
-- ============================================================
create table public.products (
  id            uuid default gen_random_uuid() primary key,
  slug          text unique not null,
  name_fr       text not null,
  name_he       text,
  name_en       text,
  description   text,
  price         integer not null,         -- en agorot (₪ × 100)
  compare_price integer,                  -- prix barre
  stock         integer default 0,
  images        text[] default '{}'::text[],
  category      text,
  tags          text[] default '{}'::text[],
  is_active     boolean default true,
  metadata      jsonb default '{}'::jsonb,
  created_at    timestamptz default now(),
  updated_at    timestamptz default now()
);

create index idx_products_slug on public.products(slug);
create index idx_products_category on public.products(category);
create index idx_products_active on public.products(is_active);

-- ============================================================
-- TABLE: orders
-- ============================================================
create table public.orders (
  id                        uuid default gen_random_uuid() primary key,
  user_id                   uuid references public.profiles(id),
  -- user_id NULL = commande invité (email stocké dans shipping_address)
  stripe_payment_intent_id  text unique,
  stripe_session_id         text unique,
  amount                    integer not null,           -- total en agorot
  currency                  text default 'ILS',
  status                    text default 'pending'
                            check (status in ('pending', 'paid', 'failed', 'refunded', 'shipped', 'delivered')),
  shipping_address          jsonb,                     -- {name, email, line1, city, country, zip}
  metadata                  jsonb default '{}'::jsonb,
  created_at                timestamptz default now(),
  updated_at                timestamptz default now()
);

create index idx_orders_user on public.orders(user_id);
create index idx_orders_status on public.orders(status);
create index idx_orders_stripe on public.orders(stripe_payment_intent_id);

-- ============================================================
-- TABLE: order_items
-- ============================================================
create table public.order_items (
  id          uuid default gen_random_uuid() primary key,
  order_id    uuid references public.orders(id) on delete cascade not null,
  product_id  uuid references public.products(id),
  quantity    integer not null default 1,
  unit_price  integer not null,   -- prix au moment de l'achat (snapshot)
  total_price integer not null,
  metadata    jsonb default '{}'::jsonb
);

create index idx_order_items_order on public.order_items(order_id);

-- ============================================================
-- ROW LEVEL SECURITY (RLS) — OBLIGATOIRE sur TOUTES les tables
-- ============================================================
alter table public.profiles enable row level security;
alter table public.products enable row level security;
alter table public.orders enable row level security;
alter table public.order_items enable row level security;

-- Profiles: chaque user voit/modifie uniquement le sien
create policy "profiles_select_own" on public.profiles
  for select using (auth.uid() = id);

create policy "profiles_update_own" on public.profiles
  for update using (auth.uid() = id);

create policy "profiles_insert_own" on public.profiles
  for insert with check (auth.uid() = id);

-- Admins voient tout
create policy "profiles_admin_all" on public.profiles
  using (exists (
    select 1 from public.profiles p
    where p.id = auth.uid() and p.role = 'admin'
  ));

-- Products: lecture publique, écriture admin uniquement
create policy "products_select_all" on public.products
  for select using (is_active = true);

create policy "products_admin_write" on public.products
  for all using (exists (
    select 1 from public.profiles p
    where p.id = auth.uid() and p.role = 'admin'
  ));

-- Orders: user voit ses commandes, admin voit tout
create policy "orders_select_own" on public.orders
  for select using (auth.uid() = user_id);

create policy "orders_insert_authenticated" on public.orders
  for insert with check (auth.uid() = user_id or user_id is null);

create policy "orders_admin_all" on public.orders
  for all using (exists (
    select 1 from public.profiles p
    where p.id = auth.uid() and p.role = 'admin'
  ));

-- ============================================================
-- TRIGGER: updated_at automatique
-- ============================================================
create or replace function public.handle_updated_at()
returns trigger language plpgsql as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create trigger profiles_updated_at before update on public.profiles
  for each row execute procedure public.handle_updated_at();

create trigger products_updated_at before update on public.products
  for each row execute procedure public.handle_updated_at();

create trigger orders_updated_at before update on public.orders
  for each row execute procedure public.handle_updated_at();

-- ============================================================
-- TRIGGER: créer profile automatiquement à l'inscription
-- ============================================================
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
  insert into public.profiles (id, email, full_name)
  values (
    new.id,
    new.email,
    coalesce(new.raw_user_meta_data-&gt;'full_name', '')::text
  );
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

B3. Stripe Integration Standard


// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next'
import Stripe from 'stripe'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { z } from 'zod'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-28.clover'
})

const CheckoutSchema = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive()
  })).min(1),
  successUrl: z.string().url().optional(),
  cancelUrl: z.string().url().optional()
})

export async function POST(request: NextRequest) {
  const cookieStore = cookies()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    { cookies: { get: (name) =&gt; cookieStore.get(name)?.value } }
  )

  try {
    const body = await request.json()
    const { items, successUrl, cancelUrl } = CheckoutSchema.parse(body)

    // Récupérer les produits depuis Supabase
    const productIds = items.map(i =&gt; i.productId)
    const { data: products, error } = await supabase
      .from('products')
      .select('id, name_fr, price, images')
      .in('id', productIds)
      .eq('is_active', true)

    if (error || !products?.length) {
      return NextResponse.json({ error: 'Produits introuvables' }, { status: 404 })
    }

    // Construire les line_items Stripe
    const line_items = items.map(item =&gt; {
      const product = products.find(p =&gt; p.id === item.productId)!
      return {
        price_data: {
          currency: 'ils',
          product_data: {
            name: product.name_fr,
            images: product.images?.slice(0, 1) ?? []
          },
          unit_amount: product.price  // déjà en agorot
        },
        quantity: item.quantity
      }
    })

    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items,
      success_url: successUrl ?? `${process.env.NEXT_PUBLIC_SITE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: cancelUrl ?? `${process.env.NEXT_PUBLIC_SITE_URL}/checkout`,
      payment_method_types: ['card'],
      metadata: { items: JSON.stringify(items) }
    })

    return NextResponse.json({ url: session.url })

  } catch (err) {
    if (err instanceof z.ZodError) {
      return NextResponse.json({ error: 'Données invalides', details: err.errors }, { status: 400 })
    }
    console.error('Stripe checkout error:', err)
    return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
  }
}

// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'
import { Resend } from 'resend'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2026-01-28.clover' })
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
const resend = new Resend(process.env.RESEND_API_KEY!)

export async function POST(request: NextRequest) {
  const body = await request.text()
  const signature = request.headers.get('stripe-signature')!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch {
    return NextResponse.json({ error: 'Signature invalide' }, { status: 400 })
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session

    // 1. Créer commande dans Supabase
    const { data: order } = await supabase
      .from('orders')
      .insert({
        stripe_session_id: session.id,
        stripe_payment_intent_id: session.payment_intent as string,
        amount: session.amount_total,
        currency: session.currency?.toUpperCase() ?? 'ILS',
        status: 'paid',
        shipping_address: {
          name: session.customer_details?.name,
          email: session.customer_details?.email,
        }
      })
      .select()
      .single()

    // 2. Email confirmation via Resend
    if (session.customer_details?.email &amp;&amp; order) {
      await resend.emails.send({
        from: 'DreamNova &lt;noreply@dreamnova.vercel.app&gt;',
        to: session.customer_details.email,
        subject: `Confirmation commande #${order.id.slice(0, 8).toUpperCase()}`,
        html: `&lt;h1&gt;Merci pour votre commande!&lt;/h1&gt;&lt;p&gt;Montant: ${(session.amount_total! / 100).toFixed(2)} ₪&lt;/p&gt;`
      })
    }
  }

  return NextResponse.json({ received: true })
}

B4. Performance Checklist CTO (Lighthouse >90 OBLIGATOIRE)

Cette checklist est intégrée dans le CI/CD. Un score < 90 BLOQUE le deploy.


IMAGES (impact: +15-20 pts)
  [ ] next/image sur TOUTES les images (jamais &lt;img&gt; directe)
  [ ] Format WebP/AVIF avec fallback (next.config.js: formats)
  [ ] sizes= défini sur chaque Image (prevent layout shift)
  [ ] priority= sur l'image hero above-the-fold uniquement
  [ ] lazy loading implicite sur tout le reste (default Next.js)
  [ ] max 800x800px pour produits, optimisé en build
  [ ] SVG pour icons (jamais PNG/JPG pour icons)

FONTS (impact: +5-10 pts)
  [ ] next/font/google ou next/font/local (jamais @import CSS)
  [ ] display: 'swap' sur toutes les fonts
  [ ] Preload uniquement la font primaire
  [ ] Max 2 familles de fonts par projet

CODE SPLITTING (impact: +10-15 pts)
  [ ] dynamic(() =&gt; import(...)) sur composants non-critiques
  [ ] Suspense boundaries avec fallback skeleton
  [ ] Route-based code splitting (automatique App Router)
  [ ] Barrel exports supprimés (import direct)

CACHE &amp; HEADERS (impact: +5-10 pts)
  [ ] Cache-Control: public, max-age=31536000 sur /public/*
  [ ] Revalidation Supabase: next: { revalidate: 3600 }
  [ ] Static rendering sur toutes les pages sans auth
  [ ] generateStaticParams sur pages produits

CSS (impact: +5 pts)
  [ ] Tailwind CSS purge activé (automatique en prod)
  [ ] Variables CSS pour tokens (pas de valeurs hardcodées)
  [ ] No unused CSS classes en prod
  [ ] Critical CSS inliné (automatique Next.js)

CORE WEB VITALS
  [ ] LCP &lt; 2.5s (image hero optimisée + preload)
  [ ] CLS &lt; 0.1 (width/height sur images, skeleton loaders)
  [ ] FID/INP &lt; 100ms (pas de calculs lourds sur main thread)
  [ ] TTFB &lt; 800ms (SSG ou ISR quand possible)

B5. Deployment Checklist Moyen Projet (20 items)


# Checklist à exécuter DANS CET ORDRE avant chaque deploy production

[ ] 01. TypeScript: npx tsc --noEmit → ZERO erreurs
[ ] 02. Build local: npm run build → ZERO warnings critiques
[ ] 03. Tests Playwright: npx playwright test → PASS
[ ] 04. Env vars Vercel: toutes les variables .env.local.example sont sur Vercel
[ ] 05. Supabase migrations: supabase db push (sur branch preview d'abord)
[ ] 06. RLS policies testées: chaque table a au moins 1 policy
[ ] 07. Stripe webhook URL mise à jour sur dashboard Stripe
[ ] 08. Test paiement: 1 commande test avec carte 4242 4242 4242 4242
[ ] 09. Test email: vérifier réception email confirmation (Resend logs)
[ ] 10. Screenshot mobile: iPhone 12 Chrome DevTools
[ ] 11. Screenshot desktop: 1440px
[ ] 12. Lighthouse: score &gt;90 Performance + Accessibility + SEO
[ ] 13. Meta tags: title + description sur chaque page
[ ] 14. OG image: /public/og-image.png (1200×630)
[ ] 15. 404 page: /app/not-found.tsx personnalisée
[ ] 16. Error boundary: /app/error.tsx définie
[ ] 17. Loading states: /app/loading.tsx sur routes dynamiques
[ ] 18. Git tag: git tag v1.0.0-$(date +%Y%m%d) avant deploy
[ ] 19. Deploy: vercel --prod --token=VCP_TOKEN_REDACTED
[ ] 20. Vérification live: tester les 5 pages critiques en production

PARTIE C — Grand Projet (> 1 semaine)

C1. Architecture Multi-Services

Pour DreamOS, DreamNova Platform, et tout projet > 30K ₪/mois potentiel.


┌─────────────────────────────────────────────────────────┐
│                    INTERNET / CDN                        │
│                  Cloudflare (DNS + WAF)                  │
└────────────────────────┬────────────────────────────────┘
                         │
         ┌───────────────┴───────────────┐
         │                               │
┌────────▼────────┐             ┌────────▼────────┐
│  Next.js (Vercel)│             │  Static Assets  │
│  Frontend + API  │             │  (Cloudflare R2) │
│  dreamnova.app   │             │  CDN worldwide  │
└────────┬────────┘             └─────────────────┘
         │ HTTP/REST
┌────────▼────────────────────────────────────────┐
│              Supabase (Database Layer)            │
│   PostgreSQL + Auth + Storage + Realtime          │
│   Row Level Security sur TOUTES les tables        │
└────────┬────────────────────────────────────────┘
         │
┌────────▼────────────────────────────────────────┐
│              VPS IONOS (66.179.240.214)           │
│   Python FastAPI (workers asynchrones)            │
│   - Job queue (BullMQ/n8n)                        │
│   - Batch processing (translations, images)       │
│   - Webhooks handlers                             │
│   - Cron jobs (heartbeat, daily-scan)             │
└─────────────────────────────────────────────────┘
         │
┌────────▼────────────────────────────────────────┐
│              Monitoring Stack                     │
│   Sentry (erreurs) + Upstash Redis (rate limit)  │
│   Telegram alerts (David @nerostats)              │
└─────────────────────────────────────────────────┘

C2. CI/CD Pipeline Complet


# .github/workflows/deploy.yml
name: DreamNova Deploy Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
  TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
  TELEGRAM_CHAT_ID: "7269582214"

jobs:
  # ============================================================
  # JOB 1: TypeScript + Lint
  # ============================================================
  typecheck:
    name: TypeScript &amp; Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: TypeScript check (ZERO errors)
        run: npx tsc --noEmit
      - name: ESLint
        run: npm run lint

  # ============================================================
  # JOB 2: Build
  # ============================================================
  build:
    name: Next.js Build
    needs: typecheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: .next/

  # ============================================================
  # JOB 3: Tests Playwright E2E
  # ============================================================
  e2e:
    name: Playwright E2E Tests
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install chromium --with-deps
      - name: Run E2E tests
        run: npx playwright test
        env:
          BASE_URL: 'http://localhost:3000'
          TEST_STRIPE_KEY: ${{ secrets.STRIPE_TEST_KEY }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

  # ============================================================
  # JOB 4: Lighthouse CI
  # ============================================================
  lighthouse:
    name: Lighthouse Score &gt;90
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci &amp;&amp; npm run build
      - run: npm start &amp;
      - name: Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          urls: |
            http://localhost:3000
            http://localhost:3000/pricing
          budgetPath: .github/lighthouse-budget.json
          uploadArtifacts: true

  # ============================================================
  # JOB 5: Deploy to Vercel (main branch uniquement)
  # ============================================================
  deploy:
    name: Deploy to Production
    needs: [e2e, lighthouse]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' &amp;&amp; github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm install -g vercel
      - name: Deploy to Vercel Production
        run: vercel --prod --token=$VERCEL_TOKEN --yes
      - name: Tag release
        run: |
          git config user.email "ci@dreamnova.app"
          git config user.name "DreamNova CI"
          git tag "v$(date +%Y%m%d-%H%M%S)"
          git push --tags

  # ============================================================
  # JOB 6: Notification Telegram David
  # ============================================================
  notify:
    name: Notify David
    needs: deploy
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Send Telegram notification
        run: |
          STATUS="${{ needs.deploy.result }}"
          if [ "$STATUS" = "success" ]; then
            MSG="[DEPLOY OK] $(git log -1 --pretty=format:\'%s\') deploye en production"
          else
            MSG="[DEPLOY FAIL] Echec deploy: ${{ github.sha }}"
          fi
          curl -s -X POST \
            "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
            -d "chat_id=${TELEGRAM_CHAT_ID}&amp;text=${MSG}"

C3. Monitoring & Auto-Repair


# ~/scripts/monitoring/sentry_auto_repair.py
"""
Sentry Webhook -&gt; Gemini analyse -&gt; PR GitHub automatique
Principe: erreur detectee -&gt; analyse automatique -&gt; suggestion de fix -&gt; PR pour David
"""
import os
import json
import hashlib
import requests
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import google.generativeai as genai

app = FastAPI()
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
REPO = "CodeNoLimits/dreamnova-boilerplate"
GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts")
TELEGRAM_CHAT_ID = "7269582214"
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]


def notify_david(message: str) -&gt; None:
    requests.post(
        f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
        json={"chat_id": TELEGRAM_CHAT_ID, "text": message},
        timeout=5
    )


def analyze_error_with_gemini(error_data: dict) -&gt; str:
    """Gemini analyse l'erreur Sentry et propose un fix."""
    genai.configure(api_key=GEMINI_KEY)
    model = genai.GenerativeModel("gemini-2.0-flash")

    prompt = f"""
    Erreur Sentry en production (Next.js/TypeScript/Supabase):

    Message: {error_data.get('message', '')}
    Type: {error_data.get('type', '')}
    Stack trace:
    {error_data.get('stacktrace', '')}

    Contexte: App Next.js avec Supabase et Stripe.

    Fournis:
    1. Cause probable (1 phrase)
    2. Fix exact (code TypeScript ready-to-paste)
    3. Fichier a modifier
    4. Test a faire pour valider le fix
    """
    response = model.generate_content(prompt)
    return response.text


def create_github_pr(fix_description: str, error_id: str) -&gt; str:
    """Crée une PR GitHub avec le fix suggéré."""
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json"
    }

    # Créer une branche
    branch_name = f"fix/sentry-{error_id[:8]}"
    base_sha = requests.get(
        f"https://api.github.com/repos/{REPO}/git/refs/heads/main",
        headers=headers
    ).json()["object"]["sha"]

    requests.post(
        f"https://api.github.com/repos/{REPO}/git/refs",
        headers=headers,
        json={"ref": f"refs/heads/{branch_name}", "sha": base_sha}
    )

    # Créer la PR
    pr = requests.post(
        f"https://api.github.com/repos/{REPO}/pulls",
        headers=headers,
        json={
            "title": f"fix: auto-repair Sentry #{error_id[:8]}",
            "body": f"## Fix automatique\n\n{fix_description}\n\n_Généré automatiquement par DreamNova Auto-Repair_",
            "head": branch_name,
            "base": "main"
        }
    ).json()

    return pr.get("html_url", "")


@app.post("/webhook/sentry")
async def sentry_webhook(request: Request):
    """Handler webhook Sentry."""
    payload = await request.json()
    event = payload.get("data", {}).get("event", {})

    if not event:
        raise HTTPException(status_code=400, detail="Payload invalide")

    error_id = hashlib.md5(
        f"{event.get('type', '')}{event.get('message', '')}".encode()
    ).hexdigest()

    # Analyse Gemini
    fix = analyze_error_with_gemini({
        "message": event.get("message", ""),
        "type": event.get("type", ""),
        "stacktrace": str(event.get("exception", {}).get("values", [{}])[0].get("stacktrace", ""))
    })

    # Créer PR
    pr_url = create_github_pr(fix, error_id)

    # Notifier David
    notify_david(
        f"[AUTO-REPAIR] Erreur Sentry detectee\n"
        f"Type: {event.get('type', '')}\n"
        f"PR creee: {pr_url}\n"
        f"Valider sur GitHub avant merge."
    )

    return JSONResponse({"status": "PR created", "url": pr_url})

C4. Database Migration Strategy


#!/bin/bash
# ~/scripts/db/safe_migration.sh
# Usage: ./safe_migration.sh 002_add_variants.sql "Add product variants table"

MIGRATION_FILE="$1"
DESCRIPTION="$2"
SUPABASE_PROJECT="rhwthxmphohqdawglhfde"  # Adapter par projet

if [ -z "$MIGRATION_FILE" ] || [ -z "$DESCRIPTION" ]; then
  echo "Usage: ./safe_migration.sh migration.sql 'Description'"
  exit 1
fi

echo "[1/5] Backup avant migration..."
supabase db dump --project-ref "$SUPABASE_PROJECT" &gt; "/tmp/backup_$(date +%Y%m%d_%H%M%S).sql"
echo "Backup OK: /tmp/backup_$(date +%Y%m%d_%H%M%S).sql"

echo "[2/5] Test sur branch Supabase preview..."
supabase db push --dry-run

echo "[3/5] Application migration..."
supabase db push

echo "[4/5] Regeneration types TypeScript..."
npx supabase gen types typescript --project-id "$SUPABASE_PROJECT" &gt; lib/database.types.ts

echo "[5/5] Commit migration..."
git add supabase/migrations/ lib/database.types.ts
git commit -m "chore(db): $DESCRIPTION"

echo "Migration terminee. Tester les API routes critiques maintenant."

Règles absolues migrations :

  1. Jamais supprimer de colonne en phase 1 — marquer comme deprecated en commentaire
  2. Toujours additive — ajouter colonnes nullable ou avec DEFAULT
  3. Test sur preview Supabase avant de toucher la production
  4. Backup automatique avant chaque migration (script ci-dessus)
  5. Types TypeScript regénérés immédiatement après toute migration

C5. Security Grand Projet


// middleware.ts — Rate limiting + session Supabase
import { NextRequest, NextResponse } from 'next'
import { createServerClient } from '@supabase/ssr'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(100, '1 m'),  // 100 req/min par IP
  analytics: true
})

export async function middleware(request: NextRequest) {
  // 1. Rate limiting sur routes API
  if (request.nextUrl.pathname.startsWith('/api/')) {
    const ip = request.ip ?? '127.0.0.1'
    const { success, limit, remaining } = await ratelimit.limit(ip)

    if (!success) {
      return NextResponse.json(
        { error: 'Too many requests' },
        { status: 429, headers: { 'X-RateLimit-Limit': String(limit), 'X-RateLimit-Remaining': String(remaining) } }
      )
    }
  }

  // 2. Session Supabase refresh automatique
  const response = NextResponse.next()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { cookies: { get: (name) =&gt; request.cookies.get(name)?.value, set: (name, value, options) =&gt; { response.cookies.set({ name, value, ...options }) } } }
  )
  await supabase.auth.getUser()

  // 3. Protéger routes dashboard
  if (request.nextUrl.pathname.startsWith('/(dashboard)')) {
    const { data: { user } } = await supabase.auth.getUser()
    if (!user) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return response
}

export const config = {
  matcher: ['/api/:path*', '/(dashboard)/:path*']
}

# .github/hooks/pre-commit — Bloquer secrets dans les commits
#!/bin/bash
# Installer: cp .github/hooks/pre-commit .git/hooks/pre-commit &amp;&amp; chmod +x .git/hooks/pre-commit

SECRETS_PATTERN="sk_live_|whsec_|eyJ|AIzaSy|vcp_|re_[A-Za-z0-9]"

if git diff --cached --name-only | xargs grep -l -E "$SECRETS_PATTERN" 2&gt;/dev/null; then
  echo "[ERREUR] Secrets potentiels detectes dans les fichiers staged!"
  echo "Verifier: .env, .env.local, config files"
  echo "Utiliser: git reset HEAD &lt;file&gt; pour unstager"
  exit 1
fi
exit 0

RÈGLES D'OR CTO (10 Non-Négociables)

Ces règles sont gravées. Elles ne sont pas des suggestions. Chaque violation a coûté du temps, de l'argent, ou un client DreamNova.

1. Screenshot AVANT → Gemini Vision → Modification → Screenshot APRÈS

Pourquoi : Le site Keren a été détruit le 12 fév 2026 en travaillant sans validation visuelle.

Comment : openclaw browser screenshot avant + après. Gemini Vision compare. Si degradation → rollback immédiat.

2. TypeScript strict : ZERO any, ZERO @ts-ignore

Chaque any est une bombe à retardement. Exception unique : interface externe mal typée → créer un type wrapper documenté avec // REASON:.

3. Backup AVANT toute migration DB


supabase db dump --project-ref PROJECT_ID &gt; backup_$(date +%Y%m%d_%H%M%S).sql

Pas de backup = pas de migration. La règle est absolue.

4. Git tag AVANT chaque deploy production


git tag "v$(date +%Y%m%d-%H%M%S)" &amp;&amp; git push --tags
vercel --prod --token=VCP_TOKEN_REDACTED

Rollback en 30 secondes : vercel rollback --token=...

5. Docker sandbox pour tout agent avec accès shell

OpenClaw, Gemini Junior, tout agent autonome → jamais d'accès direct au filesystem de production. Sandbox Docker avec volume /output uniquement.

6. Lighthouse >90 BLOQUE le deploy

Intégré dans .github/workflows/deploy.yml. Pas de bypass. Score 89 = fix avant deploy, pas d'exception.

7. Variables d'env : JAMAIS hardcodées, JAMAIS commitées


# Vérification avant commit (hook pre-commit)
grep -r "sk_live_\|whsec_\|AIzaSy" src/ &amp;&amp; echo "SECRET DETECTED" &amp;&amp; exit 1

Le token Vercel permanent dans ce document est accepté uniquement en doc interne.

8. Test paiement Stripe mode Test AVANT prod

Carte test : 4242 4242 4242 4242, date future, CVC quelconque.

Tester : succès, échec, remboursement. Si un des 3 échoue → pas de deploy.

9. Mobile test iPhone 12 Chrome DevTools AVANT livraison client

Résolution : 390×844. Vérifier : navigation, CTA visibles, formulaires cliquables, images chargées, pas de scroll horizontal.

10. Rétro-engineering : chaque nouveau problème résolu → script réutilisable


Problème résolu → ~/scripts/workflows/NOM_DU_PROBLEME.py

Répertoire ~/scripts/workflows/ = mémoire technique DreamNova. Auditer mensuellement.


APPENDICE — Commandes Rapides CTO


# Setup nouveau projet
dn-init nom-projet

# Deploy production (token permanent)
vercel --prod --token=VCP_TOKEN_REDACTED

# Rollback deploy
vercel rollback --token=VCP_TOKEN_REDACTED

# Migration DB avec backup
~/scripts/db/safe_migration.sh supabase/migrations/002_feature.sql "Description"

# Regenerer types Supabase
npx supabase gen types typescript --project-id PROJECT_ID &gt; lib/database.types.ts

# Lighthouse local
npx lighthouse http://localhost:3000 --output=html --output-path=/tmp/report.html

# Screenshot production via OpenClaw
openclaw browser screenshot --url https://projet.vercel.app --output /tmp/check.png

# Check TypeScript
npx tsc --noEmit

# Pipeline voice-&gt;image
python3 ~/scripts/workflows/voice_to_image_pipeline.py message.ogg photo.jpg

# Alert Telegram David
openclaw message send --channel telegram --target 7269582214 --message "Texte"

Document maintenu par DreamNova CTO — David Amor, Jérusalem

Dernière mise à jour: 2026-02-19

Code: [NOVA:CTO-PMG-V1-20260219]