The JS FormData was built from the CONFIG object (DB state), missing
all AI inputs the user types: api_key, models, checkboxes (nlu,
ai_for_media). Now reads them directly from DOM before the fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NluRouter in AiBot: builds permission-filtered flow catalog, asks AI
to route user intent to exact flow key or return chat response
- MediaTranscriber: downloads WA media, transcribes audio (Whisper/Gemini),
extracts image intent (OpenAI/Gemini/Claude vision)
- BotRouter hybrid: text → NormalBot → NLU fallback if no match;
audio/image → transcribe → NLU route; sends transcript preview first
- Admin IA tab: NLU checkbox with description
- Per-company nlu flag saved in config_json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Global settings: custom AI panel with OpenAI/Gemini/Claude each with
API key, model selector, and live test-connection button (no save needed)
- Per-company IA tab: same three providers + fallback to global when empty
- AiBot: reads per-company config_json first, falls back to env() global
- Added callClaude() for Anthropic Messages API
- More OpenAI models: gpt-4.1, gpt-4.1-mini
- test-ai endpoint: accepts provider/api_key/model from POST for live test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added gemini-2.5-flash to the admin panel selector (first/default option)
and updated the fallback default in AiBot and migrate seed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After user selects a company, send short confirmation text then reset
bot_context and trigger BotRouter with empty message to fire the greeting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando un número pertenece a más de una empresa, el bot muestra un
menú interactivo "¿Con cuál empresa deseas comunicarte?" antes de
cualquier flujo normal. La selección se cachea 8h en la tabla
multi_company_sessions. Keywords de reinicio (salir, inicio, menú...)
limpian la sesión y vuelven a mostrar el pre-menú.
Números de una sola empresa: flujo intacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Banner en tab Endpoints muestra la URL base de General
- Inputs de URL aceptan rutas relativas (?param=x o /ruta)
- Preview live de la URL completa (base+ruta) bajo cada input
- Backend prepend api_base_url en test y sync si URL no es absoluta
- Compatible hacia atrás: URLs absolutas existentes siguen funcionando
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega panel de clonación en la pestaña Endpoints API del editor de empresa.
Permite copiar los endpoints de otra empresa con dos modos: sobreescribir todo
o solo añadir los endpoints que aún no tienen URL configurada.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace onchange+AJAX with explicit button '💾 Guardar categorías'
that shows alert on success/error for clear feedback
- Get company_id from DOM (not COMPANY_ID JS const from another script block)
- Main form save now preserves welcome_menu and per_type from DB even if
POST is truncated by max_input_vars
- Restore welcome_menu='welcome_msg' in DB (was wiped by a previous save)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The main form exceeded max_input_vars (27 flows × 37 fields each) so PHP
silently dropped per_type_menu before reaching the save handler.
- Add POST /admin/bot-config/save-per-type — reads existing DB config,
merges only the per_type_menu values, saves back
- Per-type selects fire savePerType() onchange → AJAX → toast on success
- Add public/.user.ini with max_input_vars=5000 as belt+suspenders
- Remove array_is_list() (PHP 8.1) — replaced with PHP 7.4 compatible check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show welcome_msg (button with "Ver menú") for all new sessions before category menu
- Fix saveConfig() AJAX to include per_type_menu DOM values so they survive individual saves
- Add welcome_menu field to General tab in admin (render + save)
- Add 'atras' command alias (→ show_main_menu)
- Add ciclos_plagas, ciclos_palm, ciclos_tratamientos flows to DB config
- Fix per_type in DB from [] to {} (proper JSON object)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixed vars use their preset (month_start, current_date, etc.); ask-mode
vars fall back to today's date so the test always sends a valid URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin: each endpoint with {vars} shows a vars config section; per-var
mode is "ask user" (bot prompts in WhatsApp) or "fixed" (preset: today,
month_start, month_end, last_7, last_30, year_start, or literal)
- Bot: resolves fixed vars before calling endpoint; queues "ask" vars and
collects them sequentially from the user before executing the report
- date_mode now uses fecha_desde/fecha_hasta (ERP expected names); added
current_month mode
- JSON responses from ERP (sin_datos etc.) shown as text, not uploaded as file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveConversation() now returns bool; BotRouter only runs when the
message_id is new (INSERT IGNORE rowCount>0). Prevents WhatsApp
webhook retries from triggering duplicate bot responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of defining fields inline per flow, the admin now defines them
once on the endpoint (direction=upload). The bot reads body_fields from
company_endpoints at runtime, so the same endpoint can be reused by
multiple collect_and_post flows without duplicating field config.
DB: ALTER TABLE company_endpoints ADD COLUMN body_fields TEXT DEFAULT NULL
Changes:
- NormalBot: falls back to endpoint.body_fields when flow.fields is empty
- DashboardController: endpoint rows show a body_fields section for
upload endpoints (key / label / prompt per field, add/remove rows)
- companyEndpointSave: reads + validates body_fields JSON and persists it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New flow type that asks the user for a configured list of fields one
by one, shows a summary with confirm/cancel, then POSTs all answers
as JSON to the configured endpoint with Bearer auth.
Config example:
{
"type": "collect_and_post",
"endpoint_key": "subir_cosecha",
"header": "🌿 Ciclos Cosecha",
"confirm": true,
"success_text": "✅ Ciclo registrado.",
"fields": [
{"key":"fecha", "label":"Fecha", "prompt":"📅 ¿Fecha del ciclo? (YYYY-MM-DD)"},
{"key":"cantidad", "label":"Racimos", "prompt":"🔢 ¿Cantidad de racimos?"},
{"key":"finca", "label":"Finca", "prompt":"🏡 ¿Nombre de la finca?"}
]
}
POST payload: {fecha, cantidad, finca, telefono, nombre}
Also adds the UI panel in bot-config with dynamic add/remove field rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dropdown option '🌧 Registro por ítem' in both PHP-rendered cards
and the addFlowCard() JS template
- Fields panel: endpoint lista, endpoint upload, campo ID, campo nombre,
encabezado, pregunta por ítem, mensaje de éxito
- flowTabChange() now shows/hides ftg-foreach panel
- Save handler in index.php parses all flow_fe_* POST fields and
builds the collect_for_each config object
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creating a company with the Activa checkbox unchecked (or missing)
set is_active=0, silently disabling the bot for that company.
New companies now default to active; existing companies keep their
current value when the form loads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: WpWebhook called OutboundWorker::processQueue() after every
incoming message. BotRouter already sends synchronously in enqueueResponse();
failed messages stayed status='queued'. On the next incoming message
processQueue() would re-send ALL previously-failed messages, causing
the bot to repeat old responses.
- Remove OutboundWorker::processQueue() from WpWebhook::processEvent()
BotRouter handles immediate delivery; retries should be manual/cron only
- Fix chatSend(): instead of queuing + processQueue() (which drains ALL
queued messages), call WhatsAppSender::sendText() directly for the
one message being sent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CompanyRepository::save() now catches PDOException and throws a
RuntimeException with a human-readable message (duplicate name, etc.)
- Also fixes unchecked checkboxes (requires_approval, is_active) not
being saved as 0 on UPDATE
- index.php save handler catches RuntimeException and redirects back to
the form with an ?error= param instead of crashing with 500
- companyEdit() renders the error banner when ?error= is present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All companies use the same WhatsApp phone number ID. When creating a
new company, auto-fill the field with the value already in use so the
admin doesn't have to look it up and type it manually.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add extractContent() helper: parses JSON WhatsApp payloads to show
human-readable text (button titles, body text, captions) instead of
raw JSON in both the conversation list preview and message bubbles
- Fix unread_count subquery: was comparing outbound_queue.id with
conversations.id (different sequences); now uses created_at comparison
- Remove broken outbound_queue secondary query from chatMessages():
all messages (bot + admin) are already in conversations table —
the extra join caused duplicates and wrong ordering
- chatSend() now calls OutboundWorker::processQueue() immediately after
queuing so admin messages are sent to WhatsApp without manual trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
panel-general had class tab-panel (display:none) but showTab('general')
was only called in edit mode — so new company form showed no fields.
Adding active class as default makes it visible immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add phone_number_id and display_phone fields to create/edit form
(were never shown or saved despite existing in the DB)
- Add phone_number_id and display_phone to CompanyRepository::save()
- New company form shows "Copiar configuración de empresa" dropdown:
selecting an existing company pre-fills api_base_url, api_key,
bot_type, config_json, etc. so admins only need to change
the name and phone_number_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pending-approval flow is no longer used. Remove the nav link so the
page is no longer reachable from the sidebar (routes and backend logic
left intact in case they're needed later).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The function had an unclosed arrow function callback (convs.map(c => {)
followed by a misplaced catch, creating a JS syntax error that prevented
the entire <script> block from parsing — so no conversations or messages
rendered at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
addFlowCard() referenced CONFIG.menus, CONFIG.flows and ENDPOINTS but
those variables were only defined in the conversation-tree page (a
different route). On /admin/bot-config the access threw ReferenceError,
aborting the function before insertAdjacentHTML ran, so no card appeared.
Compute botConfigJson/botEndpointsJson from already-loaded PHP data and
emit them as const CONFIG / ENDPOINTS at the top of the page's <script>.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Gemini option to ai_provider select (alongside OpenAI/Mock)
- Show/hide OpenAI vs Gemini fields dynamically via aiProviderChange()
- Add gemini_api_key and gemini_model fields to the form
- Add openai_api_key field in bot-config (per-company override)
- Save handler now persists gemini_api_key, gemini_model, openai_api_key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all backtick template literals inside addFlowCard (which lives
inside a PHP heredoc) with plain string concatenation. PHP was evaluating
${k}, ${Date.now()}, ${menuOpts}, etc. as PHP variable expressions,
emitting deprecation warnings inside the <script> block and corrupting
the JavaScript — causing all tab buttons on bot-config to stop responding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Load name+direction in botConfig() endpoint query (was missing name column)
- Show friendly endpoint names in all flow select dropdowns (was showing raw key)
- Fix per_type save: merge with existing data instead of replacing (was wiping custom flows/commands per category)
- Fix category labels: Categoría 1 = Solo reporta, 2 = Solo recibe, 3 = Reporta y recibe
- Remove incorrect "menú debe ser de tipo botón" restriction text
- Add datalist to command action inputs so admin can pick from existing menu/flow IDs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix display:none vs display:flex conflict in PHP flow card rendering (sections were always visible)
- Add addFlowCard() JS function that properly inserts new flow cards into the PHP form
- Fix flowTabChange() to use block vs flex display correctly per section type
- Rewrite endpoints tab as full CRUD table (add/edit/delete) with name, direction, active toggle
- Add companyEndpointDelete() handler and route
- Update companyEndpointSave() to handle ep_id, name, params, is_active fields
- Add company_endpoints columns: name, params
- Add saveEpRow/testEpRow/deleteEpRow/addNewEp JS functions
- NormalBot: set __greeted sentinel after showing greeting menu to prevent re-send on every message
- NormalBot: add substituteUrlVars() to replace {param} tokens in endpoint URLs with collected metadata
- DB: submenu_ciclos_sanidad (4 items) → list; prod_kilos_finca endpoint linked; all >3-button menus → list
- DB: fincas_list endpoint activated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>