| @@ -531,8 +531,13 @@ | ||
| 531 | 531 | // means the request-terminating nginx isn't user-editable on this |
| 532 | 532 | // host — the banner uses this to switch to honest messaging |
| 533 | 533 | // instead of dangling a snippet the user can't apply. |
| 534 | 534 | 'behind_proxy' => Server::is_behind_proxy(), |
| 535 | + // Mirrors /status (see the note there). On first paint the | |
| 536 | + // probe is whatever was last cached, so this is usually | |
| 537 | + // `unknown` until the Health tab runs a live probe — the | |
| 538 | + // dashboard needs the key present either way. | |
| 539 | + 'rules' => Cache::rules_state( $probe ), | |
| 535 | 540 | ); |
| 536 | 541 | } |
| 537 | 542 | |
| 538 | 543 | return array( |
| @@ -551,9 +556,18 @@ | ||
| 551 | 556 | // /status block so the dashboard callout renders on first paint |
| 552 | 557 | // without waiting for a status re-fetch. |
| 553 | 558 | 'mobile_separate' => array( |
| 554 | 559 | 'enabled' => (bool) ( $opts['cache_enabled'] ? ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false ), |
| 555 | - 'blocking' => $rewrite_capable && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 560 | + // Gated to servers that HAVE a static fast path — on IIS or | |
| 561 | + // an undetected server block_reason still falls through to | |
| 562 | + // mobile_separate, and reporting that as "blocking" would | |
| 563 | + // nag about a rewrite that does not exist there (#108). | |
| 564 | + // LiteSpeed joined the capable set with the Static Fast | |
| 565 | + // Path opt-in (#509); its own refusal (litespeed_dropin) | |
| 566 | + // outranks mobile_separate, so this stays false there | |
| 567 | + // until the opt-in is on. | |
| 568 | + 'blocking' => ( $rewrite_capable || Server::LITESPEED === $server_type ) | |
| 569 | + && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 556 | 570 | 'needs_review' => Cache::mobile_separate_needs_review(), |
| 557 | 571 | ), |
| 558 | 572 | // One consolidated nginx server-block snippet aggregating |
| 559 | 573 | // every enabled module's directives (Cache static-rewrite, |
| @@ -560,8 +574,14 @@ | ||
| 560 | 574 | // BrowserCache headers, GZIP, …). Null on non-nginx hosts |
| 561 | 575 | // or when no module contributes directives. Replaces the |
| 562 | 576 | // per-module "paste this snippet" notices. |
| 563 | 577 | 'nginx_server_block' => Cache::full_nginx_server_block(), |
| 578 | + // Mirrors the /status block so the enable-time disclosure is | |
| 579 | + // correct on FIRST PAINT. Without it the top-bar switch can be | |
| 580 | + // clicked before a status fetch lands, and the one moment the | |
| 581 | + // warning exists for — a leftover drop-in about to be | |
| 582 | + // replaced — is exactly when it would be missing. | |
| 583 | + 'dropin' => Page_Cache_Detector::dropin_disclosure(), | |
| 564 | 584 | ), |
| 565 | 585 | // Registered Modules (Free + Pro). The React app discovers them |
| 566 | 586 | // here and renders one sidebar item + one panel per module that |
| 567 | 587 | // declares a settings schema. Hidden modules are filtered. |
| @@ -613,8 +633,13 @@ | ||
| 613 | 633 | if ( empty( $schema ) && empty( $custom_panel ) ) { |
| 614 | 634 | continue; |
| 615 | 635 | } |
| 616 | 636 | $settings = Settings_Manager::get_public( $slug ); |
| 637 | + // Something else on the site has taken over this module's job. It | |
| 638 | + // stands down whatever its own switch says, so it is reported off, | |
| 639 | + // with the reason in place of its usual one. | |
| 640 | + $blocked = $module->blocked_by(); | |
| 641 | + $active = $module->is_active(); | |
| 617 | 642 | |
| 618 | 643 | $entry = array( |
| 619 | 644 | 'slug' => $slug, |
| 620 | 645 | 'tier' => $module->tier(), |
| @@ -639,9 +664,9 @@ | ||
| 639 | 664 | // must read THIS, not `enabled`, which only ever described the |
| 640 | 665 | // modules that happen to store that one key. null means the |
| 641 | 666 | // module has no meaningful on/off and should be excluded from |
| 642 | 667 | // any count rather than treated as off. (#363) |
| 643 | - 'active' => $module->is_active(), | |
| 668 | + 'active' => ( null !== $blocked && true === $active ) ? false : $active, | |
| 644 | 669 | // One sentence explaining the line above, computed next to it |
| 645 | 670 | // so the two cannot disagree. The UI shows it behind an (i) |
| 646 | 671 | // beside the status pill: "On" is a bare assertion otherwise, |
| 647 | 672 | // and least obvious exactly where it matters -- Media |
| @@ -646,12 +671,23 @@ | ||
| 646 | 671 | // beside the status pill: "On" is a bare assertion otherwise, |
| 647 | 672 | // and least obvious exactly where it matters -- Media |
| 648 | 673 | // Optimization reads On while its two most prominent switches |
| 649 | 674 | // are off, because three other flags are on. (#363) |
| 650 | - 'active_reason' => $module->active_reason(), | |
| 675 | + 'active_reason' => null !== $blocked ? $blocked : $module->active_reason(), | |
| 676 | + // A purchase or licence the module still waits on. Shown in the | |
| 677 | + // page header in place of the On/Off pill. Not while another | |
| 678 | + // module has taken over: that reason is the one to show. | |
| 679 | + 'status_label' => null !== $blocked ? null : $module->status_label(), | |
| 651 | 680 | 'label' => $meta['label'] ?? ucfirst( $slug ), |
| 652 | 681 | 'icon' => $meta['icon'] ?? 'Square', |
| 653 | 682 | 'description' => $meta['description'] ?? '', |
| 683 | + // The dashboard group the module belongs to (cache, performance, | |
| 684 | + // network, insights, tools, ai-agents, settings), declared by the | |
| 685 | + // module itself. The Hub kept its own copy of this map, so every | |
| 686 | + // new module landed in its "Other" bucket until the Hub shipped. | |
| 687 | + // Must agree with src/components/sidebarGroups.ts; a unit test | |
| 688 | + // holds the two together. | |
| 689 | + 'group' => $meta['group'] ?? null, | |
| 654 | 690 | // Short label for the module's own tab when it hosts a tabbed |
| 655 | 691 | // page (FBS-83633). Only set on host modules. |
| 656 | 692 | 'tab_label' => $meta['tab_label'] ?? null, |
| 657 | 693 | // Public view: real values except secret fields, which are masked. |
| @@ -657,11 +693,22 @@ | ||
| 657 | 693 | // Public view: real values except secret fields, which are masked. |
| 658 | 694 | // The dashboard bundle localizes this into page HTML, so a raw |
| 659 | 695 | // credential here would be readable from view-source. (#115) |
| 660 | 696 | 'settings' => $settings, |
| 697 | + // Where each value actually came from: a wp-config.php constant, | |
| 698 | + // the option row, or the schema default. The panel renders a | |
| 699 | + // constant-sourced field read-only and names the constant, so it | |
| 700 | + // can never present an editable box over a value the site is not | |
| 701 | + // using. Every module gets this, not just the ones that declare | |
| 702 | + // constants today. (#398) | |
| 703 | + 'setting_origins' => Settings_Manager::origins( $slug ), | |
| 661 | 704 | 'schema' => $schema, |
| 662 | 705 | 'notices' => $module->ui_notices(), |
| 663 | 706 | 'custom_panel' => $meta['custom_panel'] ?? null, |
| 707 | + // Why this module may not be switched on, when something else | |
| 708 | + // on the site has taken over from it. Null for almost every | |
| 709 | + // module almost always — Free never blocks anything itself. | |
| 710 | + 'blocked_by' => $blocked, | |
| 664 | 711 | ); |
| 665 | 712 | |
| 666 | 713 | /** |
| 667 | 714 | * Last-mile descriptor filter. Lets Pro (or third-party |