feat: calendario bajo Social, SEO/GEO con datos live, Drive = 03-Clientes

Calendario editorial anidado en Social; Orgánico con queries/páginas GSC y landings GA4 + dashboard GEO; Biblioteca enlazada a la carpeta real del cliente bajo 03-Clientes (no carpetas homónimas); previews Ads solo en Meta/Google.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 16:12:07 +02:00
parent 3425364b07
commit f4dfc50cc9
9 changed files with 699 additions and 107 deletions

View File

@@ -9,14 +9,105 @@ import {
import { resolveActiveTenant } from '@/lib/tenant-context';
import type { GeoSnapshot, OrganicDailyRow } from '@/lib/organic';
import {
fetchGa4TopLandings,
fetchGscTopRows,
organicNeedsRefresh,
syncAllConnectedOrganic,
} from '@/lib/google-organic';
import { getAgencyOAuthStatus } from '@/lib/google-drive';
import { isoYesterday } from '@/lib/ad-tracker-range';
export const dynamic = 'force-dynamic';
export const maxDuration = 120;
async function loadLiveSeoDims(
integrations: { provider?: string; status?: string; external_account_id?: string | null; meta?: { property_id?: string; site_url?: string } }[]
): Promise<{
gscQueries: { key: string; clicks: number; impressions: number; ctr: number; position: number }[];
gscPages: { key: string; clicks: number; impressions: number; ctr: number; position: number }[];
ga4Landings: { page: string; sessions: number; users: number }[];
liveWarning?: string;
}> {
const empty = { gscQueries: [], gscPages: [], ga4Landings: [] };
try {
const oauth = await getAgencyOAuthStatus();
if (!oauth.connected) return empty;
const gsc = integrations.find(
(i) => i.provider === 'search_console' && i.status === 'connected'
);
const ga4 = integrations.find(
(i) => i.provider === 'ga4' && i.status === 'connected'
);
const until = isoYesterday();
const sinceDate = new Date(`${until}T12:00:00Z`);
sinceDate.setUTCDate(sinceDate.getUTCDate() - 27);
const since = sinceDate.toISOString().slice(0, 10);
const siteUrl =
gsc?.meta?.site_url ||
(gsc?.external_account_id?.includes('http') ||
gsc?.external_account_id?.startsWith('sc-domain:')
? gsc.external_account_id
: null);
const propertyId =
ga4?.meta?.property_id ||
(ga4?.external_account_id?.startsWith('properties/')
? ga4.external_account_id
: ga4?.external_account_id?.match(/^\d+$/)
? `properties/${ga4.external_account_id}`
: null);
const warnings: string[] = [];
let gscQueries: Awaited<ReturnType<typeof fetchGscTopRows>> = [];
let gscPages: Awaited<ReturnType<typeof fetchGscTopRows>> = [];
let ga4Landings: Awaited<ReturnType<typeof fetchGa4TopLandings>> = [];
if (siteUrl) {
try {
gscQueries = await fetchGscTopRows({
siteUrl,
startDate: since,
endDate: until,
dimension: 'query',
});
} catch (e) {
warnings.push(e instanceof Error ? e.message : 'gsc queries');
}
try {
gscPages = await fetchGscTopRows({
siteUrl,
startDate: since,
endDate: until,
dimension: 'page',
});
} catch (e) {
warnings.push(e instanceof Error ? e.message : 'gsc pages');
}
}
if (propertyId) {
try {
ga4Landings = await fetchGa4TopLandings({
propertyId,
startDate: since,
endDate: until,
});
} catch (e) {
warnings.push(e instanceof Error ? e.message : 'ga4 landings');
}
}
return {
gscQueries,
gscPages,
ga4Landings,
liveWarning: warnings[0],
};
} catch {
return empty;
}
}
async function gate() {
const supabase = await createClient();
const {
@@ -89,7 +180,7 @@ export async function GET(req: NextRequest) {
{ headers, cache: 'no-store' }
),
fetch(
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tid)}&provider=in.(ga4,search_console,tag_manager)&select=provider,status,enabled,external_account_id,last_sync_at,error_message`,
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tid)}&provider=in.(ga4,search_console,tag_manager)&select=provider,status,enabled,external_account_id,last_sync_at,error_message,meta`,
{ headers, cache: 'no-store' }
),
fetch(
@@ -126,6 +217,15 @@ export async function GET(req: NextRequest) {
workspace = rows[0]?.settings?.organico || {};
}
const live = await loadLiveSeoDims(
integrations as {
provider?: string;
status?: string;
external_account_id?: string | null;
meta?: { property_id?: string; site_url?: string };
}[]
);
return NextResponse.json({
portfolioWide: false,
label: ctx.label,
@@ -135,7 +235,14 @@ export async function GET(req: NextRequest) {
integrations,
workspace,
autoSynced,
warning: warningParts[0] || autoSynced?.warning || null,
gscQueries: live.gscQueries,
gscPages: live.gscPages,
ga4Landings: live.ga4Landings,
warning:
warningParts[0] ||
autoSynced?.warning ||
live.liveWarning ||
null,
});
}
@@ -156,12 +263,21 @@ export async function GET(req: NextRequest) {
supabase
.from('integrations')
.select(
'provider,status,enabled,external_account_id,last_sync_at,error_message'
'provider,status,enabled,external_account_id,last_sync_at,error_message,meta'
)
.in('provider', ['ga4', 'search_console', 'tag_manager']),
supabase.from('tenants').select('settings').eq('id', tid).maybeSingle(),
]);
const live = await loadLiveSeoDims(
(integ.data || []) as {
provider?: string;
status?: string;
external_account_id?: string | null;
meta?: { property_id?: string; site_url?: string };
}[]
);
return NextResponse.json({
portfolioWide: false,
label: ctx.label,
@@ -171,7 +287,15 @@ export async function GET(req: NextRequest) {
integrations: integ.data || [],
workspace: (ten.data?.settings as { organico?: unknown } | null)?.organico || {},
autoSynced,
warning: o.error?.message || ge.error?.message || autoSynced?.warning || null,
gscQueries: live.gscQueries,
gscPages: live.gscPages,
ga4Landings: live.ga4Landings,
warning:
o.error?.message ||
ge.error?.message ||
autoSynced?.warning ||
live.liveWarning ||
null,
});
}