PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.5
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.5
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 1.1.6 All 31 releases
← All changes | includes/modules/Health/HealthModule.php +181 -7 1.0.71.3.5 View file →
@@ -48,10 +48,20 @@
48 48 * vs the optimal cache hit path.
49 49 */
50 50 public function boot(): void {
51 51 add_filter( 'site_status_tests', array( $this, 'register_site_status_tests' ) );
52 +
53 + // Out-of-band refresh of the Set-Cookie probe. Health::checks()
54 + // only ever reads the cached verdict, so the HTTP round-trip
55 + // happens here instead of inside a request the user waits on.
56 + add_action( \XSpeed\Cookie_Inspector::CRON_HOOK, array( $this, 'refresh_cookie_probe' ) );
52 57 }
53 58
59 + /** Cron callback: perform the real (blocking) probe off-request. */
60 + public function refresh_cookie_probe(): void {
61 + \XSpeed\Cookie_Inspector::probe( true );
62 + }
63 +
54 64 public function register_site_status_tests( array $tests ): array {
55 65 $tests['direct']['xspeed_static_rewrite'] = array(
56 66 'label' => __( 'xSpeed static-rewrite cache', 'xspeed' ),
57 67 'test' => array( $this, 'site_status_static_rewrite' ),
@@ -65,8 +75,52 @@
65 75 * embedded when nginx is detected. Skipped entirely when cache is
66 76 * disabled — no point telling the user to install a rewrite they
67 77 * haven't opted into.
68 78 */
79 + /**
80 + * Decide what the nginx static rewrite is actually doing.
81 + *
82 + * Extracted so the ordering is testable without a WordPress bootstrap,
83 + * and so Site Health and the dashboard Health panel cannot drift apart
84 + * again — the whole point of #480.
85 + *
86 + * Returns one of: 'active', 'mobile_separate', 'skipped_nonce',
87 + * 'unverified', 'required'.
88 + *
89 + * @param array<string, mixed> $probe probe_static_rewrite() result.
90 + * @param string $block_reason A known refusal, or ''.
91 + */
92 + public static function nginx_rewrite_verdict( array $probe, string $block_reason ): string {
93 + $is_active = (bool) ( $probe['active'] ?? false );
94 + $inconclusive = (bool) ( $probe['inconclusive'] ?? false );
95 +
96 + // A known refusal OUTRANKS the probe. probe_static_rewrite() writes
97 + // its own file under the static-cache dir and fetches that, which
98 + // succeeds whenever the server can serve a static file at all — even
99 + // when no real page is on the static path. It also outranks
100 + // "inconclusive", so a blocked rewrite whose probe merely failed to
101 + // complete is reported as the refusal it is. (FBS-83145)
102 + if ( '' !== $block_reason ) {
103 + if ( 'mobile_separate' === $block_reason ) {
104 + return 'mobile_separate';
105 + }
106 + if ( 'skipped_nonce' === $block_reason ) {
107 + return 'skipped_nonce';
108 + }
109 + return 'required';
110 + }
111 +
112 + if ( $is_active ) {
113 + return 'active';
114 + }
115 +
116 + // The probe never reached a verdict (blocked loopback, self-signed
117 + // cert, timeout, a CDN/WAF answering instead of the origin). That is
118 + // not evidence the config is wrong, and must not produce a
119 + // "paste this snippet" banner. (FBS-84012, #480)
120 + return $inconclusive ? 'unverified' : 'required';
121 + }
122 +
69 123 public function site_status_static_rewrite(): array {
70 124 $result = array(
71 125 'label' => __( 'xSpeed static-rewrite cache is active', 'xspeed' ),
72 126 'status' => 'good',
@@ -87,9 +141,23 @@
87 141 }
88 142
89 143 $server_type = \XSpeed\Server::type();
90 144 if ( \XSpeed\Server::APACHE === $server_type || \XSpeed\Server::LITESPEED === $server_type ) {
91 - if ( ! \XSpeed\Cache::rewrite_installed() ) {
145 + // Only call the block "missing" when it is genuinely absent by
146 + // accident. When static_rewrite_allowed() deliberately refused it,
147 + // "toggle Enable Cache off and on" cannot reinstall anything —
148 + // the same condition suppresses the write and auto_heal() strips
149 + // the block again on the next admin load. Explain the real cause.
150 + $block_reason = \XSpeed\Cache::static_rewrite_block_reason();
151 + if ( 'no_mod_headers' === $block_reason ) {
152 + $result['label'] = __( 'xSpeed is serving cache hits through PHP', 'xspeed' );
153 + $result['status'] = 'recommended';
154 + $result['description'] = '<p>' . esc_html__( 'Caching is working — hits are served by the xSpeed drop-in and tagged X-XSpeed-Cache: HIT (php). The faster .htaccess fast path is off because Apache\'s mod_headers module is not loaded, without which a static hit could not be tagged or counted. Enable mod_headers (a2enmod headers on Debian/Ubuntu, then restart Apache) to shave roughly 20-30ms off each cache hit.', 'xspeed' ) . '</p>';
155 + } elseif ( 'mobile_separate' === $block_reason ) {
156 + $result['label'] = __( 'xSpeed static rewrite is off (Separate Mobile Cache)', 'xspeed' );
157 + $result['status'] = 'recommended';
158 + $result['description'] = '<p>' . esc_html__( 'Separate Mobile Cache is on, so cache hits are served by the PHP drop-in to keep per-device HTML correct. Turn Separate Mobile Cache off if your site serves the same HTML to every device to regain the faster static path.', 'xspeed' ) . '</p>';
159 + } elseif ( \XSpeed\Server::APACHE === $server_type && ! \XSpeed\Cache::rewrite_installed() ) {
92 160 $result['label'] = __( 'xSpeed .htaccess rewrite block is missing', 'xspeed' );
93 161 $result['status'] = 'recommended';
94 162 $result['description'] = '<p>' . esc_html__( 'Without the static-rewrite block, cache hits go through the PHP drop-in (~85ms TTFB) instead of the web server (~5-15ms). Toggle Enable Cache off and on in xSpeed to reinstall the block.', 'xspeed' ) . '</p>';
95 163 }
@@ -96,8 +164,62 @@
96 164 return $result;
97 165 }
98 166
99 167 if ( \XSpeed\Server::NGINX === $server_type ) {
168 + // Ask the same question the dashboard Health panel asks, the same
169 + // way. This test used to return "config required" unconditionally,
170 + // so every correctly-configured nginx site — every xCloud site,
171 + // where the panel installs the block for you — was told to paste a
172 + // snippet it already had, and re-running the check never cleared
173 + // it. Worse, the dashboard said the opposite at the same moment.
174 + // Reuse probe_static_rewrite() + the refusal reasons so the two
175 + // surfaces cannot disagree. (#480)
176 + $probe = \XSpeed\Cache::probe_static_rewrite( true );
177 + $probe_reason = (string) ( $probe['reason'] ?? '' );
178 +
179 + $block_reason = \XSpeed\Cache::static_rewrite_block_reason();
180 + $skip = \XSpeed\Cache::last_static_skip();
181 + if ( '' === $block_reason && ! empty( $skip['reason'] ) ) {
182 + $block_reason = 'skipped_' . (string) $skip['reason'];
183 + }
184 +
185 + switch ( self::nginx_rewrite_verdict( $probe, $block_reason ) ) {
186 + case 'active':
187 + $result['label'] = __( 'xSpeed nginx static rewrite is active', 'xspeed' );
188 + $result['status'] = 'good';
189 + $result['description'] = '<p>' . esc_html__( 'nginx is serving cache hits directly — PHP is bypassed (~5-15ms TTFB). No action needed.', 'xspeed' ) . '</p>';
190 + return $result;
191 +
192 + case 'mobile_separate':
193 + $result['label'] = __( 'xSpeed static rewrite is off (Separate Mobile Cache)', 'xspeed' );
194 + $result['status'] = 'recommended';
195 + $result['description'] = '<p>' . esc_html__( 'Separate Mobile Cache is on, so cache hits are served by the PHP drop-in to keep per-device HTML correct. Turn Separate Mobile Cache off if your site serves the same HTML to every device to regain the faster static path.', 'xspeed' ) . '</p>';
196 + return $result;
197 +
198 + case 'skipped_nonce':
199 + $result['label'] = __( 'xSpeed is serving cache hits through PHP (pages contain nonces)', 'xspeed' );
200 + $result['status'] = 'recommended';
201 + $result['description'] = '<p>' . esc_html__( 'Your nginx config is correct, but pages are not reaching the static cache, so hits are served by PHP. They contain nonces, and a static file is served with no PHP — nothing could ever refresh them, so every anonymous form on the page would break once they expire. Keeping these pages on PHP is deliberate.', 'xspeed' ) . '</p>';
202 + return $result;
203 +
204 + case 'unverified':
205 + // The probe never reached a verdict (blocked loopback,
206 + // self-signed cert, timeout, a CDN/WAF answering instead of
207 + // the origin). Not evidence the config is wrong, so don't
208 + // say "required" and don't dump a snippet the user has
209 + // probably already pasted. (FBS-84012, and why #480 was filed.)
210 + $result['label'] = __( 'xSpeed could not verify the nginx static rewrite', 'xspeed' );
211 + $result['status'] = 'recommended';
212 + $result['description'] = '<p>' . esc_html(
213 + sprintf(
214 + /* translators: %s: the reason the probe could not complete. */
215 + __( 'The check itself did not complete, so this is not evidence that your config is wrong — if you have already pasted the snippet it may well be working. Reason: %s', 'xspeed' ),
216 + $probe_reason
217 + )
218 + ) . '</p>';
219 + return $result;
220 + }
221 +
100 222 $snippet = \XSpeed\Cache::nginx_snippet();
101 223 $result['label'] = __( 'xSpeed nginx server config required', 'xspeed' );
102 224 $result['status'] = 'recommended';
103 225 $result['description'] = '<p>' . esc_html__( 'xSpeed can\'t write nginx config from PHP. Paste this snippet into your site\'s server { } block, then reload nginx so cache hits serve without booting PHP:', 'xspeed' ) . '</p>'
@@ -116,15 +238,17 @@
116 238 }
117 239
118 240 public function ui_metadata(): array {
119 241 return array(
120 - 'label' => 'Health',
242 + 'label' => __( 'Health', 'xspeed' ),
121 243 'icon' => 'HeartPulse',
122 - 'description' => 'Diagnostics, hit ratio, and recent cache activity.',
123 - // Tells the React side to render HealthCard instead of
124 - // schema-driven settings (SETTINGS.md §6.2 allows custom
125 - // panels for non-settings surfaces).
126 - 'custom_panel' => 'HealthCard',
244 + 'description' => __( 'Diagnostics, hit ratio, and recent cache activity.', 'xspeed' ),
245 + // Health is the single host page for all Insights (FBS-83633):
246 + // a Recommendations action card + Cache / Visitors / PageSpeed
247 + // tabs. HealthPanel renders the Free cache diagnostics (the old
248 + // HealthCard) as the Cache tab and hosts the Pro insight panels
249 + // as the other tabs via ProSlot.
250 + 'custom_panel' => 'HealthPanel',
127 251 );
128 252 }
129 253
130 254 // No settings — explicit empty so Module::rest_routes() doesn't
@@ -148,11 +272,61 @@
148 272 array(
149 273 'name' => 'xspeed health',
150 274 'callback' => array( $this, 'cli_handler' ),
151 275 'shortdesc' => 'Print diagnostic checks + cache stats + recent activity.',
276 + 'ai_hint' => 'Full diagnostic sweep: what is misconfigured or degraded on this site right now, plus cache stats and recent activity. The best FIRST call for open-ended "why is my site slow" or "is anything wrong" questions.',
152 277 'synopsis' => array(),
153 278 ),
279 + array(
280 + 'name' => 'xspeed recommend',
281 + 'callback' => array( $this, 'cli_recommend' ),
282 + 'shortdesc' => 'List ranked next-best-action recommendations, or apply one by id.',
283 + 'ai_hint' => 'The ranked list of what to do next to make this site faster, and the way to apply one. Use when asked "what should I improve" or "what\'s the biggest win" — each item is actionable and ordered by impact.',
284 + 'synopsis' => array(
285 + array(
286 + 'type' => 'positional',
287 + 'name' => 'action',
288 + 'options' => array( 'list', 'apply' ),
289 + 'optional' => true,
290 + ),
291 + array(
292 + 'type' => 'positional',
293 + 'name' => 'id',
294 + 'optional' => true,
295 + ),
296 + ),
297 + ),
154 298 );
299 + }
300 +
301 + /** CLI: `wp xspeed recommend [list|apply <id>]` — MCP-reachable via run_command. */
302 + public function cli_recommend( array $args, array $assoc ): void {
303 + $action = isset( $args[0] ) ? (string) $args[0] : 'list';
304 +
305 + if ( 'apply' === $action ) {
306 + $id = isset( $args[1] ) ? (string) $args[1] : '';
307 + if ( '' === $id ) {
308 + \WP_CLI::error( 'Usage: wp xspeed recommend apply <id>' );
309 + return;
310 + }
311 + $result = \XSpeed\Recommendations::apply( $id );
312 + if ( is_wp_error( $result ) ) {
313 + \WP_CLI::error( $result->get_error_message() );
314 + return;
315 + }
316 + \WP_CLI::success( sprintf( 'Applied "%s". %d recommendation(s) remain.', $id, count( $result['recommendations'] ) ) );
317 + return;
318 + }
319 +
320 + $recs = \XSpeed\Recommendations::all();
321 + if ( empty( $recs ) ) {
322 + \WP_CLI::success( 'No recommendations — configuration looks healthy.' );
323 + return;
324 + }
325 + foreach ( $recs as $i => $rec ) {
326 + $fixable = 'apply' === ( $rec['action']['type'] ?? '' ) ? ' (one-click: wp xspeed recommend apply ' . $rec['id'] . ')' : '';
327 + \WP_CLI::log( sprintf( '%d. [%s] %s — %s%s', $i + 1, $rec['id'], $rec['title'], $rec['detail'], $fixable ) );
328 + }
155 329 }
156 330
157 331 /**
158 332 * Single endpoint that backs the dashboard panel. Refreshed lazily by