From cbbadae965601df8166605748c9714745406f358 Mon Sep 17 00:00:00 2001 From: brobert Date: Thu, 16 Oct 2025 23:17:14 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20documenta=20onboarding=20y=20dise=C3=B1?= =?UTF-8?q?o;=20a=C3=B1ade=20Card,=20migraciones=20y=20cola?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- docs/plan-diseno-web.md | 93 ++++++++++++++++++++++ docs/plan-onboarding-usuarios.md | 131 +++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 docs/plan-diseno-web.md create mode 100644 docs/plan-onboarding-usuarios.md diff --git a/docs/plan-diseno-web.md b/docs/plan-diseno-web.md new file mode 100644 index 0000000..5ef0390 --- /dev/null +++ b/docs/plan-diseno-web.md @@ -0,0 +1,93 @@ +# Plan de Diseño Web: Claridad de interacción, “tareas mías”, deadlines e identidad visual por grupo + +Objetivos +- Distinguir de un vistazo qué elementos son clicables (box-shadow y estados). +- Señalar cuando “tú” estás entre los responsables (acento visual claro). +- Sustituir el emoji de calendario por un icono más entendible de “fecha límite” (SVG consistente). +- Dar color estable y reconocible a las pills de grupo con una paleta accesible y determinista. + +Fases + +Fase B1 — Paleta determinista de grupos +- Idea: asignar una de 12–15 combinaciones (border, fondo tenue, texto) por group_id usando un hash determinista (mod N). Si hay más grupos que colores, se repiten. +- Paleta sugerida (AA sobre fondo claro): + 1) Blue: border #2563EB, fondo #DBEAFE, texto #1E3A8A + 2) Indigo: border #4F46E5, fondo #E0E7FF, texto #312E81 + 3) Violet: border #7C3AED, fondo #EDE9FE, texto #4C1D95 + 4) Purple: border #9333EA, fondo #F3E8FF, texto #581C87 + 5) Fuchsia: border #C026D3, fondo #FAE8FF, texto #701A75 + 6) Pink: border #DB2777, fondo #FCE7F3, texto #831843 + 7) Rose: border #E11D48, fondo #FFE4E6, texto #881337 + 8) Red: border #DC2626, fondo #FEE2E2, texto #7F1D1D + 9) Orange: border #EA580C, fondo #FFE7D1 (aprox.), texto #7C2D12 + 10) Amber: border #D97706, fondo #FEF3C7, texto #78350F + 11) Green: border #16A34A, fondo #DCFCE7, texto #14532D + 12) Teal: border #0D9488, fondo #CCFBF1, texto #134E4A +- Implementación: + - Crear apps/web/src/lib/utils/groupColor.ts con una función colorForGroup(groupId) → { border, bg, text } usando hash ligero (p. ej., sumatoria de charCodes) y modulo N. + - Aplicar en la pill de grupo de TaskItem.svelte y donde aparezcan chips/etiquetas de grupo. + +Fase B2 — Icono de “fecha límite” en SVG +- Sustituir emoji de calendario por un icono más semántico: + - Recomendación: “clock” (reloj) o “hourglass” (arena). +- Implementación: + - Crear apps/web/src/lib/ui/icons/Clock.svelte (y/o Hourglass.svelte) como SVG inline con fill="currentColor", tamaño 16–18px. + - Reemplazar en TaskItem.svelte donde se muestra due_date. Mantener aria-label/tooltip pertinentes. + +Fase B3 — Indicador cuando “tú” estás asignado +- Mantener icono/contador de responsables y añadir acento visual si el usuario actual está entre los assignees: + - Anillo/borde con el color primario alrededor del icono/badge o un pequeño dot superpuesto. + - aria-label dinámico: “n responsables; tú incluido” | “n responsables; tú excluido”. + - Tooltip opcional en desktop. +- Implementación: + - En TaskItem.svelte, derivar isMine comprobando si App.locals.userId (o prop userId) ∈ assignees[] y aplicar clase/modificador que active el acento. + +Fase B4 — Box-shadow solo en elementos interactivos +- Principio: toda superficie clicable debe tener pistas visuales coherentes (cursor, sombra/hover, focus visible). Superficies no interactivas no deben tener sombra. +- Implementación: + - apps/web/src/lib/styles/tokens.css: definir variables de sombras (--shadow-sm, --shadow-md, --shadow-focus). + - apps/web/src/lib/styles/base.css: patrones de hover/focus/active para botones, links con rol=button y chips clicables (sombra sutil en reposo, incremento ligero en hover/focus-visible, compresión en active). + - Mantener focus-visible claro (ring) y contraste AA. +- QA: verificar en móvil ≤480px que no haya desbordes; mantener targets ~44px sin inflar paddings. + +Accesibilidad +- Contraste AA para texto sobre fondo en las pills de grupo. +- Focus visible en todos los elementos interactivos. +- aria-label correcto en iconos y tooltips; roles adecuados si se usan popovers/modales. + +Archivos a editar/crear + +Crear +- apps/web/src/lib/utils/groupColor.ts (hash + paleta). +- apps/web/src/lib/ui/icons/Clock.svelte (y/u Hourglass.svelte) (SVG). + +Editar +- apps/web/src/lib/ui/data/TaskItem.svelte + - Aplicar paleta determinista en pill de grupo. + - Sustituir emoji de fecha por SVG “deadline”. + - Añadir acento visual “isMine” en el indicador de responsables con aria/tooltip. +- apps/web/src/lib/styles/tokens.css + - Añadir variables de sombras y, si procede, refinar escala de colores. +- apps/web/src/lib/styles/base.css + - Estados interactivos con sombras coherentes y cursor correcto. +- apps/web/src/lib/ui/layout/Card.svelte (opcional) + - Ajustar padding vertical si hiciese falta para mantener densidad. + +Criterios de aceptación +- Elementos interactivos distinguibles de un vistazo (sombra + cursor + focus). +- El indicador de responsables comunica “es mía” sin leer texto. +- El icono de due se interpreta como “fecha límite”. +- Las pills de grupo mantienen color estable sesión tras sesión. + +Caveats +- Evitar sombras en contenedores no interactivos para no elevar ruido visual. +- Mantener densidad: no incrementar la altura de filas. +- Si hay dark-mode en el futuro, revisar la paleta para asegurar contraste. + +Orden de entrega sugerido +- B1 + B2 primero (impacto alto, bajo riesgo). +- B3 después (requiere condicionar por userId). +- B4 al final (pulido transversal + QA de accesibilidad). + +Notas operativas +- Para aplicar estos cambios, comparte en este chat los archivos UI relevantes (TaskItem.svelte, tokens.css, base.css y/o componentes de iconos) y propondré los parches en bloques SEARCH/REPLACE. diff --git a/docs/plan-onboarding-usuarios.md b/docs/plan-onboarding-usuarios.md new file mode 100644 index 0000000..a9cd917 --- /dev/null +++ b/docs/plan-onboarding-usuarios.md @@ -0,0 +1,131 @@ +# Plan de Onboarding de Usuarios y Resolución de Alias (JID opaco → número) + +Resumen y diagnóstico (basado en el código actual) +- Fuentes automáticas actuales de aprendizaje de alias (sin intervención de usuarios): + 1) Mensajes en grupos: en src/server.ts, si participantAlt y participant difieren, se hace IdentityService.upsertAlias(participant, participantAlt, 'message.key'), mapeando un @lid “opaco” al JID real @s.whatsapp.net. + 2) Sincronización de miembros: en src/services/group-sync.ts, si el payload trae p.id y p.jid, se hace upsertAlias(id, jid, 'group.participants') y además se asegura el usuario con ensureUserExists. + 3) Actualizaciones de contactos/chats: en src/services/contacts.ts, si se recibe objeto con id=@lid y jid=@s.whatsapp.net, se hace upsertAlias(alias, jid, 'contacts.update'). +- Alta de usuarios sin DM: src/services/group-sync.ts::reconcileGroupMembers llama ensureUserExists(userId) para cada miembro activo; si tenemos número, el usuario “existe” aunque no haya enviado DM. +- Normalización robusta: utils/whatsapp.normalizeWhatsAppId elimina dominio y sufijo “:xx”, quedando el número limpio si venía en el JID. +- Problema detectado: en src/services/command.ts, al parsear menciones, si una mención llega como @lid sin alias aún, se descarta (aunque a veces podría venir como JID real). Resultado: “se puede crear tarea pero no asignar responsables” en algunos casos, especialmente con usuarios “silenciosos” (no escriben en grupo) cuando la API no aporta número en ninguno de los eventos. + +Objetivo +- Minimizar fricción y tiempo hasta poder asignar y autenticar: que los usuarios queden asignables con la mínima acción (idealmente sin DM). +- Evitar spam y códigos por-usuario. Publicar, solo si hace falta, un único mensaje por grupo con enlace wa.me. +- Cubrir casos de usuarios “silenciosos” (no escriben en grupo) garantizando una vía de mapeo fiable (DM “hola”). + +Estrategia general +- Exprimir al máximo el aprendizaje automático que ya tenemos (participants y contacts). +- Ajustar CommandService para no descartar menciones válidas con números reales. +- Medir cobertura y publicar un único mensaje por grupo con wa.me únicamente si faltan usuarios por resolver; con cooldown. +- DM “hola” como último recurso confiable para cerrar huecos de usuarios silenciosos; considerar “código por grupo” solo como fallback extremo si una instancia no correlaciona jamás sin token. + +Fases + +Fase A0 — Verificación y observabilidad (rápida, sin UX visible) +- Métricas nuevas: + - alias_coverage_ratio{group_id}: gauge con el porcentaje aproximado de miembros activos con número resoluble (o alias). + - onboarding_prompts_sent_total / onboarding_prompts_skipped_total: counters para controlar ruido. + - onboarding_assign_failures_total: counter de menciones no resolubles al crear tareas. +- Logs de desarrollo (NODE_ENV=development): en src/server.ts, comparar participant vs participantAlt (normalizados) y mentionedJid normalizados para comprobar la frecuencia de correlación automática en tu instancia Evolution. + +Fase A1 — Aprendizaje “agresivo” al entrar en grupos (ya casi implementado) +- Al recibir groups.upsert (src/server.ts): + - syncGroups → refreshActiveGroupsCache → syncMembersForActiveGroups (ya implementado). + - Efectos: + - Crea usuarios con ensureUserExists(userId). + - Si el payload incluye id + jid, rellena alias automáticamente. +- Mantener activo ContactsService.updateFromWebhook para capturar correlaciones adicionales en los primeros minutos. + +Fase A2 — Ajuste clave sin fricción: conservar menciones con números +- En src/services/command.ts, al construir los candidatos a assignees: + - Si normalizeWhatsAppId(token) produce dígitos y resolveAliasOrNull devuelve null, CONSERVAR ese número (no descartarlo). + - Asegurar ensureUserExists para esos IDs conservados antes de usarlos. + - Filtrar CHATBOT_PHONE_NUMBER para evitar autoasignaciones al bot. +- Efecto: reduce drásticamente los fallos de asignación por mención sin necesidad de DM. + +Fase A3 — Mensaje único por grupo con wa.me (solo si hace falta) +- Condición de publicación: + - Tras A1 y un breve grace period (≈1–2 min) para permitir contacts/chats updates, calcular alias_coverage_ratio{group_id}. + - Si cobertura = 100% → NO publicar. + - Si cobertura < 100% → publicar UNA vez un mensaje por grupo con el texto: + - “Para poder asignarte tareas y acceder a la web, envía ‘hola’ al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/” +- Control de ruido: + - Persistir timestamp de último envío por grupo (cooldown, p. ej., 7 días) y re-publicar solo si entran nuevos miembros sin resolver tras el cooldown. +- Fallback extremo (probablemente no necesario con tu stack): + - Si en una instancia concreta el DM “hola” no basta para correlacionar, usar “código por grupo” como texto pre-rellenado en wa.me: “alta XYZ123” (único por grupo, nunca por-usuario), almacenado con caducidad. Aplicarlo solo si la métrica demuestra que el caso existe. + +Fase A4 — Asistentes “just-in-time” y UX mínima +- Si una asignación falla por mención no resoluble: + - Enviar DM al asignador (ResponseQueue) con: “No puedo asignar a X aún. Pídele que toque este enlace y diga ‘hola’: https://wa.me/”. +- Primer DM “hola” de un usuario: + - Asegurar ensureUserExists y responder con: “Listo, ya puedes reclamar/ser responsable en: …”. +- Opcional web: si el usuario llega sin estar identificado, mostrar banner con botón a wa.me “hola”. + +Criterios de aceptación +- p95 del tiempo desde que un usuario toca el enlace a quedar asignable < 1 minuto. +- En la mayoría de grupos no se publica ningún mensaje (cobertura ≈100% tras primer sync + contacts). +- Caída significativa de onboarding_assign_failures_total respecto al baseline. +- Sin spam: un único mensaje por grupo y cooldown aplicado. + +Archivos a ver/editar/crear + +A) Núcleo bot +- src/services/command.ts (EDITAR) + - Puntos: construcción de mentionsNormalizedFromContext y normalizedFromAtTokens. + - Cambio: fallback a número normalizado cuando resolveAliasOrNull no resuelva; ensureUserExists; filtrar CHATBOT_PHONE_NUMBER; incrementar onboarding_assign_failures_total cuando una mención no sea resoluble en absoluto. +- src/services/group-sync.ts (EDITAR) + - Tras reconcileGroupMembers, computar cobertura aproximada (miembros activos con número conocido / miembros activos totales). + - Exponer alias_coverage_ratio{group_id} vía Metrics.set. Si coverage < 100% y cooldown vencido → disparar encolado de un mensaje único por grupo (ResponseQueue). + - Método util: getActiveGroupIdsForUser, isSnapshotFresh, etc., ya existen; añadir tracking de onboarding_prompted_at (ver persistencia). +- src/server.ts (EDITAR mínimo) + - Logs de desarrollo comparando participant vs participantAlt y mentionedJid normalizados para verificar correlación automática. + - Opcional: hook tras groups.upsert para forzar el chequeo de cobertura post-sync (o dejarlo en scheduler de miembros). +- src/services/contacts.ts y src/services/identity.ts (VER) + - Ya aportan alias automáticamente; no requieren cambios inmediatos. +- src/services/response-queue.ts (VER/EDITAR si hiciera falta) + - Asegurar que puedes encolar mensajes hacia group_id@g.us sin cambios (parece OK con sendText si el backend lo admite). Añadir, si quieres, etiquetas/metadata para identificar “onboarding”. + +B) Persistencia +- Opción simple recomendada: columna en groups + - Nueva columna: groups.onboarding_prompted_at TEXT NULL. + - Lógica: publicar si coverage < 100% y (onboarding_prompted_at IS NULL o han pasado ≥ X días). +- Alternativa: tabla dedicada group_onboarding (group_id PK, last_prompt_at, last_coverage, last_pending_count, last_sent_by). +- Archivos: + - src/db/migrations/index.ts (EDITAR): añadir migración para onboarding_prompted_at (o tabla nueva). + - src/db.ts (VER): no requiere cambios, migrador ya está integrado. + +C) Mensajería y copy +- Texto base: + - “Para poder asignarte tareas y acceder a la web, envía ‘hola’ al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/” +- Fallback extremo (solo si es necesario, ver A3): + - “… o envía ‘alta XYZ123’ si el enlace no funciona.” +- Dónde generarlo: en GroupSyncService (o un OnboardingService nuevo) cuando coverage < 100% y cooldown vencido; encolar con ResponseQueue para recipient = group_id@g.us. + +D) UI web (opcional a posteriori) +- Banner SSR-safe cuando App.Locals.userId no esté disponible: botón wa.me con “hola”; desaparecer al resolver. + +Métricas propuestas +- alias_coverage_ratio{group_id} (gauge). +- onboarding_prompts_sent_total / onboarding_prompts_skipped_total (counters). +- onboarding_assign_failures_total (counter). +- identity_alias_upserts_total, identity_alias_resolved_total / identity_alias_unresolved_total (ya existen). +- time_to_link_p95 (opcional; aproximable por “primer avistamiento” → “primer DM/alias resuelto”). + +Caveats y buenas prácticas +- No spamear: un único mensaje por grupo + cooldown; nada si coverage=100%. +- Privacidad: DM “hola” iniciado por el usuario; no mostrar datos sensibles en grupos. +- Entornos de test: suprimir publicación y logs invasivos; respetar NODE_ENV='test'. +- Resiliencia: si Evolution deja de enviar participantAlt/p.jid, el flujo de DM cubre a los silenciosos; si los envía, el DM apenas será necesario. + +Checklist de ejecución +- A0: Añadir métricas y logs de verificación (dev). +- A1: Confirmar que el sync de miembros corre al entrar; mantener contacts.update. +- A2: Ajuste en CommandService (fallback a número + ensureUserExists + métricas de fallo). +- A3: Publicación condicional de mensaje por grupo con cooldown + persistencia mínima. +- A4: DM “just-in-time” al asignador ante fallo de mención + confirmación al primer DM de usuario. + +Archivos adicionales que podríamos necesitar añadir al chat en la implementación +- src/utils/whatsapp.ts (para confirmar normalizeDigits si lo reutilizamos en copy/URLs). +- apps/web/src/app.d.ts (si añadimos banner basado en session/locals). +- tests/unit/* y tests/web/* (para cubrir la nueva lógica de fallback y métricas).