# Architecture Technique
## Application "Livre dont vous êtes le héros"

**Version:** 1.0  
**Date:** 31 Décembre 2025  
**Statut:** Proposition d'architecture MVP

---

## 1. Vue d'ensemble

### 1.1 Architecture globale
Architecture web classique **client-serveur** avec API REST.

```
┌─────────────────┐
│   Frontend      │
│   (React)       │
│   Static files  │
└────────┬────────┘
         │ HTTP/REST
         │
┌────────▼────────┐      ┌──────────────┐
│   Backend       │─────►│   OpenAI     │
│   (Flask)       │      │   API        │
│   + Passenger   │      └──────────────┘
└────────┬────────┘
         │
┌────────▼────────┐
│   MySQL         │
│   (O2Switch)    │
└─────────────────┘
```

### 1.2 Principes architecturaux
- **Séparation Frontend/Backend** : API RESTful comme contrat
- **Stateless API** : authentification par tokens JWT
- **WSGI natif** : Flask avec Passenger sur O2Switch (hébergement mutualisé)
- **Simplicité** : stack adaptée à l'environnement d'hébergement
- **Sécurité** : validation des inputs, rate limiting, modération

---

## 2. Stack technique

### 2.1 Frontend

**Framework :** React 18+ avec TypeScript
- **Motivation :** Écosystème riche, performances, réactivité

**Bibliothèques principales :**
- **Routing :** React Router v6
- **State Management :** React Context API + hooks (ou Zustand si besoin)
- **HTTP Client :** Axios
- **Forms :** React Hook Form + Zod (validation)
- **Styling :** CSS Modules + Sass ou Tailwind CSS (thème personnalisé)
- **UI Components :** Composants custom (ambiance bibliothèque)
- **Animations :** Framer Motion (transitions douces)
- **Markdown :** React Markdown (affichage texte riche si besoin)

**Outils de développement :**
- Vite (build tool moderne et rapide)
- ESLint + Prettier
- TypeScript strict mode

### 2.2 Backend

**Framework :** Flask 3.0+ (Python 3.12+)
- **Motivation :** WSGI natif (compatible Passenger), mature, simple à déployer sur O2Switch

**Bibliothèques principales :**
- **ORM :** SQLAlchemy 2.0
- **Flask Extensions :**
  - **Flask-SQLAlchemy** : intégration ORM
  - **Flask-Migrate** : migrations de base de données (Alembic)
  - **Flask-JWT-Extended** : authentification JWT
  - **Flask-CORS** : gestion CORS
  - **Flask-Limiter** : rate limiting
- **Validation :** Pydantic v2 (standalone, pour validation des données)
- **Auth :** passlib (hashing bcrypt)
- **OpenAI :** openai SDK officiel
- **MySQL Driver :** PyMySQL ou mysqlclient
- **Email :** Flask-Mail ou SendGrid

**Structure du projet :**
```
/home/soje3943/story.cosmail.fr/
├── app.py                      # Point d'entrée Flask (application)
├── passenger_wsgi.py           # Point d'entrée Passenger
├── .htaccess                   # Configuration Apache/Passenger
├── config.py                   # Configuration (env vars)
├── requirements.txt            # Dépendances Python
├── .env                        # Variables d'environnement (secret)
├── .env.example                # Template variables
│
├── models/                     # Modèles SQLAlchemy
│   ├── __init__.py
│   ├── user.py
│   ├── story.py
│   ├── story_turn.py
│   ├── lorebook.py
│   ├── story_like.py
│   └── story_favorite.py
│
├── routes/                     # Blueprints Flask (routes API)
│   ├── __init__.py
│   ├── auth.py
│   ├── users.py
│   ├── stories.py
│   ├── gameplay.py
│   └── gallery.py
│
├── services/                   # Logique métier
│   ├── __init__.py
│   ├── auth_service.py
│   ├── story_service.py
│   ├── publication_service.py
│   ├── ai_service.py          # OpenAI integration
│   ├── lorebook_service.py
│   ├── moderation_service.py
│   └── image_service.py       # DALL-E
│
├── schemas/                    # Schémas Pydantic (validation)
│   ├── __init__.py
│   ├── user.py
│   ├── story.py
│   └── turn.py
│
├── utils/                      # Utilitaires
│   ├── __init__.py
│   ├── email.py
│   ├── security.py
│   ├── prompts.py             # Templates de prompts OpenAI
│   └── validators.py
│
├── migrations/                 # Migrations Alembic (Flask-Migrate)
│   ├── versions/
│   └── alembic.ini
│
├── tests/                      # Tests unitaires et intégration
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_auth.py
│   ├── test_stories.py
│   └── test_gameplay.py
│
├── static/                     # Fichiers statiques (si frontend servi par Flask)
│   └── index.html
│
├── docs/                       # Documentation du projet
│   ├── README.md
│   ├── specifications_fonctionnelles.md
│   ├── use_cases.md
│   ├── architecture.md
│   └── ui_ux_specs.md
│
└── tmp/                        # Fichiers temporaires
    └── restart.txt             # Touch pour redémarrer Passenger
```

### 2.3 Base de données

**SGBD :** MySQL 8.0+ (disponible via CPanel O2Switch)
- **Motivation :** Disponible nativement sur O2Switch, support JSON (5.7+), robuste, performant

**Driver Python :** PyMySQL (pur Python) ou mysqlclient
- **Recommandation :** PyMySQL pour simplicité (pas de compilation C)
- **URI de connexion :** `mysql+pymysql://user:password@localhost/dbname`

**Fonctionnalités utilisées :**
- Type **JSON** : pour LoreBook, choix disponibles, contexte (MySQL 5.7+)
- Type **CHAR(36)** : pour les UUIDs (stockés en string)
- **Full-text search** : pour recherche dans la galerie (FULLTEXT indexes)
- **Transactions** : pour cohérence des données

### 2.4 Services externes

**OpenAI API :**
- **Modèle de chat :** GPT-4 Turbo (ou GPT-4o)
- **Modèle d'images :** DALL-E 3
- **Modération :** OpenAI Moderation API

**Email :**
- SMTP O2Switch (inclus avec l'hébergement)
- Alternative : SendGrid API

**Hébergement :**
- **Backend + Database :** O2Switch (hébergement mutualisé)
  - Python 3.12.11 via "Setup Python App" (CPanel)
  - Passenger WSGI pour servir Flask
  - MySQL 8.0 via CPanel
  - Domaine : https://story.cosmail.fr/
- **Frontend :** 
  - Option 1 : Servi depuis O2Switch (dossier `/static`)
  - Option 2 : Vercel/Netlify (gratuit, recommandé pour performance)

---

## 3. Modèle de données

### 3.1 Schéma de base de données (MySQL)

**Note :** Les UUIDs sont stockés comme `CHAR(36)` et générés par Python (via `uuid.uuid4()`).

#### Table : `users`
```sql
CREATE TABLE users (
    id CHAR(36) PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    username VARCHAR(50) UNIQUE NOT NULL,
    hashed_password VARCHAR(255) NOT NULL,
    is_active BOOLEAN DEFAULT FALSE,
    is_verified BOOLEAN DEFAULT FALSE,
    verification_token VARCHAR(255),
    reset_token VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

#### Table : `stories`
```sql
CREATE TABLE stories (
    id CHAR(36) PRIMARY KEY,
    user_id CHAR(36) NOT NULL,
    title VARCHAR(255) NOT NULL,
    
    -- Configuration initiale
    genre VARCHAR(100),
    epoch VARCHAR(100),
    reality_level INT DEFAULT 0, -- 0-100
    
    -- Perspective narrative
    narrative_perspective VARCHAR(10) DEFAULT 'second', -- 'second' (Tu) ou 'third' (Il/Elle)
    protagonist_name VARCHAR(100), -- Nom du protagoniste (si 3ème personne, NULL si non défini)
    protagonist_gender VARCHAR(10), -- 'he' ou 'she' (si 3ème personne)
    
    -- Contexte de création
    context_answers JSON, -- Questions/réponses initiales
    
    -- Squelette narratif
    narrative_skeleton JSON,
    
    -- Couverture
    cover_image_url TEXT,
    
    -- Mode de jeu
    game_mode VARCHAR(20) DEFAULT 'choices', -- 'choices' ou 'free'
    
    -- Statut
    status VARCHAR(20) DEFAULT 'in_progress', -- 'in_progress', 'completed', 'abandoned'
    is_published BOOLEAN DEFAULT FALSE,
    publication_status VARCHAR(20) DEFAULT 'ongoing', -- 'ongoing' ou 'finished' (pour histoires publiées)
    publication_description TEXT,
    published_turn_count INT DEFAULT 0, -- Nombre de passages publiés et figés
    
    -- Statistiques
    turn_count INT DEFAULT 0,
    views_count INT DEFAULT 0,
    likes_count INT DEFAULT 0,
    
    -- Timestamps
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    last_played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    published_at TIMESTAMP NULL, -- Date de première publication
    last_chapter_published_at TIMESTAMP NULL, -- Dernier chapitre publié
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_stories_user_id (user_id),
    INDEX idx_stories_published (is_published),
    INDEX idx_stories_genre (genre),
    INDEX idx_stories_status (status),
    INDEX idx_stories_pub_status (publication_status, is_published)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

#### Table : `story_turns`
```sql
CREATE TABLE story_turns (
    id CHAR(36) PRIMARY KEY,
    story_id CHAR(36) NOT NULL,
    turn_number INT NOT NULL,
    
    -- Contenu narratif généré par IA
    narrative_text TEXT NOT NULL,
    
    -- Action du joueur
    player_action TEXT, -- Le choix sélectionné ou texte libre
    action_type VARCHAR(20), -- 'choice' ou 'free_text'
    is_technical BOOLEAN DEFAULT FALSE, -- TRUE si message OFF (instruction technique)
    
    -- Choix proposés (si mode choices)
    available_choices JSON, -- ["Choix A", "Choix B", "Choix C"]
    selected_choice_index INT, -- Index du choix sélectionné (0-3), NULL si texte libre
    
    -- Publication
    is_published BOOLEAN DEFAULT FALSE, -- TRUE si ce passage fait partie de la version publique figée
    
    -- Métadonnées
    tokens_used INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_story_turn (story_id, turn_number),
    FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
    INDEX idx_turns_story_id (story_id),
    INDEX idx_turns_technical (is_technical),
    INDEX idx_turns_published (is_published)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

#### Table : `lorebooks`
```sql
CREATE TABLE lorebooks (
    id CHAR(36) PRIMARY KEY,
    story_id CHAR(36) UNIQUE NOT NULL,
    
    -- Contenu structuré
    characters JSON, -- [{"name": "...", "traits": [...], ...}]
    locations JSON,
    items JSON,
    key_events JSON,
    protagonist_state JSON,
    
    -- Métadonnées
    last_updated_turn INT DEFAULT 0,
    version INT DEFAULT 1,
    
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
    UNIQUE KEY unique_story_lorebook (story_id),
    INDEX idx_lorebooks_story_id (story_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

#### Table : `story_likes`
```sql
CREATE TABLE story_likes (
    id CHAR(36) PRIMARY KEY,
    user_id CHAR(36) NOT NULL,
    story_id CHAR(36) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_user_story_like (user_id, story_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
    INDEX idx_likes_user_id (user_id),
    INDEX idx_likes_story_id (story_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

#### Table : `story_favorites`
```sql
CREATE TABLE story_favorites (
    id CHAR(36) PRIMARY KEY,
    user_id CHAR(36) NOT NULL,
    story_id CHAR(36) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_user_story_favorite (user_id, story_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
    INDEX idx_favorites_user_id (user_id),
    INDEX idx_favorites_story_id (story_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

### 3.2 Relations

```
users (1) ──── (N) stories
stories (1) ──── (1) lorebooks
stories (1) ──── (N) story_turns
stories (N) ──── (N) users (via story_likes)
stories (N) ──── (N) users (via story_favorites)
```

---

## 4. API REST - Endpoints

### 4.1 Authentification

```
POST   /api/auth/register          # Inscription
POST   /api/auth/login             # Connexion (retourne JWT)
POST   /api/auth/verify-email      # Vérification email
POST   /api/auth/forgot-password   # Demande reset password
POST   /api/auth/reset-password    # Reset password avec token
GET    /api/auth/me                # Info utilisateur connecté
```

### 4.2 Utilisateurs

```
GET    /api/users/me               # Profil de l'utilisateur connecté
PATCH  /api/users/me               # Modifier profil
GET    /api/users/{id}             # Profil public d'un utilisateur
```

### 4.3 Histoires

```
POST   /api/stories                        # Créer une nouvelle histoire
GET    /api/stories                        # Liste des histoires de l'utilisateur
GET    /api/stories/{id}                   # Détails d'une histoire
PATCH  /api/stories/{id}                   # Modifier une histoire (titre, etc.)
DELETE /api/stories/{id}                   # Supprimer une histoire
POST   /api/stories/{id}/publish           # Publier une histoire (progressive)
PATCH  /api/stories/{id}/mark-finished    # Marquer histoire publiée comme terminée
```

### 4.4 Gameplay

```
POST   /api/gameplay/init-story           # Initialiser histoire (skeleton, cover)
POST   /api/gameplay/start-story/{id}     # Démarrer narration (1er passage)
POST   /api/gameplay/action               # Envoyer une action (choix, texte, ou OFF)
                                           # Body: { action, is_technical: bool }
GET    /api/gameplay/story/{id}/turns     # Historique des tours (version auteur complète)
GET    /api/gameplay/story/{id}/context   # Contexte pour reprendre
```

### 4.5 LoreBook

```
GET    /api/lorebooks/{story_id}         # Obtenir le lorebook
POST   /api/lorebooks/{story_id}/update  # Forcer mise à jour (admin/debug)
```

### 4.6 Galerie publique

```
GET    /api/gallery/stories                       # Liste histoires publiées (filtres, tri, statut)
                                                   # Query params: genre, epoch, status (ongoing/finished)
GET    /api/gallery/stories/{id}                  # Détails histoire publiée
GET    /api/gallery/stories/{id}/read             # Version nettoyée pour lecture
                                                   # Retourne uniquement passages narratifs + actions,
                                                   # SANS messages OFF, SANS choix non retenus
POST   /api/gallery/stories/{id}/like             # Liker une histoire
DELETE /api/gallery/stories/{id}/like             # Retirer like
POST   /api/gallery/stories/{id}/favorite         # Ajouter aux favoris
POST   /api/gallery/stories/{id}/follow           # Suivre histoire (notifications) - futur
GET    /api/gallery/search                        # Recherche par critères
```

### 4.7 Administration (futur)

```
GET    /api/admin/moderation/reports     # Signalements
POST   /api/admin/moderation/review      # Valider/rejeter
```

---

## 5. Publication progressive et version nettoyée

### 5.1 Concept de publication progressive

Les histoires peuvent être publiées **même si elles ne sont pas terminées**, permettant une publication "chapitre par chapitre" comme une série à épisodes.

**Fonctionnement :**
1. L'auteur publie son histoire (statut : `ongoing`)
2. Tous les passages actuels sont marqués comme `is_published = TRUE` (figés)
3. L'auteur continue d'écrire : les nouveaux passages sont automatiquement ajoutés à la version publique
4. Les lecteurs peuvent suivre l'histoire en cours d'écriture
5. L'auteur peut marquer l'histoire comme `finished` quand elle est terminée

**Règles :**
- Les passages publiés (`is_published = TRUE`) **ne peuvent plus être modifiés**
- Les messages OFF sont **toujours invisibles** dans la version publique
- Seuls les nouveaux passages (non encore publiés) peuvent être modifiés

### 5.2 Génération de la version nettoyée

**Objectif :** Les lecteurs voient une version **purement narrative**, sans les artefacts de jeu (choix, interface, messages techniques).

#### Algorithme de nettoyage

**Fichier :** `services/story_service.py` → `generate_clean_version(story_id)`

```python
def generate_clean_version(story_id: UUID) -> List[CleanPassage]:
    """
    Génère la version nettoyée d'une histoire pour lecture publique.
    
    Filtre :
    - Messages OFF (is_technical = TRUE)
    - Choix non sélectionnés
    - Interface de jeu
    
    Retourne :
    - Liste de passages narratifs fluides
    """
    story = get_story(story_id)
    turns = get_all_published_turns(story_id)  # Uniquement is_published = TRUE
    clean_passages = []
    
    for turn in turns:
        # Ignorer les messages OFF
        if turn.is_technical:
            continue
        
        # Construire le passage nettoyé
        passage = {
            "narrative": turn.narrative_text,  # Texte IA
            "action": None
        }
        
        # Ajouter l'action si narrative (pas OFF)
        if turn.player_action and not turn.is_technical:
            if turn.action_type == 'choice' and turn.selected_choice_index is not None:
                # Mode choix : récupérer le choix sélectionné
                selected = turn.available_choices[turn.selected_choice_index]
                
                # Adapter selon la perspective
                action_text = format_action_for_perspective(
                    selected, 
                    story.narrative_perspective,
                    story.protagonist_name,
                    story.protagonist_gender
                )
                passage["action"] = action_text
                
            elif turn.action_type == 'free_text':
                # Mode libre : l'action textuelle (déjà dans la bonne perspective)
                passage["action"] = turn.player_action
        
        clean_passages.append(passage)
    
    return clean_passages


def format_action_for_perspective(
    action: str, 
    perspective: str, 
    name: Optional[str], 
    gender: Optional[str]
) -> str:
    """
    Formate une action selon la perspective narrative.
    
    Args:
        action: Texte de l'action (ex: "Entrer prudemment dans le château")
        perspective: 'second' ou 'third'
        name: Nom du protagoniste (si 3ème personne avec nom)
        gender: 'he' ou 'she' (si 3ème personne)
    
    Returns:
        Action formatée pour intégration narrative
    """
    if perspective == 'second':
        # 2ème personne : "Tu décides de..."
        return f"Tu décides de {action.lower()}"
    
    else:  # third person
        pronoun = "Elle" if gender == 'she' else "Il"
        
        if name:
            # Alternance nom/pronom pour fluidité
            # (Logique simple : utiliser le nom)
            return f"{name} décide de {action.lower()}"
        else:
            # Uniquement le pronom
            return f"{pronoun} décide de {action.lower()}"
```

#### Exemple de transformation

**Base de données (tours bruts) - Histoire en 2ème personne "Tu" :**
```
Story config:
  narrative_perspective: "second"
  protagonist_name: NULL
  protagonist_gender: NULL

Turn 1:
  narrative_text: "Tu te tiens devant les portes du château..."
  available_choices: ["Entrer prudemment", "Contourner", "Examiner", "Utiliser le médaillon"]
  player_action: "Utiliser le médaillon"
  selected_choice_index: 3
  is_technical: FALSE

Turn 2 (message OFF):
  narrative_text: NULL (pas de génération IA pour ce tour)
  player_action: "[OFF] Fais en sorte que le médaillon révèle un secret"
  is_technical: TRUE

Turn 3:
  narrative_text: "Le médaillon émet une lueur dorée. Les portes s'ouvrent..."
  player_action: NULL (suite directe après message OFF)
  is_technical: FALSE
```

**Version nettoyée (2ème personne) :**
```json
[
  {
    "chapter": 1,
    "text": "Tu te tiens devant les portes du château... Tu décides d'utiliser le médaillon. Le médaillon émet une lueur dorée. Les portes s'ouvrent mystérieusement..."
  }
]
```

---

**Base de données (tours bruts) - Histoire en 3ème personne avec nom "Aria" (Elle) :**
```
Story config:
  narrative_perspective: "third"
  protagonist_name: "Aria"
  protagonist_gender: "she"

Turn 1:
  narrative_text: "Aria se tient devant les portes du château..."
  available_choices: ["Aria entre prudemment", "Elle contourne", "Aria examine", "Elle utilise le médaillon"]
  player_action: "Elle utilise le médaillon"
  selected_choice_index: 3
  is_technical: FALSE

Turn 2 (message OFF):
  narrative_text: NULL
  player_action: "[OFF] Fais en sorte que le médaillon révèle un secret"
  is_technical: TRUE

Turn 3:
  narrative_text: "Le médaillon émet une lueur dorée. Les portes s'ouvrent..."
  player_action: NULL
  is_technical: FALSE
```

**Version nettoyée (3ème personne avec nom) :**
```json
[
  {
    "chapter": 1,
    "text": "Aria se tient devant les portes du château... Aria décide d'utiliser le médaillon. Le médaillon émet une lueur dorée. Les portes s'ouvrent mystérieusement..."
  }
]
```

**Remarques :**
- Les 3 tours sont condensés en un seul passage fluide
- Les choix A/B/C non retenus sont invisibles
- Le message OFF est complètement omis
- L'action retenue est intégrée naturellement au récit
- **La perspective narrative est préservée** (Tu vs Nom/Pronom)

### 5.3 Service de publication

**Fichier :** `services/publication_service.py`

```python
class PublicationService:
    
    def publish_story(self, story_id: UUID, status: str = "ongoing") -> None:
        """
        Publie une histoire (initiale ou continuation).
        
        Args:
            story_id: ID de l'histoire
            status: "ongoing" ou "finished"
        """
        story = get_story(story_id)
        
        if not story.is_published:
            # Première publication
            story.is_published = True
            story.published_at = datetime.now()
            story.publication_status = status
            
            # Marquer tous les turns actuels comme publiés
            mark_all_turns_as_published(story_id)
            story.published_turn_count = story.turn_count
        
        else:
            # Histoire déjà publiée : publier les nouveaux chapitres
            unpublished_turns = get_unpublished_turns(story_id)
            for turn in unpublished_turns:
                turn.is_published = True
            
            story.published_turn_count = story.turn_count
            story.last_chapter_published_at = datetime.now()
        
        # Générer/mettre à jour la version nettoyée
        generate_clean_version(story_id)
        
        save(story)
    
    def mark_as_finished(self, story_id: UUID) -> None:
        """Marque une histoire publiée comme terminée."""
        story = get_story(story_id)
        story.publication_status = "finished"
        save(story)
```

### 5.4 Continuation après publication

Lorsqu'un auteur continue d'écrire une histoire publiée :

1. **Nouveau passage créé** → `is_published = FALSE` (pas encore publié)
2. **Automatiquement publié** dès la sauvegarde du tour
3. Mise à jour de `last_chapter_published_at`
4. La version nettoyée est régénérée automatiquement

**Workflow backend :**
```python
def save_new_turn(story_id, turn_data):
    story = get_story(story_id)
    turn = create_turn(story_id, turn_data)
    
    # Si l'histoire est publiée, publier automatiquement le nouveau tour
    if story.is_published and story.publication_status == "ongoing":
        turn.is_published = True
        story.last_chapter_published_at = datetime.now()
        
        # Régénérer version nettoyée
        generate_clean_version(story_id)
    
    save(turn)
    save(story)
```

---

## 6. Service IA - OpenAI Integration

### 5.1 Architecture du service AI

**Fichier :** `services/ai_service.py`

**Responsabilités :**
1. Génération du squelette narratif
2. Génération de passages narratifs
3. Génération de choix multiples
4. Mise à jour automatique du LoreBook
5. Génération de couvertures (DALL-E)
6. Modération du contenu

### 5.2 Prompts système

#### Prompt : Génération du squelette

```python
SKELETON_PROMPT = """
Tu es un auteur de fiction expérimenté. Crée un squelette narratif pour une histoire interactive.

Paramètres :
- Genre : {genre}
- Époque : {epoch}
- Niveau de réalité : {reality_level}/100
- Contexte : {context_answers}

Structure à générer (format JSON) :
{
  "act_1": {
    "description": "Situation initiale et déclencheur",
    "key_points": ["point 1", "point 2"],
    "trigger": "événement déclencheur"
  },
  "act_2": {
    "description": "Développement et obstacles",
    "key_points": ["point 1", "point 2", "point 3"],
    "twists": ["retournement potentiel 1", "retournement potentiel 2"]
  },
  "act_3": {
    "description": "Climax et résolution",
    "key_points": ["point 1", "point 2"],
    "climax": "description du climax"
  },
  "themes": ["thème 1", "thème 2"],
  "tone": "description du ton général"
}

Important : reste flexible, ce squelette guidera l'histoire mais pourra s'adapter aux choix du joueur.
"""
```

#### Prompt : Génération de passage narratif

```python
NARRATIVE_PROMPT = """
Tu es un narrateur immersif pour une histoire interactive.

## Contexte de l'histoire
Genre : {genre}
Époque : {epoch}
Squelette : {narrative_skeleton}

## Perspective narrative
{narrative_perspective_instruction}

## LoreBook actuel
{lorebook}

## Historique récent
{recent_history}

## Dernière action du joueur
{player_action}

{technical_instruction}

## Instructions
1. Continue l'histoire de manière cohérente
2. Prends en compte l'action du joueur
3. {tech_instruction_note}
4. Respecte le squelette narratif (acte {current_act})
5. **IMPORTANT : Respecte strictement la perspective narrative définie ci-dessus**
6. Garde un style immersif et littéraire
7. Longueur : 150-300 mots
8. Termine sur une situation appelant à l'action
9. Reste approprié (pas de contenu violent/sexuel explicite)

Génère maintenant le passage narratif.
"""
```

**Gestion de la perspective et des messages OFF :**

```python
def build_narrative_prompt(story, player_action, is_technical=False):
    """Construit le prompt en tenant compte de la perspective et des messages techniques."""
    
    # Construire l'instruction de perspective
    if story.narrative_perspective == 'second':
        perspective_instruction = """
**Narration en 2ème personne ("Tu")**
- Utilise "Tu" pour désigner le protagoniste
- Exemples : "Tu te tiens...", "Tu décides de...", "Ta main se crispe..."
        """
    else:  # third person
        pronoun = "Elle" if story.protagonist_gender == 'she' else "Il"
        pronoun_possessive = "sa/son" if story.protagonist_gender == 'she' else "son/sa"
        
        if story.protagonist_name:
            perspective_instruction = f"""
**Narration en 3ème personne avec nom du protagoniste**
- Protagoniste : {story.protagonist_name} ({pronoun})
- Utilise le nom "{story.protagonist_name}" et le pronom "{pronoun}"
- Exemples : "{story.protagonist_name} se tient...", "{pronoun} décide de...", "{pronoun_possessive} main se crispe..."
            """
        else:
            perspective_instruction = f"""
**Narration en 3ème personne sans nom**
- Protagoniste : {pronoun}
- Utilise uniquement le pronom "{pronoun}" (pas de nom propre)
- Exemples : "{pronoun} se tient...", "{pronoun} décide de...", "{pronoun_possessive} main se crispe..."
            """
    
    # Gestion des messages OFF
    if is_technical:
        # Message OFF : instruction technique de l'auteur
        technical_instruction = f"""
## Instruction technique de l'auteur
L'auteur souhaite orienter l'histoire ainsi :
{player_action}

Intègre cette directive de manière naturelle dans la narration suivante.
        """
        tech_instruction_note = "Intègre subtilement la directive technique de l'auteur"
    else:
        # Message narratif normal
        technical_instruction = ""
        tech_instruction_note = ""
    
    return NARRATIVE_PROMPT.format(
        genre=story.genre,
        epoch=story.epoch,
        narrative_skeleton=story.narrative_skeleton,
        narrative_perspective_instruction=perspective_instruction,
        lorebook=story.lorebook,
        recent_history=get_recent_history(story.id),
        player_action=player_action,
        technical_instruction=technical_instruction,
        tech_instruction_note=tech_instruction_note,
        current_act=determine_current_act(story)
    )
```

**Exemples de prompts générés :**

**Cas 1 : 2ème personne "Tu"**
```
## Perspective narrative
**Narration en 2ème personne ("Tu")**
- Utilise "Tu" pour désigner le protagoniste
- Exemples : "Tu te tiens...", "Tu décides de...", "Ta main se crispe..."
```

**Cas 2 : 3ème personne avec nom "Aria" (Elle)**
```
## Perspective narrative
**Narration en 3ème personne avec nom du protagoniste**
- Protagoniste : Aria (Elle)
- Utilise le nom "Aria" et le pronom "Elle"
- Exemples : "Aria se tient...", "Elle décide de...", "sa main se crispe..."
```

**Cas 3 : 3ème personne sans nom (Il)**
```
## Perspective narrative
**Narration en 3ème personne sans nom**
- Protagoniste : Il
- Utilise uniquement le pronom "Il" (pas de nom propre)
- Exemples : "Il se tient...", "Il décide de...", "sa main se crispe..."
```

**Exemple avec message OFF :**

```
## Dernière action du joueur
[Message OFF - Non visible en public]

## Instruction technique de l'auteur
L'auteur souhaite orienter l'histoire ainsi :
"Fais en sorte que le bibliothécaire révèle un secret sur le passé du protagoniste"

Intègre cette directive de manière naturelle dans la narration suivante.

## Instructions
...
3. Intègre subtilement la directive technique de l'auteur
...
```

L'IA génère alors un passage qui répond à la directive sans la mentionner explicitement.

#### Prompt : Génération des choix

```python
CHOICES_PROMPT = """
Contexte : le joueur vient de lire ce passage :
{narrative_passage}

Perspective narrative : {perspective_info}

État du protagoniste : {protagonist_traits}

Génère 3-4 choix d'action cohérents et variés :
- Choix pragmatique
- Choix audacieux
- Choix réfléchi
- Choix créatif (optionnel)

**IMPORTANT : Formule les choix selon la perspective narrative définie ci-dessus.**

Format JSON :
{
  "choices": [
    "Action 1",
    "Action 2",
    "Action 3",
    "Action 4"
  ]
}

Chaque choix doit être une phrase complète et engageante, dans la bonne perspective.
"""
```

**Fonction de construction :**

```python
def build_choices_prompt(story, narrative_passage):
    """Construit le prompt de génération des choix selon la perspective."""
    
    # Construire l'info de perspective
    if story.narrative_perspective == 'second':
        perspective_info = 'Les choix doivent être formulés avec "Tu" (ex: "Entrer prudemment dans le château")'
    else:
        if story.protagonist_name:
            name = story.protagonist_name
            perspective_info = f'Les choix doivent utiliser le nom "{name}" (ex: "{name} entre prudemment dans le château")'
        else:
            pronoun = "Elle" if story.protagonist_gender == 'she' else "Il"
            perspective_info = f'Les choix doivent utiliser "{pronoun}" (ex: "{pronoun} entre prudemment dans le château")'
    
    return CHOICES_PROMPT.format(
        narrative_passage=narrative_passage,
        perspective_info=perspective_info,
        protagonist_traits=get_protagonist_traits(story)
    )
```

**Exemples de choix générés :**

**2ème personne (Tu) :**
- A) Entrer prudemment dans le château, épée dégainée
- B) Contourner le bâtiment pour chercher une autre entrée
- C) Examiner les gargouilles de plus près
- D) Utiliser le médaillon pour voir s'il réagit

**3ème personne avec nom (Aria / Elle) :**
- A) Aria entre prudemment dans le château, épée dégainée
- B) Elle contourne le bâtiment pour chercher une autre entrée
- C) Aria examine les gargouilles de plus près
- D) Elle utilise le médaillon pour voir s'il réagit

**3ème personne sans nom (Il) :**
- A) Il entre prudemment dans le château, épée dégainée
- B) Il contourne le bâtiment pour chercher une autre entrée
- C) Il examine les gargouilles de plus près
- D) Il utilise le médaillon pour voir s'il réagit

#### Prompt : Mise à jour du LoreBook

```python
LOREBOOK_UPDATE_PROMPT = """
Analyse le dernier passage narratif et l'action du joueur.

Passage : {narrative_passage}
Action : {player_action}

LoreBook actuel : {current_lorebook}

Détermine s'il y a des éléments importants à ajouter ou mettre à jour :
- Nouveau personnage significatif ?
- Nouveau lieu important ?
- Objet clé obtenu/perdu ?
- Événement majeur ?
- Évolution du protagoniste ?

Si OUI, retourne le LoreBook mis à jour (format JSON).
Si NON, retourne : {"update_needed": false}

Format attendu (si mise à jour) :
{
  "update_needed": true,
  "characters": [...],
  "locations": [...],
  "items": [...],
  "key_events": [...],
  "protagonist_state": {...}
}
"""
```

### 5.3 Gestion des appels API

**Configuration :**
```python
# config.py
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL_CHAT = "gpt-4-turbo-preview"  # ou "gpt-4o"
OPENAI_MODEL_IMAGE = "dall-e-3"
MAX_TOKENS_NARRATIVE = 500
TEMPERATURE = 0.8  # Créativité élevée
```

**Gestion d'erreurs :**
- Retry automatique (3 tentatives) avec backoff exponentiel
- Timeout de 30 secondes
- Fallback sur réponse générique si échec total
- Logging des erreurs pour monitoring

**Optimisation des coûts :**
- Cache des réponses identiques (rare mais possible)
- Truncation intelligente de l'historique (garder les N derniers tours)
- Utilisation du LoreBook pour limiter le contexte
- Monitoring de la consommation de tokens

---

## 6. Sécurité

### 6.1 Authentification

**JWT (JSON Web Tokens) :**
- Token d'accès : durée de vie 1 heure
- Refresh token : durée de vie 7 jours
- Algorithme : HS256
- Secret stocké dans variables d'environnement

**Hashing des mots de passe :**
- Algorithme : bcrypt
- Nombre de rounds : 12

### 6.2 Validation des inputs

**Backend (Pydantic) :**
- Validation stricte de tous les inputs
- Sanitization des chaînes de caractères
- Limites de longueur

**Frontend :**
- Validation côté client pour UX
- Ne jamais faire confiance au client

### 6.3 Rate Limiting

**Endpoints critiques :**
- `/api/auth/login` : 5 requêtes / minute / IP
- `/api/auth/register` : 3 requêtes / heure / IP
- `/api/gameplay/action` : 30 requêtes / minute / utilisateur
- `/api/gallery/*` : 100 requêtes / minute / utilisateur

### 6.4 Modération

**Contenu généré par IA :**
- Utilisation de l'API OpenAI Moderation
- Filtrage automatique des contenus flaggés
- Régénération si contenu inapproprié

**Contenu utilisateur :**
- Modération des inputs en mode libre
- Liste de mots interdits (basique)
- Signalement communautaire

### 6.5 Protection des données

**RGPD :**
- Données minimales collectées
- Droit à l'oubli (suppression compte)
- Politique de confidentialité
- Consentement cookies

**Variables sensibles :**
```bash
# .env
SECRET_KEY=...
OPENAI_API_KEY=...
DATABASE_URL=...
EMAIL_API_KEY=...
```

---

## 7. Performance et scalabilité

### 7.1 Optimisations frontend

- **Code splitting** : Lazy loading des routes
- **Memoization** : React.memo sur composants lourds
- **Virtualisation** : Liste d'histoires si > 100 items
- **Images** : Optimisation et lazy loading

### 7.2 Optimisations backend

- **Connection pooling** : MySQL (SQLAlchemy gère automatiquement)
- **Pagination** : Toutes les listes (limit/offset)
- **Indexation DB** : Sur clés étrangères et champs de recherche (déjà définis)
- **Caching** : Flask-Caching pour réponses fréquentes (futur)
- **Compression** : Gzip pour réponses API (middleware Flask)
- **Optimisation requêtes** : Eager loading avec SQLAlchemy (joinedload)

### 7.3 Monitoring

**Métriques à suivre :**
- Temps de réponse API
- Consommation tokens OpenAI
- Taux d'erreur
- Nombre d'utilisateurs actifs
- Histoires créées / jour

**Outils suggérés :**
- Sentry (error tracking)
- Prometheus + Grafana (métriques)
- Logging structuré (loguru)

---

## 8. Déploiement sur O2Switch + Passenger

### 8.1 Configuration O2Switch

#### Étape 1 : Setup Python App (CPanel)

1. Se connecter à CPanel O2Switch
2. Aller dans "Setup Python App"
3. Créer une nouvelle application :
   - **Python version** : 3.12.11
   - **Application root** : `/home/soje3943/story.cosmail.fr`
   - **Application URL** : `story.cosmail.fr` (ou sous-domaine)
   - **Application startup file** : `passenger_wsgi.py`
   - **Application Entry point** : `app`

4. Activer l'environnement virtuel créé automatiquement

#### Étape 2 : Fichier `.htaccess` (racine du projet)

```apache
PassengerEnabled On
PassengerAppRoot /home/soje3943/story.cosmail.fr
PassengerAppType wsgi
PassengerStartupFile passenger_wsgi.py
PassengerPython /home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/python

# Redirections pour SPA (si frontend servi depuis O2Switch)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^api/ - [L]
RewriteRule ^static/ - [L]
RewriteRule . /index.html [L]
```

#### Étape 3 : Fichier `passenger_wsgi.py`

```python
#!/home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/python
import sys
import os

# Ajouter le répertoire de l'application au path
sys.path.insert(0, os.path.dirname(__file__))

# Activer l'environnement virtuel
INTERP = "/home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/python"
if sys.executable != INTERP:
    os.execl(INTERP, INTERP, *sys.argv)

# Charger les variables d'environnement
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), '.env'))

# Importer l'application Flask
from app import app as application

# Pour le debugging (désactiver en production)
# application.debug = False
```

#### Étape 4 : Fichier `app.py` (application Flask principale)

```python
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from flask_migrate import Migrate
from config import Config

# Initialisation de l'application
app = Flask(__name__)
app.config.from_object(Config)

# Extensions
db = SQLAlchemy(app)
migrate = Migrate(app, db)
jwt = JWTManager(app)
CORS(app, origins=app.config['CORS_ORIGINS'])

# Import des modèles (pour migrations)
from models import user, story, story_turn, lorebook, story_like, story_favorite

# Import et enregistrement des blueprints
from routes.auth import bp as auth_bp
from routes.users import bp as users_bp
from routes.stories import bp as stories_bp
from routes.gameplay import bp as gameplay_bp
from routes.gallery import bp as gallery_bp

app.register_blueprint(auth_bp, url_prefix='/api/auth')
app.register_blueprint(users_bp, url_prefix='/api/users')
app.register_blueprint(stories_bp, url_prefix='/api/stories')
app.register_blueprint(gameplay_bp, url_prefix='/api/gameplay')
app.register_blueprint(gallery_bp, url_prefix='/api/gallery')

# Route de santé
@app.route('/api/health')
def health():
    return {'status': 'ok', 'environment': app.config['ENV']}

# Pour développement local uniquement
if __name__ == '__main__':
    app.run(debug=True, port=5000)
```

#### Étape 5 : Fichier `config.py`

```python
import os
from datetime import timedelta

class Config:
    """Configuration Flask pour O2Switch."""
    
    # Base
    ENV = os.getenv('FLASK_ENV', 'production')
    DEBUG = os.getenv('DEBUG', 'False') == 'True'
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
    
    # Database (MySQL via CPanel)
    DATABASE_USER = os.getenv('DB_USER', 'soje3943_story')
    DATABASE_PASSWORD = os.getenv('DB_PASSWORD')
    DATABASE_HOST = os.getenv('DB_HOST', 'localhost')
    DATABASE_NAME = os.getenv('DB_NAME', 'soje3943_story')
    
    SQLALCHEMY_DATABASE_URI = (
        f"mysql+pymysql://{DATABASE_USER}:{DATABASE_PASSWORD}"
        f"@{DATABASE_HOST}/{DATABASE_NAME}?charset=utf8mb4"
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ENGINE_OPTIONS = {
        'pool_size': 10,
        'pool_recycle': 3600,
        'pool_pre_ping': True,
    }
    
    # JWT
    JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY)
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
    JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7)
    
    # CORS
    CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'https://story.cosmail.fr').split(',')
    
    # OpenAI
    OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
    OPENAI_MODEL_CHAT = os.getenv('OPENAI_MODEL_CHAT', 'gpt-4-turbo-preview')
    OPENAI_MODEL_IMAGE = os.getenv('OPENAI_MODEL_IMAGE', 'dall-e-3')
    
    # Email (SMTP O2Switch)
    MAIL_SERVER = os.getenv('MAIL_SERVER', 'mail.o2switch.net')
    MAIL_PORT = int(os.getenv('MAIL_PORT', 587))
    MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'True') == 'True'
    MAIL_USERNAME = os.getenv('MAIL_USERNAME')
    MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
    MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', 'noreply@story.cosmail.fr')
    
    # Uploads
    UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16 MB max
```

### 8.2 Base de données MySQL (via CPanel)

1. **Créer la base de données :**
   - CPanel → MySQL® Databases
   - Créer : `soje3943_story`
   - Créer utilisateur : `soje3943_story` avec mot de passe fort
   - Ajouter l'utilisateur à la base avec tous les privilèges

2. **Exécuter les migrations :**
```bash
# SSH dans le serveur ou terminal CPanel
cd /home/soje3943/story.cosmail.fr
source /home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/activate
flask db init  # Première fois seulement
flask db migrate -m "Initial migration"
flask db upgrade
```

### 8.3 Installation des dépendances

**Fichier `requirements.txt` :**
```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
Flask-JWT-Extended==4.6.0
Flask-CORS==4.0.0
Flask-Limiter==3.5.0
Flask-Mail==0.9.1
SQLAlchemy==2.0.23
PyMySQL==1.1.0
python-dotenv==1.0.0
pydantic==2.5.0
passlib==1.7.4
bcrypt==4.1.1
openai==1.3.0
requests==2.31.0
gunicorn==21.2.0
```

**Installation :**
```bash
cd /home/soje3943/story.cosmail.fr
source /home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/activate
pip install -r requirements.txt
```

### 8.4 Variables d'environnement

**Fichier `.env` (à la racine, **NE PAS commiter**) :**
```bash
# Flask
FLASK_ENV=production
DEBUG=False
SECRET_KEY=votre-secret-key-ultra-secure-ici
JWT_SECRET_KEY=votre-jwt-secret-key-ultra-secure-ici

# Database MySQL
DB_USER=soje3943_story
DB_PASSWORD=votre-mot-de-passe-mysql
DB_HOST=localhost
DB_NAME=soje3943_story

# OpenAI
OPENAI_API_KEY=sk-...votre-cle-openai

# Email (SMTP O2Switch)
MAIL_SERVER=mail.o2switch.net
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=noreply@story.cosmail.fr
MAIL_PASSWORD=votre-mot-de-passe-email
MAIL_DEFAULT_SENDER=noreply@story.cosmail.fr

# CORS
CORS_ORIGINS=https://story.cosmail.fr,https://www.story.cosmail.fr

# Environment
ENVIRONMENT=production
```

**Fichier `.env.example` (à commiter) :**
```bash
# Flask
FLASK_ENV=production
DEBUG=False
SECRET_KEY=change-me
JWT_SECRET_KEY=change-me

# Database
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_HOST=localhost
DB_NAME=your_db_name

# OpenAI
OPENAI_API_KEY=sk-your-key

# Email
MAIL_SERVER=mail.o2switch.net
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your_email
MAIL_PASSWORD=your_password
MAIL_DEFAULT_SENDER=noreply@yourdomain.com

# CORS
CORS_ORIGINS=https://yourdomain.com
```

### 8.5 Redémarrage de l'application

Passenger se redémarre automatiquement quand on modifie `passenger_wsgi.py` ou qu'on touch `tmp/restart.txt` :

```bash
# Méthode 1 : Touch restart.txt
touch /home/soje3943/story.cosmail.fr/tmp/restart.txt

# Méthode 2 : Modifier passenger_wsgi.py
touch /home/soje3943/story.cosmail.fr/passenger_wsgi.py
```

### 8.6 Frontend (options)

**Option 1 : Frontend servi par Flask (dossier `/static`)**
- Build React : `npm run build`
- Copier le contenu de `dist/` vers `/home/soje3943/story.cosmail.fr/static/`
- Flask servira automatiquement les fichiers statiques

**Option 2 : Frontend sur Vercel/Netlify (recommandé)**
- Héberger le frontend séparément (gratuit)
- Meilleure performance (CDN global)
- Configurer l'URL de l'API : `https://story.cosmail.fr/api`

### 8.7 SSL/HTTPS

O2Switch fournit un certificat SSL gratuit via Let's Encrypt :
- Activer dans CPanel → SSL/TLS Status
- Forcer HTTPS dans `.htaccess`

### 8.8 Monitoring et logs

**Logs Passenger :**
```bash
# Logs d'erreur
tail -f ~/logs/story.cosmail.fr/error.log

# Logs d'accès
tail -f ~/logs/story.cosmail.fr/access.log
```

**Logs Flask :**
Configurer un logger dans `app.py` :
```python
import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
    file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    app.logger.setLevel(logging.INFO)
    app.logger.info('Story app startup')
```

---

## 9. Tests

### 9.1 Frontend

**Types de tests :**
- **Unit tests** : Fonctions utilitaires, hooks
- **Component tests** : React Testing Library
- **E2E tests** : Playwright (critiques : création histoire, gameplay)

### 9.2 Backend (Flask)

**Types de tests :**
- **Unit tests** : Services, utils (pytest)
- **Integration tests** : Routes API (Flask test client)
- **E2E tests** : Scénarios complets

**Configuration des tests (`tests/conftest.py`) :**
```python
import pytest
from app import app, db
from models.user import User

@pytest.fixture
def client():
    """Crée un client de test Flask."""
    app.config['TESTING'] = True
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    
    with app.test_client() as client:
        with app.app_context():
            db.create_all()
            yield client
            db.session.remove()
            db.drop_all()

@pytest.fixture
def auth_token(client):
    """Crée un utilisateur de test et retourne son token JWT."""
    # Créer utilisateur
    user = User(
        email='test@example.com',
        username='testuser',
        hashed_password='hashed_password_here'
    )
    db.session.add(user)
    db.session.commit()
    
    # Login et récupérer token
    response = client.post('/api/auth/login', json={
        'email': 'test@example.com',
        'password': 'testpassword'
    })
    
    return response.json['access_token']
```

**Exemple de test :**
```python
# tests/test_story_creation.py
def test_create_story(client, auth_token):
    """Test de création d'une histoire."""
    response = client.post(
        "/api/stories",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={
            "title": "Test Story",
            "genre": "fantasy",
            "epoch": "medieval",
            "narrative_perspective": "second",
            "reality_level": 50
        }
    )
    assert response.status_code == 201
    data = response.get_json()
    assert data["title"] == "Test Story"
    assert data["genre"] == "fantasy"

def test_create_story_third_person(client, auth_token):
    """Test de création avec 3ème personne."""
    response = client.post(
        "/api/stories",
        headers={"Authorization": f"Bearer {auth_token}"},
        json={
            "title": "Story of Aria",
            "genre": "fantasy",
            "epoch": "medieval",
            "narrative_perspective": "third",
            "protagonist_name": "Aria",
            "protagonist_gender": "she"
        }
    )
    assert response.status_code == 201
    data = response.get_json()
    assert data["protagonist_name"] == "Aria"
```

**Lancer les tests :**
```bash
# Activer l'environnement virtuel
source /home/soje3943/virtualenv/story.cosmail.fr/3.12/bin/activate

# Installer pytest
pip install pytest pytest-cov

# Lancer tous les tests
pytest

# Avec couverture de code
pytest --cov=app tests/
```

---

## 10. Évolutions techniques futures

### 10.1 Court terme (post-MVP)
- **Flask-Caching** : Cache Redis pour réponses fréquentes
- **Celery** : Tâches asynchrones longues (génération massive, exports)
- **CDN** : Stockage images (Cloudinary, ou S3 compatible)
- **WebSockets** : Narration en streaming temps réel (Flask-SocketIO)
- **Compression** : Gzip automatique pour les réponses API

### 10.2 Moyen terme
- **Multi-langue** : i18n frontend (React i18next) + backend (Flask-Babel)
- **Mode hors-ligne** : PWA avec service workers
- **Notifications** : Push notifications (Firebase Cloud Messaging)
- **Analytics** : Mixpanel, Google Analytics, ou self-hosted (Plausible)
- **Backups automatiques** : Scripts de sauvegarde MySQL quotidiens

### 10.3 Long terme (si croissance importante)
- **Migration VPS** : Serveur dédié pour meilleures performances
- **Microservices** : Séparation service IA sur infrastructure dédiée
- **Load balancer** : Distribution de charge si trafic élevé
- **GraphQL** : Alternative à REST (Apollo Server)
- **ML custom** : Fine-tuning GPT pour style narratif spécifique au projet
- **Kubernetes** : Orchestration (si nécessité de scale massif)

---

## 11. Conclusion

Cette architecture a été conçue spécifiquement pour l'environnement **O2Switch + Passenger**, en privilégiant :

### Points forts de cette stack

✅ **Simplicité de déploiement**
- Flask WSGI natif avec Passenger (pas de configuration complexe)
- MySQL disponible directement via CPanel
- Pas besoin de conteneurisation ou serveurs externes

✅ **Adapté à l'hébergement mutualisé**
- Stack légère et performante
- Ressources optimisées pour environnement partagé
- Configuration 100% compatible O2Switch

✅ **Maintenabilité**
- Code Python pur, pas de dépendances système complexes
- Stack mature et bien documentée (Flask + SQLAlchemy)
- Facile à déboguer et monitorer

✅ **Coûts maîtrisés**
- Hébergement O2Switch (déjà payé)
- Pas de frais additionnels pour base de données
- OpenAI API : seul coût variable (tokens)

### Compromis acceptés

- **Pas d'async** : Flask sync suffit largement pour ce projet (appels IA séquentiels)
- **Hébergement mutualisé** : ressources partagées, mais amplement suffisant pour MVP et croissance initiale
- **Pas de PostgreSQL** : MySQL 8.0 offre toutes les fonctionnalités nécessaires (JSON, transactions, performances)

### Évolution future

Si le projet croît significativement, des évolutions possibles :
- Migration vers serveur dédié (VPS)
- Ajout de Redis pour caching
- Séparation du service IA sur infrastructure dédiée
- Passage à un load balancer si besoin

**L'architecture actuelle est parfaitement adaptée au MVP et peut supporter plusieurs milliers d'utilisateurs sans problème.**

L'architecture sera ajustée en fonction des retours utilisateurs et des besoins réels en production.

---

**Document vivant** : Cette architecture évoluera durant le développement.

