# CLAUDE.md — Système de veille automatisée multi-thèmes ## Section 1 : Vue d'ensemble de l'architecture Ce projet est un système de veille IA modulaire. Un daemon Node.js tourne en continu, consulte `registry.json` pour savoir quelles veilles lancer, appelle Claude CLI avec WebSearch, persiste les résultats en JSON, et synchonise via FTP vers OVH. Le PHP lit les JSON et génère le HTML à la volée. **Veilles actives :** crypto (6h), epstein (7j), techno (7j), retro (7j) **Fichiers clés :** - `scripts/run-veille.js` — runner générique (~450 lignes), remplace tous les anciens *-veille.js - `scripts/veille-daemon.js` — daemon polling + FTP sync - `uploads/veille/registry.json` — configuration centrale de toutes les veilles - `veille//index.php` — 3 lignes, include `_veille-page.php` - `veille//_article.php` — template PHP pour l'article structuré - `uploads/veille//updates.json` — flux de news brutes (ftp_files[0]) - `uploads/veille//article.json` — document structuré enrichi chaque cycle (ftp_files[1]) ## Section 2 : registry.json — structure complète ```json { "veilles": { "": { "slug": "retro", "label": "Consoles Rétro", "icon": "🕹️", "color": "#22c55e", "frequency_hours": 168, "prune_days": 180, "script": "generic", "categories": ["NOUVELLE_CONSOLE", "FIRMWARE", "BON_PLAN"], "ftp_files": [ { "local": "uploads/veille/retro/updates.json", "remote": "/www/uploads/veille/retro/updates.json" }, { "local": "uploads/veille/retro/article.json", "remote": "/www/uploads/veille/retro/article.json" } ], "prompt": "...", "render_prompt": "...", "summary_render_prompt": "", "summary_weekly_day": 0, "summary_period_days": 7, "created_at": "2026-03-24T00:00:00Z" } }, "updated_at": "2026-03-24T21:37:31+01:00" } ``` **Règle critique :** Toujours bumper `updated_at` à `new Date().toISOString()` avant de déployer `registry.json`. Le daemon compare `updated_at` local vs OVH — si OVH >= local, il écrase le local. ## Section 3 : Pattern deux passes Claude **Passe 1 — fetch (avec WebSearch) :** - Claude appelle WebSearch/WebFetch pour collecter les infos - Retourne `{ items: [...], ...topLevelFields }` en JSON - `items[]` = flux de news → merge dans `updates.json` - `topLevelFields` (ex: `current_prices`) → passés à la passe 2 **Passe 2 — render (sans WebSearch) :** - Reçoit : `render_prompt` + données fraîches du cycle + `article.json` actuel en contexte - Retourne un **patch JSON partiel** (uniquement les champs modifiés) - Le runner fusionne le patch dans `article.json` avec règles spéciales : - `ARTICLE_PREPEND_FIELDS` (`timeline`, `evidence`) : nouveaux items prépendés (max 50) - `persons_update[]` : merge par nom - `france_section{}` : shallow merge - Tous les autres champs : remplacement direct **La passe render n'a PAS accès à WebSearch.** Elle travaille uniquement avec les données passées en contexte. ## Section 4 : render_prompt — règles de rédaction ``` Tu mets à jour le guide JSON. Tu reçois : - NOUVELLES INFORMATIONS : données du cycle (items[], current_prices, etc.) - DONNÉES ACTUELLES : article.json existant (contexte) Retourne UNIQUEMENT un JSON "patch" avec les champs modifiés. RÈGLE ABSOLUE [champ critique] : Si [condition], tu DOIS OBLIGATOIREMENT inclure [champ] dans le patch. [Raison explicite — Claude optimise et peut omettre si pas contraint] Champs disponibles à modifier : - "market_note" (string) - "highlights" (array) — MAX 5 items. Format : [{"category":"...","date":"YYYY-MM-DD","title":"...","summary":"..."}] - "consoles" (array) — TOUJOURS envoyer le tableau COMPLET si tu modifies quoi que ce soit Schema : {"rank":1,"name":"...","price":"~XX€",...} - Jamais de markdown, jamais de texte hors du JSON ``` **Règles d'or pour le render_prompt :** 1. Demander un patch (champs modifiés uniquement), pas un remplacement complet 2. Lister explicitement les champs disponibles avec leur schema 3. Si un tableau peut grossir, préciser "envoyer le tableau COMPLET si tu modifies quoi que ce soit" 4. Pour les champs critiques, ajouter une RÈGLE ABSOLUE explicite 5. Terminer par "Jamais de markdown, jamais de texte hors du JSON" ## Section 5 : Pièges connus et bugs historiques ### Bug 1 — Secondary files loop écrase article.json La boucle sur `ftp_files[1+]` dans `run-veille.js` écrit les top-level fields du cycle AVANT que `renderArticle()` ne tourne. Si `article.json` est en `ftp_files[1]`, il est écrasé avec les données partielles du cycle. **Fix appliqué (ligne 220 de run-veille.js) :** ```js if (f.local.endsWith('/article.json')) continue; // géré par renderArticle(), pas ici ``` **Ne jamais retirer cette ligne.** ### Bug 2 — Daemon réécrase registry.json modifié localement Le daemon compare `updated_at` local vs OVH. Si OVH >= local, il écrase le local. **Fix : toujours bumper `updated_at` avant deploy.** ### Bug 3 — render_prompt n'applique pas les champs critiques Claude optimise son output et peut omettre des champs "inchangés" même si leur contenu doit être mis à jour. **Fix : ajouter une RÈGLE ABSOLUE explicite dans le render_prompt pour chaque champ critique.** ### Bug 4 — URLs d'images inventées Le render_prompt n'a pas WebSearch. Si on lui demande de "trouver une URL image", il invente des URLs plausibles qui retournent 404. **Fix : collecter les URLs d'images lors de la passe 1 (WebSearch), ou les injecter manuellement après vérification `curl -I`.** ## Section 6 : FTP deploy snippet ```javascript // node --input-type=module import { readFileSync } from 'fs'; import { Client } from 'basic-ftp'; const envContent = readFileSync('scripts/.veille-ftp.env', 'utf8'); const env = {}; for (const line of envContent.split('\n')) { const match = line.match(/^(\w+)=(.*)$/); if (match) env[match[1]] = match[2].trim(); } const client = new Client(); await client.access({ host: env.FTP_HOST, user: env.FTP_USER, password: env.FTP_PASS, secure: 'explicit', secureOptions: { rejectUnauthorized: true, servername: env.FTP_HOST }, }); await client.uploadFrom('chemin/local.json', '/www/chemin/remote.json'); client.close(); ``` ## Section 7 : Créer une nouvelle veille — checklist 1. **registry.json** : ajouter l'entrée `veilles.`. Bumper `updated_at`. 2. **`uploads/veille//article.json`** : créer le JSON initial avec la structure cible. 3. **`veille//index.php`** : ```php '; include __DIR__ . '/../_veille-page.php'; ``` 4. **`veille//_article.php`** : template PHP qui lit `article.json` et rend le HTML. 5. **Routing** : ajouter `RewriteRule` dans `.htaccess` + route dans `router.php`. 6. **FTP deploy** : déployer `registry.json` + `article.json` + `_article.php` + `index.php`. 7. **Test** : `node scripts/run-veille.js --slug --dry-run` puis sans `--dry-run`. **Fréquences recommandées :** - News rapides (bourse, crypto) : `frequency_hours: 6` - Veille thématique (tech, retro) : `frequency_hours: 168` (7j) - Affaires longues (judiciaire) : `frequency_hours: 168` ou `336` ## Section 8 : Commandes systemd ```bash # Daemon principal (veille générique) journalctl --user -u veille-daemon -f systemctl --user restart veille-daemon systemctl --user status veille-daemon # Lancer un cycle manuellement node scripts/run-veille.js --slug retro node scripts/run-veille.js --slug retro --dry-run node scripts/run-veille.js --slug retro --render-only # Legacy crypto (son propre timer) journalctl --user -u crypto-veille -f systemctl --user start crypto-veille.service ```