Commit Graph

12 Commits

Author SHA1 Message Date
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
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
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
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
df9420b954 dashboard 2026-01-27 02:41:39 -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
c7c56ddbfc productos, equivalencias, cross-sell y cantidades 2026-01-18 18:28:28 -03:00
Lucas Tettamanti
63b9ecef61 ux improved 2026-01-17 04:13:35 -03:00
Lucas Tettamanti
8bb21b4edb mejoras varias en frontend, separacion de intent y state, pick de articulos 2026-01-06 15:50:02 -03:00
Lucas Tettamanti
5c67b27859 base con front 2026-01-01 22:49:44 -03:00