PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 All 32 releases
← All changes | includes/modules/Preloader/PreloaderModule.php +111 -16 1.1.1 → 1.3.6 View file →
@@ -28,12 +28,12 @@
28 28 public const VERSION = '1.0.0';
29 29
30 30 public function ui_metadata(): array {
31 31 return array(
32 - 'label' => 'Preloader',
33 - 'tab_label' => 'Crawl Now', // its own tab on the Preloader page
32 + 'label' => __( 'Preloader', 'xspeed' ),
33 + 'tab_label' => __( 'Crawl Now', 'xspeed' ), // its own tab on the Preloader page
34 34 'icon' => 'Wand2',
35 - 'description' => 'Crawl the sitemap to warm cache so visitors never hit a cold MISS.',
35 + 'description' => __( 'Crawl the sitemap to warm cache so visitors never hit a cold MISS.', 'xspeed' ),
36 36 // Custom panel wraps the schema-driven settings with a
37 37 // Start/Stop control surface + a live status readout
38 38 // (queue depth, last URL, recent errors).
39 39 'custom_panel' => 'PreloaderHost',
@@ -39,15 +39,44 @@
39 39 'custom_panel' => 'PreloaderHost',
40 40 );
41 41 }
42 42
43 + /**
44 + * Surface a firewall refusing our warmer.
45 + *
46 + * A 403/406 on a warm used to reach the admin only as "HTTP 403" buried
47 + * in the activity log, so a site whose every warm was refused looked
48 + * simply idle — newly published posts were never warmed and nothing said
49 + * why. The page is fine; the fix is a server rule, so the notice names
50 + * the user-agent to allow. (#481)
51 + */
52 + public function ui_notices(): array {
53 + $block = Preloader::firewall_block();
54 + if ( null === $block ) {
55 + return array();
56 + }
57 +
58 + return array(
59 + array(
60 + 'tone' => 'warn',
61 + 'title' => __( 'Your server is blocking the xSpeed cache warmer', 'xspeed' ),
62 + 'body' => sprintf(
63 + /* translators: 1: HTTP status code, 2: the user-agent xSpeed sends. */
64 + __( 'A warm request was refused with HTTP %1$d, which is how a firewall answers when it judges a request by its user-agent. Pages are not being warmed, so the first visitor to each new page waits for an uncached render. Allow the user-agent "%2$s" in your server\'s bad-bot rules — on xCloud this is the 8G firewall — or change it with the xspeed_preloader_user_agent filter. This notice clears itself once a warm gets through.', 'xspeed' ),
65 + (int) $block['code'],
66 + (string) ( $block['user_agent'] ?? Preloader::user_agent() )
67 + ),
68 + ),
69 + );
70 + }
71 +
43 72 public function settings_schema(): array {
44 73 return array(
45 74 'enabled' => array(
46 75 'type' => 'bool',
47 76 'default' => false,
48 - 'label' => 'Enable Preloader',
49 - 'description' => 'When on, xSpeed crawls the sitemap on the schedule below and warms the page cache.',
77 + 'label' => __( 'Enable Preloader', 'xspeed' ),
78 + 'description' => __( 'When on, xSpeed crawls the sitemap on the schedule below and warms the page cache.', 'xspeed' ),
50 79 ),
51 80 'schedule' => array(
52 81 'type' => 'enum',
53 82 'default' => 'manual',
@@ -57,10 +86,11 @@
57 86 'hourly' => 'Hourly',
58 87 'daily' => 'Daily',
59 88 'weekly' => 'Weekly',
60 89 ),
61 - 'label' => 'Schedule',
62 - 'description' => 'How often to start a fresh crawl. Manual means you trigger it from the dashboard.',
90 + 'label' => __( 'Schedule', 'xspeed' ),
91 + 'description' => __( 'How often to start a fresh crawl. Manual means you trigger it from the dashboard.', 'xspeed' ),
92 + 'dependsOn' => array( 'field' => 'enabled' ),
63 93 ),
64 94 'batch_size' => array(
65 95 'type' => 'int',
66 96 'default' => 5,
@@ -65,28 +95,32 @@
65 95 'type' => 'int',
66 96 'default' => 5,
67 97 'min' => 1,
68 98 'max' => 50,
69 - 'label' => 'Batch Size',
70 - 'description' => 'URLs warmed per cron tick. Higher = faster crawl, more load on the origin.',
99 + 'label' => __( 'Batch Size', 'xspeed' ),
100 + 'description' => __( 'URLs warmed per cron tick. Higher = faster crawl, more load on the origin.', 'xspeed' ),
101 + 'dependsOn' => array( 'field' => 'enabled' ),
71 102 ),
72 103 'sitemap_url' => array(
73 104 'type' => 'string',
74 105 'default' => '',
75 - 'label' => 'Sitemap URL (optional)',
76 - 'description' => 'Override the auto-detected WordPress core sitemap (/wp-sitemap.xml). Leave blank for default.',
106 + 'label' => __( 'Sitemap URL (optional)', 'xspeed' ),
107 + 'description' => __( 'Override the auto-detected WordPress core sitemap (/wp-sitemap.xml). Leave blank for default.', 'xspeed' ),
108 + 'dependsOn' => array( 'field' => 'enabled' ),
77 109 ),
78 110 'warm_on_publish' => array(
79 111 'type' => 'bool',
80 112 'default' => true,
81 - 'label' => 'Warm new content immediately',
82 - 'description' => 'When a post or page is published, fetch it once so the first visitor sees a cache HIT, not a cold MISS.',
113 + 'label' => __( 'Warm new content immediately', 'xspeed' ),
114 + 'description' => __( 'When a post or page is published, fetch it once so the first visitor sees a cache HIT, not a cold MISS.', 'xspeed' ),
115 + 'dependsOn' => array( 'field' => 'enabled' ),
83 116 ),
84 117 'warm_on_comment' => array(
85 118 'type' => 'bool',
86 119 'default' => false,
87 - 'label' => 'Re-warm after comments',
88 - 'description' => 'Re-warm a page after a comment is posted (Cache purges the page on comment; this fetches it back into cache).',
120 + 'label' => __( 'Re-warm after comments', 'xspeed' ),
121 + 'description' => __( 'Re-warm a page after a comment is posted (Cache purges the page on comment; this fetches it back into cache).', 'xspeed' ),
122 + 'dependsOn' => array( 'field' => 'enabled' ),
89 123 ),
90 124 );
91 125 }
92 126
@@ -121,8 +155,9 @@
121 155 array(
122 156 'name' => 'xspeed preloader',
123 157 'callback' => array( $this, 'cli_handler' ),
124 158 'shortdesc' => 'Drive the cache preloader (start | stop | status).',
159 + 'ai_hint' => 'Cache warming: crawl the site so visitors hit a warm cache instead of paying for the first render. Use after a full purge, or when the first visitor to each page reports a slow load.',
125 160 'synopsis' => array(
126 161 array(
127 162 'type' => 'positional',
128 163 'name' => 'action',
@@ -134,8 +169,27 @@
134 169 );
135 170 }
136 171
137 172 public function boot(): void {
173 + /*
174 + * Deferred to `init`. This module reads its own settings to decide
175 + * what to hook, and reading settings builds settings_schema(), whose
176 + * labels are declared through __(). boot() runs on `plugins_loaded`,
177 + * before `after_setup_theme` — the point WordPress 6.7+ treats as the
178 + * earliest safe moment to translate — so doing that here fires
179 + * _load_textdomain_just_in_time on every request AND resolves the
180 + * labels against a domain that is not loaded yet.
181 + *
182 + * Everything below hooks actions that fire after `init`, so running
183 + * one hook later is equivalent.
184 + */
185 + add_action( 'init', array( $this, 'boot_on_init' ) );
186 + }
187 +
188 + /**
189 + * The real boot body — see boot() for why it runs on `init`.
190 + */
191 + public function boot_on_init(): void {
138 192 add_action( Preloader::CRON_HOOK, array( Preloader::class, 'tick' ) );
139 193 add_action( 'xspeed_preloader_recurring', array( Preloader::class, 'recurring_kickoff' ) );
140 194
141 195 // Apply schedule changes immediately whenever this module's
@@ -265,8 +319,30 @@
265 319 \WP_CLI::error( 'Preloader is disabled. Enable it via wp xspeed preloader set --enabled=1 first.' );
266 320 return;
267 321 }
268 322 $state = Preloader::start();
323 + // Don't print a green Success over a crawl that queued
324 + // nothing — that exit-0 was the whole complaint in #142.
325 + $sitemap_error = (string) ( $state['sitemap_error'] ?? '' );
326 + if ( 0 === (int) $state['total'] ) {
327 + \WP_CLI::error(
328 + '' !== $sitemap_error
329 + ? sprintf( 'Queued 0 URLs. %s', $sitemap_error )
330 + : 'Queued 0 URLs — nothing to warm. Check the sitemap URL and the cache exclusion rules.'
331 + );
332 + return;
333 + }
334 + if ( 'fallback' === ( $state['source'] ?? '' ) ) {
335 + \WP_CLI::warning( $sitemap_error );
336 + \WP_CLI::success(
337 + sprintf(
338 + 'Queued %d URL%s from the site content instead of the sitemap.',
339 + $state['total'],
340 + 1 === $state['total'] ? '' : 's'
341 + )
342 + );
343 + return;
344 + }
269 345 \WP_CLI::success( sprintf( 'Queued %d URL%s.', $state['total'], 1 === $state['total'] ? '' : 's' ) );
270 346 return;
271 347
272 348 case 'stop':
@@ -283,9 +359,28 @@
283 359 }
284 360 if ( ! empty( $state['errors'] ) ) {
285 361 \WP_CLI::log( 'Errors : ' . count( $state['errors'] ) );
286 362 foreach ( array_slice( $state['errors'], -5 ) as $e ) {
287 - \WP_CLI::log( ' ' . $e['url'] . ' — ' . $e['error'] );
363 + // Tolerate a bare string as well as the {url, error}
364 + // shape. A string entry fataled this command outright
365 + // ("Cannot access offset of type string on string"),
366 + // which also took MCP's get_preloader_status down with
367 + // it — an agent asking why a preload failed got a type
368 + // error instead of the reason. The writer is fixed, but
369 + // `status` is a diagnostic: it should survive whatever
370 + // it is handed rather than die reporting on it. (QA F1)
371 + if ( is_array( $e ) ) {
372 + $url = isset( $e['url'] ) ? (string) $e['url'] : '';
373 + $msg = isset( $e['error'] ) ? (string) $e['error'] : '';
374 + // The sitemap message already names the URL, so
375 + // prefixing it would print the URL twice on one line.
376 + $line = ( '' !== $url && false === strpos( $msg, $url ) )
377 + ? $url . ' — ' . $msg
378 + : $msg;
379 + } else {
380 + $line = (string) $e;
381 + }
382 + \WP_CLI::log( ' ' . $line );
288 383 }
289 384 }
290 385 return;
291 386