'
. ''
. ''
. ''
. ''
/*
* Three dots, matching assets/ai-o-icon.png, which the in-page
* navigation uses. The sidebar mark had two, so the same logo drew
* differently depending which part of the plugin you looked at.
*
* Monochrome is deliberate and stays: a full-colour logo in the
* admin menu fights every colour scheme WordPress ships.
*
* The placement is measured, not eyeballed. At 20x20 the ring
* already reaches x=19.9, so a third dot beside it collides -- the
* first attempt overlapped it by 0.2 units and would have looked
* fused at real size. Up and to the left clears by 1.09 units, the
* widest gap available without redrawing the composition. Re-measure
* anything moved here; at this size the difference between separated
* and touching is about one unit.
*/
. ''
. ''
. ''
. ''
. ''
. '';
return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- data URI for a menu icon, not obfuscation.
}
public function register_menu() {
add_menu_page(
__( 'AI-O Optimiser', 'ai-o-optimiser' ),
__( 'AI-O Optimiser', 'ai-o-optimiser' ),
self::dashboard_capability(),
'ai-o-optimiser-dashboard',
array( $this, 'render_dashboard' ),
self::menu_icon(),
80
);
add_submenu_page( 'ai-o-optimiser-dashboard', __( 'Dashboard', 'ai-o-optimiser' ), __( 'Dashboard', 'ai-o-optimiser' ), self::dashboard_capability(), 'ai-o-optimiser-dashboard', array( $this, 'render_dashboard' ), 1 );
add_submenu_page( 'ai-o-optimiser-dashboard', __( 'Metadata', 'ai-o-optimiser' ), __( 'Metadata', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-optimise', array( $this, 'render_optimise' ), 10 );
add_submenu_page( 'ai-o-optimiser-dashboard', __( 'Settings', 'ai-o-optimiser' ), __( 'Settings', 'ai-o-optimiser' ), self::SENSITIVE_CAPABILITY, 'ai-o-optimiser-settings', array( $this, 'render_settings' ), 60 );
add_submenu_page( null, __( 'Notifications', 'ai-o-optimiser' ), __( 'Notifications', 'ai-o-optimiser' ), self::SENSITIVE_CAPABILITY, 'ai-o-optimiser-notifications', array( $this, 'render_notifications' ) );
// Logs, AI Guidance Files, System Check, Safety Check and the AI Review
// Prompt are configure-once / check-occasionally screens, not daily
// workflow screens. They stay fully reachable at their existing URLs
// (no broken links/emails/bookmarks) but no longer clutter the main
// sidebar — they're grouped as tabs alongside Settings instead, via a
// null parent slug (WordPress's documented way to register a page
// without a visible menu entry).
add_submenu_page( null, __( 'Logs', 'ai-o-optimiser' ), __( 'Logs', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-logs', array( $this, 'render_logs' ) );
/*
* The Metadata Workflow slug is registered by the add-on when it is
* present (its own real screen, or its own locked state). When it is
* not, eleven links elsewhere in this admin still point at the slug --
* dashboard priority actions, the API-keys button on Settings,
* background-job rows -- so core registers it, hidden, to answer them
* with the absent-state presentation rather than "Sorry, you are not
* allowed to access this page".
*/
if ( ! class_exists( 'AI_O_Optimiser_Extensions' ) || ! AI_O_Optimiser_Extensions::is_available( 'metadata' ) ) {
add_submenu_page( null, __( 'Metadata', 'ai-o-optimiser' ), __( 'Metadata', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-metadata-workflow', array( $this, 'render_optimise' ) );
}
add_submenu_page( null, __( 'AI Guidance Files', 'ai-o-optimiser' ), __( 'AI Guidance Files', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-guidance', array( 'AI_O_Optimiser_LLMS_Admin', 'render_admin_page' ) );
add_submenu_page( null, __( 'Environmental Safety Check', 'ai-o-optimiser' ), __( 'Safety Check', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-environment-check', array( 'AI_O_Optimiser_Environment_Check', 'render_admin_page' ) );
add_submenu_page( null, __( 'System Check', 'ai-o-optimiser' ), __( 'System Check', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-system-check', array( 'AI_O_Optimiser_System_Check', 'render_admin_page' ) );
add_submenu_page( null, __( 'AI-O Optimiser Review Prompt', 'ai-o-optimiser' ), __( 'AI Review Prompt', 'ai-o-optimiser' ), self::CAPABILITY, 'ai-o-optimiser-deep-review-prompt', array( $this, 'render_deep_review_prompt' ) );
}
/**
* Force the exact intended sidebar order regardless of registration
* timing across plugins. add_submenu_page()'s numeric $position argument
* did not reliably produce the intended order once a second plugin
* (the legacy Metadata module) was registering into the same top-level menu — so
* this runs last, after every plugin's own admin_menu callback, and
* rebuilds the submenu array directly by slug instead of trusting
* position values to sort correctly across plugin boundaries.
*/
public function reorder_menu() {
global $submenu;
if ( empty( $submenu['ai-o-optimiser-dashboard'] ) || ! is_array( $submenu['ai-o-optimiser-dashboard'] ) ) {
return;
}
$desired_order = array(
'ai-o-optimiser-dashboard', // What is happening.
'ai-o-optimiser-optimise', // Metadata workflow.
'ai-o-optimiser-schema-review', // Structured-data workflow.
'ai-o-optimiser-settings', // Administrator-only configuration and diagnostics.
);
$by_slug = array();
foreach ( $submenu['ai-o-optimiser-dashboard'] as $item ) {
$by_slug[ $item[2] ] = $item;
}
$ordered = array();
foreach ( $desired_order as $slug ) {
if ( isset( $by_slug[ $slug ] ) ) {
$ordered[] = $by_slug[ $slug ];
unset( $by_slug[ $slug ] );
}
}
// Workflow/detail pages are registered with a null parent by their owning
// modules. Do not register them under ai-o-optimiser-dashboard and then remove them
// here: WordPress uses the parent relationship when validating direct admin
// URLs, and stripping those submenu rows makes valid detail URLs fail with
// a permissions error.
$submenu['ai-o-optimiser-dashboard'] = $ordered;
}
/**
* Persistent in-window navigation for every AI-O Optimiser screen.
*
* Dashboard is intentionally the only area available to non-admin viewers.
* Operational, technical and configuration pages require the dedicated
* administrator capability and carry an explicit advanced-change warning.
*/
public function render_global_nav() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screen state (filters, tab selection, pagination, redirect result notices). Nothing is written here, so there is no form submission to carry a nonce.
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
$groups = array(
'ai-o-optimiser-dashboard' => 'dashboard',
'ai-o-optimiser-optimise' => 'metadata',
'ai-o-optimiser-metadata-workflow' => 'metadata',
'ai-o-optimiser-content-opportunities' => 'metadata',
'ai-o-optimiser-schema-review' => 'schema',
'ai-o-optimiser-system-check' => 'settings',
'ai-o-optimiser-guidance' => 'settings',
'ai-o-optimiser-environment-check' => 'settings',
'ai-o-optimiser-logs' => 'settings',
'ai-o-optimiser-deep-review-prompt' => 'settings',
'ai-o-optimiser-settings' => 'settings',
'ai-o-optimiser-notifications' => 'settings',
);
if ( ! isset( $groups[ $page ] ) ) {
return;
}
$active = $groups[ $page ];
$is_admin = current_user_can( self::CAPABILITY );
$items = array(
'dashboard' => array( 'label' => __( 'Dashboard', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-dashboard' ) ),
);
if ( $is_admin ) {
$items['metadata'] = array( 'label' => __( 'Metadata', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-optimise' ) );
$items['schema'] = array( 'label' => __( 'Schema', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-schema-review' ) );
$items['settings'] = array( 'label' => __( 'Settings', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-settings' ) );
}
?>
__( 'AI brands', 'ai-o-optimiser' ),
'ai-o-optimiser-visibility-overview' => __( 'Overview', 'ai-o-optimiser' ),
'ai-o-optimiser-ai-league' => __( 'League table', 'ai-o-optimiser' ),
'ai-o-optimiser-traffic-impact' => __( 'Traffic impact', 'ai-o-optimiser' ),
'ai-o-optimiser-page-detail' => __( 'Pages', 'ai-o-optimiser' ),
);
echo '';
}
/**
* Print the dashboard row into the shell, on the dashboard only.
*
* @param string $active Primary navigation group.
* @param string $page Current admin page slug.
*/
public static function render_secondary_nav( $active, $page ) {
if ( 'dashboard' !== $active || 'ai-o-optimiser-dashboard' !== $page ) {
return;
}
self::render_dashboard_tab_nav();
}
public static function render_settings_tabs( $active_slug ) {
// The diagnostic screens live behind the Health section of Settings.
// Until 1.27.1 they shared one flat strip with a "Settings" link, which
// read as ten peers once the section tabs were added beside them. Logs
// and the AI review prompt remain secondary tools rather than navigation
// items; they simply show Health as the active section.
//
// AI Guidance Files was the fourth of them and is not here as of 1.37.1.
// The rail carries it under Publish, so listing it here printed the same
// label, marked open, in two navigations about 150px apart -- which is
// the thing the rail was built to stop rather than to cause. What is left
// is the screens that belong to the rail's Health & checks item: that is
// secondary navigation inside one rail item, not a second copy of one.
// See AI_O_Optimiser_Admin_Rail::groups().
$tabs = array();
if ( current_user_can( self::SENSITIVE_CAPABILITY ) ) {
$tabs['notifications'] = array( 'label' => __( 'Notifications', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-notifications' ) );
}
$tabs['system-check'] = array( 'label' => __( 'System Check', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-system-check' ) );
$tabs['safety-check'] = array( 'label' => __( 'Environment & storage', 'ai-o-optimiser' ), 'url' => admin_url( 'admin.php?page=ai-o-optimiser-environment-check' ) );
AI_O_Optimiser_Settings_Navigation::render( $active_slug, $tabs );
}
/**
* Load the plugin's small admin stylesheet, only on our own screens.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_assets( $hook ) {
if ( strpos( $hook, 'ai-o-optimiser-' ) === false ) {
return;
}
$css_path = AI_O_Optimiser_PLUGIN_DIR . 'assets/admin.css';
$css_ver = file_exists( $css_path ) ? AI_O_Optimiser_VERSION . '.' . filemtime( $css_path ) : AI_O_Optimiser_VERSION;
wp_enqueue_style( 'ai-o-optimiser-admin', AI_O_Optimiser_PLUGIN_URL . 'assets/admin.css', array(), $css_ver );
$consistency_path = AI_O_Optimiser_PLUGIN_DIR . 'assets/admin-modules.css';
$consistency_ver = file_exists( $consistency_path ) ? AI_O_Optimiser_VERSION . '.' . filemtime( $consistency_path ) : AI_O_Optimiser_VERSION;
wp_enqueue_style( 'ai-o-optimiser-admin-modules', AI_O_Optimiser_PLUGIN_URL . 'assets/admin-modules.css', array( 'ai-o-optimiser-admin' ), $consistency_ver );
$guard_path = AI_O_Optimiser_PLUGIN_DIR . 'assets/admin-notice-guard.js';
$guard_ver = file_exists( $guard_path ) ? AI_O_Optimiser_VERSION . '.' . filemtime( $guard_path ) : AI_O_Optimiser_VERSION;
wp_enqueue_script( 'ai-o-optimiser-admin-notice-guard', AI_O_Optimiser_PLUGIN_URL . 'assets/admin-notice-guard.js', array(), $guard_ver, true );
// Hover and keyboard focus on the signal "?" explainers are pure CSS. This
// adds only tap-to-toggle and Escape-to-dismiss, so the definitions still
// work with the script blocked.
$hints_path = AI_O_Optimiser_PLUGIN_DIR . 'assets/admin-signal-hints.js';
$hints_ver = file_exists( $hints_path ) ? AI_O_Optimiser_VERSION . '.' . filemtime( $hints_path ) : AI_O_Optimiser_VERSION;
wp_enqueue_script( 'ai-o-optimiser-admin-signal-hints', AI_O_Optimiser_PLUGIN_URL . 'assets/admin-signal-hints.js', array(), $hints_ver, true );
/*
* A dependency-free handle that exists only to carry inline scripts.
*
* Several screens need a few lines of behaviour that depend on a
* translated string or on which page is being rendered, so they cannot
* be a static .js file. They used to be printed as raw