NormalBot new flow types:
- collect_input: asks user a question, saves answer to metadata
- dynamic_list: calls endpoint to get options, displays WhatsApp list,
saves selection to metadata; max 10 rows
- submit_form: reads accumulated metadata fields and sends to api_report
Commands always take priority (escape from any collecting state).
Interactive selections during collecting state are caught before the
static menu lookup.
Admin flow editor redesign:
- All fields use select dropdowns (menus, endpoints, next-node)
- Grouped options: Respuesta / Navegar / Acción directa / Formulario paso a paso
- collect_input, dynamic_list, submit_form panels with contextual fields
- addNewFlow() opens blank modal; saveFlow() handles all types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pass ENDPOINTS to JS context from company_endpoints table
- Add api_report and goodbye options to flow function select
- When api_report selected: show endpoint dropdown + date period +
filename + caption fields
- saveFlow() persists params.endpoint_key, date_mode, caption, filename
- flowFuncToggle() shows/hides the endpoint panel on function change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Text/interactive/button messages → NormalBot only, never escalates to AI
- Image/audio/video/document → AiBot.processMedia() if ai_for_media enabled,
else falls back to category greeting menu
- AiBot.processMedia() uses permission_type context so AI acknowledges uploads
for perm 3 and redirects others to their menu
- WpWebhook.handleMedia now calls BotRouter so the bot responds to media
- Admin UI: checkbox toggle "Procesar imágenes y archivos con IA" in AI tab
- categoryMenuFallback() extracted in BotRouter as shared helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a "Por Categoría" tab in the bot config UI where each permission
category (1, 2, 3) can be assigned its own greeting button menu.
When a user writes without an active context, the bot shows the menu
configured for their category instead of falling through to the global
greeting text.
NormalBot.process() now checks per_type[X].greeting_menu first, which
directly references a menu key and serves as both greeting and fallback
for that category. Fully backward-compatible with existing greeting_flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
conversationFlow saveConfig():
- Was not serializing buttons[] array for button-type menus (would erase
all buttons on save). Now sends menu_btn_id/menu_btn_title like botConfig.
- Defaulted menu type to 'list' instead of 'button'.
conversationFlow addNew('menu') modal:
- Defaulted to 'list' with header/footer/button fields.
- Now defaults to 'button' with inline buttons editor; toggle via type select.
- saveNewMenu() creates correct {type,body,buttons} or {type,...,sections}.
conversationFlow orphan-flow detection:
- Only checked sections[].rows, so flows linked via button menus appeared
as orphaned. Now checks buttons[] array for button-type menus.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of making a separate fetch to /admin/chat/conversations (which
Nginx intercepts), pre-load conversations in the PHP chat() method and
embed them as window.INITIAL_CONVS JSON in the page script.
renderConvs(INITIAL_CONVS) runs synchronously on page load — no HTTP
request needed. setInterval still tries to refresh via fetch every 15s
but the initial render no longer depends on it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nginx intercepts any request to /admin/chat/* sub-routes. Fix by handling
all chat API requests on the same /admin/chat path using query params:
GET /admin/chat?api=convs → conversations list
GET /admin/chat?api=msgs&phone=X → messages for a phone
POST /admin/chat → send message
JS fetch URLs updated accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/admin/chat/conversations → /admin/chat-convs
/admin/chat/messages → /admin/chat-msgs
/admin/chat/send → /admin/chat-send
The production server was intercepting all subpaths of /admin/chat and
redirecting them back to /admin/chat, so fetch never received JSON.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If msgInput element is somehow null, addEventListener throws and prevents
loadConvs() from being called — wrapping in null check makes it safe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NormalBot: fix type check 'buttons' -> 'button' in buildMenuResponse
- DashboardController: botConfig editor now shows buttons editor for button-type menus,
list fields (header/footer/button trigger) hidden for button type; menuTypeChange/addBtnRow
JS functions toggle the UI; addMenu() defaults to button type
- DashboardController: renderMenuNode and renderFlowNode now read buttons[] array for
button-type menus instead of only sections[].rows
- DashboardController: editMenu modal shows buttons editor or sections editor based on type;
editMenuTypeToggle/addEditBtnRow; saveMenu reads buttons for button type
- public/index.php: botConfigSave handler parses menu_btn_id/menu_btn_title for button menus;
saves compact {type,body,buttons} structure; list menus still save {type,header,...,sections}
- DB: migrated all list menus to button type for both companies (show_main_menu,
submenu_informes, submenu_ciclos, show_menu_cat1, show_menu_cat2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Saves a raw entry to webhook_logs immediately on any POST to the webhook,
before HMAC check or JSON parsing. Shows HMAC status (ok/invalida/sin-firma).
Live feed now shows all traffic including rejected and malformed payloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSE kept a PHP-FPM worker open for 28s per connection, blocking the whole
server with only 20 workers shared across all vhosts. Polling with
setInterval(3000) releases the worker on each request immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add gemini_api_key and gemini_model to allowed keys in settings save handler
- Add POST /admin/settings/test-ai endpoint for testing Gemini/OpenAI connection
- Add "Probar conexion IA" card in settings page with live feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings: agrega Gemini al selector de proveedor, campos gemini_api_key y gemini_model
- Modelos: gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
- AiBot::callGemini(): llama generateContent API con system_instruction y historial
- migrate.php: seed defaults gemini_api_key y gemini_model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Consume el endpoint numeros_dn configurado para la empresa
- Acepta JSON con array raíz o {numeros:[...]}
- Mapea campos wa_number/numero/phone, nombre/name/label, permiso/permission_type/tipo
- Upsert en company_phones: nuevos se insertan, existentes se actualizan
- Reporta conteo de nuevos / actualizados / omitidos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- El número WA Business es global (configuración general), no por empresa
- OutboundWorker usa fallback a WHATSAPP_DEFAULT_PHONE_NUMBER_ID si la empresa no tiene
- CompanyRepository::save() ya no requiere phone_number_id
- phone_number_id permite DEFAULT '' en schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El routing ahora es por número remitente (company_phones), no por phone_number_id.
phone_number_id solo se usa para enviar mensajes salientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convierte backtick template literals con \${it.id} a concatenación de strings
- Fix query de chatConversations: GROUP BY solo por phone_number, evita duplicados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /admin/test-message: formulario con empresa, número, nombre y texto
- POST /admin/test-message/send: construye payload WhatsApp, firma HMAC y
hace POST al webhook propio — pipeline completo sin tocar Meta
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Layout: fusiona topbar + nav en una sola barra de 56px (elimina doble menú)
- Desktop: brand izquierda, topnav centrado con underline activo, usuario derecha
- Móvil: hamburger animado (3 líneas → X), drawer lateral con overlay
- app.css: reescritura completa sin gradientes ni sombras pesadas
- DashboardController: corrige heights 104px→56px (chat, bot-config tabs)
- Live Feed: cierra EventSource en pagehide para evitar bloqueo al navegar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- admin/Layout.php: nueva clase compartida con topbar, nav agrupado y hamburger móvil
- assets/app.css: CSS centralizado (~130 líneas), elimina ~2000 líneas duplicadas
- DashboardController: 11 vistas refactorizadas con Layout; tarjetas de stats clicables;
drawer de aprobación rápida desde el dashboard; Live Feed reemplaza polling 3s por SSE;
bot-config con preview WhatsApp en tiempo real; chat con back-button móvil y textarea auto-resize
- public/index.php: rutas SSE (/admin/webhook/stream/sse) y pending-list (/admin/pending-list)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>