fix: vídeos en Social y notas Superhuman por día desde Drive

Los docs de seguimiento son Google Docs embebidos: se exportan, se parten por fecha y se acumulan. Social renderiza VIDEO/REELS y trae vídeos de página.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 17:09:49 +02:00
parent 06024cf473
commit 751673c341
6 changed files with 419 additions and 107 deletions

View File

@@ -99,7 +99,7 @@ export async function POST(req: NextRequest) {
? String(body.tenantId)
: undefined;
const exportBodies = body.exportBodies !== false;
const maxEntries = Math.min(Number(body.maxEntries) || 24, 40);
const maxEntries = Math.min(Number(body.maxEntries) || 80, 150);
try {
const docs = await listDocs();
@@ -175,6 +175,7 @@ export async function POST(req: NextRequest) {
try {
const rows = await buildSeguimientoRowsForDoc({
tenantId: tenant.id,
tenantName: tenant.name,
docId,
maxEntries,
exportBodies,

View File

@@ -57,7 +57,7 @@ export async function GET() {
if (agency && ctx.viaView) {
const tid = encodeURIComponent(tenantId);
const e = await restGet<SeguimientoEntry[]>(
`/rest/v1/seguimiento_entries?tenant_id=eq.${tid}&select=*&order=occurred_at.desc.nullslast&limit=200`
`/rest/v1/seguimiento_entries?tenant_id=eq.${tid}&select=*&order=occurred_at.desc.nullslast&limit=300`
);
entries = e.data || [];
} else {
@@ -65,7 +65,7 @@ export async function GET() {
.from('seguimiento_entries')
.select('*')
.order('occurred_at', { ascending: false, nullsFirst: false })
.limit(200);
.limit(300);
entries = (e as SeguimientoEntry[]) || [];
}

View File

@@ -23,6 +23,8 @@ type SocialPost = {
platform: 'facebook' | 'instagram';
message?: string;
mediaUrl?: string | null;
/** Poster / still for VIDEO / REELS when mediaUrl is a video. */
thumbnailUrl?: string | null;
permalink?: string | null;
createdAt?: string | null;
likes: number;
@@ -33,6 +35,17 @@ type SocialPost = {
mediaType?: string | null;
};
function isVideoType(t?: string | null): boolean {
const s = (t || '').toUpperCase();
return (
s === 'VIDEO' ||
s === 'REELS' ||
s === 'REEL' ||
s.includes('VIDEO') ||
s === 'ANIMATED_IMAGE_VIDEO'
);
}
function insightValue(
data: { name?: string; values?: { value?: number }[] }[] | undefined,
name: string
@@ -70,30 +83,75 @@ async function fetchPageBundle(token: string, pageId: string) {
}
const postsRes = await fetch(
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,shares,likes.summary(true),comments.summary(true)&limit=12&access_token=${encodeURIComponent(token)}`,
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,status_type,shares,likes.summary(true),comments.summary(true),attachments{media_type,type,url,media,subattachments}&limit=16&access_token=${encodeURIComponent(token)}`,
{ cache: 'no-store' }
);
const posts: SocialPost[] = [];
const seen = new Set<string>();
if (postsRes.ok) {
const j = await postsRes.json();
for (const p of j.data || []) {
const att = p.attachments?.data?.[0];
const attType = String(att?.media_type || att?.type || '').toLowerCase();
const videoSrc =
att?.media?.source ||
att?.url ||
att?.subattachments?.data?.[0]?.media?.source ||
null;
const isVideo =
attType.includes('video') ||
String(p.status_type || '').includes('video') ||
Boolean(videoSrc && /\.mp4|video/i.test(String(videoSrc)));
const thumb =
p.full_picture ||
att?.media?.image?.src ||
att?.subattachments?.data?.[0]?.media?.image?.src ||
null;
posts.push({
id: p.id,
platform: 'facebook',
message: p.message || '',
mediaUrl: p.full_picture || null,
mediaUrl: isVideo ? videoSrc || thumb : thumb,
thumbnailUrl: thumb,
permalink: p.permalink_url || null,
createdAt: p.created_time || null,
likes: Number(p.likes?.summary?.total_count || 0),
comments: Number(p.comments?.summary?.total_count || 0),
shares: Number(p.shares?.count || 0),
mediaType: p.full_picture ? 'image' : 'status',
mediaType: isVideo ? 'VIDEO' : thumb ? 'IMAGE' : 'status',
});
seen.add(p.id);
}
} else {
profile.postsError = (await postsRes.text()).slice(0, 160);
}
// Native page videos / reels often missing from /posts
const videosRes = await fetch(
`${META}/${encodeURIComponent(pageId)}/videos?fields=id,description,created_time,source,permalink_url,picture,length,title&limit=10&access_token=${encodeURIComponent(token)}`,
{ cache: 'no-store' }
);
if (videosRes.ok) {
const j = await videosRes.json();
for (const v of j.data || []) {
if (!v?.id || seen.has(v.id)) continue;
posts.push({
id: v.id,
platform: 'facebook',
message: v.description || v.title || '',
mediaUrl: v.source || v.picture || null,
thumbnailUrl: v.picture || null,
permalink: v.permalink_url || null,
createdAt: v.created_time || null,
likes: 0,
comments: 0,
shares: 0,
mediaType: 'VIDEO',
});
seen.add(v.id);
}
}
return { ...profile, pageInsights, posts };
}
@@ -137,18 +195,29 @@ async function fetchIgBundle(token: string, igId: string) {
}
const mediaRes = await fetch(
`${META}/${encodeURIComponent(igId)}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=12&access_token=${encodeURIComponent(token)}`,
`${META}/${encodeURIComponent(igId)}/media?fields=id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count&limit=16&access_token=${encodeURIComponent(token)}`,
{ cache: 'no-store' }
);
const posts: SocialPost[] = [];
if (mediaRes.ok) {
const j = await mediaRes.json();
for (const m of j.data || []) {
const mediaType = m.media_type || null;
const video = isVideoType(mediaType);
// VIDEO/REELS: keep media_url for <video>; thumbnail for poster
const mediaUrl = video
? m.media_url || m.thumbnail_url || null
: m.media_url || m.thumbnail_url || null;
const thumbnailUrl = m.thumbnail_url || (video ? null : m.media_url) || null;
let reach: number | null = null;
let impressions: number | null = null;
let shares = 0;
try {
const metrics = video
? 'reach,saved,shares,plays'
: 'reach,impressions,saved,shares';
const mi = await fetch(
`${META}/${encodeURIComponent(m.id)}/insights?metric=reach,impressions,saved,shares&access_token=${encodeURIComponent(token)}`,
`${META}/${encodeURIComponent(m.id)}/insights?metric=${metrics}&access_token=${encodeURIComponent(token)}`,
{ cache: 'no-store' }
);
if (mi.ok) {
@@ -158,23 +227,10 @@ async function fetchIgBundle(token: string, igId: string) {
values?: { value?: number }[];
}[];
reach = insightValue(data, 'reach');
impressions = insightValue(data, 'impressions');
const shares = insightValue(data, 'shares');
posts.push({
id: m.id,
platform: 'instagram',
message: m.caption || '',
mediaUrl: m.media_url || m.thumbnail_url || null,
permalink: m.permalink || null,
createdAt: m.timestamp || null,
likes: Number(m.like_count || 0),
comments: Number(m.comments_count || 0),
shares: shares ?? 0,
reach,
impressions,
mediaType: m.media_type || null,
});
continue;
impressions =
insightValue(data, 'impressions') ??
insightValue(data, 'plays');
shares = insightValue(data, 'shares') ?? 0;
}
} catch {
/* fall through without per-media insights */
@@ -183,13 +239,16 @@ async function fetchIgBundle(token: string, igId: string) {
id: m.id,
platform: 'instagram',
message: m.caption || '',
mediaUrl: m.media_url || m.thumbnail_url || null,
mediaUrl,
thumbnailUrl,
permalink: m.permalink || null,
createdAt: m.timestamp || null,
likes: Number(m.like_count || 0),
comments: Number(m.comments_count || 0),
shares: 0,
mediaType: m.media_type || null,
shares,
reach,
impressions,
mediaType,
});
}
} else {