Commit Graph

63 Commits

Author SHA1 Message Date
Lucas Tettamanti
c0916c3050 env.example: limpiar variables sin uso
- WOO_BASE_URL: no se referencia en el código (las credenciales se leen
  del tenant en DB y como fallback de WOO_CONSUMER_KEY/SECRET).
- DEBUG_WOO_PRODUCTS: 0 referencias en src/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:18:04 -03:00
Lucas Tettamanti
47de1efe86 Login + ABM de operadores + audit log con UI
Backend:
- 3 migrations: system_users (citext email único, password_hash, active),
  system_sessions (UUID + expires_at + revoked_at), ALTER audit_log con
  actor_user_id/actor_email/actor_ip/action_path/summary y entity_id NULL.
- src/modules/auth/: usersRepo, sessionsRepo, passwords (bcrypt cost 10),
  auth (login/logout), bootstrap (crea admin desde ADMIN_EMAIL/PASSWORD si
  la tabla está vacía). 4 tests passwords (hash distinto cada vez, verify
  rechaza, longitud mínima 8).
- middleware/requireAuth: lee cookie bot_session, busca sesión activa,
  popula req.user. Whitelist: /styles, /components, /lib, /login, /, /home
  y SPA paths (HTML carga sin auth, el JS gatea con /api/auth/me).
- middleware/auditWriter: registra cada POST/PUT/DELETE 2xx en audit_log
  con req.user, IP, body redactado (passwords/tokens/secrets). Handlers
  pueden enriquecer summary via res.locals.audit.
- routes: /api/auth/{login,logout,me} (cookie httpOnly + DB session),
  /api/system-users (ABM con guards: cant_delete_self, cant_deactivate_self,
  email único, password ≥ 8), /api/audit-log + /api/audit-log/actors.
- src/app.js: orden estricto — webhooks (sin auth) → auth routes (sin auth)
  → /login HTML → static → SPA HTML → requireAuth + auditWriter → API admin.

Bootstrap del primer admin se ejecuta en index.js antes de listen. Usa
ADMIN_EMAIL/ADMIN_PASSWORD/ADMIN_NAME del .env. Si no están seteados y la
tabla está vacía, warn y exit (nadie puede loguearse).

Frontend:
- /login.html + /login.js: form simple, POST a /api/auth/login con
  credentials:include, redirect a ?next=... o /home. Si ya hay sesión
  activa, va directo a /home.
- public/app.js gate: chequea /api/auth/me antes de initRouter; sin sesión
  redirige a /login?next=<path>. window.__USER__ disponible para shell.
- ops-shell: nav agrega "Operadores" + "Actividad". Header derecha muestra
  email del user + botón Salir (POST /api/auth/logout + redirect /login).
- system-users-crud: CRUD lista/form (estilo settings). Crear/editar/
  cambiar password/eliminar. UI muestra badge "Vos" + bloquea eliminarse
  ni desactivarse a uno mismo.
- audit-log: tabla read-only con filtros (actor, entity_type, since,
  search), paginación 50, badges por acción, modal de detalles con
  changes JSON. /api/audit-log/actors pobla el dropdown de operadores.

Smoke E2E: login OK + cookie set, /me 200; logout → /me 401; settings POST
genera fila en audit_log con actor_email + action_path; ABM crea/borra
operadores con guards; intentar borrarse devuelve 400 cant_delete_self.

161/161 tests verde (pre-existentes 157 + 4 passwords nuevos).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:02:37 -03:00
Lucas Tettamanti
4a64256ef4 SPA: agregar /home a las rutas catch-all + sacar /test (ya borrado)
F5 en /home tiraba "Cannot GET /home" porque la ruta no estaba en la
lista de spaRoutes que sirve index.html. Quedó fuera cuando se introdujo
la nav con /home (antes el "/" cubría el caso).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:41:47 -03:00
Lucas Tettamanti
c410133c4c home: fix scroll vertical (host sin height definido)
:host no tenía display/height, así que min-height:100% del .container no
encontraba contenedor de referencia y los padres .view/.layout-crud
(overflow:hidden) recortaban el contenido. Movido el overflow-y:auto al
:host con height:100% explícito.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:40:36 -03:00
Lucas Tettamanti
cbbb88c052 Config: layout más amplio + toolbar sticky con Guardar
Antes: container 800px centrado, 3 panels stacked vertical, save al final.
Ahora:
- Container 1600px max, padding 24px.
- Toolbar sticky arriba con título "Configuración" + Restaurar + Guardar
  (reemplaza la sección final).
- Settings grid de 2 columnas (340px / 1fr) que colapsa a 1 col en <960px:
  - Izq: panel "Información del Negocio" (campos apilados, más densos) +
    panel "Retiro en Tienda" (toggle + grid).
  - Der: panel "Zonas de Entrega" full-width — el mapa ocupa la mayor parte
    de la pantalla (height:calc(100vh-220px), min 520px).
- zones-layout dentro del panel: 300px lista/form / 1fr mapa flex.
- Sin pérdida funcional: mismos campos, mismas validaciones, mismas tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:35:31 -03:00
Lucas Tettamanti
448b3d7c44 last_delivery: reusar dirección/zona del último pedido + retitular header
Persistencia: cuando confirm_order encola create_order y la orden tiene
shipping/zona/window, runTurn snapshot a context.last_delivery
{is_delivery, shipping_address, matched_zone, pending_location, delivery_window}.
pipeline preserva ese campo cuando resetea por stale (>24h), igual que
external_customer_id.

Agente lo ve via working_memory.last_delivery en cada turno.

Nueva tool reuse_last_delivery() que copia shipping_address + matched_zone
(+ pending_location) al order actual. Pickup-only sólo setea is_delivery=false.

systemPrompt: instrucciones para que el bot proactivamente ofrezca "te lo
mandamos al mismo lugar que la última vez (dirección, zona, $)" cuando
last_delivery existe y todavía no se eligió método de envío. Cliente puede
aceptar (reuse_last_delivery) o pedir otra dirección/retiro. delivery_window
NO se asume — siempre se vuelve a preguntar día/hora.

Smoke E2E: cliente recurrente con conversación stale 25h+
- 1ra orden: 1kg vacío → location → mar 12h → confirma.
- DB: context.last_delivery con zona Centro Test + dirección + ventana.
- 2da orden: "hola, 500g bondiola" → bot: "¿al mismo lugar (Av. Corrientes
  1234, Centro, $1.500)?" → "sí" → "¿qué día? La última fue mar 12h, puede
  ser otro" → "jueves 11hs" → orden cerrada sin re-pedir pin.

Header: "Bot Ops Console" → "Piaf Console" (index.html + ops-shell).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:33:58 -03:00
Lucas Tettamanti
3c70eb5ff7 Sacar pestaña Test del admin
- Borrado public/components/test-panel.js + su nav button + su rango de
  router + sus rutas /test/products-with-stock y /test/order.
- Borrados handleGetProductsWithStock + handleCreateTestOrder y los
  api wrappers getProductsWithStock + createTestOrder. Ya no se usan en
  ningún flujo: pruebas de bot se hacen vía simulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:29:20 -03:00
Lucas Tettamanti
c93955fa55 Limpiar legacy delivery_* + arreglar carga del mapa en shadow DOM
Backend cleanup (todo el delivery vive ahora en delivery_zones.zones[]):
- Migration drop columns delivery_enabled / delivery_days / delivery_hours_start /
  delivery_hours_end / delivery_min_order y limpieza de schedule.delivery JSONB.
- settingsRepo: SELECT/INSERT/UPDATE sólo con campos vigentes, formatScheduleHours
  trabaja sobre pickup.
- handlers/settings: defaults sin legacy, validateSchedule sólo para pickup,
  validateDeliveryZones nuevo (estructura GeoJSON + días).
- seed_piaf_settings_and_replies + tenant_settings migrations alineadas: schedule
  sólo tiene pickup, delivery_zones queda en {} para reconfigurar via UI.

Frontend cleanup:
- settings-crud: borrado el panel "Delivery (Envío a domicilio)" + minOrder,
  toggle deliveryEnabled, y el grid schedule.delivery. collectScheduleFromInputs
  ahora sólo procesa pickup. save() ya no envía delivery_enabled/min_order.

Fix mapa (no cargaba):
- zone-map-editor: los <link> a leaflet.css/leaflet-geoman.css se inyectaban en
  document.head, que NO cruza el shadow DOM de settings-crud, por lo que las
  reglas de Leaflet no aplicaban al div del mapa. Ahora los <link> se anclan
  como hijos del propio web component; al estar en light DOM dentro del shadow
  root del padre, sí aplican.
- Espera explícita a que el stylesheet cargue antes de instanciar L.map.
- ResizeObserver + invalidateSize() para cuando el contenedor cambia tamaño
  (router muestra/oculta panel, tabs, etc).

Smoke E2E sin regresión: 1kg vacío + envío → location en Centro → "martes 12hs"
→ orden confirmada con $28.000 (producto + envío). 157/157 tests verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:37:47 -03:00
Lucas Tettamanti
aed79078de Zonas de delivery por polígono + horarios + location share por WhatsApp
Schema delivery_zones JSONB pasa a { zones: [{ id, name, polygon (GeoJSON),
delivery_cost, delivery_days, delivery_hours, min_order_amount, enabled }] }.
Tirado el modelo legacy de 48 barrios CABA hardcoded.

Backend:
- lib/geo.js: pointInPolygon + findZoneForPoint (ray casting, sin deps) + 9 tests.
- storeContext.checkAddressInZone ahora valida con lat/lng (necesita ubicación
  del cliente; no geocodifica texto). buildZonesForLLM expone zonas resumidas
  para el agente. summarizeDeliveryZones genera prosa con costo+días+horas.
- settingsRepo expone delivery_zones (bug pre-existente: nunca se devolvía).
- pipeline: inboundLocation ⇒ persistir order.pending_location; orderModel
  acepta pending_location, matched_zone, delivery_window.

Intake:
- evolutionParser detecta locationMessage/liveLocationMessage (Baileys).
- evolution + sim handlers propagan inboundLocation al pipeline.

Agent (DeepSeek tool-calling):
- workingMemory inyecta store.delivery.zones[], store.pickup.schedule,
  order.pending_location/matched_zone/delivery_window.
- setAddress: matchea zona con la ubicación pendiente; sin location devuelve
  need_location y el LLM le pide el pin al cliente.
- setShipping: para delivery, indica requires_location si faltan coords.
- confirmOrder: valida día+hora contra zone.delivery_days/hours o pickup.schedule.
- nueva tool set_delivery_window(day, time?) para registrar el slot pedido.
- systemPrompt agrega instrucciones de envío/zonas + flujo location share.

Frontend:
- zone-map-editor: web component (light DOM) que carga Leaflet 1.9 +
  leaflet-geoman lazy desde CDN y permite dibujar/editar polígonos sobre OSM.
  API zones get/set, eventos change/select, paleta tomada de --chart-*.
- settings-crud: borrada lista CABA_BARRIOS, nueva UI con mapa al lado y
  formulario por zona seleccionada (nombre, costo, días, horario start/end,
  mínimo, habilitada). Save serializa al schema nuevo.

Smoke E2E manual:
- "1kg vacío + envío" → bot pide pin → location en Centro → matched_zone
  $1.500, lun-sab 10-20h → "martes 12hs" → confirma orden con total + envío.
- Location en Palermo Test → mar/jue 11-19h respetado.
- Location fuera de zonas → "no llegamos a esa zona" + lista de zonas válidas.
- Domingo en Centro → rechazado con días disponibles.

157/157 tests verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:31:25 -03:00
Lucas Tettamanti
0bf26f8eb5 Restyling light pastel: tema blanco/azul/verde + Inter self-hosted
- theme.css reescrito: paleta light (sky/emerald accents, slate neutrals),
  tokens de spacing/typography/radii/shadows, @font-face Inter +
  JetBrains Mono variable woff2 self-hosted.
- ops-shell: header blanco con nav active=accent-soft + status pill pastel.
- run-timeline: bubbles emerald-100 (user) / blue-100 (bot) sobre blanco.
- home-dashboard: helpers cssVar + withAlpha, 6 charts coordinados a
  paleta pastel (--chart-blue/green/purple/orange/pink/gray).
- 8 CRUDs (users, products, orders, conversations, aliases,
  recommendations, quantities, takeovers, settings, debug) migrados
  de hex hardcoded oscuros a var(--*).
- modal.js + toast.js refactor a vars con fallbacks; modal blanco
  con shadow-lg y soft icon backgrounds.
- test-panel: aliases :host apuntan a globals en vez de override dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:56:48 -03:00
Lucas Tettamanti
675a449ce8 D9 cleanup: borrar NLU/handlers/machine/replyTemplates legacy + activar agente + prompt caching
Después de validar el agente E2E con DeepSeek, el legacy queda muerto.
51 archivos cambiados (la mayoría borrados), el motor único es ahora el
agente tool-calling.

Borrados (~3500 LOC):
- src/modules/3-turn-engine/nlu/ (router + 4 specialists + promptLoader +
  schemas + humanFallback + 6 default prompts) — reemplazado por systemPrompt.js
- src/modules/3-turn-engine/stateHandlers/ (cart.js, cartHelpers.js, idle.js,
  shipping.js, utils.js, index.js) — reemplazado por tools del agente
- src/modules/3-turn-engine/stateHandlers.js (re-export shim)
- src/modules/3-turn-engine/openai.js (NLU clásico v3 + jsonCompletion +
  llmRecommendWriter + llmPlanningRecommend) — el agente crea su propio
  cliente OpenAI con tools nativos
- src/modules/3-turn-engine/replyRewriter.js (rewriting LLM) — el agente
  escribe say directo, no necesita reescribir
- src/modules/3-turn-engine/replyTemplates.js + test (rotación de variantes)
  — el agente varía naturalmente con tool_choice=required + temperature
- src/modules/3-turn-engine/recommendations.js (cross-sell + planning) —
  el agente decide cuándo recomendar via tool calls
- src/modules/3-turn-engine/machine/ (XState v5 completo + 19 tests) —
  reemplazado por la FSM podada en fsm.js + agent/runTurn.js
- src/modules/3-turn-engine/turnEngineV3.helpers.js, .units.js,
  .pendingSelection.js (helpers del legacy)
- src/modules/0-ui/controllers/prompts.js, handlers/prompts.js,
  db/promptsRepo.js — admin de prompts NLU (ya no hay prompts editables)
- public/components/prompts-crud.js + nav entry en ops-shell

turnEngineV3.js se reduce a un thin wrapper que exporta runTurnV3 (alias
de runTurnAgent) + safeNextState (re-export de fsm.js). Mantiene la firma
pública para no tocar pipeline.js.

Activado:
- AGENT_MAX_TOOL_CALLS=10 y AGENT_TURN_TIMEOUT_MS=25000 son los únicos
  flags. Borradas: USE_MODULAR_NLU, USE_XSTATE, XSTATE_SHADOW,
  XSTATE_SETTLE_MS, REPLY_REWRITER, REPLY_REWRITER_TIMEOUT_MS, TURN_ENGINE,
  AGENT_TURN_ENGINE, AGENT_TURN_ENGINE_SHADOW (el agente es default).

Prompt caching DeepSeek:
- systemPrompt.js: era función con storeName interpolado → ahora export
  const SYSTEM_PROMPT (100% estático). storeName se pasa por user message
  via working_memory.store.name. Cualquier cambio al system invalida cache,
  por eso es estático estricto.
- runTurn.js: captura usage.prompt_cache_hit_tokens (DeepSeek) o
  prompt_tokens_details.cached_tokens (OpenAI compat) y suma a métricas.
- /api/metrics/agent ahora reporta prompt_tokens_total,
  completion_tokens_total, prompt_cache_hit_tokens, cache_hit_ratio.
- Smoke test 3 turnos: cache_hit_ratio = 0.72 (17664 cached / 24546 total
  prompt tokens). Saving directo en costo: ~$0.02/M cached vs $0.27/M no
  cached en DeepSeek.

Tests: 148/148 (perdimos 90 tests del legacy XState/replyTemplates que
ya no aplican). Sim flow E2E confirmado: hola → agent responde, multi-turn
con cache caliente.

Si más adelante hace falta volver al legacy: git revert este commit
(c c9c69cf8 es el último estado verde con doble motor).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 13:14:59 -03:00
Lucas Tettamanti
03621f16f4 Redesign: agente tool-calling con DeepSeek (D2-D10 del plan)
Reemplaza el NLU rígido (intent+entities) por un agente LLM con tool-calling
que decide y muta estado en cada turno. Opt-in vía AGENT_TURN_ENGINE=1.
DeepSeek V4 (deepseek-chat) configurado como modelo (OpenAI-compatible).

Arquitectura nueva en src/modules/3-turn-engine/agent/:

- workingMemory.js: arma el JSON contextual que recibe el LLM cada turno
  (cart, pending, last_shown_options, store, customer_profile, history,
  preparsed quantity).
- systemPrompt.js: prompt estático ~70 líneas. Define rol + reglas duras +
  cómo procesar mensajes + cómo escribir el say. Sin enumeración de intents.
- runTurn.js: loop de tool-calling con tool_choice="required". Cap 10 tool
  calls / 20s timeout. Métricas in-memory.
- customerProfile.js: lookup de frequent_items en woo_orders_cache por
  teléfono (chat_id → phone), top 5 últimos 6 meses. Cache 10 min.
- tools/schemas.js: 11 tools (search_catalog, add_to_cart, set_quantity,
  select_candidate, remove_from_cart, set_shipping, set_address,
  confirm_order, pause, escalate_to_human, say).
- tools/executor.js: validación Ajv + dispatch + observación al LLM.
  woo_id se valida contra snapshot — si no existe el agente vuelve a
  search_catalog (anti-halucinación).
- tools/searchCatalog.js: wrappea retrieveCandidates + fallback por
  categoría usando jsonb_array_elements_text del snapshot. Persiste
  last_shown_options automáticamente.
- tools/{addToCart, setQuantity, selectCandidate, removeFromCart,
  setShipping, setAddress, confirmOrder, pause, escalateToHuman}.js:
  side effects atómicos sobre el order.
- quantityParser.js (D1): determinístico, parsea fracciones, frases
  compuestas (media docena, cuarto kilo), numéricos. 46 tests.

FSM extendida (fsm.js): nuevo estado PAUSED (TTL 7d, cart preservado,
"después te digo" → pause tool).

pipeline.js: TTL stale ahora 24h general, 7d si PAUSED, infinito si
AWAITING_HUMAN.

turnEngineV3.js: nuevas flags AGENT_TURN_ENGINE y AGENT_TURN_ENGINE_SHADOW.
Branch a runTurnAgent cuando full o corre en paralelo escribiendo diffs
estructurales en audit_log (entity_type='agent_shadow') para validar
paridad antes de flippar.

Endpoint nuevo: GET /api/metrics/agent → turns, avg_tool_calls, fallback
rate, escalations, pauses, orders_confirmed.

Smoke test E2E con DeepSeek real:
- "hola" → say (2.3s, 1 tool)
- "2kg de vacio" → search → add_to_cart → say (8.8s, 3 tools)
- "media docena de chorizos" → search → say con clarificación (10.3s, 4 tools)
- "listo" → say (3.3s, 1 tool)
- "retiro" → set_shipping → confirm → say (5.1s, 3 tools)
Cart final correcto: 2kg de Vacío. Estado: CART → SHIPPING.

Tests: 238/238 pasando.

D9 (cleanup legacy ~1200 LOC NLU/handlers/replyRewriter) DEFERRED:
se hace después de paridad shadow validada con tráfico real. Hoy
agente coexiste con legacy; default sigue siendo el motor V3.

Plan completo en ~/.claude/plans/ok-creo-que-tiene-humming-sutton.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:52:47 -03:00
Lucas Tettamanti
9c69cf8911 D1 redesign: quantityParser determinista (es-AR) + 46 tests
Primer paso del rediseño tool-calling agent. Setup:
- DeepSeek V4 confirmado vía OPENAI_BASE_URL en .env (no commiteado).
- Smoke test exitoso: tools+tool_choice nativos andan con deepseek-chat.

Nuevo: src/modules/3-turn-engine/agent/quantityParser.js
- Parser determinista que pre-procesa la query del usuario para extraer
  cantidad+unidad ANTES del LLM. Resultado va al agente como side-channel
  (working_memory.preparsed); el agente puede sobreescribirlo.
- Cubre AR-es: fracciones (1/4 kg, 3/4), frases compuestas (media docena,
  cuarto kilo, tres cuartos, medio kilo, par, docena), numéricos pegados
  (2kg, 0.5kg, 500g, 2,5 kilos), numéricos solos, palabras + unidad.
- Confidence escalonado: fraction 0.95, phrase/numeric+unit 0.9,
  word+unit 0.85, numeric solo 0.7.

Tests: 46/46 pasan, incluyen casos de WhatsApp real, casos negativos,
edge cases (división por cero, string vacío, decimales con coma).

Total suite: 238/238 (192 previos + 46 quantity).

Próximos pasos del plan: D1 workingMemory.js + runTurn skeleton, D2 tools
cart, D3 tools shipping/checkout, D4 customerProfile, D5 catalog fallback,
D6 system prompt + tuning, D7 persistencia, D8 shadow validation,
D9 cleanup legacy, D10 hardening. Plan completo en
~/.claude/plans/ok-creo-que-tiene-humming-sutton.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:31:59 -03:00
Lucas Tettamanti
6376739f48 Frontend: scroll fix, SSE reconnect, toast global, stale states, theming
5 bloques en una pasada:

A. Scroll fixes (síntoma reportado por el usuario):
   - run-timeline: nuevo flag _userScrolledUp con detección por rAF en
     bindScroll. Auto-scroll al final SOLO si el usuario está abajo
     (umbral 150px) o si él mismo disparó un optimistic bubble. Cuando
     el usuario manda mensaje, se resetea el flag y se scrollea.
   - conversation-inspector: mismo patrón. ui:chatScroll respeta el
     flag userScrolledUp del emisor para no perseguir al usuario que
     lee arriba.
   - .bubble: + min-width:0, overflow-wrap:anywhere para URLs/JSON
     largos sin espacios.
   - <pre>: + overflow-x:auto, max-width:100%.
   - chat-simulator: textarea con resize:vertical, min/max heights
     viewport-friendly. inputs-col con overflow-y:auto.

B. Stale state options:
   - conversation-list y conversations-crud: dropdowns ahora muestran
     IDLE / CART / SHIPPING / AWAITING_HUMAN. Quitados BROWSING,
     BUILDING_ORDER, WAITING_ADDRESS, WAITING_PAYMENT, COMPLETED.
   - main.js: simulated plan.next_state pasa a CART.

C. SSE resilience (lib/sse.js):
   - connect() con backoff exponencial (1s → 30s).
   - safeParse() helper: cada evento envuelve JSON.parse en try-catch
     para que un payload malformado no rompa otros listeners.
   - reset retryDelay al primer "hello" exitoso.
   - ops-shell: indicador con dot (verde "En vivo" / naranja pulsante
     "Reconectando…") en lugar de texto plano.

D. Toast service global (public/lib/toast.js):
   - API simple: toast({ kind, text, ms }). Apila, animación slide-in,
     auto-dismiss 4s. Click para cerrar.
   - safeFetch en api.js: wrapper que dispara toast en network error y
     non-OK. Migrados simEvolution + retryLast.
   - chat-simulator usa toast en lugar de status text efímero.

E. Theming con CSS vars:
   - public/styles/theme.css con paleta completa (panels, borders,
     text, accents, bubbles, charts, radii, shadows). Linkeado desde
     index.html.
   - Migrados a var(--*) los 5 componentes más visibles:
     run-timeline, chat-simulator, conversation-inspector,
     conversation-list, home-dashboard. Custom properties heredan a
     través del shadow DOM, así que los demás componentes pueden
     migrar gradualmente sin cambios estructurales.
   - home-dashboard ya tenía vars locales: ahora apuntan a las globales.

Backend: 192/192 tests pasando. Sin cambios de API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 03:25:03 -03:00
Lucas Tettamanti
7b6c62b23d Mejoras: idempotency webhook, métricas rewriter, recommend en XState, seeds
7 frentes en una pasada:

1. Idempotency en pipeline.processMessage: si el message_id ya existía
   (Evolution suele reentregar webhooks), skip todo el turn y devuelve
   { duplicate: true }. Antes el ON CONFLICT DO NOTHING evitaba el insert
   pero igual procesaba NLU + side effects.

2. Limpieza de payment residual de admin/UI:
   - ordersRepo.getMonthlyStats / getTotals: out las queries cash/card
   - home-dashboard: out el donut "Efectivo vs Tarjeta"
   - orders-crud: out columna "Pago" + sección de detalle de pago
   - conversation-inspector: out 💵💳 del resumen
   - takeovers: out payment_link en run, out payment del summary
   - public/main.js: out la invariant no_checkout_without_payment_link
   - prompts-crud: out la entry "payment" del PROMPT_LABELS
   - wooOrders.parseOrder: out lectura de payment_method/is_paid (estado
     del pago lo gestiona el comercio offline, fuera del bot)
   - ordersRepo: out is_cash/is_paid del row mapping

3. Seed migration 20260501130000_seed_piaf_settings_and_replies:
   - schedule realista para piaf (delivery/pickup días+horas)
   - delivery_zones con barrios CABA reales (Palermo, Belgrano, etc)
   - 41 reply_templates con 17 keys + variantes (todo lo de DEFAULTS)
   Permite editar respuestas sin redeploy y desbloquea {{store_hours_today}},
   {{delivery_zones_summary}} reales en los templates.

4. Address validation en shipping: nuevo checkAddressInZone() en
   storeContext.js. Cuando el usuario da dirección, se valida contra
   zonas configuradas. Si está fuera, renderiza shipping.address_out_of_zone
   con sugerencia de zonas alternativas. Sin zonas configuradas → accept-by-default.

5. Métricas rewriter: getRewriterMetrics() expuesto, contadores
   ok/fallback/timeouts + avg_ms + fallback_rate. Endpoint nuevo
   /api/metrics/rewriter.

6. Shadow XState → audit_log: el shadow mode pasa de console.log a
   insertAuditLog con entity_type='xstate_shadow'. Permite review post-mortem.

7. Recommend portado a XState: nuevo recommendActor (fromPromise) wrappea
   handleRecommend; sub-state cart.recommending invoca el actor; ingestRecommendResult
   absorbe { plan, decision } en context. RECOMMEND event funciona desde idle
   y desde cart con USE_XSTATE=1.

8. tenantId opcional en renderReply/loadReplyVariants — defaultea a
   getTenantId() del módulo shared/tenant.js. Backward-compat: callers
   pueden seguir pasando tenantId o omitirlo.

E2E tests nuevos en machine/e2e.test.js: golden flow pickup, golden flow
delivery con address-in-zone, snapshot rehydrate full flow, universal
cart-on-add desde shipping. 192/192 tests pasando (188 previos + 4 E2E).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:18:29 -03:00
Lucas Tettamanti
6b7889ef4e Mono-tenant: resolver id una vez al boot, eliminar lookups por turno
El sistema nunca fue realmente multi-tenant en la práctica. El esquema
DB conserva las columnas tenant_id (queda lista para escalar más adelante
sin migración), pero la app ahora resuelve el tenant una sola vez al
arranque y todas las capas leen de un único punto.

- src/modules/shared/tenant.js: nuevo módulo. setTenant() en boot,
  getTenantId() lo lee desde cualquier lado.
- index.js: ensureTenant() → setTenant({ id, key }). Sin cambios externos.
- pipeline.resolveTenantId(): pasa de hacer 1-2 queries a DB por turno
  a un return sincrónico del id cacheado. Mantiene firma async para no
  romper callers.
- intake handlers (sim.js, evolution.js): usan getTenantId() directo,
  sin parsing de tenant_key del chat_id ni lookup por canal.
- wooWebhooks: ya no requiere ?tenant_key=... en la query string.
  El webhook va al único tenant configurado.
- repo.js: eliminados getTenantByKey() y getTenantIdByChannel() (no más
  callers).

Plumbing del parámetro tenantId en signatures de handlers/repos/machine
queda intacto — bajar eso es ruido de alto riesgo y no aporta hoy.
188 tests pasando.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:00:22 -03:00
Lucas Tettamanti
17cea4aa9e Eliminar payment + waiting (legacy): el bot toma pedidos, no cobra
El bot conversacional no maneja pagos. Su trabajo: pedidos, datos de
entrega, dejar la orden anotada en Woo (status=pending). El cobro lo
gestiona el comercio offline. Todo lo de payment_type / is_paid /
PAYMENT / WAITING_WEBHOOKS era legacy de un flow viejo que se baja.

Nuevo flow: IDLE → CART → SHIPPING → IDLE (con orden creada).
Cuando el usuario completa shipping (pickup elegido OR delivery+address),
shipping.js emite create_order y el bot cierra con order.confirmed.

- fsm.js: 4 estados (IDLE/CART/SHIPPING/AWAITING_HUMAN). hasPaymentInfo
  e isPaid eliminados. deriveNextState gira SHIPPING→IDLE en vez de
  →PAYMENT→WAITING. ALLOWED transitions actualizadas.
- orderModel.js: createEmptyOrder() sin payment_type/is_paid.
  migrateOldContext deja de leer payment_method / mp.payment_status.
- stateHandlers: payment.js y waiting.js eliminados. shipping.js gana
  finalizeOrder() que emite create_order action y vuelve a IDLE.
- replyTemplates: payment.* y waiting.* fuera. order.confirmed nuevo,
  con 3 variantes y rewriter habilitado.
- NLU openai.js + nlu/schemas.js: select_payment fuera del enum, payment_method
  fuera de entities. Prompt sin la regla de SELECCIONAR PAGO.
- nlu/router.js + nlu/index.js: dominio "payment" eliminado.
  shouldSkipRouter ya no chequea PAYMENT.
- nlu/specialists/payment.js: eliminado.
- promptsRepo.js + promptLoader.js: PROMPT_KEYS sin "payment".
- turnEngineV3.js: switch ya no dispatcha a PAYMENT/WAITING. normalizeState
  mapea estados legacy (PAYMENT/WAITING_WEBHOOKS/COMPLETED) a IDLE.
  context_patch ya no emite payment_method.
- wooOrders.createOrder: paymentMethod param eliminado. Order queda en
  status=pending sin payment_method (cobro offline).
- pipeline.js: paymentMethod fuera del create_order glue. Invariant
  "no_checkout_without_payment_link" eliminado. signal payment_selected
  reemplazado por shipping_completed.
- XState machine: top-level PAYMENT y WAITING eliminados. SELECT_PAYMENT
  event fuera. SHIPPING ahora cierra con enqueueWooCreateOrder +
  replyOrderConfirmed → IDLE. Guards hasPayment/isPaid borrados.
- Tests fsm.test.js / orderModel.test.js / machine/index.test.js
  actualizados al nuevo contrato. 188 tests pasando.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:53:19 -03:00
Lucas Tettamanti
04ac33430f Tier 2: XState statechart como motor de turno (opt-in)
Reemplaza el dispatcher en turnEngineV3.js por un statechart formal en
XState v5. La machine es pura: produce un effect log (pending_actions) +
un descriptor de reply (pending_reply) que el runner traduce afuera.

API externa intacta: runTurnV3 sigue retornando { plan, decision } con
shape compatible con pipeline.js. Snapshot persiste en
context.xstate_snapshot dentro del JSONB existente.

- machine/index.js: statechart top-level (idle/cart/shipping/payment/
  waiting/awaiting_human) + cart sub-statechart con todo el flujo
  multi-turno (searching/resolving/askingClarification/askingQuantity/
  computingFromPersonas/added/showing/pricing/researching).
- guards.js: portados de fsm.js (hasCart, wantsToAddProduct, etc).
- actions.js: assigns para mutations + reply descriptors (pending_reply
  con templateKey/vars/rawText). Las async no entran en la machine.
- actors.js: fromPromise wrappers de retrieveCandidates y getProductQtyRules.
- runner.js: boot con prev_context.xstate_snapshot o migrateOldContext.
  NLU → nluToEvent → send → settle (espera invokes) → realizeReply
  (renderReply real con rewriter) → getPersistedSnapshot → format.
- nluToEvent.js: adapter NLU intent → evento XState (1:1).

Feature flags: USE_XSTATE=1 reemplaza el path; XSTATE_SHADOW=1 corre
ambos en paralelo, devuelve legacy y loguea diffs estructurales para
validar antes de flippar prod.

16 unit tests para la machine cubren: arranque, regla universal cart-on-add,
flow de cart con strong/multi match, checkout completo (shipping/pickup/
payment/cash) y rehidratación de snapshot. 224 tests totales pasando.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:38:26 -03:00
Lucas Tettamanti
f784ddd62d Tier 1: chat quality — fuzzy aliases, reply templates, dedup, rewriter
Foco: matar repetición y adaptar respuestas. Los handlers tenían ~30 strings
hardcodeadas (3-7 lugares cada una). Aliases hacían substring exacto.

- pg_trgm + GIN indexes en product_aliases / alias_product_mappings.
  Captura plurales, diminutivos, typos sin reglas. catalogRetrieval re-busca
  el snapshot con normalized_alias cuando el query original no rinde
  (vasio→vacio→Vacío).
- reply_templates table + replyTemplates.js. 20 keys, 2-3 variantes c/u
  con DEFAULTS hardcodeados como fallback. pickVariant excluye las usadas
  en context.recent_replies (FIFO cap 8). Wired en idle/cart/cartHelpers/
  shipping/payment/waiting.
- failed_searches counter en context. count>=3 escala via humanFallback.
  Reset en cada add_to_cart exitoso.
- storeContext.js: vars derivadas de getStoreConfig (delivery_zones, hours,
  zonas) listas para inyectar en templates cuando los datos se carguen.
- replyRewriter.js: LLM call opcional (REPLY_REWRITER=1) que adapta el
  template al hilo conversacional. 1.5s timeout, fallback al template puro.
  Sólo activo en 8 slots semánticamente importantes.
- 12 unit tests para replyTemplates (rotation, recency, FIFO, vars).
  208 tests totales pasando.

Plan completo: ~/.claude/plans/ok-creo-que-tiene-humming-sutton.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 19:29:02 -03:00
Lucas Tettamanti
525679cf8b local dev setup + OPENAI_BASE_URL support + dashboard fix
- CLAUDE.md con arquitectura y comandos del proyecto
- env.example: agregar LIMIT_CONVERSATIONS, MAX_CHARS_PER_MESSAGE, OPENAI_BASE_URL
- docker-compose.override: puerto 3001, extra_hosts para modelo local en Linux
- OpenAI clients: soporte OPENAI_BASE_URL para apuntar a modelo local compatible
- stats.js: sync de órdenes en background, dashboard no bloquea al cargar
- package-lock: dbmate movido a prod dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:32:22 -03:00
Lucas Tettamanti
b933db88df remove database_url 2026-02-04 18:36:28 -03:00
Lucas Tettamanti
d8a0677912 more work with Dockerfile and dbmate 2026-02-04 18:16:32 -03:00
Lucas Tettamanti
f838603877 Docker compose and coolify solved 2026-02-04 17:59:30 -03:00
Lucas Tettamanti
5e79f17d00 20260204 2026-02-04 16:06:51 -03:00
Lucas Tettamanti
2f8e267268 docker compose override for local without affecting coolify 2026-01-27 03:13:44 -03:00
Lucas Tettamanti
1e84d19db8 configs 2026-01-27 02:59:31 -03:00
Lucas Tettamanti
df9420b954 dashboard 2026-01-27 02:41:39 -03:00
Lucas Tettamanti
493f26af17 corregidos bugs de: ret, vs delivery, efectivo vs link, charsets, price query 2026-01-26 23:27:47 -03:00
Lucas Tettamanti
53293ce9b3 badges on the right, evolution api sender 2026-01-26 01:21:08 -03:00
Lucas Tettamanti
e85afab3e6 vitest 2026-01-26 00:13:03 -03:00
Lucas Tettamanti
b1c8a3685c refactor stateHandlers 2026-01-25 23:58:56 -03:00
Lucas Tettamanti
debad78781 borrado de articulos del carrito 2026-01-25 23:43:00 -03:00
Lucas Tettamanti
bd63d92c50 modificando el patron del sistema, orientado mas al usuario 2026-01-25 22:32:58 -03:00
Lucas Tettamanti
93e331535f correjido unidades y kgs 2026-01-25 21:08:23 -03:00
Lucas Tettamanti
bb947ea75e remove users, chats, etc 2026-01-25 21:04:54 -03:00
Lucas Tettamanti
a489ec66a2 modularizado de prompts 2026-01-25 20:51:33 -03:00
Lucas Tettamanti
b91ece867b routes updated 2026-01-18 20:28:27 -03:00
Lucas Tettamanti
9754347a36 pedidos 2026-01-18 20:07:40 -03:00
Lucas Tettamanti
23c3d44490 audit and sync 2026-01-18 19:00:49 -03:00
Lucas Tettamanti
3b39e706af categorias working well 2026-01-18 18:32:47 -03:00
Lucas Tettamanti
c7c56ddbfc productos, equivalencias, cross-sell y cantidades 2026-01-18 18:28:28 -03:00
Lucas Tettamanti
8cc4744c49 carrito semi saneado 2026-01-17 15:20:32 -03:00
Lucas Tettamanti
204403560e mejoras en el modelo de clarificacion de productos 2026-01-17 06:31:49 -03:00
Lucas Tettamanti
63b9ecef61 ux improved 2026-01-17 04:13:35 -03:00
Lucas Tettamanti
98e3d78e3d modules/0-ui 2026-01-15 22:53:37 -03:00
Lucas Tettamanti
ea62385e3d separated in modules 2026-01-15 22:45:33 -03:00
Lucas Tettamanti
eedd16afdb added Mercado Pago integration with new payment handling functions and updated app routing 2026-01-15 13:06:37 -03:00
Lucas Tettamanti
29fa2d127e added NLU v3 functionality with JSON schema validation and error handling in OpenAI service 2026-01-14 18:16:59 -03:00
Lucas Tettamanti
47ba68049f separado en routes 2026-01-14 13:03:11 -03:00
Lucas Tettamanti
2d01972619 travel to another computer 2026-01-10 12:39:32 -03:00