PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 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 All 35 releases
← All changes | includes/class-admin.php +61 -3 1.2.4 → 1.4.1 View file →
@@ -14,8 +14,15 @@
14 14 const PAGE_SLUG = 'xspeed';
15 15
16 16 const THEME_COOKIE = 'xspeed_theme';
17 17
18 + /**
19 + * The dashboard's sidebar offer reads a module's `purchase_needed` before
20 + * its `status_label`. An add-on checks for this constant before it labels
21 + * a setup step, which an older dashboard would read as "still for sale".
22 + */
23 + const OFFER_READS_PURCHASE_NEEDED = true;
24 +
18 25 public function __construct() {
19 26 add_action( 'admin_menu', array( $this, 'register_menu' ) );
20 27 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
21 28 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_styles' ) );
@@ -434,8 +441,12 @@
434 441 // Pro), so Pro filters this via `xspeed_pro_state`. Default:
435 442 // not_installed when Pro is absent; 'active' when Pro is present
436 443 // (Pro downgrades to 'unlicensed' when its license isn't valid).
437 444 'proState' => self::pro_state(),
445 + // The xCloud purge plugin is here: the site already has
446 + // Cloudflare Enterprise from its host, so the dashboard does
447 + // not offer the add-on.
448 + 'xcloudPurgePlugin' => Managed_Edge_Purge::installed(),
438 449 // Setup Wizard URL, surfaced in the sidebar profile popover.
439 450 'wizardUrl' => admin_url( 'admin.php?page=' . Onboarding::PAGE_SLUG ),
440 451 'bootstrap' => self::bootstrap_payload(),
441 452 )
@@ -531,8 +542,13 @@
531 542 // means the request-terminating nginx isn't user-editable on this
532 543 // host — the banner uses this to switch to honest messaging
533 544 // instead of dangling a snippet the user can't apply.
534 545 'behind_proxy' => Server::is_behind_proxy(),
546 + // Mirrors /status (see the note there). On first paint the
547 + // probe is whatever was last cached, so this is usually
548 + // `unknown` until the Health tab runs a live probe — the
549 + // dashboard needs the key present either way.
550 + 'rules' => Cache::rules_state( $probe ),
535 551 );
536 552 }
537 553
538 554 return array(
@@ -551,9 +567,18 @@
551 567 // /status block so the dashboard callout renders on first paint
552 568 // without waiting for a status re-fetch.
553 569 'mobile_separate' => array(
554 570 'enabled' => (bool) ( $opts['cache_enabled'] ? ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false ),
555 - 'blocking' => $rewrite_capable && 'mobile_separate' === Cache::static_rewrite_block_reason(),
571 + // Gated to servers that HAVE a static fast path — on IIS or
572 + // an undetected server block_reason still falls through to
573 + // mobile_separate, and reporting that as "blocking" would
574 + // nag about a rewrite that does not exist there (#108).
575 + // LiteSpeed joined the capable set with the Static Fast
576 + // Path opt-in (#509); its own refusal (litespeed_dropin)
577 + // outranks mobile_separate, so this stays false there
578 + // until the opt-in is on.
579 + 'blocking' => ( $rewrite_capable || Server::LITESPEED === $server_type )
580 + && 'mobile_separate' === Cache::static_rewrite_block_reason(),
556 581 'needs_review' => Cache::mobile_separate_needs_review(),
557 582 ),
558 583 // One consolidated nginx server-block snippet aggregating
559 584 // every enabled module's directives (Cache static-rewrite,
@@ -560,8 +585,14 @@
560 585 // BrowserCache headers, GZIP, …). Null on non-nginx hosts
561 586 // or when no module contributes directives. Replaces the
562 587 // per-module "paste this snippet" notices.
563 588 'nginx_server_block' => Cache::full_nginx_server_block(),
589 + // Mirrors the /status block so the enable-time disclosure is
590 + // correct on FIRST PAINT. Without it the top-bar switch can be
591 + // clicked before a status fetch lands, and the one moment the
592 + // warning exists for — a leftover drop-in about to be
593 + // replaced — is exactly when it would be missing.
594 + 'dropin' => Page_Cache_Detector::dropin_disclosure(),
564 595 ),
565 596 // Registered Modules (Free + Pro). The React app discovers them
566 597 // here and renders one sidebar item + one panel per module that
567 598 // declares a settings schema. Hidden modules are filtered.
@@ -613,8 +644,13 @@
613 644 if ( empty( $schema ) && empty( $custom_panel ) ) {
614 645 continue;
615 646 }
616 647 $settings = Settings_Manager::get_public( $slug );
648 + // Something else on the site has taken over this module's job. It
649 + // stands down whatever its own switch says, so it is reported off,
650 + // with the reason in place of its usual one.
651 + $blocked = $module->blocked_by();
652 + $active = $module->is_active();
617 653
618 654 $entry = array(
619 655 'slug' => $slug,
620 656 'tier' => $module->tier(),
@@ -639,9 +675,9 @@
639 675 // must read THIS, not `enabled`, which only ever described the
640 676 // modules that happen to store that one key. null means the
641 677 // module has no meaningful on/off and should be excluded from
642 678 // any count rather than treated as off. (#363)
643 - 'active' => $module->is_active(),
679 + 'active' => ( null !== $blocked && true === $active ) ? false : $active,
644 680 // One sentence explaining the line above, computed next to it
645 681 // so the two cannot disagree. The UI shows it behind an (i)
646 682 // beside the status pill: "On" is a bare assertion otherwise,
647 683 // and least obvious exactly where it matters -- Media
@@ -646,12 +682,23 @@
646 682 // beside the status pill: "On" is a bare assertion otherwise,
647 683 // and least obvious exactly where it matters -- Media
648 684 // Optimization reads On while its two most prominent switches
649 685 // are off, because three other flags are on. (#363)
650 - 'active_reason' => $module->active_reason(),
686 + 'active_reason' => null !== $blocked ? $blocked : $module->active_reason(),
687 + // A purchase or licence the module still waits on. Shown in the
688 + // page header in place of the On/Off pill. Not while another
689 + // module has taken over: that reason is the one to show.
690 + 'status_label' => null !== $blocked ? null : $module->status_label(),
651 691 'label' => $meta['label'] ?? ucfirst( $slug ),
652 692 'icon' => $meta['icon'] ?? 'Square',
653 693 'description' => $meta['description'] ?? '',
694 + // The dashboard group the module belongs to (cache, performance,
695 + // network, insights, tools, ai-agents, settings), declared by the
696 + // module itself. The Hub kept its own copy of this map, so every
697 + // new module landed in its "Other" bucket until the Hub shipped.
698 + // Must agree with src/components/sidebarGroups.ts; a unit test
699 + // holds the two together.
700 + 'group' => $meta['group'] ?? null,
654 701 // Short label for the module's own tab when it hosts a tabbed
655 702 // page (FBS-83633). Only set on host modules.
656 703 'tab_label' => $meta['tab_label'] ?? null,
657 704 // Public view: real values except secret fields, which are masked.
@@ -657,11 +704,22 @@
657 704 // Public view: real values except secret fields, which are masked.
658 705 // The dashboard bundle localizes this into page HTML, so a raw
659 706 // credential here would be readable from view-source. (#115)
660 707 'settings' => $settings,
708 + // Where each value actually came from: a wp-config.php constant,
709 + // the option row, or the schema default. The panel renders a
710 + // constant-sourced field read-only and names the constant, so it
711 + // can never present an editable box over a value the site is not
712 + // using. Every module gets this, not just the ones that declare
713 + // constants today. (#398)
714 + 'setting_origins' => Settings_Manager::origins( $slug ),
661 715 'schema' => $schema,
662 716 'notices' => $module->ui_notices(),
663 717 'custom_panel' => $meta['custom_panel'] ?? null,
718 + // Why this module may not be switched on, when something else
719 + // on the site has taken over from it. Null for almost every
720 + // module almost always — Free never blocks anything itself.
721 + 'blocked_by' => $blocked,
664 722 );
665 723
666 724 /**
667 725 * Last-mile descriptor filter. Lets Pro (or third-party