Files
4ventures-client-portal/scripts/remediate_brand_truth.py
4Ventures 4a1abd3318 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>
2026-10-16 04:15:51 +02:00

633 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()