#!/usr/bin/env node // worker/daily-digest.js — Email récap quotidien nouvelles fiches AEP // Cron : 0 8 * * * (tous les jours à 8h) // Usage : node worker/daily-digest.js import 'dotenv/config' // ─── CONFIG DEPUIS .env ─────────────────────────────────────────────────────── const NOCODB_URL = process.env.NOCODB_URL || 'http://localhost:8070'; const NOCODB_TOKEN = process.env.NOCODB_TOKEN; const NOCODB_BASE = process.env.NOCODB_BASE; const NOCODB_TABLE_ORGAS = process.env.NOCODB_TABLE_ORGAS; const RESEND_API_KEY = process.env.RESEND_API_KEY; const RESEND_FROM = process.env.RESEND_FROM || 'AEP Digest '; const EMAIL_DEST = process.env.EMAIL_JULES || 'transformationsresilientes@gmail.com'; const NOCODB_ADMIN_URL = process.env.NOCODB_ADMIN_URL || 'http://localhost:8070'; // ─── UTILITAIRES LOG ───────────────────────────────────────────────────────── function log(...args) { const ts = new Date().toISOString(); console.log(`[${ts}]`, ...args); } // ─── NOCODB — FETCH FICHES DERNIÈRES 24H ───────────────────────────────────── async function fetchNewFiches() { const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // NocoDB ne supporte pas bien le filtre datetime sur CreatedAt — on récupère // les 200 dernières fiches triées par date décroissante et on filtre en JS // (même pattern que getBudgetMoisCourant dans enrich.js) const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE}/${NOCODB_TABLE_ORGAS}?limit=200&sort=-CreatedAt`; const res = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } }); if (!res.ok) { throw new Error(`NocoDB GET fiches → ${res.status}: ${await res.text()}`); } const data = await res.json(); const rows = data.list || []; // Filtre JS sur les 24 dernières heures const recent = rows.filter(row => { const created = new Date(row.CreatedAt || row.created_at || 0); return created >= new Date(since); }); return recent; } // ─── CONSTRUCTION EMAIL HTML ───────────────────────────────────────────────── function buildEmailHtml(fiches) { const count = fiches.length; const date = new Date().toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); const lignes = fiches.map(f => { const nom = f.nom || '(sans nom)'; const url = f.url || null; const echelle = f.echelle || '—'; const statut = f.moderation_status || 'pending'; const ficheUrl = `https://aep.trans-former.fr/fiche/${f.Id}`; const nomHtml = url ? `${escHtml(nom)}` : escHtml(nom); const statutColor = statut === 'approved' ? '#057a55' : statut === 'rejected' ? '#c81e1e' : statut === 'ai_processed' ? '#1a56db' : '#9ca3af'; // pending / autre return ` ${nomHtml} ${escHtml(echelle)} ${escHtml(statut)} fiche #${f.Id} `; }).join(''); return `

AEP — Agences En Pratique

${count} nouvelle${count > 1 ? 's' : ''} fiche${count > 1 ? 's' : ''} soumise${count > 1 ? 's' : ''}

${date}

${count} fiche${count > 1 ? 's ont été soumises' : ' a été soumise'} dans les dernières 24 heures.

${lignes}
Organisation Échelle Statut Lien

Digest automatique AEP — envoyé chaque matin à 8h

`; } // Échappement HTML minimal function escHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ─── ENVOI VIA RESEND ───────────────────────────────────────────────────────── async function sendDigest(fiches) { const count = fiches.length; const subject = `AEP — ${count} nouvelle${count > 1 ? 's' : ''} fiche${count > 1 ? 's' : ''} soumise${count > 1 ? 's' : ''}`; const html = buildEmailHtml(fiches); const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${RESEND_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: RESEND_FROM, to: [EMAIL_DEST], subject, html }) }); if (!res.ok) { const err = await res.text(); throw new Error(`Resend API ${res.status}: ${err}`); } const data = await res.json(); log(`Email digest envoyé → ${EMAIL_DEST} | id: ${data.id} | sujet: "${subject}"`); } // ─── MAIN ───────────────────────────────────────────────────────────────────── async function run() { log('=== daily-digest démarré ==='); // Vérification variables obligatoires if (!NOCODB_TOKEN || !NOCODB_BASE || !NOCODB_TABLE_ORGAS) { log('ERREUR : Variables NocoDB manquantes (NOCODB_TOKEN, NOCODB_BASE, NOCODB_TABLE_ORGAS)'); process.exit(1); } if (!RESEND_API_KEY) { log('ERREUR : RESEND_API_KEY manquante'); process.exit(1); } try { // Fetch fiches des 24 dernières heures const fiches = await fetchNewFiches(); log(`${fiches.length} fiche(s) trouvée(s) dans les dernières 24h`); if (fiches.length === 0) { log('Aucune nouvelle fiche — digest non envoyé.'); process.exit(0); } // Envoi email await sendDigest(fiches); log('=== daily-digest terminé avec succès ==='); } catch (e) { log('ERREUR daily-digest:', e.message); console.error(e.stack); process.exit(1); } } run();