Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/quant/assets/build/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '8039e1392598e54e4aad');
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'd001e9dadb9dbbba31c4');
16 changes: 8 additions & 8 deletions plugins/quant/assets/build/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/quant/assets/src/components/Progress/Progress.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export function Progress( {
) }
{ state !== 'active' && failedRun > 0 && (
<Button variant="secondary" onClick={ handleRetry }>
{ __( 'Retry Failed', 'quant' ) }
{ __( 'Retry failed', 'quant' ) }
</Button>
) }
{ state !== 'active' && (
Expand Down
7 changes: 4 additions & 3 deletions plugins/quant/assets/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState, useCallback } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { useNavigate } from 'react-router-dom';
import { visibleErrors } from './queueErrors';

function Stat( { label, value, tone } ) {
return (
Expand Down Expand Up @@ -343,14 +344,14 @@ export function Dashboard() {
</tr>
</thead>
<tbody>
{ errors.slice( 0, 5 ).map( ( e, i ) => (
{ visibleErrors( errors ).map( ( e, i ) => (
<tr key={ i }>
<td>{ e.type }</td>
<td>
<code>{ e.object_id }</code>
<code>{ e.objectId }</code>
</td>
<td style={ { color: '#721c24' } }>
{ e.error_message }
{ e.message }
</td>
</tr>
) ) }
Expand Down
24 changes: 12 additions & 12 deletions plugins/quant/assets/src/pages/Diagnostics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { errorReportLines } from './queueErrors';

export function Diagnostics() {
const [ diagnostics, setDiagnostics ] = useState( null );
Expand Down Expand Up @@ -82,6 +83,13 @@ export function Diagnostics() {
data.environment?.php_max_execution_time || 'Unknown'
}`,
`Multisite: ${ data.environment?.is_multisite ? 'Yes' : 'No' }`,
`WP-Cron Disabled: ${
data.environment?.cron_disabled ? 'Yes' : 'No'
}`,
`Background Processing: ${
data.environment?.background_processing ? 'Yes' : 'No'
}`,
`Last Queue Run: ${ data.environment?.last_queue_run || 'Never' }`,
'',
'## Plugin Configuration',
`Mode: ${ data.plugin?.mode || 'Unknown' }`,
Expand All @@ -93,6 +101,9 @@ export function Diagnostics() {
'## API Connection',
`Connected: ${ data.api?.connected ? 'Yes' : 'No' }`,
data.api?.error ? `Error: ${ data.api.error }` : '',
data.api?.error_detail
? `Error detail: ${ data.api.error_detail }`
: null,
'',
'## Database',
`V2 Table Exists: ${
Expand Down Expand Up @@ -121,18 +132,7 @@ export function Diagnostics() {
'',
];

if ( data.recent_errors && data.recent_errors.length > 0 ) {
lines.push( '## Recent Errors' );
data.recent_errors.forEach( ( err, i ) => {
lines.push(
`${ i + 1 }. [${ err.type }:${ err.object_id }] ${
err.error_message
}`
);
lines.push( ` At: ${ err.processed_at }` );
} );
lines.push( '' );
}
lines.push( ...errorReportLines( data.recent_errors ) );

lines.push( '='.repeat( 60 ) );
lines.push( 'End of Report' );
Expand Down
52 changes: 52 additions & 0 deletions plugins/quant/assets/src/pages/queueErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Queue failures as the admin shows them.
*
* Each failure from /quant/v1/queue/errors and the diagnostics endpoint has
* a short `error_message` for editors and, for a transport or server
* failure, the raw `error_detail` (for example "cURL error 28: ...").
* The cards show only the message. The detail goes only into the
* diagnostics report, for support.
*/

/**
* The rows the queue card shows.
*
* @param {Array<Object>} errors Failures from the REST API.
* @param {number} limit The most rows to show.
* @return {Array<{type: string, objectId: string, message: string}>} Rows without the raw detail.
*/
export function visibleErrors( errors, limit = 5 ) {
return ( errors || [] ).slice( 0, limit ).map( ( err ) => ( {
type: err.type,
objectId: err.object_id,
message: err.error_message,
} ) );
}

/**
* The "Recent Errors" section of the diagnostics report.
*
* @param {Array<Object>} errors Failures from the diagnostics endpoint.
* @return {Array<string>} Report lines; empty when there are no failures.
*/
export function errorReportLines( errors ) {
if ( ! errors || errors.length === 0 ) {
return [];
}

const lines = [ '## Recent Errors' ];
errors.forEach( ( err, i ) => {
lines.push(
`${ i + 1 }. [${ err.type }:${ err.object_id }] ${
err.error_message
}`
);
if ( err.error_detail ) {
lines.push( ` Detail: ${ err.error_detail }` );
}
lines.push( ` At: ${ err.processed_at }` );
} );
lines.push( '' );

return lines;
}
65 changes: 65 additions & 0 deletions plugins/quant/assets/src/pages/test/queueErrors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { visibleErrors, errorReportLines } from '../queueErrors';

const timeout = {
type: 'post',
object_id: '12',
error_message:
'API error publishing /about/: Quant did not answer in time. The plugin will try again.',
error_detail:
'http_request_failed: cURL error 28: Operation timed out after 30001 milliseconds with 0 bytes received (3 attempts)',
processed_at: '2026-11-15 01:02:03',
};

const notFound = {
type: 'route',
object_id: '/gone',
error_message: 'HTTP 404 response for /gone',
error_detail: null,
processed_at: '2026-11-15 01:00:00',
};

describe( 'visibleErrors', () => {
it( 'shows the short message and never the raw detail', () => {
const rows = visibleErrors( [ timeout ] );

expect( rows ).toEqual( [
{
type: 'post',
objectId: '12',
message:
'API error publishing /about/: Quant did not answer in time. The plugin will try again.',
},
] );
expect( JSON.stringify( rows ) ).not.toContain( 'cURL' );
} );

it( 'limits the rows', () => {
const many = Array.from( { length: 8 }, () => notFound );

expect( visibleErrors( many ) ).toHaveLength( 5 );
expect( visibleErrors( many, 2 ) ).toHaveLength( 2 );
} );

it( 'accepts a missing list', () => {
expect( visibleErrors( undefined ) ).toEqual( [] );
} );
} );

describe( 'errorReportLines', () => {
it( 'keeps the raw detail in the diagnostics report', () => {
expect( errorReportLines( [ timeout, notFound ] ) ).toEqual( [
'## Recent Errors',
'1. [post:12] API error publishing /about/: Quant did not answer in time. The plugin will try again.',
' Detail: http_request_failed: cURL error 28: Operation timed out after 30001 milliseconds with 0 bytes received (3 attempts)',
' At: 2026-11-15 01:02:03',
'2. [route:/gone] HTTP 404 response for /gone',
' At: 2026-11-15 01:00:00',
'',
] );
} );

it( 'has no section when nothing failed', () => {
expect( errorReportLines( [] ) ).toEqual( [] );
expect( errorReportLines( undefined ) ).toEqual( [] );
} );
} );
26 changes: 26 additions & 0 deletions plugins/quant/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ Version 2.0 requires PHP 8.2. Sites on older PHP are not offered the update and

Set these to configure the plugin without the admin UI (for example on Quant Cloud): `QUANT_API_ENDPOINT`, `QUANT_CUSTOMER`, `QUANT_PROJECT`, `QUANT_TOKEN`, `QUANT_WEBSERVER_URL`, `QUANT_WEBSERVER_HOST`, `QUANT_ENABLED`, `QUANT_PURGE_ON_SAVE`, `QUANT_DISABLE_TLS_VERIFY`, `QUANT_HTTP_REQUEST_TIMEOUT`, `QUANT_MODE`. The same names work as PHP constants in `wp-config.php`. Overridden fields are read-only in the UI.

`QUANT_HTTP_REQUEST_TIMEOUT` is the time in seconds that the plugin waits for one answer from the Quant API (default 30, minimum 5). It is the same value as "HTTP request timeout" on the Settings page. File uploads, and pages larger than 512 KB, always get at least 120 seconds. The connection itself must open within 10 seconds (the WordPress HTTP API default). Increase the value if a slow network makes pushes fail with "Quant did not answer in time":

`
define('QUANT_HTTP_REQUEST_TIMEOUT', 60);
`

== Styling the search page ==

The search page (the page the plugin creates, the "Quant Search" block and the `[quant_search]` shortcode) uses the theme's fonts and text colour. Result titles are in the text colour and are underlined on hover and focus. Lines, count badges, tags and secondary text are mixes of the text colour, so the page also works on a dark theme. The plugin's own rules apply only inside `.quant-search`; the bundled InstantSearch.js theme styles the `ais-` widget classes.
Expand Down Expand Up @@ -130,12 +136,32 @@ The plugin renders pages by requesting them from the web server. Set **Webserver

Enable search for the project in the Quant dashboard, then open **QuantCDN > Search**, turn on search records and create a search page. Place the "Quant Search" block or `[quant_search]` shortcode on any page for more search pages, and the "Quant Search Autocomplete" block or `[quant_search_autocomplete]` shortcode in a header or sidebar.

= A push failed with "Quant did not answer in time" =

The Quant API did not answer within the HTTP request timeout, or a gateway between your server and Quant was busy (HTTP 502, 503 or 504).

* A page or file push that fails fast (a gateway error, or a refused or reset connection) is sent again up to two times in the same run, after about 1 and 3 seconds.
* A request that timed out is sent again only when another attempt of the same length still fits in 45 seconds. With the default 30 second timeout it is not sent again in the same run; with a 10 second timeout it can be sent again twice.
* All retries in one run share the same 45 seconds. When one push used both retries and still failed, the other pushes in the same run are not retried until a push works again.
* Unpublish, purge, redirects and search records are not sent again in the same run.

If the push still fails, the job stays in the queue as failed. While the job has attempts left (three in total) and the queue runs in the background, the message ends with "The plugin will try again." and the background run tries it again, with a longer wait each time. The queue runs in the background when WP-Cron is enabled, or when `DISABLE_WP_CRON` is set and a system cron ran the queue in the last 15 minutes. When the job has used all attempts, or nothing runs the queue in the background, the message ends with "Select Retry failed on the dashboard.": select "Retry failed" on the QuantCDN dashboard (or in the push progress), or run `wp quant queue retry`. A failed connection test says "Test the connection again in a minute." The raw error text (for example the cURL error) is in the report from QuantCDN > Diagnostics > Download Report.

= Forms on a static site =

Follow the [Forms documentation](https://docs.quantcdn.io/docs/dashboard/forms). Contact Form 7 needs `define('WPCF7_LOAD_JS', false);`.

== Changelog ==

= 2.0.6 =
* Fixed: when the Quant API did not answer, the push showed raw transport text such as "cURL error 28: Operation timed out after 30001 milliseconds with 0 bytes received". The dashboard queue card and the push progress now show a short message, for example "Quant did not answer in time. The plugin will try again." The message says "Select Retry failed on the dashboard." instead when the job has used all attempts or nothing runs the queue in the background (WP-Cron disabled and no system cron run in the last 15 minutes), and a failed connection test says "Test the connection again in a minute." The raw text is kept apart and is in the diagnostics report and in `wp quant queue errors`.
* Fixed: one stalled request or gateway error (502, 503, 504) failed the page until the next background run, and the background run only happens when WP-Cron runs. A page or file push that fails fast is now sent again up to two times in the same run, after about 1 and 3 seconds. A request that timed out is sent again only when another attempt still fits in 45 seconds. All retries in one run share those 45 seconds, and after one push fails with both retries the others in the run are not retried.
* Other request failures (for example too many redirects) show "The request to Quant failed." and are not sent again in the same run.
* A page larger than 512 KB now gets the same 120 second minimum timeout as a file upload.
* Readme: `QUANT_HTTP_REQUEST_TIMEOUT` is documented.
* The queue table has a new column for the error detail. It is added automatically on the first request after the update; the schema version is recorded only after the column exists.
* Diagnostics: the report shows whether the queue runs in the background and when it last ran.

= 2.0.5 =
* Search page: a cleaner default style that still follows the theme. The checkboxes, counts, headings, tags, buttons and pagination have even spacing, every control has a visible focus outline, the "Clear refinements" button uses the theme text colour (it was pale blue), the result title is larger than its summary, and on a phone the facets move above the results. The InstantSearch widgets no longer turn the text navy.
* Search page: every size, space and colour is a CSS custom property (`--quant-search-gap`, `--quant-search-accent` and others). See "Styling the search page" in the readme.
Expand Down
15 changes: 14 additions & 1 deletion plugins/quant/src/Admin/DiagnosticsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ public function getDiagnostics(WP_REST_Request $request): array
{
global $wp_version, $wpdb;

$api = ['connected' => false, 'error' => null, 'search_enabled' => false];
$api = ['connected' => false, 'error' => null, 'error_detail' => null, 'search_enabled' => false];
if ($this->settings->hasValidCredentials()) {
$response = $this->api->ping();
$api = [
'connected' => $response->success,
'error' => $response->success ? null : $response->message,
'error_detail' => $response->success || $response->detail === '' ? null : $response->detail,
'search_enabled' => !empty($response->data['config']['search_enabled']),
];
}
Expand All @@ -119,6 +120,8 @@ public function getDiagnostics(WP_REST_Request $request): array
'php_max_execution_time' => ini_get('max_execution_time'),
'is_multisite' => is_multisite(),
'cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON,
'background_processing' => \Quant\Core\Scheduler::backgroundProcessingEnabled(),
'last_queue_run' => $this->lastQueueRun(),
'next_queue_run' => wp_next_scheduled('quant_process_queue') ?: null,
'permalink_structure' => get_option('permalink_structure') ?: 'plain',
],
Expand Down Expand Up @@ -166,4 +169,14 @@ public function getOnboardingStatus(WP_REST_Request $request): array
'has_settings' => $this->detector->hasV2Settings(),
];
}

/**
* When the scheduled processor last ran the queue, in UTC, or null.
*/
private function lastQueueRun(): ?string
{
$time = (int) get_option(\Quant\Core\Scheduler::OPTION_LAST_RUN, 0);

return $time > 0 ? gmdate('Y-m-d H:i:s', $time) . ' UTC' : null;
}
}
3 changes: 2 additions & 1 deletion plugins/quant/src/Admin/RestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Quant\Admin;

use Quant\Api\ContentApiInterface;
use Quant\Core\Scheduler;
use Quant\Core\Settings;
use Quant\Core\SiteTime;
use Quant\Redirects\RedirectProvider;
Expand Down Expand Up @@ -121,7 +122,7 @@ public function getStatus(WP_REST_Request $request): array
],
'next_cron' => $nextCron,
'next_cron_label' => $nextCron === null ? null : SiteTime::time($nextCron),
'cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON,
'cron_disabled' => !Scheduler::backgroundProcessingEnabled(),
];
}

Expand Down
21 changes: 18 additions & 3 deletions plugins/quant/src/Api/ApiResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,35 @@

class ApiResponse
{
/**
* @param string $message Short text that the admin can show to an editor.
* @param string $detail Raw transport or server text for diagnostics; never shown in the progress card.
* @param bool $transient True when Quant did not answer or answered 5xx: the same call can work later.
*/
public function __construct(
public readonly bool $success,
public readonly string $message = '',
public readonly array $data = [],
public readonly int $statusCode = 200
public readonly int $statusCode = 200,
public readonly string $detail = '',
public readonly bool $transient = false
) {}

public static function success(array $data = [], string $message = ''): self
{
return new self(true, $message, $data);
}

public static function error(string $message, int $statusCode = 400, array $data = []): self
public static function error(string $message, int $statusCode = 400, array $data = [], string $detail = '', bool $transient = false): self
{
return new self(false, $message, $data, $statusCode);
return new self(false, $message, $data, $statusCode, $detail, $transient);
}

/**
* The message with the raw detail in brackets, for administrators (WP-CLI, logs).
*/
public function messageWithDetail(): string
{
return $this->detail === '' ? $this->message : "{$this->message} ({$this->detail})";
}
}
Loading
Loading