/** * One-shot sync: organic + ads + superhuman + SEO audits. * Usage: * npx tsx -r dotenv/config -r tsconfig-paths/register scripts/sync_all_data.ts * Optional: TENANT_SLUG=bps */ import { config } from 'dotenv'; import { resolve } from 'path'; config({ path: resolve(process.cwd(), '.env.local') }); config({ path: resolve(process.cwd(), '.env') }); import { serviceHeaders, supabaseUrl } from '../src/lib/agency'; import { syncAllConnectedOrganic } from '../src/lib/google-organic'; import { syncAdsForTenants } from '../src/lib/ads-sync'; import { syncAllSuperhumanTenants } from '../src/lib/superhuman-docs'; import { runSeoSiteAudit } from '../src/lib/seo-audit'; import { resolveMetaAccessTokens } from '../src/lib/ads-discovery'; const META = 'https://graph.facebook.com/v21.0'; type Tenant = { id: string; name: string; slug: string }; async function sbGet(path: string): Promise { const headers = serviceHeaders(); if (!headers) throw new Error('no service role'); const r = await fetch(`${supabaseUrl()}/rest/v1/${path}`, { headers, cache: 'no-store', }); if (!r.ok) throw new Error(`${path}: ${(await r.text()).slice(0, 300)}`); return (await r.json()) as T; } async function sbPatch(path: string, body: unknown) { const headers = serviceHeaders(); if (!headers) throw new Error('no service role'); const r = await fetch(`${supabaseUrl()}/rest/v1/${path}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=minimal', }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`PATCH ${path}: ${(await r.text()).slice(0, 300)}`); } async function sbPost(path: string, body: unknown) { const headers = serviceHeaders(); if (!headers) throw new Error('no service role'); const r = await fetch(`${supabaseUrl()}/rest/v1/${path}`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=minimal', }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`POST ${path}: ${(await r.text()).slice(0, 400)}`); } async function syncSocial(tenants: Tenant[]) { const tokens = await resolveMetaAccessTokens(); const token = tokens[0]; if (!token) return { ok: false as const, warning: 'Sin META token', results: [] as unknown[] }; const results: { tenant: string; fb?: string; ig?: string }[] = []; for (const tenant of tenants) { const integs = await sbGet< { provider: string; external_account_id?: string | null; status: string; meta?: Record | null; }[] >( `integrations?tenant_id=eq.${encodeURIComponent(tenant.id)}&provider=in.(facebook,instagram)&status=eq.connected&select=provider,external_account_id,status,meta` ); if (!integs.length) continue; const out: { tenant: string; fb?: string; ig?: string } = { tenant: tenant.name, }; for (const i of integs) { const id = i.external_account_id; if (!id) continue; try { let payload: Record | null = null; if (i.provider === 'facebook') { const r = await fetch( `${META}/${encodeURIComponent(id)}?fields=name,fan_count,followers_count&access_token=${encodeURIComponent(token)}`, { cache: 'no-store' } ); if (r.ok) { payload = (await r.json()) as Record; out.fb = 'ok'; } else { out.fb = `err ${(await r.text()).slice(0, 80)}`; } } else { const r = await fetch( `${META}/${encodeURIComponent(id)}?fields=name,followers_count,media_count&access_token=${encodeURIComponent(token)}`, { cache: 'no-store' } ); if (r.ok) { payload = (await r.json()) as Record; out.ig = 'ok'; } else { out.ig = `err ${(await r.text()).slice(0, 80)}`; } } if (payload) { const now = new Date().toISOString(); await sbPatch( `integrations?tenant_id=eq.${encodeURIComponent(tenant.id)}&provider=eq.${i.provider}`, { last_sync_at: now, error_message: null, meta: { ...(i.meta || {}), social_cache: payload, social_cached_at: now, }, } ); } } catch (e) { const msg = e instanceof Error ? e.message : 'err'; if (i.provider === 'facebook') out.fb = msg; else out.ig = msg; } } results.push(out); } return { ok: true as const, results }; } async function syncSeoAudits(tenants: Tenant[]) { const results: { tenant: string; score?: number; grade?: string; error?: string; }[] = []; for (const tenant of tenants) { const [gsc, ga4] = await Promise.all([ sbGet< { external_account_id?: string | null; meta?: { site_url?: string } }[] >( `integrations?tenant_id=eq.${encodeURIComponent(tenant.id)}&provider=eq.search_console&status=eq.connected&select=external_account_id,meta&limit=1` ), sbGet<{ id: string }[]>( `integrations?tenant_id=eq.${encodeURIComponent(tenant.id)}&provider=eq.ga4&status=eq.connected&select=id&limit=1` ), ]); const site = gsc[0]?.meta?.site_url || gsc[0]?.external_account_id || null; if (!site) continue; try { const audit = await runSeoSiteAudit({ gscSiteUrl: site, ga4Connected: ga4.length > 0, gscConnected: true, }); await sbPost('seo_audits', { tenant_id: tenant.id, site_url: audit.siteUrl, score: audit.score, grade: audit.grade, warning_count: audit.warningCount, critical_count: audit.criticalCount, checks: audit.checks, summary: audit.summary, }); results.push({ tenant: tenant.name, score: audit.score, grade: audit.grade, }); } catch (e) { results.push({ tenant: tenant.name, error: e instanceof Error ? e.message.slice(0, 160) : 'audit error', }); } } return results; } async function main() { if (!serviceHeaders()) throw new Error('Falta SUPABASE_SERVICE_ROLE_KEY'); let tenants = await sbGet( 'tenants?select=id,name,slug&order=name.asc' ); const slug = process.env.TENANT_SLUG?.trim(); const tid = process.env.TENANT_ID?.trim(); if (slug) tenants = tenants.filter((t) => t.slug === slug); if (tid) tenants = tenants.filter((t) => t.id === tid); console.log(`\n=== SYNC ALL · ${tenants.length} tenant(s) ===\n`); console.log('--- ORGANIC ---'); const organic = await syncAllConnectedOrganic({ tenantId: tenants.length === 1 ? tenants[0].id : null, days: 28, }); for (const r of organic.results) { const name = tenants.find((t) => t.id === r.tenantId)?.name || r.tenantId; console.log( ` ${name}: ${r.daysWritten}d${r.warning ? ` · ⚠ ${r.warning}` : ''}` ); } console.log('\n--- ADS ---'); const ads = await syncAdsForTenants({ tenantId: tenants.length === 1 ? tenants[0].id : undefined, days: 30, }); for (const r of ads.results) { console.log( ` ${r.tenantName}: meta=${r.metaRows} google=${r.googleRows}` + (r.errors.length ? ` · ERR ${r.errors.join('; ').slice(0, 100)}` : '') ); } console.log('\n--- SUPERHUMAN ---'); try { const sh = await syncAllSuperhumanTenants({ tenantId: tenants.length === 1 ? tenants[0].id : null, maxEntries: 100, exportBodies: true, }); for (const r of sh.results) { console.log( ` ${r.tenantName}: ${r.status}` + (r.upserted ? ` · ${r.upserted}` : '') + (r.docName ? ` · ${r.docName}` : '') + (r.error ? ` · ${r.error.slice(0, 80)}` : '') ); } } catch (e) { console.log(' ', e instanceof Error ? e.message : e); } console.log('\n--- SOCIAL ---'); const social = await syncSocial(tenants); if (!social.ok) console.log(' ', social.warning); for (const r of social.results as { tenant: string; fb?: string; ig?: string }[]) { console.log(` ${r.tenant}: fb=${r.fb || '-'} ig=${r.ig || '-'}`); } console.log('\n--- SEO AUDIT ---'); for (const r of await syncSeoAudits(tenants)) { console.log( ` ${r.tenant}:` + (r.grade != null ? ` ${r.grade} (${r.score})` : '') + (r.error ? ` ERR ${r.error}` : '') ); } console.log('\n=== DONE ===\n'); } main().catch((e) => { console.error(e); process.exit(1); });