Documentación de la API
Endpoints REST expuestos por n8n para el portal de gestión Yurest. Base URL: {BASE_URL}
Revisada el 20-oct-2026 contra config.js, js-cliente/ y los workflows de n8n.
Introducción
Esta API se expone vía workflows de n8n y persiste en Postgres self-host (ex-Supabase). Todos los endpoints comparten el prefijo:
{BASE_URL}
{BASE_URL} es la URL de webhooks del servidor n8n de Yurest seguida de /webhook. En el portal sale de js/config.js; en esta documentación se escribe así para no publicar el host.
El frontend usa YurestConfig.apiFetch(url, opts), que añade automáticamente la cabecera X-Session-Token (vía getAuthHeaders()), deduplica GETs idénticos en vuelo, aplica un timeout de 30 s (override con timeoutMs) y maneja sesiones expiradas (401/403 → limpia sesión y redirige a login, salvo skipAuthLogout:true).
200 OK con un objeto JSON. Operaciones de escritura devuelven al menos { success: boolean } y, donde aplica, datos de diagnóstico (filas afectadas, errores específicos).
Autenticación
Authorization: Basic fue eliminada del cliente y rotada. Ya no se envía. Todos los webhooks validan ahora un token de sesión firmado (HMAC-SHA256).
El login lo emite el workflow WF16 (auth/login). Cada petición del portal lo manda en la cabecera X-Session-Token, que añade automáticamente YurestConfig.apiFetch() a través de getAuthHeaders(). Cada workflow valida el token server-side (el «Auth Gate HMAC» inline, ~51 workflows migrados).
X-Session-Token: <userId>|<exp>.<hmac>
Content-Type: application/json
Formato del token
| Parte | Contenido |
|---|---|
userId | ID del usuario autenticado (tabla usuarios). |
exp | Epoch de expiración. TTL 30 días si «recordar», 8 h si no. |
hmac | HMAC_SHA256(userId|exp, SECRET). El secreto vive solo en n8n. |
localStorage.yurest_backend === 'laravel', getAuthHeaders() manda en su lugar Authorization: Bearer <token> (Sanctum) + Accept: application/json y reescribe la URL al backend nuevo (api.yurest.com). Los endpoints sin mapping degradan a n8n. Ojo: el flag solo se respeta en desarrollo local — config.js lleva una guarda que fuerza n8n en producción, porque api.yurest.com aún no está desplegado y sin ella un yurest_backend='laravel' olvidado dejaría el portal muerto. Por defecto el portal sigue contra n8n con X-Session-Token.
Si la sesión expira, cualquier endpoint responde 401/403; apiFetch limpia la sesión y redirige a login.html. Las peticiones de fondo pasan { skipAuthLogout:true } para no expulsar al usuario ante un 401 best-effort.
Endpoints públicos (sin sesión)
El cliente final no tiene cuenta; estos endpoints se autorizan por token aleatorio incrustado en el enlace del email, no por sesión.
| Endpoint | Autorización | Por qué es público |
|---|---|---|
auth/login | usuario + contraseña | Emite el token de sesión. |
responderSolicitud | access_token | El cliente rellena la ficha desde el email. |
completarFicha?t=<token> | token de solicitud | Precarga datos del formulario del cliente. |
oferta-publica?t=<token> | token de oferta | Vista pública de la oferta (configurador). |
pago/iniciar | token de oferta | El cliente paga desde la oferta pública (Redsys). |
portal-cliente/* | token propio del cliente | API del Espacio Cliente: base y token distintos, ver su sección. |
oferta-cambios (POST) | token de oferta | Telemetría de cambios del cliente en la oferta (fire-and-forget). |
carga-datos/confirmar-email (POST) | firma HMAC del enlace | El cliente confirma desde el botón del correo de su carpeta que ya subió los documentos (WF-116). |
bienvenida/huecos · bienvenida/reservar (POST) | token de solicitud (t) | El cliente reserva su sesión de bienvenida grupal (WF-124). Desde el 24-oct-2026 el enlace se comprueba en la BD antes de leer el calendario: sin enlace válido → { ok:false, error:'enlace_invalido' } sin tocar Google. |
sepa/mandato (GET) · sepa/firmar (POST) | token HMAC de re-firma | El cliente vuelve a firmar un mandato SEPA desde firmar-sepa.html (WF70). |
agenda-privadas/estado · agenda-privadas/reservar (POST) | token del proyecto (t) | El cliente grupal agenda sus dos sesiones privadas desde agendar-sesion.html (WF-138). |
escalado-local/solicitud · escalado-local/responder (POST) | token del escalado (t) | El cliente rellena los datos de su local nuevo desde escalado-local.html (WF-142). |
Errores y diagnóstico
| Status | Significado | Cuándo |
|---|---|---|
200 | OK · revisa body.success | Casi todas las respuestas (incluso fallos lógicos vienen 200). |
401 / 403 | Sesión expirada | Frontend redirige a login.html. |
500 | Error interno del workflow | Excepción no controlada en n8n. |
eliminarFicha, grabadoA3) devuelven además { success, affected/ficha_count/sol_count, errores: [] } para detectar UPDATEs sin efecto (RLS bloqueando, ID inexistente, etc.).
CORS
Todos los endpoints aceptan peticiones desde cualquier origen y responden con los siguientes headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Session-Token
Las rutas del Espacio Cliente aceptan además X-Cliente-Token y X-Cliente-Proyecto.
🔑 Auth y usuarios
Workflow WF16. Valida credenciales contra la tabla usuarios y, si son correctas, emite el token de sesión firmado (HMAC) que el resto de la API exige.
Body
{ "username": "usuario@ejemplo.com", "password": "..." }
Respuesta 200 (éxito)
{
"success": true,
"user": {
"id": "uuid",
"username": "usuario@ejemplo.com",
"nombre": "Ana García",
"email": "usuario@ejemplo.com",
"rol": "admin | user",
"token": "<userId>|<exp>.<hmac>",
"permisos": { "read": [...], "write": [...], "delete": [...] }
}
}
success && user.username && user.rol. Credenciales inválidas → mensaje neutro (no distingue usuario inexistente de contraseña incorrecta).Verifica que la sesión sigue viva (chequeo periódico del portal). Un 401/403 fuerza logout.
Gestión de usuarios y roles (Administración → admin.html). auth/usuarios lista/crea/edita/borra usuarios y sus permisos; auth/roles gestiona los roles base. Acciones vía action en el body. El rol se calcula como base + deltas granulares { read, write, delete }.
| Endpoint | Uso |
|---|---|
auth/usuarios | CRUD de usuarios + permisos (create/update/password/delete). |
auth/roles | Catálogo de roles base y sus permisos por defecto. |
auth/usuarios/historial | GET del audit log de acciones sobre usuarios (pestaña «Historial»). Tabla usuarios_historial. |
📋 Fichas
Workflow 04-fichas-alta. Devuelve la lista completa de fichas (con su cs_estado del Kanban CS). Alias en config.js: ENDPOINTS.login · ENDPOINTS.altas.
Respuesta 200
{
"clientes": [
{
"id": "uuid",
"denominacion": "Test SL",
"cif": "B40597437",
"email": "contacto@test.es",
"tipo_cliente": "lite | planes | corporate",
"estado": "rellenado | completada",
"cs_estado": "en_implementacion | post_primer_mes | ...",
"grabado_a3": false,
"grabado_a3_at": null,
"introducido_yurest": false,
"promocion_id": null,
"fecha_solicitud": "2026-04-17T...",
"fecha_rellenado": "2026-04-17T...",
"fecha_completado": "2026-04-17T...",
"sepa_mandato": { ... },
"adjuntos": [ ... ],
"paquetes_carrito": { ... },
"deleted_at": null
// ... resto de columnas
}
]
}
Filtrado backend
El workflow excluye filas con deleted_at != NULL. Devuelve todas las columnas, incluidas las vacías como "".
Workflow 04-fichas-alta. Crea una ficha nueva (sin id) o actualiza una existente (con id). En modo edición solo actualiza los campos enviados — no destruye los demás.
Body (todos opcionales en edición)
| Campo | Tipo | Notas |
|---|---|---|
id | uuid | Si presente → UPDATE; ausente → INSERT |
Nombre Sociedad | string | Mapea a denominacion |
CIF/NIF | string | Mapea a cif |
Email · Email Factura · Email CC | string | |
Tipo Cliente | string | lite · planes · corporate |
Estado | string | "rellenado" cuando consultor completa |
JP * · Firmante * · TPV * | strings | Bloques de datos |
Lite · Distribuidor | "Sí" / "" | Booleanos como string |
Adjuntos | array | PDF/JPG/PNG en base64 |
SEPA | object | Mandato firmado (datos + firma_base64) |
paquetes_carrito | object | Snapshot del carrito (módulos, precios, periodos) |
locales | array | Locales de la ficha (insertados aparte) |
Respuesta 200 (éxito)
{ "id": "uuid-de-la-ficha", "success": true }
Respuesta 200 (error en INSERT Locales)
{ "error": "..." }
El nodo INSERT Locales tiene continueOnFail: true, así que un fallo en locales no rompe el INSERT/UPDATE de la ficha principal pero sí aparece en el body.
Workflow 12-completar-ficha. Devuelve los datos de una ficha o solicitud para precargar el formulario. Acepta tres formas de identificación.
Query params
| Param | Tipo | Uso |
|---|---|---|
t | string (16-128 chars) | Token aleatorio público (preferente) |
id | uuid o número | UUID de ficha o ID-Solicitud numérico (uso interno) |
Respuesta 200 (encontrado)
{
"found": true,
"id": "uuid",
"ID Solicitud": 5,
"Nombre Sociedad": "Test SL",
"Email": "contacto@test.es",
"Tipo Cliente": "planes",
"SEPA": { "iban": "ES...", "deudor": {...}, "firma_base64": "..." },
// ... resto de campos
}
Respuesta 200 (no encontrado)
{ "found": false, "error": "Solicitud no encontrada" }
Workflow 10-eliminar. Soft-delete (UPDATE deleted_at = NOW()) en fichas_alta y solicitudes. Como los UUIDs son únicos entre tablas, sólo afecta a la que contiene el ID.
Body
{
"action": "delete",
"entity": "ficha" | "solicitud",
"id": "uuid",
"row_number": "uuid" // alias por compat
}
Respuesta 200
{
"success": true,
"entity": "ficha",
"id": "uuid",
"ficha_count": 1,
"sol_count": 0,
"errores": []
}
errores (problemas Supabase) o si ficha_count + sol_count == 0 → el id ya no existía.Acciones puntuales sobre una ficha ya creada (no re-guardan la ficha entera). Todas reciben { ficha_id } y devuelven { ok, ... }.
| Path | Workflow | Efecto |
|---|---|---|
/ficha/notificar-completa | WF19 | Al completar la ficha: email Drive al cliente + tarea Asana + email integraciones. Antes vivía en WF11. |
/ficha/introducir-yurest | WF64 | Flag «Introducida en Yurest» (idempotente). 1ª vez envía aviso. → { ok, introducido_yurest, introducido_yurest_at }. |
/ficha/marcar-ganado-hl | WF65 | { ficha_id, marcar_ganado:true } → mueve la oportunidad HighLevel a won + «Ganados Planes» y escribe MRR/ARR/nº locales/periodicidad. Desde el 8-oct-2026 rellena además el id de cliente en producto (id_bbdd) del Business, buscado por CIF, solo si está vacío y ningún otro Business lo lleva ya; nunca pisa un valor. Si la ficha no tiene contacto de HighLevel no toca ninguna oportunidad. |
/contrato-enviar | WF84 | Envía el contrato a firmar (efirma) → { ok, id_operacion, destinatarios }. Con { preview:true } devuelve el HTML sin enviar. |
/fichas-adjuntos-upload | WF35 | { nombre, tipo, data(dataURL), ficha_id? } → sube al bucket Storage. → { storage_path, signed_url, size }. |
/sepa/solicitar-firma | WF70 | { ficha_id, mandato_id, email } → envía al cliente enlace a firmar-sepa.html (token HMAC). |
/fichas/importar-sheet | WF54 | { sheetUrl } → importa ficha desde Google Sheets. → { ok, ficha, avisos }. |
Modal «Ver cliente» (clientes.html): notas internas, movimiento en el Kanban CS y asignación de promoción.
| Path | Método | Uso |
|---|---|---|
/cliente-notas | GET ?ficha_id= / POST | Notas internas firmadas con el email del JWT. GET → { notas:[{id,autor_email,texto,created_at}] }; POST { ficha_id, texto }. |
/cliente-notas-editar · /cliente-notas-borrar | POST | Editar / borrar una nota. |
/cs-estado | POST | Mueve el cliente entre columnas del Kanban CS. Actualiza cs_estado + cs_estado_historial. |
/ficha-asignar-promo | POST | { ficha_id, promo_id, promo_turno } → patch de promocion_id/promocion_turno/promo_asignada sin re-guardar la ficha. Si la ficha tiene carga_datos_task_gid, renombra la tarea de carga en Asana con « · Promo <nombre> (<turno>)» (la quita al desasignar; no toca el resto del nombre). Un fallo de Asana no afecta a la asignación. |
Workflow WF-132. Pestaña HighLevel del importador de clientes (importador-clientes.html). Solo lee de HighLevel y del portal: la ficha la guarda después el flujo normal (guardarFicha). La conversión HL → ficha es js/lib/parse-ficha-hl.js, inlineada en el workflow.
| op | Body | Respuesta |
|---|---|---|
buscar | { op, q } (≥ 2 letras) | { ok, items:[{ id, nombre, sociedad, cif, tipo, ciudad, id_bbdd, ficha|null }] }. ficha no nulo = ya está en el portal. |
ficha | { op, business_id } | { ok, ficha, avisos[], resumen, existente|null }. resumen: business_id, nombre, cif, tipo, locales, locales_inactivos, contactos, contactos_lista[{ …, clave }], venta, cs, escalados, jp, citas, promo, duplicados, notas, sesiones. Error: { ok:false, code:'sin_business' }. |
vincular | { op, ficha_id, business_id, locales:[{ nombre, highlevel_record_id }] } | { ok, n }. Tras guardar la ficha, ancla el Business y los locales de HighLevel. Solo rellena vacíos: nunca pisa un vínculo. |
avisos.📨 Solicitudes
Workflow 08-solicitudes. Inserta solicitud en estado pendiente y envía email al cliente con la URL del formulario.
Body
{
"ID Solicitud": 5,
"access_token": "32-chars-uuid-sin-guiones",
"Nombre Sociedad": "Empresa SL",
"Nombre": "Ana García",
"Email": "ana@empresa.com",
"Consultor": "Nombre Consultor",
"Tipo Cliente": "planes",
"Fecha": "2026-04-17",
"Estado": "Pendiente",
"email_to": "ana@empresa.com",
"email_subject": "Solicitud de creación de ficha de alta",
"email_body": "Hola Ana, te mando este formulario...",
"form_url": "https://.../solicitud.html?t=..."
}
Si access_token no viene, el workflow lo genera con crypto.randomUUID().
Respuesta 200
{ "id": "uuid", "success": true }
Listados y utilidades del flujo de solicitudes.
| Path | Método | Uso |
|---|---|---|
/webhook/1757fdcc-… (listaSolicitudes) | GET | WF08. Solicitudes NO completadas → { solicitudes:[{id,access_token,"ID Solicitud","Nombre Sociedad",Email,Estado,Fecha}] }. |
/webhook/fa16b994-… (listaRellenado) | GET | WF09. Solicitudes con estado=completada (esperan al consultor) → { items:[{id,"Nombre Sociedad",Email,Consultor}] }. |
/solicitud-prellenar-cliente | POST | WF38. El consultor pre-rellena todo menos SEPA: { access_token, datos_parciales } → merge JSONB sobre solicitudes.datos respetando SEPA. El cliente solo firma. |
/webhook/a2b1b1d6-… (eliminarSolicitud) | POST | Mismo webhook que eliminarFicha (soft-delete). { action:'delete', entity:'solicitud', id }. |
Workflow 11-auxiliares. El cliente envía sus datos desde solicitud.html. Crea la ficha, carpeta Drive, marca solicitud completada y dispara emails + tarea Asana si TPV no integrado.
Body (todos los datos del formulario)
{
"ID Solicitud": 5,
"access_token": "...",
"Estado": "Rellenado cliente",
"Nombre Sociedad": "...", "CIF/NIF": "...",
"JP Nombre": "...", "Firmante DNI": "...",
"TPV": "Revo",
"TPV No Integrado": "Sí", // si checkbox marcado
"TPV NI Nombre": "...", "TPV NI Contacto": "...", "TPV NI Email": "...",
"SEPA": {
"iban": "ES91...",
"swift": "CAIXESBB",
"deudor": { "nombre": "...", "direccion": "...", ... },
"tipo_pago": "recurrente",
"firma_base64": "data:image/png;base64,...",
"firmado_at": "2026-04-17T..."
},
"Contactos Extra": [ // opcional, máx 10: otras personas de la implementación (paso 03)
{ "nombre": "...", "apellidos": "...", "puesto": "...", "email": "...", "telefono": "..." }
]
}
Efectos
- INSERT en
fichas_altaconestado=completada+ SEPA persistido. - Crea carpeta en Google Drive y guarda
carpeta_driveen la ficha. - Email al cliente con enlace al Drive.
- Email al consultor avisando.
- UPDATE solicitudes:
estado=completado+ficha_id(dispara trigger que copiafecha_solicitud). - Si TPV no integrado: tarea en el proyecto de Integraciones de Asana + email al equipo de soporte e integraciones.
Contactos Extra→ lista de contactos del cliente (RPCcontactos_cliente_importar): deduplica por email, no pisa ninguno existente, ignora el del jefe de proyecto y los deja conconvocar = true. Al crearse el proyecto, un trigger los copia aproyectos.contactos.
Respuesta 200
{ "success": true }
💼 Ofertas y pagos
Workflow 30-ofertas. Ofertas generadas por el configurador (departamento Consultor). GET lista con filtros; ?id=<uuid> devuelve una sola.
POST — action
| action | Efecto |
|---|---|
create · update | Alta/edición de la oferta. |
archivar · reactivar | Estado de archivo. |
cambiar_estado | enviada (email) / ganada (ficha arrancada) / etc. |
vincular_ficha | Asocia la oferta a una ficha de alta. |
PRO-YYYY-<uuid8> (preview, sin numeración fiscal); pedidos hardware (WF21) → PRO-AAAA-NNNN correlativa reservada por RPC.| Path | Auth | Uso |
|---|---|---|
/oferta-publica?t=<token> | Público (token) | WF36. Vista pública de la oferta (configurador.html?t=). |
/oferta-enviar-comercial | X-Session-Token | WF37. { oferta_id, portal_base_url } → email al cliente con link, enviado desde la credencial SMTP/Gmail del consultor dueño (fallback a soporte). La ruta antigua /oferta-enviar (WF36) está en desuso: el portal ya no la llama. |
/oferta-cambios | POST público · GET HMAC | WF80. POST {token, snapshot} (telemetría fire-and-forget con debounce desde el configurador); GET historial por ?oferta_id=N o counts agrupados desde ofertas.html. |
Workflow 55-pasarela (Redsys/BBVA). El cliente, desde la oferta pública, pulsa «Pagar».
POST /pago/iniciar
// Request
{ "token": "<token-oferta>" }
// Respuesta — el front auto-submitea un form a Redsys
{
"ok": true,
"redirect": {
"url": "https://sis.redsys.es/...",
"Ds_SignatureVersion": "HMAC_SHA256_V1",
"Ds_MerchantParameters": "<base64>",
"Ds_Signature": "<firma>"
}
}
El importe es final_amount × 1,21 (IVA). La firma Redsys (HMAC_SHA256_V1, 3DES en JS puro) y el inserto en payments se hacen server-side. La notificación S2S y las páginas OK/KO las sirve el propio workflow. /pago/enviar-ficha-link reenvía al email del cliente el enlace de su solicitud («Continuar más tarde»).
Workflow WF26. Ampliaciones contractuales sobre clientes existentes (módulos, locales o firmas). GET lista; POST crea o cambia el estado.
Filtros del GET, todos opcionales: ?ficha_id=<uuid> · ?tipo=modulo|local|firmas · ?estado=solicitado|pendiente|aplicado|cancelado · ?incluir_archivados=1 (los soft-deleted salen al final). Orden: pendientes primero y, dentro, más recientes primero.
| action | Body | Efecto |
|---|---|---|
create (por defecto) | { ficha_id, tipo:'modulo'|'local'|'firmas', estado:'pendiente', detalle, setup, recurrencia, notas, creado_por } | Alta en pendiente. |
update | { action, id, … } | Edita detalle, importes y notas. |
solicitar | { action, ficha_id, solicitud_email, detalle:{ modulos[], periodicidad }, setup, recurrencia, notas, creado_por } | Solo tipo:'local' (19-oct-2026). Crea el escalado en solicitado con lo comercial fijado por el staff; la BD le pone un token y se manda al cliente por correo (cuenta de Implementación) el enlace a escalado-local.html?t=. El resto lo rellena el cliente (WF-142). |
reenviar | { action, id, solicitud_email? } | Solo desde solicitado. Mismo token, sin fila nueva: renueva solicitud_enviada_at (la caducidad de 30 días cuenta desde ahí) y, si llega, cambia el email. |
aplicar | { action, id } | Solo desde pendiente. firmas: suma a fichas_alta.firmas_contratadas. modulo / local: contrata los módulos nuevos en la ficha y crea su sección en el proyecto (con el sello escalado_id). local además da de alta el local (ver abajo). |
cancelar · archivar · reactivar | { action, id } | Estado y soft-delete. cancelar vale desde pendiente o solicitado. |
detalle según tipo
modulo: { modulos[], locales_ids[], setup, recurrencia, periodicidad:'mensual'|'anual' }
local: { local:{ nombre, … },
sepa:{ modo:'existente', mandato_id } | { modo:'nuevo', iban, bic, titular, sociedad_cif, sociedad_denominacion },
modulos[], setup, recurrencia, periodicidad:'mensual'|'anual' }
firmas: { packs, firmas, setup, recurrencia }
escalado_local_aplicar() crea en una transacción la fila de locales (dirección, email, TPV y mandato), el mandato SEPA nuevo en fichas_alta.sepa_mandato si el cliente dio otra cuenta, y el encargado del local en la lista de contactos del cliente. Es idempotente por escalados.local_id. Si el TPV es integrado, pide a WF62 el ticket Zendesk + tarea Asana de integración de ese local ({ ficha_id, local_id }, etiqueta tpv-local-<local_id>), aunque el cliente ya tenga otros tickets de TPV. El local en el entorno Yurest del cliente sigue siendo manual.pendiente (el cliente envía el formulario o el staff lo crea directo), WF-143 manda un correo a Soporte, al implementador, al CSM y al comercial del cliente (uno por persona, idempotente por avisos_enviados_at; se dispara al responder y con un barrido cada 5 min). En el home, la alerta escalado_local_nuevo sale de GET /escalados?tipo=local (últimos 7 días desde que el cliente rellenó o se creó, no cancelados): quien lee Escalados ve todos; el comercial, el CSM y el implementador, los de sus clientes. Enlaza a escalados.html?alerta=escalado_local_nuevo.solicitado no es venta: no suma en el KPI «Escalados» del home (cuenta el día que el cliente lo rellena) ni en la recurrencia.detalle.periodicidad === 'anual' (6-oct-2026) ⇒ recurrencia es el importe del año (SEPA de pago único). Si no viene (escalados antiguos), es mensual. Contabilidad la pinta como «al año».Respuesta 200
{ "success": true, "escalado": { … }, "errores": [] }
solicitar y reenviar añaden email_enviado (bool) y email_error. El escalado queda guardado aunque el correo falle; el front ofrece entonces «Copiar enlace» o «Reenviar».
Workflow WF-142. Formulario del cliente escalado-local.html?t=<token> para un escalado de local en solicitado. Sin sesión: lo autoriza el token del escalado (64 hex). Las reglas (token, estado, caducidad de 30 días desde el último envío, validación) viven en SQL: escalado_local_solicitud() y escalado_local_responder(), migración 2026-10-19_02.
| Path | Body | Respuesta |
|---|---|---|
/escalado-local/solicitud | { t } | { found:true, estado:'solicitado', cliente, modulos[], periodicidad, setup, recurrencia, mandatos:[{ id, titular, iban, tipo_pago }], caduca_at }. El IBAN va enmascarado (ES12 •••• 1234); nunca datos de otros locales. Otros casos: { found:false } · { found:true, estado:'cancelado'|'enviada'|'caducada' }. |
/escalado-local/responder | { t, datos, sol_campo_x9 } | { ok:true } y el escalado pasa a pendiente (con rellenado_at) y se disparan los avisos de WF-143. Ya enviado antes: { ok:true, ya_enviada:true }. Errores: { ok:false, error } con error ∈ token_invalido · cancelada · caducada · validacion (con campos[]) · enlace_invalido · peticion_no_valida · demasiado_grande · error_bd. |
datos
{ local: { nombre, email?, telefono?, calle, numero?, cp, municipio, provincia },
contacto: { nombre, puesto?, email, telefono },
tpv: { nombre, no_integrado?, contacto?, email? },
sepa: { modo:'existente', mandato_id }
| { modo:'nuevo', iban, titular, bic?, cif, denominacion, firma } }
sol_campo_x9 es el honeypot: si viene relleno se responde ok sin tocar nada. Lo comercial (módulos, importes, periodicidad) lo fija el staff al solicitar y el cliente solo lo ve.📒 Contabilidad
Workflow 13-grabado-a3. Toggle del flag grabado_a3 en una ficha. Trigger SQL setea grabado_a3_at al pasar de FALSE a TRUE.
Body
{ "id": "uuid", "grabado_a3": true | false }
Respuesta 200
{
"success": true,
"id": "uuid",
"grabado_a3": true,
"affected": 1,
"errores": []
}
Workflow 39. Lectura de Azure SQL (yurestazure / Info_PowerBI). La página pinta una tabla genérica con auto-detección de columnas; la query se ajusta en n8n (la página no envía SQL).
Respuesta 200
{ "ok": true, "total": 120, "columnas": [...], "rows": [...], "generated_at": "ISO" }
Workflow WF137 (14-oct-2026). Cruce de lo facturado en A3 con el portal, HighLevel y producto (cruce-a3.html, permiso cruce_a3). Devuelve la última foto guardada en a3_cruce_snapshot: → { ok, generado_at, corte, datos }, o { ok, datos:null } si aún no hay.
scripts/a3-cruce/. El endpoint solo lee.Workflow WF102. Facturas emitidas al cliente, que este ve en su Espacio Cliente. El PDF vive en un bucket privado: el listado nunca devuelve storage_path.
| Path | Método | Uso |
|---|---|---|
/facturas | GET | → { facturas:[…] }. Listado para facturas.html. |
/factura | POST | { ficha_id, numero, fecha_emision, concepto, importe_total, sociedad, subido_por, data } — data es el PDF en data-URL base64. Sube el objeto al bucket e inserta la fila; si el INSERT falla, el workflow compensa borrando el objeto subido. |
/factura/borrar | POST | { id }. |
Workflow WF99. Facturas que los distribuidores emiten a Yurest por sus comisiones. Viven en la BD del portal de distribuidores (schema distribuidores), no en la de yurest: WF99 es el único que las toca, con la service key guardada en n8n.
| Path | Método | Uso |
|---|---|---|
/facturas-distribuidor | GET | → { facturas:[…] }. Alimenta también la alerta del home y el badge del sidebar. |
/facturas-distribuidor/pdf | POST | { id } → { url }. Bucket privado: enlace firmado de 60 s, la página nunca recibe la ruta de storage. |
/facturas-distribuidor/revisar | POST | { id, accion, motivo } con accion ∈ aceptar|rechazar|pagar. Al pagar responde además { comisiones_marcadas }. |
enviada → aceptada → pagada, con salida rechazada (el distribuidor corrige y sube otra del periodo). Moverlo es responsabilidad exclusiva de contabilidad; el distribuidor solo puede crear y retirar mientras está enviada.📁 Proyectos
Workflow 01-proyectos-crud. CRUD de proyectos de implementación.
| Método | Body | Respuesta |
|---|---|---|
| GET | — | { proyectos: [...] } |
| POST | { proyecto: {...} } | { data: {...}, success: true } |
| PUT | { proyecto: { id, ...campos } } | id devuelto |
| DELETE | { id: "uuid" } | Soft-delete |
El POST/PUT acepta asanaProjectId y asanaProjectUrl para vincular un tablero Asana.
Qué trae cada proyecto en el GET
El GET devuelve los proyectos vivos (los borrados se filtran en la BD desde el 24-oct-2026) y acepta ?id=<uuid> para leer uno solo. Cada fila, completa: id, cliente, implementador, tipo (el «plan»: Basic / Pro / Corporate), estado, fecha_inicio, ultima_actividad, tpv, motivo_pausa, plan_accion, asana_project_id, asana_project_url, promocion_id, promocion_turno, ficha_id, origen_secciones, meta_updated_at y las marcas de tiempo. En JSONB: secciones, orden_secciones, anotaciones (bitácora), contactos, participantes, adjuntos y formularios.
Cada objeto de secciones puede llevar además escalado_id y escalado_fecha: el sello que pone WF26 a las secciones que nacen al aplicar un escalado. Su ausencia significa «implementación inicial». Es lo que separa las vistas «Implementación / Escalado N» del tab Proyecto; el progreso y los KPIs no cambian, las secciones de escalado siguen contando en el total.
El detalle de sesiones con sus fechas vive dentro de secciones[].tareas[]: nombre, completada, show (Show / No Show / null), fechaEntrega, tiempoEstimado, notas y subtareas[].
contactos es una copia de la lista única de contactos del cliente (cs_contactos_cliente) que mantiene un trigger. Un PUT de proyecto ya no lo escribe y lo ignora: los contactos se cambian con /cs/accion (contacto_cliente_*). Cada tarea puede traer además bloqueada, modulo, noAplicaManual («No aplica» puesto a mano en una sesión de promoción: no se le convoca), googleEventId y eventoCompartido.formularios.cierre_fase es una lista con un cierre por módulo (clave: el nombre de la sección, sin acentos ni mayúsculas): { seccion, fecha_cierre, periodo, sesiones[], asistentes[], criterios[6 booleanos], incidencias:[{ descripcion, impacto:'Bajo'|'Medio'|'Alto', accion }], recomendacion:{ titulo, texto }|null, decision:'avanzar'|'correctivas'|'no_avanzar', decision_texto, proximas_fases, creado_por, fecha, editado_por?, editado_at? }. Se guarda con el PUT de este endpoint (formularios completo): quien reescriba formularios debe conservar la clave, o el siguiente PUT la borra.obtenerResumenProyecto en js-gestor/data.js) y cocina central (sale de la ficha del cliente, del flag es_cocina_central de sus locales).ultima_actividad vs ultima_actividad_efectiva
La columna ultima_actividad solo la escriben este workflow (alta y PUT) y WF02 al guardar o mover una tarea. No la tocan el panel de sesiones (WF52, donde se marcan Show / No Show), el espejo de Asana (WF81) ni el importador de tareas de promoción (WF83), así que se queda vieja en cuanto las sesiones se marcan fuera del gestor.
Por eso el GET añade ultima_actividad_efectiva: el máximo entre esa columna y la fecha de la última sesión realmente dada (tarea o subtarea completada o marcada Show / No Show, con fechaEntrega en pasado; agendar a futuro no cuenta). Se calculan sobre las mismas secciones que el progreso: fuera las administrativas, Hardware, «Módulos terminados de implementar» y las marcadas descontratado.
ultima_actividad_efectiva. La columna cruda se sigue devolviendo intacta y no cambia de comportamiento. Banco de pruebas: tests/wf01-ultima-actividad.test.js.Token de solo lectura (consultas automatizadas)
El GET de este endpoint —y solo el GET— acepta además un token de solo lectura, pensado para informes que corren sin que nadie tenga la sesión abierta en el navegador. Va en la misma cabecera que la sesión de staff:
curl -H "X-Session-Token: ro|informe-semanal|1788000000000.<hmac>" \
{BASE_URL}/proyectos
Se firma con un secreto distinto del de las sesiones. Ese es justo el mecanismo de seguridad: para los demás webhooks el token es una firma inválida, así que devuelven 401 sin que haya hecho falta cambiar nada en ellos. Un token de lectura filtrado no abre escritura ni ningún otro endpoint.
| Se pide a | Administración de sistemas. Se emite con scripts/mint-token-lectura.js. |
|---|---|
| Caducidad | La que se fije al emitirlo (365 días por defecto). Va dentro del token; al caducar, el endpoint responde 401. |
| Revocación | No es individual: se rota el secreto de lectura y caducan todos a la vez. Las sesiones de staff no se ven afectadas. |
| Alcance | GET /webhook/proyectos con cualquier token de lectura. GET de fichas (WF04, /webhook/018f3362-…) y GET /cs (Cascada CS, WF-122) solo con los tokens del conector de Claude (etiqueta mcp-portal): devuelven la ficha completa o el detalle CS y el conector los filtra antes de responder. Igual GET integraciones-tpv/estado (WF87), carga-datos/estado (WF-98) e incidencias-dev/estado (WF85): solo sesión de staff o mcp-portal. carga-datos/ficha y /comentarios siguen siendo solo de sesión. Cualquier otro método o ruta → 401. |
n8n-auth-gate/README.md. Banco de pruebas: tests/token-lectura.test.js.mcp-portal/): OAuth con el login del portal (auth/login), permiso api_claude comprobado en vivo con auth/verify y lectura con un token ro|mcp-portal… que nunca sale del Worker. Herramientas: buscar_proyectos, ver_proyecto, buscar_fichas, ver_ficha, cascada_cs, ver_cliente_cs. Lista blanca de campos: sin IBAN, mandatos SEPA, credenciales ni DNI. Banco: tests/mcp-portal.test.mjs.Workflow 02-proyectos-tareas. Actualiza una tarea/sección dentro de un proyecto (toggle completada, edición, etc.).
Body
{
"proyectoId": "uuid",
"seccionNombre": "Puesta en Marcha / Finalización",
"tarea": { "id": "...", "completada": true, "_v": "versión que se cargó", ... },
"comprobarVersion": true
}
Respuesta 200
{ "success": true, "version": "2026-10-24T17:46:13.544670Z" }
Respuesta 409 (otra persona guardó la tarea después)
{ "success": false, "code": "conflict", "error": "Otro usuario guardó esta tarea…", "version": "versión vigente" }
2026-10-24_01): el RPC sella cada tarea con _v. Con comprobarVersion:true (lo manda el gestor) rechaza con 409 (SQLSTATE PT409) la tarea que no lleva la versión vigente. Quien no lo pide (WF52, WF77, WF99) guarda sin comprobar, pero su guardado sí cambia la versión, así que una pestaña vieja del gestor ya no lo pisa. El gestor encola los guardados de una misma tarea y toma la versión nueva de cada respuesta.proyecto_upsert_tarea, único camino que no reescribe secciones entero. Acepta p_escalado_id / p_escalado_fecha opcionales y los aplica solo en la rama que crea la sección, para que el sello de escalado sobreviva (migración 2026-09-05_02_upsert_tarea_sello_escalado.sql). Sin esa migración el sello se perdía en silencio.GET /proyectos) justo antes de escribir; así lo hace el auto-resumen de sesiones desde el 13-oct-2026.Mover una tarea entre secciones (drag & drop kanban) o eliminarla.
Body — mover
{ "proyectoId": "uuid", "tareaId": "...", "seccionOrigen": "...", "seccionDestino": "..." }
Hilo cliente ↔ staff e historial del proyecto (WF01).
| Path | Método | Uso |
|---|---|---|
/proyectos/mensajes | GET ?proyectoId= | → { ok, mensajes:[…] } (asc). El mismo hilo que ve el cliente en espacio-cliente.html. |
/proyectos/mensajes | POST | { proyectoId, texto } (máx 4000). El backend fija autor_tipo='staff' y el nombre a partir del token. |
/proyectos/mensajes-pendientes | GET | Agregado que alimenta la alerta mensaje_cliente_pendiente del home: una fila por proyecto cuyo hilo termina en mensaje del cliente — { proyecto_id, nombre, implementador, pendientes, desde }. No devuelve el texto de los mensajes. |
/proyectos/historial | GET | Timeline de acciones del proyecto. |
Acciones puntuales sobre un proyecto que no reescriben el proyecto entero.
| Path | Método | Body → Respuesta |
|---|---|---|
/proyectos/jefe-proyecto | POST | WF01. { proyectoId, jp:{ nombre, apellidos, rol, mail, telefono } } → { ok, ficha_id, jp }. Solo toca fichas_alta.jp_*. |
/proyectos/preferencia-turno | POST | WF01. { proyectoId, preferencia:'manana'|'tarde'|'indiferente'|null } → { ok, ficha_id, preferencia_turno }. |
/proyectos/onboarding-reactivar | POST | WF01. { proyectoId } → { ok, ficha_id, estado } o { ok:false, error }. Saca de la pausa por no-shows un onboarding grupal. |
/proyectos/adjuntos | POST | WF01. { accion:'subir', proyecto_id, nombre, data } → { ok, adjunto } · { accion:'ver', proyecto_id, adjunto_id } → { ok, url } (firmada, 60 s) · { accion:'borrar', proyecto_id, adjunto_id } → { ok }. El binario vive en un bucket privado; el proyecto solo guarda { id, nombre, tipo, size, fecha, storage_path }. |
/proyectos/anotaciones | PUT | WF02 → RPC proyecto_anotaciones_op (24-oct-2026). { proyectoId, op:'crear'|'editar'|'borrar', entrada }: aplica ese cambio de UNA entrada sobre la bitácora del servidor, bajo bloqueo, y devuelve { success, anotaciones } completa. editar conserva autor y fecha de creación; editar una entrada ya borrada → 409. La forma antigua { proyectoId, anotaciones:[…] } se fusiona (gana el cliente por id, se conservan las del servidor que no trae). La bitácora ya no viaja en el PUT de /proyectos. |
/proyectos/carga-datos/marcar | POST | WF-127. { ficha_id } → { ok, estado:'en_curso', marcada_at } o { ok, duplicado:true, marcada_at }. El implementador marca «Datos cargados»: crea la tarea de revisión para el equipo de carga de datos en Asana y marca la ficha. Errores: 400 ficha_invalida · 403 sin_permiso · 404 no_ficha · 409 sin_carpeta · 502 asana (no se marca nada; se puede reintentar). |
Pestaña «Comunicaciones automáticas» de Proyectos y de la Cascada CS: lo que el portal ha mandado al cliente (tabla comunicaciones_cliente).
| Path | Método | Uso |
|---|---|---|
/proyectos/comunicaciones | GET ?proyecto_id= o ?ficha_id= (exactamente uno) | WF-128. → { ok, comunicaciones:[{ id, canal:'email'|'calendar'|'drive'|'zendesk', tipo, destinatarios[], asunto, origen, estado, detalle, ref, enviado_at, aproximado, … }], generado_at }. En fichas individuales, las invitaciones, reprogramaciones y cancelaciones de Calendar salen de las tareas del proyecto con googleEventId, sea cual sea la sección (14-oct-2026); el «Reenviar convocatoria» de una sola sesión llega con tipo:'convocatoria_individual_sesion'. |
/proyectos/comunicaciones/reenviar | POST | WF-134. { comunicacion_id, destinatarios:[email…] } (1-10). Busca el correo original en los Enviados de los buzones del portal y lo reenvía tal cual (mismo HTML y adjuntos) desde el buzón de implementación. → { ok:true, enviado_a, asunto }. |
/proyectos/comunicaciones/reenviar | POST modo:'ver' | WF-134 (16-oct-2026). { comunicacion_id, modo:'ver' }. Misma búsqueda, pero no envía nada: devuelve el original leído → { ok:true, asunto, de, para, cc, fecha, html|null, texto|null, adjuntos:[{ nombre, tipo, bytes }] }. Las imágenes cid: van incrustadas en html como data: (hasta 3 MB). Permiso: admin, o lectura en proyectos o cs_kanban. |
{ ok:false, code } (sin_permiso, no_existe, canal_no_reenviable, no_se_envio, sin_destino_original, no_encontrado, ambiguo, gmail_lectura, gmail_envio), porque un 403 cerraría la sesión. Petición mal formada → 400. Calendar, Drive y Zendesk no se reenvían. Permiso para reenviar: admin, o escritura en proyectos o cs_kanban; para ver basta lectura. El portal pinta el html solo dentro de un iframe sandbox sin scripts.WF-139. Menciones «@usuario» en las anotaciones (bitácora) de Proyectos y de la Cascada CS, y su aviso en el panel de alertas del escritorio. La regla vive en SQL (menciones_accion, tabla menciones, migración 2026-10-15_02): la bitácora es la misma en proyectos y cs_kanban, así que se puede mencionar a quien entra (cualquier nivel) en cualquiera de las dos, y a los comerciales (usuarios activos con rol Consultor) aunque no entren en ninguna (16 y 20-oct-2026); admin entra en todas. Quien escribe sí necesita acceso a la página. La mención se guarda con la página por la que entra el mencionado. La entrada guarda menciones:[{ id, nombre }] en proyectos.anotaciones.
| Petición | Método | Uso |
|---|---|---|
?accion=usuarios&pagina=proyectos|cs_kanban | GET | A quién puedo mencionar: usuarios activos con acceso a la página, sin uno mismo. → { ok, usuarios:[{ id, nombre }] }. Quien pregunta debe tener acceso a la página. |
?accion=bitacora&proyecto_id= | GET | Lectura de la bitácora para quien fue mencionado en ese proyecto (el comercial no tiene permiso de la página). → { ok, proyecto:{ id, cliente, ficha_id, anotaciones } }; sin mención en el proyecto → { ok:false, code:'sin_permiso' }. Solo lectura. |
?accion=mias | GET | Mis menciones sin leer (máx. 200), solo de páginas a las que sigo teniendo acceso (el comercial las ve todas; cada una lleva acceso para que el front abra la página o la lectura en el home). → { ok, menciones:[{ id, pagina, proyecto_id, ficha_id, cliente, entrada_id, autor, extracto, created_at }] }. |
{ accion:'entrada', pagina, proyecto_id, entrada_id, extracto, usuarios:[uuid…] } | POST | Deja las menciones de esa entrada como la lista (máx. 30): alta de las nuevas, baja de las quitadas; [] = entrada borrada. Las que siguen no vuelven a avisar (solo se actualiza el extracto, 280 caracteres). → { ok, creadas:[uuid], rechazadas:[uuid] } (rechazadas = sin acceso, inactivo o uno mismo). |
{ accion:'leer', proyecto_id } · { ids:[…] } · { todas:true } | POST | Marca leídas mis menciones (nunca las de otro). Lo hace el front al abrir las anotaciones de un proyecto. → { ok, leidas }. |
{ ok:false, code } (sin_sesion, sin_permiso, pagina_no_valida, proyecto_no_existe, entrada_de_otro_proyecto, peticion_no_valida, error_bd), porque un 403 cerraría la sesión. Petición mal formada (acción que no toca a ese método, ids que no son uuid…) → 400 con code. Sin token → 401.Workflow WF-101. Accesos multiusuario al Espacio Cliente de un proyecto. Solo staff.
| Método | Entrada | Salida |
|---|---|---|
| GET | ?proyectoId=<uuid> | { ok, accesos:[…] } |
| POST | { proyectoId, nombre, email } | { ok, acceso, enlace } — alta y enlace de alta de contraseña |
| POST | { accesoId, op:'reenviar' } | { ok, enlace } |
| POST | { accesoId, op:'revocar' } | { ok } |
pass_hash ni set_token: el enlace solo viaja en la respuesta de la acción que acaba de generarlo.Proxy e importadores de Asana (las credenciales viven solo en n8n).
| Path | Uso |
|---|---|
/asana/tasks?projectId= · /asana/task/stories?taskId= | WF07. Proxy de lectura: tareas de un proyecto e historias (comentarios) de una tarea. |
/proyectos/asana-import | WF81. Importa proyectos arrancados en Asana. POST { op:'tasks'|'detalles' }. |
/asana/project-sections | En desuso: el portal no lo llama desde el 8-jul-2026. Listaba las secciones de un proyecto (pestaña Formación grupal). |
/importador/buscar-drive · /importador/buscar-asana | WF88. Buscadores del importador de clientes unificado. POST { q } → { ok, items }: Sheets «Ficha Cliente» ({ title, url }) y proyectos de Asana ({ name, gid, url }). |
🎓 Promociones
Tandas de implementación (Customer Success): 16 plazas (8 mañana + 8 tarde). Doble proyecto Asana por promoción.
| Path | Uso |
|---|---|
/promociones | Vista con la ocupación ya calculada. La creación es manual. |
/promociones/asana?project=<gid> | WF56. Lee nombre + secciones en vivo → { ok, gid, nombre, total, ocupados, libres }. |
/promociones/asana/ocupar | En desuso: el portal no lo llama desde el 16-sep-2026 (la promoción la asigna el implementador desde Proyectos). WF63. { project_gid, title } → renombra la 1ª sección libre → { ok, section_gid?, nuevo_titulo?, libres_restantes? }. |
/promociones/asana-import-tareas | Retirado. WF83 importaba las tareas del Asana de la promo; lo sustituye /promociones/sesiones (WF-109) y el front ya no lo llama. |
Sesiones de las promociones grupales y su convocatoria en Google Calendar.
| Path | Uso |
|---|---|
/promociones/sesiones | WF-109. { promocion_id, turno } → reescribe la sección «Sesiones de la promoción» de cada proyecto de la tanda con la plantilla fija del cronograma: fechas rotadas a su semana de arranque y las sesiones de módulos no contratados marcadas bloqueada. Idempotente: conserva lo ya impartido y el googleEventId de lo ya agendado. |
/promociones/convocatorias | WF-113. { op, promocion_id, … } con op ∈ preview · convocar · reprogramar { convocatoria_id, inicio } · cancelar { convocatoria_id, motivo } · reenviar { proyecto_id, emails[1-10] } · ajustar { clave_sesion, turno:'manana'|'tarde'|'*', inicio } · fechas { promocion_ids[1-60] }. reenviar convoca como siempre y vuelve a mandar la bienvenida con su calendario a esos correos, aunque ya la tuvieran. Todo op salvo preview y fechas exige escritura en Promociones, Proyectos o Cascada CS (o admin): si no, { ok:false, error:'Sin permiso…' } (24-oct-2026; antes bastaba cualquier sesión). Lo mismo en /promociones/recordatorios y /promociones/cierre (code:'forbidden'). Se invita a todos los contactos del cliente con convocar = true; una sesión marcada «No aplica» no se convoca. Guarda en promociones_convocatorias. → { ok, creadas, actualizadas, movidas, bienvenidas:{ enviadas, fallidas }, proyectos:{ actualizados, conflictos, sinSeccion }, avisos[], omitidas[], erroresObsoletos[], … } o { ok:false, error }. |
pr y WF07 rechaza tocarlos (^(dev-sim-)?pr[0-9a-f]{32}$), así que la tarea con eventoCompartido no se reagenda ni se cancela por separado.ajustar fija a mano la fecha/hora (Madrid, YYYY-MM-DDTHH:mm) de una sesión del cronograma de una promoción que no ha arrancado, tenga o no clientes; va por sesión y turno, no por convocatoria. Se guarda en su fila con inicio_manual y manda sobre la plantilla: con clientes se convoca ya en esa fecha, sin ellos los que entren después se convocan en ella. Si la sesión ya está en Google hay que usar reprogramar; no admite pasado ni festivos. fechas es solo lectura para el Calendario de promociones: → { ok, op:'fechas', sesiones:[{ promocion_id, clave_sesion, turno, inicio, convocada, manual }] } (sin las canceladas).cancelar borra el evento y además deja esa sesión como «No aplica» (manual y bloqueada) en los proyectos invitados que siguen en la promoción, igual que el botón del gestor. No toca a los que salieron de la promo ni tareas con asistencia marcada o completadas.@, comas, texto suelto; los caracteres invisibles pegados se limpian antes) no se invitan y salen en avisos como { tipo:'email_invalido', email, … } para corregirlos. erroresObsoletos: ids de convocatorias que arrastraban el error de un intento anterior pero ya están bien; al convocar se limpia su ultimo_error.preview, convocar y reenviar se comprueba que cada convocatoria futura sigue viva en Calendar. Si se borró, la fila llega con borrada_google: true, no se reconvoca sola (va a omitidas con motivo:'borrada_google') y hay un aviso borrada_google: reprogramar a la misma fecha la recupera y cancelar la da de baja.convocar). Aplica a las promociones que arrancan a partir del 2-nov-2026; las anteriores siguen por HighLevel. Los recordatorios (WF-114, /promociones/recordatorios) tienen el cron apagado.📅 Sesiones y calendario
Workflow 07-calendar-asana. Google Calendar del implementador.
| Path | Uso |
|---|---|
/calendar/event (POST) | Crea/actualiza un evento (sesión de implementación). { summary, start, end, attendees[] }. El reagendado actualiza por googleEventId. |
/calendar/disponibilidad | Lee el calendario en un rango para detectar solapes al agendar. |
Panel de sesiones de implementación (panel-sesiones.html; workflows WF52 y WF53 para los dashboards). Ojo: estas rutas validan el token de sesión en Authorization: Bearer <token>, no en X-Session-Token. Las rutas con id usan query param (/api/tasks/<gid> → /panel/tasks?gid=).
| Path | Uso |
|---|---|
GET /panel/projects?key= · /panel/tasks?gid= · /panel/notes?docId= | Proyectos de Asana del implementador, sus tareas y las notas de Gemini de una sesión. |
GET /panel/recent-sessions?person=[&desde=&hasta=] | Sesiones recientes. Con desde/hasta (ISO, máx. 35 días; 20-oct-2026) trae esa ventana: una semana del mes o el mes entero; sin ellos, la ventana de siempre según la persona. Cada una trae resumenEnviado / resumenEnviadoAt (✅ resumen enviado) y resumenGuardado / resumenGuardadoAt (💾 guardado sin email). Los invitados van por email. |
POST /panel/summarize · /panel/sesion-resumen | Resumen IA. sesion-resumen: { proyectoId, seccionNombre, tareaId, implementador, cliente, fecha, titulo, googleEventId } → { encontrada, show, duracionMin, resumen }. |
PUT /panel/asana-task[?gid=] | Guardar: { implementador, destino:'asana'|'portal', proyectoId?, tareaId?, resumen, youtubeUrl, asistencia, duracion, fecha, titulo, resumenCliente, sesionId }. Sin gid solo escribe en el proyecto del portal. Deja la sesión marcada como guardada. |
POST /panel/enviar | Email real por Gmail: { implementador, correosInvitados, correosOculta, titulo, html, asunto?, sesionId }. Deja la sesión marcada como enviada. |
POST /panel/drive-find-video · /drive-share-emails · /drive-share-multiple · /drive-unshare · GET /panel/cliente-recordings?gid= | Grabaciones de Drive: localizar, compartir con invitados y retirar el acceso. |
GET /panel/dashboard-general?implementador= · /panel/cliente?gid=&desde=&hasta= · /panel/proyectos-impl?key= | WF53 (24-oct-2026: lee el portal, ya no Asana; con Auth Gate, antes era público). implementador/key = clave del catálogo (carlos, mario, hugo, javier); gid = id del proyecto en el portal. Lógica en database/n8n-workflows/53-panel-dashboards.portal.js. proyectos-impl → [{ gid, name }] (activos y pausados, por nombre comercial). cliente-recordings acepta además nombre. |
Workflow WF-99. Sesiones privadas agendadas por nosotros (clientes.html → Formación). Mismo motor de huecos que el Espacio Cliente, pero con sesión de staff y sin veto por cuota: aquí la cuota se enseña y decide quien agenda.
| Path | Método | Uso |
|---|---|---|
/staff/privadas | GET ?proyecto_id= | → { ok, … } con los huecos libres en la agenda del implementador del proyecto y la cuota consumida. |
/staff/privadas/reservar | POST | { proyecto_id, inicio } → reserva el hueco y crea el evento. |
Workflow WF-138 (15-oct-2026). El cliente grupal agenda sus dos sesiones privadas de la promoción («Sesión Privada de Seguimiento», 30 min, y «Sesión Privada», 1 h) con el implementador de su promoción, desde agendar-sesion.html?t=<token> (enlace de los correos de la promoción). Sin sesión: lo autoriza el token del proyecto (tabla agenda_privadas_tokens). Qué está pendiente, la caducidad y la escritura en el proyecto viven en SQL (agenda_privadas_estado() / agenda_privadas_reservar(), migración 2026-10-15_02).
| Path | Body | Respuesta |
|---|---|---|
/agenda-privadas/estado | { t } | { ok, sesiones:[…], huecos:{ <clave>:{ dias } } }: cada privada pendiente, agendada o hecha y, para las pendientes, los huecos libres del implementador (≥ 48 h, L–V 9–18, sin festivos nacionales, hasta el plazo del cierre). |
/agenda-privadas/reservar | { t, clave, inicio } | { ok, sesion }: crea el evento de Google y escribe la fecha en la tarea de la plantilla del proyecto. Errores { ok:false, error } con error ∈ enlace_invalido · caducada · no_aplica · sesion_invalida · ya_agendada · antelacion · fuera_de_plazo · sin_evento · calendario_no_disponible · calendario_rechazo. |
aviso_agendar (columna calculada de PostgREST en proyectos y fichas) para pintar «Tienes pendiente agendar…».Workflow WF-100. Participación en sesiones por promoción (participacion.html).
| Path | Método | Uso |
|---|---|---|
/sesiones/participacion | GET | Agrega en vivo desde sesiones: asistencia por promoción y turno. |
/sesiones/participacion?solo=reenganche | GET | Solo el re enganche: clientes que han pasado a otra promoción, con la inicial (promocion_inicial_id), las intermedias, la actual, el número de cambios y la fecha del último. Sale de fichas_historial (acción promocion_cambiada, registrada desde el 17-sep-2026). Un cambio de turno aparece en el recorrido pero no cuenta como cambio. Lo usan la pestaña Re enganche de participacion.html y la Cascada CS (carga en segundo plano con skipAuthLogout). |
/sesiones/asistencia | POST | { marcas } — marcado en bloque. Escribe la marca en el árbol del proyecto y el trigger la proyecta de vuelta. Respuesta con { detalle } si alguna marca falla. |
El implementador marca Show / No Show a mano. «Guardar y enviar» pide la vista previa del correo (WF-130) y, si se confirma, WF96 lo envía; «Guardar» a secas no llama aquí. Los dos reciben el mismo body.
Body
{ "ficha_id": "uuid", "tarea_id": "...", "motivo": "..." | null, "nota": "..." | null,
"recupera": { "fecha": "YYYY-MM-DD", "hora": "HH:mm" | null } } // opcional: se recupera en otra sesión ya agendada
Respuesta de /preview (no envía ni escribe nada)
{ "ok": true,
"correos": [{ "destino": "jp@cliente.es", "cc": "...", "asunto": "...", "html": "..." }],
"otros": [{ "tipo": "noshow_3_pausa" } | { "tipo": "reasignacion_promo", "fecha_destino": "YYYY-MM-DD", "turno_destino": "manana|tarde" }],
"motivo": "ya_enviado | sin_ausencias | sin_ficha" } // solo cuando no hay correo
Sesión de bienvenida grupal con la CSM, reservada por el cliente desde solicitud.html o bienvenida.html?t=. Un evento de Google por hueco. Cupos y antelación (36 h) viven en SQL; un cron cada 15 min revisa rechazos y cancelaciones en Google, manda el recordatorio de 24 h y el correo de no-show, y reenvía el Meet si cambia.
| Path | Auth | Uso |
|---|---|---|
/bienvenida/huecos | Público token de solicitud | WF-124. { t } → { ok, huecos:[{ inicio, fin, libres }], reserva }. |
/bienvenida/reservar | Público token de solicitud | WF-124. { t, inicio, email, nombre, cliente, aparte? } → { ok, reserva, meet, calendario_ok } (reserva o reagenda; calendario_ok:false = la reserva se guardó pero Google falló). Error: { ok:false, error } con error ∈ completo | antelacion | hueco_invalido | enlace_invalido | email_invalido. aparte (64 hex) es el permiso de un solo uso que da bienvenida-estado para una sesión aparte: con él se salta huecos, antelación de 36 h y cupo, y se revalida la hora (errores aparte_invalido y los de la sesión aparte, abajo). |
/proyectos/bienvenida-estado | X-Session-Token | WF01. { proyectoId, estado, inicio? } con estado ∈ reservada | asistio | no_show | cancelada | enlace | aparte | enviar_enlace → { ok, reserva }. cancelada: el equipo la cancela y el cliente recibe «elige otra fecha». enlace: página de reserva para que el equipo elija hueco por el cliente (→ { ok, enlace }). aparte (13-oct-2026) con inicio (ISO): la CSM da al cliente una hora fuera de los huecos → { ok, aparte, t, inicio, email, nombre, cliente }; el portal reserva después en /bienvenida/reservar con ese aparte (permiso de 15 min, un solo uso). enviar_enlace (20-oct-2026): encola en bienvenida_enlaces el correo «Reserva tu sesión de bienvenida» al JP de la ficha (lo manda el cron de WF-124; si la ficha grupal no tiene solicitud, crea la puente) → { ok, email, enlace, pedido_at }. Errores: aun_no_ha_sido, sin_reserva, sin_enlace, ya_asistio, ya_reservada, sin_email, estado_invalido. |
fecha_invalida | antelacion_aparte | demasiado_lejos | dia_aparte | hora_aparte | choca_hueco | hora_ocupada. Tiene su propio evento de Google y no ocupa plaza de ningún hueco ni del cupo semanal.cancelada sin motivo fuera de un reagendado se trata como cancelada por el equipo: el cliente recibe «elige otra fecha» (7-oct-2026).🗂️ Implementación
Workflows WF-97 (rebuild) y WF-98 (lectura). Estado de la carga de datos de los clientes. Mismo par que los snapshots de Soporte: un cron reconstruye, el GET sirve lo último guardado.
| Path | Método | Uso |
|---|---|---|
/carga-datos/estado | GET | Último snapshot (cron 06:15). Solo lleva los pendientes. |
/carga-datos/rebuild | POST | WF-97. Reconstruye el snapshot consultando Asana y Drive. |
/carga-datos/ficha | GET ?ficha_id=<uuid> | Estado de un cliente, en vivo (clientes.html → Ver → Consultor). No sale del snapshot: lee la ficha y, si hay tarea, pregunta a Asana. → { ok, estado, carpeta, carpetaUrl, marcadaAt, marcadaPor, completadaAt, gid, asanaUrl, onboardingEstado }. |
/carga-datos/comentarios | GET ?ficha_ids=<uuid>,… (máx. 100) | WF-98 (15-oct-2026). Comentarios de la tarea de carga de datos en Asana de varias fichas a la vez (Proyectos › Grupales: pop-up del último y modal con todos). → { ok, fichas:{ <ficha_id>:{ gid, asanaUrl, total, comentarios:[{ autor, texto, at }], error? } } }, el más reciente primero; solo vienen las fichas con tarea. |
/carga-datos/confirmar-email | POST Público | WF-116. { f: <ficha_id>, s: <firma 64 hex> } desde documentos-subidos.html, el botón del correo de la carpeta. Sin sesión: lo protege la firma del enlace. → { ok, duplicado? } o { ok:false, code } con code ∈ enlace_invalido | sin_carpeta | no_ficha; 502 asana (no se marca nada; se puede reintentar) · 502 guardar + gid (la tarea ya existe y la ficha no quedó marcada: la ejecución queda en error para ponerlo a mano; no reintentar). |
estado ∈ sin_carpeta | pendiente | en_curso | hecha | desconocido — el mismo vocabulario que la pestaña Documentos del Espacio Cliente (WF77).Workflow WF-104. Informe semanal de seguimiento de implementaciones. Calcula en servidor lo mismo que manda el correo de los lunes a Operaciones, así que la pantalla (informe-seguimiento.html) y el correo no pueden divergir. Sin parámetros.
js/lib/informe-seguimiento.js.Workflow «112 - Checklists (IA + API Yurest)». Backend del Configurador de checklists (checklists.html, Implementación → Configuradores).
| Path | Uso |
|---|---|
/checklists/ia | { prompt } → gpt-4o (JSON) → { ok, texto } o { ok:false, error }. La clave de la IA no sale del servidor. |
/checklists/yurest | { entorno:'prod'|'dev', token, recurso:'roles'|'stores'|'products'|'providers'|'formats' } (products, providers y formats: Configurador de recetas; formats lee la tabla formatos_compra de la BD de producto) → proxy acotado a la API de Yurest (bases fijas, sin SSRF). Existe porque cliente.yurest.com/ws no manda CORS. |
{ ok, status }, también cuando Yurest devuelve 403: un 403 leído por apiFetch cerraría la sesión del portal.Workflow «140 - Configurador recetas (IA)» (lvfFVRT3q8Vgn134). IA del Configurador de recetas (configurador-recetas.html, Implementación → Configuradores), que llegó como artifact de Claude: sustituye a window.claude.use('sample'). Credencial «Anthropic API (bot ayuda)».
| Campo | Uso |
|---|---|
prompt | Texto del configurador (≤ 60 000 caracteres). |
images | Opcional: [{ media_type:'image/jpeg', data:<base64> }], hasta 10 fotos de la carta (el navegador las baja a JPEG de 1568 px). |
tier | 'default' → claude-sonnet-5-5 (leer la carta) · 'quick' → claude-haiku-5-5 (ingredientes de un elaborado, IVA, familias). |
{ ok:true, texto, cortado } o { ok:false, code }; code usa los códigos del runtime de Claude (rate_limited, prompt_too_large, image_rejected, upstream_error…).WF-145 «Recetas Historial». Copias en el servidor del trabajo del Configurador de recetas (el mismo JSON que guarda en localStorage), para rescatar o consultar las fichas de un cliente desde cualquier equipo. La regla vive en SQL (recetas_historial_accion, tabla recetas_historial, migración 2026-10-20_03): entra quien tiene el permiso configurador_recetas (o admin) y el historial es de todo el equipo. El front guarda solo al exportar, antes de «Nuevo cliente» y antes de restaurar otra copia.
| Petición | Método | Qué hace |
|---|---|---|
?accion=lista[&q=texto] | GET | Copias (máx. 300, la más nueva primero; q busca en el cliente) sin el estado. → { ok, copias:[{ id, cliente, motivo, resumen:{ fichas, revisadas, elaborados, productos, fuente_productos }, creado, autor }] }. |
?accion=ver&id=uuid | GET | Una copia con su estado. → { ok, copia } o { ok:false, code:'no_existe' }. |
{ accion:'guardar', motivo, estado } | POST | motivo: exportar · nuevo_cliente · restaurar · manual. estado con fichas[] y products[] (máx. 8 MB). Sin fichas no guarda (code:'vacio'); un estado idéntico a otro ya guardado no se duplica. → { ok, id, duplicada }. |
{ accion:'eliminar', id } | POST | Borra esa copia (migración 2026-10-21_02), mismo permiso que el resto. → { ok, id } o { ok:false, code:'no_existe' }. |
sin_permiso, vacio, no_existe) con 200 y { ok:false, code }; petición mal formada 400; sin sesión 401.🖥️ Hardware
| Path | Uso |
|---|---|
/hardware/pedidos | WF21. Pedidos por proyecto: ciclo solicitada → proforma → pago → lista_envío. Consumido desde proyecto.Hardware, contabilidad.Proformas y Soporte envíos. POST { action, pedido:{ id, … } } con action ∈ create, create_tablet, actualizar_items, generar_proforma, editar_proforma, guardar_proforma_pdf, adjuntar_proforma, enviar_proforma_email, adjuntar_justificante, aprobar_justificante, confirmar_pago, devolver_a_contabilidad, devolver_a_implementador, marcar_enviado, anular → { success, pedido, errores[] }. marcar_enviado y create_tablet exigen permiso de edición de Hardware. editar_proforma { id, items, proforma_iva_pct, proforma_envio, proforma_envio_zona } corrige artículos, precios, envío e IVA de una proforma proforma_generada o proforma_enviada conservando su número de serie; con justificante, pagada o anulada no casa ninguna fila (errores «0 filas»). Desde el 13-oct-2026 se usa también desde el proyecto (pestaña Hardware), no solo desde hardware.html; el PDF se regenera después con guardar_proforma_pdf. |
/hardware/stock | Catálogo del almacén (Soporte): stock actual, mínimo, precios y movimientos. Acciones: create, update, archivar, reactivar, movimiento. Escribir exige permiso de edición de Stock o Hardware (si no, 200 { ok:false, code:'forbidden' }); created_by lo pone el servidor con el usuario de la sesión. |
/hardware/tablets | GET → { pendientes, en_curso }: fichas con necesita_tablet y aún sin pedido, y los pedidos tipo='tablet' ya en curso. Fuente distinta al listado de pedidos — aquí manda la ficha de alta, no hardware_pedidos. |
/seur/envios (POST) | WF42. Envíos y recogidas con SEUR desde hardware.html. { action:'crear_envio', pedido_id, receiver, bultos, service_code, product_code, comments } · { action:'crear_recogida', pedido_id, collection_date, franja, receiver, bultos, observations } · { action:'cancelar_recogida', pedido_id, collection_ref } → { ok, shipment_code?, label_pdf?, collection_ref?, collection_date?, error? }. Exige sesión y permiso de edición de Hardware. pedido_id UUID; crear_envio solo con el pedido en lista_envio; envío y recogida reservan el pedido 90 s (un segundo intento en ese tiempo responde «Ya se está generando…»); collection_ref tiene que ser la del pedido. tracking y cancelar están retirados (25-oct-2026). |
🧩 Producto
Desarrollos a medida por cliente (reemplaza el Excel de Producto).
| Path | Uso |
|---|---|
/presupuestos | CRUD: quién paga, estado de aprobación y de entrega. |
/presupuestos-asana | GET → { tareas:[{ …, ya_importado }], total, nuevas, importadas }: tareas de la sección «Pendiente de presupuesto» (Back Clientes). POST { action:'import'|'refresh', asana_gid } las importa. |
/presupuestos-asana-attach | Adjunta el PDF generado a la tarea Asana de origen (base64 → multipart). |
/presupuestos/enviar-cliente | WF22. Envía el PDF por email al cliente desde el buzón de soporte. |
Workflow WF66. Horas contratadas vs consumidas por cliente. Las contratadas se registran como compras (cantidad + PDF pago + PDF factura); las consumidas se derivan de los presupuestos aceptados/pagados.
| Path | Uso |
|---|---|
GET /bolsa-horas | → { compras:[…], asignaciones:[…] }. |
/bolsa-horas-crear | Sube ambos PDFs e inserta la compra. |
/bolsa-horas-editar · -archivar · -reemplazar-pdf | Editar horas/fecha, archivar, reemplazar un PDF. |
/bolsa-horas/asignacion · -borrar | Upsert / borra una asignación bolsa↔presupuesto. |
Workflow WF57. Lee en vivo el proyecto Asana «Sugerencias optimizaciones clientes» (tareas + secciones + etiquetas + campos, normalizado y paginado).
| Path | Uso |
|---|---|
GET /triaje/asana | Sugerencias normalizadas. |
/triaje/asana/edit | Edita una sugerencia → escribe en Asana (PUT name/notes + add/removeTag de módulo). |
/triaje/asana/orden | Orden de prioridad manual compartido (mapa módulo→[gids], Data Table triaje_orden). |
Workflows WF-106 (Asana) y WF-107 (vacaciones). El tablero mensual «Sprint #N - <nombre>» de Producto, leído y escrito en vivo contra Asana. Lo usa triaje-sprint.html.
| Path | Método | Uso |
|---|---|---|
/sprint-triaje/asana | GET [?project=<gid>] | Resuelve el sprint activo (el que cubre hoy entre los «Sprint #N - nombre»; las plantillas peladas tipo «Sprint #44» quedan fuera) y devuelve tareas, secciones, miembros, catálogo de campos por gid y las notas del proyecto, de donde js/lib/sprint-triaje.js saca la capacidad de cada persona. |
/sprint-triaje/asana/edit | POST | { gid, assignee?, custom_fields:{ <gid>: valor } } — escribe la tarea en Asana. |
/sprint-triaje/asana/mover | POST | { gid, seccion } — cambia la tarea de columna. |
/sprint-triaje/productivas | POST | { dev, pct } — fija el % de horas productivas de una persona; pct: null vuelve al de su seniority (70 % senior, 50 % junior). Se guarda en la Data Table sprint_productivas y el GET de /sprint-triaje/asana lo devuelve en productivas: { <nombre>: pct }. La capacidad se calcula sobre las horas disponibles, no sobre las notas. |
/sprint-triaje/vacaciones | GET ?desde=YYYY-MM-DD&hasta=YYYY-MM-DD | WF-107. Vacaciones del equipo de desarrollo leídas del WS API de Yurest (GET /v2/hr/absences, type_id=2, estados 1 accepted y 2 pending; fuera canceladas y borradas). → { ok, desde, hasta, tramos:[{ user_id, nombre, desde, hasta, estado }] }. |
vacacionesPorDev() en js/lib/sprint-triaje.js.Workflow WF-110. Productividad por desarrollador (productividad.html, permiso triaje_sprint).
| Path | Método | Uso |
|---|---|---|
/sprint-horas/dias | GET ?project=<gid> | Entradas de tiempo del sprint, día a día. Es la única fuente con granularidad diaria: actual_time_minutes es un total por tarea, sin fecha. |
/sprint-horas/sync | POST | { project } — fuerza el refresco desde Asana (el cron ya lo hace cada 2 h). |
Workflow WF-111. Informe de errores de sprint (errores.html, permiso triaje_sprint).
| Path | Método | Uso |
|---|---|---|
/errores/estado | GET | Corpus global de errores de Asana (Tipo ∈ Error, ERROR - P1, Problema configuración): todo lo abierto, tenga la edad que tenga, más lo resuelto de los últimos 6 meses. |
/errores/rebuild | POST | Fuerza el refresco (~30 s). El cron lo hace cada madrugada a las 06:30. |
Workflow WF-115. Vista Tareas Asana de Producto (tareas-asana.html, permiso tareas_asana). Proyecto Back Clientes, solo tareas abiertas de las secciones Pendiente de revisión, Pendiente de presupuesto, Necesita más info y Aprobadas.
| Path | Método | Uso |
|---|---|---|
/tareas-asana/estado | GET | Snapshot (tareas_asana_snapshot) con las tareas y la definición de sus campos. El cron lo refresca cada 15 min. |
/tareas-asana/sync | POST | {} — fuerza el refresco. |
/tareas-asana/detalle | GET ?gid= | En vivo: notas, campos, comentarios, subtareas y adjuntos de una tarea. |
/tareas-asana/guardar | POST | { gid, notas?, campos?: { <campo_gid>: valor | opcion_gid | null } } — escribe en Asana y actualiza la tarea en el snapshot. Las duraciones (Estimated/Actual time) son de solo lectura. |
/tareas-asana/comentar | POST | { gid, texto } — crea el comentario en Asana. |
Base de conocimiento de sprints (gpt-4o). Se pega la URL de un proyecto Asana, se elige un campo enum y sus valores, y se genera un documento markdown de las funcionalidades del sprint.
| Path | Uso |
|---|---|
POST /documento-ia/analizar | { asana_url } → { proyecto, campos[], tareas_completadas }. |
POST /documento-ia/generar | { asana_gid, sprint_nombre, campo_gid, campo_nombre, valores[] } → { documento }. |
GET /documento-ia | → { documentos:[…] } (vivos). /borrar = soft delete; /editar. |
POST /documento-ia/preguntar | { pregunta } → { respuesta(markdown), sprints_citados[] }. |
Workflow WF73. Newsletter de sprint (/updates). Analiza un proyecto Asana, reescribe títulos con IA y arma el email.
| Path | Uso |
|---|---|
/newsletter/analizar · /vincular | Detecta proyecto/sección/campos; vincula la newsletter. |
GET /newsletter · /cargar · /guardar | Lista / carga (merge en front) / guarda { titulo, intro, items }. |
/newsletter/generar | { id, task_gids[] } → { items:[{task_gid, titulo_reescrito, beneficio}] }. |
/newsletter/enviar-prueba · /borrar · /asignar-tipo | Envío de prueba, soft delete, escribe el campo «Tipo» en Asana. |
/newsletter/combinar | { id, extra_id, quitar? } → añade (o quita) otro sprint a la newsletter id. Al añadir importa el borrador del otro; /cargar trae las tareas de todos. |
/newsletter/marcar-enviada | { id, enviada, usuario } → enviada_at + enviada_por. Marca manual: el envío masivo aún no vive en el portal, y estado no sirve porque lo pisa el envío de prueba (enviado_prueba). |
🤝 Customer Success
Workflow WF-118. Backend de la Cascada CS (clientes.html → Customer), los paneles cs-inicio · cs-actividad · cs-kpis y la pestaña Contactos del gestor. El workflow solo valida la sesión y llama a funciones SQL (cs_ficha, cs_resumen, cs_agenda, cs_actividad, cs_kpis, cs_tareas_impl, cs_accion): la lógica vive en la base de datos. El autor de cada acción sale del token, nunca del body.
GET /cs?recurso=…
| recurso | Parámetros | Uso |
|---|---|---|
ficha | ficha_id (uuid) | Todo lo de CS de un cliente: reuniones (hitos), contactos, follow-ups, oportunidades, incidencias, riesgo y contactos_cliente. |
resumen | — | Cartera por fase. |
agenda | csm? · desde? (YYYY-MM-DD, hoy) · dias? (1-31, 7) | Reuniones de la CSM en la ventana. |
actividad | csm? · canal? (llamada|whatsapp|correo|reunion|otro) · solo_sin? | Seguimiento de clientes. |
kpis | mes? (YYYY-MM) · csm? | Panel de KPIs Customer. |
tareas_impl | ficha_id o implementador · pendientes=1 | Tareas de CS a Implementación (pestaña Customer y alerta del home). |
POST /cs/accion
{ "accion": "contacto_cliente_crear",
"datos": { "ficha_id": "uuid", "nombre": "Ana", "apellidos": "", "rol": "Gerente", "email": "ana@cliente.es", "telefono": "", "convocar": true } }
| accion | datos / efecto |
|---|---|
hito_crear · hito_actualizar · hito_cancelar · hito_eliminar | Reuniones CS: { ficha_id, tipo, fecha_prevista } · { id, updated_at, estado, … } · { id, updated_at, motivo } · { id, updated_at }. Cancelar deja la reunión «por reagendar», cancela el evento de Google y pide al cliente que elija otra fecha. Eliminar (7-oct-2026) es un borrado lógico sin correo al cliente: la reunión desaparece de la Cascada, el evento de Google se cancela en la siguiente pasada del cron y queda apunte en cs_estado_historial → { ok, hito }. No se puede eliminar una reunión ya realizada ni la bienvenida individual (409 transicion). |
contacto_crear · followup_crear · followup_actualizar | Registro de contactos con el cliente y de seguimientos. |
oportunidad_crear · oportunidad_actualizar · incidencia_crear · incidencia_actualizar | Oportunidades e incidencias CS. |
handover_guardar · drive_vincular · formulario_guardar | Handover, carpeta de Drive y formularios (post 1 mes, QBR). |
riesgo_marcar · riesgo_quitar · csm_asignar | Marca manual de riesgo (con motivo) y CSM del cliente. |
tarea_impl_crear · tarea_impl_actualizar | Tareas a Implementación. Permiso propio: el implementador las cierra sin escritura en CS. |
contacto_cliente_crear · contacto_cliente_actualizar · contacto_cliente_borrar | Lista única de contactos del cliente (cs_contactos_cliente), compartida por CS e Implementación: { ficha_id, nombre, apellidos?, rol?, email, telefono?, convocar? } · { id, …campos } · { id }. → { ok, contacto_cliente, contactos }; 409 duplicado si el email ya está. contacto_cliente_actualizar { id, hacer_jp: true } hace al contacto jefe de proyecto: copia sus datos en fichas_alta.jp_*, el JP anterior entra en la lista y responde además { jp, jp_anterior }; 400 si el contacto no tiene email. |
Respuesta
{ "ok": true, ... }
{ "ok": false, "code": "validacion | sin_usuario | sin_permiso | no_encontrado | conflicto | transicion | duplicado | accion_desconocida", "error": "texto legible" }
proyectos.contactos es una copia de esta lista que mantiene un trigger (ver proyectos).Integración HighLevel (CRM). Credenciales (LOCATION_ID + TOKEN) solo en n8n.
| Path | Uso |
|---|---|
/highlevel/contactos | WF40. Listado de contactos. Filtros server-side: ?limit(1-500), ?cursor("ts,id"), ?q, ?tag, ?origen. |
/highlevel/contacto-notas · /tags | Notas del CRM de un contacto bajo demanda (GET ?contactId); catálogo de tags del location. Si el contacto ya no existe en HighLevel (borrado o fusionado) → 404 { ok:false, code:'contacto_hl_no_encontrado' }: reintentar no sirve, hay que corregir el contacto vinculado en la ficha o en su oferta. |
POST|PUT /highlevel/contacto-notas | Crea ({contactId, texto}) o edita ({contactId, noteId, texto}) una nota del contacto. El autor sale del token de sesión, cruzado por email con los usuarios de HighLevel; si no existe allí, la firma va al final del texto. Sin borrado. La usa la pestaña Anotaciones del proyecto. |
/highlevel/usuarios | Usuarios de HighLevel {usuarios:[{id, nombre}]}, para mostrar el autor de las notas. |
/highlevel/clientes-ganados | WF49. Snapshot de oportunidades ganadas (3 pipelines) enriquecidas con contacto/business. Formato { ok, total, columnas, rows, generated_at }. Desde el 7-oct-2026 el snapshot guarda también los clientes Lite (los necesita Estado clientes) y es este GET el que los filtra. |
/highlevel/clientes/refresh | WF50. Refresca el snapshot on-demand (cron 6:30 UTC). Si HighLevel limita peticiones (429) y fallan más de max(5, 2 %) de las lecturas, aborta antes de borrar: se conserva el snapshot anterior. |
POST /highlevel/cs/refresh | WF-136 (7-oct-2026). Refresca highlevel_cs_snapshot: oportunidades de los pipelines de Customer Success (onboarding Planes y Lite, Corporate y Enterprise; sin Churn ni Upsell). Responde 202 { ok:true, lanzado:true } y sigue por detrás; cron diario 6:45. Misma regla de aborto que WF50. Es la fuente principal del tipo de implementación de Estado clientes. |
/highlevel/prod-sin-crm | WF51. Clientes vivos en producto SIN oportunidad ganada (match prod_id↔id_bbdd, fallback CIF). |
/highlevel/oportunidades-abiertas · /contacto-detalle | WF67. Picker de oferta desde oportunidad abierta. |
Workflow WF76. Clientes Free 3 meses. GET lista clientes_free_3m; /toggle persiste Free(0)/Lite(1); /refresh re-sincroniza contra HighLevel sin esperar al cron de las 6.
Resumen del día para el dashboard del home. GET sin parámetros → { fecha, ofertas, fichas, bajas, escalados } (contadores incrementales de home_kpis_diarios, mantenidos por triggers AFTER INSERT).
mensaje_cliente_pendiente (que sí tiene endpoint, /proyectos/mensajes-pendientes) , factura_distribuidor_pendiente y escalado_local_nuevo (lee /escalados?tipo=local), las alertas se calculan en el navegador sobre las fichas ya cargadas (lista.js) y solo hacen deep-link a lista.html con el filtro puesto.fichas_alta.estado: completada = la rellenó el cliente y falta que la complete el consultor; rellenado = ya la completó el consultor (estado terminal). Por eso la alerta ficha_pendiente_introducir filtra por rellenado sin introducido_yurest, y solo desde el 22-jun-2026, fecha en que nació esa columna: sin el corte entraría todo el histórico, que la tiene a false por defecto.Workflow WF-117. Contadores de los badges del menú calculados en el servidor, para que el home no tenga que bajarse /altas completo (3,6 MB → 161 B). Sin parámetros.
{
"version": 1,
"a3": 12,
"sinasignar_ids": ["…"],
"sinasignar_sin_id": 0,
"proformas": 3,
"hardware": { "lista_envio": 2, "solicitada": 1 }
}
sinasignar_ids va como lista de ids y no como número porque el navegador descuenta los borrados recientes que guarda en localStorage. Las tablets pendientes y las facturas de distribuidor no salen de aquí.database/n8n-workflows/117-home-contadores.build.mjs) y en js/lib/contadores-home.js, copia exacta de _contarXCliente de config.js, que es el respaldo si WF-117 falla. tests/contadores-home.test.js compara ambas: cambiar una regla obliga a cambiar las dos y redesplegar el workflow.🛟 Soporte
| Path | Uso |
|---|---|
/zendesk/tickets-heatmap | Mapa de calor: ?from=YYYY-MM-DD&to=YYYY-MM-DD → matriz [día 0-6][hora 0-23]. |
/zendesk/tickets-heatmap-ia | Igual pero solo tickets aún en el agente IA (asunto «Conversation with»): cobertura del bot. |
/zendesk/resumen-semanal | Resumen ejecutivo IA de la semana. ?week=actual|anterior o ?from&to. |
/zendesk/resumen-mensual | WF29. Análisis profundo (gpt-4o) con cache por (año, mes). ?refresh=1. |
Workflow WF105. Informe mensual de KPIs de soporte: una fila por mes en informes_mensuales_soporte. El cron del día 1 lanza las 52 consultas a la Search API de Zendesk del mes anterior y guarda el informe ya redactado. Lo consume informe-mensual-soporte.html.
| Parámetro | Uso |
|---|---|
?month=YYYY-MM | Mes del informe. Por defecto, el mes anterior. |
?refresh=1 | Regenera. Responde 202 y sigue por detrás. |
refresh no toca Zendesk: sirve lo guardado. Con refresh=1 la generación pasa del minuto y el proxy corta a los 60 s, así que la página sondea después sin refresh hasta ver un generado_en nuevo — no reintentes el refresh en bucle.Asociación cliente ↔ organización Zendesk (modal «Ver cliente» → Tickets) y registro de errores.
| Path | Uso |
|---|---|
/zendesk/orgs | WF41. Lista organizaciones con filtro server-side (nombre/external_id/details/notes). |
/cliente/zendesk-link | WF42. PATCH zendesk_org_id (o NULL). 409 si la org ya está asociada a otro cliente. |
/zendesk/tickets-cliente | WF43. Tickets de la org asociada (ventana default 180d). |
/registro-error/org-users?org_id= · /registro-error | Crea ticket Zendesk + tarea Asana enlazados. GET org-users → { users:[…] } (miembros de la organización). POST { org_id, requester_id, modulo, asunto, descripcion, cliente, proyecto_id? | ficha_id?, origen?, miembro_nombre?, imagenes:[{ nombre, tipo, base64 }] } → { ok, ticket_id, ticket_url, asana_gid, asana_url, imagenes_subidas }. Con origen:'cascada_cs' (Soporte técnico de la Cascada CS, 6-oct-2026) el ticket lleva la etiqueta cascada_cs en vez de gestor_proyectos. |
Workflow WF07. Actividad de un ticket Zendesk desde la tarjeta de integraciones.html.
| Path | Método | Uso |
|---|---|---|
/zendesk/ticket/recordatorios | GET ?ticketId= | → { ok, ticketId, ultimoComentario:{ texto, fecha, autor, publico } | null, recordatorios:[{ fecha, regla, email, estado }] }, lo más reciente primero. Los recordatorios salen de los audits del ticket (automatizaciones): no son comentarios y /comments no los ve. |
/zendesk/ticket/nota-interna | POST { ticketId, texto } | Añade una nota interna (19-oct-2026). El autor sale del token de sesión, no del body («Nota de <nombre>…»); si su email es agente de Zendesk, la nota sale además a su nombre. → { ok, ticketId, comentario:{ texto, fecha, autor, publico:false } } o 400 { ok:false, error } (texto vacío, ticket cerrado o inexistente…). |
Snapshots diarios (06:00) + refresco manual. Cruce Zendesk × Asana.
| Path | Uso |
|---|---|
GET /incidencias-dev/estado | Último snapshot de incidencias DEV. |
POST /incidencias-dev/rebuild | Reconstruye el snapshot. |
/integraciones-tpv/estado · /rebuild | Gemelo para integraciones TPV (filtro por proveedor). |
Pestaña Formación · proyecto Asana asociado al cliente.
| Path | Uso |
|---|---|
/cliente/asana-formacion-link | WF44. { cliente_id, asana_url } → extrae GID, verifica el proyecto y PATCH asana_formacion_gid. null desvincula. |
/asana/formacion-tasks?project_gid= | WF45. → { project, counts:{done,pending,total}, tasks:[…] }. |
Mini-app «Churn técnico» (iframe). Webhooks autenticados por el Auth Gate HMAC: churnClientes, churnResumenGenerar, churnNiveles, churnResumenBuscar, churnTickets.
Notificaciones automáticas de Integraciones: configuración, grupos de destinatarios e historial de envíos.
| Path | Método | Uso |
|---|---|---|
/notif-integraciones/config | GET · PUT | Configuración. PUT { activo, umbral_dias, secciones_seguimiento[], updated_by }. |
/notif-integraciones/grupos | POST | Alta, edición y borrado de grupos de destinatarios. |
/notif-integraciones/historial | GET | Historial de envíos. |
📦 Producto Yurest (snapshot prod)
Uso del producto Yurest cacheado en Supabase (prod_clientes_snapshot) para evitar la query pesada al MySQL de producción en cada apertura.
| Path | Uso |
|---|---|
GET /prod/clientes | WF47. Lee el snapshot → { ok, total, refreshed_at, clientes:[…], hl }. Cada cliente trae, entre otros, cantidad_locales e integraciones (texto separado por comas, Sin integraciones si no hay), de donde Estado clientes calcula en el navegador la tipología, los KPIs y el filtro por integración. hl: { ok, oportunidades, cs } son las oportunidades ganadas de HighLevel (WF50) y las de los pipelines de CS (WF-136): con ellas se pinta la columna Implementación (Grupal / Individual). Si hl.ok no es true, la columna queda sin dato. |
POST /prod/clientes/refresh | WF48. Reejecuta la query MySQL (cron 6am UTC). Tarda 5-15s → { ok, count, refreshed_at }. |
GET /prod/clientes/nombres | Lista ligera id+nombre+nombre_comercial de TODA la tabla (para casar nombre HL → id cliente). |
⚙️ Administración
Workflow WF91. «Mis contraseñas»: gestor de credenciales de portales externos. El servidor resuelve userId y rol del token; un no-admin solo ve/edita las suyas, el admin todas.
| Path | Uso |
|---|---|
GET /admin/passwords | Lista propia (o ?scope=all admin). Nunca devuelve la contraseña, solo metadatos. |
POST /admin/passwords | { action:'save'|'delete', entry:{…} }. La contraseña se cifra en BD (pgcrypto) vía RPC; la clave la pone el workflow, nunca viaja al cliente. |
POST /admin/passwords/reveal | { id } → plano (dueño|admin) y audita en admin_passwords_audit. |
Workflow WF92. «Mi equipo»: inventario por persona. Jerarquía tipo(sección) → catálogo(modelo, coste solo-admin) → unidad(física asignada a un usuario).
| Método | Uso |
|---|---|
GET ?what=tipos|catalogo|unidades[&scope=all] | unidades: propias sin coste; admin+scope=all → todas + coste + inventario económico agregado. |
| POST | { what:'tipo'|'catalogo'|'unidad', action:'save'|'delete', payload:{…} }. Escrituras solo admin. |
Audit log.
| Path | Uso |
|---|---|
/historial | Historial de acciones por ficha. |
/historial/global | Feed unificado (usuarios + fichas + proyectos) para Logs de Administración (solo admin). Query: ?limit&offset&fuente&desde&hasta&q → { historial:[…], hasMore }. Vista v_historial_global. q busca en el actor, la descripción y, desde el 13-oct-2026, también en el id y el tipo de la entidad (pegar el id de una ficha, un proyecto o un usuario). |
👥 RRHH
Tareas de cocina (tareas-cocina.html, autoservicio).
| Path | Método | Uso |
|---|---|---|
/cocina/estado?desde=&hasta= | GET | → { hoy, dia_activo, gestionar, personas, turnos, tareas, checks }. Nunca devuelve los PIN. |
/cocina | POST | { accion:'marcar', fecha, tarea, hecho, pin } · { accion:'intercambiar', fecha_a, fecha_b, pin } · { accion:'gestion', op, payload } (admin o escritura rrhh). |
Gestión documental (documentos-rrhh.html): NDA y cesión de equipos en PDF por empleado. Quién ve qué lo decide la base de datos: el gestor ve a todos; el resto, solo lo suyo.
| Método | Body | Respuesta |
|---|---|---|
| GET | — | { gestionar, yo, empleados:[{ id, nombre, documentos:{ nda?, cesion_equipos? } }] } |
| POST | { accion:'subir', usuario_id, tipo, nombre, data } · { accion:'borrar', id } | Admin o escritura rrhh. |
| POST | { accion:'ver', id } | { url } firmada, 60 s. |
| POST | { accion:'cesion_preparar' } · { accion:'cesion_firmar', equipo_hash, dni, … } · { accion:'cesion_detalle', id } | El empleado revisa y firma su declaración de cesión de equipos. |
📉 Bajas
Workflow 05-bajas. Gestión de bajas de clientes (registro, edición de campos individuales y borrado).
| Endpoint | Operación |
|---|---|
bajas | GET listado |
bajaCliente · bajaLocal · bajaModulos · bajaEditar | POST campos específicos (mismo webhook con action) |
bajaBorrar | POST borra una baja |
Workflow WF112 («Bonificaciones CRUD»). Bonificaciones de MRR de la vista Churn (churn.html, permiso bajas).
| Método | Entrada | Salida |
|---|---|---|
| GET | — | { ok, bonificaciones:[…] } |
| POST | { accion:'crear'|'editar'|'borrar', id?, cliente, periodo:'AAAA-MM', … } | { ok, bonificacion } o { ok:false, error } |
📊 Distribución
Workflow 06-distribucion. Asigna un implementador a una ficha.
distribucion.html es un panel de solo lectura).Body
{ "fichaId": "uuid", "implementador": "Nombre Implementador" }
Workflow WF78. Distribuidores potenciales: cuando el consultor rellena el TPV de un local y su email de contacto, el workflow saca el dominio y, si no consta como distribuidor conocido, avisa al responsable de distribuidores y registra la fila.
| Path | Uso |
|---|---|
GET /distribuidor/detectados | → { ok, rows:[{ dominio, email_origen, contacto_nombre, tpv_nombre, ficha_id, cliente_nombre, local_nombre, estado, avisado_at }] }. |
POST /distribuidor/detectados-estado | { dominio, estado } (nuevo|contactado|descartado|alta) → PATCH de la fila. |
📁 Drive
Workflow 11-auxiliares. getDrive devuelve la URL de la carpeta Drive de una ficha; docsSubidos consulta si hay documentos subidos.
docsSubidos desde el 24-abr-2026. getDrive sigue en uso.Query
?id=<ficha_uuid>
🏠 Espacio Cliente
API que consume espacio-cliente.html (servido en espacio-cliente.yurest.es). No comparte base ni token con el resto de esta documentación.
{BASE_URL}/portal-clienteCabecera distinta:
X-Cliente-Token (no X-Session-Token)Las rutas están declaradas públicas en el gate de n8n porque la validación la hace el propio workflow
WF77. El cliente final no tiene cuenta de portal: su sesión vive en localStorage.yurest_cliente_sesion y un 401 la borra y devuelve al login.
ficha_id o proyecto_id que llegue en el cuerpo se ignora.GET /proyectos y manda en cada petición X-Cliente-Proyecto: <proyecto_id>; WF77 solo atiende esa cabecera si el acceso es maestra. Es de solo lectura: cualquier método distinto de GET se corta en el front y también en WF77 (code: 'solo_lectura').Alta de contraseña, login y recuperación. Los accesos los crea el staff desde portal-accesos (WF-101).
| Path | Entrada | Salida |
|---|---|---|
/set-password | { set_token, password } | { ok }. Valida token y caducidad, escribe hash+salt, estado='activo' y quema el token. |
/login | { email, password } | { token, proyecto:{ id, nombre } } o { error } genérico. |
/reset | { email } | { ok } siempre. |
pendiente (aún sin contraseña) y uno bloqueado responden igual que una contraseña mala. No se le dice al atacante en qué estado está la cuenta.| Path | Método | Uso |
|---|---|---|
/proyecto | GET | Payload del espacio: nombre, implementador, secciones con sus tareas (nombre, completada, show, fechaEntrega, notas), adjuntos, contactos y el resumen de sesión publicado. |
/proyectos | GET | Solo llave maestra. → { ok, proyectos:[{ id, nombre, razon_social, implementador, estado }] } para el selector de cliente. |
/mensajes | GET · POST | Hilo con el staff (asc). POST { texto }, máx 4000. El mismo hilo que ve el staff en /proyectos/mensajes. |
/accion | POST | { tareaId, accion, hecha } — marca una acción del resumen de sesión. |
/tickets | GET | Tickets de Zendesk de su organización (whitelist de 6 campos; la organización sale del token). |
GET /proyecto se construye campo a campo, no es un SELECT * podado. Fuera sin excepción: datos financieros, sepa_mandato, cualquier cred_*, anotaciones, motivo_pausa y plan_accion.Pedidos de hardware desde el Espacio Cliente (WF77). Sin aviso a Slack: se ven en la pestaña Hardware y en la alerta del home del staff.
| Path | Método | Uso |
|---|---|---|
/hardware/catalogo | GET | Artículos que el cliente puede pedir. |
/hardware/pedidos | GET | Sus pedidos y el estado de cada uno. |
/hardware/pedido | POST | { idem_key, items, direccion_entrega }. La idem_key la genera el front: reenviar el mismo pedido no duplica. |
/hardware/pdf | GET ?pedido_id=&tipo= | → { ok, pdf }. Proforma o factura del pedido. |
/hardware/justificante | POST | { pedido_id, nombre, base64 } — el cliente sube el justificante de pago. |
/hardware/proforma-pdf | POST | { pedido_id, base64 } — guarda el PDF de la proforma que genera el navegador del cliente. Responde 404 si el pedido no es suyo. |
solicitada (femenino, concuerda con «proforma»). Escribirlo en masculino rompe los filtros.| Path | Método | Uso |
|---|---|---|
/facturas | GET | Sus facturas (las que sube el staff por WF102). Sin storage_path. |
/factura/enlace | GET ?id= | Enlace firmado de 60 s al PDF. No copiar la firma a 1 año de WF35. |
/privadas | GET | Huecos libres para una sesión privada y la cuota que le queda. |
/privadas/reservar | POST | { inicio }. A diferencia de staff/privadas, aquí la cuota sí veta. |
/privadas/cancelar | POST | { tarea_id }. |
/carga-datos/estado | GET | Estado de su carga de datos (mismo vocabulario que WF-98). |
/carga-datos/marcar | POST | Body vacío. El cliente declara que ya ha subido sus datos → crea la tarea de revisión en Asana (idempotente por gid). Errores: 502 asana (no se marca nada; se puede reintentar) · 502 guardar + gid (tarea creada, ficha sin marcar: ejecución en error para arreglarlo a mano; el Espacio Cliente lo da por recibido). |
/contrato | GET | → { ok, estado, numero, firmado_at, tiene_pdf }. estado es lo que escribe efirma (FIRMADO, enviado…); el front lo pasa a minúsculas. |
/contrato/pdf | GET | → { ok, pdf:{ data } } en base64, o 404. El PDF se descarga de Drive en el servidor: así se abre desde el móvil sin sesión de Google y la carpeta no se expone. |
/contrato responde estado: null y la pestaña enseña su estado vacío.Apéndice A · Tipos de datos
Objeto SEPA
{
"referencia": "YUREST-5",
"acreedor": {
"identificador": "ES##ZZZB40597437",
"nombre": "Yurest Solutions Sociedad Limitada",
"direccion": "Calle Transits, 6",
"cp": "46002", "poblacion": "Valencia", "provincia": "Valencia",
"pais": "España"
},
"deudor": {
"nombre": "...", "direccion": "...",
"cp": "...", "poblacion": "...", "provincia": "...", "pais": "..."
},
"swift": "CAIXESBB",
"iban": "ES9121000418450200051332",
"tipo_pago": "recurrente" | "unico",
"fecha_firma": "2026-04-17",
"localidad": "Valencia",
"firma_base64": "data:image/png;base64,...",
"firmado_at": "2026-04-17T14:33:33.152Z"
}
Objeto Adjunto
{
"id": "adj_xxx",
"nombre": "contrato.pdf",
"tipo": "application/pdf" | "image/png" | "image/jpeg",
"size": 12345,
"data": "data:application/pdf;base64,...",
"fecha": "2026-04-17T..."
}
Objeto paquetes_carrito
{
"items": [{ "id": "plan_pro", "precio": 1580 }],
"descuentos": { "cart": 0, "planes_setup": 0, "planes_recur": 0 },
"periodos": { "planes": "mensual" | "anual" },
"corp_module_locales": { "modulo_x": [0, 1] },
"locales_mensual": [25, 30],
"modo": "por_local", // opcional: carrito configurado local a local
"locales_modulos": [{ "modulos": [{ "modulo": "Compras" }] }], // con modo por_local: módulos de cada local, en el orden de los locales
"impl_grupal": { "precio": 250, "descuento": 0 } // implementación grupal: bruto + % de descuento; ImporteSetup guarda el neto
}
Apéndice B · Tabla de estados
fichas_alta.estado
| Estado | Significado | Quién lo asigna |
|---|---|---|
pendiente | Inicial (poco usado) | — |
completada | Cliente rellenó su parte (workflow 11) | Cliente vía solicitud.html |
rellenado | Consultor completó la ficha (workflow 04) | Consultor vía index.html |
solicitudes.estado
| Estado | Significado |
|---|---|
pendiente | Esperando que el cliente la rellene |
completado | Cliente rellenó (genera ficha) |
fichas_alta ≠ "completado" en solicitudes. Los triggers SQL ya alinean fechas con esta semántica (fecha_rellenado, fecha_completado, grabado_a3_at) para que la analítica sea coherente.
Apéndice C · Permisos de página
Cada usuario lleva permisos granulares { read, write, delete } por id de página (emitidos en el login). El rol es una base + deltas. usuarios.permisos es un JSONB sin CHECK: el catálogo vive solo en el front (PERMISOS_DISPONIBLES de config.js), así que un permiso nuevo no necesita migración y WF16 ignora los ids que no conoce. IDs disponibles:
| Grupo | IDs de página |
|---|---|
| Informes | distribucion · informe_implementacion · informe_seguimiento · carga_datos · participacion · informe_tickets · informe_tickets_ia · incidencias_dev · integraciones_tpv · resumen_semanal · resumen_mensual · informe_mensual_soporte · churn_tecnico |
| Consultor | lista · escalados · configurador · ofertas · distribuidores_detectados |
| Implementación | sinasignar · proyectos · panel_sesiones · promociones · importador_clientes · checklists · configurador_recetas |
| Contabilidad | contabilidad · clientes_a3 · cruce_a3 · proformas · facturas_cliente · facturas_distribuidores |
| Customer Success | cs_inicio · cs_kanban · cs_actividad · cs_kpis · cs_direccion · bajas · churn · highlevel · highlevel_clientes · huerfanos_prod |
| Producto | triaje · triaje_sprint · presupuestos · bolsa_horas · documento_interno_ia · updates · tareas_asana · errores · productividad |
| Soporte | estado_clientes_yurest · integraciones · documentacion_integraciones · hardware · stock · clientes_free · resumen_semanal · resumen_mensual · informe_mensual_soporte |
| I+D | permisos_roles · editor_etiquetas |
| Admin / Otros | admin · logs · rrhh · mis_contrasenas · mi_equipo · docs |
requireAuth). clientes queda en el catálogo sin uso (la Cascada CS pide cs_kanban). Son públicas para cualquier sesión: «Landings», «Doc. integraciones», «Mis contraseñas» y las de RRHH de autoservicio (Tareas de cocina, Mi equipo, Gestión documental). La fuente de verdad es sidebar.js (campo permiso, y si falta, el id).