ApproSearch - Releases
======================

2.30.1 - 2026-10-15
-------------------
- CORREGIDO: el configurador del back office salia sin estilos y sin su
  JavaScript (asistente, indexar ahora). El CSS y el JS se anadian con la
  version en la ruta (?v=) y PrestaShop comprueba el fichero con la ruta
  completa, query incluida, asi que los descartaba sin avisar. Ahora se
  enlazan sin esa comprobacion y conservan la version para la cache.
  Visto en PrestaShop 1.7.7.8; el codigo del core es el mismo en 8 y 9.

2.30.0 - 2026-10-14
-------------------
- SEGURIDAD: el HTML de la tienda ya no publica la API key del conector.
  Esa key tiene permiso de indexacion: cualquiera que la copiara podia
  vaciar el indice o desactivar el filtro de grupos de acceso (R-5). El
  front publica solo una key de solo busqueda que el modulo pide a la API
  (POST /api/v1/feed/public-key) desde el servidor de la tienda.
  - La pide al guardar la conexion, en el upgrade, al abrir el configurador
    y en cada ejecucion del cron. Se guarda en APPROSEARCH_PUBLIC_KEYS, una
    por cada API key del multitienda.
  - Cada instalacion pide su propia key publica (APPROSEARCH_INSTANCE_ID):
    una tienda de pruebas con la misma API key no invalida la de produccion.
  - Las paginas de la tienda no llaman a la API. Sin key publica el buscador
    no se pinta y el configurador lo avisa. Si la API no responde durante
    el upgrade, el buscador vuelve en la siguiente ejecucion del cron o al
    abrir el configurador.
  - Si la tienda usa una cache de pagina completa, vaciala tras actualizar:
    las paginas guardadas antes siguen llevando la key anterior.
  - Tras actualizar, rota la API key del conector en el panel y pegala de
    nuevo: la anterior ha estado publicada en el HTML.
- Script de upgrade: upgrade-2.30.0.php pide la key publica de cada conexion.
  Si la API no responde, el upgrade no falla.

2.29.0 - 2026-10-07
-------------------
- NUEVO: configurador del back office redisenado.
  - Cabecera con la version y el estado: conexion (con el nombre de la
    tienda), buscador activo o desactivado y ultima indexacion completa,
    mas un enlace al panel de ApproSearch.
  - Pestanas: Resumen (plan y uso), Conexion, Buscador (con la pagina de
    prueba), Indexacion (indexacion, progreso del cron y sincronizacion del
    catalogo), Precios y acceso, y Avanzado. El configurador recuerda la
    pestana al guardar y admite enlaces directos (#approsearch-tab-indexacion).
    El aviso de version y "Puesta en marcha" se ven en todas las pestanas.
  - Indexacion del catalogo: la estrategia se elige con dos tarjetas, los
    botones de indexar van agrupados y las URLs llevan boton Copiar.
  - "Configuracion de tiempos avanzada" y "URLs de cron por indice" son
    desplegables con un boton visible ("Configurar tiempos", "Ver URLs") y la
    programacion activa en la cabecera. Sliders con barra rellena y etiqueta.
  - Plan, uso y progreso del cron en cifras grandes.
  - Sin JS, el configurador se ve entero, sin pestanas, y funciona igual.
- CAMBIO: los paneles de "Opciones avanzadas" ya no se pliegan: estan en
  su pestana. Los paises de "Precios por pais" usan los mismos chips con
  buscador que los grupos de cliente.
- CAMBIO: el CSS y el JS del configurador se cargan con la version en la
  URL, para que el navegador no use los de la version anterior.
- CORREGIDO: los mensajes con datos (tienda conectada, idiomas, productos
  indexados, resultado de la indexacion...) usan marcadores posicionales
  (%1$s). En tiendas con el traductor de PrestaShop parcheado para borrar el
  "%s" literal, el dato no aparecia.
- Sin manejadores de eventos ni estilos en linea en las plantillas: las
  confirmaciones y la seleccion de las URLs van en approsearch-admin.js.
- Sin cambios de base de datos, configuracion ni hooks: no hay script de
  upgrade.

2.28.1 - 2026-10-07
-------------------
- CAMBIO: la lista "Puesta en marcha" desaparece cuando los cuatro pasos
  obligatorios estan bien (conexion, indices por idioma, productos en el
  buscador y buscador en la tienda). Vuelve sola en cuanto alguno falla o
  mientras dura la primera indexacion.
- CAMBIO: el desplegable "Configuracion de tiempos avanzada" se ve como una
  barra con la programacion activa (pasadas por lotes y limpieza del
  incremental). Antes era un texto gris facil de pasar por alto. Sigue
  plegado por defecto.
- Sin cambios de base de datos, configuracion ni hooks: no hay script de
  upgrade.

2.28.0 - 2026-10-07
-------------------
- NUEVO: desplegable "Configuracion de tiempos avanzada" en Indexacion del
  catalogo > Programacion por cron. Empieza plegado y con los valores de
  siempre. Se ajusta con sliders:
  - Repetir cada: tiempo entre pasadas del motor por lotes, de continuo a
    30 dias (APPROSEARCH_RESYNC_INTERVAL, 1 dia por defecto). Desde la
    2.19.0 solo se podia cambiar en la base de datos.
  - Hora de inicio: hora fija para empezar las pasadas, en tramos de 30 min
    (APPROSEARCH_RESYNC_AT, minutos desde medianoche; -1 = sin hora fija,
    el valor por defecto). Con 1 dia y las 08:00, la pasada empieza todos
    los dias a las 08:00. Sin hora fija, la espera sigue contando desde el
    final de la pasada anterior, asi que la hora se desplaza cada dia lo
    que dura la pasada.
  - Limpieza del estado incremental: frecuencia (desactivada, 1 a 30 dias)
    y hora (APPROSEARCH_INCR_RESET_DAYS y APPROSEARCH_INCR_RESET_AT). A esa
    hora, el cron de productos hace lo mismo que what=reset_incremental:
    vacia los hashes, reinicia el cursor y empieza una pasada que reenvia
    todos los productos. Solo actua con la sincronizacion incremental
    activa. La cuenta empieza al guardar (APPROSEARCH_INCR_RESET_LAST).
- CAMBIO: la URL what=reset_incremental pasa de "Opciones avanzadas de
  cron" al desplegable de tiempos, con una linea de crontab de ejemplo que
  sigue los sliders.
- CAMBIO: el panel muestra la programacion de las pasadas y de la limpieza,
  y "Progreso del cron por lotes" muestra la proxima limpieza.
- Si el cron no llama a la hora exacta, la pasada empieza en la primera
  llamada despues de esa hora. Las horas son las de la tienda (zona
  horaria de PrestaShop).
- Sin cambios de base de datos ni de hooks. Las claves nuevas se leen con
  su valor por defecto si no existen: no hay script de upgrade.

2.27.0 - 2026-10-06
-------------------
- NUEVO: instalacion rapida. Para conectar la tienda basta con pegar la API
  key y pulsar Guardar conexion. La URL de la API llega rellena
  (https://api.approsearch.com) y, al guardar, el modulo:
  - Obtiene el ID de la tienda (GET /api/v1/feed/connect). El campo Store ID
    sale del formulario; el valor guardado se conserva.
  - Crea los indices de productos, categorias, marcas y paginas que falten
    para cada idioma activo, el idioma por defecto primero
    (POST /api/v1/feed/indices). Si el plan no admite todos los idiomas, solo
    envia los que tienen indice (APPROSEARCH_INDEX_LANGS) y lo avisa.
  - Activa la multi-moneda si la tienda tiene mas de una moneda activa y el
    plan admite mas de una (solo al conectar una key nueva).
  - Si el indice de productos esta vacio, arranca la primera indexacion
    completa (productos, categorias, marcas y paginas CMS) con el motor por
    lotes. El configurador la avanza con pasos AJAX de 20 s mientras esta
    abierto; no hace falta programar el cron para la primera carga.
- NUEVO: lista de comprobacion "Puesta en marcha" al principio del
  configurador: conexion, indices por idioma, productos indexados, buscador
  en la tienda y multitienda, cada uno con lo que hay que hacer. Incluye los
  botones Crear indices, Indexar ahora y Activar el buscador.
- CAMBIO: el configurador muestra primero la lista de comprobacion y la
  configuracion; los paneles de plan, uso, progreso del cron y pagina de
  prueba pasan al final.
- CAMBIO: la URL del widget sale del formulario de conexion. El valor
  guardado se conserva y se sigue usando.
- Requiere una API con GET /feed/connect y POST /feed/indices. Con una API
  anterior, guardar la conexion funciona como antes y avisa de que los indices
  se crean en el panel.
- Sin cambios de base de datos ni de hooks. Las claves nuevas
  (APPROSEARCH_INDEX_LANGS y APPROSEARCH_ONBOARDING_INDEX) se leen vacias si
  no existen: no hay script de upgrade.

2.26.1 - 2026-10-05
-------------------
- CORREGIDO: las referencias, los EAN y los MPN de las combinaciones se envian
  como texto. Al deduplicarlos, PHP convertia en numero los codigos solo de
  digitos y un EAN llegaba a la API como entero. En los indices creados antes
  del 2026-06-18 eso rompia la busqueda de toda la tienda ("Error de conexion").
- Sin cambios de base de datos, configuracion ni hooks: no hay script de upgrade.

2.26.0 - 2026-10-01
-------------------
- NUEVO: el modulo se identifica ante la API. Cada llamada (apiCall) lleva la
  cabecera X-ApproSearch-Client con la version del modulo, de PrestaShop y de
  PHP, p. ej. "prestashop/2.26.0; platform=8.1.2; php=8.1.27". La API la
  guarda por tienda: el admin de ApproSearch ve que version tiene instalada
  cada tienda y el panel del cliente muestra si hay una version mas reciente.
- No envia datos de la tienda ni de los clientes, solo esas tres versiones.
- Sin cambios de base de datos ni de configuracion: no hay script de upgrade.

2.25.1 - 2026-10-01
-------------------
- Version de prueba del aviso de actualizacion de la 2.25.0: las tiendas en
  2.25.0 ven en el configurador que hay una version nueva y la instalan
  subiendo el zip en Modulos > Gestor de modulos > Subir un modulo.
- Sin cambios de funcionamiento. upgrade-2.25.1.php no hace nada; existe para
  que la prueba recorra tambien la ejecucion de scripts de actualizacion.

2.25.0 - 2026-09-30
-------------------
- NUEVO: aviso de version nueva en el configurador. El modulo consulta
  GET /api/v1/downloads/prestashop/latest.json (publico, sin API key; timeout
  de 4 s) y, si hay una version mas reciente que la instalada, muestra un
  aviso con el boton de descarga del zip y como subirlo en Modulos > Gestor de
  modulos > Subir un modulo. No descarga ni instala nada por si solo.
  - Sin APPROSEARCH_API_URL configurada consulta https://api.approsearch.com.
  - Si la API no responde o no tiene el endpoint, no se muestra nada.
  - Las variantes a medida de una tienda deben poner la constante
    UPDATE_NOTICE a false: el zip publicado es el del repositorio y borraria
    su codigo propio.
- Sin cambios de base de datos ni de configuracion: no hay script de upgrade.

2.24.0 - 2026-09-29
-------------------
- NUEVO: precios propios por PAIS (opt-in, no depende del plan). Switch
  APPROSEARCH_PRICE_COUNTRIES + checkboxes de paises activos de la tienda
  (APPROSEARCH_PRICE_COUNTRIES_LIST) en el formulario de precios.
  - Por cada pais seleccionado se exportan priceExclTax_c<iso> y
    priceInclTax_c<iso> (y originalPrice... si hay descuento), con el sufijo
    de moneda delante cuando hay multi-moneda. El precio sale de
    Product::priceCalculation() con el id_country, que aplica los
    specific_price de ese pais, a nivel de producto (como el listado).
  - En el front se inyecta el pais del visitante (el que ya resuelve
    PrestaShop) si es uno de los exportados; el buscador ordena y filtra por
    ese precio.
  - Requiere API con soporte de POST /api/v1/feed/price-countries: sin la
    declaracion los campos por pais NO se emiten (sin romper el sync).
  - Multiplica el tiempo de sincronizacion por el numero de paises. Al
    activarlo, lanzar una reindexacion completa.
- MEJORA: correcciones del validador de PrestaShop Addons, sin cambios de
  funcionamiento:
  - Paneles del BO (plan, uso, progreso del cron, pagina de prueba,
    indexacion y avisos de plan) renderizados con plantillas Smarty en
    views/templates/admin/, sin HTML en el PHP. Todas las variables se
    escapan, tambien los datos que llegan de la API.
  - La consulta de la pagina de resultados se escapa en results.tpl.
  - .htaccess en la raiz que impide listar/ejecutar los ficheros del modulo
    (deja pasar css, js, imagenes y fuentes).
  - Descripcion del modulo en ingles (igual en approsearch.php y config.xml),
    con su traduccion al espanol en translations/es.php.
  - Cabeceras de licencia (@author, @copyright, @license) en PHP, JS y TPL.
  - $this->context en lugar de Context::getContext(); codigo postal como
    string en priceCalculation(); metodos privados sin uso eliminados; el
    cron comprueba que el modulo es Approsearch; setMedia() devuelve bool.
  - displaySearch sin metodo hookDisplaySearch (hook marcado como eliminado):
    el modulo implementa WidgetInterface y PrestaShop lo atiende con
    renderWidget(), que pinta el disparador solo en la posicion configurada.
    Mismo comportamiento que antes; admite ademas
    {widget name='approsearch' hook='displaySearch'} en el tema.
  - Miniaturas del tema: en PrestaShop 1.7.0-1.7.4 el presenter antiguo se
    registra como alias del actual (class_alias), sin instanciar por nombre
    dinamico. MPN solo se lee en PrestaShop 1.7.7+ (version_compare).

2.23.0 - 2026-08-21
-------------------
- NUEVO: control de acceso a productos por grupos de cliente (opt-in, requiere
  plan con B2B). Formulario "Acceso por grupos de cliente" en el BO:
  - Switch APPROSEARCH_GROUP_ACCESS + checkboxes de grupos a exportar
    (APPROSEARCH_GROUP_ACCESS_GROUPS). El grupo Visitante se exporta SIEMPRE
    ademas de los seleccionados (es el baseline de los visitantes sin login).
  - El modulo exporta, por producto y por categoria, que grupos pueden verlos
    segun la asignacion de categorias a grupos de PrestaShop
    (ps_category_group), con la misma regla que Product::checkAccess: un grupo
    ve el producto si CUALQUIERA de sus categorias se lo permite. Campo nuevo
    `accessGroups` en el documento; producto sin ningun grupo permitido emite
    el centinela `none` (oculto para todos).
  - En el front, el modulo inyecta en la config del widget TODOS los grupos
    del cliente logueado presentes entre los exportados (accessGroups, CSV);
    el buscador filtra server-side en busqueda, autocompletado, busqueda por
    defecto y busqueda por imagen. Sin login se aplica el acceso del grupo
    Visitante. El parametro viaja en claro (como priceTier): no es un secreto.
  - Compatible con el "Modo catalogos grandes" (SQL puro por lotes, sin ORM),
    a diferencia de las tarifas de precio.
  - Al ACTIVAR o DESACTIVAR: lanzar una reindexacion completa. El campo nuevo
    cambia el hash incremental, asi que la primera pasada tras el cambio
    reenvia el catalogo entero. Al desactivar, el modulo limpia la declaracion
    en el servidor (el filtro se apaga al instante; los accessGroups
    residuales del indice quedan inertes y se limpian al reindexar).
  - Requiere API con soporte de POST /api/v1/feed/access-groups: con una API
    antigua la declaracion falla y el campo NO se emite (sin romper el sync).

2.22.0 - 2026-08-20
-------------------
- CAMBIO: "Reemplazar busqueda nativa" cambia de significado.
  - Activado: la caja de busqueda del TEMA se queda visible y pasa a ser el
    disparador del widget (enfocarla, clicarla o enviar la busqueda abren
    ApproSearch con la consulta). El boton propio de ApproSearch NO se
    renderiza y deja de cargarse hide-native-search.css. Permite instalar el
    modulo sin alterar el aspecto de la tienda.
  - Desactivado: se muestra el boton de ApproSearch en la posicion elegida y
    se oculta la caja del tema (hide-native-search.css pasa a cargarse en este
    caso). Es el visual "clasico" del modulo.
  - MIGRACION AUTOMATICA: el upgrade invierte el valor guardado (tambien los
    valores por tienda del multitienda) para que cada tienda conserve el
    aspecto que ya tenia. Tiendas que mostraban el boton de ApproSearch lo
    siguen mostrando (quedan en OFF); tiendas que mantenian la caja del tema
    la mantienen y esta pasa a abrir ApproSearch (quedan en ON, y el boton
    duplicado del modulo desaparece).
- NUEVO: campo "Selector CSS adicional" en el formulario "Buscador (frontend)".
  Para maquetaciones personalizadas cuya caja de busqueda no se detecta con
  los selectores estandar: se añade a la lista que abre el widget al
  enfocar/clicar y a la deteccion de formularios cuyo envio se intercepta.
  Admite varios selectores separados por comas. Un selector invalido se
  ignora sin romper el widget (proteccion tambien en el bundle del widget,
  que requiere redeploy de la API para servirse actualizado).
- FIX: con "Pagina de resultados" activa, el JS del modulo ya no intercepta el
  submit del form nativo: se deja pasar para que el redirect server-side del
  controlador search lleve a la pagina de resultados con la consulta.

2.21.1 - 2026-08-19
-------------------
- FIX: el formulario "Sincronizacion del catalogo" no guardaba NADA. El bloque
  que persiste esos ajustes estaba condicionado a APPROSEARCH_BATCH_SIZE, campo
  que dejo de existir en el formulario en 2.19.0 (16-07-2026): al no viajar
  nunca en el POST, la condicion no se cumplia nunca y el bloque se saltaba en
  silencio, sin mensaje de error.
  - Afectaba a "Lectura directa de BD (catalogos grandes)", "Sincronizacion
    incremental", "Desactivar indexacion automatica por hooks", "Excluir del
    feed por visibilidad" y "Caracteristica numerica para reglas de boost".
  - El marcador pasa a ser APPROSEARCH_LARGE_CATALOG, que es un switch y por
    tanto siempre viaja en el POST.
  - El tamano de lote y el intervalo de re-sync solo se escriben si vienen en
    el POST: al no ser ya campos del formulario, guardarlos a ciegas los
    habria machacado con el minimo (10) y con 0.
  - Tras actualizar, revisa esos ajustes en el back office: cualquier cambio
    que hicieras desde 2.19.0 no llego a guardarse.

2.21.0 - 2026-08-19
-------------------
- NUEVO: el feed de productos envia `feature_num`, el valor NUMERICO de una
  caracteristica del catalogo. Habilita las reglas de boost "Proporcional al
  valor" y "Por tramo" del panel, que necesitan un campo numerico: las
  caracteristicas normales se indexan como texto y no sirven para ordenar ni
  escalar.
  - Nuevo ajuste "Caracteristica numerica para reglas de boost", en el
    formulario "Sincronizacion del catalogo" de la configuracion del modulo.
    Lista las caracteristicas de la tienda; por defecto "Ninguna". Elige la
    que quieras (potencia, capacidad, prioridad propia...) y resincroniza.
  - Se guarda el id_feature, no el nombre: renombrar la caracteristica en el
    catalogo no rompe la configuracion. Si el id ya no existe, vuelve a
    "Ninguna" al guardar.
  - Con "Ninguna" el campo no se envia: mientras nadie elija caracteristica,
    el comportamiento del feed no cambia respecto a versiones anteriores.
  - El valor admite coma decimal ("12,5") y texto alrededor ("150 W" -> 150).
    Un valor sin digitos no se envia, para no falsear los filtros de rango.
  - Ambas rutas de indexacion lo incluyen: lectura por ORM (hooks / directo) y
    lectura directa de BD (cron por lotes).
  - El campo cambia el hash de los productos que tengan la caracteristica, asi
    que el primer resync los reenvia una vez.

2.20.1 - 2026-07-27
-------------------
- FIX del origen de "sales_count" (2.20.0): se calculaba desde
  ps_product_sale.quantity, cache que mantiene el cron de estadisticas de
  PrestaShop pero que en la practica se desincroniza del dato real (visto
  hasta 40% por debajo en produccion). Ahora se calcula igual que
  "Estadisticas > Mejores ventas" del BO: SUM(order_detail.product_quantity)
  de pedidos validos, historico completo. Coste extra insignificante (~60ms
  por lote de 500 productos, medido contra ~300k lineas de pedido).

2.20.0 - 2026-07-24
-------------------
- NUEVO: el feed de productos envia el numero de ventas acumuladas
  (sales_count) para que el factor de ranking "Popularidad (ventas)" del panel
  (Relevancia -> Popularidad) funcione. Hasta ahora el conector no mandaba este
  dato, asi que el campo sales_count del indice quedaba vacio y el boost por
  ventas no tenia ningun efecto (mover el slider no cambiaba nada).
  - Ambas rutas de indexacion lo incluyen: lectura por ORM (hooks / directo) y
    lectura directa de BD (cron por lotes).
  - Tras actualizar, el primer resync reenvia todo el catalogo una vez (el nuevo
    campo cambia el hash de cada documento) y a partir de ahi solo se reenvia un
    producto cuando cambian sus ventas. Recomendado: lanzar una reindexacion.
  - NOTA: esto NO ordena los resultados por ventas; es un factor mas de la
    relevancia (multiplicador logaritmico). No hay orden "Mas vendidos" en el
    widget (solo Relevancia / Precio / Nombre).

2.19.0 - 2026-07-16
-------------------
- REDISENO DE LA INDEXACION: registro unificado de indexaciones + limpieza
  automatica de obsoletos (sin necesidad de rebuild manual con &fresh=1).
  - El sync (estrategias Completa y Reanudable, con o sin incremental) abre un
    "run" en la API, sella cada documento con su generacion y al cerrarlo la API
    PURGA los documentos que ese run no reenvio ni toco: asi se limpian los
    productos desactivados/eliminados en lote que no dispararon hooks (antes se
    quedaban como huerfanos en el indice). Aplica a productos, categorias,
    marcas y CMS. Sin ventana de resultados incompletos (a diferencia de &fresh).
  - Modo incremental: los documentos sin cambios ya no se descartan sin mas; se
    "tocan" (POST /feed/touch) para que sobrevivan a la purga.
  - Compatibilidad: si la API aun no soporta runs (POST /feed/runs -> 404), el
    conector cae automaticamente al comportamiento clasico (upsert + clear en
    fresh). Las URLs de cron actuales (what=..., &fresh=1) siguen funcionando.
- CONFIGURACION SIMPLIFICADA:
  - "Productos por lote" e "Intervalo de re-sync" salen del back-office (defaults
    internos; ajustables solo por Configuration/BD para casos puntuales).
  - El panel muestra la ESTRATEGIA (Completa / Reanudable) y UNA sola linea de
    cron recomendada; las URLs por indice y el reinicio del incremental quedan en
    un desplegable "avanzado".
  - "Modo catalogos grandes" pasa a llamarse "Lectura directa de BD".
- Retirado el controlador admin huerfano (AdminApprosearchSyncController).

2.18.0 - 2026-07-14
-------------------
- El limite de busquedas simultaneas del plan pasa a ser solo informativo: la
  API ya no rechaza busquedas por concurrencia. El configurador muestra el pico
  de busquedas simultaneas como dato, sin barra de porcentaje ni aviso de
  "se pueden rechazar busquedas".
- Sin cambios de base de datos ni de configuracion: no hay script de upgrade.

2.17.0 - 2026-07-07
-------------------
- NUEVO: Marcas (fabricantes) como tipo indexable propio, igual que las
  categorias. Ahora una marca es un documento buscable: al buscar sale un
  bloque "Marcas" con las marcas que casan por nombre o descripcion, cada una
  enlazando a su pagina real de marca (como el bloque de Categorias).
  - Nuevo tipo de indice MANUFACTURER (crear el indice "Marcas" por idioma desde
    el panel, igual que el de Categorias).
  - Sincronizacion: boton "Solo marcas" en el BO; cron what=manufacturers y
    what=sync_manufacturers (y ademas incluido en what=full/all/sync y en
    "Reindexar todo").
  - Hooks: alta/edicion de marca indexa el documento al vuelo; edicion tambien
    reindexa sus productos (llevan el nombre de marca); baja de marca la borra
    del indice.

2.16.0 - 2026-07-03
-------------------
- NUEVO: "Pagina de resultados" a pagina completa (opt-in, OFF por defecto).
  Nuevo controlador front /module/approsearch/results que renderiza una pagina
  con la cabecera y el pie del tema y, en el contenido, el widget en modo
  "pagina" (inline): resultados en el area principal y filtros anclados a la
  izquierda, como una categoria de PrestaShop. El widget lee la consulta de la
  URL (?s=) y sincroniza la URL al buscar/paginar (compartible, atras/adelante).
  - Al pulsar Intro en la caja de busqueda, el JS del modulo navega a esta
    pagina (?s=CONSULTA) en vez de abrir el overlay flotante.
  - El controlador `search` NATIVO de PrestaShop redirige SIEMPRE a esta pagina
    (via hookActionFrontControllerSetMedia; sin override del core).
  - Interruptor "Pagina de resultados" en el configurador (Buscador frontend).
    Con OFF, Intro sigue abriendo el overlay como hasta ahora.

2.15.0 - 2026-07-02
-------------------
- NUEVO: modo "miniaturas del tema" (themeCards). Nuevo controlador front
  /module/approsearch/card que devuelve el HTML de la miniatura de producto
  tal y como la pinta el tema (ProductAssembler + ProductListingPresenter +
  catalog/_partials/miniatures/product.tpl), con el contexto de la sesion del
  visitante (grupo, moneda, impuestos, multishop). El widget lo usa cuando el
  panel activa la opcion; el endpoint se inyecta siempre en la config
  (cardEndpoint). Solo productos activos/visibles de la tienda; respuesta
  no cacheable; tope de 48 ids por peticion; se eliminan <script> y se
  normalizan imagenes lazy-load (data-src -> src). Antes de renderizar se
  ejecuta el initContent del core para asignar a smarty las variables
  generales del front ($urls, $configuration, $shop, $language...) con el
  contexto real de la tienda, como en cualquier pagina del front.
- NUEVO: pagina de prueba del buscador (/module/approsearch/test?token=...).
  Pagina con la cabecera/pie del tema y la caja de busqueda en el contenido;
  carga los assets del widget AUNQUE el modulo este desactivado para los
  visitantes. Protegida por token (APPROSEARCH_TEST_TOKEN, visible y
  regenerable en el configurador del BO); sin token valido responde 404.
- El trigger tambien se enlaza por clase .approsearch-trigger (ademas del id),
  para poder colocar mas de una caja de busqueda en la misma pagina.

2.14.0 - 2026-07-01
-------------------
- NUEVO: modo "precios en vivo" (livePricing). Controlador front
  /module/approsearch/price que devuelve el precio contextual del visitante
  para un lote de ids; el widget lo usa cuando el panel activa la opcion.

2.13.0 - 2026-06-29
-------------------
- NUEVO: se indexa y se busca por MPN (Manufacturer Part Number) del producto y
  de sus combinaciones (combinationMpns), junto a referencia/EAN/UPC/ISBN. Match
  exacto y por subcadena (comodin), con boost de combinacion > producto. Requiere
  reindexar para poblar el campo en productos ya indexados.

2.12.0 - 2026-06-19
-------------------
- NUEVO: "Grupo del precio por defecto" (APPROSEARCH_BASE_PRICE_GROUP). Grupo
  de cliente cuyo precio se exporta como precio por defecto de la tienda: el
  que ven los visitantes sin login y por el que se ordena y filtra por
  defecto. Se crea con el grupo Visitante (PS_UNIDENTIFIED_GROUP), asi que el
  comportamiento por defecto no cambia.
- "Precio por cantidad como precio base" busca el minimo entre las reglas de
  cantidad del grupo base y las genericas (id_group IN (0, grupo base)).
- Sin migracion de datos. Si cambias el grupo del precio por defecto, lanza una
  reindexacion completa.

2.11.0 - 2026-06-19
-------------------
- "Precio por cantidad como precio base" es compatible con "Precios por
  tarifa": con las dos opciones activas, cada tarifa de grupo exporta el precio
  por cantidad mas bajo aplicable a ese grupo (reglas del grupo y genericas
  id_group = 0), calculado con Product::priceCalculation para respetar el
  descuento del grupo.
- Sin configuracion nueva ni migracion de datos. Si usas ambas opciones, lanza
  una reindexacion completa.

2.10.2 - 2026-06-19
-------------------
- FIX critico de indexacion: getActiveProductCategories() filtraba por
  category_shop.active, una columna que no existe en PrestaShop. Con el modo
  debug activo, el error SQL dejaba todos los productos sin indexar
  ("0 enviados"); sin debug, los productos se indexaban sin categorias. Se
  filtra por category.active = 1.
- Sin migracion de datos. Tras actualizar, lanza una reindexacion completa para
  recuperar el indice y las facetas de categoria.

2.10.1 - 2026-06-19
-------------------
- FIX: el calculo de "precio por cantidad como precio base" ya no puede impedir
  la indexacion. Si la consulta a specific_price fallaba (por ejemplo, con
  sql_mode ANSI_QUOTES y debug activo), buildProductData devolvia null para
  todos los productos y el indice quedaba vacio. Los literales SQL usan comillas
  simples y el calculo va en try/catch: ante cualquier fallo se mantiene el
  precio normal.
- Sin cambios de configuracion ni migracion de datos.

2.10.0 - 2026-06-19
-------------------
- NUEVO: seccion "Opciones avanzadas" en el configurador, con la opcion
  "Precio por cantidad como precio base" (APPROSEARCH_ADV_QTY_BASE_PRICE, OFF
  por defecto). Activada, exporta como precio base de cada producto el precio
  por cantidad mas bajo: el de las reglas de precio especifico que dependen
  solo de la cantidad y no tienen ningun otro condicional.
- Con el valor por defecto el comportamiento no cambia. Sin migracion de datos.

2.9.1 - 2026-06-18
------------------
- FIX: las categorias desactivadas (active = 0) dejan de indexarse en los
  filtros de productos; ya no aparecen como faceta/filtro de categoria en el
  buscador. Antes se enviaban al feed aunque estuvieran desactivadas en la
  tienda.

2.9.0 - 2026-06-18
------------------
- BUSQUEDA POR REFERENCIA Y EAN DE LAS COMBINACIONES (variantes). Antes solo se
  indexaba/buscaba la referencia y el EAN del PRODUCTO; las de las combinaciones
  se ignoraban. Ahora se indexan y se buscan, con boost por encima del producto
  para que un match de combinacion prime. Tras actualizar, reindexa el catalogo
  para que las combinaciones ya existentes entren en el indice.

2.8.0 - 2026-06-17
------------------
- MOTOR DE INDEXACION UNIFICADO. Toda la indexacion (productos, categorias y
  paginas CMS) pasa por un unico motor parametrizado por "descriptor de tipo"
  (getFeedDescriptor). Se elimina el codigo duplicado entre rutas que causaba
  errores en una sola de ellas:
  - assembleProductDoc(): unica fuente de la FORMA del documento de producto;
    los dos builders (ORM buildProductData y SQL buildProductDataBatch) la
    comparten, asi no pueden volver a divergir.
  - FIX: un producto sin nombre tumbaba el lote bulk ENTERO (la API valida el
    array con Zod, name.min(1)); ahora se descarta por-producto. Misma guardia
    de campo vacio anadida a categorias (name) y paginas CMS (title); las CMS
    ademas se trocean a 500 (antes se enviaban todas en una peticion, lo que
    superaba el max de la API si habia mas de 500).
  - sendBulk()/runSimple()/runBatchSlice()/processChunkBatch() genericos: un
    unico envio (con manejo de 403) y los dos modos comparten el trabajo por
    chunk. Categorias y CMS ganan modo por lotes reanudable y manejo de 403.
- MODO DE INDEXACION configurable (APPROSEARCH_INDEX_MODE: simple|batch),
  elegible con BOTONES en el panel "Indexacion del catalogo" (default simple =
  comportamiento previo de los botones del BO). La unica diferencia entre modos
  es el desgranado por lotes / la reanudacion.
- RUTAS de cron mode-aware: what=sync (todos), what=sync_products,
  what=sync_categories, what=sync_cms (cada indice). El panel muestra siempre
  esas 4 peticiones + reset_incremental (valido en ambos modos). Las rutas
  legadas (what=full|products|all|categories|cms|reset_incremental) siguen
  funcionando igual: NO hay que cambiar los crons ya desplegados.

2.7.0 - 2026-06-11
------------------
- REDISENO DEL FORMULARIO de configuracion del back office (solo UI; sin
  cambios de datos ni claves de configuracion nuevas):
  - Las opciones se reorganizan en paneles tematicos, cada uno con su propio
    boton de guardar: "Conexion con ApproSearch" (credenciales), "Buscador
    (frontend)" (activar, reemplazar busqueda nativa, posicion),
    "Sincronizacion del catalogo" (lote, intervalo, catalogos grandes,
    incremental, hooks, visibilidades excluidas) y "Precios (monedas y
    tarifas)" (multi-moneda, tarifas). saveConfig() guarda cada bloque de
    forma independiente segun los campos presentes en el POST.
  - Los multiselect (grupos a exportar como tarifas y visibilidades excluidas)
    se sustituyen por grupos de CHECKBOXES estilizados como "chips" clicables
    (nuevo views/css/approsearch-admin.css + views/js/approsearch-admin.js).
    El formato guardado (CSV en APPROSEARCH_PRICE_TIERS_GROUPS y
    APPROSEARCH_EXCLUDE_VISIBILITY) no cambia.
  - Con mas de 8 opciones (tiendas con muchos grupos de cliente, hasta ~100),
    el grupo de checkboxes anade buscador (sin distinguir mayusculas/acentos),
    lista con scroll, acciones "Marcar visibles" (respeta el filtro activo) /
    "Desmarcar todos" y contador de seleccionados. Todo client-side sobre el
    marcado estandar de HelperForm.
  - Los paneles "Reindexacion manual" y "URLs para cron" se fusionan en un
    unico panel "Indexacion del catalogo" organizado POR ESTRATEGIA: dos
    tarjetas lado a lado — "Modo directo (una pasada)" (botones manuales
    Reindexar todo / Solo productos + URL de cron diaria con su ejemplo de
    crontab) y "Modo por lotes (cron reanudable)" (URL de cron cada 5 min con
    ejemplo, botones de mantenimiento Reiniciar progreso / Limpiar estado
    incremental y URL de reset periodico) — mas un bloque "Contenido
    adicional" (categorias y CMS: boton manual + URL de cron en la misma
    fila) y el pie con Probar conexion / Regenerar token. Mismos submit
    names: el postProcess de getContent() no cambia.
  - Las opciones de "Precios por tarifa" SOLO se muestran si el plan admite
    mas de una tarifa (maxPriceTiers > 1 en GET /feed/plan); si no, se muestra
    una nota informativa. Defensivo: si las tarifas ya estan activas o el plan
    no se puede consultar (API caida/antigua), no se ocultan. Cuando el bloque
    esta oculto, el guardado del panel de precios no toca su configuracion.
  - GET /feed/plan se consulta UNA vez por peticion (cache getPlanData()),
    compartida entre el panel "Plan actual" y la visibilidad de tarifas.
  - La incompatibilidad tarifas <-> modo catalogos grandes se sigue aplicando
    aunque ahora vivan en formularios distintos (se resuelve contra el valor
    guardado del otro bloque, con aviso; las tarifas tienen prioridad).
- Sin migracion de datos. Compatibilidad total con 2.6.x.

2.6.0 - 2026-06-11
------------------
- Nueva opcion "Excluir del feed por visibilidad"
  (APPROSEARCH_EXCLUDE_VISIBILITY, vacia por defecto): permite excluir del
  feed los productos segun su visibilidad ('none', 'catalog', 'search';
  'both' nunca es excluible). Con el valor por defecto el comportamiento es
  identico al anterior (se indexa todo producto activo). El filtro se aplica
  en todos los motores de sync (una pasada, por lotes, catalogos grandes) y
  en los hooks. upgrade-2.6.0.php solo siembra la clave; sin migracion.

2.5.0 - 2026-06-11
------------------
- PRECIOS POR TARIFA / grupos de cliente (opt-in, toggle "Precios por tarifa" /
  APPROSEARCH_PRICE_TIERS, OFF por defecto + multiselect de grupos en
  APPROSEARCH_PRICE_TIERS_GROUPS). Con OFF el comportamiento es identico.
  - Con ON, buildTierPrices() envia ademas, por cada grupo seleccionado, el
    precio del producto para ese grupo (precios especificos por grupo +
    descuento del grupo, cantidad 1, fecha actual) como campos planos
    price{Incl|Excl}Tax_t<id_group> (moneda base) y, con multi-moneda activa,
    price{Incl|Excl}Tax_<ISO>_t<id_group> por cada moneda del feed
    (+ originalPrice* solo si hay descuento). Calculo via
    Product::priceCalculation() con id_group explicito (firma estable 1.7-8.x).
  - Los precios de tarifa se emiten SIEMPRE aunque coincidan con el base (si el
    campo faltara, el buscador trataria el producto como "sin precio" al
    ordenar/filtrar por esa tarifa).
  - El conector DECLARA sus tarifas en POST /api/v1/feed/price-tiers (claves =
    id_group) al sincronizar; la plataforma valida contra el limite del plan
    (maxPriceTiers; el precio base cuenta como tarifa 1) y prepara el indice.
    Se autolimita a maxPriceTiers - 1 grupos (leido de /feed/plan). Si la API
    no expone maxPriceTiers (API antigua) no se declara ni se emite nada.
  - ApproSearchConfig ahora incluye `priceTier` (id del grupo por defecto del
    cliente logueado, solo si esta entre los exportados): el widget muestra y
    el buscador ordena/filtra por el precio de esa tarifa. Sin login se usa el
    precio base. La tarifa viaja en claro (sin token firmado): no usar para
    precios confidenciales.
  - INCOMPATIBLE con "Modo catalogos grandes": al activar tarifas se desactivan
    APPROSEARCH_LARGE_CATALOG y APPROSEARCH_INCREMENTAL automaticamente (con
    aviso), y el motor de sync fuerza el modo normal aunque la config quedara
    inconsistente. Motivo: el modo SQL en bloque no pasa por priceCalculation.
  - Panel "Plan actual": nueva fila "Tarifas de precio" (maxPriceTiers).
  - Tras activar tarifas, lanzar una reindexacion completa para poblar los
    campos; los hooks de specific_price ya reindexan los cambios de precio.
- Sin migracion de datos (upgrade-2.5.0.php solo siembra los toggles).
  Compatibilidad total con 2.4.x.

2.4.2 - 2026-06-11
------------------
- Nueva opcion "Desactivar indexacion automatica por hooks"
  (APPROSEARCH_DISABLE_HOOK_INDEX, OFF por defecto). Con ON, el modulo deja de
  indexar/borrar en tiempo real desde los hooks add/update/delete (productos,
  precios especificos, stock, categorias, paginas CMS y fabricantes). Pensada
  para tiendas con mucho catalogo o importaciones masivas (ERP), donde cada
  hook lanza peticiones HTTP sincronas a la API y ralentiza el guardado: en
  esos casos se desactiva y el indice se mantiene con el cron POR LOTES +
  sincronizacion incremental.
  - No afecta a la reindexacion manual del BO ni a los crons (full y por lotes).
  - Aviso: con la opcion activa, las bajas de productos solo se reflejan en el
    indice con un rebuild limpio ("Reindexar todo" o cron con &fresh=1).
- Sin migracion de datos (upgrade-2.4.2.php solo siembra el toggle en OFF).
  Compatibilidad total con 2.4.x.

2.4.0 - 2026-06-10
------------------
- MULTI-MONEDA por producto (opt-in, toggle "Multi-moneda" / APPROSEARCH_MULTICURRENCY,
  OFF por defecto). Con OFF el comportamiento es identico (solo moneda base).
  - Con ON, los builders (modo normal y catalogos grandes) envian, ademas del precio
    base, el precio de cada producto en TODAS las monedas activas de la tienda como
    campos planos priceInclTax_<ISO>, priceExclTax_<ISO>, originalPrice(Incl|Excl)Tax_<ISO>.
    Honra precios especificos por moneda (SpecificPrice.id_currency) y la conversion,
    computando getPriceStatic con la moneda fijada en el Context (que se restaura).
  - El documento incluye `currency` = moneda base (PS_CURRENCY_DEFAULT).
  - El conector DECLARA sus monedas en POST /api/v1/feed/currencies al sincronizar
    (full y por lotes); la plataforma valida contra el limite del plan (maxCurrencies)
    y prepara el indice. Se autolimita al maximo del plan (leido de /feed/plan).
  - ApproSearchConfig ahora incluye `currency` (moneda activa del visitante): el
    widget muestra el simbolo/formato correcto y elige el precio de esa moneda
    (con fallback al precio base). Antes el widget mostraba siempre EUR.
- Sin migracion de datos (upgrade-2.4.0.php solo siembra el toggle en OFF).
  Compatibilidad total con 2.3.1.

2.3.1 - 2026-06-09
------------------
- Nueva opcion de LIMPIEZA del modo incremental para reindexar el catalogo
  desde 0. La sincronizacion incremental guarda un hash md5 por documento en
  ps_approsearch_index_state y omite los productos sin cambios; hasta ahora no
  habia forma de forzar el reenvio completo, y un fresh=1 vaciaba el indice
  remoto pero NO esa tabla de hashes -> los productos sin cambios se seguian
  omitiendo y el indice quedaba incompleto.
  - Nuevo helper interno clearIndexState(): vacia (TRUNCATE) la tabla de hashes
    ps_approsearch_index_state (no-op si no existe; existencia via
    information_schema, no SHOW TABLES).
  - Nuevo boton de BO "Limpiar estado incremental (reindexar de 0)" (submit
    submitApprosearchClearIncremental): limpia los hashes y reinicia el cursor
    del cron por lotes -> la siguiente pasada reenvia TODOS los productos.
  - Nueva accion de cron what=reset_incremental
    (/module/approsearch/cron?token=XXX&what=reset_incremental): hace lo mismo
    que el boton (limpia hashes + reinicia cursor) y responde JSON. NO indexa;
    pensada para programarla puntualmente (p.ej. 1x/semana) seguida del cron
    normal what=products. Metodo publico resetIncrementalState() compartido por
    BO y cron. El panel "URLs para cron" del BO la muestra con su ejemplo crontab.
  - El run "fresh" (rebuild limpio: boton "Reindexar todo" y URL de cron con
    &fresh=1) ahora ademas limpia el estado incremental, de modo que un rebuild
    reindexa de verdad desde 0 aunque la sincronizacion incremental este activa.
- Sin migracion de datos (upgrade-2.3.1.php es no-op; la tabla se crea/limpia de
  forma perezosa). Compatibilidad total con 2.3.0.

2.3.0 - 2026-06-09
------------------
- Indexacion de productos reestructurada en DOS crons independientes y claros:
  1) "Indexar todo" (what=full): reindexa productos + categorias + CMS en UNA
     pasada (rebuild limpio via fullSync()). Sin reanudacion ni troceo por tiempo.
     Recomendado para catalogos pequenos (<5000). Es lo que dispara el boton
     "Reindexar todo" del BO. AVISO: en catalogos grandes el servidor puede cortar
     por timeout y dejar la indexacion incompleta -> usar el cron por lotes.
  2) "Por lotes" (what=products): motor reanudable (cursor + time-box) que se
     autogestiona; hace el backfill y, al completar una pasada, se REINICIA solo
     pasado el nuevo intervalo de re-sync. Pensado SOLO para ser llamado por cron
     (ya no hay boton que lo dispare). Recomendado cada 5 min.
- Nueva Configuration APPROSEARCH_RESYNC_INTERVAL (minutos; 0 = continuo;
  def. 1440 = 1x/dia): controla cuanto espera el cron por lotes tras completar
  antes de empezar otra pasada. Sustituye al "reinicio diario" externo.
- ELIMINADO el campo configurable "Tiempo maximo por ejecucion"
  (APPROSEARCH_CRON_BUDGET). El time-box del cron por lotes pasa a ser interno y
  fijo (constante CRON_SLICE_SECONDS = 50 s, bajo el timeout tipico de nginx/PHP-FPM).
  Para acelerar el backfill inicial, ejecutar el cron mas a menudo (no subir nada).
- BO: el panel "URLs para cron" ahora guia a elegir UNA estrategia (full vs lotes)
  con su cadencia recomendada y dos ejemplos de crontab. Botones "Reindexar todo"
  y "Solo Productos" hacen reindexacion EN UNA PASADA (fullSync), no el motor por
  lotes. Panel de progreso renombrado a "cron por lotes" + fila "Proximo re-sync".
- Compatibilidad: instalaciones <2.3.0 migran con upgrade/upgrade-2.3.0.php
  (siembra APPROSEARCH_RESYNC_INTERVAL=1440, limpia APPROSEARCH_CRON_BUDGET). Las
  URLs de cron existentes (what=products, what=all) siguen funcionando; what=all
  se mantiene como legado (productos por lotes + categorias + CMS). No se reinstala
  ni se toca el cursor de sync en curso.

2.2.5 - 2026-06-05
------------------
- Fix (modo catalogos grandes): el tope de tiempo por ejecucion
  (APPROSEARCH_CRON_BUDGET) solo se comprobaba entre chunks completos
  (APPROSEARCH_BATCH_SIZE), asi que con lote grande (p.ej. 5000) una ejecucion
  podia exceder bastante el presupuesto. Ahora el chunk se procesa en sub-lotes
  de 500 (build SQL + envio) y el tope de tiempo se comprueba ENTRE sub-lotes,
  avanzando el cursor de forma incremental. El presupuesto se respeta con
  granularidad de ~500 productos (+ el sub-lote en curso).
  Nota: el limite duro real sigue siendo request_terminate_timeout de PHP-FPM;
  el budget es una pausa cooperativa, no un kill.

2.2.4 - 2026-06-05
------------------
- "Modo catalogos grandes" (opt-in, por defecto OFF): construye el indice con
  consultas SQL en bloque y lee el stock de ps_stock_available, en vez de cargar
  cada producto con el ORM y consultar el stock uno a uno. Para catalogos de
  cientos de miles / millones; en tiendas pequenas no hace falta.
  - Nuevo build set-based buildProductDataBatch() + processChunkLarge().
  - Mismo documento que el modo normal; el precio sigue via getPriceStatic.
- "Sincronizacion incremental" (opt-in, por defecto OFF; requiere modo catalogos
  grandes): solo reenvia los productos cuyo documento cambio, comparando un hash
  md5 guardado en la tabla ps_approsearch_index_state. Acelera las pasadas
  periodicas tras el primer indexado. Panel BO muestra "Omitidos (sin cambios)".
- Config nuevas: APPROSEARCH_LARGE_CATALOG, APPROSEARCH_INCREMENTAL (ambas OFF).
- Comportamiento por defecto SIN cambios: con ambos OFF, el sync es identico a
  2.2.3 (build por producto). Conectores no actualizados siguen funcionando igual.

2.2.3 - 2026-06-04
------------------
- Indexacion de productos REANUDABLE (cursor + time-box) para catalogos grandes:
  evita el timeout del full sync (500k-1M productos).
  - Nuevo motor processProductSyncSlice()/cronProductSync(): recorre el catalogo
    por lotes con cursor (id_product) persistido en Configuration
    (APPROSEARCH_SYNC_STATE); reanudable entre llamadas. Lock MySQL
    GET_LOCK('approsearch_product_sync') anti-solape.
  - controllers/front/cron.php (what=products|all) usa el motor reanudable y se
    auto-limita por APPROSEARCH_CRON_BUDGET (def. 25 s). Pensado para cron 1x/min.
    ?fresh=1 vacia el indice al iniciar la generacion (rebuild limpio).
  - BO: "Solo Productos"/"Sincronizar TODO" arrancan/continuan el run reanudable
    (ya no corren sincronos), boton "Reiniciar progreso" y panel de progreso
    (cursor, enviados, errores).
  - Sin /feed/products/clear por defecto (upsert sobre el indice vivo); fullSync()
    se conserva como fallback (ya no lo invocan cron/BO).
  - Nuevas Configuration: APPROSEARCH_CRON_BUDGET, APPROSEARCH_SYNC_STATE
    (autocreadas en getContent/cron; no requieren reinstalar).
  - "Productos por lote" admite hasta 5000 (lectura de BD por ciclo); el envio a
    la API se trocea siempre en bloques de 500 (limite duro de la plataforma) y se
    comprueba el tope de tiempo entre bloques.
  - APPROSEARCH_CRON_BUDGET (tiempo max por ejecucion del cron de productos) ahora
    es configurable en el BO: 5-3600 s (hasta 1 hora). El corte efectivo lo marca
    el timeout de nginx/PHP-FPM; al ser reanudable, continua en la siguiente.

2.2.2 - 2026-05-29
------------------
- Refactor a estandar PrestaShop: se eliminan los <style> y <script> inline de
  los templates Smarty.
  - Nuevo views/css/approsearch.css con los estilos del boton trigger
    (se quita la font-family propia; ahora hereda la tipografia del tema).
  - Nuevo views/js/approsearch.js con el comportamiento (trigger + intercepcion
    del formulario de busqueda nativo).
  - hookActionFrontControllerSetMedia registra CSS/JS con
    registerStylesheet/registerJavascript (el widget remoto con server=remote).
  - La config del widget se expone con Media::addJsDef (window.ApproSearchConfig)
    en vez de un <script> inline.
  - header.tpl queda solo con preconnect/dns-prefetch.
  - Anadidos views/css/index.php y views/js/index.php.

2.2.1 - 2026-05-19
------------------
- Fix: las URLs indexadas de productos, categorías y páginas CMS se enviaban
  siempre en el idioma por defecto de la tienda (PS_LANG_DEFAULT).
  Causa: Link::getProductLink/getCategoryLink/getCMSLink se llamaban sin
  $id_lang, así que tomaban el idioma del contexto, que ensureShopContext()
  fija a PS_LANG_DEFAULT.
  Solución: pasar el $langId que recibe buildProductData / indexCategory /
  indexCmsPage para que la URL indexada lleve el prefijo correcto (/fr/,
  /en/, ...).
- Requiere re-sincronizar (fullSync*) tras actualizar para reescribir las
  URLs ya almacenadas en el índice.

2.2.0 - 2026-04-22
------------------
- Añadida URL de cron pública para sincronización de índices (productos, categorías, CMS).
- Nuevo front controller: controllers/front/cron.php
  - Endpoint: /module/approsearch/cron?token=XXX&what=all|products|categories|cms
  - Autenticación por token configurable (APPROSEARCH_CRON_TOKEN).
  - set_time_limit(0) e ignore_user_abort(true) para ejecuciones largas.
  - Responde JSON con duración y resultados.
- Panel en el back office (getContent) con las 4 URLs listas para copiar
  al crontab y botón "Regenerar token".
- Token de cron generado automáticamente en install() y en la primera
  apertura de la configuración (regenerable desde el BO).
- fullSync(), fullSyncCategories() y fullSyncCmsPages() ahora son public
  para poder invocarse desde el front controller.

2.1.0 - 2026-02-24
------------------
- Versión previa a la publicación del endpoint de cron.
