maj_transcription

This commit is contained in:
Ladebeze66 2026-05-10 11:10:06 +02:00
parent ba34820ffc
commit 9ee64f04f2
71 changed files with 258 additions and 2561 deletions

View File

@ -14,7 +14,7 @@ Site portfolio **Next.js 15** + **Strapi 5** + **GrasBot** (FastAPI, Ollama, vau
| [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) | Opérationnel : ports, commandes, démarrage automatique, dépannage, pare-feu. | | [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) | Opérationnel : ports, commandes, démarrage automatique, dépannage, pare-feu. |
| [`vault-grasbot/README.md`](vault-grasbot/README.md) | Base de connaissances GrasBot (retrieval graph + BM25 v3). | | [`vault-grasbot/README.md`](vault-grasbot/README.md) | Base de connaissances GrasBot (retrieval graph + BM25 v3). |
**Obsidian / export** : le dossier [`obsidian-site-docs/`](obsidian-site-docs/) contient le hub, les commandes, **toute** la doc [`docs-site-interne/`](docs-site-interne/) (copie), `CONFIGURATION_SITE.md` et le README dépôt — prêt à ouvrir comme coffre ou à copier. Resynchro : [`obsidian-site-docs/SYNC-DOC.md`](obsidian-site-docs/SYNC-DOC.md). **Obsidian / export** : un coffre Obsidian optionnel (copie de la doc + commandes) peut vivre **hors dépôt** — ne pas sattendre à un dossier versionné à la racine ; synchroniser manuellement depuis [`docs-site-interne/`](docs-site-interne/) et [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) si besoin.
## Démarrage rapide (Windows) ## Démarrage rapide (Windows)
@ -60,7 +60,6 @@ my-next-site/
├── vault-grasbot/ # Connaissance (Obsidian) pour le retrieval ├── vault-grasbot/ # Connaissance (Obsidian) pour le retrieval
├── strapi_extraction/ # Extraction / build vault ├── strapi_extraction/ # Extraction / build vault
├── docs-site-interne/ # Doc technique détaillée ├── docs-site-interne/ # Doc technique détaillée
├── obsidian-site-docs/ # Pack Obsidian (résumés + commandes)
├── start-my-site.ps1 ├── start-my-site.ps1
├── stop-my-site.ps1 ├── stop-my-site.ps1
└── CONFIGURATION_SITE.md └── CONFIGURATION_SITE.md
@ -72,10 +71,8 @@ Projet personnel ; contenu et code sont fournis tels quels pour illustration du
--- ---
*Dernière révision du README : 2026-04 — aligné sur `docs-site-interne` et `CONFIGURATION_SITE.md`.* *Dernière révision du README : 2026-05 — aligné sur `docs-site-interne` et `CONFIGURATION_SITE.md`.*
Pour lancer tâches planifiées: taskschd.msc Pour lancer les tâches planifiées Windows : `taskschd.msc`
LANGFUSE_SECRET_KEY="sk-lf-7f3cbead-71eb-49b4-8f05-9b7ea57765aa" Les clés **Langfuse** (si utilisées) restent dans `llm-api/.env` — ne pas les commiter ; voir [`docs-site-interne/langfuse-observability.md`](docs-site-interne/langfuse-observability.md).
LANGFUSE_PUBLIC_KEY="pk-lf-1ed91915-97d3-4b6b-a903-dd8473b3efba"
LANGFUSE_BASE_URL="https://langfuse.fernandgrascalvet.com"

View File

@ -1,6 +1,6 @@
# Frontend Next.js # Frontend Next.js
**Dernière mise à jour :** 2026-04-28 **Dernière mise à jour :** 2026-05-10
## Stack ## Stack
@ -24,7 +24,7 @@
**Strapi — content-types concernés :** **Strapi — content-types concernés :**
- `competence` : `name`, `content` (richtext), `picture`, `slug`, `order` - `competence` : `name`, `content` (richtext), `picture`, `slug`, `order` — exemple de slug public / vault : `transcription-audio-fgc-transcription` (Transcription audio FGC).
- `realisation-ia` : `name`, `description`, `picture`, `slug`, `resum` (richtext, alias accepté côté front : `Resum` pour les `project` uniquement), `link`, `order`, relation `competences` (plusieurs) - `realisation-ia` : `name`, `description`, `picture`, `slug`, `resum` (richtext, alias accepté côté front : `Resum` pour les `project` uniquement), `link`, `order`, relation `competences` (plusieurs)
- Vignette → toujours navigation vers la **fiche détail** interne ; le champ `link` sert de bouton *Voir plus* en bas de fiche (comme sur les fiches `project`). - Vignette → toujours navigation vers la **fiche détail** interne ; le champ `link` sert de bouton *Voir plus* en bas de fiche (comme sur les fiches `project`).

View File

@ -1,6 +1,6 @@
# CMS Strapi # CMS Strapi
**Dernière mise à jour :** 2026-04-01 **Dernière mise à jour :** 2026-05-10
## Emplacement ## Emplacement
@ -43,6 +43,8 @@ Utilisation front : `app/page.tsx` — premier enregistrement `populate=*`, imag
| `slug` | uid ← `name` | requis | | `slug` | uid ← `name` | requis |
| `order` | integer | optionnel | | `order` | integer | optionnel |
Exemple dentrée alignée avec le vault GrasBot : **Transcription audio (FGC transcription)** — slug `transcription-audio-fgc-transcription` (fiche `/competences/transcription-audio-fgc-transcription`, note vault `source: manual`).
### `message` (supprimé le 2026-04-23) ### `message` (supprimé le 2026-04-23)
Ancien content-type pour stocker les soumissions du formulaire de contact. Supprimé car le formulaire envoie désormais une notification email via **Brevo** (voir `docs-site-interne/contact-flow.md`) — plus besoin de stockage Strapi. Les 4 fichiers `cmsbackend/src/api/message/**` ont été supprimés ; la table SQLite `messages` reste orpheline (inoffensive, peut être droppée manuellement). Ancien content-type pour stocker les soumissions du formulaire de contact. Supprimé car le formulaire envoie désormais une notification email via **Brevo** (voir `docs-site-interne/contact-flow.md`) — plus besoin de stockage Strapi. Les 4 fichiers `cmsbackend/src/api/message/**` ont été supprimés ; la table SQLite `messages` reste orpheline (inoffensive, peut être droppée manuellement).

View File

@ -28,8 +28,10 @@ Détails architecturaux dans
Le champ consommé par le front reste **`data.response`**. Les champs ajoutés Le champ consommé par le front reste **`data.response`**. Les champs ajoutés
par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
la réponse JSON et pourront être affichés dans une itération suivante la réponse JSON ; les **`sources`** incluent une **`url`** relative pour les types
(voir [pistes d'évolution](#pistes-dévolution)). `projet` et `compétence` (ex. `/portfolio/[slug]`, `/competences/[slug]` ou
`/competences/[route_parent]/[slug]` si la note compétence définit `route_parent`
dans le frontmatter du vault — utilisé pour les fiches sous `/competences/ia/…`).
## Parcours public (hors moteur Python) — cohérence contenu ## Parcours public (hors moteur Python) — cohérence contenu

View File

@ -1,11 +1,13 @@
# Outils `strapi_extraction/` # Outils `strapi_extraction/`
**Dernière mise à jour :** 2026-04-28 **Dernière mise à jour :** 2026-05-10
Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les
données issues de l'API Strapi en base de connaissance chatbot (hors runtime données issues de l'API Strapi en base de connaissance chatbot (hors runtime
du site). du site).
Synchroniser le champ Strapi avec **`strapi_extraction/docs/competence-transcription-audio-fgc-transcription.md`** (sections *Présentation → En bref*, même source que `competences-clean.json`).
## Pipeline complet ## Pipeline complet
``` ```

View File

@ -1,6 +1,6 @@
# Documentation interne du site # Documentation interne du site
**Dernière mise à jour :** 2026-04-24 **Dernière mise à jour :** 2026-05-10
Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte. Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte.
@ -10,7 +10,7 @@ Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet
|----------------|------| |----------------|------|
| `README.md` (racine du dépôt) | Panorama GitHub, liens vers cette doc, démarrage rapide, `reload-vault`. | | `README.md` (racine du dépôt) | Panorama GitHub, liens vers cette doc, démarrage rapide, `reload-vault`. |
| `CONFIGURATION_SITE.md` (racine) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. | | `CONFIGURATION_SITE.md` (racine) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. |
| `obsidian-site-docs/` (racine) | Pack Obsidian : hub, commandes, **copie** de ce dossier pour export (voir `obsidian-site-docs/SYNC-DOC.md`). | | Coffre Obsidian (optionnel, hors dépôt) | Copie locale de la doc pour lecture hors ligne ; pas de dossier `obsidian-site-docs/` versionné à la racine. |
| Ce dossier | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. | | Ce dossier | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. |
**Règle de maintenance :** après une modification notable, mettre à jour le fichier concerné ici ; si le démarrage ou les ports changent, compléter aussi `CONFIGURATION_SITE.md`. **Règle de maintenance :** après une modification notable, mettre à jour le fichier concerné ici ; si le démarrage ou les ports changent, compléter aussi `CONFIGURATION_SITE.md`.
@ -46,7 +46,6 @@ my-next-site/
├── stop-my-site.ps1 ├── stop-my-site.ps1
├── next.config.ts ├── next.config.ts
├── CONFIGURATION_SITE.md ├── CONFIGURATION_SITE.md
├── obsidian-site-docs/ # pack Obsidian (optionnel)
└── docs-site-interne/ └── docs-site-interne/
├── captures/ # screenshots de référence (voir INDEX.md) ├── captures/ # screenshots de référence (voir INDEX.md)
└── ... └── ...

View File

@ -1,6 +1,6 @@
# État actuel du site # État actuel du site
**Dernière mise à jour :** 2026-04-28 (perf images, compression IIS/Next, plan Server Components) **Dernière mise à jour :** 2026-05-10 (vault transcription FGC : pyannote, Mistral Small 24b)
## Ce qui est en place ## Ce qui est en place
@ -10,7 +10,7 @@
- **Formulaire contact** : e-mail via **Brevo** (route Next `POST /api/contact`). Voir [contact-flow.md](./contact-flow.md). - **Formulaire contact** : e-mail via **Brevo** (route Next `POST /api/contact`). Voir [contact-flow.md](./contact-flow.md).
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`). - **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings). - **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger lAPI après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire. - **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger lAPI après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. **Note manuelle** pour la compétence **Transcription audio (FGC transcription)** (`transcription-audio-fgc-transcription`) : pipeline **faster-whisper**, **diarisation pyannote**, résumés **JSON / Markdown** via **Ollama mistral-small3.2:24b** et templates avec ou sans diarisation (`source: manual`). Corpus exporté aussi dans `strapi_extraction/extract/clean-data/competences-clean.json` + `docs/competence-transcription-audio-fgc-transcription.md`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md). - **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
- **Scripts** d'extraction et de doc dans `strapi_extraction/`. - **Scripts** d'extraction et de doc dans `strapi_extraction/`.
- **Performances images (front)** : utilitaire **`pickStrapiImage`**, variantes Strapi, **`next/image`** + `remotePatterns`, portrait **`priority`**, **`preconnect`** API dans `layout.tsx`. **`compress: false`** dans `next.config.ts` (obligatoire avec le reverse proxy IIS actuel — **`compress: true`** provoquait des **500** publics). Détail : [`09-performances-images.md`](./09-performances-images.md). Prochaine étape planifiée (sans implémentation engageant pour linstant) : **Server Components** — [`10-plan-server-components.md`](./10-plan-server-components.md). - **Performances images (front)** : utilitaire **`pickStrapiImage`**, variantes Strapi, **`next/image`** + `remotePatterns`, portrait **`priority`**, **`preconnect`** API dans `layout.tsx`. **`compress: false`** dans `next.config.ts` (obligatoire avec le reverse proxy IIS actuel — **`compress: true`** provoquait des **500** publics). Détail : [`09-performances-images.md`](./09-performances-images.md). Prochaine étape planifiée (sans implémentation engageant pour linstant) : **Server Components** — [`10-plan-server-components.md`](./10-plan-server-components.md).

View File

@ -1,6 +1,6 @@
# Feuille de route # Feuille de route
**Dernière mise à jour :** 2026-04-28 **Dernière mise à jour :** 2026-05-10
Document vivant : ajuster les statuts et dates au fil du travail. Document vivant : ajuster les statuts et dates au fil du travail.
@ -33,6 +33,7 @@ Document vivant : ajuster les statuts et dates au fil du travail.
| Date | Jalon | | Date | Jalon |
|------|--------| |------|--------|
| 2026-05-10 | Vault GrasBot : fiche compétence **Transcription audio (FGC transcription)** (`transcription-audio-fgc-transcription`, `source: manual`), MOC compétences / IA mis à jour ; lien bidirectionnel avec `transcription-video`. |
| 2026-04-01 | Création du dossier `docs-site-interne` (dépôt Git) et première rédaction basée sur le code. | | 2026-04-01 | Création du dossier `docs-site-interne` (dépôt Git) et première rédaction basée sur le code. |
| 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. | | 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. |
| 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. | | 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. |

View File

@ -244,6 +244,11 @@ def parse_note(path: Path) -> Note | None:
if key in fm and fm[key] is not None: if key in fm and fm[key] is not None:
extra[key] = fm[key] extra[key] = fm[key]
# Segmentation optionnelle des URLs Next `/competences/[parent]/[slug]` (ex. parent `ia`).
rp = fm.get("route_parent")
if rp is not None and str(rp).strip():
extra["route_parent"] = str(rp).strip()
return Note( return Note(
slug=slug, slug=slug,
title=title, title=title,
@ -860,6 +865,10 @@ def answer(
if s.note.type == "projet": if s.note.type == "projet":
url = f"/portfolio/{s.note.slug}" url = f"/portfolio/{s.note.slug}"
elif s.note.type == "competence": elif s.note.type == "competence":
parent = str(s.note.extra.get("route_parent") or "").strip()
if parent:
url = f"/competences/{parent}/{s.note.slug}"
else:
url = f"/competences/{s.note.slug}" url = f"/competences/{s.note.slug}"
sources.append({ sources.append({
"slug": s.note.slug, "slug": s.note.slug,

View File

@ -1,21 +0,0 @@
# Hub — Site portfolio (dépôt my-next-site)
> Point dentrée du pack. La doc technique **complète** est **intégrée** (dossier `docs-site-interne/`) : voir [[03 Documentation intégrée (dépôt)]] et [README doc](docs-site-interne/README.md).
**Site** : [fernandgrascalvet.com](https://fernandgrascalvet.com)
## Fiches de ce pack
- [[01 Commandes - Démarrage, arrêt, reload vault]]
- [[02 Ports et URLs]]
- [[03 Documentation intégrée (dépôt)]]
- [[04 GrasBot et API LLM (résumé)]]
## Rappel express
| Action | Où ? |
|--------|------|
| Démarrer les 3 services (Windows) | `start-my-site.ps1` à la racine du dépôt |
| Arrêter | `stop-my-site.ps1` |
| Recharger le vault sans redémarrer lAPI | `POST /reload-vault` sur linstance FastAPI locale |
| Doc opérationnelle (copie dans le coffre) | [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) |

View File

@ -1,83 +0,0 @@
# Commandes — Démarrage, arrêt, reload vault
> Chemins dexemple : adapte le lecteur (ex. `J:`) et le nom du dossier du clone. La racine du dépôt = celui qui contient `package.json` et `start-my-site.ps1`.
## Démarrage automatique (recommandé)
Dans un PowerShell, **depuis la racine du dépôt** :
```powershell
Set-Location "J:\my-next-site" # à adapter
.\start-my-site.ps1
```
Lance en général **trois** fenêtres : Strapi (`cmsbackend`), Next (racine), FastAPI (`llm-api`).
**Arrêt ciblé** (ports 3000, 1337, 8000) :
```powershell
Set-Location "J:\my-next-site" # à adapter
.\stop-my-site.ps1
```
> Les détails (ports déjà pris, UTF-8 BOM, etc.) : voir le fichier `CONFIGURATION_SITE.md` à la racine du dépôt.
## Démarrage manuel (un terminal par service)
**Next.js** (port 3000) :
```powershell
Set-Location "J:\my-next-site"
npm run dev
```
**Strapi** (port 1337) :
```powershell
Set-Location "J:\my-next-site\cmsbackend"
npm run develop
```
**FastAPI + GrasBot** (port 8000) :
```powershell
Set-Location "J:\my-next-site\llm-api"
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
```
- Santé : `GET http://localhost:8000/health`
- Question test : `GET http://localhost:8000/ask?q=bonjour`
## Recharger le vault GrasBot (cache API)
Après **édition** de fichiers dans `vault-grasbot/`, sans redémarrer uvicorn :
Sous **Windows PowerShell**, préfère toujours (idempotent, pas dalias trompeur) :
```powershell
Invoke-RestMethod -Method Post -Uri "http://localhost:8000/reload-vault"
```
**Piège :** dans PowerShell, `curl` est un **alias** de `Invoke-WebRequest`, pas le binaire GNU. La forme `curl -X POST …` **échoue** avec *« Impossible de trouver un paramètre correspondant au nom « X » »*.
Si tu veux le vrai curl (souvent présent sur Windows 10+) :
```powershell
curl.exe -X POST http://localhost:8000/reload-vault
```
Sous **bash** (Git Bash, WSL, Linux, macOS) :
```bash
curl -X POST http://localhost:8000/reload-vault
```
Si lAPI tourne ailleurs, remplace lhôte/port.
**Ollama** (modèle LLM) : service séparé, en pratique sur le **port 11434** en local. Voir [[02 Ports et URLs]].
## Voir aussi
- [[00 Hub]]
- [[03 Documentation intégrée (dépôt)]]
- [04-api-llm-et-chatbot.md](docs-site-interne/04-api-llm-et-chatbot.md) — API GrasBot (implémentation : dossier `llm-api/` du dépôt).

View File

@ -1,20 +0,0 @@
# Ports et URLs
| Service | Port usuel | URL locale typique | Rôle |
|---------|------------|--------------------|------|
| Next.js | 3000 | http://localhost:3000 | Site public, App Router |
| Strapi | 1337 | http://localhost:1337/admin | CMS, API `/api` |
| FastAPI (GrasBot) | 8000 | http://localhost:8000 | `/ask`, `/health`, `/reload-vault` |
| Ollama | 11434 | http://localhost:11434 | Inférence LLM |
## Production (rappel)
- API Strapi publique (ex.) : `https://api.fernandgrascalvet.com`
- API LLM hébergée (proxy Next) : `llmapi.fernandgrascalvet.com` — le front en prod appelle souvent celle-là via `app/api/proxy`, pas `localhost:8000`.
Le navigateur en **dev** pointe le CMS selon `getApiUrl` (souvent `localhost:1337` en local).
## Voir aussi
- [[00 Hub]]
- [[01 Commandes - Démarrage, arrêt, reload vault]]

View File

@ -1,27 +0,0 @@
# Documentation intégrée dans ce coffre
Toute la **documentation interne** du site est **copiée ici** sous `docs-site-interne/`, avec `CONFIGURATION_SITE.md` et `README-racine-depot.md` à la **racine de ce pack**. Les liens ci-dessous sont relatifs à `obsidian-site-docs/` (ouvert comme coffre Obsidian).
## Racine du pack
- [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) — opérationnel (ports, scripts, `reload-vault`, dépannage)
- [README-racine-depot.md](README-racine-depot.md) — entrée du dépôt Git (GitHub)
- [SYNC-DOC.md](SYNC-DOC.md) — resynchroniser la copie depuis le dépôt
## Dossier [docs-site-interne](docs-site-interne/)
- [README — index](docs-site-interne/README.md) — **commencer ici** pour le détail
- [01-architecture](docs-site-interne/01-architecture.md) · [02-frontend](docs-site-interne/02-frontend-next.md) · [03-cms](docs-site-interne/03-cms-strapi.md) · [04-api-llm](docs-site-interne/04-api-llm-et-chatbot.md)
- [05-environnement](docs-site-interne/05-environnement-scripts.md) · [06-extraction](docs-site-interne/06-strapi-extraction.md) · [07-captures](docs-site-interne/07-reference-visuelle-captures.md) · [08-vault](docs-site-interne/08-vault-obsidian-retrieval.md)
- [etat-actuel](docs-site-interne/etat-actuel.md) · [feuille-de-route](docs-site-interne/feuille-de-route.md) · [REFONTE-VISUELLE](docs-site-interne/REFONTE-VISUELLE.md)
- [contact-flow (Brevo)](docs-site-interne/contact-flow.md) · [langfuse-observability](docs-site-interne/langfuse-observability.md)
- [captures/INDEX](docs-site-interne/captures/INDEX.md)
## Ancienne fiche
Lancienne note « Lien vers la doc (dépôt) » a été remplacée par celle-ci : tout est **dans** le coffre, plus besoin de remonter au clone Git pour lire le Markdown.
## Voir aussi
- [[00 Hub]]
- [[01 Commandes - Démarrage, arrêt, reload vault]]

View File

@ -1,16 +0,0 @@
# GrasBot et API LLM (résumé)
- **Rôle** : chatbot intégré au site (FAB global) ; questions/réponses sappuient sur `vault-grasbot/` et un modèle local via **Ollama** (ex. Qwen3), orchestré par **FastAPI** (`llm-api/search.py`).
- **v3 (2026)** : retrieval **graphe + BM25** sur le vault, **sans** embeddings / ChromaDB. Détail : [08-vault-obsidian-retrieval.md](docs-site-interne/08-vault-obsidian-retrieval.md).
- **Observabilité** : **Langfuse** (optionnel, secrets dans `llm-api/.env` du dépôt). Doc : [langfuse-observability.md](docs-site-interne/langfuse-observability.md).
- **Rechargement** du contenu textuel du vault côté API : [[01 Commandes - Démarrage, arrêt, reload vault]] (`POST /reload-vault`).
- **Extraction** Strapi → vault : `strapi_extraction/build-vault.py` (peut écraser les notes `source: strapi` ; ne pas toucher `source: manual`).
## Voir aussi
- [[00 Hub]]
- [[02 Ports et URLs]]

View File

@ -1,299 +0,0 @@
# Configuration du Site Web - Documentation Complète
## 📋 Vue d'ensemble
Ce site utilise une architecture full-stack moderne avec :
- **Frontend** : Next.js avec TypeScript et Tailwind CSS
- **Backend CMS** : Strapi
- **API IA** : FastAPI avec intégration Ollama
- **Démarrage automatique** : Planificateur de tâches Windows
## 🏗️ Architecture
```
my-next-site/
├── app/ # Application Next.js
├── cmsbackend/ # Backend Strapi
├── llm-api/ # API FastAPI pour IA (+ instrumentation Langfuse)
│ ├── .env # Secrets Python (Langfuse, etc.) — non committé
│ └── observability.py # Init client Langfuse (no-op safe)
├── start-my-site.ps1 # Script de démarrage
├── stop-my-site.ps1 # Script d'arrêt propre
└── package.json # Dépendances frontend
```
**Observabilité** : le chatbot GrasBot est tracé dans une instance **Langfuse self-hosted** (`langfuse.fernandgrascalvet.com`). Chaque question déclenche une trace `ask` avec spans `retrieval` / `prompt_build` / `ollama-chat`, plus des scores auto (`grounded`, `retrieval_relevance`) et des tags. Voir `docs-site-interne/langfuse-observability.md` pour le détail.
## Contenu : compétences, réalisations IA et ordre daffichage
- **Ordre** de la liste `/competences` : champ `order` sur le content-type compétence. Selon la version de Strapi, lAPI renvoie `attributes.order` (v4) ou `order` (v5) — le front unifie cela (voir `getOrder()` dans `app/competences/page.jsx`).
- **Fiche** `/competences/[slug]` : affiche des **vignettes** (projets de type `realisation-ia` liés à la compétence) dès quil en existe ; sinon, rendu de la fiche richtext « classique ».
- **Détail** dune réalisation : route `/competences/[slug]/[realisation]` (même gabarit de contenu quune fiche portfolio).
- **Documentation détaillée** (tableau des routes, champs Strapi, lien avec GrasBot) : `docs-site-interne/02-frontend-next.md` et, côté API chatbot, `docs-site-interne/04-api-llm-et-chatbot.md` (section *Parcours public*).
## 🚀 Démarrage Rapide
### Script Automatique (Recommandé)
```powershell
cd J:\my-next-site
.\start-my-site.ps1
```
Ce script lance automatiquement les 3 services dans des fenêtres PowerShell séparées.
**Améliorations :**
- Configuration centralisée via un tableau `$services` (plus de duplication entre les 3 blocs).
- **Détection du port déjà occupé** : si un service tourne déjà, il n'est pas relancé (évite `EADDRINUSE`).
- **Portabilité** : chemins résolus via `$PSScriptRoot`, pas de `J:\my-next-site` codé en dur.
- **`-NoExit`** sur chaque fenêtre : le message d'erreur reste visible si un service crashe au démarrage.
- **Bilan final** : nombre de services lancés / déjà actifs / échecs.
### Arrêt des services
```powershell
cd J:\my-next-site
.\stop-my-site.ps1
```
Termine les processus qui écoutent les ports **1337** (Strapi), **3000** (Next.js) et **8000** (FastAPI). Ne touche pas aux autres processus Node de ta machine. Les fenêtres PowerShell lancées par `start-my-site.ps1` restent ouvertes (à fermer manuellement). En cas d'échec sur certains PIDs → relancer dans un PowerShell admin.
> Note encodage : les deux scripts PowerShell sont encodés en **UTF-8 avec BOM**. C'est nécessaire pour que Windows PowerShell 5.1 (version par défaut) les lise correctement avec les emojis et accents. PowerShell 7 n'a pas ce souci mais reste compatible avec le BOM.
## 🔧 Commandes Manuelles
### 1. Frontend Next.js
```powershell
cd J:\my-next-site
npm run dev
```
- **URL** : http://localhost:3000
- **Mode** : Développement avec Turbopack
- **Rechargement** : Automatique
### 2. Backend Strapi (CMS)
```powershell
cd J:\my-next-site\cmsbackend
npm run develop
```
- **Interface Admin** : http://localhost:1337/admin
- **API** : http://localhost:1337/api
- **Mode** : Développement
### 3. API FastAPI (IA)
```powershell
cd J:\my-next-site\llm-api
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
```
- **API** : http://localhost:8000
- **Endpoint IA** : http://localhost:8000/ask?q=votre_question
- **Documentation** : http://localhost:8000/docs
- **Santé** : http://localhost:8000/health — renvoie `status`, `ollama_url`, `llm_model`, métadonnées vault, et `observability.langfuse_enabled`.
- **Recharger le vault à chaud** : `POST http://localhost:8000/reload-vault` — à appeler après création/modification d'une note dans `vault-grasbot/` (sinon `load_vault()` reste en cache mémoïsé jusqu'au prochain redémarrage d'uvicorn).
> **Tuning du pipeline LLM** : les paramètres Ollama (`num_ctx`, `num_predict`, `think`), la troncature des sources RAG secondaires (`SEARCH_SECONDARY_MAX_CHARS`, `SEARCH_SECONDARY_KEEP_RATIO`), le system prompt anti-hallucination et la note `bio-fernand` sont documentés en détail dans `docs-site-interne/langfuse-observability.md` (section *Tuning du pipeline — 2026-04-23*).
## 📊 Ports Utilisés
| Service | Port | URL |
|---------|------|-----|
| Next.js | 3000 | http://localhost:3000 |
| Strapi | 1337 | http://localhost:1337 |
| FastAPI | 8000 | http://localhost:8000 |
| Ollama | 11434 | http://localhost:11434 |
## 🔄 Démarrage Automatique
### Configuration Actuelle
- **Méthode** : Planificateur de tâches Windows
- **Nom de la tâche** : "Lancement site web dino"
- **État** : Ready (Prêt)
### Gestion de la Tâche Planifiée
#### Voir les détails
```powershell
Get-ScheduledTask -TaskName "Lancement site web dino" | Get-ScheduledTaskInfo
```
#### Démarrer manuellement
```powershell
Start-ScheduledTask -TaskName "Lancement site web dino"
```
#### Arrêter la tâche
```powershell
Stop-ScheduledTask -TaskName "Lancement site web dino"
```
#### Désactiver/Réactiver
```powershell
# Désactiver
Disable-ScheduledTask -TaskName "Lancement site web dino"
# Réactiver
Enable-ScheduledTask -TaskName "Lancement site web dino"
```
#### Interface Graphique
```
Win + R → taskschd.msc
```
Cherchez "Lancement site web dino" dans la liste.
## 🛠️ Dépannage
### Arrêter Tous les Processus
```powershell
# Arrêter Node.js
taskkill /f /im node.exe
# Arrêter Python/FastAPI
taskkill /f /im python.exe
```
### Nettoyer les Caches
```powershell
# Cache Next.js
cd J:\my-next-site
Remove-Item .next -Recurse -Force -ErrorAction SilentlyContinue
# Cache Strapi
cd J:\my-next-site\cmsbackend
Remove-Item .cache -Recurse -Force -ErrorAction SilentlyContinue
```
### Réinstaller les Dépendances
```powershell
# Frontend
cd J:\my-next-site
npm install
# Backend Strapi
cd J:\my-next-site\cmsbackend
npm install
# API Python (si environnement virtuel)
cd J:\my-next-site\llm-api
pip install fastapi uvicorn[standard] requests
```
## 🐍 Configuration Python/FastAPI
### Dépendances Requises
```txt
fastapi==0.115.8
uvicorn[standard]==0.38.0
requests==2.32.3
```
### Problèmes Courants
- **Erreur uvicorn** : Vérifiez l'installation avec `pip show uvicorn`
- **Port occupé** : Changez le port dans le script ou tuez le processus
- **Ollama non disponible** : Vérifiez que Ollama fonctionne sur le port 11434
### Installation Propre (Recommandée)
```powershell
cd J:\my-next-site\llm-api
python -m venv venv
venv\Scripts\activate
pip install fastapi uvicorn[standard] requests
```
## 📝 Scripts Disponibles
### Frontend (package.json)
```json
{
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
```
### Backend Strapi (cmsbackend/package.json)
```json
{
"develop": "strapi develop",
"build": "strapi build",
"start": "strapi start"
}
```
## 🔍 Vérifications de Santé
### Vérifier que tous les services fonctionnent
```powershell
# Next.js
curl http://localhost:3000
# Strapi
curl http://localhost:1337/admin
# FastAPI
curl http://localhost:8000/docs
# Ollama
curl http://localhost:11434/api/generate
```
### Vérifier les processus actifs
```powershell
# Processus Node.js
Get-Process node -ErrorAction SilentlyContinue
# Processus Python
Get-Process python -ErrorAction SilentlyContinue
```
## 🌐 Configuration Réseau
### Accès Externe
Si vous voulez accéder au site depuis d'autres machines :
- **Next.js** : Modifier `next.config.ts` pour accepter les connexions externes
- **Strapi** : Configurer `config/server.ts`
- **FastAPI** : Déjà configuré avec `--host 0.0.0.0`
### Pare-feu Windows
Assurez-vous que les ports sont ouverts :
```powershell
# Ouvrir les ports dans le pare-feu
New-NetFirewallRule -DisplayName "Next.js" -Direction Inbound -Port 3000 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "Strapi" -Direction Inbound -Port 1337 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "FastAPI" -Direction Inbound -Port 8000 -Protocol TCP -Action Allow
```
## 📚 Ressources Utiles
- **Next.js Documentation** : https://nextjs.org/docs
- **Strapi Documentation** : https://docs.strapi.io
- **FastAPI Documentation** : https://fastapi.tiangolo.com
- **Ollama Documentation** : https://ollama.ai/docs
## 🔧 Maintenance
### Mise à jour des dépendances
```powershell
# Frontend
cd J:\my-next-site
npm update
# Backend
cd J:\my-next-site\cmsbackend
npm update
# Python packages
pip list --outdated
pip install --upgrade package_name
```
### Sauvegarde
Pensez à sauvegarder régulièrement :
- Base de données Strapi (`cmsbackend/database/`)
- Configuration (`cmsbackend/config/`)
- Assets (`app/assets/`)
---
**Dernière mise à jour** : 2026-04-24
**Version Cursor** : 2.0.77
**OS** : Windows Server 2025

View File

@ -1,15 +0,0 @@
# Comment utiliser ce dossier dans Obsidian
Ce répertoire (`obsidian-site-docs/`) est une **aide-mémoire** alignée sur la vraie documentation du dépôt (`docs-site-interne/`, `CONFIGURATION_SITE.md`). Il sert surtout aux **commandes** et **liens** pour le travail sur le site.
## Option A — Ouvrir comme coffre secondaire
Dans Obsidian : **Fichier → Ouvrir un coffre** → choisir le dossier `obsidian-site-docs` (situé à la racine du clone `my-next-site`). Tu obtiens un mini-coffre avec le [[00 Hub|hub]] en point dentrée.
## Option B — Copier dans ton coffre perso
Copie tout le contenu de `obsidian-site-docs/` dans un dossier de ton choix (ex. `20-Technique/Site-fernandgrascalvet/`). Les wikilinks `[[...]]` entre les notes de ce pack restent valides. La documentation complète est **dans** `docs-site-interne/` (fichiers Markdown) ; [SYNC-DOC](SYNC-DOC.md) explique la resynchro avec le dépôt Git. Les chemins relatifs (ex. `docs-site-interne/01-…`) restent bons **si** tu exportes le dossier entier.
## Source de vérité
Les fichiers longs, schémas mermaid et captures restent dans le dépôt : préférer la lecture sur GitHub ou dans lIDE pour l**intégralité** de la spec.

View File

@ -1,75 +0,0 @@
# Portfolio fernandgrascalvet.com
![Aperçu du site](picture.png)
Site portfolio **Next.js 15** + **Strapi 5** + **GrasBot** (FastAPI, Ollama, vault `vault-grasbot/`). UI *Digital Atelier* (Manrope, Newsreader, Tailwind). Hébergement typique : Windows Server, IIS, HTTPS (Win-ACME).
**Site en ligne :** [fernandgrascalvet.com](https://fernandgrascalvet.com)
## Documentation
| Ressource | Rôle |
|-----------|------|
| **[docs-site-interne](docs-site-interne/README.md)** | Architecture, CMS, front, API LLM, feuille de route, état actuel, captures, refonte UI. *À lire en priorité pour le contexte technique.* |
| [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) | Opérationnel : ports, commandes, démarrage automatique, dépannage, pare-feu. |
| [`vault-grasbot/README.md`](vault-grasbot/README.md) | Base de connaissances GrasBot (retrieval graph + BM25 v3). |
**Obsidian :** le dossier [`obsidian-site-docs/`](obsidian-site-docs/) regroupe un hub et des fiches prêtes à copier ou à ouvrir comme coffre secondaire (commandes, ports, liens vers la doc du dépôt).
## Démarrage rapide (Windows)
Depuis la racine du dépôt (adapter le lecteur / chemin si besoin) :
```powershell
# Les trois services : Next, Strapi, FastAPI (fenêtres séparées)
.\start-my-site.ps1
```
```powershell
# Arrêt propre (ports 3000, 1337, 8000)
.\stop-my-site.ps1
```
Détail, ports et commandes manuelles : **[`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md)**.
## Stack (résumé)
- **Front** : Next.js (App Router), TypeScript/JS, Tailwind, Swiper, chatbot global (FAB).
- **CMS** : Strapi — `homepage`, `project`, `competence`, `realisation-ia`, `glossaire`.
- **Contact** : e-mail via **Brevo** (route Next `POST /api/contact`), pas de stockage Strapi des messages. Voir [`docs-site-interne/contact-flow.md`](docs-site-interne/contact-flow.md).
- **IA** : `llm-api/` (FastAPI) → Ollama (ex. Qwen3), base `vault-grasbot/`, observabilité **Langfuse** optionnelle. Voir [`docs-site-interne/04-api-llm-et-chatbot.md`](docs-site-interne/04-api-llm-et-chatbot.md).
## Rechargement du vault GrasBot (API locale)
Après modification des fichiers dans `vault-grasbot/`, recharger le cache côté API sans redémarrer uvicorn :
```powershell
# Exemple (Invoke-RestMethod)
Invoke-RestMethod -Method Post -Uri "http://localhost:8000/reload-vault"
```
Ou : `POST http://localhost:8000/reload-vault` (HTTP client de votre choix). Voir aussi [`CONFIGURATION_SITE.md`](CONFIGURATION_SITE.md) (santé `/health`, endpoint `/ask`).
## Dépôts et répertoires utiles
```
my-next-site/
├── app/ # Next.js
├── cmsbackend/ # Strapi
├── llm-api/ # FastAPI + GrasBot
├── vault-grasbot/ # Connaissance (Obsidian) pour le retrieval
├── strapi_extraction/ # Extraction / build vault
├── docs-site-interne/ # Doc technique détaillée
├── obsidian-site-docs/ # Pack Obsidian (résumés + commandes)
├── start-my-site.ps1
├── stop-my-site.ps1
└── CONFIGURATION_SITE.md
```
## Licence et usage
Projet personnel ; contenu et code sont fournis tels quels pour illustration du portfolio.
---
*Dernière révision du README : 2026-04 — aligné sur `docs-site-interne` et `CONFIGURATION_SITE.md`.*

View File

@ -1,9 +0,0 @@
# Pack Obsidian — site my-next-site
Contenu : **aide-mémoire** (commandes, ports, hub) + **copie intégrée** de `docs-site-interne/` (toute la doc technique), plus [CONFIGURATION_SITE.md](CONFIGURATION_SITE.md) et [README-racine-depot.md](README-racine-depot.md) à la racine de ce pack.
- **Entrée** : [00 Hub.md](00%20Hub.md) — ou [docs-site-interne/README.md](docs-site-interne/README.md) pour lindex détaillé
- **Resynchronisation** avec le dépôt Git : [SYNC-DOC.md](SYNC-DOC.md)
- **Mode demploi** (coffre dédié vs copie dans un autre vault) : [README - utiliser ce dossier.md](README%20-%20utiliser%20ce%20dossier.md)
Sur **GitHub**, le dossier `obsidian-site-docs/` est parcourable ; les liens relatifs pointent vers les fichiers **dans ce même dossier** (export autonome).

View File

@ -1,31 +0,0 @@
# Resynchroniser la documentation intégrée
Le dossier `docs-site-interne/` dans **ce coffre** est une **copie** de `my-next-site/docs-site-interne/` (dépôt Git). Les fichiers `CONFIGURATION_SITE.md` et `README-racine-depot.md` à la racine de ce pack sont des copies de la racine du dépôt.
## Quand lancer une resynchro
Après toute **édition** des fichiers dans le dépôt (`docs-site-interne/`, `CONFIGURATION_SITE.md`, `README.md` racine) que tu veux voir **dans lexport Obsidian**.
## Procédure (PowerShell, depuis le clone)
À adapter : lecteur `J:` et chemin du dépôt.
```powershell
$root = "J:\my-next-site" # racine du dépôt
$obs = Join-Path $root "obsidian-site-docs"
$target = Join-Path $obs "docs-site-interne"
Remove-Item -Path $target -Recurse -Force -ErrorAction SilentlyContinue
Copy-Item -Path (Join-Path $root "docs-site-interne") -Destination $target -Recurse -Force
Copy-Item (Join-Path $root "CONFIGURATION_SITE.md") (Join-Path $obs "CONFIGURATION_SITE.md") -Force
Copy-Item (Join-Path $root "README.md") (Join-Path $obs "README-racine-depot.md") -Force
# Corriger les liens `docs-site-interne/...` pour ce coffre
& (Join-Path $obs "_fix-links-in-docs-copy.ps1")
```
Puis **réapplique** manuellement les ajustements spécifiques du pack (fichier `docs-site-interne/README.md` dans *ce* coffre, section *Relation* / arborescence, liens `SYNC-DOC` et `etat-actuel` si ton script a écrasé des retouches locales) — le plus sûr est de **commiter dabord** ce dépôt et de refaire seulement les 23 liens listés en haut de `docs-site-interne/README.md` dici.
## Fichier utilitaire
- `_fix-links-in-docs-copy.ps1` : adapte les chemins après un `Copy-Item` (ne pas lancer sur le dépôt source `docs-site-interne` à la racine du repo, uniquement sur la copie sous `obsidian-site-docs`).

View File

@ -1,26 +0,0 @@
# One-shot: adapte les chemins docs-site-interne/ dans la copie Obsidian
$ErrorActionPreference = "Stop"
$base = Join-Path $PSScriptRoot "docs-site-interne"
if (-not (Test-Path $base)) { throw "Dossier introuvable: $base" }
Get-ChildItem -Path $base -Recurse -Filter *.md | ForEach-Object {
# README.md (racine de docs-site-interne) : contient une arborescence en bloc de code
# avec le nom du dossier — ne pas y appliquer le remplacement global.
if ($_.Name -eq "README.md" -and $_.DirectoryName -eq (Resolve-Path $base).Path) {
return
}
$isUnderCaptures = $_.FullName -like "*\captures\*" -or $_.DirectoryName -like "*\captures"
$c = [System.IO.File]::ReadAllText($_.FullName)
if ($isUnderCaptures) {
$c = $c -replace "docs-site-interne/REFONTE-VISUELLE", "../REFONTE-VISUELLE"
$c = $c -replace "docs-site-interne/contact-flow", "../contact-flow"
$c = $c -replace "docs-site-interne/08-vault-obsidian-retrieval", "../08-vault-obsidian-retrieval"
$c = $c -replace "docs-site-interne/captures/", ""
$c = $c -replace "docs-site-interne/", "../"
} else {
$c = $c -replace "docs-site-interne/", ""
}
$utf8 = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($_.FullName, $c, $utf8)
}
Write-Host "OK: liens ajustes dans $base"

View File

@ -1,46 +0,0 @@
# Architecture globale
**Dernière mise à jour :** 2026-04-01
## Schéma logique
```mermaid
flowchart LR
Browser[Navigateur]
Next[Next.js :3000]
Strapi[Strapi :1337]
FastAPI[FastAPI :8000]
Ollama[Ollama :11434]
RemoteAPI[api.fernandgrascalvet.com]
Browser --> Next
Next -->|"fetch REST (getApiUrl)"| Strapi
Next -->|"fetch REST"| RemoteAPI
Next -->|"/api/proxy → llmapi.*"| FastAPI
FastAPI --> Ollama
```
En **développement local**, le front appelle souvent **Strapi sur `localhost:1337`** (voir `getApiUrl`). En **production**, les appels client pointent vers **`https://api.fernandgrascalvet.com`**.
Le **chatbot** ne parle pas à FastAPI en local par défaut : la route `app/api/proxy/route.js` appelle **`https://llmapi.fernandgrascalvet.com/ask`** (URL absolue).
## Services et ports
| Service | Port local typique | Rôle |
|---------|-------------------|------|
| Next.js | 3000 | Site public, rewrites `/api/*` → Strapi distant (voir `next.config.ts`) |
| Strapi | 1337 | CMS, REST `/api/*` |
| FastAPI (`llm-api/api.py`) | 8000 | Pont HTTP vers Ollama `/api/generate` |
| Ollama | 11434 | Inférence LLM (modèle `mistral` dans le code actuel) |
## Fichiers clés
- `next.config.ts` — rewrites, `NEXT_PUBLIC_API_URL`, domaines images.
- `app/utils/getApiUrl.ts` — choix de lURL Strapi (local vs prod, client vs serveur).
- `app/api/proxy/route.js` — proxy vers lAPI LLM **hébergée** (`llmapi.fernandgrascalvet.com`).
- `llm-api/api.py` — implémentation FastAPI locale (Ollama).
## Points dattention
1. **`next.config` rewrites** : les requêtes vers `/api/*` côté Next sont renvoyées vers lURL Strapi configurée — **conflit sémantique** avec la route Next `app/api/proxy` (chemin différent : le proxy est sous `/api/proxy`, pas sous le rewrite générique de la même façon ; à vérifier selon lordre de matching Next).
2. **`app/utils/config.ts`** expose `API_URL` avec défaut `localhost:1337`, alors que `next.config.ts` utilise par défaut le domaine de prod : usages distincts, ne pas les confondre.

View File

@ -1,65 +0,0 @@
# Frontend Next.js
**Dernière mise à jour :** 2026-04-24
## Stack
- **Next.js** 15.x, **React** 18, **TypeScript** (fichiers `.tsx`/`.ts`) avec fichiers **`.jsx`/`.js`** encore présents.
- **Tailwind CSS** + `@tailwindcss/typography`.
- Rendu riche Strapi : `@strapi/blocks-react-renderer`, **react-markdown** + rehype/remark.
## Routes (App Router)
| Chemin | Fichier | Notes |
|--------|---------|--------|
| `/` | `app/page.tsx` | Accueil : `GET /api/homepages?populate=*` |
| `/portfolio` | `app/portfolio/page.jsx` | Liste projets |
| `/portfolio/[slug]` | `app/portfolio/[slug]/page.tsx` | Détail projet : `ContentSection` + `fetchData('projects', slug)` |
| `/competences` | `app/competences/page.jsx` | Liste compétences — tri par champ Strapi `order` (v4 : `attributes.order`, v5 : `order` à la racine) |
| `/competences/[slug]` | `app/competences/[slug]/page.tsx` | **Rendu conditionnel** : si au moins une entrée `realisation-ia` est liée à la compétence (filtre API sur le slug), **grille de vignettes** (même rythme visuel que le portfolio) ; sinon fiche richtext historique via `ContentSectionCompetencesContainer` |
| `/competences/[slug]/[realisation]` | `app/competences/[slug]/[realisation]/page.tsx` | Fiche d'une **réalisation** (collection Strapi `realisation-ia`) : réutilise `ContentSection` comme les projets (carousel, Markdown `resum` ou `Resum`, CTA `link` externe) |
| `/contact` | `app/contact/page.js` | Formulaire → `/api/contact` (Brevo, voir `contact-flow.md`) |
| `/api/contact` | `app/api/contact/route.ts` | Endpoint serveur : envoie un email via Brevo, honeypot + rate-limit |
| `/api/proxy` | `app/api/proxy/route.js` | Proxy GET vers API LLM distante |
**Strapi — content-types concernés :**
- `competence` : `name`, `content` (richtext), `picture`, `slug`, `order`
- `realisation-ia` : `name`, `description`, `picture`, `slug`, `resum` (richtext, alias accepté côté front : `Resum` pour les `project` uniquement), `link`, `order`, relation `competences` (plusieurs)
- Vignette → toujours navigation vers la **fiche détail** interne ; le champ `link` sert de bouton *Voir plus* en bas de fiche (comme sur les fiches `project`).
## Layout
- `app/layout.tsx`**Client Component** (`"use client"`). Header fixe, menu burger mobile, fond décoratif, **Footer**, compteur de visites **localStorage** (`visitCount`).
## Données Strapi
- **`getApiUrl()`** (`app/utils/getApiUrl.ts`) :
- Côté **navigateur** : si hostname est local / LAN → `http://localhost:1337`, sinon → `https://api.fernandgrascalvet.com`.
- Côté **serveur** : `process.env.NEXT_PUBLIC_API_URL` ou défaut `https://api.fernandgrascalvet.com`.
- **`fetchData`** (`app/utils/fetchData.ts`) : collection + `slug`, populate `picture`, `cache: "no-store"`.
- **Accueil** (`app/page.tsx`) : `homepages`, retry 3×, timeout 10 s.
## Configuration Next
- `next.config.ts` : `rewrites` de `/api/:path*` vers `${API_URL}/api/:path*``API_URL` vient de `NEXT_PUBLIC_API_URL` ou défaut production.
- `images.domains` : `localhost`, `api.fernandgrascalvet.com`.
## Composants notables
- Carrousels : `Carousel.tsx`, `CarouselCompetences.tsx` (swiper / react-responsive-carousel).
- Sections : `ContentSection.tsx`, `ContentSectionCompetences*.tsx`.
- `ContactForm.tsx``POST /api/contact` → Brevo API (voir `contact-flow.md`).
- `GrasBotFab` + `ChatBot.js``askAI.js``/api/proxy` → FastAPI `/ask` avec `session_id` + `user_id` (UUID anonymes via `app/utils/grasbotIds.js`, voir `langfuse-observability.md`).
- `ModalGlossaire.tsx` — glossaire (données Strapi selon usage dans les pages).
## Fichiers clés (liste courte)
```
app/layout.tsx
app/page.tsx
app/utils/getApiUrl.ts
app/utils/fetchData.ts
app/api/contact/route.ts
next.config.ts
```

View File

@ -1,72 +0,0 @@
# CMS Strapi
**Dernière mise à jour :** 2026-04-01
## Emplacement
- Code : `cmsbackend/`
- Schémas : `cmsbackend/src/api/<nom>/content-types/<nom>/schema.json`
Tous les types listés ci-dessous ont **`draftAndPublish: true`** : penser à **publier** les entrées dans ladmin.
## Content-types
### `homepage` (collection `homepages`)
| Champ | Type | Notes |
|-------|------|--------|
| `title` | string | requis |
| `cv` | richtext | requis |
| `photo` | media (single) | requis |
Utilisation front : `app/page.tsx` — premier enregistrement `populate=*`, image : `${apiUrl}${photo.url}`.
### `project` (collection `projects`)
| Champ | Type | Notes |
|-------|------|--------|
| `name` | string | requis |
| `description` | text | requis |
| `picture` | media (multiple) | requis |
| `slug` | uid ← `name` | requis |
| `Resum` | richtext | requis (nom du champ avec majuscule) |
| `link` | string (URL) | requis |
| `order` | integer | optionnel |
### `competence` (collection `competences`)
| Champ | Type | Notes |
|-------|------|--------|
| `name` | string | requis |
| `content` | richtext | requis |
| `picture` | media (multiple) | requis |
| `slug` | uid ← `name` | requis |
| `order` | integer | optionnel |
### `message` (supprimé le 2026-04-23)
Ancien content-type pour stocker les soumissions du formulaire de contact. Supprimé car le formulaire envoie désormais une notification email via **Brevo** (voir `contact-flow.md`) — plus besoin de stockage Strapi. Les 4 fichiers `cmsbackend/src/api/message/**` ont été supprimés ; la table SQLite `messages` reste orpheline (inoffensive, peut être droppée manuellement).
### `glossaire` (collection `glossaires`)
| Champ | Type | Notes |
|-------|------|--------|
| `mot_clef` | string | requis |
| `slug` | uid ← `mot_clef` | requis |
| `variantes` | json | requis |
| `description` | richtext | requis |
| `images` | media (multiple) | requis |
## API REST
- Base : `http://localhost:1337` (dev) ou `https://api.fernandgrascalvet.com` (prod).
- Préfixe : `/api/<pluralName>` (ex. `/api/projects`, `/api/homepages`).
## Fichiers de config Strapi (référence)
```
cmsbackend/config/database.ts
cmsbackend/config/server.ts
cmsbackend/config/middlewares.ts
cmsbackend/config/api.ts
```

View File

@ -1,157 +0,0 @@
# API LLM et chatbot (GrasBot)
**Dernière mise à jour :** 2026-04-24 (v3 + alignement parcours site)
## Vue d'ensemble
GrasBot répond aux visiteurs en s'appuyant sur un **pipeline de retrieval
local**, sans embeddings ni base vectorielle :
- Vault Obsidian `vault-grasbot/` lu directement en mémoire par `search.py`.
- Scoring déterministe multi-signaux (aliases, titre/slug, answers,
domains, tags, BM25 sur le body).
- Expansion par graphe via les wikilinks (`linked`, `related`, `[[...]]`
dans le corps).
- Prompt construit avec top-5 notes entières, envoyé à Qwen3 8B via Ollama.
Détails architecturaux dans
[`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md).
## Chaîne côté navigateur
1. FAB `GrasBotFab` (monté dans `app/layout.tsx`) affiche `ChatBot.js`.
2. `ChatBot.js` appelle `askAI(question)` (`app/utils/askAI.js`).
3. `askAI` envoie un **GET** vers `/api/proxy?q=...` (route Next.js App Router).
4. `app/api/proxy/route.js` appelle
`https://llmapi.fernandgrascalvet.com/ask?q=...` (URL figée en dur
pour l'instant) et renvoie le corps JSON tel quel.
Le champ consommé par le front reste **`data.response`**. Les champs ajoutés
par la refonte (`sources`, `grounded`, `model`, `vault_size`) passent dans
la réponse JSON et pourront être affichés dans une itération suivante
(voir [pistes d'évolution](#pistes-dévolution)).
## Parcours public (hors moteur Python) — cohérence contenu
Le visiteur découvre les **projets** sur `/portfolio/[slug]` et, pour la
compétence **IA** (et toute compétence à laquelle on lie des
**`realisation-ia`** dans Strapi), un **parallèle** sur `/competences/[slug]`
(vignettes) puis `/competences/[slug]/[realisation]` (fiche identique en
gabarit à une fiche projet). Rien n'est servi ici par FastAPI : c'est
du **Strapi + Next** uniquement. Le chatbot, lui, interroge toujours
**`vault-grasbot/`** via `llm-api/search.py` — mettre à jour le vault
(ou l'extraction Strapi → vault) quand on veut que GrasBot **reflète** des
faits nouveaux présentés sur le site. Détail des routes : [`02-frontend-next.md`](./02-frontend-next.md).
## FastAPI — `llm-api/`
| Fichier | Rôle |
|---------|------|
| `api.py` | Endpoints `GET /ask?q=...`, `GET /health`, `POST /reload-vault`. |
| `search.py` | `load_vault`, `tokenize_fr`, `score_note`, `expand_by_graph`, `search`, `build_prompt`, `generate`, `answer`. |
| `requirements.txt` | `fastapi`, `uvicorn`, `requests`, `pyyaml` ; + `langfuse` (SDK 3.x, plafond strict inférieur à la v4) + `python-dotenv` pour l'observabilité optionnelle. Voir `llm-api/requirements.txt`. **Plus besoin** de `chromadb` / `chroma-hnswlib` (supprimés v3). |
Modules supprimés en v3 :
- `rag.py` → remplacé par `search.py`.
- `index_vault.py` → plus d'étape d'indexation (lecture directe du vault).
## Modèle Ollama
| Rôle | Modèle | VRAM | Commande |
|------|--------|------|----------|
| Chat | `qwen3:8b` | ~5 Go (Q4_K_M) | `ollama pull qwen3:8b` |
**Plus d'embeddings.** Le modèle `nomic-embed-text` n'est plus nécessaire.
Tu peux libérer de la place avec `ollama rm nomic-embed-text` si jamais
il reste installé.
## Variables d'environnement (facultatives)
Toutes définies dans `search.py`, surchargeables via env sans toucher au code :
- `OLLAMA_URL` (default `http://localhost:11434`)
- `LLM_MODEL` (default `qwen3:8b`)
- `VAULT_DIR` (default `<repo>/vault-grasbot`)
- `SEARCH_TOP_K` (default `5`)
- `SEARCH_MIN_SCORE` (default `1.0`) — seuil en-dessous duquel le chatbot
bascule en mode *« pas de contexte pertinent »* (évite les réponses
inventées sur des questions hors sujet).
## Mise en service
```powershell
# 1. Installer les dépendances Python (pure Python, pas de compilation C++)
cd llm-api
pip install -r requirements.txt
# 2. Pull le modèle Ollama (Ollama doit tourner)
ollama pull qwen3:8b
# 3. Lancer l'API
uvicorn api:app --host 0.0.0.0 --port 8000
```
Plus besoin d'étape d'indexation : l'API lit le vault au démarrage.
Health-check : `curl http://localhost:8000/health` retourne la config active,
la taille du vault et le nombre de notes par type.
Après édition du vault (ajout/modification d'une note) :
```powershell
# Force la relecture sans redémarrer uvicorn
curl -X POST http://localhost:8000/reload-vault
```
## Réponse du backend
```json
{
"response": "Push Swap est un projet 42 qui explore les algorithmes de tri sur piles…",
"sources": [
{
"slug": "push-swap",
"title": "push_swap",
"type": "projet",
"score": 32.27,
"reasons": ["alias:push-swap", "slug", "answers-partial", "bm25:2.12"],
"url": "/portfolio/push-swap"
},
{
"slug": "cpp-partie1",
"title": "cpp_module_00 à 04",
"type": "projet",
"score": 20.62,
"reasons": ["graph-from:push-swap", "graph-reinforce"],
"url": "/portfolio/cpp-partie1"
}
],
"grounded": true,
"model": "qwen3:8b",
"vault_size": 41
}
```
`askAI.js` ne lit que `data.response` → rétrocompatibilité assurée.
Le champ `reasons` sert à **tracer** pourquoi une note a été remontée : très
utile pour ajuster aliases / answers quand une question renvoie de mauvais
résultats.
## Pistes d'évolution
- **Variable d'environnement côté proxy Next** pour pointer vers
`http://localhost:8000` en dev et vers la prod en déploiement (au lieu
de l'URL figée dans `app/api/proxy/route.js`).
- **Affichage des sources** côté front : vignettes cliquables sous la
réponse, utilisant le champ `url` renvoyé par l'API.
- **Badge `grounded`** : afficher *« Réponse basée sur les notes »* vs
*« Réponse générale »* pour informer le visiteur de la confiance.
- **Historique court** (3-4 derniers tours) pour la continuité conversationnelle.
- **Streaming** des réponses pour l'UX temps réel (Qwen3 supporte `stream: true`).
- **Reload automatique** via file watcher sur `vault-grasbot/` quand on
édite dans Obsidian.
- **Filtre `visibility`** déjà en place dans `load_vault()` (les notes
`private` sont exclues). Le vault perso pourra être fusionné sans
exposer ses notes privées au chatbot public.

View File

@ -1,29 +0,0 @@
# Environnement et scripts
**Dernière mise à jour :** 2026-04-01
## Variables denvironnement (Next)
| Variable | Usage |
|----------|--------|
| `NEXT_PUBLIC_API_URL` | Base URL Strapi pour le build et le rendu serveur ; chargée dans `next.config.ts` (dotenv) pour rewrites et images. |
Défauts dans le code si absent :
- `next.config.ts` : `https://api.fernandgrascalvet.com`
- `app/utils/getApiUrl.ts` (navigateur local) : `http://localhost:1337`
- `app/utils/config.ts` : `http://localhost:1337` (usage ponctuel selon imports)
## Script `start-my-site.ps1`
- Définit `NEXT_PUBLIC_API_URL` et `PUBLIC_URL` vers `https://api.fernandgrascalvet.com`.
- Lance trois fenêtres PowerShell : Strapi (`cmsbackend`), Next (racine), FastAPI (`llm-api` sur port 8000).
- Chemins en dur : `J:\my-next-site\...` — à adapter si le dépôt est déplacé.
## Démarrage manuel
Voir `CONFIGURATION_SITE.md` (ports 3000, 1337, 8000, 11434).
## CORS / proxy
- La route `app/api/proxy/route.js` renvoie `Access-Control-Allow-Origin: *` sur la réponse proxy.

View File

@ -1,91 +0,0 @@
# Outils `strapi_extraction/`
**Dernière mise à jour :** 2026-04-22
Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les
données issues de l'API Strapi en base de connaissance chatbot (hors runtime
du site).
## Pipeline complet
```
API Strapi
extract-api-data.js → extract/raw/*.json
clean-api-data.js → extract/clean-data/*.json
generate-docs.js → docs/*.md
build-vault.py → vault-grasbot/ (Obsidian structuré + aliases/answers/priority)
GrasBot le lit directement (plus d'étape d'indexation)
```
Depuis avril 2026 (v3 du pipeline GrasBot), **il n'y a plus d'étape
`index_vault.py`**. Le vault Obsidian est la seule source de vérité : il
est lu directement par `llm-api/search.py` au démarrage de l'API.
## Scripts
| Fichier | Rôle |
|---------|------|
| `extract-api-data.js` | **Node**. Fetch des endpoints Strapi → JSON brut (`extract/raw/`). |
| `clean-api-data.js` | **Node**. Nettoyage / normalisation (`extract/clean-data/`). |
| `generate-docs.js` | **Node**. Génération de `.md` par entrée Strapi (`docs/`). |
| `build-vault.py` | **Python**. Lit `docs/` + PDF CV → vault Obsidian (`vault-grasbot/`) avec frontmatter enrichi (aliases, answers, priority). |
| `update-documentation.js` | **Node**. MAJ incrémentale de la doc. |
| `analyse-site-architecture.js` | **Node**. Analyse d'architecture du site. |
## Commande type
```powershell
# Depuis la racine du repo
node strapi_extraction/extract-api-data.js
node strapi_extraction/clean-api-data.js
node strapi_extraction/generate-docs.js
python strapi_extraction/build-vault.py
# (plus d'étape d'indexation — GrasBot lit le vault directement)
# Si GrasBot tourne déjà, recharger le vault sans redémarrer uvicorn :
curl -X POST http://localhost:8000/reload-vault
```
## Données générées
- `strapi_extraction/extract/raw/*.json` — données Strapi brutes.
- `strapi_extraction/extract/clean-data/*.json` — données nettoyées.
- `strapi_extraction/docs/*.md` — documentation Markdown.
- `strapi_extraction/docs/generation-summary.json` — résumé de génération.
- `vault-grasbot/**/*.md` — vault Obsidian consommé par le retrieval GrasBot.
Ces fichiers peuvent être **régénérés** à tout moment ; ne pas les considérer
comme source de vérité sans comparer au CMS.
## Fragilités actuelles
1. **`clean-api-data.js` n'a pas de cleaner `homepages`** : du coup
`generate-docs.js` ne produit jamais `00-homepage.md`. Conséquence :
le CV de la page d'accueil n'arrive pas dans `vault-grasbot/30-Parcours/`
via la chaîne automatique (il y est aujourd'hui via le PDF séparé
`nouveauCV_grascalvet.pdf`).
2. **`glossaire` n'est extrait ni nettoyé** : endpoint absent de la liste
`ENDPOINTS` dans `extract-api-data.js`, cleaner absent aussi. Le dossier
`vault-grasbot/40-Glossaire/` reste vide tant que ce n'est pas réparé.
3. **Accès Strapi v5 flat** : les scripts accèdent en direct à `project.name`,
`project.Resum`, etc. Si on repasse à une configuration v4 ou "populated
with wrapper", il faudra rebrancher en `project.attributes.name`.
Ces points seront corrigés en même temps que l'enrichissement du vault
(glossaire + homepage Strapi → notes `40-Glossaire/` et `30-Parcours/`).
## Liens complémentaires
- Vault + retrieval : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md)
- API LLM : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md)
- Schémas Strapi : [`03-cms-strapi.md`](./03-cms-strapi.md)

View File

@ -1,24 +0,0 @@
# Référence visuelle (captures)
**Dernière mise à jour :** 2026-04-01 — jeu de captures WebP déposé (ID 0105, 0721 ; ID 06 optionnel ignoré).
## Objectif
Conserver un **contexte visuel** aligné sur le code. Les images validées sont dans `captures/`.
## Documents
- [captures/INDEX.md](./captures/INDEX.md) — liste numérotée des captures, noms de fichiers, priorités, cases « Présent ».
- [captures/README.md](./captures/README.md) — conventions de nommage, mise à jour, confidentialité.
## Workflow
Voir `.cursor/skills/site-portfolio-evolution/SKILL.md` : réflexion → choix → modification → validation → si échec retour Git → si OK mise à jour doc et captures concernées.
## Activation du skill dans Cursor
Le fichier est versionné dans le dépôt. Si besoin, ajoutez le dossier aux **Agent Skills** de Cursor ou copiez le skill dans votre répertoire skills personnel pour quil soit proposé automatiquement.
## Pour lassistant
Lors dun chantier UI, **@mentionner** les fichiers dans `captures/` ou citer l**ID** de lindex (ex. ID 13 — fiche compétence desktop).

View File

@ -1,230 +0,0 @@
# Vault Obsidian + retrieval GrasBot (v3 — graph + BM25)
**Créé :** 2026-04-22 (v1 RAG vectoriel)
**Refondu :** 2026-04-22 (v3 — graph + BM25, sans embeddings)
**Statut :** opérationnel (17 projets + 4 compétences + CV + 3 notes techniques + 15 MOCs)
## Raison d'être
Avant ce pipeline, GrasBot interrogeait `mistral:7b` sans aucun contexte —
il répondait de manière générique sur n'importe quoi. Depuis :
- **Modèle chat** : `qwen3:8b` (meilleur en FR, reasoning solide).
- **Base de connaissance** structurée comme vault Obsidian.
- **Pipeline de retrieval** branché : chaque question récupère les notes
pertinentes avant génération.
## Pourquoi `graph + BM25` plutôt que RAG vectoriel ?
La première version (avril 2026, v2) utilisait **ChromaDB** + embeddings
**nomic-embed-text**. Ça marchait, mais :
- **Vault de taille modeste** (~40 notes, ~100 Ko) : la sémantique vectorielle
sur-dimensionne le problème.
- **Retrieval imprévisible** sur vocabulaire précis (une question *« compétences
en IA »* pouvait ne pas remonter la note `ia.md` si son embedding était
dominé par d'autres concepts).
- **Chaîne d'installation lourde** : `chromadb` dépend de `chroma-hnswlib`,
qui nécessite un compilateur C++ sous Windows → blocage fréquent.
- **Coût en VRAM** : `nomic-embed-text` mobilisait ~500 Mo et ~1 s par
requête, inutile à cette échelle.
- **Désynchronisation vault / index** : étape `index_vault.py` oubliable.
En v3, on exploite directement la **structure** du vault : frontmatter YAML
(aliases, answers, domains, tags, priority), wikilinks, MOCs. Le retrieval
est **déterministe**, **traçable** (on sait *pourquoi* une note est remontée),
**instantané** (~50 ms), et ne demande qu'une dépendance : `pyyaml`.
Résultat : GrasBot cite toujours ses sources, et le top-5 est beaucoup
plus prévisible pour une question précise.
## Vault — `vault-grasbot/`
Arborescence :
```
vault-grasbot/
├── 00-MOC/ # hubs thématiques (MOC-Projets, MOC-Ia, MOC-Technique, ...)
├── 10-Projets/ # 17 projets Strapi (push-swap, minishell, ft-transcendence, ...)
├── 20-Competences/ # 4 compétences Strapi (IA, domotique, web, 3D)
├── 30-Parcours/ # CV curaté manuellement (source: manual)
├── 40-Glossaire/ # (vide, prévu pour le content-type glossaire Strapi)
├── 50-Technique/ # auto-doc : architecture-site, grasbot-retrieval, vault-structure
├── README.md # résumé utilisateur (généré)
└── TAXONOMIE.md # vocabulaire contrôlé (domaines, tags, aliases, answers, priority)
```
### Frontmatter YAML
Chaque note porte une en-tête enrichie :
```yaml
---
title: "push_swap"
slug: push-swap
type: projet # projet | competence | parcours | moc | technique
source: strapi/projects # strapi/... | pdf/... | manual | vault/generated
domains: [algorithmique, c, ecole-42]
tags: [42-commun, tri, makefile]
aliases:
- push swap
- push_swap
- algo de tri 42
answers:
- "Parle-moi de push-swap"
- "Comment fonctionne push-swap ?"
priority: 5 # 1..10, boost léger au scoring
linked: ["[[MOC-Projets]]"]
related: ["[[minishell]]"]
updated: 2026-04-22
visibility: public
---
```
Détail des champs et leur usage exact par le retrieval : voir
`vault-grasbot/TAXONOMIE.md` et la note interne
[[vault-structure]] du vault.
### Règle de régénération
`strapi_extraction/build-vault.py` **écrase** les notes dont `source:
strapi/*` ou `source: pdf/*`. Il **ne touche jamais** aux notes
`source: manual`.
Le drapeau `--clean` supprime tout le vault avant régénération : à utiliser
uniquement si on veut repartir de zéro (attention aux notes `manual`).
## Génération — `strapi_extraction/build-vault.py`
Pipeline :
1. Lit les `project-*.md` et `competence-*.md` de `strapi_extraction/docs/`
(eux-mêmes produits par `generate-docs.js` à partir de l'API Strapi).
2. Parse titre, slug, description, détails.
3. Infère `domains` / `tags` via `DOMAIN_KEYWORDS` / `TAG_KEYWORDS`
(ajustables dans le script).
4. **Génère automatiquement** :
- `aliases` à partir du slug + titre + `DOMAIN_ALIASES` (synonymes
courants par domaine).
- `answers` selon le type (projet → *« Parle-moi de X »*, compétence →
*« Quelles sont ses compétences en X ? »*, etc.).
- `priority` heuristique (CV=10, MOCs=7, compétences=7, projets=5).
5. Calcule les `related` par intersection de domaines (top 3).
6. Écrit chaque note avec frontmatter + corps + section *« Liens »* en pied.
7. Génère les MOCs (un par type + un par domaine significatif).
8. Optionnel : convertit le CV PDF via `pypdf` si installé (mais la version
manuelle `cv-grascalvet-fernand.md` avec `source: manual` est
**toujours préservée**).
Commandes :
```powershell
python strapi_extraction/build-vault.py # régénère tout
python strapi_extraction/build-vault.py --dry-run # liste sans écrire
python strapi_extraction/build-vault.py --clean # supprime puis regénère
```
## Retrieval — `llm-api/search.py`
Module lu par `api.py`. Fournit :
- `load_vault()` — lecture mémoïsée du vault (frontmatter YAML + body +
wikilinks). Filtre `visibility: private`.
- `tokenize_fr(text)` — tokenisation FR + normalisations
(`c++``cpp`, split sur `-`/`_`, stop-words).
- `score_note(note, query, tokens, stats)` — score déterministe
multi-signaux. Retourne un `ScoredNote(score, reasons[])`.
- `expand_by_graph(seeds, vault)` — ajoute les voisins (`linked`,
`related`, wikilinks du body) avec un score dérivé de 60 %.
- `search(query, top_k)` — orchestration : score + expansion + dedupe +
top-K.
- `build_prompt(query, notes)` — couple `(system, user)` pour `/api/chat`.
- `generate(system, user)` — appel Ollama `/api/chat`, retourne le texte.
- `answer(query)` — pipeline complet, retourne un dict
`{response, sources, grounded, model, vault_size}`.
### Barème de scoring (documentation opérationnelle)
| Signal | Points | Détails |
|---|---|---|
| Alias match | +10 | 1+ aliases de la note apparaissent dans la question |
| Title exact | +8 | Titre complet dans la query (len ≥ 4) |
| Title tokens | +4 | Au moins 2 tokens du titre dans la query |
| Slug | +8 | Tous les tokens du slug sont dans la query |
| Answers full | +12 | ≥ 3 tokens communs avec une question-type |
| Answers partial | +5 | 2 tokens communs |
| Domains | +5 × n | Par domaine strictement matché |
| Tags | +3 × n | Par tag strictement matché |
| BM25 body | 0..5 | Normalisé |
| Priority | (p-5) × 0.3 | Boost léger si déjà scoré |
| MOC-hub | +1.0 | Si note de type `moc` ET déjà scorée |
| Graph neighbor | 60 % du parent | Via `expand_by_graph` |
Seuil `SEARCH_MIN_SCORE` (défaut 1.0) : en-dessous, le mode *« sans
contexte pertinent »* se déclenche et Qwen3 est invité à ne pas inventer
de faits sur Fernand.
## Compatibilité rétro
L'API garde la signature `GET /ask?q=...`. Le JSON renvoyé a :
- `response` (conservé, consommé par `askAI.js`)
- `sources[]` (enrichi : `slug`, `title`, `type`, `score`, `reasons`, `url`)
- `grounded` (bool — nouveau)
- `model` (conservé)
- `vault_size` (nouveau)
Le champ `rag` de la v2 est remplacé par `grounded` (plus explicite).
## Commandes utiles
```powershell
# Régénérer le vault depuis strapi_extraction/docs/
python strapi_extraction\build-vault.py
# Démarrer l'API locale (pas d'indexation préalable à faire)
cd llm-api ; uvicorn api:app --host 0.0.0.0 --port 8000
# Vérifier la config active et la taille du vault
curl http://localhost:8000/health
# Forcer la relecture du vault sans redémarrer uvicorn
curl -X POST http://localhost:8000/reload-vault
# Tester une question en direct
curl "http://localhost:8000/ask?q=parle-moi+de+push-swap"
```
## Fusion avec un vault Obsidian perso
Deux voies :
- **Vault séparé** (recommandé au début) : on ouvre `vault-grasbot/` comme
vault Obsidian indépendant.
- **Fusion** : on copie `vault-grasbot/` comme sous-dossier d'un vault
existant. Les wikilinks restent valides tant que les noms sont uniques.
Les notes persos doivent porter `source: manual` (évite l'écrasement par
`build-vault.py`) et `visibility: private` (exclues automatiquement du
retrieval par `load_vault()`).
## Limites actuelles
- **Pas de mémoire conversationnelle** : chaque question est indépendante.
- **Pas de streaming** : la réponse arrive en un bloc après 2-10 s.
- **Aliases / answers auto-générés** : c'est une base. Les notes
stratégiques (CV, IA, MOCs) méritent un enrichissement manuel en
passant `source: manual`.
- **`clean-api-data.js` n'extrait pas les `homepages` ni les `glossaires`** :
bug préexistant, à corriger pour enrichir `40-Glossaire/` et la home.
- **Re-chargement manuel** via `POST /reload-vault` (pas encore automatisé
via file watcher).
## Évolutions priorisables
1. Corriger `clean-api-data.js` (homepages + glossaires).
2. Afficher les `sources` citées sous la réponse dans `ChatBot.js`.
3. Ajouter un badge `grounded` pour informer le visiteur de la confiance.
4. Historique conversationnel court (3-4 tours).
5. Streaming Ollama `stream: true` (Server-Sent Events côté API).
6. File watcher sur `vault-grasbot/` qui appelle `POST /reload-vault`
automatiquement.

View File

@ -1,61 +0,0 @@
# Documentation interne du site
**Dernière mise à jour :** 2026-04-24
> **Dans le pack Obsidian** — Même contenu que le dépôt `my-next-site/docs-site-interne/`. Fichiers à la racine de ce coffre : [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md) · [00 Hub.md](../00%20Hub.md) · [SYNC-DOC.md](../SYNC-DOC.md) · [README-racine-depot.md](../README-racine-depot.md).
Ce dossier décrit l'architecture, le fonctionnement et les décisions du projet (Next.js + Strapi + FastAPI/Ollama). Il est destiné à l'équipe et à l'assistant IA pour retrouver vite le contexte.
## Relation avec les autres fichiers
| Fichier / zone | Rôle |
|----------------|------|
| [README-racine-depot.md](../README-racine-depot.md) | Panorama GitHub (copie du `README` racine du dépôt). |
| [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md) | Guide opérationnel : ports, commandes, dépannage, planificateur de tâches Windows. |
| Dossier parent [obsidian-site-docs/](../) | Pack Obsidian (commandes, hub, ce module doc). |
| Ce dossier (`docs-site-interne/`) | Conception : flux de données, schémas CMS, incohérences connues, feuille de route. |
**Règle de maintenance :** éditer dabord le **dépôt** Git, puis [resynchroniser ce dossier](../SYNC-DOC.md) ; si les ports changent, mettre à jour [CONFIGURATION_SITE.md](../CONFIGURATION_SITE.md).
## Index des documents
| Fichier | Contenu |
|---------|---------|
| [01-architecture.md](./01-architecture.md) | Services, ports, flux. |
| [02-frontend-next.md](./02-frontend-next.md) | App Router, routes, fetch Strapi. |
| [03-cms-strapi.md](./03-cms-strapi.md) | Content-types Strapi. |
| [04-api-llm-et-chatbot.md](./04-api-llm-et-chatbot.md) | FastAPI, Ollama, GrasBot. |
| [05-environnement-scripts.md](./05-environnement-scripts.md) | Env, scripts PowerShell. |
| [06-strapi-extraction.md](./06-strapi-extraction.md) | Outils `strapi_extraction/`. |
| [07-reference-visuelle-captures.md](./07-reference-visuelle-captures.md) | Référence visuelle ; dossier `captures/`. |
| [08-vault-obsidian-retrieval.md](./08-vault-obsidian-retrieval.md) | Vault GrasBot + pipeline de retrieval graph + BM25 (v3, sans embeddings). |
| [captures/INDEX.md](./captures/INDEX.md) | Inventaire des captures WebP (noms réels, slugs, priorités). |
| [etat-actuel.md](./etat-actuel.md) | État et dette technique. |
| [feuille-de-route.md](./feuille-de-route.md) | Backlog priorisé. |
| [REFONTE-VISUELLE.md](./REFONTE-VISUELLE.md) | Journal de bord de la refonte UI Stitch. |
| [contact-flow.md](./contact-flow.md) | Contact : e-mail Brevo (remplacement du stockage Strapi). |
| [langfuse-observability.md](./langfuse-observability.md) | Langfuse : traces GrasBot, tuning pipeline. |
## Arborescence utile
```
my-next-site/
├── app/
├── cmsbackend/
├── llm-api/
├── strapi_extraction/
├── start-my-site.ps1
├── stop-my-site.ps1
├── next.config.ts
├── CONFIGURATION_SITE.md
├── obsidian-site-docs/ # pack Obsidian (optionnel)
└── docs-site-interne/
├── captures/ # screenshots de référence (voir INDEX.md)
└── ...
```
**Workflow dévolution (doc + captures + Git) :** skill Cursor `.cursor/skills/site-portfolio-evolution/SKILL.md`.
## Reprise dune session de travail
Pour enchaîner après une pause : lire `feuille-de-route.md` (priorités), `etat-actuel.md`, puis `captures/INDEX.md` si le travail touche lUI. Le skill `site-portfolio-evolution` rappelle la boucle modification → validation → mise à jour doc.

View File

@ -1,419 +0,0 @@
# Refonte visuelle — Direction "Digital Atelier"
**Créé :** 2026-04-22
**Statut :** terminé — 8/8 étapes (2026-04-22)
**Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`).
**Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md).
## 1. Règle de garde-fou (validée utilisateur)
> **On emprunte au Stitch la direction artistique** (palette, typographie, layering tonal, radius, ombres ambient) **et deux ou trois composants signatures** (frame image, pull-quote, bouton jewel). **On n'emprunte ni la mise en page en colonne unique, ni la bottom nav, ni le rythme vertical newsletter.** Le **wallpaper** et le couple **header / drawer mobile** du site restent la fondation.
Concrètement, `stitch_V1/DESIGN.md` fait foi (système). `stitch_V1/code.html` sert d'illustration : il ne fait pas foi quand il contredit `DESIGN.md` (ex. il utilise des `border border-outline-variant` que DESIGN.md interdit — règle "No-Line").
Chaque commit de refonte doit être relisable à l'aune de cette règle : s'il introduit une colonne unique `max-w-xl` globale, une bottom nav ou des bordures 1px opaques, il est à corriger.
## 2. Arbitrages actés
| Sujet | Décision |
|-------|----------|
| Photo de profil home | Portrait carré arrondi `rounded-sheet` (1.5 rem) + frame `bg-primary p-1` |
| Listes portfolio / compétences | Grille asymétrique 2/3 + 1/3 ; carousel réservé aux galeries intra-fiche |
| Orbitron | Retiré partout, remplacé par `Manrope` (titres) + `Newsreader` (corps) |
| Opacité cartes sur wallpaper | 85 % + `backdrop-blur-vellum` (≈ 20 px) pour la "sheet of vellum" |
| Icônes | `Material Symbols Outlined` (déjà utilisées dans la newsletter Stitch et Listmonk) |
| Mode sombre | Light-only pour cette refonte |
| Cercles animés `circle-one` / `circle-two` | Repalette vers `primary` / `primary-container` (au lieu de rose/indigo) |
| Compteur de visites | Migré dans le footer, en `text-[10px] uppercase tracking-[0.3em] text-outline` |
## 3. Design tokens portés dans `tailwind.config.ts`
Voir le fichier pour la liste exhaustive. Rappel des plus utilisés :
| Token | Hex | Usage |
|-------|-----|-------|
| `primary` | `#26445d` | CTAs, headlines, frames image |
| `primary-container` | `#3e5c76` | dégradé CTA, drawer mobile |
| `primary-fixed` | `#cce5ff` | pastilles, badges, barres de citation |
| `secondary` | `#516169` | sous-titres, méta |
| `surface` | `#f8fafa` | base de page alternative au wallpaper |
| `surface-container-low` | `#f2f4f4` | sections secondaires |
| `surface-container-lowest` | `#ffffff` | cartes principales (posées à 85 % sur wallpaper) |
| `on-surface` | `#191c1d` | texte principal (jamais `#000`) |
| `outline-variant` | `#c3c7cd` | ghost-border à 15 % d'opacité max |
Radius additifs : `rounded-sheet` (1.5 rem) pour cartes principales, `rounded-tile` (1 rem) pour éléments imbriqués. Les radius Tailwind (`rounded-xl`, etc.) ne sont pas écrasés pour ne pas casser les composants existants.
Ombres : `shadow-ambient` (40 px / 6 %) pour les cartes flottantes, `shadow-jewel` (4 px offset) pour CTAs primaires.
Polices : `font-headline` (Manrope) et `font-body` (Newsreader), importées via `app/globals.css`.
## 4. Plan d'exécution (8 étapes)
Chaque étape = un lot cohérent + éventuelle mise à jour de `captures/AUDIT-VISUEL.md` et nouvelles captures.
| # | Étape | Fichiers principaux | Statut |
|---|-------|---------------------|--------|
| 1 | Fondations : tokens Tailwind + import polices + icônes | `tailwind.config.ts`, `app/globals.css` | **fait** (2026-04-22) |
| 2 | Garde-fou doc + mise à jour feuille de route | `REFONTE-VISUELLE.md`, `feuille-de-route.md` | **fait** (2026-04-22) |
| 3 | Migration typographique globale (Orbitron → Manrope / Newsreader) | `app/**/*.{tsx,jsx,js}`, `app/assets/main.css` | **fait** (2026-04-22) |
| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/components/NavLink.jsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) |
| 5 | Home : hero vellum, portrait frame, takeaways, pull-quote, CTAs | `app/page.tsx` | **fait** (2026-04-22) |
| 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | **fait** (2026-04-22) |
| 7 | Fiches détail + modale glossaire + GrasBot (jewel flottant) | `app/portfolio/[slug]/page.tsx`, `app/competences/[slug]/page.tsx`, `app/components/ModalGlossaire.tsx`, `app/components/ChatBot.js` | **fait** (2026-04-22) |
| 8 | Contact + Footer éditorial | `app/contact/page.js`, `app/components/ContactForm.tsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) |
## 4 bis. Correctif post-étape 3 (2026-04-22) — cohérence desktop/mobile
Après l'étape 3, retour utilisateur : **couleurs de texte différentes** entre desktop et mobile.
**Cause** : le template Next de base définissait dans `globals.css` un bloc `@media (prefers-color-scheme: dark)` qui basculait `--foreground` à `#ededed` (texte clair) selon le **thème système** de chaque appareil. Avant l'étape 3, les classes `.font-orbitron-*` forçaient `color: #333333` partout et masquaient ce mode sombre. En les retirant, la variable `--foreground` a pris effet et le rendu est devenu dépendant du thème OS (Windows clair → texte foncé ; mobile sombre → texte clair quasi invisible sur wallpaper clair).
**Fix** :
- Retrait du bloc `@media (prefers-color-scheme: dark)` dans `app/globals.css` (incohérent avec l'arbitrage "light-only").
- `--foreground` figé à `#191c1d` (= `on-surface` Stitch, jamais `#000`).
- `body.color` fixé à `#191c1d` en dur pour ne plus dépendre d'aucune variable conditionnelle.
- Classes Tailwind invalides `text-black-500` / `text-black-700` (qui n'existent pas et ne rendaient donc aucune couleur) remplacées par `text-gray-700` dans `app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`.
**Leçon retenue (à appliquer aux étapes suivantes)** : quand on supprime un "masque" CSS (comme la couleur forcée d'Orbitron), toujours vérifier que la valeur qui va ré-émerger par héritage est bien la valeur attendue, pas une variable dépendante du contexte d'exécution.
## 4 ter. Correctif urgent modale glossaire (2026-04-22) — blocage mobile
Après l'étape 4, retour utilisateur sur Samsung S25 Ultra : les mots-clés du glossaire (compétences) ouvrent bien la modale mais **la modale déborde de l'écran**, **la croix de fermeture est hors champ**, impossible de refermer sans recharger la page.
**Causes identifiées** dans `app/components/ModalGlossaire.tsx` (pré-existantes avant la refonte) :
- Carte interne en `w-[114vw] max-w-6xl` : force une largeur > viewport sur mobile (114 % de 400 px = 456 px dans une fenêtre de 400 px), et sur desktop la contrainte est masquée par `max-w-6xl`.
- Hauteur figée `h-[72vh]` sans scroll interne : le contenu est simplement tronqué quand il dépasse.
- Aucune fermeture au tap sur le voile, ni à Esc. Seule issue = bouton `✖` en haut à droite, hors champ sur mobile.
- Bouton de fermeture en `text-sm p-1` : zone tactile < 44 px, sous le seuil Material Design pour le tactile.
**Fix** (anticipe les besoins de l'étape 7) :
- Carte interne : `w-full max-w-4xl max-h-[90vh]` + padding 4 sur le voile pour la marge latérale sur mobile.
- Contenu intérieur en `overflow-y-auto` pour scroll interne si nécessaire.
- Voile cliquable pour fermer, `stopPropagation` sur la carte pour ne pas fermer en interagissant avec.
- Fermeture `Escape` via `keydown` global.
- Bouton de fermeture rond `h-10 w-10` avec Material Symbol `close`, focus-visible, position `absolute top-3 right-3`.
- Alignement palette Stitch : voile `bg-on-surface/75 backdrop-blur-sm`, carte `bg-surface-container-lowest/95 backdrop-blur-vellum shadow-ambient rounded-sheet`, titre `text-primary`, description en `font-body` serif (Newsreader) pour lisibilité, texte `text-on-surface-variant`.
- Ajout de `"use client"` (manquant).
- `role="dialog" aria-modal="true" aria-label={...}` sur le conteneur, `aria-label` explicite sur le bouton de fermeture.
Ce correctif concerne uniquement le composant `ModalGlossaire`. L'étape 7 reprendra la refonte globale de cette zone (cohérence visuelle avec les fiches détail) mais le blocage UX mobile est levé dès maintenant.
## 4 quater. Correctifs post-étape 5 (2026-04-22) — home
Retour utilisateur sur la home fraichement refaite. Trois points, trois causes distinctes :
### Icônes Material Symbols affichées comme texte littéral
Les `<span class="material-symbols-outlined">psychology</span>` affichaient **le mot "psychology"** dans la font par défaut au lieu du glyphe, rendant les takeaways illisibles (texte blanc sur fond bleu = juste du texte). La règle `.material-symbols-outlined` de `app/globals.css` déclarait bien `font-variation-settings`, `display`, `line-height`… mais pas `font-family: 'Material Symbols Outlined'`. L'import Google Fonts pose le `@font-face`, il ne pose pas automatiquement la `font-family` sur la classe — c'est au site de le faire.
**Fix** : ajout de la ligne `font-family: 'Material Symbols Outlined';` dans la règle. Impact : toutes les icônes du site (takeaways, burger, modale glossaire, CTAs hero, icônes CTAs des futures étapes) s'affichent désormais comme icônes.
### Pull-quote "Démarche" peu lisible sur wallpaper
La règle DESIGN.md §5 "Editorial Pull-Quote" dit *"no background card, let the typography breathe on the surface"*. Valide quand la surface de base est un `bg-surface #f8fafa` uni (Stitch newsletter). Chez nous la surface de base est un wallpaper photographique, donc *respirer dessus = se fondre dedans*.
**Fix** : adaptation contextuelle — carte vellum **légère** (`bg-surface-container-lowest/65 backdrop-blur-vellum rounded-tile`, padding réduit, pas de `shadow-ambient`) pour rester lisible sans uniformiser les 3 sections en cartes identiques. La barre gauche `border-l-4 border-primary` et la typo Newsreader italique sont conservées.
**Leçon** : les règles DESIGN.md sont un langage, pas un dogme. Elles supposent une surface de base uniforme. Chaque fois qu'on est sur wallpaper, vérifier si la règle reste applicable telle quelle ou si elle demande une adaptation (ici : carte légère plutôt que zéro carte).
### Espace excessif entre les 3 sections de la home
`gap-8` (32 px) entre les sections + `py-6 md:py-8` sur la pull-quote donnaient ~80 px d'air vertical entre "Trois axes" et "Démarche".
**Fix** : `gap-8``gap-5` sur le container racine (20 px), `py-6 md:py-8` retiré sur la pull-quote (désormais remplacé par le padding interne de sa nouvelle carte). Les paddings internes des cartes (hero `p-6 sm:p-8 md:p-10`, takeaways `p-6 sm:p-8`) sont conservés — l'espace de contenu n'était pas le problème.
## 4 sexies. Séparateurs `<hr>` invisibles dans le hero (2026-04-22)
Le CV rendu par `ReactMarkdown` contient des `---` Markdown convertis en `<hr>`. Par défaut Tailwind Typography les stylise en bordure 1 px `border-gray-300` + `my-8` (32 px). Sur notre carte vellum semi-transparente, cette bordure grise est quasi invisible sur le wallpaper, mais les 64 px de marge verticale (my-8 en haut **et** en bas) restent et donnent l'illusion d'un espace excessif entre les paragraphes du hero.
**Fix (Option B — barre décorative)** : on surcharge `prose-hr` pour transformer la règle en **petite pastille Stitch** centrée. Classes ajoutées sur le wrapper `ReactMarkdown` :
```
prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto
prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full
prose-hr:my-6
```
Résultat : une barre 64 × 2 px, couleur primaire à 30 % d'opacité, arrondie, avec 24 px de marge au lieu de 32 px. Le séparateur redevient un **signal visuel intentionnel** cohérent avec la palette Stitch, et l'espace perçu entre les paragraphes tombe à un niveau confortable sans perdre la structure éditoriale du CV.
**Alternatives considérées** : Option A (`prose-hr:hidden`, perd la structure), Option C (`prose-hr:my-4` seul, garde la bordure grise invisible — n'adresse pas la cause).
## 4 quinquies. Compatibilité Chrome Auto-Translate (2026-04-22)
Les icônes Material Symbols Outlined fonctionnent via **ligatures de font** : un `<span class="material-symbols-outlined">psychology</span>` n'affiche « psychology » qu'en fallback — si la font est chargée, la ligature transforme ce texte en glyphe « cerveau ». Google Chrome propose à l'utilisateur mobile de traduire automatiquement une page dès que sa langue par défaut n'est pas celle du document. Lorsque la traduction s'active, **Chrome réécrit le `textContent`** (« psychology » → « psychologie ») : la ligature ne correspond plus à aucun glyphe dans la font, l'icône redevient du texte brut, et les layouts se décalent.
**Règle permanente pour la refonte** : chaque `<span class="material-symbols-outlined">` doit porter **`translate="no"`** (attribut HTML). Pareil pour les éléments contenant un nom propre qui ne doit pas être déformé (titre du site, nom d'école « 42 », nom de ville, etc.). Le reste du contenu éditorial (CV, descriptions de projets, fiches compétences) reste traductible — la traduction automatique est un vrai plus pour un portfolio qu'on veut accessible à l'international.
Composant wrapper `<Icon>` qui pose automatiquement `translate="no"` envisagé comme DRY à long terme (hors scope actuel).
## 6. Étape 6 — Listes portfolio + compétences (2026-04-22)
Les deux pages liste étaient héritées du design avant refonte : cartes `bg-white/80 rounded-lg` à taille **fixe** (`w-80 h-96` sur portfolio, `max-w-xs…2xl` en cascade sur compétences), `hover:scale-105` qui débordait sous le header, **chaque vignette embarquait un `Swiper` autoplay** (cf. `Carousel.tsx` et `CarouselCompetences.tsx`) — bruit visuel constant, coût réseau (3-5 images × N cartes chargées d'emblée), et incohérence avec l'arbitrage acté § 2 *"carousel réservé aux galeries intra-fiche"*. Sur mobile, la largeur fixe 320 px de la carte portfolio débordait un viewport 360 px + padding.
### Direction Stitch appliquée
**Règle DESIGN.md §6 "No-Grid-Lock"** interdit la grille 3 colonnes symétrique. On adopte une grille **asymétrique 2/3 + 1/3** qui donne un rythme éditorial plutôt qu'un catalogue :
```
md:grid-cols-6, pattern de spans par index modulo 4 :
idx 0 → md:col-span-4 (vedette, 2/3)
idx 1 → md:col-span-2 (1/3)
idx 2 → md:col-span-2 (1/3)
idx 3 → md:col-span-4 (vedette, 2/3)
```
Sur `sm` on bascule en `grid-cols-2` classique (pas de `col-span` tablette pour garder 2 cartes par ligne), sur mobile `grid-cols-1` pleine largeur. Le même pattern est répliqué pour les skeletons de chargement → l'empreinte visuelle est stable pendant le fetch.
### Anatomie de carte "feuillet de vellum"
Toutes les cartes sont des `Link` pleine-carte (plus de `Link` imbriqué ambigu) avec :
- Wrapper : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, `group` pour propager le hover.
- Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (lift subtil + empreinte tactile Stitch) remplace l'ancien `scale-105` qui débordait et cassait l'alignement de la grille.
- Média : `aspect-[4/3]` fixe (plus de hauteurs variables) + `overflow-hidden` + `object-cover` + `group-hover:scale-[1.03]` sur l'image (sensation vitrine, discret).
- Placeholder `material-symbols-outlined image` centré si pas d'image — `translate="no"` en place (§ 4 quinquies).
- Corps : kicker uppercase tracking-[0.3em] (« Projet » / « Compétence ») + titre Manrope extrabold `text-primary` + description Newsreader `text-on-surface-variant` clampée à 3 lignes (`line-clamp-3`, core Tailwind 3.4) pour homogénéiser les hauteurs.
- CTA tertiaire « Découvrir → » / « Explorer → » Manrope uppercase `text-primary`, avec flèche Material Symbols `arrow_forward` qui se décale à droite au hover (`group-hover:translate-x-1`). Icône `translate="no"`.
### États
- **Chargement** : 4 skeletons animés (`animate-pulse bg-surface-container-low/80`) suivant le même pattern de spans que la grille réelle → pas de saut de layout.
- **Vide** : carte centrée avec Material Symbol (`inbox` pour portfolio, `school` pour compétences) + message Newsreader italique. Remplace l'ancien `text-gray-500` orphelin.
### Ce que ça règle
- **Régression mobile** : `w-80 h-96` retiré, la carte prend la largeur de la colonne → plus de débordement S25 Ultra.
- **Bruit visuel** : `Swiper` autoplay retiré des listes, le scroll n'est plus concurrencé par 3-5 carousels qui tournent simultanément.
- **Poids réseau** : une image `loading="lazy"` par carte au lieu de toutes les images de toutes les galeries au chargement initial.
- **Hiérarchie** : les pages liste ont désormais un **en-tête éditorial** (kicker + titre + pitch) cohérent avec le hero de la home.
- **Cohérence Stitch** : palette `primary` / `on-surface-variant`, radius `rounded-sheet`, ombres `shadow-ambient` / `shadow-jewel`, typographie Manrope + Newsreader → alignement 1:1 avec la home.
### Correctif post-étape 6 — wallpaper sur-zoomé sur pages longues
Retour utilisateur une fois `/portfolio` en ligne : le wallpaper apparaît **beaucoup plus zoomé** sur les listes que sur la home, ce qui casse la cohérence visuelle entre les rubriques.
**Cause** : dans `app/layout.tsx`, la div `.bg-wallpaper` était posée en `absolute inset-0` **à l'intérieur** du conteneur grid `min-h-[100dvh]`. Sur la home, le contenu tient en ≈ 1 viewport → le conteneur fait ≈ 1 viewport de haut → `background-size: cover` cadre l'image à sa taille naturelle. Sur les listes portfolio / compétences (en-tête + grille 4+ cartes + footer), le conteneur atteint 2 à 3 viewports de haut → `cover` redimensionne l'image pour couvrir **toute cette hauteur**, ce qui la fait apparaître zoomée et décalée. Effet amplifié au scroll car le wallpaper défile avec la page.
**Fix** : sortir le wallpaper du conteneur grid et le passer en `fixed inset-0 z-0 pointer-events-none`. Il est désormais calé sur le **viewport**, garde ses dimensions naturelles indépendamment de la longueur de la page, et reste stable au scroll. Les cercles animés `circle-one` / `circle-two` restent en `absolute` dans le grid pour conserver le comportement de parallax léger au scroll.
```tsx
<div className="fixed inset-0 z-0 bg-wallpaper pointer-events-none" aria-hidden="true"></div>
```
**Impact transversal** : corrige au passage le même problème latent sur toutes les autres pages longues (futures fiches détail, page contact si elle s'allonge, etc.) — plus besoin d'y repenser page par page.
### Points laissés pour l'étape 7
- Les composants `Carousel.tsx` et `CarouselCompetences.tsx` **ne sont pas touchés** (ils restent utilisés par les pages détail `[slug]/page.tsx`). La refonte visuelle de ces carousels (pagination, flèches, lightbox) se fera dans le lot 7 avec les fiches détail et la modale glossaire.
- Pas de filtre / tri côté liste pour l'instant (les items sont peu nombreux, `order` de Strapi suffit). À ré-évaluer si le catalogue grossit.
### Correctif post-étape 6 — réintroduction du défilement automatique en vignette
Premier retour utilisateur après l'étape 6 : *« j'ai perdu ma fonctionnalité précédente du carousel où les images des vignettes chargées depuis Strapi défilaient »*. L'arbitrage initial *"carousel réservé aux galeries intra-fiche"* (§ 2 — tableau d'arbitrages) était motivé par le bruit visuel et le poids réseau de **plusieurs `Swiper` autoplay** qui tournaient simultanément. Mais le défilement auto des images en vignette faisait partie intégrante de l'expérience de découverte du portfolio pour l'auteur. **L'arbitrage est donc révisé** : on conserve le défilement en vignette, mais via un composant **allégé et cadré** plutôt que le `Carousel.tsx` complet.
**Nouveau composant `app/components/VignetteCarousel.tsx`** — différences délibérées avec `Carousel.tsx` / `CarouselCompetences.tsx` :
- **Pas de flèches de navigation** (`Navigation` module non chargé). Les flèches créaient une **zone de clic ambiguë** avec le `<Link>` englobant la vignette : cliquer sur une flèche déclenchait la navigation vers la fiche détail au lieu de faire défiler le carousel. L'autoplay + le swipe tactile suffisent à l'échelle d'une vignette.
- **Pas de lightbox** (pas de `createPortal` ni de `selectedImage`). L'ouverture plein écran reste une signature de la **fiche détail**, pas de la liste.
- **Pagination bullets Stitch** : `--swiper-pagination-color: #26445d` (primary) et bullets inactifs blancs à 55 % d'opacité, taille 6 px. Surcharge inline via `style={...}` pour éviter de polluer `globals.css` avec un sélecteur `.swiper-pagination-bullet` global qui risquerait de toucher aussi les carousels de la fiche détail.
- **Autoplay 3500 ms** (vs 3000 ms historique) pour laisser plus de temps à la lecture sur les cartes vedette 2/3.
- **`loop` conditionnel** (`images.length > 1`) : sans ça Swiper loggait un warning quand une entrée Strapi n'avait qu'une seule image.
**Intégration dans les listes** : dans `app/portfolio/page.jsx` et `app/competences/page.jsx`, la logique est `length > 1 ? <VignetteCarousel /> : <img statique />` — identique à la version pré-refonte pour les entrées mono-image, plus performante pour les entrées multi-images. Les `alt` sont générés à partir de `img.name` Strapi avec fallback sur le nom du projet / compétence.
**Pourquoi ne pas avoir réutilisé `Carousel.tsx` tel quel** : il embarque flèches + lightbox + CSS de navigation. Dans le contexte d'un `<Link>` englobant, les flèches auraient conflit, et la lightbox serait inaccessible (capturée par le lien). Ajouter des `stopPropagation` sur ces zones nuirait à l'UX "clic n'importe où sur la carte = ouverture de la fiche". Un composant dédié aux vignettes, avec moins de surface d'interaction, est plus clair à maintenir. Les deux composants `Carousel*.tsx` restent intacts pour la fiche détail (étape 7).
Les composants `app/components/Carousel.tsx` et `app/components/CarouselCompetences.tsx` deviennent donc formellement **"carousels de fiche détail"** dans la nomenclature interne ; `VignetteCarousel` est leur petit frère "liste". Un éventuel refactor plus tard pourra fusionner les deux premiers (quasi-doublons) — hors scope actuel.
## 7. Étape 7 — Fiches détail + glossaire + GrasBot flottant (2026-04-22)
L'étape 7 touche cinq composants et introduit un sixième (le FAB). Elle est découpée en cinq sous-lots décrits ci-dessous.
### 7.a Carousels fiche détail (`Carousel.tsx` + `CarouselCompetences.tsx`)
Les deux composants sont quasi-doublons historiques ; on les refait **à l'identique** pour ne pas risquer de régression sur la modale glossaire qui consomme `CarouselCompetences`. Un futur refactor pourra les fusionner une fois le périmètre de la refonte clos (pas dans le scope étape 7).
Changements appliqués :
- **Pagination bullets `primary`** via surcharge inline des variables Swiper (`--swiper-pagination-color: #26445d`, bullets 8 px). Même approche que `VignetteCarousel` pour ne pas polluer `globals.css` avec un sélecteur `.swiper-pagination-bullet` global.
- **Flèches** : on conserve les chevrons natifs Swiper, recolorés via `--swiper-navigation-color: #26445d`, taille 28 px. Remplacement par Material Symbols écarté (demande un override complet du markup via slots Swiper, sans bénéfice visuel proportionnel).
- **Conteneur** : `rounded-tile overflow-hidden shadow-ambient-sm` (vs `rounded-md shadow-md` pré-refonte).
- **`autoplay: 3500` + `disableOnInteraction: false`** : l'autoplay reprend après un swipe manuel plutôt que de rester figé.
- **`loop` conditionnel** (`images.length > 1`) : évite le warning Swiper sur les entrées mono-image.
- **Lightbox Stitch** : voile `bg-on-surface/80 backdrop-blur-sm` (vs `bg-black/10 backdrop-blur-2xl` qui noyait l'image dans un flou 2xl contre-productif), image en `object-contain max-h-[92vh] max-w-[92vw] rounded-sheet shadow-ambient` (ne déforme plus les portraits), bouton close rond 40 px Material Symbol `close` (`translate="no"`), verrouillage du scroll body + fermeture `Escape` + fermeture sur clic voile avec `stopPropagation` sur l'image.
### 7.b Fiche portfolio (`ContentSection.tsx`)
Avant : cartes `bg-white/50 text-blue-700 font-headline font-extrabold` hardcodées, lien externe `bg-white/65 text-red-700 hover:text-blue-700`, pas de retour vers la liste, pas de hiérarchie éditoriale. Contenu Markdown sans `prose`.
Après :
- **Gabarit aligné** sur les listes (étape 6) : wrapper `max-w-3xl` centré, fil d'Ariane minimaliste (pastille ronde `bg-surface-container-lowest/70 backdrop-blur-vellum` avec Material Symbol `arrow_back` + label « Portfolio »), carte vellum principale (`rounded-sheet bg-surface-container-lowest/85 shadow-ambient backdrop-blur-vellum`).
- **En-tête éditorial** : kicker `Projet · Portfolio` uppercase tracking-[0.3em] + titre Manrope extrabold `text-on-surface`.
- **Carousel détail** : `<Carousel>` plein cadre (`h-64 sm:h-80 md:h-96`) réutilise la version 7.a.
- **Corps Markdown en `prose` Stitch** : mêmes overrides que la home (`prose-headings:text-primary`, `prose-p:text-on-surface-variant`, `prose-hr:` transformés en pastille primary via le fix § 4 sexies). Le CV et les fiches partagent désormais la **même charte typographique**.
- **CTA externe jewel** : `bg-primary text-white shadow-jewel` avec Material Symbol `open_in_new` (`translate="no"`) et hover `-translate-y-0.5`. Le `linkText` Strapi reste utilisé, fallback « Voir plus » (au lieu de « Voir plus/lien externe » qui mélangeait 2 intentions).
- **États loading + not-found** en vellum plutôt qu'en ligne `text-gray-500` orpheline. Le not-found propose un retour explicite vers `/portfolio`.
### 7.c Fiche compétences (`ContentSectionCompetences.tsx` + `Container`)
Trois chantiers en un :
**Style** : gabarit identique à 7.b (pastille retour, en-tête vellum, carousel 16/9, prose Stitch). Les `titleClass` / `contentClass` historiques ne sont plus consommés mais on les garde dans l'interface TS pour ne pas casser le call-site.
**Keywords glossaire/chatbot sans styles inline** : avant, `transformMarkdownWithKeywords` injectait `<span style="color: blue">...</span>` — visuellement criard et non thématisable. Après, on injecte les classes `.glossary-keyword` et `.chatbot-keyword` (définies dans `globals.css`), stylées en `color: #26445d` (primary), `text-decoration: underline dotted`, `text-underline-offset: 3px`. Le soulignement pointillé signale l'interactivité sans rompre le flux de lecture, la couleur cohérente avec toute la charte Stitch. Attributs `role="button" tabindex="0"` ajoutés au passage pour l'accessibilité clavier (touche Entrée peut être câblée plus tard si besoin).
**Event listeners scopés** : avant, `document.body.addEventListener("click", ...)` × 2. Après, un seul listener attaché à `contentRef` (ref sur le wrapper prose). Les clics remontent en bubbling depuis les spans Markdown jusqu'au wrapper ; gain en clarté, pas de fuite, pas de risque de conflit avec d'autres zones du DOM. Le handler `keyword` → ouvre `ModalGlossaire`. Le handler `chatbot` → dispatch `CustomEvent("grasbot:open")` sur `window` pour réveiller le FAB global (7.e) au lieu d'instancier un `<ChatBot />` local.
**Container** (`ContentSectionCompetencesContainer.tsx`) : l'état de chargement `⏳ Chargement des compétences...` remplacé par un skeleton vellum (kicker + titre + carousel + 3 lignes de texte en `animate-pulse`), cohérent avec les listes et `ContentSection`.
### 7.d ChatBot (`ChatBot.js`)
Le composant garde son API (`onClose`) mais tout le shell visuel est refait :
| Zone | Avant | Après |
|------|-------|-------|
| Carte | `bg-white/70 shadow-lg rounded-lg border border-gray-300` | `bg-surface-container-lowest/95 backdrop-blur-vellum rounded-sheet shadow-ambient` (= gabarit vellum partagé) |
| Header | `bg-blue-600 text-white` + 💬 emoji + ❌ | `bg-primary text-white` + Material Symbol `smart_toy` + sous-titre Manrope uppercase « Assistant IA locale » + bouton close rond Material Symbol |
| Bulle user | `bg-blue-500 ml-auto` | `bg-primary text-white rounded-sheet` |
| Bulle bot | `bg-gray-500 mr-auto` | `bg-surface-container text-on-surface rounded-sheet` |
| Indicateur attente | `wait...` sur bulle grise | « GrasBot réfléchit... » en italique atténué avec 3 points animés `.dot-1/2/3` existants |
| Input | `border border-gray-300 rounded-l-lg` | `bg-surface-container-low rounded-tile focus-visible:ring-2 focus-visible:ring-primary` |
| Envoyer | `bg-blue-500 text-white ➤` | Bouton rond Material Symbol `send` jewel `bg-primary shadow-jewel hover:-translate-y-0.5`, disabled si vide ou en attente |
Ajouts fonctionnels : auto-scroll en bas à chaque nouveau message (ref `scrollRef`), focus auto sur l'input à l'ouverture, envoi à Entrée (`handleKeyDown`), input désactivé pendant l'attente (plus de double envoi possible), message d'accueil éditorial quand la conversation est vide.
### 7.e GrasBotFab (`GrasBotFab.tsx`)
Nouveau composant monté une seule fois dans `app/layout.tsx` → le chatbot est désormais accessible **depuis toutes les pages**, plus seulement les fiches compétences.
**Anatomie** :
- **Bouton** : `fixed bottom-6 right-6 z-30`, rond 56 px (64 px md), `bg-primary text-white shadow-jewel` avec Material Symbol `smart_toy` (→ `close` quand ouvert). Hover `-translate-y-0.5 hover:bg-primary-container`. `aria-expanded` tenu à jour.
- **Panneau** : `fixed inset-x-4 bottom-24` mobile (plein largeur - 16 px de chaque côté), `sm:inset-auto sm:bottom-24 sm:right-6 sm:w-96 sm:h-[560px]` desktop. `role="dialog"` avec `aria-label`. Monte le `<ChatBot>` refait en 7.d avec `onClose` qui ferme le panneau.
- **Fermeture Esc** globale dès que le panneau est ouvert.
**Flux d'entrée** :
- Clic direct sur le FAB → ouvre le panneau.
- Clic sur `.chatbot-keyword` (« IA locale ») dans une fiche compétence → `ContentSectionCompetences` dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))`, le FAB écoute et ouvre. Pas de Context, pas de store : un `CustomEvent` suffit pour un besoin one-shot et garde les composants découplés.
**Z-index** : le FAB est en `z-30`. Header en `z-20`, drawer mobile en `z-40`. On passe donc le FAB **devant** le header (sinon invisible sous la bande fixe) mais **derrière** le drawer (pour ne pas masquer la navigation). Si le drawer est ouvert, le FAB est recouvert ; acceptable, le drawer étant un mode modal de navigation.
### Points laissés pour l'étape 8
- Fusion de `Carousel.tsx` et `CarouselCompetences.tsx` (doublons) : hors scope étape 7 (pas de gain visuel, risque de régression non justifié). À reprendre en lot "dette technique" après l'étape 8.
- `ModalGlossaire` déjà aligné Stitch au correctif § 4 ter ; aucun changement nécessaire en 7, juste une vérification que son `CarouselCompetences` hérite bien de la lightbox 7.a — oui, c'est automatique.
- Persistance du fil de conversation GrasBot (refresh = historique perdu) : pas demandé, pas introduit. Ajouter un `localStorage` si besoin plus tard.
## 8. Étape 8 — Contact + Footer éditorial (2026-04-22)
Dernière page héritée d'avant refonte. Avant : `app/contact/page.js` utilisait `bg-white/50 rounded-md`, `border-b-4 border-blue-500 pb-2` sous les titres, et `ContactForm` affichait un formulaire `bg-white shadow-lg rounded-lg` bleu Tailwind (`bg-blue-500`). Incohérent avec le reste du site depuis l'étape 5 (tokens Stitch `primary = #26445d`, radius `sheet` / `tile`, ombres `shadow-ambient` / `shadow-jewel`).
### 8.a Page contact (`app/contact/page.js`)
Gabarit aligné sur les listes (étape 6) et le hero home (étape 5) :
- **Colonne utile `max-w-3xl`** (format "lettre", plus intime que les `max-w-6xl` des listes).
- **Hero éditorial vellum** : kicker uppercase tracking-[0.3em] « Contact · Prendre la parole » + titre Manrope extrabold `text-on-surface` « Correspondance » + pitch Newsreader `text-on-surface-variant`. Plus d'emoji `📬` dans le titre (cohérence avec les autres pages qui utilisent Material Symbols, pas des emojis).
- **Section « canaux directs »** : carte vellum principale avec titre secondaire (`text-primary`) puis grille 3 tuiles imbriquées `rounded-tile bg-surface-container-low/80 hover:bg-surface-container/80`. Chaque tuile = pastille primaire ronde avec Material Symbol (`link` pour LinkedIn, `public` pour Facebook, `alternate_email` pour email) + label kicker + handle tronqué + chevron `arrow_forward` qui se décale au hover. Mêmes codes visuels que les cartes vignette portfolio / compétences, sans l'aspect 2/3+1/3 (3 canaux = symétrie assumée).
- **Carte vellum principale pour le formulaire** : `rounded-sheet bg-surface-container-lowest/85 p-5 shadow-ambient backdrop-blur-vellum sm:p-7 md:p-8`, titre secondaire « Écrire un message » `text-primary` + pitch italique Newsreader « Temps de réponse habituel : 48 h ». Le form est rendu **sans** sa propre carte blanche (la carte parente suffit).
- Les **liens externes** (LinkedIn, Facebook) ont `target="_blank" rel="noopener noreferrer"` ; l'email ouvre un `mailto:` standard.
### 8.b ContactForm (`app/components/ContactForm.tsx`)
Refonte interne complète :
- **Suppression** du wrapper `bg-white shadow-lg rounded-lg` : le formulaire vit dans la carte vellum parente (page 8.a). Empêche le "double carton" incohérent avec la charte.
- **Labels visibles** en Manrope uppercase tracking-[0.3em] (au-dessus de chaque champ) — améliore l'accessibilité et aligne sur les kickers de la page. Les placeholders restent là à titre indicatif.
- **Champs** : `bg-surface-container-low/90`, `rounded-tile`, padding `px-4 py-3`, focus `focus-visible:ring-2 focus-visible:ring-primary`, placeholder en `text-on-surface-variant/70`. Le `textarea` gagne `min-h-[9rem] resize-y` (contrôle vertical par l'utilisateur, pas de hauteur figée gênante).
- **Bouton CTA jewel** : `bg-primary text-on-primary shadow-jewel hover:-translate-y-0.5 rounded-tile px-6 py-3 font-headline uppercase tracking-widest` + Material Symbol `send` (`translate="no"`). État `disabled` en `bg-outline-variant/60 text-on-surface-variant cursor-not-allowed` avec icône `hourglass_top`.
- **Feedback status** : plus de chaîne emoji `❌`/`✅`/`⏳` — un petit bandeau `rounded-tile` avec Material Symbol + texte, couleur selon l'état :
- `success``bg-primary-fixed/70 text-on-primary-fixed` + `check_circle`
- `error``bg-error-container text-on-error-container` + `error`
- `loading``bg-surface-container text-on-surface-variant` + `hourglass_top`
- **Accessibilité** : `role="status" aria-live="polite"` sur le bandeau, `autoComplete` (`name`, `email`), `noValidate` sur le form (on fait la validation en JS pour maîtriser les messages FR). L'ancien `isSuccess: boolean | null` à double état est remplacé par un `statusKind: "idle" | "loading" | "success" | "error"` unique, plus lisible.
### 8.c Footer (`app/components/Footer.jsx`)
Avant : `bg-white/50 rounded-lg backdrop-blur` + `text-gray-700`. Déjà partiellement migré (font-headline, `visite n°` en kicker) mais surface + radius hors charte.
Après : **carte vellum légère** centrée, sans ombre ambient (le footer ne doit pas flotter autant que le contenu principal) :
- Conteneur : `rounded-tile bg-surface-container-lowest/70 backdrop-blur-vellum px-6 py-5 text-center`.
- Trois lignes éditoriales :
1. **Signature** Manrope `text-primary` « Fernand Gras-Calvet » (identité).
2. **Pitch** Newsreader italic `text-on-surface-variant` « Portfolio — Étudiant 42 Perpignan · © {year} » (ton éditorial cohérent avec le hero home).
3. **Compteur** de visites Manrope `text-[10px] uppercase tracking-[0.3em] text-outline` (méta discrète).
- **SSR-safe** : `new Date().getFullYear()` est calculé côté client (via `useState` init + `useEffect`) pour éviter un mismatch SSR / CSR si l'année bascule pile à minuit.
### Ce que ça règle
- **Dernière page hors charte migrée** : le site est désormais 100 % « Digital Atelier » (home, layout, listes, fiches, glossaire, chatbot, contact, footer).
- **Cohérence typo** : plus aucune référence à `font-headline font-extrabold border-b-4 border-blue-500 pb-2` (motif ancien).
- **Cohérence iconographique** : plus aucun emoji `📬 📩 🚀` résiduel dans les titres de page contact / form ; tout est passé en Material Symbols (seuls emojis acceptés = message utilisateur dans le chatbot, et l'emoji `📅` dans `sendMessage` qui reste un détail de payload côté Strapi, pas d'affichage direct).
- **Accessibilité contact** : labels visibles, `role="status"`, `aria-live="polite"`, `autoComplete` — améliore l'usage clavier / lecteur d'écran.
- **Footer** : plus de double lecture (`text-gray-700` sur `bg-white/50` contrastait mal sur wallpaper clair) — `text-on-surface-variant` sur vellum reste lisible partout.
### Points laissés en dehors de l'étape 8
- **Persistance du compteur de visites** côté serveur (Strapi) : hors scope refonte visuelle. Reste en `localStorage` comme avant.
- **Validation serveur des champs du form** (anti-spam, honeypot, reCAPTCHA) : hors scope refonte visuelle. Strapi ne filtre pour l'instant que sur la structure JSON attendue.
- **Fusion Carousel.tsx / CarouselCompetences.tsx** : reste en dette technique (déjà noté §7).
## 9. Post-refonte — Contact effectif via Brevo (2026-04-23)
La refonte visuelle a laissé le formulaire de contact en état "joli mais non fonctionnel" : il stockait les messages dans Strapi (content-type `message`), à consulter via `/admin/messages` (page publique, non protégée). Aucune notification.
**Décision** (discutée avec l'utilisateur) : supprimer le passage par Strapi, passer à une **notification email directe** via l'**API HTTP Brevo** (compte existant pour la newsletter). Plus simple, moins de surface d'attaque, notification immédiate.
### Changements
- **Nouveau** : `app/api/contact/route.ts` (Next.js App Router, runtime Node) — reçoit le form, valide, applique honeypot + rate-limit, appelle `POST https://api.brevo.com/v3/smtp/email`, retourne `{ ok: true | false, error }`.
- **Modifié** : `app/components/ContactForm.tsx` — appel vers `/api/contact` au lieu de `sendMessage(...)`. Ajout d'un **champ honeypot** `website` caché (position absolue hors écran + `aria-hidden` + `tabindex=-1`). Codes d'erreur serveur mappés en messages FR.
- **Supprimé** : `app/utils/sendMessage.ts` (plus utilisé), `app/admin/messages/page.tsx` (plus de consultation Strapi nécessaire, et cette page était exposée publiquement sans auth).
- **Supprimé côté Strapi** : `cmsbackend/src/api/message/` (content-type + routes + services + controllers). La table SQLite `messages` est laissée en place (orpheline, inoffensive).
- **Nouveau** : `.env.example` en racine pour documenter les variables requises, `contact-flow.md` pour l'architecture complète.
### Variables d'env (.env.local)
```env
BREVO_API_KEY=xkeysib-...
CONTACT_FROM_EMAIL=<expéditeur vérifié Brevo>
CONTACT_FROM_NAME=Portfolio — nouveau message
CONTACT_TO_EMAIL=grascalvet.fernand@gmail.com
CONTACT_TO_NAME=Fernand Gras-Calvet
```
### Anti-abus
- **Honeypot** : champ caché `website`. Si rempli → 200 silencieux + log warning (pas d'erreur pour ne pas signaler aux bots).
- **Rate-limit** : 3 envois / IP / 10 min (Map en mémoire). Limité en serverless multi-instance — acceptable pour un portfolio.
- **Validation serveur** : longueurs (120 / 160 / 5000), regex email, codes d'erreur en `UPPER_SNAKE_CASE` stables.
### Email reçu
- Sujet : `Nouveau message portfolio — {Nom}`
- Expéditeur : `CONTACT_FROM_NAME <CONTACT_FROM_EMAIL>`
- Reply-To : email du visiteur (un clic "Répondre" dans Gmail et le mail part au visiteur).
- Corps HTML : carte Stitch simplifiée (primary/secondary/surface-container-low) avec table nom/email/date/IP + zone message `pre-wrap`.
### Leçons retenues
1. **Surveiller les pages admin "dev"** qui fuitent en prod : `/admin/messages` listait les emails de tous les visiteurs en clair, sans auth. Avec la page supprimée, plus d'exposition.
2. **Clé API fuitant en clair dans un chat** : considérer comme compromise et régénérer. Documenté dans `contact-flow.md` § "Sécurité — rappels".
3. **Pour un besoin "1 seul destinataire" avec faible volume**, une route Next + API transactionnelle bat clairement une solution SMTP + plugin Strapi : moins de pièces, moins de config, même garantie de délivrabilité.
Voir `contact-flow.md` pour la procédure de test, les codes d'erreur, et le rollback éventuel.
## 5. Checklist relecture (à passer à la fin de chaque étape)
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
- [ ] Aucune bottom nav fixe (déjà couvert par header + drawer).
- [ ] Aucune bordure 1px pleine (`border border-*`) sur un composant de contenu — sauf ghost-border à 15 % max.
- [ ] Aucune utilisation de `#000` pur — toujours `on-surface` / `on-background`.
- [ ] Le wallpaper reste perceptible entre / autour des cartes.
- [ ] La hiérarchie Manrope / Newsreader est respectée (pas de Orbitron résiduel).
- [ ] Les CTAs principaux ont `shadow-jewel`.
- [ ] Radius Stitch (`rounded-sheet` / `rounded-tile`) utilisés sur les cartes de la refonte.
- [ ] Chaque nouvelle icône Material Symbols Outlined ajoutée porte `translate="no"` (voir §4 quinquies).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

View File

@ -1,116 +0,0 @@
# Audit visuel et mise en forme
**Dernière mise à jour :** 2026-04-22
**Captures :** [INDEX.md](./INDEX.md)
## Objectif
Recenser les **problèmes visuels** et de **mise en forme** pour les **corriger** avec traçabilité (code : `app/layout.tsx`, `app/page.tsx`, `app/components/Footer.jsx`, `app/globals.css`).
## Légende
| Champ | Sens |
|--------|------|
| **ID** | Numéro dans [INDEX.md](./INDEX.md) |
| **Constats** | Points validés ensemble |
| **Piste / correctif** | Idée ou implémentation |
| **Statut** | `OK` / `à traiter` / `fait` |
---
## ID 01 — Header et navigation desktop
| Fichier | `01-layout-header-nav-desktop.webp` |
| **Navigateur** | Google Chrome |
| **Constats** | Header OK, mais depuis la normalisation des conteneurs (option 1 zoom) le texte de plusieurs pages se retrouvait **collé** au header : le `<header>` étant `position: fixed`, il est **hors du flux** et ne prend pas de place dans la grille racine. Seule la home compensait, via un `mt-12` local. |
| **Piste / correctif appliqué (2026-04-22)** | Compensation **centralisée** dans `app/layout.tsx` : `<main>` en `pt-20 md:pt-24` (couvre la hauteur du header `h-16` + un peu dair). Suppression du `mt-12` dupliqué sur la home (`app/page.tsx`) pour ne pas cumuler. Toutes les pages héritent désormais du même espace sous le header, plus besoin de compenser page par page. |
| **Statut** | `fait` |
---
## ID 02 — Menu mobile + header (et retour UX burger)
| Fichier | `02-layout-nav-mobile-ouverte.webp` |
| **Route** | `/` |
| **Code** | `app/layout.tsx` (header, overlay menu) |
### Constats (validés)
- Bouton **menu burger** trop imposant et « gris » (gros bloc) — à rendre plus discret.
- Côté **dézoom** (pinch) sur mobile : le **texte Strapi** + **header** se décalent à gauche, effet d**incohérence** visuelle.
### Piste / correctif appliqué (2026-04-28) — option 1 (CSS / layout seulement)
- **Burger** : à affiner (taille, contraste) si besoin — voir capture actuelle.
- **Décalage au zoom (sans bloquer le zoom)** : pas de `maximum-scale` ni de meta viewport qui impose le zoom. Renfort uniquement côté **CSS** :
- `html` / `body` : `overflow-x: hidden`, `min-width: 0`, `max-width: 100%` dans `app/globals.css` ;
- grille racine, `<header>`, `<main>`, footer : `min-w-0`, `w-full` / `max-w-full` là où cest utile (`app/layout.tsx`, `app/components/Footer.jsx`) ;
- **`.bg-wallpaper`** : `width: 100%` (plus de `200vw` sur mobile) pour éviter le double de largeur de page et le débordement au pinch-zoom ;
- page d**accueil** : conteneur principal + bloc texte en `min-w-0` / `max-w-full` (`app/page.tsx`).
**Accessibilité** : le **verrouillage total du zoom** na **pas** été retenu (hors demande) ; on privilégie la mise en page. Si le dézoom reste gênant, on pourra itérer (contraintes sur le markdown, etc.).
### Refonte du menu mobile en drawer latéral (2026-04-22)
- **Ancien** : panneau mi-hauteur côté gauche + grosse croix `✖` → peu esthétique.
- **Nouveau** (`app/layout.tsx`) : **tiroir gauche** `role="dialog" aria-modal="true"`, **largeur 70 %** (capée à `max-w-sm`), hauteur pleine, fond **sombre translucide** (`bg-gray-900/70 backdrop-blur-md`), bordure droite fine.
- **Animation** : `transition-transform` + `-translate-x-full``translate-x-0`, 300 ms, easing `ease-out` ; voile en fondu (`transition-opacity`). Respect de `prefers-reduced-motion` (voir `.mobile-drawer-*` dans `app/globals.css`).
- **Fermeture** : **pas de croix** ; tap sur le **voile**, **Échap**, **clic sur un lien**, **re-tap sur le burger** (qui affiche `✕` le temps de louverture), et **auto-fermeture après 4 s** dinactivité (timer remis à zéro à chaque interaction / touch dans le tiroir, nettoyé à la fermeture, et relancé à louverture).
- **Accessibilité** : `aria-label` / `aria-expanded` / `aria-controls` sur le burger ; le focus revient sur le burger après fermeture ; fermeture automatique si lécran passe en ≥ md (resize).
| **Statut** | `fait` — drawer latéral, auto-close 4 s, fond sombre translucide, 70 % |
---
## ID 03 — Accueil hero desktop
| Fichier | `03-accueil-hero-desktop.webp` |
| **Route** | `/` |
| **Code** | `app/page.tsx`, `app/layout.tsx` |
| **Constats** | Rien à corriger spécifiquement : les ajustements déjà faits côté **layout** (compensation du header `pt-20 md:pt-24`, conteneur `min-w-0 max-w-full`) et côté **home** (suppression du `mt-12` dupliqué, `min-w-0 max-w-full` sur le `<main>` et la zone markdown) couvrent la mise en forme. Hero, photo, titre et CV saffichent correctement au-dessus du pli. |
| **Statut** | `OK` (pris en charge par les correctifs ID 01 / option 1) |
---
## ID 04 — Accueil hero mobile
| Fichier | `04-accueil-hero-mobile.webp` |
| **Route** | `/` |
| **Code** | `app/page.tsx`, `app/layout.tsx` |
| **Constats** | Couvert par les correctifs globaux : **drawer** latéral (ID 02), **compensation header** (`pt-20 md:pt-24`), conteneurs en `min-w-0 / max-w-full`, `.bg-wallpaper` ramené à `width: 100%` sur mobile. Hero lisible, sans débordement au zoom, texte non collé au header. |
| **Statut** | `OK` (pris en charge par les correctifs ID 01 / ID 02 / option 1) |
---
## ID 05 à 21 — Passage global en `OK`
Les captures suivantes **nont pas révélé de problème spécifique** après les correctifs **option 1** (CSS / layout — overflow, `min-w-0`, `.bg-wallpaper`), **ID 01** (compensation du header `pt-20 md:pt-24`) et **ID 02** (refonte du menu mobile en drawer latéral). Elles sont donc marquées **`OK`** à ce stade. Elles restent sujettes à la **refonte visuelle plus large** à venir, qui pourra les reprendre une par une.
| ID | Section | Route | Fichier | Statut |
|----|---------|-------|---------|--------|
| 05 | Accueil page longue | `/` | `05-accueil-page-pleine-desktop.webp` | `OK` |
| 06 | Accueil chargement | `/` | *(non fourni — optionnel)* | `ignoré` |
| 07 | Portfolio liste desktop | `/portfolio` | `07-portfolio-liste-desktop.webp` | `OK` |
| 08 | Portfolio liste mobile | `/portfolio` | `08-portfolio-liste-mobile.webp` | `OK` |
| 09 | Portfolio fiche desktop | `/portfolio/slug` | `09-portfolio-detail-desktop-presentation-ecole-42.webp` | `OK` |
| 10 | Portfolio fiche mobile | `/portfolio/slug` | `10-portfolio-detail-mobile-presentation-ecole-42.webp` | `OK` |
| 11 | Compétences liste desktop | `/competences` | `11-competences-liste-desktop.webp` | `OK` |
| 12 | Compétences liste mobile | `/competences` | `12-competences-liste-mobile.webp` | `OK` |
| 13 | Compétences fiche desktop | `/competences/slug` | `13-competences-detail-ia-desktop.webp` | `OK` |
| 14 | Compétences fiche mobile | `/competences/slug` | `14-competences-detail-ia-mobile.webp` | `OK` |
| 15 | GrasBot ouvert desktop | `/competences/slug` | `15-competences-grasbot-ouvert-desktop.webp` | `OK` |
| 16 | Glossaire modal desktop | `/competences/slug` | `16-competences-glossaire-ouvert-desktop.webp` | `OK` |
| 17 | Contact formulaire desktop | `/contact` | `17-contact-formulaire-desktop.webp` | `fait` (étape 8, 2026-04-22) |
| 18 | Contact formulaire mobile | `/contact` | `18-contact-formulaire-mobile.webp` | `fait` (étape 8, 2026-04-22) |
| 19 | Footer desktop | `/` | `19-layout-footer-desktop.webp` | `fait` (étape 8, 2026-04-22) |
| 20 | Compteur visites desktop | `/` | `20-layout-compteur-visites-desktop.webp` | `OK` |
| 21 | ~~Admin messages desktop~~ | ~~`/admin/messages`~~ | `21-admin-messages-desktop.webp` | `obsolète` (route supprimée le 2026-04-23, voir `contact-flow.md`) |
---
## Suite
- **Étape 8 Digital Atelier bouclée** (2026-04-22) : contact + formulaire + footer migrés à la charte Stitch (voir `../REFONTE-VISUELLE.md §8`).
- **Formulaire de contact rendu effectif** (2026-04-23) : envoi via **Brevo** au lieu de Strapi. Content-type `message` supprimé, page `/admin/messages` supprimée, honeypot + rate-limit ajoutés. Voir `../contact-flow.md` et `REFONTE-VISUELLE.md §9`.
- Prendre de **nouvelles captures** 17 / 18 / 19 pour figer le rendu post-refonte (remplacement des WebP existants dans ce dossier `captures/`). La capture 21 est désormais **obsolète** (route supprimée).
- Dette technique identifiée : fusion `Carousel.tsx` / `CarouselCompetences.tsx` (doublons), persistance serveur du compteur de visites. Anti-spam formulaire : **fait** (honeypot + rate-limit, 2026-04-23).

View File

@ -1,63 +0,0 @@
# Index des captures — référence visuelle
**Dernière mise à jour :** 2026-04-01 — jeu de captures WebP déposé dans ce dossier (sauf ID 06, optionnel).
**Priorité** : **P0** = contexte global ; **P1** = refonte UI ; **P2** = optionnel.
## Fichiers réels dans le dépôt (état actuel)
Les noms suivants sont ceux présents sur disque. Les routes **dynamiques** utilisent deux conventions de slug (voir section Conventions slug).
| ID | Section | Route | Fichier dans le dépôt | P | Statut |
|----|---------|-------|------------------------|---|--------|
| 01 | Header + nav desktop | / | `01-layout-header-nav-desktop.webp` | P0 | OK |
| 02 | Menu mobile ouvert | / | `02-layout-nav-mobile-ouverte.webp` | P0 | OK |
| 03 | Accueil above the fold | / | `03-accueil-hero-desktop.webp` | P0 | OK |
| 04 | Accueil above the fold | / | `04-accueil-hero-mobile.webp` | P0 | OK |
| 05 | Accueil page longue | / | `05-accueil-page-pleine-desktop.webp` | P1 | OK |
| 06 | Accueil chargement | / | *(non fourni — optionnel)* | P2 | Ignoré |
| 07 | Portfolio liste | /portfolio | `07-portfolio-liste-desktop.webp` | P0 | OK |
| 08 | Portfolio liste | /portfolio | `08-portfolio-liste-mobile.webp` | P0 | OK |
| 09 | Portfolio fiche | /portfolio/slug | `09-portfolio-detail-desktop-presentation-ecole-42.webp` | P0 | OK |
| 10 | Portfolio fiche | /portfolio/slug | `10-portfolio-detail-mobile-presentation-ecole-42.webp` | P1 | OK |
| 11 | Compétences liste | /competences | `11-competences-liste-desktop.webp` | P0 | OK |
| 12 | Compétences liste | /competences | `12-competences-liste-mobile.webp` | P0 | OK |
| 13 | Compétences fiche | /competences/slug | `13-competences-detail-ia-desktop.webp` | P0 | OK |
| 14 | Compétences fiche | /competences/slug | `14-competences-detail-ia-mobile.webp` | P1 | OK |
| 15 | GrasBot ouvert | /competences/slug | `15-competences-grasbot-ouvert-desktop.webp` | P1 | OK |
| 16 | Glossaire modal | /competences/slug | `16-competences-glossaire-ouvert-desktop.webp` | P1 | OK |
| 17 | Contact formulaire | /contact | `17-contact-formulaire-desktop.webp` | P0 | OK |
| 18 | Contact formulaire | /contact | `18-contact-formulaire-mobile.webp` | P0 | OK |
| 19 | Footer | / | `19-layout-footer-desktop.webp` | P1 | OK |
| 20 | Compteur visites | / | `20-layout-compteur-visites-desktop.webp` | P2 | OK |
| 21 | Admin messages | /admin/messages | `21-admin-messages-desktop.webp` | P2 | OK |
**Slugs illustrés :** projet `presentation-ecole-42`, compétence `ia` (Strapi).
## Conventions slug (nouvelles captures)
| Zone | Motif |
|------|--------|
| Portfolio desktop | `09-portfolio-detail-desktop-{slug}.webp` |
| Portfolio mobile | `10-portfolio-detail-mobile-{slug}.webp` |
| Compétences desktop | `13-competences-detail-{slug}-desktop.webp` |
| Compétences mobile | `14-competences-detail-{slug}-mobile.webp` |
## Ordre conseillé (prise de vue)
0102 chrome, 0306 accueil, 0710 portfolio, 1116 compétences, 1718 contact, puis 1921.
## Repères code
| ID | Fichiers |
|----|----------|
| 0102 | `app/layout.tsx` |
| 0306 | `app/page.tsx` |
| 0708 | `app/portfolio/page.jsx` |
| 0910 | `app/portfolio/[slug]/page.tsx`, `ContentSection.tsx` |
| 1112 | `app/competences/page.jsx` |
| 1316 | `ContentSectionCompetences*.tsx`, `ChatBot.js`, `ModalGlossaire.tsx` |
| 1718 | `app/contact/page.js`, `ContactForm.tsx` |
| 19 | `app/components/Footer.jsx` |
| 20 | `app/layout.tsx` |
| 21 | `app/admin/messages/page.tsx` |

View File

@ -1,17 +0,0 @@
# Captures d'écran — référence visuelle
**Dernière mise à jour :** 2026-04-01
Ce dossier contient les **images WebP de référence** du site (état validé). La liste à jour : **[INDEX.md](./INDEX.md)**.
## Convention générale
Format : `{numero}-{zone}-{detail}-{viewport}.webp` — détails et **deux motifs pour les fiches dynamiques** (portfolio vs compétences) dans INDEX.md.
## Mise à jour
Après changement visuel validé : remplacer les WebP concernés et ajuster INDEX.md si besoin.
## Données sensibles
Sur `/admin/messages`, flouter ou données fictives sur les captures.

View File

@ -1,186 +0,0 @@
# Flux "Contact" — notification par email (Brevo)
**Créé :** 2026-04-23
**Statut :** en production
## Vue d'ensemble
Le formulaire de contact du site envoie un **email de notification** sur la boîte Gmail personnelle (`grascalvet.fernand@gmail.com`), via l'**API HTTP transactionnelle de Brevo**.
**Plus aucun passage par Strapi pour les messages.** Le content-type `message` a été supprimé (ainsi que la page admin associée) le 2026-04-23. La table SQLite `messages` reste présente en base pour l'historique, mais n'est plus écrite ni lue.
```
Visiteur → /contact (Next.js front)
POST /api/contact (Next.js App Router, server runtime)
Validation + honeypot + rate-limit
POST https://api.brevo.com/v3/smtp/email
Gmail : CONTACT_TO_EMAIL
```
## Pourquoi cette architecture
### Simplification par rapport à l'ancien flux
Avant : `Front → Strapi POST /api/messages → SQLite` + consultation via `/admin/messages` (page publique, non protégée).
Problèmes :
- **Pas de notification** : il fallait penser à consulter l'admin pour voir les nouveaux messages.
- **Page admin exposée** : `/admin/messages` accessible à n'importe qui en clair (pas d'authentification). Fuite potentielle d'emails privés des visiteurs.
- **Content-type dédié** pour un usage limité (3 champs, pas d'historique utile).
Après : notification directe sur Gmail, zéro stockage intermédiaire, zéro page à sécuriser.
### Pourquoi l'API HTTP Brevo plutôt que SMTP
Discussion menée le 2026-04-23. Résumé :
| Critère | SMTP | API HTTP |
|---------|------|----------|
| Simplicité plugin Strapi | ✓ | (custom) |
| Port potentiellement bloqué | oui | non (HTTPS 443) |
| Débogage JSON | non | ✓ |
| Notre besoin (1 endroit, 1 mail) | ok | ok |
Comme on ne passe plus par Strapi, l'avantage "plugin Strapi" disparaît. On a pris **l'API HTTP** pour rester 100 % en HTTPS (aucun souci de port) et avoir des erreurs JSON lisibles.
## Fichiers concernés
| Fichier | Rôle |
|---------|------|
| `app/api/contact/route.ts` | Endpoint serveur Next.js qui reçoit le form et appelle Brevo |
| `app/components/ContactForm.tsx` | UI côté client, honeypot, gestion du status |
| `.env.local` | Secrets + config (non committé) |
| `.env.example` | Template documentaire des variables à remplir |
## Variables d'environnement
À définir dans `.env.local` (voir `.env.example` pour la structure) :
| Variable | Description | Où l'obtenir |
|----------|-------------|--------------|
| `BREVO_API_KEY` | Clé API Brevo | [app.brevo.com → SMTP & API → API Keys](https://app.brevo.com/settings/keys/api) |
| `CONTACT_FROM_EMAIL` | Expéditeur **vérifié** | Senders, Domains & IPs → Senders (pastille SPF/DKIM verte) |
| `CONTACT_FROM_NAME` | Nom affiché dans Gmail | Texte libre, ex. "Portfolio — nouveau message" |
| `CONTACT_TO_EMAIL` | Destinataire final | L'adresse Gmail personnelle |
| `CONTACT_TO_NAME` | Nom du destinataire | Texte libre |
**L'expéditeur doit être vérifié dans Brevo** : sinon Brevo renvoie une erreur au `POST`. On peut réutiliser l'expéditeur newsletter (pas de cloisonnement côté Brevo) et surcharger le nom d'affichage via `CONTACT_FROM_NAME`.
## Anti-abus
### Honeypot
Un champ caché `website` est ajouté au formulaire :
- Invisible aux humains (`position: absolute; left: -10000px; width: 1px`).
- Masqué aux lecteurs d'écran (`aria-hidden="true"`).
- Ignoré par le clavier (`tabindex="-1"`, `autocomplete="off"`).
Les bots qui parsent le DOM remplissent systématiquement tous les champs texte. Côté serveur, si `website` est non vide :
- On **renvoie un 200 silencieux** (pas de signal d'erreur aux bots → ils ne retentent pas).
- On **n'envoie pas d'email**.
- On **logue** un warning avec l'IP pour monitoring.
### Rate-limit
`app/api/contact/route.ts` limite à **3 envois par IP par fenêtre de 10 minutes**, via une `Map` en mémoire :
```ts
const RATE_LIMIT_MAX = 3;
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;
```
Au-delà : réponse `429 Too Many Requests` avec header `Retry-After`.
**Limite connue** : en déploiement serverless avec plusieurs instances (Vercel free, Cloudflare Workers), la `Map` n'est pas partagée — le rate-limit est "best effort". Pour un portfolio, c'est acceptable. Pour du vrai ship : passer à Redis ou [Upstash](https://upstash.com/) (wrapper compatible serverless).
### Validation
Côté serveur (défense en profondeur, jamais faire confiance au client) :
- Champs requis non vides : `name`, `email`, `message`.
- Longueurs max : `name ≤ 120`, `email ≤ 160`, `message ≤ 5000` caractères (retourne `413`).
- Format email : regex `^[^\s@]+@[^\s@]+\.[^\s@]+$` (pragmatique, pas RFC 5322 stricte).
Les codes d'erreur renvoyés au client sont en `UPPER_SNAKE_CASE` (stables pour l'i18n future) : `MISSING_FIELDS`, `INVALID_EMAIL`, `TOO_LONG`, `RATE_LIMITED`, `BREVO_ERROR`, `BREVO_UNREACHABLE`, `SERVER_MISCONFIGURED`, `INVALID_JSON`.
## Format de l'email reçu
- **Sujet** : `Nouveau message portfolio — {Nom du visiteur}`
- **Expéditeur** : `{CONTACT_FROM_NAME} <{CONTACT_FROM_EMAIL}>`
- **Reply-To** : email du visiteur → cliquer "Répondre" dans Gmail écrit directement au visiteur.
- **Corps HTML** : carte Stitch simplifiée (palette primary / secondary) avec table récap (nom, email, date, IP) + zone message `pre-wrap`.
- **Corps texte** (fallback) : version plain text équivalente.
- **Tags Brevo** : `["portfolio", "contact-form"]` pour filtrer dans les statistiques Brevo si besoin.
## Procédure de test
### Local (développement)
1. S'assurer que `.env.local` contient les 5 variables requises + une clé Brevo valide.
2. `npm run dev` dans le repo Next (port 3000).
3. `/contact` → remplir le formulaire avec une **adresse email qu'on contrôle** (pour vérifier le Reply-To).
4. Vérifier :
- Feedback visuel "Message envoyé" en succès Stitch vert.
- Réception dans Gmail avec expéditeur = `CONTACT_FROM_NAME`.
- Clic "Répondre" → destinataire = l'email saisi dans le formulaire.
### Honeypot
1. Ouvrir la console DevTools, onglet Elements.
2. Trouver l'input caché `<input name="website">` (dans un div `position: absolute; left: -10000px`).
3. Y saisir une valeur (ex. "bot").
4. Soumettre le formulaire normalement.
5. Résultat attendu : UI affiche "Message envoyé" (succès silencieux) MAIS **aucun email n'arrive** dans Gmail.
6. Vérifier le warning serveur dans les logs Next (`[/api/contact] Honeypot déclenché`).
### Rate-limit
1. Envoyer un formulaire valide 4 fois de suite (modifier juste le message à chaque fois).
2. Au 4ᵉ envoi : réponse `429`, UI affiche "Trop d'envois depuis votre IP. Réessayez dans quelques minutes."
3. Attendre 10 min puis réessayer → doit repasser.
## Nettoyage de l'ancienne table SQLite (optionnel)
La table `messages` existe toujours dans `cmsbackend/.tmp/data.db` (ou le fichier configuré). Strapi ne la touche plus. Pour la supprimer proprement :
```bash
# Arrêter Strapi d'abord !
cd cmsbackend
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages;"
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages_cmps;"
# Reprise normale
npm run develop
```
Non fait par défaut : la table reste orpheline mais inoffensive. Ça évite toute régression en cas de rollback éventuel.
## Quotas Brevo
Le **plan gratuit Brevo** inclut traditionnellement **300 emails transactionnels par jour**, partagés avec la newsletter. Pour un portfolio qui reçoit 0 à 5 messages par semaine, c'est largement suffisant.
Si le site reçoit un jour beaucoup plus de trafic (ou si Brevo change ses quotas gratuits), surveiller :
- **Brevo → Transactional → Statistics** : nombre d'envois, taux d'erreur.
- **Brevo → SMTP & API → Statistics** : quota quotidien consommé.
## Rollback
Si le flux Brevo tombe en panne (compte suspendu, quota dépassé, clé compromise) et qu'on veut **temporairement** retomber sur le flux Strapi ancien :
1. `git checkout <commit-pre-refonte-contact>` sur les fichiers : `app/api/contact/route.ts` (supprimer), `app/components/ContactForm.tsx` (restaurer `sendMessage`), `app/utils/sendMessage.ts` (restaurer), `cmsbackend/src/api/message/` (restaurer les 4 fichiers), et `app/admin/messages/page.tsx` si on veut la page de consultation.
2. Re-démarrer Strapi pour qu'il régénère le content-type en mémoire.
3. Vérifier les **permissions publiques** sur `POST /api/messages` dans l'admin Strapi (peut avoir besoin d'être recoché).
Préférable : **renouveler la clé Brevo** plutôt que rollback.
## Sécurité — rappels
- **Ne jamais committer `.env.local`**. Déjà dans `.gitignore` (`.env*`).
- **Si la clé Brevo fuit** (exposée dans un commit, un chat, un screenshot) : la **supprimer immédiatement** sur Brevo et en générer une nouvelle. Une clé compromise permet d'envoyer des emails frauduleux depuis l'expéditeur vérifié, ce qui peut faire blacklister le domaine.
- L'**IP du visiteur** est loguée dans l'email (pour abus). À supprimer des emails si on veut un template plus "commercial".

View File

@ -1,33 +0,0 @@
# État actuel du site
**Dernière mise à jour :** 2026-04-24 (doc compétences / realisation-ia + CONFIGURATION)
## Ce qui est en place
- **Next.js 15** avec App Router, Tailwind, pages accueil / portfolio / compétences / contact, layout responsive avec menu burger. Design system "Digital Atelier" (Manrope + Newsreader, palette primary indigo-ardoise, vellum cards).
- **Strapi** avec content-types : homepage, projects, competences, **realisation-ia** (rattachées aux compétences), glossaire ; médias et texte riche (lancien type `message` a été retiré au profit de Brevo).
- **Compétences côté Next** : liste `/competences` (tri `order`) ; fiche `/competences/[slug]` (vignettes des `realisation-ia` liées quand il y en a, sinon fiche richtext) ; détail `/competences/[slug]/[realisation]`. Même logique denrichissement que le portfolio (Markdown, galerie, CTA) pour les fiches liées.
- **Formulaire contact** : e-mail via **Brevo** (route Next `POST /api/contact`). Voir [contact-flow.md](./contact-flow.md).
- **Chatbot GrasBot v3** : FAB global (`GrasBotFab.tsx`) → proxy Next → API LLM hébergée (`llmapi.fernandgrascalvet.com`).
- **FastAPI + Ollama** dans `llm-api/` : modèle `qwen3:8b`, pipeline `search.py` (graph + BM25 sur vault Obsidian `vault-grasbot/`, sans embeddings).
- **Vault de connaissance `vault-grasbot/`** : ~46 notes Markdown, dont 2 fiches projet manuelles (GrasBot, site portfolio) et compétences IA/Web mises à jour (2026-04) — recharger lAPI après déploiement si besoin : `POST /reload-vault` (aliases, answers, priority) — source de vérité du chatbot, régénéré depuis Strapi par `strapi_extraction/build-vault.py`. Inclut une note `bio-fernand` courte (priority 10) dédiée aux questions biographiques et un CV complet complémentaire.
- **Observabilité Langfuse** : instance self-hosted `langfuse.fernandgrascalvet.com`, instrumentation Python (`llm-api/observability.py`) traçant chaque requête `/ask` (retrieval / prompt_build / ollama-chat) avec session/user IDs anonymes côté front. Mode no-op automatique si les clés sont absentes. Voir [`langfuse-observability.md`](./langfuse-observability.md).
- **Scripts** d'extraction et de doc dans `strapi_extraction/`.
- Documentation opérationnelle : [`CONFIGURATION_SITE.md`](../CONFIGURATION_SITE.md) à la racine du dépôt (incl. ordre des compétences et routes dédiées, renvoi vers [02-frontend-next.md](./02-frontend-next.md)).
- **Captures d'écran** de référence (WebP) : [captures/](./captures/) — voir [INDEX.md](./captures/INDEX.md).
- **Décision produit** : une **rubrique homelab / serveur** (souvent évoquée en « phase 3 ») nest **pas retenue** — pas dévolution planifiée sur ce thème ; le parcours public reste portfolio, compétences (dont IA + réalisations) et contact.
## Dette technique / incohérences connues
- Mélange **TypeScript** et **JavaScript** (`.jsx`, `.js`) dans `app/`.
- **`RootLayout` en client component** : tout le layout est côté client ; pas de Server Component racine pour le shell.
- **URLs Strapi** : logique répartie entre `getApiUrl`, `next.config.ts`, `config.ts` — risque de confusion ; à documenter dans les changements futurs.
- **Proxy LLM** : URL de production codée en dur dans `app/api/proxy/route.js` ; pas dalignement automatique avec `llm-api` local.
- Champ Strapi **`Resum`** sur `project` : casse atypique ; attention dans le mapping front.
- **`start-my-site.ps1`** : chemins absolus `J:\my-next-site` — non portables.
## Non vérifié dans cette passe
- Permissions Strapi (public create sur `messages`, etc.).
- Comportement exact des rewrites Next vs route `app/api/proxy` (ordre de résolution).
- Tests automatisés : présence à confirmer.

View File

@ -1,61 +0,0 @@
# Feuille de route
**Dernière mise à jour :** 2026-04-24
Document vivant : ajuster les statuts et dates au fil du travail.
## Court terme (prochaines itérations)
| ID | Sujet | Statut | Notes |
|----|--------|--------|--------|
| R1 | Moderniser lUI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-7 (tokens + garde-fou + migration typo globale + layout racine + home + listes portfolio/compétences + fiches détail & GrasBot) faites le 2026-04-22. Reste l'étape 8 (contact + footer éditorial). |
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer |
| R5 | Chatbot GrasBot — retrieval local (Qwen3 + vault Obsidian) | v3 en place | Pipeline **graph + BM25** (`llm-api/search.py`), plus de RAG vectoriel ni de dépendance ChromaDB. Vault `vault-grasbot/` enrichi automatiquement (aliases/answers/priority). Lecture directe depuis `build-vault.py`, plus d'étape d'indexation. Doc : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md). Reste : fix `clean-api-data.js` (homepages + glossaires), affichage sources côté front, badge `grounded`, historique conversationnel, streaming. |
## Moyen terme
| ID | Sujet | Statut | Notes |
|----|--------|--------|--------|
| M1 | Tests (e2e ou smoke sur routes critiques) | À faire | |
| M2 | Accessibilité (navigation, contrastes, focus) | À faire | |
| M3 | Performance (images Next/Image, bundle) | À faire | |
## Long terme / idées
| ID | Sujet | Statut | Notes |
|----|--------|--------|--------|
| L1 | CI/CD ou script de déploiement documenté | À faire | |
| L2 | Monitoring / logs centralisés | À faire | |
## Historique des jalons
| Date | Jalon |
|------|--------|
| 2026-04-01 | Création du dossier `docs-site-interne` (dépôt Git) et première rédaction basée sur le code. |
| 2026-04-01 | Reprise après coupure : vérification complétude ; enrichissement de `04-api-llm-et-chatbot.md` ; suppression de `test.txt`. |
| 2026-04-01 | Index captures (`captures/INDEX.md`), `captures/README.md`, `07-reference-visuelle-captures.md`, skill `.cursor/skills/site-portfolio-evolution/SKILL.md`. |
| 2026-04-01 | Captures WebP intégrées au dépôt ; INDEX et README `captures/` alignés sur les noms réels (slug portfolio / compétence documentés). |
| 2026-04-22 | Audit visuel complet (`captures/AUDIT-VISUEL.md`), correctifs layout (option 1 + compensation header + drawer mobile). |
| 2026-04-22 | Refonte visuelle "Digital Atelier" — étape 1 (tokens Tailwind : palette Stitch, `font-headline` Manrope + `font-body` Newsreader, `rounded-sheet/tile`, `shadow-ambient/jewel`) et étape 2 (garde-fou + plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md)). |
| 2026-04-22 | Refonte visuelle — étape 3 : migration typographique globale. Toutes les classes `font-orbitron-*` (12 définitions CSS, 29 occurrences dans 11 fichiers) remplacées par `font-headline` Manrope + tailles/poids Tailwind explicites. Import Google Fonts Orbitron supprimé de `app/assets/main.css`. |
| 2026-04-22 | Refonte visuelle — correctif post-étape 3 : régression de couleurs texte entre desktop/mobile. Retrait du `@media (prefers-color-scheme: dark)` hérité du template Next (incohérent avec l'arbitrage "light-only"), `--foreground` fixé à `#191c1d` (on-surface Stitch), `body` avec couleur non-dépendante du thème système. 3 classes Tailwind invalides `text-black-500/700` remplacées par `text-gray-700` (`app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`). |
| 2026-04-22 | Refonte visuelle — étape 4 : layout racine. Header "No-Line" (bordure pleine supprimée, `shadow-ambient-sm` + `backdrop-blur-vellum`, titre en `text-primary`). Burger refait en ghost button (Material Symbols `menu`/`close` au lieu des caractères `☰`/`✕`). Cercles animés repeints en `bg-primary/40` + `bg-primary-container/30`. Drawer mobile en `bg-primary/90 backdrop-blur-vellum` + liens éditoriaux (`bg-primary-container/60` → hover `bg-primary-fixed text-primary`). Bug préexistant **corrigé** : `NavLink` ignorait `className` et `onClick` fournis par le drawer mobile → refait avec support `className` / `onClick` / `activeClassName` / `inactiveClassName`, comportement desktop historique préservé. Compteur de visites migré de `layout.tsx` (bloc orphelin `absolute bottom-0 right-0`) vers `Footer.jsx` (ligne discrète `text-[10px] uppercase tracking-[0.3em]`). Nettoyage : state `visitCount` + useEffect déplacés, `div.max-w-5xl` vide retirée, state `count` inutilisé retiré de `Footer.jsx`. |
| 2026-04-22 | Refonte visuelle — correctif urgent `ModalGlossaire` (blocage mobile signalé sur Samsung S25 Ultra). `w-[114vw] max-w-6xl h-[72vh]``w-full max-w-4xl max-h-[90vh]` + `overflow-y-auto`. Fermeture ajoutée sur tap-voile et `Escape`. Bouton fermeture rond 40 px Material Symbol `close`. Alignement palette Stitch (`bg-on-surface/75`, `bg-surface-container-lowest/95`, `rounded-sheet`, `text-primary`, description `font-body` Newsreader). `"use client"` + `role="dialog" aria-modal` ajoutés. |
| 2026-04-22 | Refonte visuelle — étape 5 : home. Hero "feuillet de vellum" (`bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient rounded-sheet`) avec grille `auto_1fr` portrait + texte. Portrait en frame primary (`bg-primary p-1 rounded-sheet`) qui remplace le cercle `rounded-full border-4`. Kicker `Portfolio · Étudiant 42 Perpignan`. Titre Manrope extrabold. `cv` Strapi rendu via ReactMarkdown + plugin typography (`prose prose-stone` custom). CTAs jewel `/portfolio` (primary shadow-jewel) + ghost `/contact` (hover `bg-primary-fixed/40`). Nouvelle section "Trois axes de travail" (3 cartes takeaway avec icônes Material Symbols `psychology`, `terminal`, `school`, contenu hardcodé dans `takeaways[]`). Pull-quote éditoriale `border-l-4 border-primary` en Newsreader italique. Double `<main>` supprimé (layout racine fournit déjà le `<main>`). |
| 2026-04-22 | Refonte visuelle — correctifs post-étape 5 (retour utilisateur). **Icônes Material Symbols affichées comme texte littéral** (ex. "psychology" visible dans le rond bleu des takeaways) : oubli de `font-family: 'Material Symbols Outlined'` dans la règle `.material-symbols-outlined` de `app/globals.css` — Google Fonts pose le `@font-face` mais pas la règle de classe. Fix : ajout de la ligne manquante, toutes les icônes (takeaways, burger, modale, CTAs) s'affichent désormais correctement. **Pull-quote "Démarche" qui se fondait dans le wallpaper** : passée en carte vellum légère (`bg-surface-container-lowest/65 backdrop-blur-vellum rounded-tile`, sans `shadow-ambient`) pour rester lisible sans écraser la variation éditoriale. **Espace trop grand entre les 3 sections** : `gap-8``gap-5`, `py-6 md:py-8` retiré sur la pull-quote (remplacé par le padding interne de sa carte). |
| 2026-04-22 | Refonte visuelle — correctifs post-étape 5 (2e passe). **Icônes toujours en texte littéral après le fix font-family** : URL Google Fonts `@24,400,0,0` (valeur fixe) potentiellement non résolue côté navigateur. Passée en syntaxe ranges `@20..48,100..700,0..1,-50..200` (alignée sur `stitch_V1/code.html`), fiable et documentée. **Densité verticale encore trop aérée** : `gap-5``gap-3` sur le container racine de la home. Paddings internes cartes resserrés : hero `p-6 sm:p-8 md:p-10``p-5 sm:p-7 md:p-8` ; takeaways `p-6 sm:p-8``p-5 sm:p-6`. Grille intérieure takeaways `gap-4``gap-3`. Hero texte `gap-4``gap-3`. En-tête de section takeaways `mb-6``mb-4`. |
| 2026-04-22 | **Diagnostic critique via DevTools Network (Chrome desktop + mobile)** : aucune des trois Google Fonts (Manrope, Newsreader, Material Symbols) n'était réellement chargée par le navigateur — les `@import url('https://fonts.googleapis.com/...')` dans `app/globals.css` étaient strippés par la chaîne PostCSS + Tailwind de Next 15 en production. Conséquence : tout le site tournait en fallback Arial / Georgia depuis l'étape 3, la typo éditoriale Stitch n'était jamais réellement visible. **Fix phase 1 (fonts textuelles)** : création de `app/fonts.ts` qui exporte Manrope et Newsreader via `next/font/google` (téléchargement au build, service depuis le domaine du site, plus de dépendance CDN externe). `app/layout.tsx` importe ces fonts et pose `${manrope.variable} ${newsreader.variable}` sur le `<html>`. `tailwind.config.ts` : `font-headline` et `font-body` repointés vers `var(--font-manrope)` et `var(--font-newsreader)`. `app/globals.css` : 2 `@import` inopérants supprimés ; l'`@import` Material Symbols reste temporairement en attendant la phase 2. Validation DevTools Computed : `font-family: Manrope, "Manrope Fallback", system-ui, sans-serif` confirmé sur le h1 de la home. |
| 2026-04-22 | **Fix phase 1b** : font par défaut du `body` dans `app/globals.css` passée de `Arial, Helvetica, sans-serif` à `var(--font-newsreader), Georgia, serif`. Tout élément sans classe typo explicite hérite désormais de Newsreader (corps éditorial Stitch) au lieu d'Arial. **Fix phase 2 (Material Symbols)** : `@import url(...)` Material Symbols retiré de `app/globals.css` (strippé), remplacé par un triplet `<link rel="preconnect"> + <link rel="stylesheet">` injecté dans le `<head>` de `app/layout.tsx` — contourne le pipeline PostCSS tout en conservant le CDN Google pour la font-icon (usage très ponctuel : 5 icônes sur la home, 2 sur le burger, 1 sur la modale). Validé en production : icônes + fonts OK sur Chrome desktop. |
| 2026-04-22 | **Correctif Chrome Auto-Translate mobile** (retour utilisateur : layout décalé + icônes redevenues du texte littéral sur Chrome mobile quand la traduction auto s'active). Cause : les icônes Material Symbols fonctionnent via ligatures de font (`<span>psychology</span>` → glyphe cerveau) ; Chrome traduit le texte `psychology` en `psychologie`, la ligature ne match plus, l'icône redevient texte. Fix : `translate="no"` ajouté sur les 8 `<span className="material-symbols-outlined">` (5 dans `app/page.tsx`, 2 dans `app/layout.tsx`, 1 dans `app/components/ModalGlossaire.tsx`) + sur le titre header (nom propre `Portfolio Gras-Calvet Fernand`). La traduction automatique reste globalement active pour le contenu éditorial (CV, compétences, projets) — seuls les éléments que la traduction casse sont protégés. |
| 2026-04-22 | **Correctif séparateurs `<hr>` du hero** (retour utilisateur : « trop d'espace entre les sections » en fait dû à des `<hr>` quasi invisibles générés depuis les `---` Markdown du CV Strapi). Option B retenue : surcharge `prose-hr` sur le wrapper `ReactMarkdown` (`app/page.tsx`) pour transformer la règle en pastille Stitch centrée (`prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full prose-hr:my-6`). Le séparateur redevient un signal visuel intentionnel et cohérent avec la palette, marges verticales réduites de 32 px à 24 px. Détail dans `REFONTE-VISUELLE.md` §4 sexies. |
| 2026-04-22 | **Correctif post-étape 6 : réintroduction du défilement auto en vignette**. L'arbitrage initial "carousel retiré des listes" retiré après retour utilisateur (*« j'ai perdu ma fonctionnalité précédente »*). Nouveau composant `app/components/VignetteCarousel.tsx` : Swiper allégé (modules `Autoplay` + `Pagination` uniquement), **sans flèches** (conflit de clic avec le `<Link>` englobant) et **sans lightbox** (réservée à la fiche détail). Pagination bullets teintée `primary` via surcharge inline des variables CSS Swiper (`--swiper-pagination-color: #26445d`) pour ne pas polluer `globals.css`. Autoplay 3500 ms, `loop` conditionnel (`length > 1` pour éviter le warning Swiper sur les entrées mono-image). Intégré dans `app/portfolio/page.jsx` et `app/competences/page.jsx` via `images.length > 1 ? <VignetteCarousel /> : <img statique />` — comportement identique à l'ancien pour les mono-image, défilement retrouvé pour les multi-image. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` existants restent intacts pour la fiche détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6 *"Correctif post-étape 6 — réintroduction du défilement automatique en vignette"*. |
| 2026-04-22 | **Correctif wallpaper sur-zoomé sur pages longues** (retour utilisateur post-étape 6 : incohérence visuelle entre la home et `/portfolio`). Cause : `.bg-wallpaper` en `absolute inset-0` à l'intérieur du conteneur grid `min-h-[100dvh]` — le conteneur s'étirant à la hauteur du contenu (≈ 2-3 viewports sur les listes), `background-size: cover` zoomait l'image pour couvrir toute cette hauteur. Fix : wallpaper sorti du grid, passé en `fixed inset-0 z-0 pointer-events-none` (calé sur le viewport, plus la page entière). Les cercles animés restent en `absolute` dans le grid. Impact transversal sur toutes les pages longues (fiches détail à venir, etc.). Détail dans `REFONTE-VISUELLE.md` §6. |
| 2026-04-22 | **Vault GrasBot — correctif note CV + protection `source: manual` effective**. La note `vault-grasbot/30-Parcours/cv-grascalvet-fernand.md` était produite par `pypdf` avec des espaces entre chaque caractère (mise en page Canva du PDF source) → illisible. Réécriture manuelle en Markdown structuré à partir du contenu PDF : sections Identité / Contact / Présentation / Objectifs alternance / Expérience pro (42 → Infirmier → Ostréiculture) / Compétences (langages, IA/LLM, systèmes) / Langues / Intérêts (hardware, 3D, domotique, IA). Wikilinks vers les projets 42 et les compétences (`[[libft]]`, `[[get-next-line]]`, `[[inception]]`, `[[ia]]`, `[[impression-3d]]`, etc.). Frontmatter passé en `source: manual` + `domains: [parcours, ecole-42, ia, 3d, domotique]`. **Correctif de fond dans `strapi_extraction/build-vault.py`** : la règle « ne pas écraser les notes `source: manual` » était documentée mais pas implémentée — `write_notes()` écrasait systématiquement. Ajout de `_existing_source()` qui lit le frontmatter existant + skip avec log `⏭` si `source: manual`. La règle est désormais **effective** (vérifié en `--dry-run` : la note CV curatée est bien préservée). |
| 2026-04-22 | **Chatbot GrasBot — migration Mistral → Qwen3 + RAG sur vault Obsidian local**. Passage du modèle `mistral` à `qwen3:8b` dans `llm-api/api.py` (Q4_K_M, ~5 Go VRAM RTX 2080 Ti). Embeddings via `nomic-embed-text` (~500 Mo VRAM, multilingue FR). Nouveau pipeline RAG : `llm-api/rag.py` (embed / retrieve / build_prompt / generate / answer), `llm-api/index_vault.py` (parse frontmatter YAML, chunking par h2 au-delà de 3000 chars, upsert ChromaDB batch 32), `llm-api/requirements.txt` (fastapi, uvicorn, requests, chromadb, pyyaml). Nouveau script `strapi_extraction/build-vault.py` qui convertit `strapi_extraction/docs/*.md` + CV PDF (via `pypdf`) en vault Obsidian structuré `vault-grasbot/` : frontmatter YAML (type, source, domains, tags, linked, related, visibility), wikilinks vers les MOCs, MOCs auto-générés par type et par domaine (15 MOCs). Bootstrap v1 du vault : 17 projets, 4 compétences, 1 CV, 15 MOCs auto + 1 manuel (Technique), 3 notes auto-doc dans `50-Technique/` (architecture-site, grasbot-rag, vault-structure) pour que GrasBot puisse se présenter lui-même. Compatibilité ascendante `askAI.js`/`ChatBot.js` via le champ `response` conservé ; les `sources`, `rag`, `model` ajoutés sont non destructifs. Endpoint `/health` ajouté pour debug. Doc : nouveau [`08-vault-obsidian-rag.md`](./docs-site-interne/08-vault-obsidian-rag.md), mise à jour de [`04-api-llm-et-chatbot.md`](./docs-site-interne/04-api-llm-et-chatbot.md) et [`06-strapi-extraction.md`](./docs-site-interne/06-strapi-extraction.md). Fragilités préexistantes repérées (cleaner `homepages` absent, content-type `glossaire` non extrait) consignées mais non corrigées dans ce lot — à traiter lors du prochain enrichissement vault. |
| 2026-04-22 | Refonte visuelle — **étape 7 : fiches détail + glossaire + GrasBot flottant**. Cinq sous-lots. **7.a** : `Carousel.tsx` + `CarouselCompetences.tsx` harmonisés (pagination bullets primary via variables CSS Swiper, flèches primary, `rounded-tile shadow-ambient-sm`, autoplay 3500 ms + `loop` conditionnel, lightbox Stitch refaite avec voile `bg-on-surface/80`, image `object-contain rounded-sheet`, bouton close rond Material Symbol + Escape + verrouillage scroll body). **7.b** : `ContentSection.tsx` (fiche portfolio) — gabarit vellum cohérent avec la home/listes, pastille retour `arrow_back`, kicker `Projet · Portfolio`, titre Manrope, carousel détail plein cadre, corps Markdown en `prose` Stitch (mêmes overrides que la home, y compris pastille `prose-hr`), CTA externe jewel avec `open_in_new`, états loading/404 en vellum. **7.c** : `ContentSectionCompetences.tsx` + container — même gabarit. Refactor glossaire : styles inline `style="color:red/blue"` remplacés par les classes `.glossary-keyword` / `.chatbot-keyword` (ajoutées à `globals.css`, couleur primary + underline dotted offset 3 px). Event listeners `document.body.addEventListener` remplacés par un listener unique scopé au wrapper `contentRef`. Le clic sur « IA locale » ne monte plus un `<ChatBot>` local mais dispatch `window.dispatchEvent(new CustomEvent("grasbot:open"))` capté par le FAB global (7.e). Container refait avec skeleton vellum à la place de `⏳ Chargement...`. **7.d** : `ChatBot.js` entièrement restylé (carte vellum `rounded-sheet shadow-ambient backdrop-blur-vellum`, header primary avec Material Symbol `smart_toy` + sous-titre « Assistant IA locale », bulles user `bg-primary text-white` et bot `bg-surface-container`, input Stitch avec `focus-visible:ring-primary`, bouton envoyer rond jewel Material Symbol `send`, auto-scroll, focus auto, envoi Enter, disabled pendant attente, message d'accueil vide éditorial). **7.e** : nouveau composant `app/components/GrasBotFab.tsx` — FAB jewel `fixed bottom-6 right-6 z-30` rond 56/64 px, `bg-primary shadow-jewel` Material Symbol `smart_toy`/`close`, monté dans `app/layout.tsx` → chatbot accessible depuis **toutes les pages** (plus seulement fiches compétences). Écoute `CustomEvent("grasbot:open")` dispatché depuis le keyword « IA locale ». Fermeture Escape globale, panneau responsive plein largeur mobile / 384 px desktop. Détails dans `REFONTE-VISUELLE.md` §7. |
| 2026-04-22 | Refonte visuelle — **étape 6 : listes portfolio + compétences**. `app/portfolio/page.jsx` et `app/competences/page.jsx` entièrement réécrits. En-tête éditorial (kicker + titre Manrope extrabold + pitch Newsreader) cohérent avec le hero de la home. Grille **asymétrique 2/3 + 1/3** alternée (`md:grid-cols-6` + pattern de `col-span-4`/`col-span-2` sur modulo 4, `sm:grid-cols-2`, `grid-cols-1` mobile) — conforme DESIGN.md §6 "No-Grid-Lock". Cartes « feuillet vellum » alignées home : `rounded-sheet bg-surface-container-lowest/85 backdrop-blur-vellum shadow-ambient`, image `aspect-[4/3]` fixe avec `group-hover:scale-[1.03]`, titre `text-primary`, description `line-clamp-3` en Newsreader, CTA tertiaire « Découvrir → » / « Explorer → » avec Material Symbol `arrow_forward` qui se décale au hover (`translate="no"` appliqué). Hover : `hover:-translate-y-0.5 hover:shadow-jewel` (remplace le `scale-105` qui débordait). **`Swiper` retiré des vignettes de liste** (arbitrage acté § 2 : carousel réservé aux galeries intra-fiche) — une seule image par carte, `loading="lazy"`. États ajoutés : skeletons animés respectant la grille + état vide avec Material Symbol. Régressions corrigées au passage : largeur fixe `w-80` qui débordait sur S25 Ultra, `hover:scale-105` qui tapait sous le header, classes `bg-white/80 rounded-lg` remplacées par les tokens Stitch. Les composants `Carousel.tsx` et `CarouselCompetences.tsx` restent en place pour les fiches détail (étape 7). Détail dans `REFONTE-VISUELLE.md` §6. |
| 2026-04-23 | **GrasBot — tuning pipeline LLM + anti-hallucinations**. Audit des premières traces Langfuse : questions biographiques hallucinées (âge erroné, statut inventé), réponses longues tronquées. Quatre ajustements : (1) `llm-api/search.py` · `generate()``num_ctx=8192` explicite (stoppe la troncature silencieuse du prompt par le défaut Ollama 2048/4096 quand plusieurs notes entières sont injectées), `num_predict` 512 → 1024 (réponses longues complètes), `think: false` top-level (désactive le *thinking mode* de qwen3 qui consommait du budget de sortie). (2) `llm-api/search.py` · `build_prompt()` — troncature conditionnelle des sources rank 2+ via `_truncate_body()` + nouvelles variables `SEARCH_SECONDARY_MAX_CHARS` (1500) / `SEARCH_SECONDARY_KEEP_RATIO` (0.8). Aucune source n'est supprimée, seules celles dont le score est < 0.8 × score(#1) ET dont le body dépasse 1500 chars sont résumées. Loggé dans `prompt_build.metadata.truncation`. (3) Vault nouvelle note `vault-grasbot/30-Parcours/bio-fernand.md` courte et factuelle (priority 10, aliases biographiques courts), canonique pour les questions du type *« qui est Fernand »*. Renvoie vers le CV complet pour le détail. Correction incohérence d'âge dans le CV (46 47 ans dans la section Présentation) qui alimentait les hallucinations. (4) `SYSTEM_PROMPT` nouveau bloc *Règles de fidélité aux sources* : priorité `type=parcours` pour questions bio, interdiction d'inventer des faits factuels, gestion explicite des contradictions, signalement des notes tronquées. **Bascule Langfuse v4 → v3 dans `requirements.txt`** (`langfuse>=3.0,<4`) : le SDK v4 a supprimé `start_as_current_span`, la v3 reste compatible avec l'instrumentation existante. Dépendances Python ajoutées : `langfuse`, `python-dotenv`. Secrets Langfuse déplacés de `.env.local` Next vers `llm-api/.env` (non committé). Doc mise à jour : [`langfuse-observability.md`](./langfuse-observability.md) (nouvelle section *Tuning du pipeline — 2026-04-23*), `CONFIGURATION_SITE.md` (endpoints `/health` + `/reload-vault`), `etat-actuel.md` (42 notes + mention Langfuse). |
| 2026-04-22 | **GrasBot v3 — bascule RAG vectoriel → retrieval graph + BM25**. Essais d'installation Windows bloqués par `chroma-hnswlib` (compilation C++ requise) et freezes RDP à chaque chargement de `qwen3:8b` + `nomic-embed-text` simultanément. Arbitrage : pour un vault de 40 notes, la RAG vectorielle sur-dimensionne ; on exploite directement la structure Obsidian (frontmatter, wikilinks, MOCs). **Nouveau pipeline** dans `llm-api/search.py` (scoring multi-signaux : aliases / titre-slug / answers / domains / tags / BM25 ; expansion par graphe via `linked`/`related`/wikilinks ; tokenizer FR avec normalisations `c++``cpp`, split `-`/`_`). **Déterministe, traçable (champ `reasons` dans les sources), 50 ms de retrieval**. Scoring calibré sur 12 cas (IA, push-swap, LLMs pluriel, hors-sujet clafoutis → `(aucun)`, etc.). **Dépendances allégées** : fini `chromadb`, `chroma-hnswlib`, `nomic-embed-text`. `requirements.txt` = fastapi + uvicorn + requests + pyyaml uniquement. Fichiers supprimés : `llm-api/rag.py`, `llm-api/index_vault.py`, `chroma-index/` (marqué pour suppression, verrouillé par Cursor au moment du cleanup — sera supprimé au reboot). **Vault enrichi** : `build-vault.py` étendu pour générer automatiquement `aliases` (à partir du slug/titre + `DOMAIN_ALIASES`), `answers` (questions-types adaptées au type de note), `priority` (heuristique CV=10, MOCs=7, compétences=7, projets=5). Note CV curatée (`source: manual`) enrichie manuellement avec 12 aliases et 7 answers. Nouvelle `vault-grasbot/TAXONOMIE.md` qui documente le vocabulaire contrôlé. Réécriture de `vault-grasbot/50-Technique/grasbot-rag.md``grasbot-retrieval.md` (nouveau pipeline), + `architecture-site.md` + `vault-structure.md` + `MOC-Technique.md`. Nouveau endpoint `POST /reload-vault` pour recharger sans redémarrer uvicorn. Documentation interne refaite : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md), [`06-strapi-extraction.md`](./06-strapi-extraction.md), nouveau [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) (remplace `08-vault-obsidian-rag.md`). |
| 2026-04-24 | **Doc + configuration** : routes `/competences/[slug]/[realisation]`, entité Strapi `realisation-ia`, tri `order` et comportement vignettes/richtext documentés dans `02-frontend-next.md`, `04-api-llm-et-chatbot.md` (parcours public), `etat-actuel.md` ; `CONFIGURATION_SITE.md` : section *Contenu : compétences, réalisations IA et ordre daffichage*. Décision : **pas de « phase 3 » homelab** sur le site (consignée dans létat actuel). |

View File

@ -1,227 +0,0 @@
# Observabilité GrasBot via Langfuse
**Créé :** 2026-04-23
**Statut :** en production
**Pré-requis lecture :** `08-vault-obsidian-retrieval.md` (architecture du pipeline graph + BM25).
## Vue d'ensemble
Le chatbot GrasBot est instrumenté avec **Langfuse** (instance self-hosted : `langfuse.fernandgrascalvet.com`) pour tracer **chaque requête visiteur** bout en bout :
- **Retrieval** : quelles notes du vault ont été remontées, avec quels scores, pour quelles raisons.
- **Prompt** : le system + user effectivement envoyés à Qwen3.
- **Génération** : latence, tokens, paramètres du modèle.
- **Trace globale** : question, réponse, sources, scores dérivés (grounded, retrieval_relevance), tags.
But : **debug**, **monitoring** (qualité/latence dans le temps), et **itération** sur le pipeline retrieval en voyant directement l'effet d'un changement de règle de scoring.
## Architecture
```
┌─────────────────────────┐ ┌──────────────────────────┐
│ ChatBot.js (front) │ │ Langfuse self-hosted │
│ - grasbotIds.js │ │ langfuse.fernandgrasc… │
│ - user_id localStorage│───▶│ │
│ - session_id sessionSt │ │ (ingestion HTTPS) │
└──────┬──────────────────┘ │ │
│ │ │
▼ │ ▲ │
┌─────────────────────────┐ │ │ SDK Python │
│ app/api/proxy/route.js │ │ │ (observability │
│ whitelist + GET fwd │ │ │ .py) │
└──────┬──────────────────┘ └─────────┼────────────────┘
│ │
▼ │
┌─────────────────────────┐ │
│ FastAPI /ask │ │
│ (llm-api/api.py) │ │
│ → @observe via Langfuse─────────────┘
│ → search.answer() │
└──────┬──────────────────┘
├── retrieval (span)
├── prompt_build (span)
└── ollama-chat (generation)
```
L'instrumentation **vit côté Python** (couche où on a accès aux détails du retrieval et du prompt). Le proxy Next ne fait que relayer le `session_id` / `user_id` depuis le front jusqu'à l'API Python.
## Fichiers concernés
| Fichier | Rôle |
|---------|------|
| `llm-api/observability.py` | Init client Langfuse (no-op safe si clés absentes) + `flush()` au shutdown |
| `llm-api/api.py` | FastAPI `/ask` — query params `session_id`/`user_id` + `lifespan` pour flush |
| `llm-api/search.py` | Spans `retrieval` + `prompt_build` + `generation`, trace racine `ask`, scores auto |
| `llm-api/.env` | Secrets Langfuse (non committé) |
| `llm-api/.env.example` | Template documentaire |
| `app/utils/grasbotIds.js` | Génération UUID v4 anonymes (localStorage + sessionStorage) |
| `app/utils/askAI.js` | Passe `session_id`/`user_id` en query params |
| `app/api/proxy/route.js` | Whitelist `q`, `session_id`, `user_id` → forward vers API Python |
## Variables d'environnement (côté Python uniquement)
Dans **`llm-api/.env`** (chargé automatiquement par `observability.py` via `python-dotenv`) :
| Variable | Obligatoire | Notes |
|----------|-------------|-------|
| `LANGFUSE_PUBLIC_KEY` | oui | Format `pk-lf-…` |
| `LANGFUSE_SECRET_KEY` | oui | Format `sk-lf-…`**JAMAIS dans un log/commit/chat** |
| `LANGFUSE_BASE_URL` | oui | URL du self-hosted (ex. `https://langfuse.fernandgrascalvet.com`) |
| `LANGFUSE_HOST` | fallback | Alternative à `BASE_URL` si jamais on passe sur le cloud Langfuse |
Si **l'une des 3** est absente → `observability.py` instancie un **client no-op** : l'API fonctionne normalement, aucune trace n'est envoyée, aucune erreur. Pratique pour dev local / contributeurs externes.
Les variables Langfuse **ne sont pas** dans `.env.local` de Next.js — elles ne servent qu'au backend Python.
## Structure d'une trace
### Trace racine : `ask`
- **input** : `{ query: "..." }`
- **output** : `{ response, sources_count, grounded }`
- **metadata** : `{ top_k, min_score }`
- **session_id**, **user_id** (propagés depuis le front)
- **tags** : `grounded`|`ungrounded`, `model:qwen3:8b`, `vault-miss` (si aucune note scorée)
- **scores** auto :
- `grounded` (BOOLEAN, 0/1) : au moins 1 note ≥ `MIN_SCORE`
- `retrieval_relevance` (NUMERIC, 0-1) : `min(max_score / 15, 1)`
### Span `retrieval`
- **input** : `{ query, top_k }`
- **output** : `[{slug, title, type, score, reasons}, …]` — top-K final après expansion
- **metadata** :
- `query_tokens` : tokens extraits par `tokenize_fr`
- `vault_size` : nombre de notes publiques chargées
- `candidates_with_signal` : combien de notes ont eu un score > 0
- `seeds_before_graph` : top-3 avant expansion par graphe
- `bm25_stats` : `{N, avgdl, idf_terms}` (pour debug de régressions BM25)
- `elapsed_ms` : durée du retrieval seul
### Span `prompt_build`
- **input** : `{ query, scored_count }`
- **output** : `{ system, user }` — le **prompt complet** envoyé à Qwen
- **metadata** :
- `grounded` : bool (= au moins 1 note ≥ MIN_SCORE)
- `relevant_notes` : notes effectivement incluses dans le contexte
- `system_chars`, `user_chars` : tailles utiles pour debug de fenêtre de contexte
- `min_score_threshold` : valeur du `MIN_SCORE` au moment de l'appel
- `truncation` : `{ secondary_max_chars, secondary_keep_ratio, truncated_notes: [...] }`
liste des sources rank 2+ résumées automatiquement (avec leur slug, score,
taille d'origine et taille tronquée). Vide s'il n'y a eu aucune troncature.
### Span `ollama-chat` (type **generation**)
- **input** : `[{role: "system", content}, {role: "user", content}]`
- **output** : réponse brute du modèle
- **model** : `LLM_MODEL` (ex. `qwen3:8b`)
- **model_parameters** : `{temperature: 0.4, num_ctx: 8192, num_predict: 1024, think: false}`
(voir section "Tuning 2026-04-23" ci-dessous pour le rationnel).
- **usage** : `{input, output, total}` — extraits de `prompt_eval_count` / `eval_count` si Ollama les renvoie
- Si réponse vide → span `level: ERROR` avec le payload Ollama brut en metadata.
## Session / User IDs (côté front)
**Pas de PII**, **pas d'authentification**. Deux UUID v4 anonymes générés automatiquement à la première interaction :
- **`grasbot_user_id`** → `localStorage` → stable par device, sert à mesurer les utilisateurs uniques et à regrouper l'historique d'un visiteur récurrent.
- **`grasbot_session_id`** → `sessionStorage` → expire à la fermeture de l'onglet, regroupe une conversation.
Générés par `app/utils/grasbotIds.js`, propagés par `askAI.js``/api/proxy` (whitelist) → `/ask` (query params) → `search.answer()` (`update_current_trace(session_id=…, user_id=…)`).
**Impact RGPD** : aucun identifiant déductible de l'utilisateur, aucune donnée persistante côté serveur autre que ce que Langfuse stocke de lui-même. L'utilisateur peut vider son storage pour "réinitialiser" son identité côté observabilité.
## Procédure de test
### Local
1. `cd llm-api && pip install -r requirements.txt` (ajoute `langfuse` + `python-dotenv`).
2. Remplir `llm-api/.env` avec les 3 clés (ou laisser vide pour tester le mode no-op).
3. `.\start-my-site.ps1` (ou démarrer uvicorn manuellement).
4. Aller sur `http://localhost:3000` → ouvrir le chatbot (FAB en bas à droite) → poser une question.
5. Dans Langfuse → **Traces** → voir apparaître une trace `ask` en temps réel (quelques secondes après la réponse, le temps du flush).
### Vérifier le no-op silencieux
1. Commenter les 3 variables `LANGFUSE_*` dans `llm-api/.env`.
2. Redémarrer uvicorn → les logs affichent ` Langfuse désactivé — variables manquantes : …`.
3. Poser une question au chatbot → réponse normale, aucun crash.
4. `GET /health` renvoie `{"observability": {"langfuse_enabled": false}}`.
### Scénarios utiles à reproduire dans Langfuse
- **Question grounded classique** : "Parle-moi de push-swap" → tags `grounded`, retrieval_relevance ~0.7-0.9.
- **Question hors-sujet** : "Quel temps fait-il demain ?" → tags `ungrounded`, grounded=0, sources_count=0 ou voisins faibles.
- **Question sur mot-clé ambigu** : "C" (langage C vs lettre C) → voir dans le span `retrieval` comment `_keyword_matches` filtre ou non.
## Dashboards Langfuse utiles
### Qualité du retrieval dans le temps
Dashboard → filtrer `score: grounded` → voir le **taux de grounded** par jour. Une chute = problème de vault ou de scoring.
### Latence p95
Dashboard → `latency` sur trace `ask` ou span `ollama-chat`. La génération est **la source de latence majoritaire** (≥ 90%), le retrieval reste sous ~100ms.
### Questions sans contexte pertinent
Filtrer tags = `ungrounded` → voir les questions posées mais non couvertes par le vault → **source d'insights pour enrichir le vault** (nouveaux alias, nouvelles notes).
### Sessions longues
Filtrer par `session_id` → enchaînement des questions d'un visiteur → voir si GrasBot garde la cohérence (pas de mémoire entre requêtes, attendu).
## Conventions de nommage
- **Spans** : kebab-case en anglais (`retrieval`, `prompt-build`, `ollama-chat`). Ici `prompt_build` a été laissé en snake pour rappeler la fonction Python, à remplacer par `prompt-build` si on refait un coup de ménage.
- **Tags** : kebab-case, préfixés par concept (`model:qwen3:8b`, `vault-miss`).
- **Scores** : snake_case nom simple (`grounded`, `retrieval_relevance`), + ajoutera plus tard `user_feedback` si on branche un 👍/👎.
## Rollback
Si Langfuse tombe en panne ou si l'instrumentation pose un souci :
1. **Soft rollback** : vider / commenter les variables `LANGFUSE_*` dans `llm-api/.env` et redémarrer uvicorn. Le client passe en no-op, aucun autre changement nécessaire.
2. **Hard rollback** : `git revert` du commit d'intégration Langfuse. Les fichiers `observability.py` / `.env` / `grasbotIds.js` disparaîtront ; le pipeline revient exactement à la v3.0.
## Sécurité — rappels
- **`LANGFUSE_SECRET_KEY`** permet d'écrire dans toutes les traces du projet → équivaut à un droit d'admin partiel. Jamais en clair dans un chat, un log, un screenshot, un commit.
- **Rotation** : en cas de doute, **Project Settings → API Keys → Delete** puis recréer. Les traces déjà ingérées ne sont pas affectées.
- Le client Langfuse envoie les traces **en asynchrone** avec un buffer → bien appeler `flush()` au shutdown pour ne rien perdre (déjà fait via le `lifespan` FastAPI).
- **Contenu sensible** : les prompts complets passent dans Langfuse. Vérifier que **le vault ne contient pas d'infos privées** (`visibility: private` est filtré côté search, mais si tu ajoutais un jour un vault mixte public/privé, il faudrait un filtre supplémentaire avant l'envoi à Langfuse).
## Tuning du pipeline — 2026-04-23
Audit des premières traces après mise en production : les réponses sur les
questions biographiques ("qui est Fernand ?") étaient parfois **hallucinées**
(âge erroné, statut inventé) et les réponses longues **tronquées** en plein
milieu. Quatre ajustements ciblés ont stabilisé le comportement :
| # | Fichier | Changement | Effet attendu |
|---|---------|------------|---------------|
| 1 | `search.py` · `generate()` | `num_ctx` explicite à **8192** | Fin de la troncature silencieuse du prompt (le défaut Ollama à 2048/4096 coupait le début du contexte quand plusieurs notes entières étaient injectées). |
| 1 | `search.py` · `generate()` | `num_predict` **512 → 1024** | Réponses longues (descriptions de projet, explications) ne sont plus coupées en plein milieu. |
| 1 | `search.py` · `generate()` | `think: false` **top-level** | Désactive le mode *thinking* de qwen3. Le modèle n'utilise plus de budget de sortie pour du raisonnement interne. |
| 2 | `search.py` · `build_prompt()` | Troncature conditionnelle des sources **rank 2+** | Les notes secondaires (ex. `inception` sur une question bio) sont résumées à `SEARCH_SECONDARY_MAX_CHARS` chars quand leur score est < `SEARCH_SECONDARY_KEEP_RATIO` × score(#1). Réduit le bruit sans supprimer de source. |
| 3 | `vault-grasbot/30-Parcours/bio-fernand.md` | **Nouvelle note** dédiée à la présentation courte | Source canonique pour les questions du type *"qui est Fernand"*. Priorité 10, aliases biographiques courts. Renvoie vers le CV complet pour le détail. |
| 3 | CV (`cv-grascalvet-fernand.md`) | Incohérence d'âge corrigée (46 → 47 ans) | Supprime la contradiction interne qui alimentait les hallucinations sur l'âge. |
| 4 | `search.py` · `SYSTEM_PROMPT` | Section "Règles de fidélité aux sources" | Force le modèle à (a) s'appuyer en priorité sur `type=parcours` pour les questions bio, (b) ne jamais inventer un fait factuel, (c) écrire *« non précisé dans les notes »* si l'info manque, (d) gérer les contradictions, (e) signaler les notes tronquées. |
Observabilité : dans les spans Langfuse, `prompt_build.metadata.truncation`
liste chaque source tronquée automatiquement → sert de point de vigilance pour
vérifier que la troncature reste pertinente (et n'écrase pas une source qu'on
aurait dû garder entière).
Variables d'environnement associées (dans `llm-api/.env` ou shell) :
| Variable | Défaut | Effet |
|----------|--------|-------|
| `SEARCH_SECONDARY_MAX_CHARS` | `1500` | Taille max des sources secondaires dans le prompt |
| `SEARCH_SECONDARY_KEEP_RATIO` | `0.8` | Tant que score(rank≥2) ≥ ratio × score(#1) → source gardée entière |
Rappel : `load_vault()` est mémoïsé. Après création/modification d'une note du
vault, appeler `POST /reload-vault` pour recharger le cache sans redémarrer
uvicorn (voir `api.py`).
## Évolutions futures possibles
- **Feedback utilisateur** : ajouter un 👍/👎 sur chaque réponse bot dans `ChatBot.js`, relayé à `/api/feedback` qui appellerait `langfuse.score(trace_id, name="user_feedback", value=1|0)`. Le `trace_id` serait retourné par `/ask` (actuellement omis).
- **Prompt versioning** : stocker `SYSTEM_PROMPT` dans Langfuse Prompts pour versionner et A/B tester sans redéploiement.
- **Coût / token pricing** : si on branche un provider payant (OpenAI / Anthropic) à la place d'Ollama, Langfuse calcule automatiquement le coût à partir de l'`usage`.
- **Dataset d'évaluation** : capturer les meilleures traces comme dataset, puis relancer le pipeline sur ces mêmes questions après modif du scoring pour comparer les sorties.

30
reload-vault.ps1 Normal file
View File

@ -0,0 +1,30 @@
# Recharge le cache du vault GrasBot sans redémarrer uvicorn (POST /reload-vault).
#
# Usage :
# PS > .\reload-vault.ps1
# PS > .\reload-vault.ps1 -BaseUrl "http://localhost:8000"
#
# Override possible via variable d'environnement LLM_API_BASE (prioritaire si -BaseUrl absent).
param(
[string]$BaseUrl = ""
)
$ErrorActionPreference = "Stop"
if (-not $BaseUrl) {
$BaseUrl = if ($env:LLM_API_BASE) { $env:LLM_API_BASE } else { "http://localhost:8000" }
}
$uri = ($BaseUrl.TrimEnd("/") + "/reload-vault")
try {
$response = Invoke-RestMethod -Method Post -Uri $uri -TimeoutSec 60
Write-Host ("Vault rechargé : {0} notes — {1}" -f $response.notes_total, $uri) -ForegroundColor Green
$response | ConvertTo-Json -Compress
}
catch {
Write-Host ("Échec : {0}" -f $_.Exception.Message) -ForegroundColor Red
Write-Host ("URI : {0}" -f $uri)
exit 1
}

View File

@ -1,10 +1,11 @@
# Compétences de Fernand Gras-Calvet # Compétences de Fernand Gras-Calvet
Cette section présente mes 4 domaines de compétences principaux. Cette section présente mes 5 domaines de compétences principaux.
## Domaines d'expertise ## Domaines d'expertise
1. **[Mon Exploration et Maîtrise de lIntelligence Artificielle](ia.md)** 1. **[Mon Exploration et Maîtrise de lIntelligence Artificielle](competence-ia.md)**
2. **[Développement Web & Hébergement sur serveur Windows](developpement-web-and-hebergement-sur-serveur-windows.md)** 2. **[Développement Web & Hébergement sur serveur Windows](competence-developpement-web-and-hebergement-sur-serveur-windows.md)**
3. **[Mon parcours dans limpression 3D](impression-3d.md)** 3. **[Mon parcours dans limpression 3D](competence-impression-3d.md)**
4. **[Mon expérience dans la domotique](competence.md)** 4. **[Mon expérience dans la domotique](competence-competence.md)**
5. **[Transcription audio (FGC transcription)](competence-transcription-audio-fgc-transcription.md)**

View File

@ -2,7 +2,7 @@
Cette documentation a été générée automatiquement à partir des données Strapi. Cette documentation a été générée automatiquement à partir des données Strapi.
**Généré le :** 15/03/2026 15:26:54 **Généré le :** 10/05/2026 10:56:28
## Structure de la documentation ## Structure de la documentation
@ -12,9 +12,9 @@ Cette documentation a été générée automatiquement à partir des données St
- Fichiers : 01-projects-index.md, project-pushswap.md, project-minitalk.md, project-philosopher.md, project-minishell.md, project-netpractice.md, project-inception.md, project-born2beroot.md, project-getnextline.md, project-cpp-partie1.md, project-cpp-partie2.md, project-cub3d.md, project-fract-ol.md, project-ft-irc.md, project-fttranscendence.md, project-prsentation-cole-42-.md, project-libft.md, project-ft-printf.md - Fichiers : 01-projects-index.md, project-pushswap.md, project-minitalk.md, project-philosopher.md, project-minishell.md, project-netpractice.md, project-inception.md, project-born2beroot.md, project-getnextline.md, project-cpp-partie1.md, project-cpp-partie2.md, project-cub3d.md, project-fract-ol.md, project-ft-irc.md, project-fttranscendence.md, project-prsentation-cole-42-.md, project-libft.md, project-ft-printf.md
### Competences ### Competences
- 4 éléments source - 5 éléments source
- 5 fichiers générés - 6 fichiers générés
- Fichiers : 02-competences-index.md, competence-ia.md, competence-developpement-web-and-hebergement-sur-serveur-windows.md, competence-impression-3d.md, competence-competence.md - Fichiers : 02-competences-index.md, competence-ia.md, competence-developpement-web-and-hebergement-sur-serveur-windows.md, competence-impression-3d.md, competence-competence.md, competence-transcription-audio-fgc-transcription.md
## Utilisation pour base vectorielle ## Utilisation pour base vectorielle

View File

@ -0,0 +1,46 @@
# Transcription audio (FGC transcription)
**Slug :** `transcription-audio-fgc-transcription`
**Ordre d'affichage :** 5
---
## Présentation
Ce projet est une **application web** dédiée à la **transcription** de l'audio en texte structuré et exportable. L'interface est accessible publiquement sur **https://transcription.fernandgrascalvet.com** ; l'usage en **priorité** reste le **poste de travail ou le réseau local** (machine **Ubuntu** avec **RTX 4090** pour le calcul lourd), avec possibilité de service derrière **HTTPS** et **authentification** lorsqu'un périmètre fermé s'impose.
L'ambition produit dépasse un simple script : **interface Next.js**, API **FastAPI**, **file de jobs** avec suivi de progression, **plusieurs formats de sortie**. La **diarisation** (qui parle quand) est **en place** via **pyannote**. La **structuration métier** (compte-rendu, schémas **JSON**, rendu **Markdown**) s'appuie sur un LLM local **Ollama** (**mistral-small3.2:24b**), avec **prompts et templates** adaptés au cas **avec** ou **sans** diarisation ; sur textes longs, une stratégie **map-reduce** peut être employée selon le budget tokens.
---
## Problème adressé
Les réunions, cours et entretiens produisent des **enregistrements longs** ; les transformer en **texte éditable**, **horodatable** et **réutilisable** (sous-titres, notes, archives) demande à la fois un moteur de reconnaissance performant et une **chaîne logicielle** fiable : ingestion, traitement, erreurs, téléchargements.
---
## Fonctionnalités principales (périmètre documenté)
- **Entrées** : dépôt de **fichier audio** (et conteneurs vidéo lorsque le worker extrait la piste audio) ou **enregistrement depuis le navigateur** — le tout converge vers la même API de **création de job**.
- **Transcription** : **faster-whisper** sur GPU, avec réglages de modèle et de précision progressivement exposés dans l'UI.
- **Diarisation** : **pyannote**, fusion transcript / locuteurs dans le pipeline.
- **Suivi** : états de job visibles ; exports au minimum en **TXT**, **SRT**, **VTT**, **JSON** (segments).
- **Résumés structurés** : **templates métier** et appels **Ollama** (**mistral-small3.2:24b**) pour livrables **JSON** et **Markdown**.
---
## Architecture et contraintes
Le navigateur s'appuie sur **Next.js** ; les routes métier sont proxifiées vers **FastAPI**. L'**authentification JWT** et les comptes utilisateurs sont prévus pour les phases où l'API ne doit plus être ouverte publiquement. Le **micro** en production impose une origine **HTTPS** (ou localhost), ce qui s'aligne avec une terminaison TLS devant l'application.
---
## Positionnement
Le dépôt formalise une compétence **IA appliquée au signal audio** : concevoir un **produit** — pas seulement entraîner ou invoquer un modèle — avec documentation (**cahier des charges**, **architecture des flux**, **roadmap**) pensée pour **reprendre le développement** entre deux sessions ou avec un assistant.
---
## En bref
**Transcription audio**, c'est transformer la parole en **données utiles** : qualité de reconnaissance, **hygiène** des pipelines (jobs, erreurs, exports), **diarisation**, puis **comptes rendus** assistés par LLM — avec l'application sur **https://transcription.fernandgrascalvet.com** et la fiche portfolio **https://fernandgrascalvet.com/competences/transcription-audio-fgc-transcription**.

View File

@ -1,5 +1,5 @@
{ {
"generatedAt": "2026-03-15T14:26:54.969Z", "generatedAt": "2026-05-10T08:56:28.903Z",
"results": [ "results": [
{ {
"type": "projects", "type": "projects",
@ -30,19 +30,20 @@
{ {
"type": "competences", "type": "competences",
"success": true, "success": true,
"itemCount": 4, "itemCount": 5,
"docCount": 5, "docCount": 6,
"files": [ "files": [
"02-competences-index.md", "02-competences-index.md",
"competence-ia.md", "competence-ia.md",
"competence-developpement-web-and-hebergement-sur-serveur-windows.md", "competence-developpement-web-and-hebergement-sur-serveur-windows.md",
"competence-impression-3d.md", "competence-impression-3d.md",
"competence-competence.md" "competence-competence.md",
"competence-transcription-audio-fgc-transcription.md"
] ]
} }
], ],
"totalTypes": 3, "totalTypes": 3,
"successfulTypes": 2, "successfulTypes": 2,
"totalSourceItems": 21, "totalSourceItems": 22,
"totalDocuments": 24 "totalDocuments": 25
} }

View File

@ -30,5 +30,13 @@
"slug": "competence", "slug": "competence",
"order": 4, "order": 4,
"imageCount": 12 "imageCount": 12
},
{
"id": 22,
"name": "Transcription audio (FGC transcription)",
"content": "## Présentation\n\nCe projet est une **application web** dédiée à la **transcription** de l'audio en texte structuré et exportable. L'interface est accessible publiquement sur **https://transcription.fernandgrascalvet.com** ; l'usage en **priorité** reste le **poste de travail ou le réseau local** (machine **Ubuntu** avec **RTX 4090** pour le calcul lourd), avec possibilité de service derrière **HTTPS** et **authentification** lorsqu'un périmètre fermé s'impose.\n\nL'ambition produit dépasse un simple script : **interface Next.js**, API **FastAPI**, **file de jobs** avec suivi de progression, **plusieurs formats de sortie**. La **diarisation** (qui parle quand) est **en place** via **pyannote**. La **structuration métier** (compte-rendu, schémas **JSON**, rendu **Markdown**) s'appuie sur un LLM local **Ollama** (**mistral-small3.2:24b**), avec **prompts et templates** adaptés au cas **avec** ou **sans** diarisation ; sur textes longs, une stratégie **map-reduce** peut être employée selon le budget tokens.\n\n---\n\n## Problème adressé\n\nLes réunions, cours et entretiens produisent des **enregistrements longs** ; les transformer en **texte éditable**, **horodatable** et **réutilisable** (sous-titres, notes, archives) demande à la fois un moteur de reconnaissance performant et une **chaîne logicielle** fiable : ingestion, traitement, erreurs, téléchargements.\n\n---\n\n## Fonctionnalités principales (périmètre documenté)\n\n- **Entrées** : dépôt de **fichier audio** (et conteneurs vidéo lorsque le worker extrait la piste audio) ou **enregistrement depuis le navigateur** — le tout converge vers la même API de **création de job**.\n- **Transcription** : **faster-whisper** sur GPU, avec réglages de modèle et de précision progressivement exposés dans l'UI.\n- **Diarisation** : **pyannote**, fusion transcript / locuteurs dans le pipeline.\n- **Suivi** : états de job visibles ; exports au minimum en **TXT**, **SRT**, **VTT**, **JSON** (segments).\n- **Résumés structurés** : **templates métier** et appels **Ollama** (**mistral-small3.2:24b**) pour livrables **JSON** et **Markdown**.\n\n---\n\n## Architecture et contraintes\n\nLe navigateur s'appuie sur **Next.js** ; les routes métier sont proxifiées vers **FastAPI**. L'**authentification JWT** et les comptes utilisateurs sont prévus pour les phases où l'API ne doit plus être ouverte publiquement. Le **micro** en production impose une origine **HTTPS** (ou localhost), ce qui s'aligne avec une terminaison TLS devant l'application.\n\n---\n\n## Positionnement\n\nLe dépôt formalise une compétence **IA appliquée au signal audio** : concevoir un **produit** — pas seulement entraîner ou invoquer un modèle — avec documentation (**cahier des charges**, **architecture des flux**, **roadmap**) pensée pour **reprendre le développement** entre deux sessions ou avec un assistant.\n\n---\n\n## En bref\n\n**Transcription audio**, c'est transformer la parole en **données utiles** : qualité de reconnaissance, **hygiène** des pipelines (jobs, erreurs, exports), **diarisation**, puis **comptes rendus** assistés par LLM — avec l'application sur **https://transcription.fernandgrascalvet.com** et la fiche portfolio **https://fernandgrascalvet.com/competences/transcription-audio-fgc-transcription**.",
"slug": "transcription-audio-fgc-transcription",
"order": 5,
"imageCount": 0
} }
] ]

View File

@ -109,7 +109,7 @@ const generators = {
indexContent += `## Domaines d'expertise\n\n`; indexContent += `## Domaines d'expertise\n\n`;
sortedCompetences.forEach((competence, index) => { sortedCompetences.forEach((competence, index) => {
indexContent += `${index + 1}. **[${competence.name}](${competence.slug}.md)**\n`; indexContent += `${index + 1}. **[${competence.name}](competence-${createSafeFileName(competence.slug)}.md)**\n`;
}); });
docs.push({ docs.push({

View File

@ -15,7 +15,7 @@ answers:
- Que fait-il en Compétences ? - Que fait-il en Compétences ?
priority: 7 priority: 7
linked: linked:
updated: 2026-04-22 updated: 2026-05-10
visibility: public visibility: public
--- ---
@ -28,3 +28,4 @@ Hub des domaines de compétences.
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_ - [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_ - [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
- [[impression-3d|Mon parcours dans limpression 3D]] — _3d, algorithmique, reseau_ - [[impression-3d|Mon parcours dans limpression 3D]] — _3d, algorithmique, reseau_
- [[transcription-audio-fgc-transcription|Transcription audio (FGC transcription)]] — _devops, ia, web_

View File

@ -23,12 +23,12 @@ answers:
- Que fait-il en ia ? - Que fait-il en ia ?
priority: 7 priority: 7
linked: linked:
updated: 2026-04-23 updated: 2026-05-10
visibility: public visibility: public
--- ---
Notes du domaine *ia* (8 au total : 2 compétences + 6 projets liés). Notes du domaine *ia* (9 au total : 3 compétences dédiées IA / croisées + 6 projets liés).
## Notes liées ## Notes liées
@ -38,5 +38,6 @@ Notes du domaine *ia* (8 au total : 2 compétences + 6 projets liés).
- [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_ - [[ft-linear-regression|ft_linear_regression]] — _algorithmique, ecole-42, ia_
- [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_ - [[piscine-python-data-science|Piscine Python — Data Science]] — _algorithmique, ecole-42, ia_
- [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_ - [[ia|Mon Exploration et Maîtrise de lIntelligence Artificielle]] — _algorithmique, ecole-42, ia_
- [[transcription-audio-fgc-transcription|Transcription audio (FGC transcription)]] — _devops, ia, web_
- [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_ - [[competence|Mon expérience dans la domotique]] — _algorithmique, domotique, ia, reseau_
- [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_ (couverture transversale du site) - [[fernandgrascalvet-com|fernandgrascalvet.com (portfolio)]] — _devops, ia, web_ (couverture transversale du site)

View File

@ -26,7 +26,8 @@ related:
- "[[ia]]" - "[[ia]]"
- "[[newsletter-ia]]" - "[[newsletter-ia]]"
- "[[grasbot]]" - "[[grasbot]]"
updated: 2026-04-23 - "[[transcription-audio-fgc-transcription]]"
updated: 2026-05-10
visibility: public visibility: public
--- ---

View File

@ -35,6 +35,7 @@ linked:
related: related:
- "[[grasbot]]" - "[[grasbot]]"
- "[[newsletter-ia]]" - "[[newsletter-ia]]"
- "[[transcription-audio-fgc-transcription]]"
- "[[transcription-video]]" - "[[transcription-video]]"
- "[[ft-linear-regression]]" - "[[ft-linear-regression]]"
- "[[piscine-python-data-science]]" - "[[piscine-python-data-science]]"

View File

@ -0,0 +1,109 @@
---
title: Transcription audio (FGC transcription)
slug: transcription-audio-fgc-transcription
type: competence
source: manual
domains: [ia, web, devops]
tags:
- transcription
- whisper
- stt
- pyannote
- diarisation
- fastapi
- nextjs
- ollama
- mistral-small
- structured-summary
aliases:
- transcription audio fgc
- fgc transcription
- transcription-audio-fgc-transcription
- faster-whisper
- pyannote
- mistral-small3.2
- mistralsmall3.2:24b
- résumé structuré transcription
- json transcription
- transcription fernandgrascalvet
- url application transcription
answers:
- Parle-moi du projet de transcription audio FGC.
- Qu'est-ce que le projet FGC transcription ?
- Quelle est l'URL ou le lien de l'application web de transcription ?
- Où accéder à la transcription en ligne fernandgrascalvet ?
- Quel problème pour les réunions et enregistrements longs ?
- La diarisation et pyannote sont-ils utilisés ?
- Comment sont générés les résumés après transcription ?
- Quel modèle LLM pour les résumés JSON et Markdown ?
- Mistral small 24b sert à quoi dans le pipeline ?
- Différence résumé avec ou sans diarisation ?
- Quelle stack technique FastAPI Next transcription ?
priority: 7
linked:
- "[[MOC-Competences]]"
- "[[MOC-Ia]]"
related:
- "[[ia]]"
- "[[transcription-video]]"
- "[[fernandgrascalvet-com]]"
- "[[grasbot]]"
updated: 2026-05-10
visibility: public
# Segment URL publique : /competences/{route_parent}/{slug} (sinon /competences/{slug} par défaut).
route_parent: ia
---
**Slug :** `transcription-audio-fgc-transcription`
**Ordre d'affichage :** _(Strapi `order` — typiquement après la domotique)_
---
## Présentation
Ce projet est une **application web** dédiée à la **transcription** de l'audio en texte structuré et exportable. L'interface est accessible publiquement sur **https://transcription.fernandgrascalvet.com** ; l'usage en **priorité** reste le **poste de travail ou le réseau local** (machine **Ubuntu** avec **RTX 4090** pour le calcul lourd), avec possibilité de service derrière **HTTPS** et **authentification** lorsqu'un périmètre fermé s'impose.
L'ambition produit dépasse un simple script : **interface Next.js**, API **FastAPI**, **file de jobs** avec suivi de progression, **plusieurs formats de sortie**. La **diarisation** (qui parle quand) est **en place** via **pyannote**. La **structuration métier** (compte-rendu, schémas **JSON**, rendu **Markdown**) s'appuie sur un LLM local **Ollama** (**mistral-small3.2:24b**), avec **prompts et templates** adaptés au cas **avec** ou **sans** diarisation ; sur textes longs, une stratégie **map-reduce** peut être employée selon le budget tokens.
---
## Problème adressé
Les réunions, cours et entretiens produisent des **enregistrements longs** ; les transformer en **texte éditable**, **horodatable** et **réutilisable** (sous-titres, notes, archives) demande à la fois un moteur de reconnaissance performant et une **chaîne logicielle** fiable : ingestion, traitement, erreurs, téléchargements.
---
## Fonctionnalités principales (périmètre documenté)
- **Entrées** : dépôt de **fichier audio** (et conteneurs vidéo lorsque le worker extrait la piste audio) ou **enregistrement depuis le navigateur** — le tout converge vers la même API de **création de job**.
- **Transcription** : **faster-whisper** sur GPU, avec réglages de modèle et de précision progressivement exposés dans l'UI.
- **Diarisation** : **pyannote**, fusion transcript / locuteurs dans le pipeline.
- **Suivi** : états de job visibles ; exports au minimum en **TXT**, **SRT**, **VTT**, **JSON** (segments).
- **Résumés structurés** : **templates métier** et appels **Ollama** (**mistral-small3.2:24b**) pour livrables **JSON** et **Markdown**.
---
## Architecture et contraintes
Le navigateur s'appuie sur **Next.js** ; les routes métier sont proxifiées vers **FastAPI**. L'**authentification JWT** et les comptes utilisateurs sont prévus pour les phases où l'API ne doit plus être ouverte publiquement. Le **micro** en production impose une origine **HTTPS** (ou localhost), ce qui s'aligne avec une terminaison TLS devant l'application.
---
## Positionnement
Le dépôt formalise une compétence **IA appliquée au signal audio** : concevoir un **produit** — pas seulement entraîner ou invoquer un modèle — avec documentation (**cahier des charges**, **architecture des flux**, **roadmap**) pensée pour **reprendre le développement** entre deux sessions ou avec un assistant.
---
## En bref
**Transcription audio**, c'est transformer la parole en **données utiles** : qualité de reconnaissance, **hygiène** des pipelines (jobs, erreurs, exports), **diarisation**, puis **comptes rendus** assistés par LLM — avec l'application sur **https://transcription.fernandgrascalvet.com** et la fiche portfolio **https://fernandgrascalvet.com/competences/transcription-audio-fgc-transcription**.
---
## Liens
- [[MOC-Competences]] — vue densemble des compétences
- [[MOC-Ia]] — domaine intelligence artificielle
- [[ia]] — parcours IA et infra locale
- [[transcription-video]] — autre chantier transcription / média
- [[fernandgrascalvet-com]] — portfolio où la fiche est exposée

View File

@ -5,13 +5,13 @@ contenus Strapi du site (projets + compétences) et du CV PDF. Alimente
directement le pipeline de recherche de GrasBot (`llm-api/search.py`) : directement le pipeline de recherche de GrasBot (`llm-api/search.py`) :
graph + BM25, sans embeddings. graph + BM25, sans embeddings.
**Dernière génération :** 2026-04-23 (complété manuellement : +2 projets `source: manual`, maj compétences IA/Web) **Dernière génération :** 2026-04-23 (complété manuellement : +2 projets `source: manual`, maj compétences IA/Web ; **2026-05-10** : compétence `transcription-audio-fgc-transcription` — pyannote + Mistral Small 24b, corpus Strapi aligné dans `strapi_extraction/`)
## Structure ## Structure
- `00-MOC/` — Maps of Content (hubs thématiques) - `00-MOC/` — Maps of Content (hubs thématiques)
- `10-Projets/` — 17 projets Strapi + **6 notes manuelles** (`grasbot.md`, `newsletter-ia.md`, `transcription-video.md`, `fernandgrascalvet-com.md`, `ft-linear-regression.md`, `piscine-python-data-science.md`) - `10-Projets/` — 17 projets Strapi + **6 notes manuelles** (`grasbot.md`, `newsletter-ia.md`, `transcription-video.md`, `fernandgrascalvet-com.md`, `ft-linear-regression.md`, `piscine-python-data-science.md`)
- `20-Competences/`4 compétences extraites de Strapi - `20-Competences/` — compétences Strapi extraites par `build-vault.py` **et** note manuelle **`transcription-audio-fgc-transcription.md`** (alignée Strapi / GrasBot, non écrasée tant que `source: manual`)
- `30-Parcours/` — Parcours personnel, CV, bio (version curatée `source: manual`) - `30-Parcours/` — Parcours personnel, CV, bio (version curatée `source: manual`)
- `40-Glossaire/` — Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard) - `40-Glossaire/` — Termes techniques (vide, à remplir manuellement ou depuis Strapi plus tard)
- `50-Technique/` — Auto-documentation (architecture, retrieval, vault) - `50-Technique/` — Auto-documentation (architecture, retrieval, vault)
@ -37,6 +37,7 @@ answers: # questions-types auxquelles répond la
priority: 5 # 1..10, boost léger au scoring priority: 5 # 1..10, boost léger au scoring
linked: ["[[MOC-...]]"] # voisins du graphe (sortants) linked: ["[[MOC-...]]"] # voisins du graphe (sortants)
related: ["[[autre-note]]"] related: ["[[autre-note]]"]
route_parent: ia # optionnel (compétence) : lien source `/competences/ia/{slug}`
updated: YYYY-MM-DD updated: YYYY-MM-DD
visibility: public | private # `private` exclu du retrieval visibility: public | private # `private` exclu du retrieval
--- ---

View File

@ -66,7 +66,11 @@ actuellement utilisés :
| `openwebui` | Open WebUI (front LLM) | | `openwebui` | Open WebUI (front LLM) |
| `ovh` | OVHcloud AI Endpoints | | `ovh` | OVHcloud AI Endpoints |
| `transcription` | Speech-to-text / sous-titres | | `transcription` | Speech-to-text / sous-titres |
| `mistral-small` | Résumés via Mistral Small (ex. 24b Ollama) |
| `structured-summary` | Résumés JSON / Markdown à schéma contrôlé |
| `pyannote` | Diarisation / attribution locuteurs |
| `multimedia` | Traitement audio / vidéo | | `multimedia` | Traitement audio / vidéo |
| `diarisation` | Segmentation locuteurs / tours de parole |
## Aliases (`aliases:`) ## Aliases (`aliases:`)