Plugin Directory

Changeset 3460823


Ignore:
Timestamp:
02/13/2026 02:05:37 PM (7 weeks ago)
Author:
iberpixel
Message:

Preparing version 1.1.3

Location:
holnix/trunk
Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • holnix/trunk/admin/class-holnix-admin.php

    r3421769 r3460823  
    6262
    6363        // --- CONFIGURACIÓN DE API ---
    64         $this->api_url = 'https://api.holnix.com/api';
     64        $this->api_url = $this->get_api_base_url();
    6565        $this->api_headers = [
    6666            'headers' => [
     
    6868                'Accept' => 'application/json',
    6969                'Content-Type' => 'application/json',
     70                'X-Holnix-Source' => 'wordpress',
    7071            ],
     72        ];
     73
     74        $this->holnix_log('Inicialización de cliente API', [
     75            'api_url' => $this->api_url,
     76            'token_present' => !empty($this->bearer),
     77            'token_length' => strlen((string) $this->bearer),
     78            'source' => 'wordpress',
     79        ]);
     80    }
     81
     82    /**
     83     * Construye los argumentos para requests a Holnix.
     84     *
     85     * @param array $extra_args Argumentos extra para wp_remote_*.
     86     * @return array
     87     */
     88    public function get_api_request_args($extra_args = [])
     89    {
     90        $args = [
     91            'timeout' => 45,
     92            'redirection' => 3,
     93            'httpversion' => '1.1',
     94            'headers' => [],
     95        ];
     96        $args = array_replace_recursive($args, $this->api_headers);
     97
     98        if (!empty($extra_args)) {
     99            $args = array_replace_recursive($args, $extra_args);
     100        }
     101
     102        return $args;
     103    }
     104
     105    /**
     106     * Obtiene la URL base de la API.
     107     *
     108     * @return string
     109     */
     110    private function get_api_base_url()
     111    {
     112        $default_url = 'https://api.holnix.com';
     113        $configured_url = defined('HOLNIX_API_BASE_URL') ? HOLNIX_API_BASE_URL : $default_url;
     114
     115        if (!is_string($configured_url)) {
     116            return $default_url;
     117        }
     118
     119        $configured_url = trim($configured_url);
     120        if ($configured_url === '') {
     121            return $default_url;
     122        }
     123
     124        return untrailingslashit($configured_url);
     125    }
     126
     127    /**
     128     * Indica si el logging de diagnóstico está habilitado.
     129     *
     130     * @return bool
     131     */
     132    private function is_holnix_logging_enabled()
     133    {
     134        if (defined('HOLNIX_DEBUG_LOG')) {
     135            return (bool) HOLNIX_DEBUG_LOG;
     136        }
     137
     138        if (function_exists('wp_get_environment_type') && wp_get_environment_type() === 'production') {
     139            return false;
     140        }
     141
     142        if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
     143            return true;
     144        }
     145
     146        if (defined('WP_DEBUG') && WP_DEBUG) {
     147            return true;
     148        }
     149
     150        // Por defecto, evitar ruido de logs en instalaciones productivas.
     151        return false;
     152    }
     153
     154    /**
     155     * Obtiene un extracto seguro para logging.
     156     *
     157     * @param mixed $value Valor a resumir.
     158     * @param int $max_length Longitud máxima.
     159     * @return string
     160     */
     161    private function get_log_excerpt($value, $max_length = 1200)
     162    {
     163        if (!is_string($value)) {
     164            if (is_scalar($value)) {
     165                $value = (string) $value;
     166            } else {
     167                $value = wp_json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
     168            }
     169        }
     170
     171        if (!is_string($value)) {
     172            return '';
     173        }
     174
     175        if (strlen($value) > $max_length) {
     176            return substr($value, 0, $max_length) . '...';
     177        }
     178
     179        return $value;
     180    }
     181
     182    /**
     183     * Registra mensajes de diagnóstico del plugin en debug.log.
     184     *
     185     * @param string $message Mensaje principal.
     186     * @param array $context Contexto adicional serializable.
     187     * @return void
     188     */
     189    private function holnix_log($message, $context = [])
     190    {
     191        if (!$this->is_holnix_logging_enabled()) {
     192            return;
     193        }
     194
     195        $line = '[Holnix] ' . $message;
     196        if (!empty($context)) {
     197            $json = wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
     198            if (is_string($json) && $json !== '') {
     199                $line .= ' | ' . $json;
     200            }
     201        }
     202
     203        error_log($line);
     204    }
     205
     206    /**
     207     * Resume una respuesta HTTP de WordPress para logging.
     208     *
     209     * @param array|WP_Error $response Respuesta de wp_remote_*.
     210     * @return array
     211     */
     212    private function get_response_summary($response)
     213    {
     214        if (is_wp_error($response)) {
     215            $error_codes = $response->get_error_codes();
     216            $error_data = [];
     217            foreach ($error_codes as $error_code) {
     218                $error_data[$error_code] = $response->get_error_data($error_code);
     219            }
     220
     221            return [
     222                'wp_error_codes' => $error_codes,
     223                'wp_error' => $response->get_error_message(),
     224                'wp_error_data' => $error_data,
     225            ];
     226        }
     227
     228        $body = wp_remote_retrieve_body($response);
     229        $body = $this->get_log_excerpt($body);
     230
     231        return [
     232            'status_code' => wp_remote_retrieve_response_code($response),
     233            'body' => $body,
     234        ];
     235    }
     236
     237    /**
     238     * Obtiene un posible fallback de URL alternando el prefijo /api.
     239     *
     240     * @param string $api_url URL original.
     241     * @return string
     242     */
     243    private function get_fallback_api_url($api_url)
     244    {
     245        if (!is_string($api_url) || $api_url === '') {
     246            return '';
     247        }
     248
     249        if (strpos($api_url, '/api/') !== false) {
     250            return preg_replace('#/api/#', '/', $api_url, 1);
     251        }
     252
     253        $parts = wp_parse_url($api_url);
     254        if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
     255            return '';
     256        }
     257
     258        $path = isset($parts['path']) ? $parts['path'] : '/';
     259        if (strpos($path, '/api/') === 0 || $path === '/api') {
     260            return '';
     261        }
     262
     263        $fallback = $parts['scheme'] . '://' . $parts['host'];
     264        if (!empty($parts['port'])) {
     265            $fallback .= ':' . $parts['port'];
     266        }
     267
     268        $fallback_path = '/api' . (strpos($path, '/') === 0 ? $path : '/' . $path);
     269        $fallback .= $fallback_path;
     270
     271        if (!empty($parts['query'])) {
     272            $fallback .= '?' . $parts['query'];
     273        }
     274
     275        return $fallback;
     276    }
     277
     278    /**
     279     * Obtiene el path normalizado de una URL de API.
     280     *
     281     * @param string $api_url URL completa del endpoint.
     282     * @return string
     283     */
     284    private function get_normalized_api_path($api_url)
     285    {
     286        if (!is_string($api_url) || $api_url === '') {
     287            return '';
     288        }
     289
     290        $parts = wp_parse_url($api_url);
     291        $path = is_array($parts) && isset($parts['path']) ? (string) $parts['path'] : '';
     292        if ($path === '') {
     293            return '';
     294        }
     295
     296        $path = '/' . ltrim($path, '/');
     297        $path = strtolower($path);
     298        $path = preg_replace('#^/api(?:/|$)#', '/', $path);
     299        if (!is_string($path) || $path === '') {
     300            return '';
     301        }
     302
     303        return '/' . trim($path, '/');
     304    }
     305
     306    /**
     307     * Resuelve el event_type para logging del backend.
     308     *
     309     * @param string $method Método HTTP.
     310     * @param string $api_url URL de request.
     311     * @param array $context Contexto opcional (permite override con event_type).
     312     * @return string
     313     */
     314    private function get_request_event_type($method, $api_url, $context = [])
     315    {
     316        if (!empty($context['event_type']) && is_string($context['event_type'])) {
     317            $custom_event = sanitize_key($context['event_type']);
     318            if ($custom_event !== '') {
     319                return $custom_event;
     320            }
     321        }
     322
     323        $normalized_path = $this->get_normalized_api_path($api_url);
     324        if ($normalized_path === '') {
     325            return '';
     326        }
     327
     328        $normalized_method = strtoupper((string) $method);
     329
     330        if (preg_match('#^/news(?:-v2)?(?:/|$)#', $normalized_path)) {
     331            return 'news_list';
     332        }
     333
     334        if (preg_match('#^/imprint(?:-v2)?(?:/|$)#', $normalized_path)) {
     335            return 'imprint_list';
     336        }
     337
     338        if ($normalized_method === 'POST' && preg_match('#^/search-isbns(?:/|$)#', $normalized_path)) {
     339            return 'bulk_query';
     340        }
     341
     342        if ($normalized_method === 'GET' && preg_match('#^/isbn/[^/]+$#', $normalized_path)) {
     343            return 'detail_view';
     344        }
     345
     346        if ($normalized_method === 'GET' && preg_match('#^/isbns(?:/|$)#', $normalized_path)) {
     347            return 'detail_view';
     348        }
     349
     350        if ($normalized_method === 'GET' && preg_match('#^/isbns-v2/([^/]+)$#', $normalized_path, $matches)) {
     351            return 'detail_view';
     352        }
     353
     354        return '';
     355    }
     356
     357    /**
     358     * Detecta errores de conexión candidatos a reintento.
     359     *
     360     * @param array|WP_Error $response Respuesta HTTP.
     361     * @return bool
     362     */
     363    private function is_retryable_connection_error($response)
     364    {
     365        if (!is_wp_error($response)) {
     366            return false;
     367        }
     368
     369        $message = strtolower($response->get_error_message());
     370        $needles = [
     371            'connection reset by peer',
     372            'cURL error 35',
     373            'ssl_connect',
     374            'timed out',
     375            'couldn\'t connect',
     376            'could not resolve host',
     377        ];
     378
     379        foreach ($needles as $needle) {
     380            if (strpos($message, strtolower($needle)) !== false) {
     381                return true;
     382            }
     383        }
     384
     385        return false;
     386    }
     387
     388    /**
     389     * Resume información útil de runtime para diagnóstico.
     390     *
     391     * @return array
     392     */
     393    private function get_runtime_summary()
     394    {
     395        global $wp_version;
     396
     397        $summary = [
     398            'php_version' => PHP_VERSION,
     399            'wp_version' => isset($wp_version) ? $wp_version : '',
     400            'openssl' => defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : '',
     401        ];
     402
     403        if (function_exists('curl_version')) {
     404            $curl_data = curl_version();
     405            $summary['curl_version'] = isset($curl_data['version']) ? $curl_data['version'] : '';
     406            $summary['curl_ssl_version'] = isset($curl_data['ssl_version']) ? $curl_data['ssl_version'] : '';
     407        }
     408
     409        return $summary;
     410    }
     411
     412    /**
     413     * Genera diagnóstico básico de DNS/TCP para un host remoto.
     414     *
     415     * @param string $api_url URL a diagnosticar.
     416     * @return array
     417     */
     418    private function get_network_summary($api_url)
     419    {
     420        $parts = wp_parse_url($api_url);
     421        $host = is_array($parts) && isset($parts['host']) ? $parts['host'] : '';
     422        $scheme = is_array($parts) && isset($parts['scheme']) ? $parts['scheme'] : 'https';
     423        $port = is_array($parts) && !empty($parts['port']) ? (int) $parts['port'] : (($scheme === 'http') ? 80 : 443);
     424
     425        if ($host === '') {
     426            return ['host' => '', 'port' => $port];
     427        }
     428
     429        $resolved_ip = gethostbyname($host);
     430        $dns_resolved = $resolved_ip && $resolved_ip !== $host;
     431
     432        $tcp_status = 'not_checked';
     433        $tcp_error = '';
     434        $errno = 0;
     435        $errstr = '';
     436        $socket = @fsockopen($host, $port, $errno, $errstr, 3);
     437        if ($socket) {
     438            $tcp_status = 'ok';
     439            fclose($socket);
     440        } else {
     441            $tcp_status = 'error';
     442            $tcp_error = $errno . ' ' . $errstr;
     443        }
     444
     445        return [
     446            'host' => $host,
     447            'port' => $port,
     448            'resolved_ip' => $dns_resolved ? $resolved_ip : '',
     449            'dns_resolved' => $dns_resolved,
     450            'tcp_status' => $tcp_status,
     451            'tcp_error' => $tcp_error,
     452        ];
     453    }
     454
     455    /**
     456     * Ejecuta un request contra la API con diagnóstico y fallback opcional /api.
     457     *
     458     * @param string $method Método HTTP.
     459     * @param string $api_url URL objetivo.
     460     * @param array $extra_args Argumentos extra del request.
     461     * @param array $context Contexto para logging.
     462     * @return array
     463     */
     464    public function holnix_api_request($method, $api_url, $extra_args = [], $context = [])
     465    {
     466        $request_args = $this->get_api_request_args($extra_args);
     467        $request_method = strtoupper((string) $method);
     468        $request_args['method'] = $request_method;
     469
     470        $event_type = '';
     471        if (
     472            isset($request_args['headers']) &&
     473            is_array($request_args['headers']) &&
     474            !empty($request_args['headers']['X-Holnix-Event'])
     475        ) {
     476            $event_type = sanitize_key((string) $request_args['headers']['X-Holnix-Event']);
     477        } else {
     478            $event_type = $this->get_request_event_type($request_method, $api_url, $context);
     479        }
     480
     481        if (
     482            !isset($request_args['headers']) ||
     483            !is_array($request_args['headers'])
     484        ) {
     485            $request_args['headers'] = [];
     486        }
     487
     488        if ($event_type !== '') {
     489            $request_args['headers']['X-Holnix-Event'] = $event_type;
     490        }
     491
     492        $this->holnix_log('Metadata de request API', array_merge($context, [
     493            'method' => $request_method,
     494            'api_url' => $api_url,
     495            'event_type' => $event_type,
     496            'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '',
     497        ]));
     498
     499        $attempt_urls = [$api_url];
     500        $fallback_url = $this->get_fallback_api_url($api_url);
     501        if (!empty($fallback_url) && $fallback_url !== $api_url) {
     502            $attempt_urls[] = $fallback_url;
     503        }
     504
     505        $last_response = null;
     506        $final_url = $api_url;
     507        $attempt = 0;
     508
     509        foreach ($attempt_urls as $attempt_url) {
     510            $attempt++;
     511            $final_url = $attempt_url;
     512            $response = wp_remote_request($attempt_url, $request_args);
     513            $last_response = $response;
     514
     515            if (!is_wp_error($response)) {
     516                $status_code = wp_remote_retrieve_response_code($response);
     517                if ($status_code === 404 && $attempt === 1 && count($attempt_urls) > 1) {
     518                    $this->holnix_log('HTTP 404 en request API, probando fallback de base URL', array_merge($context, [
     519                        'method' => $request_method,
     520                        'attempt' => $attempt,
     521                        'api_url' => $attempt_url,
     522                        'fallback_url' => $fallback_url,
     523                        'event_type' => $event_type,
     524                        'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '',
     525                    ]));
     526                    continue;
     527                }
     528
     529                return [
     530                    'response' => $response,
     531                    'final_url' => $final_url,
     532                    'attempt' => $attempt,
     533                    'event_type' => $event_type,
     534                ];
     535            }
     536
     537            $this->holnix_log('WP_Error en request API', array_merge($context, [
     538                'method' => $request_method,
     539                'attempt' => $attempt,
     540                'api_url' => $attempt_url,
     541                'event_type' => $event_type,
     542                'source_app' => isset($request_args['headers']['X-Holnix-Source']) ? $request_args['headers']['X-Holnix-Source'] : '',
     543                'response' => $this->get_response_summary($response),
     544                'runtime' => $this->get_runtime_summary(),
     545                'network' => $this->get_network_summary($attempt_url),
     546            ]));
     547
     548            if (!$this->is_retryable_connection_error($response)) {
     549                break;
     550            }
     551        }
     552
     553        return [
     554            'response' => $last_response,
     555            'final_url' => $final_url,
     556            'attempt' => $attempt,
     557            'event_type' => $event_type,
    71558        ];
    72559    }
     
    90577
    91578        $isbns = isset($_POST['isbns']) ? array_map('sanitize_text_field', $_POST['isbns']) : [];
     579        $this->holnix_log('Inicio holnix_start_full_import', ['isbn_count' => count($isbns)]);
    92580
    93581        if (empty($isbns)) {
     582            $this->holnix_log('holnix_start_full_import sin ISBNs');
    94583            wp_send_json_error(['message' => 'No se proporcionaron ISBNs.']);
    95584            return;
     
    99588        $isbn_chunks = array_chunk($isbns, 200);
    100589
    101         foreach ($isbn_chunks as $chunk) {
     590        foreach ($isbn_chunks as $chunk_index => $chunk) {
    102591            $search_url = $this->api_url . '/search-isbns';
    103             $args = [
    104                 'headers' => $this->api_headers['headers'],
    105                 'body' => json_encode(['isbns' => $chunk]),
     592            $request = $this->holnix_api_request('POST', $search_url, [
     593                'body' => wp_json_encode(['isbns' => $chunk]),
    106594                'timeout' => 45,
    107             ];
    108             $response = wp_remote_post($search_url, $args);
     595            ], [
     596                'context' => 'holnix_start_full_import',
     597                'chunk_index' => $chunk_index,
     598                'chunk_size' => count($chunk),
     599            ]);
     600            $response = $request['response'];
     601            $final_url = $request['final_url'];
     602
     603            $this->holnix_log('Enviando request search-isbns', [
     604                'url' => $search_url,
     605                'final_url' => $final_url,
     606                'chunk_index' => $chunk_index,
     607                'chunk_size' => count($chunk),
     608                'attempt' => $request['attempt'],
     609            ]);
    109610
    110611            if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
     612                $this->holnix_log('Error en search-isbns', [
     613                    'url' => $search_url,
     614                    'final_url' => $final_url,
     615                    'chunk_index' => $chunk_index,
     616                    'chunk_size' => count($chunk),
     617                    'response' => $this->get_response_summary($response),
     618                ]);
    111619                $error_message = 'Error conectando con Holnix API para buscar ISBNs.';
    112620                if (is_wp_error($response)) {
     
    123631            if (is_array($data)) {
    124632                $all_books_data = array_merge($all_books_data, $data);
     633            } else {
     634                $this->holnix_log('Respuesta JSON inválida en search-isbns', [
     635                    'url' => $search_url,
     636                    'chunk_index' => $chunk_index,
     637                    'chunk_size' => count($chunk),
     638                    'response' => $this->get_response_summary($response),
     639                ]);
    125640            }
    126641        }
    127642
    128643        if (!empty($all_books_data)) {
     644            $this->holnix_log('Datos recibidos para importación', ['books_count' => count($all_books_data)]);
    129645            $importer = new Holnix_Import();
    130646            $result = $importer->schedule_import($all_books_data);
    131647            if (is_wp_error($result)) {
     648                $this->holnix_log('Error al programar importación', [
     649                    'error_code' => $result->get_error_code(),
     650                    'error_message' => $result->get_error_message(),
     651                ]);
    132652                wp_send_json_error(['message' => $result->get_error_message()]);
    133653                return;
     
    135655            wp_send_json_success(['message' => 'Importación de productos iniciada correctamente.']);
    136656        } else {
     657            $this->holnix_log('search-isbns no retornó libros', ['isbn_count' => count($isbns)]);
    137658            wp_send_json_error(['message' => 'No se encontró información para los ISBNs seleccionados.']);
    138659        }
     
    164685        $api_url = add_query_arg($query_params, $api_url);
    165686
    166         $response = wp_remote_get($api_url, $this->api_headers);
     687        $this->holnix_log('Enviando request holnix_fetch_imprint_batch', [
     688            'url' => $api_url,
     689            'imprint' => $imprint,
     690            'language' => $language,
     691            'page' => $page,
     692            'per_page' => $per_page,
     693        ]);
     694
     695        $request = $this->holnix_api_request('GET', $api_url, [], [
     696            'context' => 'holnix_fetch_imprint_batch',
     697            'imprint' => $imprint,
     698            'language' => $language,
     699            'page' => $page,
     700            'per_page' => $per_page,
     701        ]);
     702        $response = $request['response'];
     703        $final_url = $request['final_url'];
    167704
    168705        if (is_wp_error($response)) {
     706            $this->holnix_log('Error en holnix_fetch_imprint_batch (WP_Error)', [
     707                'url' => $api_url,
     708                'final_url' => $final_url,
     709                'imprint' => $imprint,
     710                'language' => $language,
     711                'page' => $page,
     712                'response' => $this->get_response_summary($response),
     713            ]);
    169714            wp_send_json_error(['message' => 'Error de conexión con la API: ' . $response->get_error_message()]);
    170715            return;
     
    176721
    177722        if ($response_code !== 200) {
     723            $this->holnix_log('Error HTTP en holnix_fetch_imprint_batch', [
     724                'url' => $api_url,
     725                'final_url' => $final_url,
     726                'imprint' => $imprint,
     727                'language' => $language,
     728                'page' => $page,
     729                'response' => $this->get_response_summary($response),
     730            ]);
    178731            wp_send_json_error(['message' => 'La API devolvió un error: ' . $response_code, 'details' => $data]);
    179732            return;
     
    181734
    182735        if (empty($data)) {
     736            $this->holnix_log('Respuesta vacía en holnix_fetch_imprint_batch', [
     737                'url' => $api_url,
     738                'final_url' => $final_url,
     739                'page' => $page,
     740            ]);
    183741            wp_send_json_success(['html' => '', 'pagination' => ['currentPage' => $page, 'lastPage' => $page, 'total' => 0], 'count' => 0]);
    184742            return;
     
    191749            'total' => isset($data['total']) ? $data['total'] : count($items),
    192750        ];
     751
     752        $this->holnix_log('Respuesta OK en holnix_fetch_imprint_batch', [
     753            'url' => $api_url,
     754            'final_url' => $final_url,
     755            'page' => $pagination_data['currentPage'],
     756            'last_page' => $pagination_data['lastPage'],
     757            'total' => $pagination_data['total'],
     758            'items_count' => is_array($items) ? count($items) : 0,
     759        ]);
    193760
    194761        ob_start();
     
    316883                    </div>
    317884                <?php } elseif ($this->page === 'holnix_imprint') {
    318                     $response = wp_remote_get($this->api_url . '/imprints', $this->api_headers);
    319                     if (is_wp_error($response) || $response['response']['code'] == 401) {
    320                         echo '<div class="error"><p>Error de autentificación o conexión con la API.</p></div>';
     885                    $imprints_url = $this->api_url . '/imprints';
     886                    $request = $this->holnix_api_request('GET', $imprints_url, [], [
     887                        'context' => 'form_params_imprints',
     888                        'page' => $this->page,
     889                    ]);
     890                    $response = $request['response'];
     891                    $final_url = $request['final_url'];
     892                    $response_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response);
     893                    if (is_wp_error($response) || $response_code === 401) {
     894                        $this->holnix_log('Error cargando imprints para formulario', [
     895                            'url' => $imprints_url,
     896                            'final_url' => $final_url,
     897                            'response' => $this->get_response_summary($response),
     898                        ]);
     899                        $message = is_wp_error($response)
     900                            ? 'Error de conexión con la API: ' . $response->get_error_message()
     901                            : 'Error de autenticación con la API (401).';
     902                        echo '<div class="error"><p>' . esc_html($message) . '</p></div>';
    321903                        return;
    322904                    }
     
    4721054        {
    4731055
    474             echo wp_kses_post($this->header());
     1056            $this->header();
    4751057
    4761058            $this->display_import_progress_bar();
     
    4821064                <div class="custom-dashboard">
    4831065
    484                     <?php echo wp_kses_post($this->customNav()); ?>
    485 
    486                     <?php echo wp_kses_post($this->customSections()); ?>
     1066                    <?php $this->customNav(); ?>
     1067
     1068                    <?php $this->customSections(); ?>
    4871069
    4881070                </div>
     
    4901072                <?php
    4911073
    492                 $response = wp_remote_get($this->api_url . '/skus', $this->api_headers);
    493 
    494                 if (is_wp_error($response) || $response['response']['code'] == 401) {
    495 
    496                     echo '<div class="error"><p>Error de autentificación o conexión con la API.</p></div>';
    497 
     1074                $skus_url = $this->api_url . '/skus';
     1075                $request = $this->holnix_api_request('GET', $skus_url, [], [
     1076                    'context' => 'holnix_catalog_page',
     1077                    'page' => $this->page,
     1078                ]);
     1079                $response = $request['response'];
     1080                $final_url = $request['final_url'];
     1081                $response_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response);
     1082
     1083                if (is_wp_error($response) || $response_code === 401) {
     1084                    $this->holnix_log('Error cargando skus en dashboard', [
     1085                        'url' => $skus_url,
     1086                        'final_url' => $final_url,
     1087                        'response' => $this->get_response_summary($response),
     1088                    ]);
     1089                    $message = is_wp_error($response)
     1090                        ? 'Error de conexión con la API: ' . $response->get_error_message()
     1091                        : 'Error de autenticación con la API (401).';
     1092                    echo '<div class="error"><p>' . esc_html($message) . '</p></div>';
    4981093                    return;
    499 
    5001094                }
    5011095
     
    6181212        {
    6191213
    620             echo wp_kses_post($this->header());
     1214            $this->header();
    6211215
    6221216            ?>
     
    6261220                <div class="custom-dashboard">
    6271221
    628                     <?php echo wp_kses_post($this->customNav()); ?>
     1222                    <?php $this->customNav(); ?>
    6291223
    6301224                </div>
     
    11121706   
    11131707
    1114             echo wp_kses_post($this->header());
     1708            $this->header();
    11151709
    11161710            $this->display_import_progress_bar();
     
    11361730                <div class="custom-dashboard">
    11371731
    1138                     <?php echo wp_kses_post($this->customNav()); ?>
    1139 
    1140                     <?php echo wp_kses_post($this->customSections()); ?>
     1732                    <?php $this->customNav(); ?>
     1733
     1734                    <?php $this->customSections(); ?>
    11411735
    11421736                </div>
     
    12141808   
    12151809
    1216                 $api_response = wp_remote_get($api_url, $this->api_headers);
     1810                $this->holnix_log('Enviando request holnix_display_table', [
     1811                    'page' => $this->page,
     1812                    'endpoint' => $endpoint,
     1813                    'api_url' => $api_url,
     1814                    'params' => $input_values,
     1815                ]);
     1816
     1817                $request = $this->holnix_api_request('GET', $api_url, [], [
     1818                    'context' => 'holnix_display_table',
     1819                    'page' => $this->page,
     1820                    'endpoint' => $endpoint,
     1821                    'params' => $input_values,
     1822                ]);
     1823                $api_response = $request['response'];
     1824                $final_url = $request['final_url'];
    12171825
    12181826                if (is_wp_error($api_response) || in_array(wp_remote_retrieve_response_code($api_response), [401, 404])) {
    1219 
    1220                     echo '<div class="error"><p>No se encontraron libros con los parámetros seleccionados o hubo un error de conexión.</p></div>';
     1827                    $error_message = 'No se encontraron libros con los parámetros seleccionados o hubo un error de conexión.';
     1828                    if (is_wp_error($api_response)) {
     1829                        $error_message = 'Error de conexión con la API: ' . $api_response->get_error_message();
     1830                    } elseif (wp_remote_retrieve_response_code($api_response) === 401) {
     1831                        $error_message = 'Error de autenticación con la API (401). Verifica el token configurado.';
     1832                    } elseif (wp_remote_retrieve_response_code($api_response) === 404) {
     1833                        $error_message = 'El endpoint de Holnix no fue encontrado (404).';
     1834                    }
     1835
     1836                    $this->holnix_log('Error en holnix_display_table', [
     1837                        'page' => $this->page,
     1838                        'endpoint' => $endpoint,
     1839                        'api_url' => $api_url,
     1840                        'final_url' => $final_url,
     1841                        'params' => $input_values,
     1842                        'user_message' => $error_message,
     1843                        'response' => $this->get_response_summary($api_response),
     1844                    ]);
     1845
     1846                    echo '<div class="error"><p>' . esc_html($error_message) . '</p></div>';
    12211847
    12221848                    return;
     
    12241850                }
    12251851
    1226                 $api_response = json_decode(wp_remote_retrieve_body($api_response), true);
     1852                $api_response_body = wp_remote_retrieve_body($api_response);
     1853                $api_response = json_decode($api_response_body, true);
    12271854
    12281855                if (!$api_response || !is_array($api_response)) {
     1856                    $this->holnix_log('Respuesta no válida en holnix_display_table', [
     1857                        'page' => $this->page,
     1858                        'endpoint' => $endpoint,
     1859                        'api_url' => $api_url,
     1860                        'final_url' => $final_url,
     1861                        'params' => $input_values,
     1862                        'body_excerpt' => $this->get_log_excerpt($api_response_body),
     1863                    ]);
    12291864
    12301865                    echo '<div class="error"><p>No se encontraron libros con los parámetros seleccionados.</p></div>';
     
    12571892
    12581893                }
     1894
     1895                $this->holnix_log('Respuesta OK en holnix_display_table', [
     1896                    'page' => $this->page,
     1897                    'endpoint' => $endpoint,
     1898                    'api_url' => $api_url,
     1899                    'final_url' => $final_url,
     1900                    'items_count' => count($api_response),
     1901                ]);
    12591902
    12601903                echo "</tbody></table><div class='d-flex justify-content-between align-items-center'><span id='results-count-footer'>" . count($api_response) . " resultados</span><p class='submit'><button id='holnix-import-button-footer' class='button-import " . esc_attr($disabled) . "' " . esc_attr($disabled) . ">Importar</button>" . ($disabled ? "<br><br>Botón deshabilitado mientras se procesa la cola actual." : "") . "</p><div></div></div></div>";
     
    12761919        check_admin_referer('holnix_update_product');
    12771920        $product = wc_get_product($product_id);
    1278         $response = wp_remote_get($this->api_url . "/isbn/" . $product->sku, $this->api_headers);
     1921        $update_url = $this->api_url . "/isbn/" . $product->sku;
     1922        $request = $this->holnix_api_request('GET', $update_url, [], [
     1923            'context' => 'holnix_update_page',
     1924            'product_id' => $product_id,
     1925            'product_sku' => isset($product->sku) ? $product->sku : '',
     1926        ]);
     1927        $response = $request['response'];
     1928        $final_url = $request['final_url'];
    12791929
    12801930        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
     1931            $this->holnix_log('Error en holnix_update_page', [
     1932                'product_id' => $product_id,
     1933                'product_sku' => isset($product->sku) ? $product->sku : '',
     1934                'url' => $update_url,
     1935                'final_url' => $final_url,
     1936                'response' => $this->get_response_summary($response),
     1937            ]);
    12811938            set_transient('holnix_update_message', ['type' => 'error', 'text' => 'No se pudo actualizar. El ISBN no existe en Holnix o hubo un error de conexión.'], 30);
    12821939        } else {
     
    13081965    {
    13091966        add_menu_page('Holnix', 'Holnix', 'manage_options', 'holnix_catalog', [$this, 'holnix_catalog_page'], plugins_url('assets/images/holnix_menu_item.png', __FILE__));
    1310         add_submenu_page(null, 'Holnix Settings', 'Settings', 'manage_options', 'holnix_settings', [$this, 'holnix_settings_page']);
    1311         add_submenu_page(null, 'News', 'News', 'manage_options', 'holnix_news', fn() => $this->holnix_display_table('/news-v2', ['date' => false, 'maxDate' => false]));
    1312         add_submenu_page(null, 'ISBN', 'ISBN', 'manage_options', 'holnix_isbn', fn() => $this->holnix_display_table('/isbns-v2/{isbns}', ['isbns' => true]));
    1313         add_submenu_page(null, 'Imprint', 'Imprint', 'manage_options', 'holnix_imprint', fn() => $this->holnix_display_table('/imprint-v2/{imprint}', ['imprint' => true, 'language' => false]));
    1314         add_submenu_page(null, 'Update', 'Update', 'manage_options', 'holnix_update', [$this, 'holnix_update_page']);
     1967        add_submenu_page('holnix_catalog', 'Holnix Settings', 'Settings', 'manage_options', 'holnix_settings', [$this, 'holnix_settings_page']);
     1968        add_submenu_page('holnix_catalog', 'News', 'News', 'manage_options', 'holnix_news', fn() => $this->holnix_display_table('/news-v2', ['date' => false, 'maxDate' => false]));
     1969        add_submenu_page('holnix_catalog', 'ISBN', 'ISBN', 'manage_options', 'holnix_isbn', fn() => $this->holnix_display_table('/isbns-v2/{isbns}', ['isbns' => true]));
     1970        add_submenu_page('holnix_catalog', 'Imprint', 'Imprint', 'manage_options', 'holnix_imprint', fn() => $this->holnix_display_table('/imprint-v2/{imprint}', ['imprint' => true, 'language' => false]));
     1971        add_submenu_page('holnix_catalog', 'Update', 'Update', 'manage_options', 'holnix_update', [$this, 'holnix_update_page']);
    13151972    }
    13161973
  • holnix/trunk/admin/css/holnix.css

    r3421769 r3460823  
    143143}
    144144
    145 .body-holnix input[type=checkbox]:checked::before{
     145/* .body-holnix input[type=checkbox]:checked::before{
    146146    content:"\f00c";
    147147    font-family: "Font Awesome 6 Free";
     148    font-weight: 900;
    148149    color: black;
    149150    line-height:1;
    150151    width:100%;
    151152    margin:0;
    152 }
     153} */
    153154
    154155.body-holnix input[type=checkbox]:checked:disabled{
  • holnix/trunk/admin/import.php

    r3421769 r3460823  
    1010class Holnix_Import
    1111{
     12    /**
     13     * Indica si el logging de diagnóstico está habilitado.
     14     *
     15     * @return bool
     16     */
     17    private function is_holnix_logging_enabled()
     18    {
     19        if (defined('HOLNIX_DEBUG_LOG')) {
     20            return (bool) HOLNIX_DEBUG_LOG;
     21        }
     22
     23        if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
     24            return true;
     25        }
     26
     27        if (defined('WP_DEBUG') && WP_DEBUG) {
     28            return true;
     29        }
     30
     31        return true;
     32    }
     33
     34    /**
     35     * Obtiene un extracto seguro para logging.
     36     *
     37     * @param mixed $value Valor a resumir.
     38     * @param int $max_length Longitud máxima.
     39     * @return string
     40     */
     41    private function get_log_excerpt($value, $max_length = 1200)
     42    {
     43        if (!is_string($value)) {
     44            if (is_scalar($value)) {
     45                $value = (string) $value;
     46            } else {
     47                $value = wp_json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
     48            }
     49        }
     50
     51        if (!is_string($value)) {
     52            return '';
     53        }
     54
     55        if (strlen($value) > $max_length) {
     56            return substr($value, 0, $max_length) . '...';
     57        }
     58
     59        return $value;
     60    }
     61
     62    /**
     63     * Registra mensajes de diagnóstico del importador en debug.log.
     64     *
     65     * @param string $message Mensaje principal.
     66     * @param array $context Contexto adicional serializable.
     67     * @return void
     68     */
     69    private function holnix_log($message, $context = [])
     70    {
     71        if (!$this->is_holnix_logging_enabled()) {
     72            return;
     73        }
     74
     75        $line = '[Holnix][Import] ' . $message;
     76        if (!empty($context)) {
     77            $json = wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
     78            if (is_string($json) && $json !== '') {
     79                $line .= ' | ' . $json;
     80            }
     81        }
     82
     83        error_log($line);
     84    }
    1285
    1386    /**
     
    51124
    52125        if (!empty($actions_in_progress)) {
     126            $this->holnix_log('Importación bloqueada por cola en progreso', [
     127                'pending_actions_detected' => count($actions_in_progress),
     128            ]);
    53129            return new WP_Error('import_in_progress', 'Por favor, espera a que el proceso de la cola actual finalice para poder realizar una nueva importación.');
    54130        } elseif ($items) {
     
    56132            // Siempre se procesa en segundo plano para mejorar la experiencia de usuario.
    57133            $chunks = array_chunk($items, $chunk_count);
     134            $this->holnix_log('Programando importación', [
     135                'items_count' => count($items),
     136                'chunk_count' => count($chunks),
     137                'chunk_size' => $chunk_count,
     138            ]);
    58139            update_option('holnix_import_total_chunks', count($chunks));
    59140            update_option('holnix_import_start_time', time());
     
    70151                as_schedule_single_action(time(), 'update_related_products', [$chunk_key]);
    71152            }
     153
     154            $this->holnix_log('Importación programada correctamente', [
     155                'scheduled_chunks' => count($chunk_keys),
     156            ]);
    72157        } else {
     158            $this->holnix_log('schedule_import sin productos');
    73159            return new WP_Error('no_products', 'Sin productos selecionados para importar.');
    74160        }
     
    86172        if ($chunk_key) {
    87173            $chunk_data = get_option($chunk_key);
     174            if (false === $chunk_data) {
     175                $this->holnix_log('Chunk no encontrado en options', ['chunk_key' => $chunk_key]);
     176                return;
     177            }
    88178            $items = unserialize($chunk_data);
    89179        }
     180
     181        if (!is_array($items) || empty($items)) {
     182            $this->holnix_log('process_chunk sin items válidos', ['chunk_key' => $chunk_key]);
     183            return;
     184        }
     185
     186        $this->holnix_log('Inicio process_chunk', [
     187            'chunk_key' => $chunk_key,
     188            'items_count' => count($items),
     189        ]);
    90190
    91191        $currency = str_replace("lbs", "lb", get_woocommerce_currency());
     
    97197        $translations = get_transient('holnix_api_translations'); // Intentar obtener de caché
    98198        if (false === $translations) {
    99             $response = wp_remote_get($admin_base->api_url . '/translations', $admin_base->api_headers);
     199            $this->holnix_log('Solicitando traducciones', [
     200                'url' => $admin_base->api_url . '/translations',
     201                'chunk_key' => $chunk_key,
     202            ]);
     203            $request = $admin_base->holnix_api_request('GET', $admin_base->api_url . '/translations', [], [
     204                'context' => 'import_translations',
     205                'chunk_key' => $chunk_key,
     206            ]);
     207            $response = $request['response'];
     208            $final_url = $request['final_url'];
    100209            if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
     210                $this->holnix_log('Error obteniendo traducciones', [
     211                    'url' => $admin_base->api_url . '/translations',
     212                    'final_url' => $final_url,
     213                    'chunk_key' => $chunk_key,
     214                    'wp_error' => is_wp_error($response) ? $response->get_error_message() : '',
     215                    'status_code' => is_wp_error($response) ? null : wp_remote_retrieve_response_code($response),
     216                    'body' => is_wp_error($response) ? '' : $this->get_log_excerpt(wp_remote_retrieve_body($response)),
     217                ]);
    101218                return;
    102219            }
  • holnix/trunk/admin/js/holnix-selection.js

    r3421827 r3460823  
    33    const storeName = 'selectedItems';
    44    let db;
     5    let useMemoryFallback = false;
     6    const memoryStore = new Map();
     7
     8    function toErrorMessage(error) {
     9        if (!error) {
     10            return 'Error desconocido';
     11        }
     12
     13        if (typeof error === 'string') {
     14            return error;
     15        }
     16
     17        if (error.message) {
     18            return error.message;
     19        }
     20
     21        if (error.name) {
     22            return error.name;
     23        }
     24
     25        return String(error);
     26    }
    527
    628    async function init() {
     
    1133            }
    1234
    13             const request = indexedDB.open(dbName, 1);
     35            if (useMemoryFallback) {
     36                resolve(null);
     37                return;
     38            }
     39
     40            if (typeof window === 'undefined' || !window.indexedDB) {
     41                useMemoryFallback = true;
     42                console.warn('IndexedDB no disponible. Se usará almacenamiento temporal en memoria.');
     43                resolve(null);
     44                return;
     45            }
     46
     47            const request = window.indexedDB.open(dbName, 1);
    1448
    1549            request.onupgradeneeded = (event) => {
     
    2660
    2761            request.onerror = (event) => {
    28                 console.error('Error al abrir IndexedDB:', event.target.errorCode);
    29                 reject(event.target.errorCode);
     62                const error = event && event.target ? (event.target.error || event.target.errorCode) : null;
     63                useMemoryFallback = true;
     64                console.error('Error al abrir IndexedDB, fallback a memoria:', toErrorMessage(error));
     65                resolve(null);
    3066            };
    3167        });
     
    3470    async function addItem(item) {
    3571        const db = await init();
     72        if (useMemoryFallback || !db) {
     73            if (!item || !item.isbn) {
     74                return;
     75            }
     76            memoryStore.set(item.isbn, item);
     77            return;
     78        }
     79
    3680        return new Promise((resolve, reject) => {
    3781            const transaction = db.transaction([storeName], 'readwrite');
     
    4589    async function removeItem(isbn) {
    4690        const db = await init();
     91        if (useMemoryFallback || !db) {
     92            memoryStore.delete(isbn);
     93            return;
     94        }
     95
    4796        return new Promise((resolve, reject) => {
    4897            const transaction = db.transaction([storeName], 'readwrite');
     
    56105    async function getAllItems() {
    57106        const db = await init();
     107        if (useMemoryFallback || !db) {
     108            return Array.from(memoryStore.values());
     109        }
     110
    58111        return new Promise((resolve, reject) => {
    59112            const transaction = db.transaction([storeName], 'readonly');
     
    67120    async function clearAllItems() {
    68121        const db = await init();
     122        if (useMemoryFallback || !db) {
     123            memoryStore.clear();
     124            return;
     125        }
     126
    69127        return new Promise((resolve, reject) => {
    70128            const transaction = db.transaction([storeName], 'readwrite');
     
    171229            initializeTableInteractions();
    172230        }
     231    }).catch((error) => {
     232        console.error('Fallo al inicializar almacenamiento local:', error);
     233        updateSelectedCount();
     234        if (!document.getElementById('holnix-imprint-form')) {
     235            initializeTableInteractions();
     236        }
    173237    });
    174238
     
    208272        const allSelectedItems = await HolnixDB.getAllItems();
    209273        const isbns = allSelectedItems.map(item => item.data);
     274
     275        if (!isbns.length) {
     276            if (spinner) spinner.style.display = 'none';
     277            alert('Selecciona al menos un libro para importar.');
     278            return;
     279        }
    210280
    211281        HolnixDB.clearAllItems();
     
    220290            url: holnix_ajax_object.ajax_url,
    221291            type: 'POST',
     292            dataType: 'json',
     293            timeout: 120000,
    222294            data: {
    223295                action: 'holnix_start_full_import',
     
    227299            success: function (response) {
    228300                if (spinner) spinner.style.display = 'none';
     301                if (!response || typeof response.success === 'undefined') {
     302                    alert('Respuesta inválida del servidor. Revisa la consola para más detalle.');
     303                    console.error('Respuesta inesperada en holnix_start_full_import:', response);
     304                    return;
     305                }
     306
    229307                if (response.success) {
    230308                    const importedMessage = document.getElementById('imported-message');
     
    233311                    }
    234312                } else {
    235                     alert('Error iniciando la importación: ' + response.data.message);
     313                    const message = response.data && response.data.message ? response.data.message : 'Error desconocido.';
     314                    alert('Error iniciando la importación: ' + message);
    236315                }
    237316            },
     
    340419            url: ajaxurl,
    341420            type: 'POST',
     421            dataType: 'json',
     422            timeout: 120000,
    342423            data: {
    343424                action: 'holnix_fetch_imprint_batch',
     
    348429            },
    349430            success: function (response) {
     431                if (!response || typeof response.success === 'undefined') {
     432                    progressStatus.textContent = 'Respuesta inválida del servidor.';
     433                    spinner.style.display = 'none';
     434                    console.error("Respuesta inesperada en holnix_fetch_imprint_batch:", response);
     435                    return;
     436                }
     437
    350438                if (response.success) {
    351439                    const { html, pagination, count } = response.data;
     
    396484                    }
    397485                } else {
    398                     progressStatus.textContent = 'Error: ' + response.data.message;
     486                    const message = response.data && response.data.message ? response.data.message : 'Error desconocido.';
     487                    progressStatus.textContent = 'Error: ' + message;
    399488                    spinner.style.display = 'none';
    400489                }
  • holnix/trunk/holnix.php

    r3421827 r3460823  
    44 * Plugin URI: https://www.holnix.com/
    55 * Description: Importa catálogos editoriales (metadata ONIX) en WooCommerce con un clic.
    6  * Version: 1.1.1
     6 * Version: 1.1.3
    77 * Author: Iberpixel
    88 * Author URI: https://www.iberpixel.com/
     
    1212 */
    1313
    14 const HOLNIX_VERSION = '1.1.0';
     14const HOLNIX_VERSION = '1.1.3';
    1515
    1616// Exit if accessed directly
     
    2121// Check if WooCommerce is active
    2222if (!in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
    23     add_action('admin_notices', function() {
     23    add_action('admin_notices', function () {
    2424        echo '<div class="error"><p>Holnix requires WooCommerce to be installed and active.</p></div>';
    2525    });
     
    3131
    3232// Initialize the plugin
    33 add_action('plugins_loaded', function() {
     33add_action('plugins_loaded', function () {
    3434    new Holnix_Admin();
    3535});
    3636
    37 add_action('admin_notices', function() {
     37add_action('admin_notices', function () {
    3838    $message_data = get_transient('holnix_update_message');
    3939    if ($message_data) {
     
    9191// Inline JS
    9292$holnix_js = "
    93             document.addEventListener('DOMContentLoaded', function () {
    94                 const holnixModal = document.getElementById('holnix-modal');
    95                 const holnixBgModal= document.getElementById('holnix-bg-modal');
    96                 document.getElementById('holnix-open-modal').addEventListener('click', function () {
     93    document.addEventListener('DOMContentLoaded', function () {
     94        const holnixModal = document.getElementById('holnix-modal');
     95        const holnixBgModal = document.getElementById('holnix-bg-modal');
     96        const openModalBtn = document.getElementById('holnix-open-modal');
     97        const closeModalBtn = document.getElementById('holnix-close-modal');
     98
     99        // Solo ejecutar si el botón de abrir existe (es decir, estamos en la ficha de producto)
     100        if (openModalBtn) {
     101            openModalBtn.addEventListener('click', function () {
     102                if (holnixModal && holnixBgModal) {
    97103                    holnixModal.style.display = 'block';
    98104                    holnixBgModal.style.display = 'block';
    99                 });
    100                 document.getElementById('holnix-close-modal').addEventListener('click', function () {
     105                }
     106            });
     107        }
     108
     109        if (closeModalBtn) {
     110            closeModalBtn.addEventListener('click', function () {
     111                if (holnixModal && holnixBgModal) {
    101112                    holnixModal.style.display = 'none';
    102113                    holnixBgModal.style.display = 'none';
    103                 });
    104                 holnixBgModal.addEventListener('click', function () {
     114                }
     115            });
     116        }
     117
     118        if (holnixBgModal) {
     119            holnixBgModal.addEventListener('click', function () {
     120                if (holnixModal) {
    105121                    holnixModal.style.display = 'none';
    106122                    holnixBgModal.style.display = 'none';
    107                 });
     123                }
    108124            });
    109             ";
     125        }
     126    });
     127";
    110128
    111129// Enqueue WooCommerce admin CSS as a handle for inline style
    112 add_action('admin_enqueue_scripts', function() use ($holnix_css, $holnix_js) {
     130add_action('admin_enqueue_scripts', function () use ($holnix_css, $holnix_js) {
    113131    wp_register_style('holnix-admin-style', false, array(), HOLNIX_VERSION);
    114132    wp_enqueue_style('holnix-admin-style');
     
    121139
    122140
    123 add_action('post_submitbox_start', function(){
     141add_action('post_submitbox_start', function () {
    124142    global $post;
    125143
    126144    if ($post && 'product' === $post->post_type) {
    127145        $product_id = $post->ID;
    128         $base_url = admin_url('admin.php?page=holnix_update&product_id=' ) . $product_id;
     146        $base_url = admin_url('admin.php?page=holnix_update&product_id=') . $product_id;
    129147        $custom_url = wp_nonce_url($base_url, 'holnix_update_product');
    130148
  • holnix/trunk/readme.txt

    r3421827 r3460823  
    44Requires at least: 5.8
    55Tested up to: 6.8
    6 Stable tag: 1.1.1
     6Stable tag: 1.1.3
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    2626Aviso legal: https://www.holnix.com/aviso-legal/
    2727
    28 == Changelog =========
     28== Changelog ==
    2929= 1.0.2 =
    3030* Ajustes de optimización y seguridad
     
    3434* [Mejora] En la vista de libros y carga de libros.
    3535* [Corrección] Solucionado un error que afectaba a editoriales con grandes volúmenes de libros.
     36= 1.1.3 =
     37* [Mejora] Importación y consulta de libros más estable y confiable.
     38* [Corrección] Se solucionaron incidencias en la búsqueda e importación por ISBN para resultados más precisos.
     39* [Mejora] Experiencia de uso optimizada en el panel de administración al seleccionar e importar libros.
     40* [Mejora] Ajustes generales de estabilidad para un funcionamiento más sólido en producción.
Note: See TracChangeset for help on using the changeset viewer.