{
  "name": "Veille & Engagement — Digest matin 8h",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "id": "d8404816-10cd-4799-9cf0-1bce8f1e3f23",
      "name": "Déclencheur 6h00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        448,
        80
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1NvgtiwfgwKRjsm025O3qlXnUcw6QffZpVLI1_4IAHLc",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Profils",
          "mode": "name"
        },
        "options": {}
      },
      "id": "214aac0e-7148-4981-b0ae-0e123e0b4379",
      "name": "Lire la liste de profils",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        672,
        80
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "EvORDqxqhAIKRlIW",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Construit le tableau d'identifiants de profils à passer à Apify.\n// L'acteur batch accepte des usernames (ex: 'satyanadella') OU des URLs de profil.\n// La feuille 'Profils' doit avoir une colonne 'usernames' (et optionnellement 'actif' = TRUE).\nconst usernames = [];\nfor (const item of $input.all()) {\n  const row = item.json;\n  const actif = row.actif === undefined ? true : String(row.actif).toUpperCase() !== 'FALSE';\n  if (actif && row.usernames) usernames.push(String(row.usernames).trim());\n}\nreturn [{ json: { usernames } }];"
      },
      "id": "d68a7668-237c-48d3-a30b-cc39a4aed5ce",
      "name": "Préparer input Apify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        80
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.apify.com/v2/acts/apimaestro~linkedin-batch-profile-posts-scraper/run-sync-get-dataset-items",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ usernames: $json.usernames, limit: 1 }) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "a03c818d-d54a-4860-8bd5-7973e61b4ecd",
      "name": "Apify — Scraper posts batch",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        80
      ],
      "credentials": {
        "httpQueryAuth": {
          "id": "HugQXVQWyfUoDoLl",
          "name": "Query Auth account 2"
        }
      },
      "notes": "Acteur apimaestro/linkedin-batch-profile-posts-scraper. Endpoint run-sync-get-dataset-items : bloque jusqu'à la fin du run et renvoie directement les items du dataset. Input = { usernames:[...], limit:1-100 }. PAS de filtre de date côté serveur — on plafonne avec limit:1 (le post le plus récent/profil, run quotidien) et la dédup historique évite de re-traiter les anciens. Auth : credential Query Auth 'token' = jeton Apify (?token=...)."
    },
    {
      "parameters": {
        "jsCode": "// Normalise les items du dataset Apify (acteur BATCH apimaestro/linkedin-batch-profile-posts-scraper).\n// Robuste aux 3 formes possibles : item = post à plat, item = { posts:[...] }, item = { data:{ posts:[...] } }.\nconst out = [];\nfunction pushPost(p) {\n  if (!p) return;\n  const url = p.url || p.postUrl || '';\n  // full_urn = string stable ex: "urn:li:ugcPost:xxx". p.urn est un objet, ne pas l'utiliser directement.\n  const id = p.full_urn || (p.urn && (p.urn.activity_urn || p.urn.ugcPost_urn || p.urn.share_urn)) || p.postId || url;\n  if (!id) return;\n  const a = p.author || {};\n  const author = [a.first_name, a.last_name].filter(Boolean).join(' ').trim()\n    || a.name || p.authorName || p.fullName || 'Inconnu';\n  const s = p.stats || {};\n  const postedAt = typeof p.posted_at === 'string'\n    ? p.posted_at\n    : (p.posted_at && p.posted_at.date) || p.postedAt || p.publishedAt || '';\n  out.push({\n    json: {\n      postId: String(id),\n      author,\n      profileUrl: a.profile_url || a.url || p.profileUrl || '',\n      url,\n      text: (p.text || p.commentary || p.postText || '').slice(0, 2000),\n      postedAt,\n      likes: (s.total_reactions != null ? s.total_reactions : (s.likes != null ? s.likes : p.likesCount)) || 0,\n      comments: (s.comments != null ? s.comments : p.commentsCount) || 0\n    }\n  });\n}\nfor (const item of $input.all()) {\n  const j = item.json;\n  if (Array.isArray(j.posts)) j.posts.forEach(pushPost);\n  else if (j.data && Array.isArray(j.data.posts)) j.data.posts.forEach(pushPost);\n  else pushPost(j);\n}\nreturn out;"
      },
      "id": "0bfa3a9c-cf0a-4cb2-9163-33707642dbd1",
      "name": "Normaliser les posts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        80
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1NvgtiwfgwKRjsm025O3qlXnUcw6QffZpVLI1_4IAHLc",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Historique",
          "mode": "name"
        },
        "options": {}
      },
      "id": "5cb4bf05-f8a8-40c5-8e36-94c2cb24ab3f",
      "name": "Lire historique (déjà vus)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1328,
        320
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "EvORDqxqhAIKRlIW",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Garde uniquement les posts dont le postId n'est PAS déjà dans l'historique.\nconst seen = new Set();\nfor (const r of $('Lire historique (déjà vus)').all()) {\n  if (r.json.postId) seen.add(String(r.json.postId));\n}\nconst nouveaux = [];\nfor (const item of $('Normaliser les posts').all()) {\n  if (!seen.has(item.json.postId)) nouveaux.push(item);\n}\nif (nouveaux.length === 0) return [];\nreturn nouveaux;"
      },
      "id": "567fbb4a-06ab-47e3-94fa-a06791758a03",
      "name": "Filtrer les nouveaux",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1552,
        80
      ]
    },
    {
      "parameters": {
        "jsCode": "// Agrège tous les nouveaux posts en un seul item pour un appel Claude unique.\nconst posts = $input.all().map((i, idx) => ({ index: idx, ...i.json }));\nreturn [{ json: { posts, count: posts.length } }];"
      },
      "id": "8cd538d9-580f-445c-aa98-4a23b4369884",
      "name": "Agréger pour Claude",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1776,
        80
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'claude-sonnet-4-6',\n  max_tokens: 4000,\n  system: \"Tu es l'agent de tri matinal de Linked Connexion (cabinet Fidulia). Tu combines les critères de l'Agent Veille (50) et de l'Agent Engagement (42). Les 3 thèmes Fidulia sont : LinkedIn, IA, et professions réglementées / de confiance (CGP, experts-comptables, avocats, notaires, médecins). Pour CHAQUE post fourni, décide une catégorie parmi COMMENTER, VEILLE ou SKIP.\\n\\nCOMMENTER (logique Agent 42) : post d'un prospect, influenceur ou contact dont on veut gagner la visibilité. Sujet aligné LinkedIn/IA/professions de confiance, hors politique/religion/sport/polémique stérile, sujet où Fidulia est légitime. Engagement déjà visible (>=5 commentaires) = priorité. Si COMMENTER, propose un 'angle' (donnee | position | question_rhetorique) et un 'brouillon' d'une seule phrase, valeur standalone, sans pitch, sans tiret cadratin, sans question ouverte type 'et vous ?'.\\n\\nVEILLE (logique Agent 50) : signal d'information d'une source d'autorite (institution, rapport, presse serieuse, communique AMF/OEC/CNB/ORIAS), chiffre verifiable, changement d'algorithme LinkedIn, tendance B2B/marketing de fond. Pas d'opinion d'influenceur a citer comme fait. Si VEILLE, propose un 'tag' (POST | NEWSLETTER | SIGNAL_FORT).\\n\\nSKIP : generique, hors perimetre, sans autorite, politique/sport, contenu promotionnel pur.\\n\\nReponds UNIQUEMENT avec un tableau JSON valide, un objet par post, sans texte autour. Schema : {index:number, categorie:'COMMENTER'|'VEILLE'|'SKIP', interet:1-5, raison:string, angle?:string, brouillon?:string, tag?:string}.\",\n  messages: [{ role: 'user', content: 'Voici les posts a trier (JSON) :\\n' + JSON.stringify($json.posts) }]\n}) }}",
        "options": {}
      },
      "id": "80cefde6-a881-4f4e-9be9-a65a26b4b60f",
      "name": "Claude — Classer les posts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2000,
        80
      ],
      "notes": "Embarque les critères des agents 50/42. Modèle sonnet-4-6 (passer à haiku-4-5 pour réduire le coût). Clé via $env.ANTHROPIC_API_KEY."
    },
    {
      "parameters": {
        "jsCode": "// Parse la réponse Claude et rattache chaque décision à son post d'origine.\nconst raw = $json.content?.[0]?.text || '[]';\nlet decisions;\ntry {\n  const m = raw.match(/\\[[\\s\\S]*\\]/);\n  decisions = JSON.parse(m ? m[0] : raw);\n} catch (e) {\n  decisions = [];\n}\nconst posts = $('Agréger pour Claude').first().json.posts;\nconst byIndex = {};\nfor (const d of decisions) byIndex[d.index] = d;\n\nconst commenter = [];\nconst veille = [];\nconst toLog = [];\nconst runDate = new Date().toISOString().slice(0, 10);\n\nfor (const p of posts) {\n  const d = byIndex[p.index] || { categorie: 'SKIP', interet: 0, raison: 'non classé' };\n  toLog.push({ postId: p.postId, author: p.author, url: p.url, categorie: d.categorie, interet: d.interet, date: runDate });\n  const enriched = { ...p, ...d };\n  if (d.categorie === 'COMMENTER') commenter.push(enriched);\n  else if (d.categorie === 'VEILLE') veille.push(enriched);\n}\ncommenter.sort((a, b) => (b.interet || 0) - (a.interet || 0));\nveille.sort((a, b) => (b.interet || 0) - (a.interet || 0));\nreturn [{ json: { commenter, veille, toLog, runDate, totalNouveaux: posts.length } }];"
      },
      "id": "73497b97-2273-47f4-ab8a-b26b6df838c0",
      "name": "Scinder Commenter / Veille",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2208,
        80
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1NvgtiwfgwKRjsm025O3qlXnUcw6QffZpVLI1_4IAHLc",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Historique",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "postId"
          ],
          "value": {}
        },
        "options": {}
      },
      "id": "57dc38b3-7f16-4554-8162-93e0c30e72a7",
      "name": "Écrire dans historique",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2432,
        320
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "EvORDqxqhAIKRlIW",
          "name": "Google Sheets account"
        }
      },
      "notes": "Marque tous les posts traités comme 'vus' pour ne pas les reproposer demain."
    },
    {
      "parameters": {
        "jsCode": "// Sépare le tableau toLog en items individuels pour l'écriture Sheets.\nreturn $json.toLog.map(r => ({ json: r }));"
      },
      "id": "e3553c91-8eb4-4820-b909-509bddcafdd5",
      "name": "Éclater pour log",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2208,
        320
      ]
    },
    {
      "parameters": {
        "jsCode": "// Construit l'email HTML avec 2 sections.\nfunction esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nconst d = $json;\nconst card = (p, type) => {\n  const meta = type === 'commenter'\n    ? `Angle : <strong>${esc(p.angle)}</strong>` + (p.brouillon ? `<br><em>Brouillon : « ${esc(p.brouillon)} »</em>` : '')\n    : `Tag : <strong>${esc(p.tag)}</strong>`;\n  return `<tr><td style=\"padding:12px;border-bottom:1px solid #eee;\">\n    <div style=\"font-size:13px;color:#888;\">${esc(p.author)} · ❤ ${p.likes} · 💬 ${p.comments} · intérêt ${p.interet}/5</div>\n    <div style=\"margin:6px 0;\">${esc(p.text).slice(0,240)}…</div>\n    <div style=\"font-size:13px;color:#555;\">${esc(p.raison)}</div>\n    <div style=\"font-size:13px;margin-top:4px;\">${meta}</div>\n    <a href=\"${esc(p.url)}\" style=\"font-size:13px;color:#0a66c2;\">Voir le post →</a>\n  </td></tr>`;\n};\nconst section = (titre, emoji, arr, type) => arr.length\n  ? `<h2 style=\"font-family:Arial;font-size:18px;color:#0a66c2;\">${emoji} ${titre} (${arr.length})</h2>\n     <table style=\"width:100%;border-collapse:collapse;font-family:Arial;\">${arr.map(p=>card(p,type)).join('')}</table>`\n  : `<h2 style=\"font-family:Arial;font-size:18px;color:#999;\">${emoji} ${titre} (0)</h2><p style=\"font-family:Arial;color:#999;\">Rien aujourd'hui.</p>`;\nconst html = `<div style=\"max-width:680px;margin:auto;\">\n  <h1 style=\"font-family:Arial;font-size:22px;\">Digest matin — ${d.runDate}</h1>\n  <p style=\"font-family:Arial;color:#666;\">${d.totalNouveaux} nouveaux posts analysés · ${d.commenter.length} à commenter · ${d.veille.length} en veille.</p>\n  ${section('À commenter', '💬', d.commenter, 'commenter')}\n  ${section('Veille', '🔍', d.veille, 'veille')}\n  <p style=\"font-family:Arial;font-size:12px;color:#aaa;margin-top:24px;\">Généré automatiquement · Linked Connexion · Agents 50 + 42</p>\n</div>`;\nreturn [{ json: { html, subject: `Digest matin ${d.runDate} — ${d.commenter.length} à commenter, ${d.veille.length} en veille` } }];"
      },
      "id": "6cd8a254-eeaf-4da7-a009-c31ded8b8f7e",
      "name": "Construire l'email HTML",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2432,
        80
      ]
    },
    {
      "parameters": {
        "amount": 8,
        "unit": "hours"
      },
      "id": "33a456e0-aca8-4821-99d5-6f261a2d989a",
      "name": "Attendre jusqu'à 8h00",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        2656,
        80
      ],
      "webhookId": "001a2e37-298d-4df6-a84e-38dc087c227e",
      "notes": "Le pipeline démarre à 6h ; on attend la fenêtre d'envoi de 8h. Remplaçable par 'Wait until a specified time' = 08:00."
    },
    {
      "parameters": {
        "sendTo": "fabien@fabien-massavie.com",
        "subject": "={{ $json.subject }}",
        "emailType": "html",
        "message": "={{ $json.html }}",
        "options": {}
      },
      "id": "ae29ed36-8169-41ee-b139-45cbc26111a9",
      "name": "Envoyer le digest (Gmail)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2880,
        80
      ],
      "webhookId": "fc06f178-d3c7-477a-9ac3-bed6f6a6ef31"
    }
  ],
  "connections": {
    "Déclencheur 6h00": {
      "main": [
        [
          {
            "node": "Lire la liste de profils",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lire la liste de profils": {
      "main": [
        [
          {
            "node": "Préparer input Apify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Préparer input Apify": {
      "main": [
        [
          {
            "node": "Apify — Scraper posts batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apify — Scraper posts batch": {
      "main": [
        [
          {
            "node": "Normaliser les posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normaliser les posts": {
      "main": [
        [
          {
            "node": "Filtrer les nouveaux",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lire historique (déjà vus)": {
      "main": [
        [
          {
            "node": "Filtrer les nouveaux",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filtrer les nouveaux": {
      "main": [
        [
          {
            "node": "Agréger pour Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agréger pour Claude": {
      "main": [
        [
          {
            "node": "Claude — Classer les posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude — Classer les posts": {
      "main": [
        [
          {
            "node": "Scinder Commenter / Veille",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scinder Commenter / Veille": {
      "main": [
        [
          {
            "node": "Construire l'email HTML",
            "type": "main",
            "index": 0
          },
          {
            "node": "Éclater pour log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Éclater pour log": {
      "main": [
        [
          {
            "node": "Écrire dans historique",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construire l'email HTML": {
      "main": [
        [
          {
            "node": "Attendre jusqu'à 8h00",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attendre jusqu'à 8h00": {
      "main": [
        [
          {
            "node": "Envoyer le digest (Gmail)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "pinData": {},
  "meta": {
    "instanceId": "4f1b23afe1c1f425a998bc69ab78d2ebd64d2175c452226921ae034356fabed4"
  }
}
