diff --git a/.env b/.env index dbedf90..a329116 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# Archivo de variables de entorno para la configuración de Google Sheets +# Archivo de variables de entorno para la configuración de Google Sheets GOOGLE_SERVICE_ACCOUNT_EMAIL=dbsheets@dbsheets-487314.iam.gserviceaccount.com GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCi2DVTbgH5WzE4\nrsT4BLcjplwszwkiZmhUxcVma/xI/6G+LQpzWTmmETmPWGFBpPukcoEDJDIugSFF\nuiKM+cSJIJJTYYWb5sBGXC6bi36AiA3W/zuRYGoFdNP+34iqepWpXpjKY8T0bbJJ\njhIgjW/SN2fYZeVBZqLATEzRqCcPYXI5gs9fyb2bzTEjiB3kCklVCudl/JDoxAWm\nJ9vO/OOXEgt/t97v9cMoo9OZ4Y3txqiQB41UdS97btDvpezc59u+8K9s5FT+yljC\nMyfRiRM+4MpycCoJobiT8NnCaNblMTd8mkfuujrFvnT/k9O794gBOJ2sgF7AYej4\nzm9tXpknAgMBAAECggEAM50bqcngTkydGT4rqAC2m1ILPRiR6JlU0CmvG3t2hxyf\nA0v7V/wbzYsAF3MHwGquZ7zzLy+1yA+doCAS+2Pe6yeruUnbs0I8BxEIpLxd6Bc3\na8GR+833TKtu1gW7p20bKoeVHfDpaB+stquVC0RJAPQWsfv54fTJ+PE2F+2YE8Np\njhWvM4OkUNZVBPZxG+RF0gzylIx2tw3QpLN3DMSr9Z2wGfohp7yryP96qqC1RFGQ\nCsaHsPRfzv+UNinGmA0Yzv79Stuqgsg+J3V/01Pm4ANutHix/kP0YAYCyg29UtVZ\nIJZtPOAfHWICYw+hQMpcfSLWzbkUQpwTTG0Bk6xmwQKBgQDUNf226UzdAm/V2Pag\nc1YF4P6Bru1A5jiFTpyrAYPSN+Wk84Gkl8XtuDDb7zXL+MaVb8aIVXTKhM2b0RDw\nHtDayoCCXci8VfP4sDRzjEGB1O1OPOl/wPD6L94CmNB0QeepNs4Q5xD0HEsoNN6s\nZGafIkIbVVUBCkuZwNxoLVXSKQKBgQDEcnKkgE/ror9vXSj0XY9Rn0wU2W6DreUt\nMizWyH9R0wBbaSJQ/4Wd3F6co2H2DSuZgXxLJvsPlqffUivs/oZnlx8HhWi6vjLL\nvSiqNC1D+Bh5TT7PEonENU99lruKtZfKcMpRtVF6WycW5ZlYok4lRCEBBvbjtGgr\nIHnqc8WazwKBgD0oHilDjQI4fJBtiY3uD0F8ePxYtEk6z9kjMaVat2my8DycbKVJ\nlCIRLguEIcXZuSlB4D5UGmdu5G5eTvpUdy5Go8huTLZyMvBdn5AZQJuxCH6+sTA/\nI3OGrf2jmpeWkrPCdpqKbOlc6g/5RJ78BFMiJVkJO4kkNWtnGF3xisUZAoGAJ8VT\n2GHxzC7CuvUNWYunfErXyFDMwvPttdy2nzUwMYyaQedi/yr8Dh2TOfsJ1hqMfSNt\n2nkl0t4ZVmj4Y56T71z09zzXGxduiTjehrbRgzUzHzu4P4vtQD/au/5MMaTZ+i4j\nh69Bs5fIriYFiaAyWfEVDXQvf6IChNiqVgDiPJkCgYEAooIiW4HHVUNGMnIeB7UI\nBXv5gSs2heHYSmEK+Er7In5cfJQzkcJvwiOMfOPCNuwVPn+ow5IN8le1acDon5I2\n0OsXhWmgyg9zmgguj273mH3h6+o6XTg5GrEYgPVpU3m2dJkd05oWhQvzy7/OxHs8\nE4ZMz97uqmU2r8XJCo//tv8=\n-----END PRIVATE KEY-----\n" GOOGLE_SHEET_ID=12X6qeU0W4ZDw00vS4OreyC4nFjKBeBYo7rq3wyRsrNQ @@ -14,8 +14,11 @@ EMAIL_API_KEY=9UShpS8oh5Iun92TJSfevElI3Lp99TCv # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings -# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres -# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the +# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres +# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the # one found in a remote Prisma Postgres URL, does not contain any sensitive information. -DATABASE_URL="file:./dev.db" \ No newline at end of file +DATABASE_URL="file:./dev.db" + +N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary" +N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH" diff --git a/package.json b/package.json index 8de6810..6e72802 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "tinacms dev -c \"astro dev\"", "build": "astro build", + "postbuild": "node scripts/send-to-n8n.js", "preview": "astro preview", "astro": "astro" }, diff --git a/scripts/n8n-workflow-translation-report.json b/scripts/n8n-workflow-translation-report.json new file mode 100644 index 0000000..06cda9f --- /dev/null +++ b/scripts/n8n-workflow-translation-report.json @@ -0,0 +1,172 @@ +{ + "name": "📊 Reporte de Traducciones - cdrdpyj", + "nodes": [ + { + "id": "webhook-1", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1.2, + "position": [250, 300], + "parameters": { + "path": "news-summary", + "httpMethod": "POST", + "authentication": "headerAuth", + "responseMode": "onReceived", + "responseData": "{{ $json }}", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": null, + "name": "crpj-summary-key" + } + } + }, + { + "id": "code-1", + "name": "Formatear datos", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [480, 300], + "parameters": { + "language": "javaScript", + "code": "const langNames = {\n es: 'Español',\n en: 'English',\n fr: 'Français',\n pt: 'Português',\n rw: 'Kinyarwanda',\n he: 'עברית',\n uk: 'Українська',\n ru: 'Русский',\n kr: 'Kreyòl',\n};\n\nconst ORDER = ['es', 'en', 'fr', 'pt', 'rw', 'he', 'uk', 'ru', 'kr'];\nconst payload = $input.first().json;\n\nconst summaryRows = [];\nfor (const code of ORDER) {\n const s = payload.summary[code];\n if (!s) continue;\n summaryRows.push({\n language: langNames[code] || code,\n code,\n totalEs: payload.totals.spanishArticles,\n translated: s.translated,\n missing: s.missing,\n percent: s.percent,\n });\n}\n\nconst detailRows = [];\nfor (const art of payload.articles) {\n const row = {\n date: art.date,\n spanishTitle: art.spanishTitle,\n spanishFile: art.spanishFile,\n };\n for (const lang of ORDER) {\n row[lang] = art.files[lang] ? '✅' : '❌';\n }\n detailRows.push(row);\n}\n\nfor (const orphan of payload.orphaned) {\n const row = {\n date: orphan.groupId,\n spanishTitle: `⚠️ Huérfano: solo en ${orphan.languages.join(', ')}`,\n spanishFile: '',\n };\n for (const lang of ORDER) {\n row[lang] = orphan.languages.includes(lang) ? '⚠️' : '—';\n }\n detailRows.push(row);\n}\n\nreturn [summaryRows, detailRows];\n", + "mode": "raw" + }, + "nodesOnOutput": { + "main": [ + { + "type": "n8n-nodes-base.googleSheets", + "index": [ + 0 + ] + }, + { + "type": "n8n-nodes-base.googleSheets", + "index": [ + 1 + ] + } + ] + } + }, + { + "id": "sheets-summary", + "name": "Sheets - Resumen", + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4, + "position": [720, 180], + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "", + "mode": "id" + }, + "sheetName": "Resumen", + "columns": { + "mappingMode": "defineBelow", + "value": { + "A": "={{ $json.language }}", + "B": "={{ $json.code }}", + "C": "={{ $json.totalEs }}", + "D": "={{ $json.translated }}", + "E": "={{ $json.missing }}", + "F": "={{ $json.percent }}" + } + }, + "options": { + "cellFormat": "USER_ENTERED", + "dataLocationOnSheet": "A:F" + }, + "handshake": false + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": null, + "name": "Google Sheets" + } + } + }, + { + "id": "sheets-detail", + "name": "Sheets - Detalle", + "type": "n8n-nodes-base.googleSheets", + "typeVersion": 4, + "position": [720, 420], + "parameters": { + "operation": "append", + "documentId": { + "__rl": true, + "value": "", + "mode": "id" + }, + "sheetName": "Detalle", + "columns": { + "mappingMode": "defineBelow", + "value": { + "A": "={{ $json.date }}", + "B": "={{ $json.spanishTitle }}", + "C": "={{ $json.spanishFile }}", + "D": "={{ $json.es }}", + "E": "={{ $json.en }}", + "F": "={{ $json.fr }}", + "G": "={{ $json.pt }}", + "H": "={{ $json.rw }}", + "I": "={{ $json.he }}", + "J": "={{ $json.uk }}", + "K": "={{ $json.ru }}", + "L": "={{ $json.kr }}" + } + }, + "options": { + "cellFormat": "USER_ENTERED", + "dataLocationOnSheet": "A:L" + }, + "handshake": false + }, + "credentials": { + "googleSheetsOAuth2Api": { + "id": null, + "name": "Google Sheets" + } + } + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Formatear datos", + "type": "main", + "index": 0 + } + ] + ] + }, + "Formatear datos": { + "main": [ + [ + { + "node": "Sheets - Resumen", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Sheets - Detalle", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "staticData": null, + "tags": [], + "versionId": "1.0" +} diff --git a/scripts/send-to-n8n.js b/scripts/send-to-n8n.js new file mode 100644 index 0000000..f2c3250 --- /dev/null +++ b/scripts/send-to-n8n.js @@ -0,0 +1,146 @@ +import '@dotenvx/dotenvx/config'; +import { readFileSync, readdirSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, '..'); +const NEWS_DIR = join(ROOT, 'src', 'content', 'news'); +const LANGUAGES = ['es', 'en', 'fr', 'pt', 'rw', 'he', 'uk', 'ru', 'kr']; + +function extractField(fm, field) { + const re = new RegExp(`^${field}:\\s*(.*)$`, 'm'); + const m = fm.match(re); + if (!m) return ''; + let val = m[1].trim(); + if ((val.startsWith("'") && val.endsWith("'")) || + (val.startsWith('"') && val.endsWith('"'))) { + val = val.slice(1, -1); + } + return val; +} + +function parseMeta(filePath) { + const raw = readFileSync(filePath, 'utf-8'); + const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!m) return {}; + const fm = m[1]; + return { + locale: extractField(fm, 'locale'), + title: extractField(fm, 'title'), + date: extractField(fm, 'date'), + draft: extractField(fm, 'draft'), + }; +} + +function normFilename(fileName) { + return fileName.replace(/\.md$/, '').replace(/-0(\d)$/, '-$1'); +} + +function main() { + const groups = {}; + + for (const lang of LANGUAGES) { + const dir = join(NEWS_DIR, lang); + if (!existsSync(dir)) continue; + + for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) { + const meta = parseMeta(join(dir, file)); + if (meta.draft === 'true') continue; + + const key = normFilename(file); + if (!groups[key]) groups[key] = {}; + groups[key][lang] = { + file, + title: meta.title || '', + date: meta.date || '', + }; + } + } + + const allKeys = Object.keys(groups).sort(); + const esKeys = allKeys.filter(k => groups[k].es); + + const summary = Object.fromEntries(LANGUAGES.map(l => { + const matches = esKeys.filter(k => groups[k][l]).length; + return [l, { + total: allKeys.filter(k => groups[k][l]).length, + translated: matches, + missing: esKeys.length - matches, + percent: esKeys.length > 0 ? Math.round((matches / esKeys.length) * 100) : 0, + }]; + })); + + const articles = esKeys.map(k => { + const es = groups[k].es; + return { + groupId: k, + date: es?.date || '', + spanishTitle: es?.title || '', + spanishFile: es?.file || '', + files: Object.fromEntries(LANGUAGES.map(l => [l, groups[k][l]?.file || null])), + }; + }); + + const orphaned = allKeys.filter(k => !groups[k].es).map(k => ({ + groupId: k, + languages: Object.keys(groups[k]), + files: Object.fromEntries(Object.keys(groups[k]).map(l => [l, groups[k][l].file])), + })); + + const payload = { + timestamp: new Date().toISOString(), + source: 'cdrdpyj-postbuild', + site: 'centrodelreinodepazyjusticia.com', + summary, + articles, + orphaned, + totals: { + spanishArticles: esKeys.length, + totalGroups: allKeys.length, + orphanedArticles: orphaned.length, + languages: Object.fromEntries(LANGUAGES.map(l => { + const c = allKeys.filter(k => groups[k][l]).length; + return [l, c]; + })), + }, + }; + + const url = process.env.N8N_WEBHOOK_URL; + if (!url) { + console.log('[send-to-n8n] N8N_WEBHOOK_URL not set — showing sample payload'); + console.log('Summary:', JSON.stringify(payload.summary, null, 2)); + console.log('Sample articles (first 5):'); + payload.articles.slice(0, 5).forEach(a => { + console.log(` ${a.groupId} | ${a.date} | ${a.spanishTitle.slice(0, 60)}...`); + const langs = Object.entries(a.files).filter(([_, v]) => v).map(([k]) => k).join(', '); + console.log(` → Present in: ${langs || 'ES only'}`); + }); + if (payload.orphaned.length) { + console.log('Orphaned articles:'); + payload.orphaned.forEach(o => console.log(` ${o.groupId} | languages: ${o.languages.join(', ')}`)); + } + console.log(`[send-to-n8n] Total: ${articles.length} ES articles, ${orphaned.length} orphans`); + return; + } + + const apiKey = process.env.N8N_API_KEY; + const apiKeyHeader = process.env.N8N_API_KEY_HEADER || 'X-API-Key'; + const headers = { 'Content-Type': 'application/json' }; + if (apiKey) headers[apiKeyHeader] = apiKey; + + fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(payload), + }) + .then(r => { + if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`); + console.log(`[send-to-n8n] OK — ${articles.length} ES articles, ${orphaned.length} orphans`); + }) + .catch(e => { + console.error(`[send-to-n8n] Warning: n8n unreachable (${e.message}) — build continues`); + }); +} + +main();