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

@@ -0,0 +1,632 @@
#!/usr/bin/env python3
"""
Remedia brandbooks: elimina kits inventados y aplica solo señales
verificadas desde webs oficiales (+ notas de procedencia).
"""
from __future__ import annotations
import json
import random
import time
import urllib.request
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
def env(k: str) -> str:
for line in (ROOT / ".env.local").read_text().splitlines():
if line.startswith(k + "="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
return ""
URL = env("NEXT_PUBLIC_SUPABASE_URL")
KEY = env("SUPABASE_SERVICE_ROLE_KEY")
HR = {"apikey": KEY, "Authorization": f"Bearer {KEY}"}
HW = {**HR, "Content-Type": "application/json", "Prefer": "return=minimal"}
def get(path: str) -> Any:
req = urllib.request.Request(f"{URL}/rest/v1/{path}", headers=HR)
return json.loads(urllib.request.urlopen(req).read())
def patch(path: str, body: dict) -> None:
req = urllib.request.Request(
f"{URL}/rest/v1/{path}",
data=json.dumps(body).encode(),
headers=HW,
method="PATCH",
)
urllib.request.urlopen(req)
def nid(p: str) -> str:
return f"{p}_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
def color(name: str, hex_: str, role: str, usage: str) -> dict:
return {
"id": nid("col"),
"name": name,
"hex": hex_.upper() if hex_.startswith("#") else hex_,
"role": role,
"usage": usage,
}
def font(family: str, role: str, source: str, notes: str) -> dict:
return {
"id": nid("font"),
"family": family,
"role": role,
"weights": "",
"source": source,
"url_or_notes": notes,
}
# Solo colores con evidencia fuerte en HTML/CSS de la web oficial.
# Si no hay evidencia clara → colors = [] (mejor vacío que inventado).
VERIFIED_VISUAL: dict[str, dict[str, Any]] = {
"4ventures-hub": {
"colors": [
color(
"Naranja marca (web)",
"#E05200",
"primary",
"Frecuencia alta en CSS de https://4ventures.es/",
),
],
"fonts_kit": [
font(
"DM Sans",
"headline",
"google",
"font-family detectado en HTML de 4ventures.es",
),
font(
"Cormorant Garamond",
"display",
"google",
"font-family detectado en HTML de 4ventures.es",
),
],
"source": "HTML/CSS https://4ventures.es/",
},
"agua-miia": {
"colors": [
color(
"Azul primario",
"#0085F2",
"primary",
"CSS var marca en https://aguamiia.es/",
),
color(
"Azul corporativo",
"#2071CF",
"secondary",
"CSS var / uso frecuente en aguamiia.es",
),
color(
"Azul oscuro",
"#0F4F9E",
"neutral",
"CSS var en aguamiia.es",
),
color(
"Verde acento",
"#2E9B76",
"accent",
"CSS var en aguamiia.es",
),
],
"fonts_kit": [
font(
"Urbanist",
"headline",
"google",
"font-family dominante en HTML de aguamiia.es",
),
font(
"Nunito",
"body",
"google",
"font-family secundario en HTML de aguamiia.es",
),
],
"source": "CSS variables + font-family https://aguamiia.es/",
},
"bps": {
# BPS ya tenía kit; reforzamos con theme-color oficial
"colors": [
color(
"BPS Blue",
"#0051AA",
"primary",
"theme-color meta + uso en https://blueparrotschool.com/",
),
color(
"Accent amarillo",
"#FDC526",
"accent",
"Hex frecuente en CSS de blueparrotschool.com",
),
color(
"Ink",
"#201E22",
"neutral",
"Hex frecuente en CSS de blueparrotschool.com",
),
],
"fonts_kit": [
font(
"Figtree",
"headline",
"google",
"Referenciado en HTML blueparrotschool.com",
),
font(
"Jost",
"body",
"google",
"Referenciado en HTML blueparrotschool.com",
),
],
"source": "theme-color + CSS https://blueparrotschool.com/",
"merge_colors_if_existing": True,
},
"brava-dermoestetica": {
"colors": [
color(
"Rosa/brandy clínica",
"#7A3F4D",
"primary",
"Hex dominante (69×) en https://www.bravadermoestetica.com/",
),
color(
"Rosa oscuro",
"#61323D",
"secondary",
"Hex frecuente en bravadermoestetica.com",
),
color(
"Fondo cálido",
"#F8F5F2",
"neutral",
"Hex de fondo en bravadermoestetica.com",
),
],
"fonts_kit": [
font(
"Playfair Display",
"headline",
"google",
"font-family en HTML bravadermoestetica.com",
),
font(
"Inter",
"body",
"google",
"font-family en HTML bravadermoestetica.com",
),
font(
"Myriad Pro",
"other",
"adobe",
"font-family referenciado en HTML (licencia Adobe/sistema)",
),
],
"source": "HTML/CSS https://www.bravadermoestetica.com/",
},
"delgado-abogados": {
"colors": [
color(
"Azul enlace/marca",
"#1863DC",
"primary",
"Hex frecuente en https://abogadosdelgado.es/",
),
color(
"Ink",
"#212121",
"neutral",
"Hex frecuente en abogadosdelgado.es",
),
],
"fonts_kit": [
font(
"Inter",
"body",
"google",
"WP preset font-family Inter en abogadosdelgado.es",
),
],
"source": "HTML/CSS https://abogadosdelgado.es/ (parcial; ampliar con brand kit PDF)",
},
"dtatf": {
"colors": [
color(
"Amarillo De Tú a Tú",
"#F6A800",
"primary",
"Hex en CSS/HTML https://detuatuformacion.es/",
),
color(
"Azul oscuro De Tú a Tú",
"#273645",
"secondary",
"Hex en CSS/HTML detuatuformacion.es (+ #253645)",
),
],
"fonts_kit": [
font(
"Poppins-DeTUaTU",
"headline",
"file",
"Familia custom referenciada en detuatuformacion.es (archivo/fuente propia)",
),
],
"source": "HTML/CSS https://detuatuformacion.es/",
},
"ecp": {
"colors": [
color(
"Amarillo ECP",
"#F7A801",
"primary",
"Hex dominante en https://escueladecuidadoresprofesionales.com/",
),
color(
"Azul",
"#1863DC",
"secondary",
"Hex frecuente en web ECP",
),
color(
"Ink",
"#212121",
"neutral",
"Hex frecuente en web ECP",
),
],
"fonts_kit": [
font(
"Raleway",
"headline",
"google",
"font-family dominante en web ECP",
),
font(
"Open Sans",
"body",
"google",
"font-family secundario en web ECP",
),
],
"source": "HTML/CSS https://escueladecuidadoresprofesionales.com/",
},
"equinsa-parking": {
"colors": [
color(
"Navy Equinsa",
"#000028",
"primary",
"Hex en https://equinsaparking.com/",
),
color(
"Azul",
"#003388",
"secondary",
"Hex en equinsaparking.com",
),
color(
"Azul enlace",
"#046BD2",
"accent",
"CSS var / hex en equinsaparking.com",
),
],
"fonts_kit": [
font(
"Roboto",
"body",
"google",
"font-family dominante en equinsaparking.com",
),
],
"source": "HTML/CSS https://equinsaparking.com/",
},
"fbm-abogados": {
"colors": [
color(
"Navy FBM",
"#04243C",
"primary",
"Hex dominante (75×) en https://fbmasociados.com/",
),
],
"fonts_kit": [], # tahoma genérico — no afirmar kit de marca
"source": "HTML/CSS https://fbmasociados.com/ (color). Tipografía: pendiente brand kit.",
},
"halal-safety": {
"colors": [], # scrape débil / ruido WP
"fonts_kit": [
font(
"Manrope",
"body",
"google",
"Referenciado en HTML halalsafety.com",
),
],
"source": "Tipografía HTML https://www.halalsafety.com/. Colores: insuficientes en scrape — vacíos a propósito.",
},
"la-freseria": {
"colors": [], # el scrape pilló azules de template Framer, NO son marca fresa
"fonts_kit": [
font(
"La Fresería",
"display",
"file",
"Familia custom 'La Fresería' referenciada en lafreseria.es (archivo propio)",
),
],
"source": "Fuentes custom en https://lafreseria.es/. Colores NO aplicados: el HTML mezcla tokens Framer (#046bd2) no confiables como marca.",
},
"noberplast": {
"colors": [
color(
"Azul Noberplast",
"#0072AB",
"primary",
"Hex en https://noberplast.com/",
),
color(
"Verde",
"#06A161",
"secondary",
"Hex en noberplast.com",
),
color(
"Rojo acento",
"#DB062C",
"accent",
"Hex en noberplast.com",
),
],
"fonts_kit": [],
"source": "Hex en https://noberplast.com/. Tipografía: no detectada con claridad.",
},
"tsp": {
"colors": [
color(
"Navy Scholar",
"#161B2F",
"primary",
"Hex frecuente en https://talentoscholarprogram.com/",
),
color(
"Azul claro",
"#A4CAEF",
"secondary",
"Hex frecuente en talentoscholarprogram.com",
),
color(
"Rojo acento",
"#A40406",
"accent",
"Hex en talentoscholarprogram.com",
),
],
"fonts_kit": [
font(
"Poppins",
"body",
"google",
"font-family dominante en talentoscholarprogram.com",
),
font(
"Fraunces",
"display",
"google",
"font-family en talentoscholarprogram.com",
),
],
"source": "HTML/CSS https://talentoscholarprogram.com/",
},
"tyd": {
"colors": [
color(
"Rojo TYD",
"#E92428",
"primary",
"Hex dominante en https://talentoydeporte.com/",
),
color(
"Azul TYD",
"#004885",
"secondary",
"Hex en talentoydeporte.com",
),
],
"fonts_kit": [
font(
"Montserrat",
"headline",
"google",
"font-family en talentoydeporte.com",
),
font(
"Oswald",
"display",
"google",
"font-family referenciado en talentoydeporte.com",
),
],
"source": "HTML/CSS https://talentoydeporte.com/",
},
"virginia-molet": {
"colors": [
color(
"Azul Gravitae",
"#004271",
"primary",
"Hex frecuente en https://gravitaeacademy.com/",
),
color(
"Azul enlace",
"#1863DC",
"secondary",
"Hex frecuente en gravitaeacademy.com (posible token WP; contrastar con kit)",
),
],
"fonts_kit": [
font(
"quincy-cf",
"display",
"adobe",
"font-family 'quincy-cf' en gravitaeacademy.com",
),
font(
"Inter",
"body",
"google",
"font-family Inter en gravitaeacademy.com",
),
],
"source": "HTML/CSS https://gravitaeacademy.com/ (revisar azul #1863DC vs kit oficial)",
},
"wine-advisors": {
"colors": [
color(
"Rojo WineAdvisor",
"#CC0100",
"primary",
"Hex detectado en https://www.wineadvisor.mx/",
),
],
"fonts_kit": [],
"source": "Hex limitado en https://www.wineadvisor.mx/. Tipografía: no detectada.",
},
}
PROVENANCE_HEADER = (
"[PROCEDENCIA 2026-10-15] "
"Webs/handles/LinkedIn: contrastados con sitio oficial + Meta business_discovery + páginas LinkedIn. "
"Colores/fuentes: solo si aparecen en HTML/CSS de la web (ver visual_notes). "
"Textos de posicionamiento/tono/ICP: síntesis de brand manager a partir del copy público de la web "
"(no son citas literales del cliente ni de un PDF de marca). "
"Si un campo no tiene evidencia → vacío. "
)
def parse(c: Any) -> dict:
if isinstance(c, str):
return json.loads(c)
return dict(c or {})
def main() -> None:
tenants = get(
"tenants?agency_followed=neq.false&select=id,slug,name&order=slug"
)
for t in tenants:
slug = t["slug"]
docs = get(
f"documents?tenant_id=eq.{t['id']}&type=eq.brandbook&select=id,content&order=updated_at.desc&limit=1"
)
if not docs:
print("skip", slug)
continue
book = parse(docs[0]["content"])
vis = VERIFIED_VISUAL.get(slug)
if vis is None:
# no verified visual → clear invented kits
book["colors"] = []
book["fonts_kit"] = []
book["colors_primary"] = []
book["colors_secondary"] = []
book["fonts"] = []
note = "Sin señales visuales verificables en scrape; kit vaciado."
else:
book["colors"] = vis.get("colors") or []
book["fonts_kit"] = vis.get("fonts_kit") or []
book["colors_primary"] = [
c["hex"] for c in book["colors"] if c.get("role") == "primary"
]
book["colors_secondary"] = [
c["hex"]
for c in book["colors"]
if c.get("role") in ("secondary", "accent")
]
book["fonts"] = [f["family"] for f in book["fonts_kit"]]
if book["fonts_kit"]:
heads = [f for f in book["fonts_kit"] if f["role"] in ("headline", "display")]
bodies = [f for f in book["fonts_kit"] if f["role"] == "body"]
if heads:
book["typography_headline"] = heads[0]["family"]
if bodies:
book["typography_body"] = bodies[0]["family"]
note = vis.get("source") or ""
# Visual notes: honest provenance
book["visual_notes"] = (
f"PROCEDENCIA VISUAL: {note}. "
"Los hex/fuentes listados salen del HTML/CSS público. "
"Sustituir por brand kit oficial (PDF/AI) cuando esté en Drive/Zona de marca. "
"NO usar paletas inventadas por IA."
)
# Provenance in freeform
prev = (book.get("freeform_notes") or "").strip()
# remove older provenance blocks to avoid duplication
lines = [
ln
for ln in prev.split("\n")
if not ln.startswith("[PROCEDENCIA")
and "PROCEDENCIA VISUAL" not in ln
]
book["freeform_notes"] = (
PROVENANCE_HEADER
+ (f"Visual: {note}. " if note else "")
+ ("\n" + "\n".join(lines) if lines else "")
).strip()
# Clear weak invented claims that often aren't on-site
# Keep website/handles/products — those were verified in packs
# Soften proof_points that cite unverified stats
if slug == "la-freseria":
# don't keep invented franchise € figures as hard fact without date stamp
pps = book.get("proof_points") or []
book["proof_points"] = [
p
for p in pps
if "65.000" not in p and "EBITDA" not in p
] + [
"Claim web/franquicia: 'las fresas más famosas del mundo' (lafreseria.es)",
"IG oficial @lafreseriaoficial",
"LinkedIn company/la-freseria",
]
if slug == "equinsa-parking":
# keep qualitative; avoid conflicting installation counts
book["proof_points"] = [
"Web: especializada en sistemas de gestión de aparcamientos (equinsaparking.com)",
"LinkedIn company/equinsa-parking",
"Sede comunicada: C. Primavera 16, Torrejón de Ardoz (web)",
"Contacto web: sat@equinsa.es / +34 618 18 89 24",
]
patch(f"documents?id=eq.{docs[0]['id']}", {"content": book})
n_col = len(book.get("colors") or [])
n_font = len(book.get("fonts_kit") or [])
print(f"[ok] {slug:22} colors={n_col} fonts={n_font} · {note[:80]}")
print("DONE — invented kits removed; verified web signals applied")
if __name__ == "__main__":
main()