Files
LoreMindMJ/docs/plan.md

86 KiB
Raw Blame History

Plan de développement LoreMind

Contexte du projet

LoreMind est une application d'aide aux Maîtres de Jeu (JDR) permettant de gérer le Lore, les campagnes, et intègre un moteur IA pour générer du contenu structuré à partir de templates. À terme, les données seront exportables vers FoundryVTT.

Stack technique

  • Frontend : Angular
  • Backend Core (Données & Métier) : Java (Spring Boot, DDD, Architecture Hexagonale)
  • Backend AI (Génération & LLM) : Python (FastAPI, Ollama en local)
  • Base de données : PostgreSQL

Architecture Backend Java

  • Domain-Driven Design (DDD) strict
  • Architecture Hexagonale (Ports et Adaptateurs)
  • Bounded Contexts : LoreContext, CampaignContext, GenerationContext
  • Cœur du domaine sans dépendances techniques (ni framework, ni base de données)

État actuel du projet

🆕 Projet initialisé (14 avril 2026)

  • Création des dossiers structurels (core/, web/, brain/, docs/)
  • Documentation de contexte et règles (.windsurfrules, loremind-contexte.md)

🚀 Backend Java - Phase 1 (16 avril 2026)

  • Initialisation du projet Spring Boot (pom.xml)
  • Configuration de la base de données PostgreSQL
  • Création des entités de domaine :
    • LoreContext : Lore, LoreNode, Page, Template
    • CampaignContext : Campaign, Arc, Chapter, Scene
  • Création des Ports (Repositories interfaces) pour TOUS les contexts
  • Création des Adaptateurs d'infrastructure :
    • JPA Entities (LoreJpaEntity, LoreNodeJpaEntity, etc.)
    • JPA Repositories (LoreJpaRepository, etc.)
    • Postgres Repositories (PostgresLoreRepository, etc.)
  • Création des Services d'application PARTIELS :
    • LoreService
    • CampaignService
  • Création des Services d'application restants :
    • LoreNodeService
    • PageService
    • TemplateService
    • ArcService
    • ChapterService
    • SceneService
  • Création des DTOs et Mappers :
    • LoreContext DTOs
    • CampaignContext DTOs
    • Mappers pour toutes les entités
  • Création des REST Controllers :
    • LoreController, CampaignController
    • LoreNodeController, PageController, TemplateController
    • ArcController, ChapterController, SceneController
  • Configuration CORS
  • Création du GenerationContext (entités, ports, adaptateurs, services)

À faire

Phase 2 : Frontend Angular (Priorité haute)

  • Initialisation du projet Angular
  • Création du layout de base (Sidebar + Main Content)
  • Création du composant Sidebar (redesign complet : logo, palette violette, OUTILS, version)
  • Configuration routing Angular
  • Styles globaux dark theme
  • Installation lucide-angular (icônes SVG)
  • Page Lore (Vos Univers) — grille de cartes, carte "Nouveau Lore"
  • Page Campagnes — grille de cartes, carte "Nouvelle Campagne"
  • Modal création de Lore (LoreCreateComponent)
  • Branchement LoreService.createLore()
  • Composant SecondarySidebar (réutilisable Lore + Campagne)
  • LayoutService (contrôle affichage secondary sidebar)
  • Page détail Lore (/lore/:id) avec arborescence noeuds
  • Page détail Campagne (/campaigns/:id) avec arborescence arcs/chapitres
  • Formulaire création Campagne (modal)
  • Sidebar globale contextuelle (liste lores/campagnes + retour)
  • Secondary sidebar avec toggle pli/dépli
  • Écran création de noeud (Lore)
  • Refonte secondary sidebar : actions contextuelles (Lore: + Noeud / + Page, Campagne: + Nouvel arc). Bouton "Sauvegarder" retiré (persistance immédiate via REST).
  • Écran création/modification de page (Lore) — Phase 5A livrée : domaine Page enrichi (loreId, values: Map<String,String>, notes, tags, relatedPageIds), écrans page-create (titre + grille de templates + noeud pré-rempli depuis template.defaultNodeId) et page-edit basique (champs dynamiques du template rendus en textarea + notes privées). Pages affichées dans l'arbre sous leur noeud + action "+ Nouvelle page" par noeud.
  • Écran création/modification de template (Lore) (domaine enrichi loreId + defaultNodeId + List<String> fields, panneau sidebar "Templates" fidèle à la maquette)
  • Écran création d'arc (Campagne) (champ illustration reporté — à ajouter quand l'écran détail d'un arc sera implémenté)
  • Écran création de chapitre (Campagne) (chargement chapitres existants + action "+ Nouveau chapitre" inline dans l'arbre)
  • Écran création de scène (Campagne)
  • Refactor : helper campaign-tree.helper.ts (chargement arbre + construction TreeItem[]) + rendu récursif 3 niveaux dans SecondarySidebar
  • Écrans détail/modification (Campagne) : arc-edit, chapter-edit, scene-edit avec Sauvegarder + Supprimer
  • SecondarySidebar : séparation chevron/label pour permettre expand + navigation sur un même item
  • Enrichissement domaine narratif Arc/Chapter/Scene (sous-tâches 1-3 , sous-tâche 4 cross-context Lore↔Campaign restante)

Enrichissement du domaine Template (Lore) (18 avril 2026)

Maquettes : docs/maquettes/lore/Création de template.png et Modification de template.png.

Changements backend Java (Option i : champs typés explicites, DDD-friendly) :

  • Template.java : ajout loreId, defaultNodeId, List<String> fields ; suppression Map<String,Object> structure ; méthodes métier fieldCount, addField, removeField.
  • TemplateRepository.java (port) : ajout findByLoreId(String loreId).
  • TemplateJpaEntity.java : colonnes typées lore_id (NOT NULL), default_node_id, fields (TEXT via converter JSON).
  • Nouveau converter StringListJsonConverter : List<String> ↔ JSON pour la persistance PostgreSQL.
  • PostgresTemplateRepository.java : mapping bidirectionnel enrichi + implémentation findByLoreId.
  • TemplateJpaRepository.java : méthode dérivée findByLoreId(Long).
  • TemplateDTO.java + TemplateMapper.java : alignement sur le nouveau domaine (expose fieldCount calculé côté serveur).
  • TemplateService.java : signatures refactorées ; updateTemplate en Parameter Object pattern ; loreId volontairement immuable (pas de migration cross-Lore via simple update).
  • TemplateController.java : POST /api/templates, GET /api/templates?loreId=X, GET /{id}, PUT /{id}, DELETE /{id}. Retrait du PATCH /{id}/structure obsolète.

Changements frontend Angular :

  • template.model.ts, template.service.ts : modèle TS + service HTTP complet.
  • layout.service.ts : nouveaux types BottomPanel, BottomPanelItem ; SecondarySidebarConfig.bottomPanel? ajouté. footerLabel déprécié (gardé optionnel pour compat des callers campagne).
  • SecondarySidebarComponent : rendu du bottomPanel avec toggle ouvert/fermé, items cliquables + meta (ex: "8 champs").
  • Nouveau helper @app/lore/lore-sidebar.helper.ts : charge lore + all lores + nodes + templates en parallèle et construit la config sidebar complète (panneau Templates inclus). Utilisé par lore-detail et lore-node-create (→ résout la dette "duplications de pattern côté Lore").
  • TemplateCreateComponent et TemplateEditComponent : écrans fidèles aux maquettes (colonne gauche = identité, colonne droite = liste dynamique ajout/suppression de champs).
  • Routes ajoutées : /lore/:loreId/templates/create et /lore/:loreId/templates/:templateId.

Enrichissement du domaine Page (Lore) — Phase 5A (18 avril 2026)

Maquettes : docs/maquettes/lore/création de page.png et Modification d'une page.png.

Changements backend Java (structure alignée sur les maquettes) :

  • Page.java : suppression content: String (trop vague) ; ajout loreId, values: Map<String,String> (valeurs des champs dynamiques du template, clé = fieldName), notes (privé MJ), tags: List<String>, relatedPageIds: List<String>. Méthodes métier setFieldValue, getFieldValue, addTag, removeTag.
  • PageRepository.java : ajout findByLoreId(String loreId).
  • PageJpaEntity.java : colonne lore_id NOT NULL, values_json (colonne renommée car values est un mot-clé SQL), notes, tags, related_page_ids — tous stockés en JSON (TEXT) via converters.
  • Nouveau converter StringMapJsonConverter : Map<String,String> ↔ JSON (distinct du MapJsonConverter générique Map<String,Object> dont Page n'a pas besoin).
  • PostgresPageRepository.java + PageJpaRepository.java : mapping et méthode dérivée findByLoreId.
  • PageDTO.java + PageMapper.java : alignement complet.
  • PageService.java : createPage(loreId, nodeId, templateId, title) minimaliste ; updatePage(id, changes) Parameter Object (applique title/nodeId/values/notes/tags/relatedPageIds). loreId et templateId immuables après création.
  • PageController.java : POST /api/pages, GET /api/pages?loreId=X ou ?nodeId=Y, GET /{id}, PUT /{id}, DELETE /{id}. Suppression de PATCH /{id}/template et de GET /node/{nodeId} (unifié en query param).

Changements frontend Angular :

  • page.model.ts, page.service.ts : modèle TS + service HTTP.
  • lore-sidebar.helper.ts enrichi : charge aussi les pages ; les pages apparaissent dans l'arbre sous leur noeud (TreeItem children) ; chaque noeud a un item action "+ Nouvelle page" pointant vers /lore/:loreId/nodes/:nodeId/pages/create ; bouton "+ Page" du header de sidebar pointe vers /lore/:loreId/pages/create (choix libre du template).
  • PageCreateComponent : fidèle à la maquette — titre + grille de cartes Template sélectionnables + select Noeud de destination (pré-rempli depuis template.defaultNodeId si l'URL n'impose pas déjà un nodeId). Redirige vers page-edit après création.
  • PageEditComponent basique — titre, noeud (déplaçable), un textarea par field du template (valeurs stockées dans values), notes privées. Bouton "Assistant IA" stub (sera branché en Phase 3 Python).
  • Routes ajoutées : /lore/:loreId/pages/create, /lore/:loreId/nodes/:nodeId/pages/create, /lore/:loreId/pages/:pageId.

Renommage "dossier" + icônes + hiérarchie (18 avril 2026)

Triple corrections suite au retour utilisateur :

Renommage UI "noeud" → "dossier" (nom interne LoreNode conservé côté Java pour limiter l'impact BDD et code) :

  • Textes visibles mis à jour dans : lore-node-create.*, lore-detail.*, lore.component.*, page-create.*, page-edit.*, template-create.*, template-edit.*, bouton sidebar + Dossier.

Bug corrigé — icônes de dossier invisibles :

  • Côté backend : l'icône envoyée par l'UI était silencieusement ignorée (icon absent de LoreNode.java, LoreNodeDTO.java, du mapping JPA). Ajouté partout : domaine + JPA entity (colonne icon) + DTO + mapper + repository + service (Parameter Object pour createLoreNode/updateLoreNode).
  • Côté frontend : nouveau registre partagé @app/lore/lore-icons.ts (LORE_ICON_OPTIONS + resolveIcon(key)). Refactor de lore-node-create pour utiliser ce registre. Sidebar rend l'icône via TreeItem.iconKey (nouveau champ) + méthode iconFor().

Dossiers imbriqués :

  • Le backend supportait déjà parentId — seul le frontend ne l'exposait pas. Ajout du champ parentId à LoreNode/LoreNodeCreate TS + formulaire lore-node-create (select "Dossier parent"). Nouvelle route /lore/:loreId/folders/:parentId/create pour pré-remplir depuis la sidebar.
  • Helper lore-sidebar.helper.ts refactoré : construction récursive de l'arbre (fonction buildFolderItem). Chaque dossier affiche ses sous-dossiers + ses pages + actions + Nouveau dossier et + Nouvelle page inline.

Édition et suppression de dossier (18 avril 2026)

Complète la CRUD manquante pour les LoreNode (dossiers).

Frontend Angular :

  • LoreService enrichi : getLoreNodeById, updateLoreNode, deleteLoreNode. URL des lore-nodes factorisée dans nodesUrl.
  • Nouveau LoreNodeEditComponent (lore-node-edit/) — formulaire avec nom, icône (grille), dossier parent (select). Header avec boutons Annuler / Supprimer / Sauvegarder.
  • Helper lore-sidebar.helper.ts : ajout de la fonction utilitaire collectDescendantIds(rootId, allNodes) qui calcule de manière itérative l'ensemble des descendants d'un dossier. Utilisée pour empêcher les cycles : le select "Dossier parent" de l'édition exclut le dossier courant et tous ses descendants.
  • Chaque dossier dans la sidebar a maintenant une route pointant vers son écran d'édition (clic sur le label → édition, clic sur le chevron → expand/collapse).
  • Route ajoutée : /lore/:loreId/folders/:folderId/edit.

Règle de suppression (safe) :

  • La suppression est refusée si le dossier contient des sous-dossiers ou des pages (message explicite "Videz-le d'abord : X sous-dossier(s) et Y page(s)").
  • Raison : protéger les notes MJ contre un clic accidentel. Pas de cascade silencieuse.
  • La vérification se fait côté frontend à partir des données déjà chargées dans la sidebar (pas d'appel HTTP supplémentaire). Le backend accepte le DELETE sans check, mais le frontend ne l'émet jamais si le dossier n'est pas vide.

Pages — Phases à venir

Phase 5B : édition complète (18 avril 2026)

  • Tags (chips) — nouveau composant réutilisable app-chips-input (@app/shared/chips-input/). UX : Entrée ou virgule pour ajouter, Backspace sur input vide retire le dernier chip, doublons silencieusement ignorés, trim automatique. Binding deux-sens via [value] / (valueChange).
  • Liens vers d'autres pages — nouveau composant app-lore-link-picker (@app/shared/lore-link-picker/). Input de recherche avec dropdown de suggestions filtrées (max 8), chips cliquables pour chaque page liée (clic → navigation, X → retrait). Exclut la page courante via excludePageId.
  • Intégration dans page-edit : sections "Tags" et "Pages liées" ajoutées entre les champs dynamiques et les notes privées. allPages: Page[] récupéré depuis la sidebar pour alimenter le picker.
  • Le composant app-lore-link-picker est conçu pour être réutilisé en Phase cross-context Campagne↔Lore (Arc/Chapter/Scene pourront lier des Pages du Lore via ce même picker).

Phase 5C : affichage et navigation fine

  • Compteur de pages sur chaque dossier dans le sidebar (ex: PNJ · 3) (19 avril 2026).
  • Correction compteurs home "Vos univers" (nodeCount/pageCount calculés à la volée via countByLoreId au lieu d'être stockés en BDD et jamais MAJ) (19 avril 2026).
  • Breadcrumb Lore > Dossier > Page dans page-edit (19 avril 2026) — composant réutilisable app-breadcrumb prêt pour Arc/Chapter/Scene.
  • Raccourci clavier Ctrl+S pour sauvegarder.

Phase 5D : intégration IA (dépend de la Phase 3 Python)

  • Branchement du bouton "Assistant IA" de page-edit sur l'endpoint Python Ollama.
  • Affichage streaming des suggestions par champ.

Enrichissement du domaine narratif (Campagne)

Les maquettes (docs/maquettes/campagne/détail/*.png) révèlent que chaque entité narrative doit porter bien plus que name + description. Voici le détail des champs manquants qui doivent être ajoutés côté Java (domain + JPA + DTO + mapper) et côté Angular (model + forms).

Approche retenue :

  • Colonnes TEXT dédiées (pas de JSONB) car chaque champ a un sens métier précis et sera utilisé par l'Assistant IA (Phase 3) comme prompt structuré.
  • Les liens vers le Lore (PNJ, lieux, objets) = IDs stockés en JSONB ou table de liaison, pas de relation JPA cross-context (principe DDD : Bounded Contexts isolés).

Sous-tâche 1 : Arc (5 champs texte)

  • themes — Thèmes principaux
  • stakes — Enjeux globaux
  • gmNotes — Notes et planification du MJ (privé, non exporté FoundryVTT)
  • rewards — Récompenses et progression
  • resolution — Dénouement prévu
  • Impact : Arc.java (domain), ArcJpaEntity.java (JPA + colonnes TEXT auto via ddl-auto=update), ArcDTO.java, ArcMapper.java, PostgresArcRepository.java, ArcService.updateArc() refactoré en Parameter Object pattern, ArcController.updateArc(), campaign.model.ts, arc-edit.component.* (formulaire enrichi, arc-create reste volontairement minimal comme la maquette).

Sous-tâche 2 : Chapter (3 champs texte)

  • gmNotes — Notes du Maître de Jeu (privé)
  • playerObjectives — Objectifs des joueurs
  • narrativeStakes — Enjeux narratifs
  • Impact : Chapter.java, ChapterJpaEntity.java, ChapterDTO.java, ChapterMapper.java, PostgresChapterRepository.java, ChapterService.updateChapter() (Parameter Object), ChapterController.updateChapter(), campaign.model.ts, chapter-edit.component.*.

Sous-tâche 3 : Scene (8 champs texte répartis en sections)

Sections de la maquette (chaque section est un bloc d'UI expandable) :

  • Contexte et ambiance : location (court), timing (court), atmosphere (long)
  • Narration pour les joueurs : playerNarration (long)
  • Notes et secrets du MJ (privé, variant "private" rouge) : gmSecretNotes (long)
  • Choix et conséquences : choicesConsequences (long)
  • Combat ou rencontre : combatDifficulty (court), enemies (long)
  • Impact : Scene.java, SceneJpaEntity.java, SceneDTO.java, SceneMapper.java, PostgresSceneRepository.java, SceneService.updateScene() (Parameter Object), SceneController.updateScene(), campaign.model.ts, scene-edit.component.* (11 champs totaux, 5 sections expandables).
  • Nouveau composant partagé : @app/shared/expandable-section/ — réutilisable (propriétés : title, icon emoji, initiallyOpen, variant: 'default' | 'private').

Sous-tâche 4 : Liens cross-context Lore ↔ Campaign

Tous des List<String> d'IDs de LoreNode :

  • Arc : antagonistNpcIds, allyNpcIds
  • Chapter : involvedNpcIds, visitedLocationIds
  • Scene : presentNpcIds, importantObjectIds
  • Composant Angular réutilisable : app-lore-link-picker (autocomplete + liste de chips)
  • Endpoint GET /api/lore-nodes?ids=a,b,c (résolution multi-IDs) côté Java

Phase 3 : Backend Python — Brain IA (DÉMARRÉE le 19 avril 2026)

Stack retenue : FastAPI + Ollama local + Architecture Hexagonale (Ports/Adapters via Protocol PEP 544). Le Brain est l'executor cognitif : il reçoit des demandes du Core Java, construit un prompt, appelle un LLM et renvoie un résultat structuré. Le Core Java reste le chef d'orchestre (règle .windsurfrules ligne 21).

Étape b1 — Squelette FastAPI
  • Structure brain/app/ + venv + requirements.txt minimal (fastapi, uvicorn).
  • Endpoint GET /health (sonde de vie) et Swagger UI auto sur /docs.
  • .gitignore (venv, pycache, .env).
Étape b2 — Refactor hexagonal
  • Config Pydantic Settings (.env.example + app/core/config.py + get_settings() singleton via @lru_cache).
  • Port LLMProvider (Protocol PEP 544) + exception domaine LLMProviderError dans app/domain/ports.py.
  • Adapter OllamaLLMProvider dans app/infrastructure/ollama_adapter.py — isole tout le code httpx et le protocole Ollama.
  • Factory get_llm_provider() dans main.py = unique point d'inversion de dépendance (changer d'1 ligne pour switch vers OpenAI/Claude demain).
  • Controller POST /generate fin : reçoit le port via Depends, ignore l'Adapter concret.
  • Fiche academy docs/academy/hexagonal-python.md (théorie + analogie JDR + Python vs Java + quiz 5 QCM).
  • Modèle par défaut : gemma4:e2b (validé avec sortie "Borin le forgeron nain" en ~2s). Swap possible via LLM_MODEL dans .env.
Étape b3 — Génération structurée JSON
  • b3.1 — Modèles de domaine PageGenerationContext / PageGenerationResult en @dataclass(frozen=True) dans app/domain/models.py (domaine sans dépendance Pydantic).
  • b3.1 — Port LLMProvider.generate() enrichi d'un kwarg output_format: str | None (pass-through vers format: "json" d'Ollama).
  • b3.2 — Use case GeneratePageUseCase dans app/application/generate_page.py : construction prompt système (français, orienté MJ de JDR) + appel LLM avec output_format="json" + parsing JSON défensif (filtrage sur template_fields, champs manquants → chaîne vide, cast str systématique).
  • b3.3 — Endpoint POST /generate-page + DTOs Pydantic GeneratePageRequestDTO / GeneratePageResponseDTO en frontière HTTP + factory get_generate_page_use_case() pour l'injection du use case.
Étape b4 — Branchement Core Java ↔ Brain (19 avril 2026, soir)

Chaîne complète opérationnelle : clic "Assistant IA" dans page-edit → Angular → Java (PageGenerationControllerGeneratePageValuesUseCaseBrainAiClient) → Python (/generate-page) → Ollama → retour JSON → merge dans les textareas. Zéro persistance côté génération, l'utilisateur valide et sauvegarde manuellement. Une nouvelle fiche academy docs/academy/bounded-context.md a été ajoutée pour formaliser le 3ᵉ Bounded Context (GenerationContext).

Démarrage de la stack complète pour tester :

# Terminal 1 — Ollama (normalement déjà en service système)
# http://localhost:11434, modèle `gemma4:e2b` tiré

# Terminal 2 — Brain Python
cd brain && source .venv/Scripts/activate && uvicorn app.main:app --reload --port 8000

# Terminal 3 — Core Java
cd core && mvn spring-boot:run

# Terminal 4 — Frontend Angular
cd web && npm start
b4.1 — Domaine DDD côté Java (GenerationContext)
  • Package domain/generationcontext/ créé.
  • GenerationContext (immuable @Value @Builder) + GenerationResult (immuable @Value).
  • Port AiProvider (interface pure, zéro annotation Spring) + exception domaine AiProviderException (RuntimeException).
b4.2 — Adapter HTTP Java → Brain Python
  • BrainAiClient dans infrastructure/ai/ — implémente AiProvider.
  • Revirement technique assumé : RestTemplate retenu plutôt que WebClient. Raisons : déjà présent dans spring-boot-starter-web (zéro nouvelle dépendance), usage synchrone suffisant pour un bouton "Assistant IA" (pas de streaming au MVP), plus simple à lire pour un développeur qui n'a pas encore rencontré le paradigme réactif Reactor. Le passage à WebClient reste possible sans toucher au domaine (c'est précisément le bénéfice de l'hexagonal).
  • Config brain.base-url + brain.timeout-seconds dans application.properties, bean RestTemplate dédié dans RestTemplateConfig (connect 10s, read 120s).
  • DTOs package-private BrainGeneratePageRequest / BrainGeneratePageResponse avec @JsonProperty snake_case (pas de config Jackson globale — isolation au package infrastructure/ai).
  • Traduction d'erreurs : ResourceAccessException (timeout/Brain down) / RestClientResponseException (4xx/5xx) → AiProviderException. Filet de sécurité pour toute autre Exception.
b4.3 — Use case Java GeneratePageValuesUseCase
  • application/generationcontext/GeneratePageValuesUseCase.java — injection constructeur des 5 ports (PageRepository, TemplateRepository, LoreRepository, LoreNodeRepository, AiProvider).
  • execute(pageId) : 4 lookups (Page → Template → Lore → LoreNode), validation template.fields non-vide, construction du GenerationContext, appel AiProvider.generatePage, retour direct de result.values.
  • Décision produit respectée : zéro persistance. Exceptions différenciées : IllegalArgumentException (page introuvable) vs IllegalStateException (incohérence BDD ou template sans champs).
b4.4 — REST endpoint Java
  • Écart assumé vs plan initial : endpoint placé dans un nouveau PageGenerationController dédié (pas dans PageController) — SRP strict, alignement sur le Bounded Context generationcontext. URL RESTful conservée : POST /api/pages/{id}/generate.
  • DTO GenerationSuggestionsDTO { Map<String, String> values } dans infrastructure/web/dto/generationcontext/.
  • Gestion d'erreurs inline (pas d'@ControllerAdvice — cohérent avec le style du reste du projet) : 200, 404, 422 (template sans champs, détecté sur le message), 502 (AiProviderException), 500 (autre IllegalStateException).
b4.5 — Frontend Angular : bouton "Assistant IA" branché
  • PageService.generateValues(pageId)POST /api/pages/{id}/generate, retourne Record<string, string>.
  • page-edit : état aiLoading + aiError, méthode runAssistantAI(), libellé du bouton qui passe à "Génération…" pendant l'appel, bouton désactivé si template sans champs ou appel en cours.
  • Merge soft simplifié : toute suggestion non-vide écrase (l'utilisateur a demandé la régénération), suggestion vide laisse la valeur courante intacte.
  • Banner d'erreur dismissable au-dessus du formulaire : message différencié 502 (Brain down) vs autre.
  • Pas de sauvegarde auto — l'utilisateur valide et clique "Sauvegarder" pour persister via le PUT existant.
b4.6 — Améliorations post-MVP (backlog, pas bloquant)
  • Température LLM configurable côté UI (slider "créativité" mappé sur le paramètre temperature d'Ollama).
  • Historique des générations par page (BDD côté Java + vue "revert to previous suggestion").
  • Retry automatique avec backoff côté BrainAiClient en cas d'erreur transitoire.
  • Prompt personnalisable par Lore (ex: ton "sombre et épique" vs "aventure familiale") — stocké sur l'entité Lore côté Java, transmis dans le GenerationContext.
Étape b5 — Chat IA conversationnel avec Structural Context (19 avril 2026, soir)

UX "IA qui écrit sous tes yeux" livrée de bout en bout. Drawer chat à droite de page-edit (pattern validé sur les maquettes lore/Assistance IA dans une page.png et campagne/Assistance IA.png). L'IA voit la structure du Lore (dossiers, pages, templates, tags) sans recevoir le contenu. Conversation éphémère (perdue à la fermeture du drawer). Intégration limitée au Lore pour l'instant — Campagne viendra quand on voudra l'étendre.

b5.1 — Backend Python : endpoint /chat/stream SSE
  • Dataclasses ChatMessage + LoreStructuralContext dans domain/models.py (immuables, sans Pydantic).
  • Protocol LLMChatProvider dans domain/ports.py — distinct de LLMProvider par ISP (Interface Segregation Principle). OllamaLLMProvider satisfait les deux par duck typing.
  • OllamaLLMProvider.stream_chat() : consomme /api/chat d'Ollama en mode stream=True, parse le NDJSON ligne par ligne, yield les tokens non-vides. Le formatage SSE est la responsabilité du controller, pas de l'adapter.
  • ChatUseCase dans application/chat.py : construit un system prompt riche avec le Structural Context (carte des dossiers/pages/templates/tags), délègue au port.
  • Endpoint POST /chat/stream avec StreamingResponse(media_type="text/event-stream"). Format de flux : data: {"token":"..."}, event: done, event: error.
b5.2 — Core Java : port AiChatProvider + adapter WebClient SSE
  • Ajout de spring-boot-starter-webflux au pom.xml (requis pour WebClient, seul outil Spring capable de consommer SSE côté client) + spring.main.web-application-type=servlet dans application.properties pour forcer Tomcat malgré WebFlux.
  • Domaine generationcontext/ : ChatMessage, LoreStructuralContext (+ inner FolderPage), ChatRequest, port AiChatProvider.
  • Choix pédagogique : API par callbacks (Consumer<String> onToken, Runnable onComplete, Consumer<Throwable> onError) plutôt que Flux<String>. Raisons : zéro dépendance Reactor dans le domaine, plus simple à comprendre pour un développeur qui n'a pas rencontré le paradigme réactif, mappage naturel vers SseEmitter côté controller.
  • Adapter BrainAiChatClient : WebClient.retrieve().bodyToFlux(ServerSentEvent), dispatch doOnNext → callbacks, blockLast() pour rester synchrone, timeout 120s, traduction d'erreurs en AiProviderException.
b5.3 — Core Java : endpoint POST /api/ai/chat/stream SSE
  • Use case StreamChatForLoreUseCase dans application/generationcontext/ : charge Lore + LoreNode[] + Page[] + Template[] (4 lookups), construit le LoreStructuralContext, délègue au port. 4 ports injectés côté LoreContext + 1 côté GenerationContext.
  • AiChatController expose POST /api/ai/chat/stream (produces = text/event-stream). Le streaming tourne dans un thread séparé via AsyncTaskExecutor pour ne pas bloquer le servlet. SseEmitter (timeout 5 min) thread-safe : les callbacks du port peuvent écrire dessus depuis n'importe quel thread.
  • DTOs ChatMessageDTO + ChatStreamRequestDTO dans infrastructure/web/dto/generationcontext/. Helper jsonEscape() interne (pas de pull Jackson ici).
b5.4 — Frontend : composant app-ai-chat-drawer
  • Service AiChatService.streamChat() dans web/src/app/services/ai-chat.service.ts : fetch() + ReadableStream + décodage ligne-par-ligne SSE. Retourne un Observable<ChatStreamEvent> qui emit {type:'token'} par fragment, complete sur event: done, error sur event: error ou échec réseau. Annule proprement via AbortController à l'unsubscribe.
  • Pas d'EventSource : l'API navigateur native ne supporte que GET sans body — on a besoin de POST avec JSON (messages + loreId).
  • Composant standalone AiChatDrawerComponent dans shared/ai-chat-drawer/ : @Input loreId/isOpen/welcomeMessage/quickSuggestions[]/primaryAction, @Output close/primaryActionClick. État local : messages[], currentAssistantText (buffer de streaming), isStreaming, errorMessage. Conversation éphémère perdue à la fermeture (choix MVP assumé).
  • UX fidèle aux maquettes : bulles user (droite violet) / assistant (gauche sombre), welcome message, typing-indicator avant le premier token, caret clignotant pendant le streaming, suggestions rapides en bas, input + bouton envoyer.
b5.5 — Intégration dans page-edit
  • Bouton "Assistant IA" du header change de rôle : il ouvre désormais le drawer (toggleChat()) au lieu d'appeler le one-shot directement.
  • Le one-shot b4 reste accessible via primaryAction du drawer (bouton violet pleine largeur "Remplir automatiquement tous les champs") : clic → ferme le drawer + déclenche runAssistantAI() → textareas se remplissent. Le meilleur des deux mondes, sans duplication de code.
  • Suggestions rapides hardcodées (MVP) : "Étoffe l'histoire", "Suggère des liens avec d'autres pages du Lore", "Propose une intrigue secondaire".
  • Bouton "Assistant IA" stylé active quand le drawer est ouvert (fond gris + bordure violette) — feedback visuel clair.
b5.6 — Fiche academy streaming-sse-rag.md
  • Théorie : SSE vs WebSocket vs polling avec analogie JDR (pigeon voyageur qui revient par fragments).
  • Structural Context vs RAG sémantique : pourquoi on n'a PAS encore de DB vectorielle.
  • Code réel extrait des 3 étages de la stack (Python / Java / Angular).
  • Section "Le savais-tu ?" sur la nature debug-friendly du format SSE (curl -N suffit).
  • Quiz 5 QCM.
b5.7 — Intégration dans la Campagne (arc / chapter / scene) (20 avril 2026, après-midi)

Drawer IA disponible sur les 3 écrans de Campagne. Un MJ peut dialoguer avec l'IA depuis l'arc, le chapitre ou la scène en cours. Le prompt système reçoit automatiquement l'arbre narratif (noms seulement — pas de contenu), les champs de l'entité focus, et — si la campagne est liée à un Lore — les templates + l'arbre des pages de ce Lore. Asymétrie respectée : un Lore ne voit PAS ses campagnes (sens unique Campagne → Lore).

b5.7.1 — Value Objects narratifs (core/domain)
  • CampaignStructuralContext : arbre campaignName + campaignDescription + List<ArcSummary> avec ArcSummary(name + chapters) et ChapterSummary(name + sceneNames). Lombok @Value @Builder @Singular.
  • NarrativeEntityContext : VO "focus" avec entityType ∈ {arc, chapter, scene} + title + Map<String,String> fields (description, themes, stakes, playerObjectives, atmosphere…).
  • ChatRequest étendu : loreContext nullable, campaignContext et narrativeEntity ajoutés. Un chat Lore continue à fonctionner inchangé.
b5.7.2 — Builders applicatifs (DRY + cross-context)
  • LoreStructuralContextBuilder extrait en @Component partagé : build(loreId) lance une exception si absent, buildOptional(loreId) retourne Optional.empty() pour dégradation gracieuse (Lore supprimé entre deux appels).
  • CampaignStructuralContextBuilder : traverse Campagne → Arcs (triés par order) → Chapters (triés) → noms de Scenes (triés). Pas de contenu, juste la structure.
  • NarrativeEntityContextBuilder : switch sur entityType, mappe les champs domaine vers la Map via putIfNotBlank. Ne fuit aucun secret MJ vers le prompt joueur (c'est un chat MJ, donc tout est exposé — mais le découpage par champ reste explicite).
  • StreamChatForCampaignUseCase : orchestre Campaign → Lore optionnel (via campaign.isLinkedToLore()) → Narrative entity optionnelle → délégation au port AiChatProvider.
  • StreamChatForLoreUseCase refactoré : 182 → 114 lignes, délègue à LoreStructuralContextBuilder (DRY).
b5.7.3 — Pont Java ↔ Python
  • BrainAiChatClient.toPayload() : 4 contextes optionnels (lore_context, page_context, campaign_context, narrative_entity) ajoutés au JSON snake_case seulement s'ils existent.
  • brain/app/domain/models.py : dataclasses ArcSummary, ChapterSummary, CampaignStructuralContext, NarrativeEntityContext.
  • brain/app/application/chat.py : _BASE_SYSTEM rendu générique ("contexte ci-dessous"), stream(…) en kw-only args, _build_system_prompt assemble les sections conditionnelles. Formatters dédiés _format_campaign, _format_arcs, _format_chapter_block, _format_narrative_entity.
  • brain/app/main.py : DTOs ArcSummaryDTO, ChapterSummaryDTO, CampaignContextDTO, NarrativeEntityDTO (validation entity_type via pattern). ChatStreamRequestDTO.has_scope() → HTTP 422 si aucun scope.
b5.7.4 — Controller REST + service Angular
  • AiChatController.POST /api/ai/chat/stream-campaign : ChatStreamCampaignRequestDTO(campaignId, entityType?, entityId?, messages). SSE helpers réutilisés.
  • AiChatService.streamChatForCampaign(...) + type NarrativeEntityType = 'arc' | 'chapter' | 'scene'. Helper privé streamSse(...) partagé avec streamChat(...) (Lore).
  • AiChatDrawerComponent : nouveaux @Input() campaignId, entityType, entityId. Dispatch : campaignId truthy → mode Campagne, sinon mode Lore (backward compatible).
b5.7.5 — Intégration UI dans les 3 écrans
  • arc-edit, chapter-edit, scene-edit : bouton btn-ai "Assistant IA" dans le header (Sparkles icon + état active), <app-ai-chat-drawer> injecté avec entityType + entityId appropriés, quickSuggestions adaptées au rôle narratif (thèmes/enjeux pour l'arc, objectifs/tensions pour le chapitre, ambiance/narration/choix pour la scène).
  • Style global .btn-ai extrait en _buttons.scss (violet #a5b4fc, variante .active bordure #6c63ff) pour éviter la duplication.
  • Validation finale : mvn clean compile BUILD SUCCESS + npx tsc --noEmit 0 erreur.
b5.7.6 — À faire plus tard
  • Persistance optionnelle de la conversation (entité Conversation côté Java, historique reprenable entre sessions).
  • Fiche academy dédiée à la composition de prompts multi-contextes (Lore + Campaign + Entity).
b5.8 — Enrichissement du Structural Context Campagne (20 avril 2026, après-midi)

Problème remonté par l'utilisateur : en éditant une scène, impossible de demander à l'IA "c'est quoi la scène X (qui est ailleurs dans la campagne) ?" — elle ne connaissait QUE les noms. Résolu en ajoutant les descriptions courtes à chaque niveau de l'arbre narratif, sans basculer vers du RAG sémantique.

  • Domain (core) : CampaignStructuralContext.ArcSummary gagne un champ description. ChapterSummary.sceneNames: List<String> remplacé par scenes: List<SceneSummary> avec name + description. ChapterSummary gagne également description.
  • Builder (application) : CampaignStructuralContextBuilder peuple maintenant arc.description, chapter.description, scene.description depuis les entités domaine (qui les exposent déjà — on consommait juste les noms).
  • Pont Java ↔ Python : BrainAiChatClient sérialise les nouveaux champs. Côté Python : models.py gagne la dataclass SceneSummary et les champs description ; main.py ajoute SceneSummaryDTO + helper _to_campaign_context mis à jour.
  • System prompt (chat.py) : _format_arcs et _format_chapter_block ajoutent une ligne Synopsis : … / Description : … sous chaque nœud quand renseigné. Format conditionnel (pas de ligne vide si description absente).
  • Budget tokens : ~30 tokens par scène × 100 scènes ≈ 3k tokens. Confortable. Si un jour une campagne explose ce budget, on basculera en Option C (RAG sémantique).
  • Validation finale : mvn clean compile BUILD SUCCESS + python -m py_compile sur les 3 fichiers Python + npx tsc --noEmit 0 erreur.
Étape b6 — IA dans la création de page (wizard) (20 avril 2026, nuit)

Mode wizard livré. Sur page-create, bouton " Créer avec l'IA" à côté du "Créer la page" classique. Au clic, le drawer chat s'ouvre avec un prompt système contextualisé au template (nom + liste exacte des champs + règles de cohérence) qui force l'IA à terminer chaque réponse par un bloc JSON <values>{...}</values>. L'utilisateur dialogue jusqu'à être satisfait puis clique "Appliquer et créer la page" → la page est créée en 2 étapes (POST coquille + PUT values) et navigation vers l'édition.

b6.1 — Flux côté page-create
  • Bouton " Créer avec l'IA" ajouté entre "Annuler" et "Créer la page". Désactivé tant que titre + template + dossier ne sont pas renseignés (même canSubmit que le bouton classique).
  • Au clic : le drawer s'ouvre (chatOpen = true) avec un welcomeMessage contextualisé : "Super, on va créer une page 'PNJ' ! Décrivez-la-moi en quelques mots…" (généré dynamiquement à partir du nom du template choisi).
  • Création en 2 étapes (POST puis PUT) pour appliquer les values — le backend POST /api/pages n'accepte pas encore values en payload. Choix pragmatique (zéro modification backend) documenté dans le code.
  • Erreurs gérées : "L'assistant n'a pas encore répondu", "Impossible d'extraire les valeurs", "Page créée mais impossible d'appliquer les valeurs". Affichées en banner rouge sous le formulaire.
b6.2 — Parsing JSON de la réponse assistant
  • Le system prompt wizard (construit côté Angular dans page-create.component.ts getter wizardSystemPrompt) demande à l'IA de terminer CHAQUE réponse par un bloc <values>{...}</values> avec les clés exactes du template.
  • Parsing côté Angular : regex /<values>\s*([\s\S]*?)\s*<\/values>/i + JSON.parse avec try/catch + coercion des valeurs non-string en string.
  • Chaque fin de réponse assistant alimente lastWizardReply via le nouveau @Output() assistantReply du drawer.
b6.3 — Évolutions du drawer (réutilisable)
  • Nouveau @Input() systemPromptAddon: string | null — injecté comme message role: 'system' invisible côté UI en tête du payload envoyé au backend à chaque tour. Permet au parent de contextualiser la conversation sans polluer l'historique visuel.
  • Nouveau @Output() assistantReply = EventEmitter<string>() — émis à chaque complétion d'un message assistant. Le parent l'utilise pour extraire le bloc <values> du wizard.
  • Le composant reste unique (pas de composant "wizard" séparé) : la config se fait entièrement via inputs/outputs. SRP respecté (le drawer ne connaît rien du wizard, juste du chat).
Étape b7 — Anti-hallucination (20 avril 2026, nuit)

Nuance centrale encodée : l'IA peut (et doit) inventer des éléments originaux, mais ne peut pas faire référence à des éléments du Lore comme s'ils existaient si on ne les lui a pas montrés. Appliqué via température abaissée + system prompts durcis.

b7.1 — Température configurable par use case
  • LLMProvider.generate() et LLMChatProvider.stream_chat() enrichis d'un kwarg temperature: float | None = None dans brain/app/domain/ports.py (docstring explicite la recommandation LoreMind).
  • OllamaLLMProvider propage via options.temperature dans les payloads /api/generate et /api/chat (la clé options est la convention Ollama pour les hyperparamètres).
  • GeneratePageUseCase : constante _DEFAULT_TEMPERATURE = 0.4 (remplissage factuel, peu créatif).
  • ChatUseCase : constante _DEFAULT_TEMPERATURE = 0.7 (conversation créative mais cohérente).
b7.2 — System prompts durcis
  • ChatUseCase._BASE_SYSTEM : section "Règles de cohérence (IMPORTANT)" ajoutée avec la nuance centrale ( inventer OK, référencer l'inexistant KO, dates/chiffres précis inventés).
  • GeneratePageUseCase._SYSTEM_INSTRUCTIONS : même section, adaptée au remplissage factuel. Invite explicitement à rester vague ("il y a longtemps", "un bourg voisin") quand une précision externe manque plutôt que d'inventer.
b7.3 — DTO temperature optionnel — REPORTÉ au backlog
  • Exposition du paramètre dans les DTOs Pydantic /generate-page et /chat/stream (override depuis Java/front). Non fait volontairement — YAGNI tant que personne ne demande l'override. Les constantes des use cases suffisent. À ajouter quand un slider "créativité" apparaîtra côté UI (backlog b4.6).
Étape b8 — Contextualisation page courante (serveur) (20 avril 2026, nuit)

PageContext injecté côté backend. Sur page-edit, le drawer transmet le pageId → le Core charge la Page + son Template et construit un PageContext (titre, template, champs, valeurs actuelles) → envoyé en page_context au Brain Python → injecté dans le system prompt comme bloc "PAGE EN COURS D'ÉDITION" avec instruction de focalisation exclusive. L'IA ne déborde plus sur d'autres pages/templates.

b8.1 — Brain Python
  • Nouveau dataclass PageContext dans domain/models.py (title, template_name, template_fields, values).
  • ChatUseCase.stream() accepte un page_context: PageContext | None = None optionnel. Rétro-compat totale (sans argument = comportement b5).
  • _build_system_prompt ajoute un bloc "--- PAGE EN COURS D'ÉDITION ---" quand page_context est fourni, listant titre + template + champs + valeurs actuelles + instruction de focalisation exclusive.
  • DTO Pydantic PageContextDTO + champ optionnel page_context: PageContextDTO | None = None sur ChatStreamRequestDTO. Mapping DTO → domain dans main.py.
b8.2 — Core Java
  • Nouveau value object PageContext dans domain/generationcontext/ (parallèle architectural de LoreStructuralContext). Lombok @Value/@Builder.
  • ChatRequest enrichi d'un champ pageContext nullable (JavaDoc explicite le "null = chat générique").
  • StreamChatForLoreUseCase.execute() prend désormais un pageId nullable. Nouvelle méthode privée buildPageContext(pageId) charge Page + Template via les repos existants (zéro nouveau port). Gestion des pages orphelines (template absent → PageContext minimal sans champs, pas d'exception).
  • BrainAiChatClient.toPayload() sérialise page_context en snake_case uniquement si fourni (payload léger par défaut).
  • DTO ChatStreamRequestDTO gagne un pageId optionnel + AiChatController le propage au use case.
b8.3 — Angular
  • AiChatService.streamChat(loreId, messages, pageId?) : nouveau 3ᵉ argument optionnel, inclus dans le payload JSON uniquement s'il est truthy.
  • AiChatDrawerComponent gagne un @Input() pageId: string | null = null propagé au service.
  • page-edit.component.html passe [pageId]="pageId" au drawer — le composant avait déjà cet ID (depuis la route).
  • page-create (wizard b6) volontairement ne passe pas de pageId : la page n'existe pas encore. Le wizard continue de fonctionner via son systemPromptAddon sans aucune modification — les deux mécanismes cohabitent proprement.
b7.4 — Fiche academy anti-hallucination.md
  • Théorie : pourquoi les LLM hallucinent (prédiction probabiliste sans vérification).
  • Analogie JDR : le "MJ qui improvise" — improvisation créative (OK, nouveau PNJ) vs improvisation incohérente (KO, référence à un PNJ inexistant comme session 2).
  • Les 5 leviers détaillés (température, prompt strict, contexte riche, modèle plus gros, chain-of-thought) avec tableau coût/effet.
  • Application concrète à LoreMind : ce qu'on a retenu (1+2+3), ce qu'on garde en backlog (4+5).
  • Section "Le savais-tu ?" sur le fait que temperature=0 n'est pas 100% déterministe (parallélisme GPU).
  • Quiz 5 QCM.
Dette technique Brain (non bloquante, à reprendre plus tard)
  • Client httpx réutilisé via FastAPI lifespan — actuellement un nouveau client est créé à chaque requête dans OllamaLLMProvider.generate. Impact : pool de connexions perdu entre requêtes. À corriger quand le débit augmente.
  • Tests pytest : créer un FakeLLMProvider et tester GeneratePageUseCase en isolation. L'hexagonal a été mis en place précisément pour ça — il serait dommage de ne pas en tirer parti.
  • Logging structuré (loguru ou logging standard avec JsonFormatter) à la place des prints implicites pour faciliter le debug en conditions réelles.
  • Endpoint GET /info exposant le modèle actuellement configuré (utile pour diagnostiquer "ce que voit le serveur" sans SSHer dans le container).
  • Validation Pydantic plus stricte : max_length sur prompt, max_items sur template_fields (ex: 20 max), longueur du page_title.
  • Gestion output_format autres que "json" : aujourd'hui on passe la valeur brute à Ollama. Si le Brain doit supporter un adapter qui ne comprend que certains formats, valider côté port.

Feature "Illustrations & images" (20-21 avril 2026, sessions 5 & 6)

Feature complète livrée en 6 étapes. Upload d'images via MinIO (S3-compatible), galeries éditables sur Arc/Chapter/Scene, et support d'un nouveau type IMAGE dans les champs de Template → les Pages peuvent porter des galeries par champ en plus des textes. Synchro Brain Python pour que l'IA "sache" combien d'illustrations porte chaque entité narrative (sans jamais recevoir les binaires).

Étape 1 — Shared Kernel images + MinIO (2026-04-20 sess.5)

Backend Java pur, testable via curl. Aucune intégration métier à ce stade.

  • Infrastructure : docker-compose.yml (service minio + minio-init auto-création du bucket loremind-images) ; core/pom.xml + io.minio:minio:8.5.11 ; application.properties (config minio.* + multipart 10 Mo).
  • Domaine : Image (VO) + ports ImageRepository et ImageStorage séparés (SRP : la métadonnée DB et le binaire objet-storage sont deux responsabilités distinctes).
  • Application : ImageService (validation MIME jpeg/png/webp/gif, taille max 10 Mo).
  • Adapters : MinioConfig + MinioImageStorageAdapter (binaire) ; ImageJpaEntity + PostgresImageRepository (métadonnée).
  • REST : ImageControllerPOST /api/images (multipart), GET /api/images/{id}, GET /api/images/{id}/content (proxy binaire), DELETE /api/images/{id}.
  • Academy : docs/academy/object-storage.md + docs/academy/shared-kernel.md.
  • Validation : mvn compile OK.

Étape 2 — Composants Angular partagés (2026-04-20 sess.5)

Deux composants autonomes, réutilisables partout où une galerie d'images est nécessaire.

  • web/src/app/services/image.service.ts : upload, getById, delete, contentUrl.
  • app-image-uploader (shared/image-uploader/) : drop-zone standard OU mode compact (bouton + ajouter pour galerie). Validation client alignée serveur. Gestion 413.
  • app-image-gallery (shared/image-gallery/) : grille 120×120 lazy-loading, mode editable avec uploader compact intégré + bouton X par vignette (supprime serveur + émet nouvelle liste), lightbox plein écran au clic.
  • Validation : npx tsc --noEmit OK.

Étape 3 — Illustrations sur Scene / Chapter / Arc (2026-04-20 sess.5)

Première intégration métier : les 3 entités narratives portent une liste d'images.

  • Backend : List<String> illustrationImageIds ajouté sur Arc/Chapter/Scene (domaine + JPA avec converter JSON + DTO + Mapper + Postgres repo + Service).
  • Frontend : champ dans campaign.model.ts ; section "Illustrations" en tête des *-view (lecture) et *-edit (galerie éditable) pour les 3 entités.
  • Validation : mvn compile + npx tsc --noEmit OK.

Étape 4 — Refactor Template.fields (2026-04-21 sess.6)

Pivot structurel : un champ de template n'est plus juste un nom — il a un type. Prépare l'étape 5 (Pages avec champs IMAGE).

  • Backend : nouveau enum FieldType { TEXT, IMAGE } + VO TemplateField(name, type). Template.fields devient List<TemplateField> + helper textFieldNames() (utilisé par les use cases IA qui ne savent traiter que du texte).
  • Migration BDD transparente : TemplateFieldListJsonConverter lit l'ancien format ["name", ...] ET le nouveau [{name, type}], écrit toujours au nouveau format → auto-migration à la première sauvegarde (pas de script SQL).
  • Tolérance : TemplateFieldMapper traite un type inconnu → TEXT (robuste face à une régression DTO).
  • Use cases IA mis à jour : GeneratePageValuesUseCase et StreamChatForLoreUseCase ne passent à l'IA que les champs TEXT (erreur claire si aucun).
  • Frontend : sélecteur de type dans template-create/edit, chip verte (TEXT) vs indigo (IMAGE), bouton toggle inline. page-view rend un placeholder pour les champs IMAGE (la vraie UI vient en étape 5). page-edit hydrate les TEXT uniquement, page-create wizard ne liste que les TEXT.
  • Validation : mvn compile + npx tsc --noEmit OK.

Étape 5 — Support champs IMAGE dans Pages (2026-04-21 sess.6)

Les Pages gagnent une seconde zone de stockage, parallèle à values (TEXT).

  • Backend : nouveau converter StringListMapJsonConverter (Map<String, List<String>> ↔ JSON). Page.imageValues ajouté avec helpers setImageFieldValue / getImageFieldValue. Nouvelle colonne image_values_json sur PageJpaEntity. Propagation dans DTO + Mapper + Service.
  • Frontend : Page.imageValues?: Record<string, string[]> ; page-view affiche une galerie readonly par champ IMAGE via ImageGalleryComponent ; page-edit hydrate séparément TEXT et IMAGE et rend une galerie éditable par champ IMAGE.
  • Validation : mvn compile + npx tsc --noEmit OK.

Étape 6 — Brain Python : synchro DTOs (2026-04-21 sess.6)

L'IA ne reçoit pas les binaires — juste un signal de présence (illustration_count) pour qu'elle puisse en tenir compte dans le prompt.

  • Backend Java : illustration_count ajouté sur ArcSummary / ChapterSummary / SceneSummary du CampaignStructuralContext. Le builder peuple depuis getIllustrationImageIds() (null-safe). BrainAiChatClient sérialise uniquement si > 0 (payload léger pour une campagne sans images).
  • Brain Python : illustration_count: int = 0 sur les 3 summaries dans domain/models.py ; DTOs Pydantic + _to_campaign_context mis à jour dans main.py. Les champs inconnus (ex: illustrationImageIds des Pages) sont silencieusement ignorés par Pydantic v2 (pas d'erreur).
  • Prompt : helper _illustration_hint() dans chat.py ; les lignes arc/chapter/scene du prompt affichent [N illustrations] si présentes (ex: - A (arc) [2 illustrations]).
  • Validation : mvn compile + python -m py_compile OK + démo runtime prompt validée.

Résidu de scope (non bloquant)

  • PageSummary (côté LoreStructuralContext) ne porte pas encore de signal sur les imageValues des Pages. Symétrique à faire si l'IA doit raisonner sur "cette page PNJ a 3 portraits".

Notes transverses

  • Docker-compose ne couvre aujourd'hui QUE MinIO. Postgres/Brain/Web restent lancés à la main.
  • Séparation ImageRepository / ImageStorage volontaire (SRP, pattern Shared Kernel).
  • URL publique d'une image : /api/images/{id}/content (proxy Java — évite d'exposer MinIO directement).
  • Validation MIME côté ImageService : jpeg/png/webp/gif uniquement, max 10 Mo.

Structure des dossiers

LoreMind/
├── core/                    # Backend Java
│   ├── src/main/java/com/loremind/
│   │   ├── domain/          # Entités de domaine (DDD)
│   │   │   ├── lorecontext/
│   │   │   ├── campaigncontext/
│   │   │   └── generationcontext/
│   │   ├── application/     # Use Cases
│   │   └── infrastructure/   # Adaptateurs (JPA, REST)
│   └── pom.xml
├── web/                     # Frontend Angular
│   ├── src/app/
│   │   ├── shared/          # Composants partagés (Sidebar)
│   │   ├── campaigns/       # Module Campaigns
│   │   ├── lore/            # Module Lore
│   │   └── services/        # Services HTTP
│   └── package.json
├── brain/                   # Backend Python (IA)
│   ├── app/
│   │   ├── core/
│   │   ├── domain/
│   │   ├── infrastructure/
│   │   └── api/
│   └── requirements.txt
└── docs/                    # Documentation
    ├── academy/             # Documents pédagogiques
    ├── loremind-contexte.md # Contexte du projet
    └── plan.md              # Ce fichier

Configuration prévue

Base de données PostgreSQL

  • URL : jdbc:postgresql://localhost:5432/loremind
  • Username : ietm64
  • Password : REDACTED

Serveurs

Notes importantes

  • Le Backend Java suit strictement l'Architecture Hexagonale
  • Le Frontend Angular utilise des services HTTP pour communiquer avec le Backend
  • L'IA sera configurée avec Ollama en local (pas d'API OpenAI pour le moment)
  • Les templates PostgreSQL utiliseront le type JSONB pour la flexibilité

Points à surveiller / Dette technique connue

Ces points sont à garder en tête pour de futures refactorisations. Pas bloquant aujourd'hui — à traiter quand le besoin se manifestera.

  • Champ illustration manquant sur Arc/Chapter/Scene Résolu le 20 avril 2026 (feature "Illustrations & images" étape 3 — champ devenu illustrationImageIds: List<String> avec galerie MinIO).
  • Chargement de l'arbre campagne via N+1 requêtes HTTPcampaign-tree.helper.ts fait 1 arcs + N chapters + M scenes appels HTTP en forkJoin. Correct pour des campagnes de taille modeste. À remplacer par un endpoint agrégé GET /api/campaigns/:id/tree quand les volumes l'exigeront (>20 chapitres par ex).
  • Calcul de order naïf — actuellement order = existingCount + 1. Ne gère pas les réordonnancements ni les suppressions (risque de collisions). À reprendre avec un pattern de fractional indexing ou recalcul côté backend.
  • Duplications de pattern côté Lore Résolu le 18 avril 2026 via lore-sidebar.helper.ts — utilisé par lore-detail, lore-node-create, template-create, template-edit. L'écran de création de page à venir suivra le même pattern.
  • Harmonisation du style bouton primaire Résolu le 18 avril 2026 sur les 6 composants pattern A (arc/chapter/scene × create/edit). Partials créés : web/src/styles/_buttons.scss et _forms.scss. 572 → 120 lignes (-79%). Reste la unification pattern A / pattern B (voir entrée dédiée ci-dessous).
  • Deux design systems cohabitent — Pattern A (#1f2937 / radius 8px / padding 0.75rem, utilisé par arc/chapter/scene/campaign/lore-create) vs Pattern B (#1a1a2e / radius 6px / padding 0.7rem, utilisé par template/page/lore-node). C'est un vrai choix de design à faire, pas juste de la dette. Option : garder A pour les écrans "campagne simples" et B pour les écrans "Lore premium". Option alternative : unifier sur un seul pattern.
  • Fusion create + edit — les composants *-create et *-edit partagent ~80% du code (form, layout, soumission). À fusionner en un seul composant par entité avec un flag mode: 'create' | 'edit' OU router-data. Pas urgent : YAGNI tant que les 2 écrans restent simples.
  • Breadcrumb Résolu le 19 avril 2026. Composant réutilisable app-breadcrumb créé dans web/src/app/shared/breadcrumb/. Intégré dans page-edit. Reste à intégrer sur arc-edit, chapter-edit, scene-edit si nécessaire (le composant est prêt).
  • Modale de confirmation personnalisée — la suppression utilise confirm() natif. À remplacer par une modale Angular cohérente avec la charte graphique.
  • Gestion des migrations DB — actuellement spring.jpa.hibernate.ddl-auto=update (auto-alter). Acceptable en dev, inutilisable en prod (perte de données possible). À remplacer par Flyway ou Liquibase avant la mise en prod (chaque changement de schéma devra être versionné en fichier SQL).

Dernière mise à jour

21 avril 2026 (session 6) — Feature "Illustrations & images" complète (6 étapes) : MinIO + galeries Arc/Chapter/Scene + refactor Template.fields avec types TEXT/IMAGE + champs IMAGE sur Pages + synchro Brain Python (illustration_count). Voir section dédiée au-dessus de "Structure des dossiers".

20 avril 2026 (soir, session 4) — Split View ↔ Edit : mode consultation livré sur Page / Arc / Chapter / Scene.

Problème UX remonté par l'utilisateur : consulter et modifier partageaient le même écran (formulaire avec textareas), ce qui est bruité visuellement pour la simple lecture et impose des scrollbars internes à chaque champ. Résolu par un pattern classique read-first design : une route de vue distincte par entité, où chaque champ est un bloc titré dont le corps s'étend verticalement selon son contenu (white-space: pre-wrap, pas de textarea).

Choix produit retenus :

  • Routes séparées : /lore/:id/pages/:pid = vue (défaut, bookmarkable), /lore/:id/pages/:pid/edit = édition. Idem pour Arc/Chapter/Scene côté Campagne.
  • Style "fiche de jeu" : chaque champ = bloc avec titre (petit, violet #a5b4fc, uppercase, tracking) et corps texte pleine largeur. Séparateurs fins #1e1e3a entre blocs. Variante --private rouge discret pour les notes MJ.
  • Tout en une passe : les 4 entités sont livrées ensemble pour garder un design system cohérent.

Architecture mise en place :

  • Nouveau partial SCSS global web/src/styles/_view.scss (+ @use dans styles.scss) — responsabilité unique : le style "fiche de jeu". Réutilisé par les 4 composants (DRY). Contient .view-page, .view-header, .view-section, .view-section--private, .view-row (grille 2 colonnes), .view-chips (+ variante .view-chip--tag).
  • 4 nouveaux composants standalone : @app/lore/page-view/, @app/campaigns/arc-view/, @app/campaigns/chapter-view/, @app/campaigns/scene-view/. Chacun charge les mêmes données que son pendant -edit (même sidebar, mêmes services) — le mode est juste cosmétique.
  • Les 4 SCSS des composants -view sont volontairement quasi-vides : tout le style vient du partial global. Laissés en place pour cohérence structurelle avec le reste du projet.

Routes (app.routes.ts) :

  • /lore/:loreId/pages/:pageIdPageViewComponent (ancien PageEditComponent).
  • /lore/:loreId/pages/:pageId/editPageEditComponent (nouvelle).
  • /campaigns/:campaignId/arcs/:arcIdArcViewComponent (+ /editArcEditComponent). Idem chapters et scenes.
  • Les routes des tree items de sidebar pointent déjà vers /…/:id (pas modifié) → par construction, cliquer sur un item de l'arbre ouvre la vue (défaut). Parfait.

Flux navigationnels ajustés :

  • arc-edit / chapter-edit / scene-edit : cancel() et redirection post-Sauvegarder pointent maintenant sur /view de l'entité courante (au lieu de la racine Campagne). UX : « je corrige, je valide, je vois le résultat ».
  • page-edit : bouton Annuler (btn-secondary) ajouté dans le header + save() navigue vers la vue.
  • arc-create / chapter-create / scene-create : captent désormais (created) et naviguent vers la vue de la nouvelle entité (au lieu de la racine Campagne).
  • page-create (mode classique) : navigue vers /edit de la page créée — la coquille est vide, filer directement en édition fait sens. Le mode wizard IA (b6) continue à naviguer vers la vue (les values sont déjà remplies par l'IA).

Rendu adaptatif des champs (sans scrollbar) :

  • Le texte est rendu dans un <p class="view-section-body"> natif, jamais dans un textarea. CSS : white-space: pre-wrap; word-wrap: break-word; — conserve les sauts de ligne saisis en édition, la hauteur s'adapte automatiquement au contenu. Zéro JS, zéro rows="…" à maintenir.
  • Champs vides : <p class="view-section-empty">Non renseigné</p> en italique gris discret. Évite de masquer les champs manquants sans pour autant encombrer visuellement.
  • Sections entièrement optionnelles (tags, pages liées, notes privées, combat, choix…) affichées uniquement si non-vides (*ngIf sur la section entière).

Validation : npx tsc --noEmit — 0 erreur.

À surveiller / ce qu'il reste à faire :

  • La duplication entre arc-view / chapter-view / scene-view reste acceptable car chaque entité a des champs différents. Si le domaine narratif continue à grossir, on pourra extraire un composant générique <app-entity-view [sections]="…"> piloté par un tableau de sections — YAGNI tant qu'on en reste à 3 entités.
  • template-edit, lore-node-edit, campaign-detail, lore-detail gardent leur format actuel (mix consultation/édition inline) — c'est volontaire car ces écrans sont très simples (nom + description). À reconsidérer si leur scope grossit.
  • Raccourci clavier Ctrl+E pour basculer vue ↔ édition = idée backlog.

20 avril 2026 (soir, session 3) — Phase 3 étape b9 bouclée : enrichissement du Structural Context Lore (values + tags + liens).

Problème remonté par l'utilisateur : depuis un arc/chapter/scene, le chat IA ne voyait que les noms des pages du Lore (ex: "Borin le forgeron"), jamais leur contenu. Il ne pouvait donc pas raisonner sur les fiches de PNJ (apparence, motivations, background) ni sur les interconnexions (tags, pages liées). Résolu par un enrichissement symétrique à celui fait en b5.8 pour les scènes de campagne.

  • Domain (core) : LoreStructuralContext.FolderPage renommé PageSummary (Ubiquitous Language : c'est un résumé projeté, pas un conteneur). Nouveaux champs values: Map<String,String>, tags: List<String>, relatedPageTitles: List<String>.
  • Builder (application) : LoreStructuralContextBuilder peuple les nouveaux champs. Constante MAX_VALUE_LENGTH = 500 + méthode privée truncate() pour éviter qu'un champ "Histoire" de 5000 caractères ne sature le prompt. Les relatedPageIds sont résolus en titres via une map pageTitleById construite une seule fois (pas de N²). Les IDs qui ne matchent rien (page supprimée) sont silencieusement ignorés.
  • Pont Java ↔ Python : BrainAiChatClient.pageSummaryToMap() sérialise les nouveaux champs en snake_case, seulement s'ils contiennent de l'info (payload léger pour un Lore avec beaucoup de pages vierges).
  • Python (domain) : nouveau dataclass PageSummary (title, template_name, values, tags, related_page_titles). LoreStructuralContext.folders passe de dict[str, list[tuple[str, str]]] à dict[str, list[PageSummary]].
  • Python (DTOs + mapping) : FolderPageDTO renommé PageSummaryDTO avec champs optionnels par défaut vide. Nouveau mapper _to_page_summary(). Mapping _to_lore_context() mis à jour.
  • System prompt (chat.py) : _format_folders affiche pour chaque page une fiche indentée avec les valeurs des champs, les tags, et les pages liées (uniquement si non-vide — prompt compact pour les pages vierges). Format :
    - PNJ (dossier)
        - Borin le forgeron [template: PNJ]
            · Apparence : Nain barbu au regard perçant…
            · Motivation : Venger son clan décimé…
            · tags : aventurier, forgeron
            · liée à : Le marteau de Durin, Clan Feuillefer
    
  • Budget tokens : ~150-200 tokens par page pleine. Tient jusqu'à ~50-100 pages dans un prompt typique. Au-delà, bascule vers RAG sémantique (Option D, backlog).
  • Validation finale : mvn -q compile BUILD SUCCESS + python -m py_compile sur les 3 fichiers Python.
  • Effet collatéral bénéfique : cet enrichissement profite AUSSI au chat depuis la Lore (pas uniquement depuis la Campagne) — l'IA voit désormais le contenu de toutes les autres pages du Lore, pas seulement leurs noms.
  • num_ctx porté à 16384 : sans ça, Ollama tronque silencieusement le prompt à 2048 tokens par défaut (~10 pages enrichies max). Nouveau setting llm_num_ctx: int = 16384 dans brain/app/core/config.py, surchargeable via LLM_NUM_CTX dans .env. Méthode privée _build_options() factorisée dans OllamaLLMProvidernum_ctx est TOUJOURS injecté dans les deux payloads (/api/generate et /api/chat). Coût VRAM supplémentaire : ~600 MB de KV cache max vs défaut 2048.

20 avril 2026 (nuit, session 2) — Phase 3 étape b8 bouclée : contextualisation page courante injectée côté serveur.

PageContext serveur (b8.1 → b8.3) :

  • Python : nouveau dataclass PageContext, ChatUseCase.stream() accepte un param optionnel, system prompt gagne un bloc "PAGE EN COURS D'ÉDITION" avec instruction de focalisation exclusive. DTO Pydantic ajouté.
  • Java : value object PageContext symétrique au LoreStructuralContext. StreamChatForLoreUseCase accepte un pageId nullable, charge Page + Template via les ports existants (zéro nouveau port). BrainAiChatClient sérialise en snake_case. DTO + controller propagent.
  • Angular : service + drawer enrichis d'un pageId? optionnel. page-edit le passe au drawer. Le wizard de page-create ne le passe PAS (page inexistante) — les deux mécanismes (systemPromptAddon pour wizard, pageId pour édition) cohabitent proprement.
  • Résout le bug "l'IA propose des idées pour d'autres templates" : maintenant l'IA reçoit explicitement le template + ses champs + les valeurs actuelles de la page éditée, avec injonction de ne pas déborder.

20 avril 2026 (nuit) — Phase 3 étapes b6 + b7 bouclées : wizard création de page + anti-hallucination.

Wizard création de page (b6.1 → b6.3) :

  • page-create : bouton " Créer avec l'IA" à côté du "Créer la page" classique. Au clic, drawer chat en mode wizard avec system prompt contextualisé au template choisi (nom + champs exacts + règle du bloc <values> obligatoire en fin de réponse).
  • Drawer enrichi : nouveau @Input() systemPromptAddon (message role:'system' invisible préfixé au payload à chaque tour) + @Output() assistantReply (émis à chaque complétion assistant, alimente le parsing du wizard).
  • Parsing <values> : regex + JSON.parse côté Angular, fallback gracieux sur erreur avec messages explicites.
  • Création en 2 étapes (POST coquille + PUT values) — choix pragmatique : zéro modification backend nécessaire.

Anti-hallucination (b7.1 → b7.4) :

  • Température différenciée par use case : 0.4 pour le one-shot factuel, 0.7 pour le chat créatif. Paramètre temperature ajouté aux ports LLMProvider + LLMChatProvider, propagé via options.temperature dans les payloads Ollama.
  • System prompts durcis avec la nuance clé ( inventer des éléments originaux = OK, référencer comme existant un élément absent de la carte = KO). Prompt wizard côté Angular + prompts chat/one-shot côté Python Brain.
  • Academy : fiche docs/academy/anti-hallucination.md avec analogie JDR (le MJ qui improvise bien vs mal), 5 leviers classés coût/effet, application concrète à LoreMind, quiz 5 QCM.
  • DTO temperature optionnel reporté au backlog (YAGNI tant qu'aucune UI d'override n'existe).

19 avril 2026 (soir, session 3) — Phase 3 étape b5 bouclée : chat IA conversationnel streamé + Structural Context.

Chat IA conversationnel (b5.1 → b5.6) :

  • Python (b5.1) : endpoint POST /chat/stream (SSE), port LLMChatProvider (ISP), use case ChatUseCase avec injection du Structural Context dans le system prompt.
  • Java domaine/adapter (b5.2) : port AiChatProvider par callbacks (choix pédagogique pour éviter Reactor dans le domaine), adapter BrainAiChatClient via WebClient (ajout de spring-boot-starter-webflux au pom.xml + spring.main.web-application-type=servlet pour rester en Tomcat).
  • Java REST (b5.3) : AiChatController expose POST /api/ai/chat/stream, streaming dans AsyncTaskExecutor, SseEmitter thread-safe, use case StreamChatForLoreUseCase qui charge Lore+nodes+pages+templates pour construire le Structural Context.
  • Angular (b5.4) : service AiChatService avec fetch() + ReadableStream (pas EventSource qui ne supporte que GET), composant standalone réutilisable AiChatDrawerComponent avec bulles user/assistant, typing indicator, caret clignotant, suggestions rapides, primaryAction optionnelle.
  • Intégration page-edit (b5.5) : bouton "Assistant IA" toggle le drawer, one-shot b4 relocalisé en primaryAction ("Remplir automatiquement"). Suggestions rapides hardcodées MVP.
  • Academy (b5.6) : fiche docs/academy/streaming-sse-rag.md avec analogie JDR (pigeon voyageur), comparaison Full-dump/Structural/RAG sémantique, code des 3 étages, quiz 5 QCM.
  • Extension Campagne (b5.7, 20 avril après-midi) : drawer branché sur arc-edit, chapter-edit, scene-edit. Asymétrie respectée (Campagne voit son Lore, Lore ne voit PAS ses campagnes). 4 contextes optionnels côté prompt (lore_context, page_context, campaign_context, narrative_entity). Extraction du shared LoreStructuralContextBuilder (DRY). Persistance des conversations restera pour plus tard.

19 avril 2026 (soir, session 2) — Phase 3 étape b4 bouclée : chaîne IA de bout en bout opérationnelle.

Branchement Core Java ↔ Brain Python (b4.1 → b4.5) :

  • Domaine (b4.1) : 3ᵉ Bounded Context generationcontext créé (GenerationContext, GenerationResult, port AiProvider, AiProviderException). Zéro dépendance technique.
  • Adapter HTTP (b4.2) : BrainAiClient + DTOs package-private snake_case + RestTemplateConfig (timeout 120s). Choix assumé de RestTemplate plutôt que WebClient pour la simplicité de lecture. Config brain.base-url + brain.timeout-seconds dans application.properties.
  • Use case (b4.3) : GeneratePageValuesUseCase orchestre LoreContext (chargement Page/Template/Lore/LoreNode) et GenerationContext (appel IA). 5 ports injectés, 0 Adapter référencé. Zéro persistance assumée.
  • REST (b4.4) : POST /api/pages/{id}/generate dans un PageGenerationController dédié (SRP vs colocation dans PageController). Gestion d'erreurs : 200/404/422/502/500.
  • Frontend (b4.5) : PageService.generateValues() + état aiLoading/aiError dans page-edit + merge soft (écrase sur suggestion non-vide, préserve sur vide) + banner d'erreur dismissable.
  • Academy : nouvelle fiche docs/academy/bounded-context.md avec analogie JDR (3 mondes de règles), bénéfices, exemple appliqué et quiz 5 QCM.

19 avril 2026 (soir) — Phase 3 démarrée : Brain Python opérationnel jusqu'à b3.2.

Brain LoreMind (brain/) :

  • Squelette FastAPI + Swagger /docs (étape b1).
  • Architecture hexagonale complète (étape b2) : Port LLMProvider (Protocol PEP 544), adapter OllamaLLMProvider, controller fin, injection par Depends. Fiche academy docs/academy/hexagonal-python.md rédigée.
  • Génération structurée en cours (étape b3) : modèles de domaine PageGenerationContext / PageGenerationResult, port enrichi d'un kwarg output_format, use case GeneratePageUseCase avec prompt système français + parsing JSON défensif. Reste l'endpoint HTTP POST /generate-page (b3.3) pour exposer le use case.
  • Validation manuelle : gemma4:e2b répond en ~2s sur un prompt simple. Le modèle est swappable via .env (LLM_MODEL).

19 avril 2026 - Phase 5C en cours : compteurs + breadcrumb livrés.

Breadcrumb (fil d'Ariane) dans page-edit :

  • Nouveau composant réutilisable app-breadcrumb dans web/src/app/shared/breadcrumb/. Interface BreadcrumbItem { label, route? } — item sans route = position courante (non-cliquable, style différencié).
  • Séparateur visuel géré en CSS via ::before (pas de DOM supplémentaire).
  • Intégré dans page-edit au-dessus du header. Chemin construit dynamiquement par un getter breadcrumbItems qui remonte la hiérarchie de dossiers via parentId depuis les nodes déjà chargés pour la sidebar — zéro appel HTTP supplémentaire.
  • Le getter recalcule à chaque render (Angular change detection) : peu coûteux (dizaines de nœuds max), reste réactif si le nodeId change via le select "Dossier".
  • Composant prêt pour réutilisation sur arc-edit, chapter-edit, scene-edit (dette technique correspondante marquée résolue).
  • 5 fichiers touchés (3 nouveaux + page-edit TS/HTML).

19 avril 2026 - Phase 5C démarrée : compteurs de pages.

Compteurs de pages par dossier (sidebar) :

  • Nouveau champ meta?: string sur TreeItem (layout.service.ts) pour afficher un badge aligné à droite.
  • buildFolderItem dans lore-sidebar.helper.ts renseigne meta = nodePages.length (count direct, undefined si 0 — ne pas polluer visuellement).
  • Rendu dans secondary-sidebar.component.html via <span class="tree-item-meta"> + style SCSS margin-left: auto cohérent avec .panel-item-meta existant (DRY).
  • 4 fichiers touchés, ~9 lignes ajoutées.

Bug fix — compteurs home "Vos univers" affichaient 0 :

  • Cause : les champs Lore.nodeCount / pageCount étaient stockés en BDD mais les méthodes métier incrementNodeCount() / incrementPageCount() n'étaient jamais appelées par LoreNodeService ou PageService. Les compteurs restaient à 0 depuis la création.
  • Correction : bascule vers un calcul à la volée (Option B — source of truth = tables nodes/pages).
  • Ports enrichis : countByLoreId(String loreId) ajouté sur LoreNodeRepository et PageRepository.
  • JPA : long countByLoreId(Long loreId) en Spring Data query derivation (pas de @Query à écrire).
  • Adaptateurs Postgres : impl propagée avec conversion String → Long.
  • LoreService injecte les 2 ports supplémentaires et enrichit via une méthode privée withCounts(Lore) appliquée dans getAllLores() et getLoreById(). Anciennes colonnes node_count/page_count laissées en BDD (ignorées, à nettoyer plus tard).
  • N+1 assumé (1 find + 2 COUNT par Lore). Négligeable à petite échelle.
  • 7 fichiers touchés (2 ports, 2 JPA, 2 adaptateurs, 1 service).

18 avril 2026 (fin de soirée, après B2) - Cross-context Campagne ↔ Lore complet sur Arc + Chapter + Scene.

B2 — relatedPageIds sur Arc / Chapter / Scene :

  • Backend (18 fichiers touchés, 3 entités × 6 couches) :
    • Domaine : List<String> relatedPageIds ajouté avec @Builder.Default (jamais null) + méthodes métier linkPage(id) / unlinkPage(id) idempotentes et réutilisables.
    • Persistance JPA : nouvelle colonne related_page_ids en TEXT avec le converter existant StringListJsonConverter (pattern déjà utilisé pour Page + Template).
    • Repositories Postgres : propagation dans les deux sens (toDomainEntity / toJpaEntity) avec defensive copy (new ArrayList<>(src)) pour garantir qu'un changement côté domaine ne modifie pas accidentellement l'entité JPA.
    • DTO + Mapper : idem, propagation + defensive copy. Initialisation new ArrayList<>() pour que Jackson sérialise toujours un tableau JSON (jamais null).
    • Services : updateArc/Chapter/Scene déjà en Parameter Object (full entity) → une seule ligne ajoutée par service (entity.setRelatedPageIds(updated.getRelatedPageIds())).
  • Frontend (7 fichiers touchés) :
    • Modèles Arc/Chapter/Scene + *Create : champ relatedPageIds?: string[] ajouté.
    • arc-edit, chapter-edit, scene-edit : injection PageService, chargement conditionnel des pages du Lore via switchMap (si campaign.loreId est défini), binding bidirectionnel avec app-lore-link-picker. Le picker est caché avec un message explicatif si la campagne n'a pas de Lore associé.
    • scene-edit : le picker est dans une app-expandable-section avec l'icône 🔗 pour rester cohérent avec les 5 autres sections de la scène.
  • Principe DDD réaffirmé : aucune classe du Lore Context importée dans le Campaign Context. Seuls des IDs (String) voyagent. Le LoreLinkPickerComponent est le SEUL point où les deux contextes se rejoignent, et c'est côté UI — pas côté domaine.
  • Le app-lore-link-picker que j'avais introduit pour page-edit (Phase 5B) est désormais réutilisé à l'identique dans 3 nouveaux écrans → pari design payant, la composantisation a tenu ses promesses.

18 avril 2026 (soir, après dette technique) - Extraction SCSS + mise en valeur bouton retour sidebar.

Extraction SCSS partagés :

  • Nouveaux partials web/src/styles/_buttons.scss (.btn-primary, .btn-secondary, .btn-danger + modificateurs .btn-sm et .btn-icon) et _forms.scss (.field, .field-hint, .field-row, .page-header, .form-actions). Importés via @use dans styles.scss.
  • 6 composants pattern A nettoyés : arc-create/edit, chapter-create/edit, scene-create/edit. Réduction de 572 → 120 lignes (-79%, 452 lignes économisées). Seuls les overrides contextuels subsistent (ex: .btn-danger { margin-left: auto } pour pousser Supprimer à droite).
  • Composants pattern B (template, page, lore-node) volontairement non modifiés — ils utilisent un design différent (#1a1a2e / radius 6px). Ajouté comme nouvelle entrée dans la dette "Deux design systems cohabitent".
  • ⚠️ Piège Angular ViewEncapsulation rencontré : les sélecteurs CSS locaux ont une spécificité plus élevée que les globaux (ajout d'un attribut [_ngcontent-xxx]). Il fallait donc réellement supprimer les blocs dupliqués pour que les globaux s'appliquent — sinon ils étaient silencieusement ignorés.

Bouton retour sidebar (Tous les lores / Toutes les campagnes) :

  • Avant : simple libellé gris sans bordure, confondu avec les items de contexte.
  • Après : vrai bouton bordé avec icône ArrowLeft Lucide, centré, séparateur visuel au-dessus via ::before, hover marqué (bordure violet, texte blanc, flèche qui glisse à gauche en micro-interaction).
  • Principe UX appliqué : "affordance visuelle" — un bouton doit avoir l'air cliquable avant même qu'on le survole.

18 avril 2026 (soir, après B1) - Weak reference Campaign ↔ Lore + 4 corrections de bugs.

B1 — Lien optionnel Campaign ↔ Lore (cross-context weak reference) :

  • Backend : Campaign.loreId ajouté (nullable) avec méthodes métier linkToLore / unlinkFromLore / isLinkedToLore. Pas de @ManyToOne, pas de FK — c'est volontaire, les Bounded Contexts (Lore, Campaign) doivent rester indépendants. Normalisation ""null côté service. CampaignService refacto en Parameter Object (CampaignData).
  • Frontend : select "Univers associé" (optionnel, "— Aucun univers —") dans la modal de création de campagne. Badge cliquable 🌐 <nom du Lore> dans campaign-detail qui navigue vers le Lore. Cas dégradé "Univers introuvable" (rouge italique) si le Lore a été supprimé entre-temps.

4 bugs corrigés suite retour utilisateur :

  1. Pollution cross-lore (🔴 critique) — GET /api/lore-nodes?loreId=X ignorait le query param (pas d'annotation @RequestParam), renvoyait TOUS les dossiers de TOUS les lores. Les dossiers d'un Lore apparaissaient dans les autres. Pattern aligné sur TemplateController / PageController.
  2. Grille "Dossiers" plate dans lore-detail — les sous-dossiers apparaissaient au même niveau que leurs parents dans la grille principale. Fix : rootNodes = nodes.filter(n => !n.parentId). Les sous-dossiers restent accessibles via l'arbre de la sidebar.
  3. Switch entre Lores/Campaigns ne rechargeait pas — même bug que le page-edit précédent : route.snapshot.paramMap.get('id') lu une seule fois dans ngOnInit, alors qu'Angular réutilise le composant. Fix : subscription à route.paramMap avec comparaison à l'id courant, appliqué à lore-detail ET campaign-detail (préventif).
  4. Pas de bouton Modifier/Supprimer sur Lore et Campaign — nouveau mode édition inline dans le header des deux écrans détail. Rename + description (+ select Lore associé pour Campaign) + sauvegarde/annulation. Suppression protégée : refus si Lore contient encore des dossiers, ou si Campagne contient encore des arcs (message explicite avec le nombre). Pattern cohérent avec lore-node-edit.

Encore en attente (B2, ~24 fichiers) : relatedPageIds: List<String> sur Arc + Chapter + Scene avec app-lore-link-picker filtré sur campaign.loreId. User a demandé pause pour tester B1 avant d'attaquer B2.

18 avril 2026 (fin de journée, après Option A) - Phase 5B Pages livrée : Tags + Liens entre pages. Deux nouveaux composants réutilisables dans @app/shared/ : app-chips-input (tags génériques avec Entrée/virgule/Backspace) et app-lore-link-picker (autocomplete + chips cliquables pour navigation, conçu pour être réutilisé en Phase cross-context Campagne↔Lore). Intégration dans page-edit entre champs dynamiques et notes privées. allPages récupéré depuis la sidebar pour alimenter le picker.

18 avril 2026 (fin de journée) - Édition/suppression de dossier livrée. Nouveau LoreNodeEditComponent : renommage, changement d'icône, déplacement dans un autre parent (avec collectDescendantIds pour bloquer les cycles), suppression protégée (refus si non-vide, message explicite). LoreService enrichi (getLoreNodeById, updateLoreNode, deleteLoreNode). Chaque dossier de la sidebar est maintenant cliquable (label → édition, chevron → expand/collapse — la séparation des zones existait déjà). Route /lore/:loreId/folders/:folderId/edit.

18 avril 2026 (soir, après Pages) - UX Lore : renommage + icônes + dossiers imbriqués. Trois corrections groupées suite au retour utilisateur :

  • Renommage UI "noeud" → "dossier" (texte visible uniquement ; LoreNode reste le nom interne côté Java).
  • Bug corrigé : l'icône choisie à la création d'un dossier n'était jamais persistée ni affichée. Ajout du champ icon dans LoreNode (domaine + JPA + DTO + mapper + Postgres repository) + refacto LoreNodeService en Parameter Object. Frontend : nouveau registre partagé @app/lore/lore-icons.ts consommé à la fois par lore-node-create (grille de sélection) et par la sidebar (rendu dans l'arbre via TreeItem.iconKey).
  • Dossiers imbriqués activés : le backend supportait déjà parentId, seul le frontend ne l'exposait pas. Ajout d'un select "Dossier parent" dans lore-node-create, nouvelle route /lore/:loreId/folders/:parentId/create, helper lore-sidebar.helper.ts refactoré en construction récursive (fonction buildFolderItem) avec sous-dossiers + pages + actions "+ Nouveau dossier" / "+ Nouvelle page" par dossier.

18 avril 2026 (soir) - Phase 5A Pages livrée : domaine Page enrichi (suppression content: String, ajout loreId, values: Map<String,String>, notes, tags, relatedPageIds). Nouveau converter StringMapJsonConverter. Écrans page-create (fidèle maquette : titre + grille de templates + noeud auto-rempli) et page-edit basique (champs dynamiques du template rendus en textarea + notes privées). Helper lore-sidebar.helper.ts enrichi pour afficher les pages sous leur noeud dans l'arbre + actions "+ Nouvelle page" par noeud. Phases 5B (tags/liens), 5C (breadcrumb/compteurs) et 5D (Assistant IA) planifiées.

18 avril 2026 (matin) - Enrichissement du domaine Template (Lore) : loreId, defaultNodeId, List<String> fields avec converter JSON. Panneau "Templates" fidèle à la maquette ajouté en bas de la secondary sidebar (nouveaux types BottomPanel / BottomPanelItem). Écrans template-create et template-edit avec gestion dynamique des champs. Nouveau helper lore-sidebar.helper.ts.

Prochaines étapes prioritaires (Immédiat)

1. Compléter les Services d'application

Pourquoi ? Chaque entité de domaine a besoin de son service pour orchestrer les opérations métier selon l'Architecture Hexagonale.

Tâches :

  • LoreNodeService
  • PageService
  • TemplateService
  • ArcService
  • ChapterService
  • SceneService

2. Créer les DTOs et Mappers

Pourquoi ? Les DTOs isolent l'API REST des entités de domaine, protégeant le cœur métier des changements d'interface.

Tâches :

  • LoreContext DTOs (LoreDTO, LoreNodeDTO, PageDTO, TemplateDTO)
  • CampaignContext DTOs (CampaignDTO, ArcDTO, ChapterDTO, SceneDTO)
  • Mappers pour toutes les entités

3. Créer les REST Controllers

Pourquoi ? Expose l'API pour le Frontend Angular.

Tâches :

  • LoreController, CampaignController
  • LoreNodeController, PageController, TemplateController
  • ArcController, ChapterController, SceneController
  • Configuration CORS

4. Démarrer le Frontend Angular

Pourquoi ? Une fois le Backend fonctionnel, le Frontend peut consommer l'API.

Tâches :

  • Initialisation projet Angular
  • Layout de base avec Sidebar