174 Commits

Author SHA1 Message Date
Carlos Narro
ff047cac2e Añade overlay de play sobre la captura de WhatsApp en landing B2B
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:37:53 +02:00
unknown
d783ce56d4 Configuracion de imagen 2026-06-11 12:28:40 -04:00
unknown
ba7b10a778 Merge branch 'main' of https://github.com/McGregory99/reformix-hackaton 2026-06-11 11:45:36 -04:00
unknown
b4e8f6d3a3 Configuracion de titulos 2026-06-11 11:45:23 -04:00
Carlos Narro
dbef9ef670 Añade stepper de progreso y bloque "Qué pasa después" al chooser de canal
El chooser (solicitud/[id]) ahora muestra un stepper de 3 pasos (Tus datos ✓, Tu reforma actual, Render + presupuesto pendiente) y, debajo de las tarjetas de canal, una sección "Elijas lo que elijas, esto es lo que pasa después" con los 3 pasos del flujo (nos cuentas tu reforma, render + presupuesto en minutos, visita gratuita para el presupuesto final).

Las tarjetas de canal pasan a grid de 3 columnas en desktop, con iconos SV
2026-06-11 17:42:09 +02:00
Carlos Narro
facf3cd79f Landing del cliente: galería apaisada con lightbox y quita "Entrar"
La galería de trabajos (GaleriaTrabajos) en la landing personalizada del cliente
pasa a formato apaisado (3/2) y, al pulsar una foto, se amplía en un lightbox con
navegación ‹ ›, Esc y clic fuera para cerrar. Se quita el botón "Entrar" del
header de esa landing (TenantBrand sin showLogin): el cliente final no entra al
panel.

Revierte el cambio anterior en public/b2b.html (era la landing equivocada).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:40:12 +02:00
Carlos Narro
5afda5af05 Arregla pérdida de datos al desplegar: el seed solo siembra si la DB está vacía
El docker-entrypoint corre db:seed en cada arranque. El guard comprobaba si
existía un tenant CONCRETO ("reformas-ejemplo"); si ese no estaba pero había
otros (una empresa creada por el reformista), el seed ejecutaba TRUNCATE de
todas las tablas y resembraba el demo → borraba los datos reales en cada deploy.

Ahora el guard salta si existe CUALQUIER tenant, así que con datos reales el seed
nunca toca la DB. SEED_FORCE=1 sigue forzando el reseed (borra todo) a propósito.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:38:38 +02:00
Carlos Narro
df700bcbfb Landing B2B: galería de ejemplos apaisada con lightbox y quita "Entrar"
- Nueva sección "Ejemplos reales": 3 renders (cocina, baño, salón-comedor) en
  formato apaisado (16/10), clicables.
- Lightbox reutilizable (data-zoom + data-gallery): amplía la imagen con
  navegación ‹ ›, Esc y clic fuera para cerrar; agrupa por galería.
- El antes/después de "Cómo funciona" pasa a apaisado (4/3) y también abre el
  lightbox (grupo cocina).
- Quita el botón "Entrar" del header (queda solo "Empezar gratis").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:25:15 +02:00
Carlos Narro
d92d5e2f12 Añade onboarding guiado del panel (tour con driver.js)
Tour por pestañas que explica los puntos clave: en Leads recorre la navegación
(las pestañas secundarias de pasada) + filtros y tabla; en la ficha del lead el
presupuesto/baremo, estado, render y desglose; en Precios el baremo, la mano de
obra y el catálogo. Auto-arranca la primera vez por pestaña (flag en localStorage)
y deja un botón flotante " Tour" para repetir. Pasos sin elemento visible se
descartan (degrada en móvil).

- Dependencia: driver.js (librería estándar de tours, ~5kb, sin más deps;
  evita reinventar overlay/posicionamiento/foco/accesibilidad).
- src/lib/onboarding/panel-tour.ts: pasos por ruta. PanelTour.tsx: cliente que
  lanza driver.js. data-tour en nav, leads, ficha y precios.
- Copy en COPY-GUIDE.md (sección "Onboarding del panel").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:19:01 +02:00
Carlos Narro
b815b0532b Añade baremo de rentabilidad (valor de panel) e indicador en la ficha del lead
Parte C del plan: el baremo mínimo de rentabilidad es ahora un valor configurable
del reformista, solo informativo. Los agentes NO lo usan para decidir nada.

- schema: pricing_config.baremo_minimo (céntimos, nullable) + migración 0012.
- pricing-queries / budget types: exponen baremoMinimo.
- panel/precios: sección "Baremo de rentabilidad" + action actualizarBaremo
  (vacío = sin baremo).
- panel/[id]: el presupuesto estimado se muestra en rojo con aviso "Por debajo
  de tu baremo (X €)" cuando no alcanza el baremo del tenant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:58:55 +02:00
Carlos Narro
0c033eb367 Sandbox: campos opcionales de estilo y gustos del cliente
Añade al formulario del sandbox dos inputs opcionales (estilo, gustos) y los
manda en el body de /sandbox/render, para probar desde la web cómo las
preferencias del cliente condicionan el render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:46:53 +02:00
Carlos Narro
ad87e45892 Lleva las preferencias del cliente (estilo, color, material) al render
Hasta ahora el render solo se condicionaba con tipo/m²/calidad + notas de texto
libre por zona; lo que el cliente decía hablando con Luisa o en la llamada
(estilo, colores, materiales) se guardaba en estilo/tasteText pero NO viajaba al
generador de imagen, así que el render no lo representaba.

- b2c (perfil.ts): el payload de PERFIL_WEBHOOK_URL incluye ahora
  preferencias:{estilo, gustos} (gustos = tasteText). Claves vacías se omiten.
- worker (webhook.dto): nuevo PreferenciasDto opcional.
- worker (prompt-builder): construirUserContent (función pura) inyecta el estilo
  y los gustos del cliente como bloque dedicado y omite el "modern" por defecto
  cuando hay preferencias; el system prompt prioriza colores/materiales del
  cliente sobre un estilo genérico.
- worker (pipeline): enhebra preferencias hasta generarPrompt.
- worker (sandbox): acepta estilo/gustos para poder probarlos.
- docs/arquitectura-integracion: documenta el campo preferencias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:17:45 +02:00
Carlos Narro
f6e0347143 Hace a Luisa cálida y servicial y elimina el rechazo de leads
El bot rechazaba leads con presupuesto bajo y sonaba frío/repetitivo. Ahora
Luisa nunca rechaza (el cierre siempre es positivo; la rentabilidad la valora
el reformista en el panel, no el agente) y tiene el tono del agente de voz:
simpática, variada y dispuesta a ayudar con referencias.

- leads.service: getSiguienteEstado tras presupuesto siempre fin_viable;
  evaluarViabilidad ya no filtra (informa viable=true).
- claude.service: relaja las reglas de la pasada de corrección (permite calidez,
  conectores, emoji suave, 2-3 líneas, variación) y quita el guard que tomaba
  "en qué puedo ayudarte" como frase de IA.
- prompts: reescribe el guion de Luisa (mensajes como ejemplos variables, ayuda
  con referencias de tamaño/materiales, sin rechazo) y elimina el bloque
  <DATOS_EXTRAIDOS>, que era código muerto contradictorio con el generador.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:20:52 +02:00
Carlos Narro
dabdf32b8e Calcula el presupuesto en el flujo: el PDF ya incluye el importe
El PDF llegaba con render+fotos pero sin importe porque nadie ejecutaba el motor.
Nuevo helper calcularPresupuestoLead (reutiliza computeBudget + catálogo del
orquestador). Se llama tras el post-análisis (WhatsApp/llamada) y, como red de
seguridad, al inicio de finalizarYEntregar antes de construir el PDF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:12:05 +02:00
Carlos Narro
b0871b733c Paso de fotos + flujo cross-canal llamada→WhatsApp para el render
Bot: al cerrar la cualificación, Luisa pide una foto y entra en modo recogida;
al llegar la foto la sube como "antes" con perfilCompleto:true → dispara render +
presupuesto + entrega del PDF. Nuevo webhook /whatsapp-fotos para que, tras una
llamada, Luisa escriba al lead, referencie lo hablado y le pida las fotos
(reutiliza el mismo modo).

App: el webhook de Retell, tras el análisis de la llamada, llama a pedirFotosWhatsapp
(WHATSAPP_FOTOS_WEBHOOK_URL) con el contexto de la reforma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:50:33 +02:00
Carlos Narro
c5d4a9296a Unifica la captura de datos: el webhook de Retell usa el mismo agente de análisis
Generaliza analizarTranscripcion(leadId, transcript, origen) como núcleo agnóstico
del canal. WhatsApp arma la transcripción desde conversacion_whatsapp; el webhook
de Retell, tras guardar leads.transcripcion, llama al mismo agente con la
transcripción de la llamada → ambos canales extraen espacio/m2/calidad/urgencia/
presupuesto/viable con un único cerebro LLM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:25:41 +02:00
Carlos Narro
166d52f46d Bot: dispara el post-análisis al cerrar la cualificación
Cuando la conversación llega al cierre (estado presupuesto/fin_*, o Luisa anuncia
que prepara/envía el presupuesto), el bot llama una vez a POST /api/leads/:id/analizar
para que la app capture todos los datos clave de la conversación de una pasada.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:56:05 +02:00
Carlos Narro
1471261a73 App: agente de post-análisis de la conversación de WhatsApp
POST /api/leads/:id/analizar lee toda la conversacion_whatsapp del lead, extrae
con un LLM (OpenRouter) los datos clave (tipoReforma, m2, calidad, urgencia,
presupuesto, viable + crudos) y los persiste en el lead de una pasada. Robusto
frente a la extracción turno-a-turno frágil del bot. El bot lo llamará al cerrar
la cualificación. Helper lib/ai/openrouter.ts + env OPENROUTER_API_KEY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:51:25 +02:00
Carlos Narro
50480b6fc5 Handoff bot: hallazgos reales de logs (conflict/503, persist parcial, sin trigger)
Vía SSH al VPS + docker logs: la conexión Baileys está en bucle de reconexión
(conflict:replaced + 503); persistirTurno funciona (→ ok) pero solo se llama en
algunos turnos y la máquina de estados se descuadra; y el bot NUNCA llama a
ingesta/perfilCompleto, así que la generación de presupuesto/render/entrega no
se dispara (Problema C, el que rompe el end-to-end).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:17:33 +02:00
Carlos Narro
face2d3d1b Handoff runtime del bot para Simón: estado, 2 issues y vía Evolution API
Documenta lo que funciona (flujo conversacional end-to-end, EPs, worker), los
cambios de hoy en el bot (@lid, by-phone, markOnline, modelos, apertura, /qr,
/debug) y los 2 problemas de runtime que quedan en su lado: Baileys deja de
recibir tras reconectar (vía robusta = Evolution API, no Cloud API oficial) y
la persistencia parcial del perfil (revisar logs de persistirTurno).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:16:42 +02:00
Carlos Narro
5004575768 Bot: markOnlineOnConnect=true para recibir mensajes tras reconectar
Con false, al reanudar la sesión guardada el dispositivo quedaba "no disponible"
y WhatsApp no le entregaba los mensajes entrantes (socket open pero inbound=0).
Con un escaneo fresco sí recibía (por el sync de emparejamiento). Marcando online
al conectar, WhatsApp entrega los mensajes también tras reconectar/redeploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:58:06 +02:00
Carlos Narro
e5be1220d8 Bot: resolver @lid al número real para casar el lead (mensajes entrantes)
Confirmado vía /debug: WhatsApp entrega los mensajes desde una dirección @lid
(p.ej. 239225534443615@lid), no desde el número. El bot tomaba el id del lid
como teléfono -> no casaba con ningún lead -> ignoraba el mensaje. Ahora
resolverTelefono() resuelve el @lid a número vía msg.key.remoteJidAlt o el mapa
LID->PN de Baileys antes de buscar el lead. Debug captura también remoteJidAlt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:44:00 +02:00
Carlos Narro
78079e9455 Bot: carpeta de sesión Baileys configurable + /debug captura todos los eventos
BAILEYS_AUTH_DIR permite arrancar una sesión limpia (QR fresco) sin perder la
persistencia (subcarpeta del volumen) — para re-vincular cuando WhatsApp deja
muerto el dispositivo tras varios reinicios. Y messages.upsert ahora registra
en /debug todos los eventos (incluido type != notify) antes de filtrar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:00:14 +02:00
Carlos Narro
8d565e5fb0 Bot: endpoint /debug para diagnosticar entrantes + estado de conexión
GET /debug (Basic auth, QR_TOKEN) devuelve el estado de la conexión de WhatsApp
y un anillo de los últimos mensajes entrantes (remoteJid real, fromMe, tipo) y
el resultado del matching lead↔teléfono. Para diagnosticar por qué el bot no
responde (conexión zombi vs formato de jid @lid vs no llega el mensaje).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:51:10 +02:00
Carlos Narro
a8b6d62dd6 Bot responde a mensajes entrantes: recupera lead por teléfono + fix matching
El bot no respondía a las réplicas del cliente: la sesión lead↔teléfono vivía
solo en memoria y no casaba (se pierde al reiniciar el contenedor). Ahora si no
está en memoria, getOrCreateContext busca el lead en la BD por teléfono vía un
EP nuevo GET /api/leads/by-phone (match por últimos 9 dígitos) y re-registra la
sesión. Aparte, los ids de modelo de Claude del bot estaban mal (-4-5 vs 4.5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:28:56 +02:00
Carlos Narro
0a00d42553 Bot: enviar apertura proactiva al recibir /whatsapp-start + fix clave teléfono
El funnel promete "te escribimos por WhatsApp" pero el bot solo registraba la
sesión y esperaba a que el cliente escribiera primero → no llegaba nada. Ahora
WhatsappService escucha un startEmitter y manda el mensaje de apertura de Luisa
al teléfono (verifica el número con onWhatsApp), persiste estadoWa/botStep y el
intento. Además normaliza la clave de teléfono a solo-dígitos en leadSessions
(antes "+34..." no casaba con los dígitos del jid entrante → ignoraba al cliente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:18:10 +02:00
Carlos Narro
a740d08863 Seguridad /qr: HTTP Basic Auth con QR_TOKEN dedicado (no secreto en la URL)
Review de seguridad: autenticar por query string filtra el secreto (logs,
historial, Referer) y usaba la misma FUNNEL_API_KEY que autoriza la API.
Ahora /qr usa HTTP Basic (credencial en cabecera), un QR_TOKEN dedicado
distinto de FUNNEL_API_KEY, comparación en tiempo constante (timingSafeEqual
sobre hashes) y Referrer-Policy: no-referrer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 16:51:46 +02:00
Carlos Narro
a43e7a77be Bot: página /qr para vincular WhatsApp con QR escaneable
El QR de Baileys solo salía como ASCII en los logs (ilegible en Dokploy). Ahora
el WhatsappService empuja el QR al WebhookListener, que sirve GET /qr?key=
<FUNNEL_API_KEY> con el código como imagen (lib qrcode), autorrefresco y estado
"ya conectado". Añade dependencia qrcode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 16:46:06 +02:00
Carlos Narro
062a34c144 Sandbox de renders en el image-worker + fixes del pipeline
Sandbox web (/sandbox) servido por el worker, reusando los mismos servicios del
pipeline, para iterar prompts/modelos con una imagen y ver variaciones+score, y
guardar la config ganadora (aplica al worker al instante, persistida en volumen):
- SettingsService: store de config (system prompts + modelos) con defaults de
  prompts/*.txt + env y override persistido en /app/data (sobrevive a redeploys).
- /sandbox (HTML), GET /sandbox/config, POST /sandbox/render, POST /sandbox/save
  (auth Bearer FUNNEL_API_KEY). DOM seguro, sin innerHTML de contenido externo.
- prompt-builder/supervisor/image-generator aceptan overrides y leen de Settings.

Fixes del pipeline de generación:
- image-generator: pide modalities ['image','text'] y extrae la imagen de
  message.images[] (forma real de OpenRouter), no solo de content.
- main.ts: sube el límite de body a 30mb (las fotos en data URI rompían el 100kb).

Deja de versionar artefactos de build (dist/ + *.tsbuildinfo); .gitignore en
image-worker y Whatsapp-bot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 16:32:11 +02:00
Carlos Narro
89166857e7 Doc despliegue: añade notas de integración para Simón (GETs, a pulir)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:28:41 +02:00
Carlos Narro
c5e73d1688 Quita la restricción ALLOWED_NUMBER del bot de WhatsApp
A petición del usuario: el bot conversa con cualquier número. Doc actualizada.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:13:40 +02:00
Carlos Narro
d0b9744a24 Documenta el despliegue de Luisa + image-worker en el VPS
Tres servicios unidos en Dokploy (reformix-b2c/bot/worker), dominios, webhooks
configurados en b2c, y los 2 pasos manuales pendientes (OPENROUTER_API_KEY + QR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:48:45 +02:00
Carlos Narro
d34925cd7f Prepara deploy de Luisa + image-worker: Dockerfiles + GET endpoints
- Dockerfiles multistage (node:20-alpine) + .dockerignore para mvp/Whatsapp-bot
  (expone 3000+3001, persiste prompts) y mvp/image-worker (expone 3001).
- Añade los 2 GET que el bot necesita y faltaban en la API:
  GET /api/leads/:id (estado del lead) y GET /api/leads/:id/conversacion (historial).
- Limpia el package.json/lock de la raíz (baileys colado por error; no es workspace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:36:14 +02:00
unknown
25669f3008 configuracion de arquitectura y manejo de herramientas 2026-06-07 18:30:29 -04:00
unknown
cb44779349 Proeycto de images-worker creado 2026-06-07 18:11:44 -04:00
unknown
fec365bb57 Configuracion de agente de whastapp paratrabajar con la estructura propuesta 2026-06-07 17:51:53 -04:00
Carlos Narro
d3189d7277 Reescribe el handoff de WhatsApp al modelo por EP + smoke test de los bot EPs
El bot de Luisa puebla la BD vía los 4 EPs HTTP (no SQL directo): conversacion,
perfil, calificacion, intento. Actualiza el handoff de Simón en consecuencia
(qué EP usa para cada cosa, enums/tipos a alinear, ya no necesita acceso a BD).

Añade api-docs/smoke-bot-eps.mjs: crea un lead de prueba, ejerce los 4 EPs por
HTTP, verifica en BD y limpia. Verificado end-to-end en produccion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:07:13 +02:00
Carlos Narro
8b96037dad Añade EPs HTTP para que el bot de WhatsApp pueble la BD
El bot (Luisa) es externo y no toca Postgres directamente. Cuatro endpoints
autenticados con Bearer FUNNEL_API_KEY, validados con zod:

- POST /api/leads/:id/conversacion  → turno de chat (+ estado_wa/bot_step)
- POST /api/leads/:id/perfil         → update parcial del lead (extracción)
- POST /api/leads/:id/calificacion   → upsert de lead_calificacion
- POST /api/leads/:id/intento        → registro en intentos_contacto

Helpers compartidos lib/api/funnel-auth.ts (autorizado + jsonResponse) y
lib/api/bot-request.ts (validarBotRequest: auth + JSON + zod + lead existe).
La ruta de ingesta se refactoriza para reutilizar funnel-auth (DRY).
Schemas puros en lib/funnel/bot-schemas.ts con tests, y doc en api-docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 20:04:11 +02:00
Carlos Narro
508fc43f1f Enlace del email = subir solo fotos (sin re-preguntar ni re-llamar)
Arregla 2 problemas del flujo de subir fotos desde el email:
- El enlace iba a /formulario (form completo) y al enviarlo re-ejecutaba
  procesarLead, que VOLVÍA a llamar. Ahora el email apunta a /solicitud/[id]/fotos,
  una página ligera (SubirFotos): solo sube fotos (+ nota opcional) al lead de la
  URL, re-señala perfilCompleto y NO llama.
- Guarda en procesarLead: si el lead ya tiene llamada_completada, no se vuelve a
  llamar (ni se pisa la transcripción real del webhook).
Copy de la página en COPY-GUIDE §3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:33:15 +02:00
Carlos Narro
98f02eb02e Bypass de llamadas fail-closed (review de seguridad)
El gate del allowlist miraba allow.length>0 (tras parsear), así que un typo en
RETELL_ALLOWED_NUMBERS que no dejara ningún número válido abría las llamadas a
todos (fail-open). Ahora mira la presencia de la variable: si está puesta, se
enforce; un typo restringe a nadie (fail-closed), acorde a la intención.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:04:27 +02:00
Carlos Narro
db46dc9cf3 Bypass de pruebas: solo se llaman números en RETELL_ALLOWED_NUMBERS
Guarda en iniciarLlamadaSaliente: si RETELL_ALLOWED_NUMBERS tiene valor (CSV de
E.164), solo se lanza la llamada a esos números; el resto se omite (devuelve
null, funnel en simulado). Vacío = se llama a todos. Protege tanto el form
(procesarLead) como el canal llamada (pedirLlamada). En prod = +34651194617.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:00:36 +02:00
Carlos Narro
ac04972100 Webhook Retell: fallback por teléfono cuando la llamada no trae lead_id
Si la llamada no lleva metadata.lead_id (llamadas manuales/antiguas), el webhook
busca el lead más reciente con ese teléfono (últimos 9 dígitos) y le asocia la
transcripción + grabación. Responde JSON informativo (matched/leadId) para poder
re-procesar llamadas a mano.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:56:16 +02:00
Carlos Narro
aed0ffae50 Captura transcripción + grabación reales de la llamada (webhook Retell)
- retell.ts: la llamada saliente manda metadata.lead_id; helpers obtenerLlamada
  (GET /v2/get-call, dato autoritativo) y descargarGrabacion (guarda el audio
  en nuestro sistema como data URI).
- /api/retell/webhook: en call_analyzed/call_ended relee la llamada por call_id,
  guarda la transcripción real en lead.transcripcion, descarga la grabación a
  lead.audio_url y deja el análisis + duración en un evento de pipeline. Seguro
  por re-fetch (no se fía del body del webhook).
- orchestrator/pedirLlamada: pasan leadId; procesarLead ya no guarda transcript
  simulado cuando la llamada es real (lo rellena el webhook).
- La ficha del panel ya mostraba transcripción + audio: ahora se pueblan solos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:49:12 +02:00
Carlos Narro
6ef69b403d Agente de voz: cuelga al detectar la despedida (end_call)
Añadida la herramienta end_call al Retell LLM + instrucción en el prompt para
colgar cuando el cliente se despide o tras la despedida del agente (y si no
consiente la grabación). Guía sincronizada.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:44:36 +02:00
Carlos Narro
340c25f1a4 Sincroniza guía Retell: prompt de fotos condicional (WhatsApp otro número/email)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:22:08 +02:00
Carlos Narro
af4d1fa001 El agente pide fotos + auto-email del enlace al pedir llamada
- Prompt del agente de voz (Retell, en caliente + docs/retell-setup.md):
  recuerda al cliente que envíe fotos del espacio por WhatsApp o por el enlace
  del email, para que el render sea de su reforma.
- pedirLlamada: envía automáticamente el email con el enlace al formulario al
  solicitar la llamada (antes solo con un botón manual), para que la frase del
  agente "te enviamos un email" sea cierta. Best-effort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:12:23 +02:00
Carlos Narro
35669fa207 Añade docs/retell-setup.md: guía de activación del agente de voz
Pasos en el panel de Retell, prompt del agente listo para pegar (con las
variables dinámicas {{empresa_nombre}}/{{cliente_nombre}}/{{tipo_reforma}}/
{{provincia}}), variables que envía la app, compliance pendiente y cómo se
conecta (RETELL_API_KEY/FROM_NUMBER/AGENT_ID en Dokploy).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:22:31 +02:00
Carlos Narro
782b847af5 Añade estudio de copy B2B (promesa/descripción/CTA) + marca demo decidido
- docs/copy-b2b-estudio.md: estrategia (audiencia nivel 2, trigger map, tono),
  7 opciones de H1 con ángulo/trigger/valoración, opciones de subhead y de CTA
  (orientado a registro→demo, no trial), combo recomendado y checks de
  autenticidad. Aprendizaje de compramostucoche aplicado.
- plan-accion: decisión "nada de 14 días → registro+demo" marcada como tomada;
  enlazado el estudio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:53:23 +02:00
Carlos Narro
1034994e3b Añade docs/plan-accion.md (plan de acción desde el feedback)
Plan vivo con checkboxes desglosado por áreas: canal WhatsApp, medida del m²,
landing B2B (copys/vídeo/oferta/demo vs trial/competencia), onboarding del
reformista, dominio propio y producción de vídeo. Incluye decisiones de
producto pendientes. Incluye también el ajuste del handoff de Simón.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:03:28 +02:00
Carlos Narro
620e1410f7 Corrige typo en docs/handoff-whatsapp-simon.md ("NUESTROS" → "de la API") 2026-06-04 19:24:17 +02:00