chore: scripts Dokploy/BPS (cron secret, Ads MCC, sync y probes)

Incluye utilidades de ops para CRON_SECRET, login customer Google Ads, sync de seguimiento BPS y probes de conexiones.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-29 16:48:44 +02:00
parent 8c5acb626f
commit 1a58af129d
17 changed files with 2913 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
/**
* One-shot: bootstrap Google + discover Meta → fichas, then print audit.
* Usage: npx tsx --env-file=.env.local scripts/refresh_agency_connections.ts
*/
import { bootstrapAgencyGoogle } from '../src/lib/agency-google-bootstrap';
import { discoverAndMatch } from '../src/lib/ads-discovery';
import { serviceHeaders, supabaseUrl } from '../src/lib/agency';
async function get(path: string) {
const headers = serviceHeaders();
if (!headers) throw new Error('no service role');
const r = await fetch(`${supabaseUrl()}${path}`, {
headers,
cache: 'no-store',
});
if (!r.ok) throw new Error(`${path}: ${await r.text()}`);
return r.json();
}
async function organicCount(tenantId: string): Promise<number> {
const headers = serviceHeaders();
if (!headers) return 0;
const r = await fetch(
`${supabaseUrl()}/rest/v1/organic_daily?tenant_id=eq.${tenantId}&select=day`,
{
headers: { ...headers, Prefer: 'count=exact', Range: '0-0' },
cache: 'no-store',
}
);
const cr = r.headers.get('content-range') || '';
if (cr.includes('/')) return Number(cr.split('/').pop()) || 0;
return 0;
}
async function main() {
console.log('=== 1. Google bootstrap (Drive + GA4 + GSC + Ads + sync) ===');
const boot = await bootstrapAgencyGoogle({
includeGoogleAds: true,
syncOrganic: true,
organicDays: 28,
});
console.log(JSON.stringify(boot, null, 2));
console.log('\n=== 2. Meta discover (Ads + FB + IG) ===');
const meta = await discoverAndMatch({
apply: true,
providers: ['meta_ads', 'google_ads'],
});
console.log(
JSON.stringify(
{
meta: {
accounts: meta.meta?.accounts,
linked: meta.meta?.linked,
pagesLinked: meta.meta?.pagesLinked,
igLinked: meta.meta?.igLinked,
error: meta.meta?.error,
},
google: {
accounts: meta.google?.accounts,
linked: meta.google?.linked,
error: meta.google?.error,
},
},
null,
2
)
);
console.log('\n=== 3. Audit por cliente ===');
const tenants = (await get(
'/rest/v1/tenants?select=id,name,slug&order=name.asc'
)) as { id: string; name: string; slug: string }[];
const integs = (await get(
'/rest/v1/integrations?provider=in.(ga4,search_console,google_drive,google_ads,meta_ads,facebook,instagram)&select=tenant_id,provider,status,external_account_id,error_message,meta'
)) as {
tenant_id: string;
provider: string;
status: string;
external_account_id?: string | null;
error_message?: string | null;
meta?: Record<string, unknown> | null;
}[];
const by = new Map<string, typeof integs>();
for (const i of integs) {
const arr = by.get(i.tenant_id) || [];
arr.push(i);
by.set(i.tenant_id, arr);
}
const providers = [
'google_ads',
'meta_ads',
'ga4',
'search_console',
'google_drive',
'facebook',
'instagram',
] as const;
type Row = {
name: string;
organic_days: number;
channels: Record<string, string>;
gaps: string[];
};
const rows: Row[] = [];
for (const t of tenants) {
const map = new Map(
(by.get(t.id) || []).map((i) => [i.provider, i] as const)
);
const channels: Record<string, string> = {};
const gaps: string[] = [];
for (const p of providers) {
const i = map.get(p);
if (i?.status === 'connected' && i.external_account_id) {
const id = String(i.external_account_id);
channels[p] = id.length > 42 ? id.slice(0, 40) + '…' : id;
} else if (i?.status === 'error') {
channels[p] = `ERR:${(i.error_message || '').slice(0, 40)}`;
gaps.push(p);
} else {
channels[p] = '—';
gaps.push(p);
}
}
const org = await organicCount(t.id);
rows.push({ name: t.name, organic_days: org, channels, gaps });
}
console.log(
[
'Cliente'.padEnd(28),
'GAds',
'Meta',
'GA4',
'GSC',
'Drive',
'FB',
'IG',
'OrgDías',
].join('\t')
);
for (const r of rows) {
const c = r.channels;
const flag = (v: string) => (v === '—' ? '—' : v.startsWith('ERR') ? 'ERR' : 'OK');
console.log(
[
r.name.slice(0, 28).padEnd(28),
flag(c.google_ads),
flag(c.meta_ads),
flag(c.ga4),
flag(c.search_console),
flag(c.google_drive),
flag(c.facebook),
flag(c.instagram),
String(r.organic_days),
].join('\t')
);
}
console.log('\n=== Detalle IDs (solo conectados) ===');
for (const r of rows) {
const linked = Object.entries(r.channels).filter(([, v]) => v !== '—' && !v.startsWith('ERR'));
if (!linked.length && r.organic_days === 0) continue;
console.log(`\n${r.name} · organic_daily=${r.organic_days}`);
for (const [p, v] of linked) console.log(` ${p}: ${v}`);
}
const withAds = rows.filter(
(r) => r.channels.google_ads !== '—' || r.channels.meta_ads !== '—'
).length;
const withOrg = rows.filter(
(r) =>
r.channels.ga4 !== '—' ||
r.channels.search_console !== '—' ||
r.organic_days > 0
).length;
const withSocial = rows.filter(
(r) => r.channels.facebook !== '—' || r.channels.instagram !== '—'
).length;
console.log('\n=== RESUMEN ===');
console.log(
JSON.stringify(
{
tenants: rows.length,
with_ads: withAds,
with_organic_link_or_data: withOrg,
with_fb_or_ig: withSocial,
organic_tenants_with_rows: rows.filter((r) => r.organic_days > 0).length,
},
null,
2
)
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});