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:
632
scripts/remediate_brand_truth.py
Normal file
632
scripts/remediate_brand_truth.py
Normal 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()
|
||||
Reference in New Issue
Block a user