fix(historial): registrar todos los cambios de Zona de marca
Instrumenta kit, fuentes y estilo creativo; el listado filtra por rango e incluye hoy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
type BrandAssetKind,
|
||||
type Brandbook,
|
||||
} from '@/lib/brandbook';
|
||||
import { logChange } from '@/lib/change-history';
|
||||
import { createHash, randomUUID } from 'crypto';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -254,6 +255,37 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
await logChange({
|
||||
tenantId: ctx.tenantId,
|
||||
tenantName: ctx.label,
|
||||
actor: {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: userRole(user),
|
||||
},
|
||||
entityType: 'brandbook',
|
||||
entityId: docId,
|
||||
action: 'kit_upload',
|
||||
summary: `Kit · subida ${kind}${role ? ` (${role})` : ''} · ${label} · ${ctx.label}`,
|
||||
detail: {
|
||||
kind,
|
||||
role,
|
||||
label,
|
||||
file_name: safeName,
|
||||
asset_id: asset.id,
|
||||
asset_file_id: id,
|
||||
changes: [
|
||||
{
|
||||
field: 'kit_assets',
|
||||
from: null,
|
||||
to: `${kind}: ${label}`,
|
||||
},
|
||||
],
|
||||
revertable: false,
|
||||
},
|
||||
source: 'api/brandbook/kit',
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
asset,
|
||||
@@ -311,6 +343,38 @@ export async function DELETE(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
await logChange({
|
||||
tenantId: ctx.tenantId,
|
||||
tenantName: ctx.label,
|
||||
actor: {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: userRole(user),
|
||||
},
|
||||
entityType: 'brandbook',
|
||||
entityId: docId,
|
||||
action: 'kit_delete',
|
||||
summary: `Kit · eliminado ${removed?.kind || 'asset'}${
|
||||
removed?.label ? ` (${removed.label})` : ''
|
||||
} · ${ctx.label}`,
|
||||
detail: {
|
||||
removed,
|
||||
kit_id: kitId || null,
|
||||
asset_file_id: fileId || null,
|
||||
changes: removed
|
||||
? [
|
||||
{
|
||||
field: 'kit_assets',
|
||||
from: `${removed.kind}: ${removed.label}`,
|
||||
to: null,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
revertable: false,
|
||||
},
|
||||
source: 'api/brandbook/kit',
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
brandbook: book,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
detectSourceExt,
|
||||
extractSourceText,
|
||||
} from '@/lib/extract-document-text';
|
||||
import { logChange } from '@/lib/change-history';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 60;
|
||||
@@ -246,6 +247,33 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
await logChange({
|
||||
tenantId: ctx.tenantId,
|
||||
tenantName: ctx.label,
|
||||
actor: {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: userRole(user),
|
||||
},
|
||||
entityType: 'brandbook',
|
||||
entityId: id,
|
||||
action: 'source_upload',
|
||||
summary: `Fuente · ${filename} · ${ctx.label}`,
|
||||
detail: {
|
||||
filename,
|
||||
chars: text.length,
|
||||
changes: [
|
||||
{
|
||||
field: 'source_excerpts',
|
||||
from: null,
|
||||
to: filename,
|
||||
},
|
||||
],
|
||||
revertable: false,
|
||||
},
|
||||
source: 'api/brandbook/upload',
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
brandbook: next,
|
||||
|
||||
@@ -22,9 +22,11 @@ export async function GET(req: NextRequest) {
|
||||
const role = userRole(user);
|
||||
const isAgency = isAgencyRole(role);
|
||||
const sp = req.nextUrl.searchParams;
|
||||
const limit = Number(sp.get('limit') || 50);
|
||||
const limit = Number(sp.get('limit') || 100);
|
||||
const offset = Number(sp.get('offset') || 0);
|
||||
const entityType = sp.get('entity_type') || sp.get('entityType') || null;
|
||||
const since = sp.get('since') || null;
|
||||
const until = sp.get('until') || null;
|
||||
|
||||
let tenantId =
|
||||
sp.get('tenant_id') || sp.get('tenantId') || null;
|
||||
@@ -46,9 +48,11 @@ export async function GET(req: NextRequest) {
|
||||
// Si portfolio wide, tenantId queda null → historial global
|
||||
}
|
||||
|
||||
const { rows, error } = await listChangeHistory({
|
||||
const { rows, error, total } = await listChangeHistory({
|
||||
tenantId,
|
||||
entityType,
|
||||
since,
|
||||
until,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
@@ -74,7 +78,10 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
rows,
|
||||
total: total ?? rows.length,
|
||||
tenant_id: tenantId,
|
||||
portfolio_wide: isAgency && !tenantId,
|
||||
since,
|
||||
until,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
seedBrandbook,
|
||||
type Brandbook,
|
||||
} from '@/lib/brandbook';
|
||||
import { diffFields, logChange } from '@/lib/change-history';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -206,7 +207,9 @@ export async function PUT(req: NextRequest) {
|
||||
const admin = getAdmin();
|
||||
const { data: existing, error: getErr } = await admin
|
||||
.from('tenants')
|
||||
.select('id, name, slug, settings')
|
||||
.select(
|
||||
'id, name, slug, settings, creative_style, creatives_enabled, meta_ad_account_id, meta_page_id, meta_ig_user_id, slack_channel_id, drive_folder_id, default_landing_url, canva_templates, campaign_audience, campaign_products'
|
||||
)
|
||||
.eq('id', tenantId)
|
||||
.maybeSingle();
|
||||
|
||||
@@ -224,7 +227,7 @@ export async function PUT(req: NextRequest) {
|
||||
const updatePayload: Record<string, any> = {
|
||||
creative_style: normalizedStyle,
|
||||
settings: {
|
||||
...(existing.settings || {}),
|
||||
...((existing.settings as Record<string, unknown>) || {}),
|
||||
creative_style: normalizedStyle,
|
||||
},
|
||||
};
|
||||
@@ -273,7 +276,7 @@ export async function PUT(req: NextRequest) {
|
||||
// Cargar brandbook de documents para compilar skill actualizado
|
||||
const { data: brandDoc } = await admin
|
||||
.from('documents')
|
||||
.select('content')
|
||||
.select('id, content')
|
||||
.eq('tenant_id', tenantId)
|
||||
.eq('type', 'brandbook')
|
||||
.order('updated_at', { ascending: false })
|
||||
@@ -328,14 +331,14 @@ export async function PUT(req: NextRequest) {
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (brandDoc) {
|
||||
if (brandDoc?.id) {
|
||||
await admin
|
||||
.from('documents')
|
||||
.update({
|
||||
content: updatedBrandbook,
|
||||
updated_at: updatedBrandbook.updated_at,
|
||||
})
|
||||
.eq('id', (brandDoc as any).id);
|
||||
.eq('id', brandDoc.id);
|
||||
} else {
|
||||
await admin
|
||||
.from('documents')
|
||||
@@ -349,6 +352,65 @@ export async function PUT(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const beforeFlat: Record<string, unknown> = {
|
||||
creatives_enabled: existing.creatives_enabled,
|
||||
meta_ad_account_id: existing.meta_ad_account_id,
|
||||
meta_page_id: existing.meta_page_id,
|
||||
meta_ig_user_id: existing.meta_ig_user_id,
|
||||
slack_channel_id: existing.slack_channel_id,
|
||||
drive_folder_id: existing.drive_folder_id,
|
||||
default_landing_url: existing.default_landing_url,
|
||||
canva_templates: existing.canva_templates,
|
||||
campaign_audience: existing.campaign_audience,
|
||||
campaign_products: existing.campaign_products,
|
||||
creative_style: existing.creative_style,
|
||||
};
|
||||
const afterFlat: Record<string, unknown> = {
|
||||
creatives_enabled:
|
||||
updatePayload.creatives_enabled ?? existing.creatives_enabled,
|
||||
meta_ad_account_id:
|
||||
updatePayload.meta_ad_account_id ?? existing.meta_ad_account_id,
|
||||
meta_page_id: updatePayload.meta_page_id ?? existing.meta_page_id,
|
||||
meta_ig_user_id:
|
||||
updatePayload.meta_ig_user_id ?? existing.meta_ig_user_id,
|
||||
slack_channel_id:
|
||||
updatePayload.slack_channel_id ?? existing.slack_channel_id,
|
||||
drive_folder_id:
|
||||
updatePayload.drive_folder_id ?? existing.drive_folder_id,
|
||||
default_landing_url:
|
||||
updatePayload.default_landing_url ?? existing.default_landing_url,
|
||||
canva_templates:
|
||||
updatePayload.canva_templates ?? existing.canva_templates,
|
||||
campaign_audience:
|
||||
updatePayload.campaign_audience ?? existing.campaign_audience,
|
||||
campaign_products:
|
||||
updatePayload.campaign_products ?? existing.campaign_products,
|
||||
creative_style: normalizedStyle,
|
||||
};
|
||||
const styleChanges = diffFields(beforeFlat, afterFlat, { maxFields: 30 });
|
||||
|
||||
await logChange({
|
||||
tenantId,
|
||||
tenantName: existing.name,
|
||||
actor: {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role,
|
||||
},
|
||||
entityType: 'brandbook',
|
||||
entityId: brandDoc?.id || tenantId,
|
||||
action: 'creative_style_update',
|
||||
summary: styleChanges.length
|
||||
? `Estilo creativo · ${styleChanges.length} campo(s) · ${existing.name}`
|
||||
: `Estilo creativo guardado · ${existing.name}`,
|
||||
detail: {
|
||||
changes: styleChanges,
|
||||
revertable: false,
|
||||
synced_brandbook: true,
|
||||
},
|
||||
source: 'api/creatives/style',
|
||||
});
|
||||
|
||||
const compiledMarkdown = compileClientSkillFromBrandbook(
|
||||
updatedBrandbook,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user