Files
nav-carte/JOURNAL-V2.md
2026-04-28 14:00:05 +02:00

1006 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: journal
project: NAV V2
created: 2026-04-14
status: actif
---
# NAV V2 — Journal de développement
Journal technique de la V2. Décisions, anomalies, points bloquants, TODOs.
---
## 2026-04-27 — Session V3 : Finition mobile + Blog Liberapay + 3 deploys
**Commit :** `a02a555` — feat(mobile): accordéon outremer, hamburger nav, logo AEP, fiches cliquables, chatbot fullscreen
**Pattern :** agents parallèles (3 × Sonnet) pour les 3 SSH indépendants — ~90s total vs ~20min séquentiel
### Changements implémentés
**B — OutremerMap.vue : accordéon vertical DOM-TOM**
- Template : row horizontale → accordéon `<button>` + `v-show` par territoire
- Lazy-init Leaflet : `initSingleMap(domName)` appelé au 1er clic (plus de `initMaps()` en `onMounted`)
- `invalidateSize()` sur ré-ouverture d'une carte déjà initialisée
**E — app.vue : hamburger mobile**
- Bouton `lg:hidden` tout à droite de `<!-- Actions droite -->`
- Dropdown `v-if` : 5 liens (/, /agences, /rag, /a-propos, /signaler)
- `z-index: 9999` en inline style sur le dropdown
- `watch(() => route.path)` → ferme le menu à chaque navigation
- **Fix stacking context :** header → `relative z-[9999]` (sans `position`, le z-index Tailwind n'avait pas d'effet → dropdown passait sous Leaflet)
**F — app.vue : badge "A" → "AEP"**
- `w-7 h-7``h-7 px-2`, `text-sm``text-xs tracking-tight`
**G — pages/index.vue : fiches ouvrables mobile**
- `onSelectOrgMobile` : ajout `storeFiltersForBack()` + `router.push('/fiche/${id}')`
- NB : `router.push` était déjà là (modif pilote antérieure) — seul `storeFiltersForBack` ajouté
**H — ChatbotSheet.vue : fullscreen + scroll lock iOS**
- `92dvh → 100dvh`, `border-radius: 16px 16px 0 0 → 0`
- `defineProps``const props = defineProps` (fix "can't find variable: props" en prod)
- `watch(props.modelValue)` → lock `document.body.style.overflow` + `document.documentElement.style.overflow`
- `onUnmounted` → cleanup overflow
**A — Blog trans-former.fr : Liberapay retiré**
- VPS `/opt/astro-site/src/layouts/PostLayout.astro` : import + `<DonateButton />` supprimés
- Docker rebuild → 31 pages, HTTP 200
**C — Website pro deploy**
- `index.astro` SCP → `/opt/astro-pro/`, Docker rebuild → 13 pages, HTTP 200
### TODOs ouverts AEP
- [ ] Valider visuellement les 8 fixes sur tel (Jules)
- [ ] Pousser nav-carte sur Gitea (`git.trans-former.fr/jules/nav-carte`)
---
## 2026-04-15 — Session 5 : Corrections post Phase 2 (11 retours Jules)
**Exécutant :** Sonnet (agent autonome full auto)
**Durée :** ~45min
**Commits :** 7 commits atomiques (8ae4be3 → d30ee2c)
### Retours implémentés
**#1 — Barre de recherche** ✅
- Desktop : supprimée du header (doublon avec sidebar `NavSidebar.vue`)
- Mobile : ajoutée dans le header (`app.vue`, visible `<lg`)
**#2 — Sheet swipable mobile** ✅
- Nouveau composant `MobileSheet.vue` — 3 états : collapsed (56px) / half (50dvh) / full (92dvh)
- Touch events natifs (pas de `@vueuse/core` — implémentation vanilla pour 0 dépendance)
- Drag handle + cycle d'états au clic header + snap-back si delta < 60px
- Vue mobile index.vue : carte pleine hauteur en fond + sheet en overlay
- Décision d'exécution : vanilla touch events choisis vs useSwipe (pas installé)
**#3 — Onglets header desktop** ✅
- 3 onglets centrés remplacent la barre de recherche desktop : Écosystème / Agences Inspirantes / RAG
- Active state via `route.path` + underline `--nav-primary-solid`
- Badge "en construction" sous les 2 onglets inactifs
**#4 — Supprimer "+ Ajouter carte"** ✅
- Bouton retiré du header `app.vue`
- Route `pages/ajouter-carte.vue` supprimée
**#5 — Report/modif participatif via Resend** ✅
- `server/api/report.post.ts` : rate limit 5/IP/jour, validation email + message, envoi Resend API
- `FicheDetail.vue` : bouton "Signaler une erreur" → form inline dépliable (message + email, compteur 500 chars)
- Email envoyé à `jules@trans-former.fr` (fallback Resend, pas NocoDB)
**#6 — Texte intro commentaires** ✅
- `CommentSection.vue` : intro italique avant la liste
**#7 — DOM-TOM row horizontale pleine largeur** ✅ (Option A)
- `OutremerMap.vue` : layout row flex 5 colonnes égales, hauteur 140px
- Desktop `index.vue` : layout vertical (Métropole flex-1 + DOM-TOM row 140px fixe en bas)
- Suppression de l'encart 1/3 droit
**#8 — Supprimer layer control carte** ✅
- `NavMap.vue` : `L.control.layers(...)` supprimé, seul CartoDB Positron reste
**#9 — Dark mode switch tuile** ✅
- `NavMap.vue` + `OutremerMap.vue` : `MutationObserver` sur `html.classList``setUrl()` light_all/dark_all
- Initialisation correcte dès le premier rendu (lire `document.documentElement.classList`)
**#10 — Bandeau bas : logique inversée** ✅
- `BandeauBas.vue` : `isCollapsed = ref(true)` par défaut
- `onMouseLeave` : repli immédiat (suppression du timer 3s)
- Opacité bandeau déployé : 70%
**#11 — Bouton Soutenir recentré** ✅
- `bandeau-col--center` : `padding-top: 8px` pour décaler vers le bas dans la hauteur du bandeau
### Cleanup
- Supprimés : `TerritoireTabs.vue`, `TerritoireToggle.vue` (composants morts)
- Supprimée : `pages/ajouter-carte.vue`
- Créées : `pages/agences.vue`, `pages/rag.vue` (placeholders)
- Créé : `components/MobileSheet.vue`
### Décisions d'exécution S5
- **#2 useSwipe** : `@vueuse/core` absent du `package.json` — vanilla touch events choisis pour éviter d'ajouter une dépendance. Même résultat, 0 dép supplémentaire.
- **#5 NocoDB vs Resend** : fallback Resend appliqué conformément aux décisions Jules pré-tranchées. Pas de table NocoDB créée.
- **DOM-TOM desktop** : layout du `index.vue` passé de `flex row (2/3 + 1/3)` à `flex col (Métropole flex-1 + DOM-TOM row 140px)` pour intégrer l'Option A cohéremment.
### Build & deploy
- Build : `npx nuxt build` ✅ (223 modules client, 134 modules serveur)
- Deploy : tar → SSH → `systemctl restart nav-carte`
- Vérif : `curl https://aep.trans-former.fr/ → 200`, service `active`
---
## 2026-04-15 — Session 4 : Phase 2 UX (10 features)
**Exécutant :** Sonnet (agent délégué Opus)
**Durée :** ~1h30
### Réalisé
**Feature 1 — Modal fiche sidebar desktop**
- Composant `FicheModal.vue` : overlay centré max-w-3xl (768px), 90vh scroll interne
- Backdrop semi-transparent, fermeture Esc / clic backdrop / bouton croix
- Lien "Ouvrir" → `/fiche/[id]` (URL partageable préservée)
- Fetch `$fetch('/api/fiche/X')` avec watch sur orgId
- `onSelectOrg` desktop (≥1024px) → ouvre modal
- Mobile : comportement inchangé (navigation vers `/fiche/[id]`)
**Feature 2 — Fusion Outre-mer (suppression TerritoireTabs)**
- Desktop : layout flex `flex:2` Métropole + `flex:1` bandeau DOM-TOM (max 340px, OutremerMap réutilisé)
- Mobile : section DOM-TOM scroll horizontal avec compteurs par territoire
- `TerritoireTabs` retiré du template (composant conservé, non utilisé)
**Feature 3 — CartoDB Positron + layer control + maxBounds**
- `NavMap.vue` : fond par défaut CartoDB Positron (`light_all`), layer control 3 fonds (Clair / Schématique Stamen / Standard OSM)
- `maxBounds` France métr. `[41-51.5°N, -5.5-10°E]`, `minZoom: 5`, `maxZoom: 18`
- `OutremerMap.vue` : fond CartoDB Positron par défaut (sans layer control)
**Feature 4 — Bandeau bas rétractable desktop**
- `bg-opacity-80` (rgba 0.8)
- Auto-collapse après 3s, déploie au hover ou mouvement souris < 80px du bas
- Barre fine `32px` en état rétracté avec label "AEP · Transparence IA"
**Features 5+6 — Textes bandeau corrigés + lien Liberapay**
- "Coût IA ce mois : X.XX €" + "Tokens : X" (sans ratio budget)
- Tooltip "1 € = 30 fiches mises en ligne" au hover bouton Soutenir
- Lien Liberapay → `https://liberapay.com/trans-former.fr/donate`
- Mobile : BandeauBas absent, remplacé par FAB coeur (gauche) → bottom sheet
**Feature 7 — Logo AEP tooltip**
- Sous-titre "Architecture d'Écologie Politique" visible en `lg:`
- Tooltip au hover pour `sm:` (sans le sous-titre)
- Attribut `title` sur le lien pour accessibilité
**Feature 8 — Header desktop refonte + barre de recherche**
- Barre de recherche ~500px centrée dans le header desktop (≥1024px)
- Sync URL `?q=` via watch, fonctionne depuis toutes les pages
- Boutons "+ Proposer" et "+ Ajouter carte" en haut à droite
- Route `/ajouter-carte` créée (placeholder)
**Feature 9 — Dark mode**
- Toggle soleil/lune en top nav, persistance `localStorage` clé `aep_theme`
- Variables CSS `.dark` dans `main.css` : fonds sombres, texte clair
- Overrides Leaflet (popup, control layers)
- Note : tuile CartoDB dark (`dark_all`) non switchée automatiquement — à améliorer en S5
**Feature 10 — /a-propos CTA Contribuer + lien Liberapay**
- Section "Contribuer" existante améliorée (lien `/donate`)
- Liens Liberapay unifiés sur `/donate`
### Commits
| Hash | Message |
|------|---------|
| `46b6051` | feat(aep-s4): CartoDB Positron + layer control + maxBounds France |
| `3c036db` | feat(aep-s4): logo AEP + tooltip nom complet + dark mode toggle |
| `775ec64` | feat(aep-s4): dark mode CSS variables + Leaflet overrides |
| `9736ba7` | feat(aep-s4): bandeau bas — rétractable desktop + FAB mobile + textes corrigés |
| `d092fd0` | feat(aep-s4): /a-propos — liens Liberapay corrigés (/donate) |
| `955a561` | feat(aep-s4): page /ajouter-carte (placeholder bientôt disponible) |
| `b406cc9` | feat(aep-s4): fusion outremer — suppression TerritoireTabs + vue 2/3+1/3 |
| `2b43e90` | feat(aep-s4): modal fiche sidebar desktop |
### Build & Deploy
- `npm run build` : ✓ 2.84 MB bundle
- Deploy VPS : ✓ `systemctl is-active = active`
- `curl -sI https://aep.trans-former.fr/` : HTTP/2 200
### Points d'attention pour S5
- Dark mode tuile Leaflet : switcher automatiquement vers `dark_all` CartoDB quand `.dark` est actif
- BandeauBas détection mobile avec `isMobile` ref côté client : SSR safe (montage) mais risque blink. Envisager `useWindowSize` ou classe CSS média
- `TerritoireTabs` et `TerritoireToggle` non utilisés → à archiver ou supprimer en S5
- Barre de recherche header : sur les pages `/fiche/[id]` et `/a-propos`, la recherche navigue vers `/?q=X` — comportement acceptable mais pas élégant
- Modal fiche : pas de CommentForm affiché côté SSR (correctement client-only via watch)
---
## 2026-04-14 — Session 3a : Chatbot API (Étape 5bis)
**Exécutant :** Sonnet (agent chatbot)
**Durée :** ~30 min
### Réalisé
**ChatbotSheet.vue** — Déjà complet (S2) : onboarding exact E-spec §Détails chatbot, bulles user/assistant, fiches recommandées avec lien, erreurs 429/503, animation slide-up mobile.
**API POST /api/chatbot** (nouveau) :
- Rate limit : 10 req/IP/jour via `rateLimitJson.ts` (JSON + SHA-256 RGPD, spec F §8)
- Circuit breaker : vérification budget 20€/mois via `circuitBreaker.ts` → HTTP 503 si dépassé
- Keyword scoring : extraction mots-clés question → score sur nom+description+tags → top 20 fiches
- Filtrage optionnel par `fonction` et `echelle` si fournis dans le body
- Appel Mistral Small (mistral-small-latest, temp 0.3, max 600 tokens, json_object)
- Parse JSON → `{ reponse_texte, fiches_recommandees: [{ id, nom, explication }] }`
- Log asynchrone `stats_usage` (non bloquant)
**Helpers nouveaux** :
- `server/utils/rateLimitJson.ts` — fichier JSON par IP hashée SHA-256 dans `/tmp/nav-ratelimit/`
- `server/utils/circuitBreaker.ts` — budget check NocoDB stats_usage + calcul coût Mistral Small/Nemo
- `nuxt.config.ts` — ajout `statsTableId` (défaut `mbbq7n47ixy19mc`)
### Décisions
| Décision | Détail |
|----------|--------|
| rateLimitJson vs Redis existant | JSON fichier créé séparément (spec F §8 + RGPD SHA-256). Redis existant conservé pour submit/comment. Les deux coexistent. |
| Prompt Mistral Small | Construit depuis E-spec-frontend.md §Détails chatbot — pas de prompt verbatim dans F-spec §3 (qui est Nemo enrichissement). **Checkpoint Jules requis avant deploy prod.** |
| circuitBreaker.ts | Worker absent → créé dans server/utils/ (source unique, réutilisable par le futur worker) |
| statsTableId | Défaut hardcodé `mbbq7n47ixy19mc` + env var `STATS_TABLE_ID` en override |
### Commits
- `718e9f6` — feat(chatbot): API /api/chatbot + rate limit JSON SHA-256 + circuit breaker
### Build
- `npm run build` : ✓ sans erreur, 2.78 MB bundle
### TODO avant deploy (S3b)
- [ ] `STATS_TABLE_ID=mbbq7n47ixy19mc``/opt/nav-carte/.env` (confirmer ou corriger l'ID)
- [ ] Vérifier que `/tmp/nav-ratelimit/` est accessible en écriture sur VPS (droits node)
- [ ] Cron journalier reset `/tmp/nav-ratelimit/` (0h UTC) → à ajouter en S3b
- [ ] Tester chatbot 2-3 requêtes réelles sur VPS après deploy
- [ ] **Checkpoint Jules** : valider prompt Mistral Small (construit, pas copié-collé de F-spec)
---
## 2026-04-14 — Session 2 Ajustements UX v3
**Pilote :** Sonnet
**Durée :** ~30 min
### Réalisé
**A) Search desktop déplacé top nav → sidebar (haut)**
- `TopSearchBar` retiré du header `app.vue` — top nav épuré (logo + Contribuer + Aléatoire)
- Barre recherche inline ajoutée en haut de `NavSidebar.vue` : toujours visible, pleine largeur sidebar, styles scoped `sidebar-search-*`
- Focus ring `var(--nav-primary)`, bouton clear intégré
- Mobile inchangé (search sticky bandeau entre filtres et liste)
- URL sync `?q=` conservé via `pages/index.vue`
- Commit : `3f88e86`
**B) Chatbot desktop cliquable + expand/collapse**
- `ChatbotPlaceholder.vue` refactorisé : état replié (56px) → étendu (45vh) au clic
- Header entier cliquable + chevron dédié avec rotation SVG 180° animée
- `transition: max-height 0.3s ease` — animation fluide
- Zone étendue : placeholder conversation S3, overflow-y auto
- Mobile `ChatbotSheet.vue` inchangé
- Commit : `88d0319`
### Build
- `npm run build` clean (0 erreurs, 0 warnings TS)
---
## 2026-04-14 — Session 1 : Setup + Fondations
**Pilote :** Opus
**Exécutants :** Sonnet-1 (VPS/NocoDB), Sonnet-2 (seed parsing)
**Durée estimée session :** 4-5h
### Décisions validées
| Décision | Détail | Source |
|----------|--------|--------|
| Palette | A (sobre institutionnel) + bleu nuit #1a2238 à 60% opacité **partout** (bandeau, pins, chips, surlignages) | Jules 2026-04-14 |
| Texte | Bleu plein pour titres/courant (lisibilité > esthétique) | Déduction palette |
| Liberapay | `liberapay.com/trans-former.fr` (pas `nav-archi`) | Jules |
| Seed | Bypass modération pour 94 fiches, 2-3 réservées pour test pipe IA (doc de la pipe → skill Mistral Nemo futur) | Jules |
| Liberapay transparence | Section dédiée dans `/a-propos` (étape 8), pas dans le bandeau | Jules |
| Circuit breaker dépassé | Bandeau "manque de fonds" + CTA Liberapay + pédagogie "1€ = N requêtes" + transparence origine Liberapay | Jules |
| From email modération | `contact@trans-former.fr` (existant Resend) | Jules |
| Session stratégie | 3 sessions dédiées (S1 fondations, S2 front, S3 IA+deploy) — préserve jauge Opus, use Sonnet pour exécution | Jules |
| Tokens | Mistral key stockée dans `/opt/nav-carte/.env`, jamais committée | Standard |
### Réalisé
**Sonnet-1 — Setup VPS + schéma NocoDB :**
- Token NocoDB `nav-v2-worker` créé : `R-Yhd_0KgfW0ZjFxIl5iNyLS1ca7VpP8dNbo4OOa`
- `/opt/nav-carte/.env` créé (chmod 600) avec `MISTRAL_API_KEY`, `NOCODB_TOKEN`, `NOCODB_BASE`, `NOCODB_TABLE_*`, `RESEND_FROM`, `NOCODB_URL`
- crawl4ai 0.8.6 installé via pip (`--ignore-installed` pour conflit lib `rich`)
- Table `organisations` (`m08t7g5v4wch6wb`) étendue : 19 champs ajoutés
- Contenus : url, description_user, description_enrichie, points_cles, localisation_ville, submitted_by_email, moderator_note, ai_raw_output, scrape_content
- Taxonomie : echelle (SingleSelect), territoire (SingleSelect), tags_fonction (MultiSelect)
- État : scrape_status, moderation_status, ai_processed (Checkbox)
- Géo : latitude, longitude
- Meta : submitted_at, moderated_at
- Table `stats_usage` créée : `mbbq7n47ixy19mc` (model, endpoint, tokens_in/out, cout_eur, timestamp, orga_id)
- Table `scrape_queue` skippée : F§2 n'en définit pas, la pipe utilise `scrape_status` sur `organisations`
- Tests insert/update OK sur les deux tables avec nouveau token
**Sonnet-2 — Parsing biblio + géocodage seed :**
- 93 entités parsées (A-biblio annonçait 94, erreur de comptage source)
- 72 géocodées via Nominatim, 21 sans ville (lat/lon null, pas de fallback centre France pour éviter bruit carte)
- Correction manuelle Saint-Ouen (93) vs Saint-Ouen-l'Aumône (Picardie)
- 3 fiches réservées test pipe IA : CNOA, Archireport, Collectif Fil
**Sonnet-3 — Re-tag + CROA + import NocoDB :**
- Re-tag : 32 Prospection → Développement, 17 RH → Gestion d'agence
- Formation détecté et ajouté à 9 fiches (Réseau des MA, Cité de l'Archi, MAJ, CFAA, REFC'A, DU Dauphine, UNAID, FFP, Cité Archi Formation continue)
- 13 CROA régionaux : 6 créés + 7 existants enrichis (tous avec "Gestion d'agence")
- CNOA National isolé et réservé aux 3 fiches test pipe IA
- **96 fiches importées** dans NocoDB (99 v2 3 test pipe)
- Apostrophe typographique U+2019 appliquée pour compat NocoDB
- Anomalie : 8 anciens records V1 présents (IDs 1-8, `moderation_status null`) → **à purger avant mise en prod**
### Anomalies & points à surveiller
1. **Taxonomie `moderation_status` — divergence F/G**
- F§2 définit : `approved` / `rejected`
- G mentionne : `published` / `approved`
- Front V1 existant utilise : `approved`
- **Décision Session 2 :** rester sur `approved`/`rejected` partout, retirer `published` du prompt G si la doc est mise à jour. Le statut `pending` (avant modération) existe de fait comme absence de validation.
2. **Endpoint NocoDB 0.301.5**
- L'endpoint correct pour ajouter des colonnes est `/api/v1/db/meta/tables/{id}/columns`
- L'endpoint `/api/v2/meta/tables/{id}/fields` n'existe PAS dans cette version
- Le MCP `nocodb` n'était pas configuré avec la bonne URL de base → contournement direct par SSH + curl
- **TODO :** reconfigurer le MCP nocodb pour qu'il pointe correctement (utile pour sessions futures)
3. **Disque VPS : 72,4%** (était 69,8% avant install crawl4ai)
- crawl4ai a pris ~2,6 GB
- Hypothèse : Playwright + Chromium embarqué (crawl4ai utilise Playwright pour les pages JS)
- **Marge :** OK pour Sessions 2-3. À surveiller quand on commencera à scraper et stocker du contenu dans `scrape_content`
- **Alternative si problème disque :** remplacer crawl4ai par `httpx + BeautifulSoup` pour le scraping statique (bien plus léger, mais perd le rendu JS)
- **TODO :** vérifier via `du -sh` le poids exact de crawl4ai et ses deps Playwright, décider si on garde ou on allège
4. **Conflit lib Python `rich`**
- Résolu avec `pip install --ignore-installed`
- Risque : peut casser un autre outil Python VPS qui utilise `rich` (lightrag, formations, etc.)
- **TODO :** vérifier que lightrag et autres services Python tournent toujours
### Fichiers produits
- `/opt/nav-carte/.env` (VPS, chmod 600)
- Tables NocoDB : `organisations` étendue + `stats_usage` nouvelle
- `0 INBOX/NAV-V2-recherches/palette-nav-v2.md` — palette finale CSS
- `0 INBOX/NAV-V2-recherches/palettes-preview.html` — mockup validé
- `0 INBOX/NAV-V2-recherches/seed-94-fiches.json` *(à produire par Sonnet-2)*
- `0 INBOX/NAV-V2-recherches/seed-94-rapport.md` *(à produire par Sonnet-2)*
### Prochaines étapes
- Sonnet-2 finit le parsing + géocoding
- Import seed NocoDB (bypass modération, 2-3 fiches réservées test pipe IA)
- Rédaction des prompts Sessions 2 et 3 (livrable fin Session 1)
---
## Taxonomie finale NAV V2 (validée 2026-04-14)
**Échelle** (3 niveaux) : `National` / `Régional` (inclut Départemental) / `Local`
**Territoire** : `Métropole` + 5 DOM-TOM (Guadeloupe, Martinique, Guyane, La Réunion, Mayotte)
**Fonctions** (10 tags, multi 1-5, ordre = priorité) :
1. Juridique
2. Technique
3. Économique *(strictement finance : aides, fiscalité, rentabilité, tarification)*
4. Administratif *(démarches externes : permis, OA, déclarations)*
5. Chantier
6. Comptabilité
7. **Développement** *(ex-Prospection — AO, concours, réseaux pro, acquisition clients)*
8. **Formation** *(NOUVEAU — école, MOOC, organisme, formation continue)*
9. **Gestion d'agence** *(ex-RH — élargi : RH + management + pilotage + orga interne)*
10. Santé mentale
**Renommages appliqués :**
- Prospection → Développement
- RH → Gestion d'agence (élargi)
- Ajout : Formation
**Cas particuliers :**
- CNOA → éclaté en 1 fiche National (siège Paris) + 13 fiches CROA Régional (préfectures de région)
- Option 3 (antennes pins secondaires via champ multi-coords) → backlog V3
## Points ouverts (à trancher en amont des prochaines sessions)
- [ ] Session 2 : choix rate limit chatbot (fichier JSON vs Redis)
- [ ] Session 3 : seuil email modération (N fiches en attente → envoi)
- [ ] Facteur CO2eq : confirmer `0.052 kg CO2/kWh` (RTE FR) ou utiliser valeur plus récente (ADEME 2024 ≈ 0.055)
- [ ] Nominatim policy : user-agent `NAV-V2/1.0 (contact@trans-former.fr)` → OK ?
- [ ] UX Session 2 : options échelle vides (Local) + onglet Outre-mer vide → afficher avec compteur "0" pour inviter à contribuer (cohérent esprit collaboratif)
## Enrichissement DOM-TOM (post V2)
Session d'enrichissement à lancer après déploiement V2 :
- Recherche ciblée par territoire (Guadeloupe, Martinique, Guyane, La Réunion, Mayotte)
- Sources : CROA locaux, maisons de l'archi outre-mer, réseaux pro insulaires
- Agent Sonnet-research 1-2h → complément seed
## Piste externe : team.archi
Contacter le fondateur de team.archi (forum entraide archi) pour :
- Lui présenter NAV V2 (esprit, périmètre)
- Demander si team.archi documente d'autres réseaux d'entraide qu'on pourrait intégrer
- Proposer un échange / listing mutuel
Brouillon email à préparer (requiert nom + email fondateur de Jules).
---
---
## 2026-04-14 — Session 2 : Front — Carte + Sidebar Filtres
**Exécutant :** Sonnet (agent)
**Durée :** ~1h
### Décisions prises
| Décision | Détail |
|----------|--------|
| Leaflet sans plugin Nuxt | `@nuxtjs/leaflet` a une compat instable avec Nuxt 3.15 — import dynamique direct dans `onMounted()` + `<ClientOnly>` wrapper. Plus fiable, zéro config SSR à gérer. |
| Cluster seuil 15 (spec dit 15+) | `disableClusteringAtZoom: 14` — au-delà de zoom 14 les pins se défiltrent, cohérent avec la granularité ville/rue |
| `@headlessui/vue` installé mais non utilisé pour le drawer | Le drawer implémenté en Vue natif (Teleport + transition) est plus léger et sans dépendance. `@headlessui/vue` reste disponible pour l'étape 3 (modales commentaires). |
| Seed JSON : Id fictif 1000+ | Pour éviter collision avec les IDs NocoDB réels (1-96+). Les URLs `/fiche/1000+` ne seront pas résolues en prod — comportement attendu en dev. |
| `moderation_status=approved` | Confirmé — retirer `published` du filtre comme résolu en Session 1 |
| Compteurs calculés sur orgs non filtrées | Les (0) montrent les options sans aucune fiche dans la base totale, pas dans la sélection courante. Plus honnête vis-à-vis de l'invitation à contribuer. |
### Composants créés
- `NavMap.vue` — Leaflet + OSM + clusters + pins personnalisés
- `EchelleFilter.vue` — chips exclusifs National/Régional/Local
- `FonctionFilter.vue` — multi-sélect 10 fonctions (max 5)
- `TerritoireToggle.vue` — Métropole + 5 DOM-TOM sous-onglets
- `NavSidebar.vue` — wrapper desktop
- `FilterDrawer.vue` — drawer mobile (Teleport + transition Vue native)
### Fichiers modifiés
- `assets/css/main.css` — tokens CSS palette NAV V2 + surcharges Leaflet/clusters
- `tailwind.config.js` — couleurs `nav.*` ajoutées
- `nuxt.config.ts` — optimizeDeps Leaflet pour Vite
- `server/routes/api/organisations.get.ts` — V2 champs + fallback seed JSON
- `pages/index.vue` — refonte complète layout + filtres + URL sync
### Commits Session 2
- `64f5b0a` — feat(deps): installer leaflet, markercluster, headless UI
- `dc849ef` — feat(style): palette NAV V2 — tokens CSS + tailwind
- `799d8fc` — feat(api): GET /api/organisations V2 avec fallback seed JSON
- `450a45c` — feat(components): carte Leaflet + sidebar filtres (étape 2)
- `3f486df` — feat(page): index.vue V2 — carte + sidebar + URL sync
### Validation build
- `npm run build` : ✓ sans erreur, 2.09 MB bundle
### Notes pour Sonnet 2 (étape 3 — fiche détail)
- `@headlessui/vue` installé et disponible si besoin pour la modale commentaires
- Interface `Org` définie dans `pages/index.vue` — à extraire dans `types/org.ts` si partagée (étape 3 en aura besoin)
- Route API existante : `GET /api/organisations/[id]` dans `server/routes/api/organisations/[id].get.ts` — fonctionne, utiliser tel quel
- Champ `description_enrichie` prévu mais absent du seed — prévoir fallback sur `description`
- Champ `points_cles` : chaîne ou JSON array ? Vérifier dans NocoDB avant d'afficher
### Notes pour Sonnet 3 (étape 6 — formulaire contribuer)
- Route `POST /api/organisations` existe déjà en V1 (`server/routes/api/organisations.post.ts`) — à adapter pour V2 (nouveaux champs + moderation_status: pending)
- Zod non encore installé — à ajouter dans package.json
- Rate limit : trancher JSON simple (recommandé pour MVP) vs Redis
### Points ouverts
- [ ] 8 anciens records V1 (IDs 1-8, moderation_status null) à purger en prod
- [ ] Vérifier type de `points_cles` dans NocoDB avant rendu fiche (string ou JSON)
- [ ] Tester carte sur VPS avec données NocoDB réelles (96 fiches)
---
---
## 2026-04-14 — Session 2 (suite) : Spec définitive — Top nav + chatbot + filtres
**Exécutant :** Sonnet 2/3
**Durée :** ~1h
### Décisions prises
| Décision | Détail |
|----------|--------|
| `app.vue` = layout global | Refondu avec top nav (logo + TopSearchBar + Contribuer + Aléatoire). Supprime le header V1 (vert sage). NuxtPage prend le flex-1 restant. |
| Recherche top nav → URL `?q=` | TopSearchBar émet vers app.vue → router.replace ?q=. pages/index.vue watch route.query.q. Pas de prop passée via NuxtPage (non supporté simplement). |
| Sidebar sans recherche | Input recherche retiré de NavSidebar — redondant avec top nav. |
| Sidebar sans chatbot | Zone chatbot déplacée dans la zone carte (bas). |
| ChatbotPlaceholder sous carte | Input désactivé 52px, fond `--nav-bg`. Présent sous Métropole ET Outre-mer. |
| Échelle multi-select (string[]) | EchelleFilter + NavSidebar + FilterDrawer + pages/index.vue tous adaptés en `string[]`. Plus de `string \| null`. |
| Fiche aléatoire | ?random=1 dans l'URL → pages/index.vue redirige vers /fiche/[id] aléatoire. |
| shadcn | Écarté — Vue+Tailwind+CSS scoped suffit, zéro complexité d'intégration. |
| Bonus "ouvrir chatbot" | Skippé — pas de valeur avant S3. |
### Composants créés
- `TopSearchBar.vue` — barre recherche animée (compact 44px → 280px au focus), CSS scoped
- `ChatbotPlaceholder.vue` — zone bas carte, input désactivé, réserve espace S3
### Composants modifiés
- `app.vue` — refonte complète layout global (V1 sage vert → V2 bleu nuit)
- `pages/index.vue` — supprime header dupliqué, intègre ChatbotPlaceholder, échelle string[]
- `EchelleFilter.vue` — single-select → multi-select (string[])
- `NavSidebar.vue` — supprime input recherche + zone chatbot, prop echelle string[]
- `FilterDrawer.vue` — prop echelle string[], activeCount adapté
### Commits Session 2 (suite)
- `33fbd3b` — feat(nav): top nav global avec barre de recherche animée
- `1d5d9ab` — feat(ux): chatbot placeholder sous la carte + sidebar sans chatbot
- `905f338` — feat(filtres): échelle multi-select + sidebar sans recherche ni chatbot
### Validation build
- `npm run build` : ✓ sans erreur, 2.12 MB bundle
### Points en attente / non implémentés
- [ ] TerritoireTabs dans le drawer mobile — non dupliqué (à faire si besoin)
- [ ] Fiche aléatoire nécessite que les orgs soient chargées — si orgs vides au moment du ?random=1, pas de redirect. Acceptable pour MVP.
---
---
## 2026-04-14 — Session 2 (Sonnet 2) : Fiche détail + Commentaires
**Exécutant :** Sonnet 2 (agent NAV V2)
**Durée :** ~1h
### Décisions prises
| Décision | Détail |
|----------|--------|
| types/org.ts | Interface Org canonique extraite — suppression des 2 déclarations dupliquées dans index.vue et fiche/[id].vue |
| Route API dédiée | GET /api/fiche/[id] dans server/api/fiche/ (distinct de server/routes/api/organisations/[id]) — meilleure séparation des responsabilités |
| Table commentaires | commentTableId = COMMENT_TABLE_ID ?? AVIS_TABLE_ID (fallback table V1 si pas de table dédiée configurée) |
| Mistral Nemo timeout | 2s via AbortController — fallback safe_check: 'pending' si timeout ou clé absente |
| Retour filtres | sessionStorage nav_back_filters (pas de query param _back) — plus simple, pas de fuite dans URL partagée |
| mini-carte | Leaflet non-interactif dans FicheDetail — zoom:10, dragging:false, toutes interactions désactivées |
| points_cles | Tente JSON.parse d'abord, fallback découpage par lignes si format texte |
| Rate limit Redis | Posé par Sonnet 3 — server/utils/rateLimit.ts avec fallback mémoire si Redis KO |
### Composants créés
- `components/FicheDetail.vue` — affichage complet avec mini-carte Leaflet
- `components/CommentSection.vue` — liste commentaires publiés + refresh prop
- `components/CommentForm.vue` — formulaire soumission + gestion 429
### Routes API créées
- `server/api/fiche/[id].get.ts` — proxy NocoDB champs V2 complets
- `server/api/comment/index.post.ts` — filtre éthique Mistral Nemo (timeout 2s, fallback pending)
- `server/api/comment/[orgId].get.ts` — commentaires publiés triés chronologiquement
### Fichiers modifiés
- `pages/fiche/[id].vue` — refonte complète SSR (FicheDetail + CommentSection + CommentForm + SEO)
- `pages/index.vue` — ajout storeFiltersForBack() pour sessionStorage + import type Org
- `types/org.ts` — CRÉÉ : interface canonique V2
- `nuxt.config.ts` — ajout mistralApiKey + commentTableId
### Commits Session 2 (Sonnet 2)
- `a653336` — refactor(types): extraire interface Org canonique dans types/org.ts
- `89bd22a` — feat(api): GET /api/fiche/[id] + POST/GET /api/comment avec filtre Mistral Nemo
- `06c44cd` — feat(components): FicheDetail + CommentSection + CommentForm
- `420f534` — feat(page): /fiche/[id] SSR complète — FicheDetail + comments + SEO + retour filtres
### Validation build
- `npm run build` : ✓ sans erreur, 2.74 MB bundle
### Coordination Sonnet 3 (rate limit Redis)
**Posé par Sonnet 3 :** `server/utils/rateLimit.ts` (ioredis + fallback mémoire) importé dans POST /api/comment.
Sonnet 3 a modifié `server/api/comment/index.post.ts` pour brancher `checkRateLimit(ip, 'comment', 5)`.
Configuration : `REDIS_URL` dans `.env`, fallback mémoire si Redis KO (dev sans Docker).
### Points ouverts pour Session 3
- [ ] `public/og-default.png` à créer (logo par défaut pour og:image) — actuellement référencé, pas encore créé
- [ ] `COMMENT_TABLE_ID` à ajouter dans `.env` VPS si table dédiée à part de `AVIS_TABLE_ID`
- [ ] `MISTRAL_API_KEY` à vérifier dans `/opt/nav-carte/.env` — la clé est dans le .env VPS (Session 1), juste s'assurer du nom de variable exact
- [ ] Vérifier type de `points_cles` dans NocoDB (JSON array stringifié ou texte brut ?)
- [ ] Tester sur VPS avec données NocoDB réelles (96 fiches seed)
---
## 2026-04-14 — Session 2 (parallèle) : Étape 6 — Formulaire `/contribuer` + Redis rate limit
**Exécutant :** Sonnet 3 (agent)
### Redis VPS
- `redis-server` absent au départ (pas mentionné dans VPS-check.md)
- Installé : `apt-get install redis-server``systemctl enable + start`
- Bind : `127.0.0.1 -::1` uniquement (localhost, sécurisé)
- Test : `redis-cli ping` → PONG ✓
- **En prod :** ajouter `REDIS_URL=redis://127.0.0.1:6379` dans `/opt/nav-carte/.env`
- **En dev local :** fallback compteur en mémoire (ioredis lazyConnect — pas de crash si Redis absent)
### Fichiers produits
| Fichier | Description |
|---------|-------------|
| `server/utils/rateLimit.ts` | Helper Redis générique — `checkRateLimit(ip, action, maxPerDay)` avec fallback mémoire |
| `server/api/submit/index.post.ts` | POST /api/submit — Zod + Nominatim + NocoDB pending + rate limit 3/IP/j |
| `server/api/comment/index.post.ts` | Rate limit Redis 5/IP/j ajouté en tête (base Sonnet 2) |
| `pages/contribuer.vue` | Page dédiée — 8 champs, validation Zod client+serveur, 422/429, succès + trackingUrl |
### Décisions
| Décision | Détail |
|----------|--------|
| Page vs modal | Page dédiée `/contribuer` — plus simple pour MVP. Route duale desktop (redirect ?contribute=1 + modale) en backlog V2.1 |
| description_user | K-prompt (50-500) > E-spec (20-200) — tranché : 50-500 chars |
| Champ ville | Optionnel (K-prompt) — moins de friction, fallback fiche sans coords prévu |
| NOCODB_BASE_ID | Valeur fallback `p_nav_v2` dans le code. À confirmer : `ssh vps-hetzner "grep -i base /opt/nav-carte/.env"` |
### Rate limits posés (Redis)
- `/api/submit` : **3 soumissions / IP / jour**
- `/api/comment` : **5 commentaires / IP / jour**
### Commits
- `d9b6a31` — feat(deps): ajouter zod + ioredis + REDIS_URL runtime config
- `e81625f` — feat(server): helper rate limit Redis avec fallback mémoire
- `5c24c06` — feat(api): POST /api/submit — validation Zod + geocoding + NocoDB pending
- `fc0c52c` — feat(page): /contribuer — formulaire V2 Zod client + UX mobile-first
- (rate limit /api/comment dans commit Sonnet 2 `420f534`)
### Build
- `npm run build` : ✓ sans erreur, 2.74 MB bundle
### TODO avant prod
- [ ] `REDIS_URL=redis://127.0.0.1:6379``/opt/nav-carte/.env` (VPS)
- [ ] Confirmer `NOCODB_BASE_ID` : `ssh vps-hetzner "grep -i base /opt/nav-carte/.env"`
- [ ] Tester submit valide → fiche `pending` dans NocoDB
- [ ] Tester 4ème submit → vérifier HTTP 429
---
## 2026-04-14 — Session 2 (Étape 2) : Mobile UX
**Exécutant :** Sonnet 1.8 (agent NAV V2 Mobile)
**Durée :** ~1h
### Décisions prises
| Décision | Détail |
|----------|--------|
| FilterDrawer supprimé | L'ancien drawer filtres (bouton flottant gauche, slide depuis gauche) est supprimé. Les filtres sont désormais inline dans le flow mobile. |
| ChatbotSheet nouveau composant | `ChatbotSheet.vue` — bottom sheet plein écran, Teleport + transition slide-up, `92dvh`, input désactivé (S3), fermeture backdrop + bouton Retour. |
| Carte mobile 45dvh | `height: 45dvh` sur mobile, `min-height: 180px`. `dvh` pour éviter le bug clavier mobile. |
| Tagging compact inline | Bandeau entre carte et liste : échelle 3 checkboxes `16px` avec labels courts (Nat/Rég/Loc) + fonctions scroll horizontal `overflow-x: auto`. |
| Tap card → centre carte | `onSelectOrgMobile``selectedId` change → `NavMap.vue` réagit via `watch selectedId → mapInstance.panTo`. |
| Bouton chatbot flottant | `56×56px`, `bottom: 1.5rem; right: 1rem`, `opacity: 0.88`, `z-[1000]`, `lg:hidden`. |
| Multi-Leaflet Outre-mer | Grille 5 mini-cartes existante conservée — build clean, à tester en conditions réelles. |
| Poignée draggable | Skippé — estimé > 30 min, noté pour Session 3. |
| Interface Org | Sonnet 2 a extrait `types/org.ts`, linter auto-appliqué l'import dans `pages/index.vue`. |
### Composants créés
- `ChatbotSheet.vue` — bottom sheet chatbot plein écran (92dvh, slide-up, backdrop, poignée visuelle, input désactivé)
### Composants supprimés
- `FilterDrawer.vue` — drawer filtres mobile (remplacé par tagging inline + ChatbotSheet)
### Fichiers modifiés
- `pages/index.vue` — refonte complète zone mobile + bouton chatbot flottant + ChatbotSheet
### Commits Session 2 (Mobile)
- `d39e7be` — feat(mobile): ChatbotSheet — bottom sheet plein écran avec animation slide-up
- `0843301` — feat(mobile): supprimer FilterDrawer — remplacé par tagging inline + ChatbotSheet
- `pages/index.vue` — inclus dans refacto Sonnet 2 (`a653336` → import `types/org.ts`)
### Validation build
- `npm run build` : ✓ sans erreur, 2.12 MB bundle
### Backlog mobile S3
- [ ] Poignée draggable entre carte et liste (drag resize)
- [ ] Multi-Leaflet Outre-mer mobile à tester en conditions réelles → fallback dropdown si trop lourd
- [ ] Animation léger zoom pin au tap card (spec F)
- [ ] Chatbot IA branché (Session 3)
---
---
## 2026-04-14 — Session S2 Ajustements finaux v2 (correctif branding + polish)
**Exécutant :** Sonnet 1.9b (agent AEP V2)
### Contexte
L'agent précédent avait commis une erreur de spec sur le renommage : "NAV" avait été remplacé par "Écosystème Architecture" dans les textes visibles. La bonne règle est : "NAV" (texte visible) → "AEP" (Architecture d'Écologie Politique).
### Corrections branding (commit fix)
| Fichier | Avant | Après |
|---------|-------|-------|
| `app.vue` logo icône | `N` | `A` |
| `app.vue` logo texte | `Écosystème Archi.` | `AEP` |
| `components/ChatbotSheet.vue` | `aria-label="Assistant Écosystème Architecture"` | `aria-label="Assistant AEP"` |
| `pages/index.vue` SEO title | `Écosystème Architecture — Cartographie AEP` | `AEP — Cartographie de l'écologie politique architecturale` |
| `pages/contribuer.vue` SEO title | `Proposer une ressource — Écosystème Architecture` | `Proposer une ressource — AEP` |
| `pages/ajouter.vue` SEO title | `Proposer une fiche — Écosystème Architecture` | `Proposer une fiche — AEP` |
| `pages/fiche/[id].vue` og:description fallback | `Fiche organisation — NAV Architectes` | `Fiche organisation — AEP` |
| `pages/fiche/[id].vue` SEO title | `{nom} — NAV` | `{nom} — AEP` |
| `pages/fiche/[id].vue` og:title | `{nom} — NAV Architectes` | `{nom} — AEP` |
### Ajustements complémentaires
- Bouton flottant mobile : icône seule → pill avec texte "Chatbot" visible (48px h, gap-2, font-weight 600)
### État des 4 ajustements spec
| # | Ajustement | État |
|---|-----------|------|
| 1 | Échelle inline 1 ligne | ✓ Fait par agent précédent (`EchelleFilter.vue` `flex-wrap gap-x-4`) |
| 2 | NAV → AEP (textes visibles) | ✓ Corrigé dans ce commit |
| 3 | Mobile search au-dessus liste | ✓ Fait par agent précédent (lignes 167-203 `pages/index.vue`) |
| 4 | Bouton mobile "Chatbot" | ✓ Fait dans ce commit (pill avec label visible) |
### Build
- `npm run build` : ✓ sans erreur, 2.74 MB bundle
---
## 2026-04-14 — Session 3 : Chatbot IA (Étape 5bis)
**Pilote :** Opus
**Exécutant :** agent Sonnet S3
**Durée :** ~1h
### Réalisé
**ChatbotSheet.vue (mobile) — refactorisé :**
- Conversation IA complète : messages user/assistant, scroll auto, loading dots
- Message onboarding exact (texte E §6) affiché avant la première question
- Bulles fiches recommandées : card compacte avec lien `/fiche/[id]` + explication
- Erreurs typées : 429 (rate limit), 503 (budget épuisé), fallback générique
- Emit `highlightOrgs` pour highlight carte
- Animations slide-up + `prefers-reduced-motion` respecté
**ChatbotPlaceholder.vue (desktop) — refactorisé :**
- Même conversation IA que le mobile, format panneau latéral expand/collapse
- Même onboarding, mêmes bulles fiches, même gestion d'erreurs
- Input en bas du panneau étendu, Enter pour envoyer
**server/api/chatbot/index.post.ts — créé :**
- Rate limit : `checkRateLimit(ip, 'chatbot', 10)` via helper Redis S2 existant
- Circuit breaker : lecture cumul `stats_usage` mois courant, 503 si ≥ 20€
- Fetch top-20 fiches NocoDB (`moderation_status=approved`) avec scoring keyword
- Contexte JSON compact injecté dans le prompt système Mistral Small
- Appel `mistral-small-latest` (temperature: 0.3, max_tokens: 600, json_object)
- Parse JSON → `{ reponse_texte, fiches_recommandees }`
- Log `stats_usage` (fire and forget)
### Décisions
| Décision | Détail |
|----------|--------|
| Rate limit helper | Réutilisation du helper Redis S2 (`checkRateLimit`) — plus robuste que JSON fichier /tmp. Comportement identique (10 req/j). |
| `STATS_TABLE_ID` env | Valeur par défaut `mbbq7n47ixy19mc` (créé en S1) — override via env si besoin |
| Scoring fiches | Keyword match sur nom + description + tags — suffisant pour MVP, pas de vector search |
| `prefers-reduced-motion` | Animations supprimées si l'utilisateur a activé ce préfé navigateur |
### Build
- `npm run build` : ✓ sans erreur, 2.78 MB bundle
### Commits Session 3 (étape 5bis)
- feat(chatbot): ChatbotSheet mobile + ChatbotPlaceholder desktop — conversation IA branchée
- feat(api): POST /api/chatbot — Mistral Small + rate limit + circuit breaker
### TODO avant prod
- [ ] Vérifier `STATS_TABLE_ID` dans `/opt/nav-carte/.env` (valeur par défaut : `mbbq7n47ixy19mc`)
- [ ] Tester 2-3 requêtes réelles sur VPS avec fiches NocoDB
- [ ] Vérifier index.vue passe bien `highlightOrgs` au NavMap depuis ChatbotSheet et ChatbotPlaceholder
- [ ] Tester mobile iOS Safari + Android Chrome
---
---
## 2026-04-14 — Session 3 (Étapes 4-5) : Worker IA + Test pipeline
**Exécutant :** Sonnet (agent)
**Durée :** ~2h
### Réalisé
**Étape 4 — Worker enrichissement IA :**
- `worker/enrich.js` créé (Node.js ESM, systemd timer 5 min)
- Pipeline complet : fetch pending → scrape crawl4ai → Mistral Nemo → update NocoDB → log stats_usage
- Circuit breaker budget 20€ (filtre JS, car NocoDB v0.301.5 rejette les filtres datetime)
- Email Jules via Resend si seuil 5 fiches pending en modération
- Lock anti-overlap via `/tmp/nav-worker.lock`
- Retry 2x sur erreur Mistral + flag `ai_error` si 3 échecs
**Infrastructure :**
- `worker/package.json` + `node_modules/dotenv` installés
- Variables RESEND_API_KEY, EMAIL_JULES, BUDGET_MAX_EUR, WORKER_LIMIT ajoutées au `.env`
- `nav-worker.service` + `nav-worker.timer` systemd créés et activés
**Étape 5 — Test pipeline sur 3 fiches :**
- 3 fiches injectées en NocoDB (CNOA Id=106, Archireport Id=107, Collectif Fil Id=108)
- Checkpoint 1 fiche (CNOA) : €0.000029, extrapolé 96 fiches = €0.0028 → GO
- 3 fiches enrichies avec succès
### Métriques pipeline
| Fiche | tokens_in | tokens_out | cout_eur | Temps | Confiance |
|-------|-----------|------------|----------|-------|-----------|
| CNOA | 1 248 | 167 | €0.000029 | 3.0s | haute |
| Archireport | 4 376 | 261 | €0.000091 | 3.9s | haute |
| Collectif Fil | 2 628 | 221 | €0.000057 | 4.3s | haute |
| **Total** | **8 252** | **649** | **€0.000177** | **11.2s** | — |
Extrapolation 96 fiches : **€0.0057** (budget 20€ = 3 500× la consommation réelle)
### Problèmes résolus
| Problème | Solution |
|---------|---------|
| Playwright absent → crawl4ai crash | `AsyncHTTPCrawlerStrategy` (mode statique) |
| NocoDB rejette filtres datetime | Filtre JS sur tous les records stats_usage |
| Apostrophe U+0027 dans tags refusée | U+2019 (typographique) partout dans VALID_FONCTIONS |
| import dynamique ESM incompatible | spawnSync importé statiquement |
### Décisions prises en autonomie
| Décision | Raison |
|---------|--------|
| `AsyncHTTPCrawlerStrategy` au lieu d'`AsyncWebCrawler` standard | Playwright non installé sur VPS, mode statique suffisant pour sites archi |
| Filtre budget JS (pas NocoDB) | API NocoDB 0.301.5 ne supporte pas les filtres de date en mode gte |
| `spawnSync` pour appel Python | Évite les complications d'import dynamique en ESM |
### Fichiers produits
- `/opt/nav-carte/worker/enrich.js` — Worker IA principal
- `/opt/nav-carte/worker/package.json` — Dépendances (dotenv)
- `/etc/systemd/system/nav-worker.service` — Service oneshot
- `/etc/systemd/system/nav-worker.timer` — Timer 5 min
- `nav-carte/PIPE-IA-DOC.md` — Documentation complète pipeline + résultats
### TODO avant deploy prod (Étape 9)
- [ ] Chatbot (Étape 5bis) — non implémenté dans cette session
- [ ] Bandeau bas + Liberapay (Étape 7) — non implémenté
- [ ] Page /a-propos (Étape 8) — non implémenté
- [ ] Deploy git sur VPS + build Nuxt production
- [ ] Purger 8 anciens records V1 (IDs 1-8, moderation_status null) avant prod
- [ ] Tester worker sur soumission réelle via /contribuer
- [ ] Vérifier que les 3 fiches test (Ids 106-108) sont modérées ou purgées avant prod (sont en mode test)
---
---
## Session 3b — Deploy final AEP (2026-04-14 soir)
**Durée :** ~3h · **Résultat :** AEP V2 live sur `aep.trans-former.fr` + `nav.trans-former.fr` (alias)
### Dispatch
| # | Action | Modèle | Status |
|---|--------|--------|--------|
| A | Prompt système chatbot Mistral Small | Opus + Jules | ✓ |
| B | Composant `BandeauBas.vue` + endpoint `/api/stats` | Sonnet | ✓ |
| C | Page `/a-propos` (brouillon — texte à réécrire par Jules) | Sonnet | ✓ |
| D | Cron purge `/tmp/nav-ratelimit/` (systemd timer quotidien 03:00 UTC) | Sonnet | ✓ |
| E | Enrichissement rétro 96 fiches (worker IA Mistral Nemo) | Sonnet | ✓ |
| F | DNS OVH + Caddy alias + build + deploy + tests HTTPS | Opus + Jules | ✓ |
### Décisions tranchées
| Décision | Choix | Raison |
|----------|-------|--------|
| Domaine V2 | `aep.trans-former.fr` nouveau + alias `nav.*` | Pas de V1 figée réelle à préserver : le VPS servait déjà la V2 |
| Structure Caddy | Alias simple (2 domaines → même service `:3333`) | Éviter 2 instances Node pour un code identique |
| Prompt chatbot | "AEP — Écosystème Entraide", posture engagée, règle hors-scope | Cohérent avec positionnement AEP vs terminologie NAV résiduelle |
| Rate limit chatbot | JSON SHA-256 (RGPD) — Redis retiré | Volume borné par circuit breaker, pas besoin de Redis |
| Enrichissement rétro | Bulk patch `pending` → worker → bulk approve API | Spot-check qualité accepté, tags à corriger en modération manuelle |
### Commits
```
74c9722 feat(aep-s3b): bandeau transparence + /a-propos + cron purge rate-limit
68e1e53 fix(chatbot): purge version Redis + prompt système AEP
```
### Enrichissement IA — bilan qualité
- **Coût réel** : €0.006 pour 96 fiches (×3 300 sous le seuil €20/mois)
- **Descriptions** : factuellement correctes (spot-check 10 fiches OK)
- **Tags** : 6/10 vides sur Maisons de l'Architecture (mapping ne matche pas "expositions/conférences"), 1 sur-tagging MAOP Id 11 (10/10 tags — site généraliste scraped), MAF Id 15 manque "Juridique"
- **Action Jules** : correction manuelle tags dans NocoDB UI (non bloquant pour deploy)
### Découverte imprévue — bug `deploy.sh`
Incohérence entre `deploy.sh` (rsync vers `/opt/nav-carte/`) et `nav-carte.service` (exec `/opt/nav-carte/.output/server/index.mjs`). Le `deploy.sh` écrit au mauvais endroit — le service tourne sur `.output/` qui n'est jamais mis à jour par le script. Contournement cette session : tar + ssh extract manuel dans `.output/`. **À patcher en V3** : aligner `deploy.sh` sur la structure `.output/`.
### État à la clôture
```
✓ aep.trans-former.fr — HTTPS 200 (/, /a-propos, /api/stats)
✓ nav.trans-former.fr — alias OK
✓ 99 fiches approved (96 enrichies + 3 S3a)
✓ Bandeau bas + chatbot + lien À propos intégrés
✓ Cron purge rate-limit actif (prochaine exécution 2026-04-15 03:00 UTC)
✓ V1 Redis chatbot purgée
```
### TODO post-session (Jules, async)
- [ ] Réécrire le texte de `pages/a-propos.vue` (placeholders `<!-- TODO Jules -->` en tête de chaque section)
- [ ] Corriger tags Maisons de l'Architecture + MAOP + MAF dans NocoDB UI
- [ ] Tests manuels : submit `/contribuer`, chatbot mobile (iOS + Android), feedback UX
- [ ] Patcher `deploy.sh` pour cibler `/opt/nav-carte/.output/` (voir découverte ci-dessus)
- [ ] Vérifier widget Liberapay à `liberapay.com/trans-former.fr` — compte existe ?
---
## Backlog V3 (hors scope V2)
### Infra / deploy
- **Fix `deploy.sh`** — cible `/opt/nav-carte/.output/` pas `/opt/nav-carte/` (incohérence découverte en S3b)
- **Migration `/opt/nav-carte/``/opt/aep/`** — alignement nom projet (dette terminologique NAV)
- **Rebrand service systemd** `nav-carte.service``aep.service` (low-priority)
- **Monitoring** — sonde Uptime Kuma (`status.trans-former.fr`) : ajouter `aep.*/api/stats`
- **Backup NocoDB** — dump quotidien base `pipilvsi7dibo80` vers stockage externe
### Produit / UX
- **Tags auto** — améliorer le mapping worker pour matcher "expositions/conférences/culture" → Développement + Formation (évite tags vides sur MA)
- **Modération UI custom** — interface dédiée pour batch approve/reject (au lieu de cliquer 96 rows)
- **Sidebar "Fiches récemment ajoutées"** — boost engagement
- **Recherche sémantique chatbot** — passer de keyword match à embeddings (Mistral Embed, volume à estimer)
- **Page `/transparence` publique** — détail coûts, CO2, donateurs Liberapay
### Éditorial
- **Contenu `/a-propos`** — texte politique écrit par Jules seul (hors scope IA)
- **Badge IA souveraine** — vérifier formulation (Hetzner = Falkenstein DE, pas FR — dire "Hébergé en Europe" ?)
- **Page Contribuer — modération visible** — expliquer pipeline "formulaire → worker IA → modération humaine"
### Monétisation / association
- **Bascule Liberapay → HelloAsso** quand ASO créée (scénario 2 du doc `C-systeme-dons.md`)
- **Widget Liberapay "total collecté"** — actuellement juste CTA, ajouter feedback
### Technique
- **Caching API organisations** — actuellement re-fetch NocoDB à chaque render
- **Full-text search côté client** — Fuse.js sur descriptions enrichies
- **Mode offline / PWA** — manifest + service worker pour usage terrain