| @@ -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, |
| @@ -619,8 +644,13 @@ | ||
| 619 | 644 | if ( empty( $schema ) && empty( $custom_panel ) ) { |
| 620 | 645 | continue; |
| 621 | 646 | } |
| 622 | 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(); | |
| 623 | 653 | |
| 624 | 654 | $entry = array( |
| 625 | 655 | 'slug' => $slug, |
| 626 | 656 | 'tier' => $module->tier(), |
| @@ -645,9 +675,9 @@ | ||
| 645 | 675 | // must read THIS, not `enabled`, which only ever described the |
| 646 | 676 | // modules that happen to store that one key. null means the |
| 647 | 677 | // module has no meaningful on/off and should be excluded from |
| 648 | 678 | // any count rather than treated as off. (#363) |
| 649 | - 'active' => $module->is_active(), | |
| 679 | + 'active' => ( null !== $blocked && true === $active ) ? false : $active, | |
| 650 | 680 | // One sentence explaining the line above, computed next to it |
| 651 | 681 | // so the two cannot disagree. The UI shows it behind an (i) |
| 652 | 682 | // beside the status pill: "On" is a bare assertion otherwise, |
| 653 | 683 | // and least obvious exactly where it matters -- Media |
| @@ -652,12 +682,23 @@ | ||
| 652 | 682 | // beside the status pill: "On" is a bare assertion otherwise, |
| 653 | 683 | // and least obvious exactly where it matters -- Media |
| 654 | 684 | // Optimization reads On while its two most prominent switches |
| 655 | 685 | // are off, because three other flags are on. (#363) |
| 656 | - '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(), | |
| 657 | 691 | 'label' => $meta['label'] ?? ucfirst( $slug ), |
| 658 | 692 | 'icon' => $meta['icon'] ?? 'Square', |
| 659 | 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, | |
| 660 | 701 | // Short label for the module's own tab when it hosts a tabbed |
| 661 | 702 | // page (FBS-83633). Only set on host modules. |
| 662 | 703 | 'tab_label' => $meta['tab_label'] ?? null, |
| 663 | 704 | // Public view: real values except secret fields, which are masked. |
| @@ -673,8 +714,12 @@ | ||
| 673 | 714 | 'setting_origins' => Settings_Manager::origins( $slug ), |
| 674 | 715 | 'schema' => $schema, |
| 675 | 716 | 'notices' => $module->ui_notices(), |
| 676 | 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, | |
| 677 | 722 | ); |
| 678 | 723 | |
| 679 | 724 | /** |
| 680 | 725 | * Last-mile descriptor filter. Lets Pro (or third-party |