). * * Constitution: singleton + private __construct + zero add_action/add_filter. * Hooks wired by Includes\Main::define_admin_hooks() targeting register_submenu(). * * Page slugs live in Includes\Utilities\AdminPageSlugs (RT-1, 2026-06-17) * so list tables and Settings don't depend on this sibling for constants. */ class Menu { /** @var Menu|null */ protected static $_instance = null; /** @var string */ private $plugin_name; /** @var string */ private $version; public static function instance(): self { if ( null === self::$_instance ) { self::$_instance = new self(); } return self::$_instance; } private function __construct() { $this->plugin_name = ACROSSAI_MCP_MANAGER_PLUGIN_NAME_SLUG; $this->version = ACROSSAI_MCP_MANAGER_VERSION; } /** * Register submenus under the shared `acrossai` parent menu (FR-018 / FR-020). * * Positions per FR-020 (avoiding collision with acrossai-abilities-manager's * position 1 for Abilities): * - Position 2 — MCP Manager main (Servers listing) * - Position 4 — Access Control (conditional on vendor package presence) * * Render callbacks delegate to Settings::instance() so that Settings * owns all page rendering and stays the single source of truth for * admin HTML (Constitution Admin Partials Rule). * * The shared parent menu itself is bootstrapped by \AcrossAI_Main_Menu\SettingsPage * in acrossai-mcp-manager.php on plugins_loaded priority 0 (FR-029 / D15 / DEV4). */ public function register_submenu(): void { $settings = Settings::instance(); // 1) MCP main page — position 2 under `acrossai` parent (FR-020). add_submenu_page( SettingsPage::PARENT_SLUG, __( 'MCP', 'acrossai-mcp-manager' ), __( 'MCP', 'acrossai-mcp-manager' ), 'manage_options', AdminPageSlugs::PARENT, array( $settings, 'render_list_page' ), 2 ); } /** * Prepend a "Settings" action link to the plugin's row on the Plugins screen. * Wired via `plugin_action_links_` so this callback only fires * for this plugin (FR-003). * * S5/B6: admin_url() output is wrapped with esc_url() at the boundary. * * @param array $links Existing action links. * @return array Modified links. */ public function plugin_action_links( $links ): array { $settings_url = esc_url( admin_url( 'admin.php?page=' . AdminPageSlugs::PARENT ) ); $settings_link = sprintf( '%s', $settings_url, esc_html__( 'Settings', 'acrossai-mcp-manager' ) ); // Prepend the Settings link so it appears first. array_unshift( $links, $settings_link ); return $links; } }