feat: LinkedIn editorial, accesos agencia estables y deshacer en historial

Integra estrategias LinkedIn en calendarios (backlog publicado vs pendiente), unifica roles/cookies de vista agencia y permite deshacer cambios de brandbook dejando constancia en el historial.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-16 04:15:51 +02:00
parent 3b57cdce53
commit 4a1abd3318
54 changed files with 6400 additions and 527 deletions

View File

@@ -18,6 +18,13 @@ import {
} from '@/lib/pipeline-calendar';
import type { PublishedNetworkPost } from '@/lib/calendar-strategy';
import { normalizeApprovalStatus } from '@/lib/approval-status';
import { normalizeGenOptions } from '@/lib/calendar-gen-options';
import {
collectLinkedInUsage,
loadLinkedInStrategyContext,
pickUnpublishedStrategyPosts,
resolveLinkedInStrategyUrl,
} from '@/lib/linkedin-strategy';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
@@ -57,9 +64,10 @@ export async function POST(req: NextRequest) {
.map((t) => t.trim().slice(0, 160))
.slice(0, 40)
: [];
const genOptions = (body.gen_options || body.options || null) as
const genOptionsRaw = (body.gen_options || body.options || null) as
| Record<string, unknown>
| null;
const genOptions = normalizeGenOptions(genOptionsRaw || undefined);
let slug = 'cliente';
const t = await restGet<{ slug?: string }[]>(
@@ -129,6 +137,58 @@ export async function POST(req: NextRequest) {
});
}
let linkedinStrategyUrl = '';
let linkedinPrompt: string | undefined;
let linkedinStrategyPosts:
| import('@/lib/linkedin-strategy').LinkedInStrategyPost[]
| undefined;
if (genOptions.linkedinPosts > 0 || genOptions.linkedinStrategyUrl) {
linkedinStrategyUrl = resolveLinkedInStrategyUrl({
brandbook,
slug,
overrideUrl: genOptions.linkedinStrategyUrl,
});
if (linkedinStrategyUrl) {
const liCtx = await loadLinkedInStrategyContext({
url: linkedinStrategyUrl,
brandbook,
mode: genOptions.linkedinMode,
});
if (liCtx) {
linkedinStrategyPosts = liCtx.posts;
const usage = collectLinkedInUsage({
history,
publishedPosts,
avoidTitles,
});
const pick = pickUnpublishedStrategyPosts(
liCtx.posts,
usage,
genOptions.linkedinPosts || liCtx.posts.length
);
linkedinPrompt = [
liCtx.promptBlock,
`ESTADO BACKLOG ESTRATEGIA: ${pick.publishedOrPlanned.length} ya publicados o planificados · ${pick.unpublished.length} pendientes.`,
pick.publishedOrPlanned.length
? `YA USADOS (no re-agendar): ${pick.publishedOrPlanned
.slice(0, 40)
.map((p) => p.id)
.join(', ')}`
: '',
pick.unpublished.length
? `PENDIENTES (priorizar en orden): ${pick.unpublished
.slice(0, 24)
.map((p) => `${p.id} — ${p.title}`)
.join(' | ')}`
: 'Backlog agotado → crear ideas nuevas en línea de marca.',
]
.filter(Boolean)
.join('\n');
}
}
}
const result = await generateMonthPreview({
ym: month,
slug,
@@ -137,7 +197,10 @@ export async function POST(req: NextRequest) {
brandbook,
publishedPosts,
avoidTitles,
genOptions: genOptions || undefined,
genOptions,
linkedinStrategyUrl: linkedinStrategyUrl || undefined,
linkedinPrompt,
linkedinStrategyPosts,
});
return NextResponse.json({
@@ -147,6 +210,9 @@ export async function POST(req: NextRequest) {
source: result.source,
brand: slug,
brandbook: !!brandbook,
linkedin_strategy_url: linkedinStrategyUrl || null,
linkedin_posts_from_strategy: linkedinStrategyPosts?.length || 0,
linkedin_schedule: result.linkedinMeta || null,
history_months: history.length,
network_posts: publishedPosts.length,
});