'',
'shield' => '',
'eye' => '',
'x' => '',
'clock' => '',
'code' => '',
'layout' => '',
'bell' => '',
'user' => '',
'rss' => '',
'lock' => '',
'tool' => '',
'search' => '',
'img' => '',
'chart' => '',
'check' => '',
'alert' => '',
'blog' => '',
'business' => '',
'cart' => '',
'agency' => '',
);
/**
* Hook suffixes of the plugin's admin screens, as returned by add_menu_page().
*
* @var string[]
*/
private $page_hooks = array();
/**
* Register admin hooks.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_ajax_ovkit_save_settings', array( $this, 'ajax_save' ) );
add_action( 'wp_ajax_ovkit_scan', array( $this, 'ajax_scan' ) );
add_action( 'wp_ajax_ovkit_apply_safe', array( $this, 'ajax_apply_safe' ) );
add_action( 'wp_ajax_ovkit_rollback', array( $this, 'ajax_rollback' ) );
add_action( 'wp_ajax_ovkit_site_scan', array( $this, 'ajax_site_scan' ) );
add_action( 'wp_ajax_ovkit_health_check', array( $this, 'ajax_health_check' ) );
add_action( 'wp_ajax_ovkit_apply_preset', array( $this, 'ajax_apply_preset' ) );
add_action( 'wp_ajax_ovkit_export_settings', array( $this, 'ajax_export_settings' ) );
add_action( 'wp_ajax_ovkit_import_settings', array( $this, 'ajax_import_settings' ) );
add_action( 'wp_ajax_ovkit_make_login_bypass', array( $this, 'ajax_make_login_bypass' ) );
add_action( 'admin_bar_menu', array( $this, 'admin_bar_indicator' ), 100 );
add_filter( 'admin_body_class', array( $this, 'body_class' ) );
}
/**
* Mark the toolkit's own screens so their background can match the panel skin.
*
* @param string $classes Space-separated body classes.
* @return string
*/
public function body_class( $classes ) {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( $screen && false !== strpos( (string) $screen->id, 'ovkit' ) ) {
$classes .= ' ovkit-screen';
}
return $classes;
}
// Menu registration.
/**
* Add menu.
*/
public function add_menu(): void {
$icon_url = 'data:image/svg+xml;base64,' . base64_encode( $this->logo_svg( 'black' ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Encodes the static menu icon SVG as a data URI, as add_menu_page() expects.
$title = __( 'Ovanap Admin Toolkit', 'ovanap-admin-toolkit' );
$pages = array(
array( 'ovkit', __( 'Dashboard', 'ovanap-admin-toolkit' ), 'render_dashboard' ),
array( 'ovkit-features', __( 'Features', 'ovanap-admin-toolkit' ), 'render_features' ),
array( 'ovkit-tracking', __( 'Tracking', 'ovanap-admin-toolkit' ), 'render_tracking' ),
array( 'ovkit-scan', __( 'Site Scan', 'ovanap-admin-toolkit' ), 'render_scan' ),
array( 'ovkit-log', __( 'Activity Log', 'ovanap-admin-toolkit' ), 'render_log' ),
);
$this->page_hooks[] = add_menu_page( $title, __( 'Admin Toolkit', 'ovanap-admin-toolkit' ), 'manage_options', 'ovkit', array( $this, 'render_dashboard' ), $icon_url, 80 );
foreach ( $pages as $page ) {
$this->page_hooks[] = add_submenu_page( 'ovkit', $title . ' - ' . $page[1], $page[1], 'manage_options', $page[0], array( $this, $page[2] ) );
}
}
/**
* The OvKit mark (an O and a V with the live dot), as SVG markup.
*
* @param string $color Mark colour.
* @param string $dot Colour of the dot inside the O; defaults to the mark colour.
* @return string
*/
private function logo_svg( string $color, string $dot = '' ): string {
// The OvKit mark from ovkit.com: an O (ring) and a V, with the live dot inside the O.
$dot = '' !== $dot ? $dot : $color;
return '';
}
// Asset enqueue.
/**
* Enqueue admin assets on the plugin's own screens, plus the admin bar badge style everywhere in admin.
*
* @param string $hook Current admin page hook suffix.
*/
public function enqueue_assets( string $hook ): void {
if ( is_admin_bar_showing() ) {
wp_add_inline_style( 'admin-bar', '#wpadminbar .ovkit-ab-label{display:inline-flex;align-items:center;gap:4px}#wpadminbar .ovkit-ab-count{background:#ff5f0f;color:#16181b;font-size:10px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;font-weight:600;line-height:1}' );
}
if ( ! in_array( $hook, $this->page_hooks, true ) ) {
return;
}
wp_enqueue_media();
wp_enqueue_style( 'ovkit-admin', OVKIT_URL . 'admin/css/toolkit-admin.css', array(), OVKIT_VERSION );
wp_enqueue_script( 'ovkit-admin', OVKIT_URL . 'admin/js/toolkit-admin.js', array(), OVKIT_VERSION, true );
wp_localize_script(
'ovkit-admin',
'ovkitAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
// Separate nonces by blast radius: settings-save actions, backup I/O,
// read-only diagnostics, and the login-bypass issuer each get their
// own secret so a leaked nonce from one surface cannot be replayed
// against another.
'nonce' => wp_create_nonce( 'ovkit_toolkit_save' ),
'backupNonce' => wp_create_nonce( 'ovkit_backup' ),
'diagNonce' => wp_create_nonce( 'ovkit_diag' ),
'bypassNonce' => wp_create_nonce( 'ovkit_login_bypass' ),
'featuresUrl' => admin_url( 'admin.php?page=ovkit-features' ),
'i18n' => array(
'saving' => __( 'Saving...', 'ovanap-admin-toolkit' ),
'saved' => __( 'Saved! Reloading...', 'ovanap-admin-toolkit' ),
'savedPurged' => __( 'Saved. Page cache cleared:', 'ovanap-admin-toolkit' ),
'errorRetry' => __( 'Error - retry', 'ovanap-admin-toolkit' ),
'applySafeBtn' => __( 'Apply safe defaults', 'ovanap-admin-toolkit' ),
'applying' => __( 'Applying...', 'ovanap-admin-toolkit' ),
'applied' => __( 'Applied! Reloading...', 'ovanap-admin-toolkit' ),
/* translators: 1: beginner count, 2: standard count, 3: advanced count */
'appliedDetail' => __( 'Enabled %1$d beginner + %2$d standard features. %3$d advanced features available for manual review.', 'ovanap-admin-toolkit' ),
'errorPrefix' => __( 'Error: ', 'ovanap-admin-toolkit' ),
'confirmSafe' => __( 'Enable all Safe + Recommended features that were detected? You can rollback anytime.', 'ovanap-admin-toolkit' ),
'confirmRollback' => __( 'Restore settings from this point? This will be logged as a new change and can be undone.', 'ovanap-admin-toolkit' ),
'runHealth' => __( 'Run health check', 'ovanap-admin-toolkit' ),
'runningTests' => __( 'Running tests…', 'ovanap-admin-toolkit' ),
'running' => __( 'Running…', 'ovanap-admin-toolkit' ),
'scanning' => __( 'Scanning environment...', 'ovanap-admin-toolkit' ),
'scanDone' => __( 'Done — reloading...', 'ovanap-admin-toolkit' ),
'scanFailed' => __( 'Scan failed — retry', 'ovanap-admin-toolkit' ),
'refreshScan' => __( 'Refresh scan', 'ovanap-admin-toolkit' ),
),
)
);
}
// AJAX handlers.
/**
* Ajax save.
*/
public function ajax_save(): void {
check_ajax_referer( 'ovkit_toolkit_save', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
try {
$features = OVKIT_Feature_Registry::instance()->get_all();
$old = get_option( OVKIT_OPTION, array() );
$settings = ovkit_read_posted_settings( $features, 'ovkit_toolkit_save' );
$purged = ovkit_commit_settings( $old, $settings );
// Count active.
$on = 0;
foreach ( $features as $key => $f ) {
if ( ! empty( $settings[ $key ] ) ) {
++$on;
}
}
wp_send_json_success(
array(
'active' => $on,
'total' => count( $features ),
'purged' => $purged,
)
);
} catch ( \Throwable $e ) {
wp_send_json_error( 'Save error: ' . $e->getMessage() );
}
}
/**
* Ajax scan.
*/
public function ajax_scan(): void {
check_ajax_referer( 'ovkit_diag', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
try {
$results = OVKIT_Scanner::instance()->rescan();
$summary = OVKIT_Scanner::instance()->get_summary();
wp_send_json_success(
array(
'results' => $results,
'summary' => $summary,
)
);
} catch ( \Throwable $e ) {
wp_send_json_error( 'Scan error: ' . $e->getMessage() );
}
}
/**
* Ajax apply safe.
*/
public function ajax_apply_safe(): void {
check_ajax_referer( 'ovkit_toolkit_save', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
try {
$features = OVKIT_Feature_Registry::instance()->get_all();
$scan = OVKIT_Scanner::instance()->get_cached();
$old = get_option( OVKIT_OPTION, array() );
$settings = $old;
$enabled_beginner = 0;
$enabled_standard = 0;
$skipped_advanced = 0;
foreach ( $features as $key => $f ) {
$risk = $f['risk'] ?? 'safe';
$level = $f['level'] ?? 'standard';
$det = $scan[ $key ] ?? array();
if ( 'advanced' === $level ) {
++$skipped_advanced;
continue; // never auto-enable advanced features.
}
if ( ( 'safe' === $risk || 'recommended' === $risk ) && ! empty( $det['safe'] ) && ! empty( $det['detected'] ) ) {
$settings[ $key ] = 1;
if ( 'beginner' === $level ) {
++$enabled_beginner;
} else {
++$enabled_standard;
}
}
}
ovkit_commit_settings( $old, $settings );
wp_send_json_success(
array(
'applied' => true,
'enabled_beginner' => $enabled_beginner,
'enabled_standard' => $enabled_standard,
'skipped_advanced' => $skipped_advanced,
)
);
} catch ( \Throwable $e ) {
wp_send_json_error( 'Apply error: ' . $e->getMessage() );
}
}
/**
* Ajax rollback.
*/
public function ajax_rollback(): void {
check_ajax_referer( 'ovkit_toolkit_save', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
if ( ! isset( $_POST['index'] ) ) {
wp_send_json_error( 'Missing index' );
}
$index = absint( wp_unslash( $_POST['index'] ) );
$ok = OVKIT_Activity_Log::rollback( $index );
wp_send_json_success( array( 'rolled_back' => $ok ) );
}
/**
* Ajax export settings.
*/
public function ajax_export_settings(): void {
check_ajax_referer( 'ovkit_backup', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
$settings = get_option( OVKIT_OPTION, array() );
$export = array(
'plugin' => 'ovkit',
'version' => OVKIT_VERSION,
'exported' => gmdate( 'Y-m-d H:i:s' ),
'site' => home_url(),
'settings' => $settings,
);
wp_send_json_success( $export );
}
/**
* Ajax import settings.
*/
public function ajax_import_settings(): void {
check_ajax_referer( 'ovkit_backup', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
// The browser parses the uploaded JSON file and posts it as a normal
// settings[key] form, so an import goes through exactly the same
// registry allowlist and per-type sanitizers as a regular save.
$features = OVKIT_Feature_Registry::instance()->get_all();
$sensitive = $this->import_sensitive_keys();
$clean = ovkit_read_posted_settings( $features, 'ovkit_backup', $sensitive );
// Preserve current sensitive values: an import must not blank them.
$old = get_option( OVKIT_OPTION, array() );
foreach ( array_keys( $sensitive ) as $sk ) {
if ( array_key_exists( $sk, $old ) ) {
$clean[ $sk ] = $old[ $sk ];
}
}
ovkit_commit_settings( $old, $clean );
wp_send_json_success( array( 'imported' => count( $clean ) ) );
}
/**
* Settings keys that are site-local and never accepted from an imported file.
*
* @return array Map of key => true.
*/
private function import_sensitive_keys(): array {
$keys = array();
return $keys;
}
/**
* Mint a one-off login-URL bypass token. Used when an admin locks
* themselves out of the hidden login slug — they open Toolkit in the still-
* authenticated dashboard tab, click "Generate emergency bypass link",
* and get a URL they can paste into a private window or send to another
* admin. We store only the SHA-256 hash so the plaintext token exists
* client-side for five minutes and nowhere else.
*/
public function ajax_make_login_bypass(): void {
check_ajax_referer( 'ovkit_login_bypass', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
// 32-char alphanumeric token — ~190 bits of entropy, plenty against
// brute force given the 5-minute, single-use window.
$token = wp_generate_password( 32, false );
$hash = hash( 'sha256', $token );
set_transient( 'ovkit_login_bypass_hash', $hash, 5 * MINUTE_IN_SECONDS );
// Point the bypass link at wp-login.php directly rather than the
// hidden slug. When the hide-login callback sees a valid token it
// returns early without registering its init hook, so wp-login.php
// loads natively for that one request — the escape hatch an admin
// who has lost the custom slug actually needs. Using wp_login_url()
// would resolve back to the hidden slug (because that feature
// filters it) and defeat the bypass.
$url = add_query_arg( 'ovkit-bypass', $token, site_url( 'wp-login.php' ) );
if ( function_exists( 'ovkit_log' ) ) {
ovkit_log( 'Hide login: bypass token minted (hash stored, plaintext returned once)', null, 'info' );
}
wp_send_json_success(
array(
'url' => $url,
'expires_in' => 5 * MINUTE_IN_SECONDS,
)
);
}
/**
* Ajax site scan.
*/
public function ajax_site_scan(): void {
check_ajax_referer( 'ovkit_diag', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
try {
$results = OVKIT_Site_Scan::instance()->run();
$summary = OVKIT_Site_Scan::instance()->get_summary();
wp_send_json_success(
array(
'results' => $results,
'summary' => $summary,
)
);
} catch ( \Throwable $e ) {
wp_send_json_error( 'Site scan error: ' . $e->getMessage() );
}
}
/**
* Ajax health check.
*/
public function ajax_health_check(): void {
check_ajax_referer( 'ovkit_diag', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
try {
require_once OVKIT_PATH . 'includes/class-ovkit-health-check.php';
$results = ( new OVKIT_Health_Check() )->run();
$pass = count( array_filter( $results, fn( $r ) => 'pass' === $r['status'] ) );
$fail = count( array_filter( $results, fn( $r ) => 'fail' === $r['status'] ) );
$warn = count( array_filter( $results, fn( $r ) => 'warn' === $r['status'] ) );
wp_send_json_success(
array(
'results' => $results,
'summary' => array(
'pass' => $pass,
'fail' => $fail,
'warn' => $warn,
'total' => count( $results ),
),
)
);
} catch ( \Throwable $e ) {
wp_send_json_error( 'Health check error: ' . $e->getMessage() );
}
}
/**
* One-click presets: feature keys switched on for each site type.
*
* @return array
*/
private static function presets(): array {
return array(
'blog' => array(
'disable_emojis',
'remove_dashicons',
'remove_query_strings',
'remove_dns_prefetch',
'disable_oembed',
'disable_self_pingbacks',
'limit_heartbeat',
'hide_login_errors',
'remove_wp_version',
'block_author_enum',
'disable_xmlrpc',
'disable_file_editor',
'disable_app_passwords',
'remove_rsd_link',
'remove_wlw_manifest',
'remove_shortlink',
'remove_rest_api_link',
'disable_attachment_pages',
'disable_admin_email_check',
'clean_dashboard',
'smart_notices',
),
'business' => array(
'disable_emojis',
'remove_dashicons',
'remove_query_strings',
'remove_dns_prefetch',
'disable_oembed',
'disable_self_pingbacks',
'limit_heartbeat',
'hide_login_errors',
'remove_wp_version',
'block_author_enum',
'disable_xmlrpc',
'disable_file_editor',
'disable_app_passwords',
'block_rest_users',
'remove_rsd_link',
'remove_wlw_manifest',
'remove_shortlink',
'remove_rest_api_link',
'disable_attachment_pages',
'disable_author_pages',
'disable_comments',
'disable_admin_email_check',
'clean_dashboard',
'smart_notices',
'hide_update_nag',
),
'woocommerce' => array(
'disable_emojis',
'remove_dashicons',
'remove_query_strings',
'remove_dns_prefetch',
'disable_oembed',
'limit_heartbeat',
'hide_login_errors',
'remove_wp_version',
'block_author_enum',
'disable_xmlrpc',
'disable_file_editor',
'disable_app_passwords',
'block_rest_users',
'remove_rsd_link',
'remove_wlw_manifest',
'remove_shortlink',
'remove_rest_api_link',
'disable_attachment_pages',
'disable_admin_email_check',
'clean_dashboard',
'smart_notices',
'hide_update_nag',
'tracking_codes',
'woo_conversion_tracking',
),
'agency' => array(
'disable_emojis',
'remove_dashicons',
'remove_query_strings',
'remove_dns_prefetch',
'disable_oembed',
'disable_self_pingbacks',
'limit_heartbeat',
'hide_login_errors',
'remove_wp_version',
'block_author_enum',
'disable_xmlrpc',
'disable_file_editor',
'disable_app_passwords',
'block_rest_users',
'security_headers',
'remove_rsd_link',
'remove_wlw_manifest',
'remove_shortlink',
'remove_rest_api_link',
'disable_attachment_pages',
'disable_author_pages',
'disable_comments',
'disable_rss',
'disable_admin_email_check',
'clean_dashboard',
'smart_notices',
'hide_update_nag',
'custom_admin_footer',
'hide_help_tab',
'hide_screen_options',
'custom_login_logo',
),
);
}
/**
* Ajax apply preset.
*/
public function ajax_apply_preset(): void {
check_ajax_referer( 'ovkit_toolkit_save', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Forbidden' );
}
$preset = isset( $_POST['preset'] ) ? sanitize_key( wp_unslash( $_POST['preset'] ) ) : '';
// Presets — which features to enable for each site type.
$presets = self::presets();
if ( ! isset( $presets[ $preset ] ) ) {
wp_send_json_error( 'Unknown preset' );
}
$features = OVKIT_Feature_Registry::instance()->get_all();
$old = get_option( OVKIT_OPTION, array() );
$settings = is_array( $old ) ? $old : array();
// Enable preset features, preserving any already-enabled features and their extras.
$enabled = 0;
foreach ( $presets[ $preset ] as $key ) {
if ( isset( $features[ $key ] ) ) {
$settings[ $key ] = 1;
++$enabled;
}
}
ovkit_commit_settings( $old, $settings );
wp_send_json_success(
array(
'preset' => $preset,
'enabled' => $enabled,
)
);
}
// Admin bar.
/**
* Admin bar indicator.
*
* @param mixed $bar Bar.
*/
public function admin_bar_indicator( $bar ): void {
$features = OVKIT_Feature_Registry::instance()->get_all();
$on = 0;
foreach ( $features as $key => $f ) {
if ( ovkit_is_enabled( $key ) ) {
++$on;
}
}
if ( $on > 0 ) {
$bar->add_node(
array(
'id' => 'ovkit',
'title' => '' . (int) $on . ' ' . esc_html__( 'Toolkit', 'ovanap-admin-toolkit' ) . '',
'href' => admin_url( 'admin.php?page=ovkit' ),
)
);
}
}
// SVG helpers.
/**
* Svg.
*
* @param string $name Name.
* @return string
*/
private function svg( string $name ): string {
$path = $this->icons[ $name ] ?? '';
return '';
}
/**
* Svg large.
*
* @param string $name Name.
* @return string
*/
private function svg_large( string $name ): string {
$path = $this->icons[ $name ] ?? '';
return '';
}
/**
* Human-readable relative time: "5 minutes ago", "2 hours ago", or "Apr 11 at 10:00".
*
* @param int $timestamp Timestamp.
*/
private function relative_time( int $timestamp ): string {
if ( $timestamp <= 0 ) {
return '';
}
$diff = time() - $timestamp;
if ( $diff < 60 ) {
return __( 'Just now', 'ovanap-admin-toolkit' );
}
if ( $diff < 3600 ) {
$m = intdiv( $diff, 60 );
/* translators: %d: number of minutes */
return sprintf( _n( '%d minute ago', '%d minutes ago', $m, 'ovanap-admin-toolkit' ), $m );
}
if ( $diff < 86400 ) {
$h = intdiv( $diff, 3600 );
/* translators: %d: number of hours */
return sprintf( _n( '%d hour ago', '%d hours ago', $h, 'ovanap-admin-toolkit' ), $h );
}
return date_i18n( 'M j \a\t H:i', $timestamp );
}
/**
* Risk badge.
*
* @param string $risk Risk.
* @return string
*/
private function risk_badge( string $risk ): string {
$map = array(
'safe' => array( __( 'Safe', 'ovanap-admin-toolkit' ), 'ovkit-risk-safe' ),
'recommended' => array( __( 'Recommended', 'ovanap-admin-toolkit' ), 'ovkit-risk-rec' ),
'advanced' => array( __( 'Advanced', 'ovanap-admin-toolkit' ), 'ovkit-risk-adv' ),
'risky' => array( __( 'Careful', 'ovanap-admin-toolkit' ), 'ovkit-risk-risky' ),
);
$r = $map[ $risk ] ?? $map['safe'];
return '' . $r[0] . '';
}
// Shared data helpers.
/**
* Svg allowlist.
*
* @return array
*/
private function svg_allowlist(): array {
return array(
'svg' => array(
'width' => true,
'height' => true,
'viewbox' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'xmlns' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'fill-rule' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'rx' => true,
),
'line' => array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
),
'polyline' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
),
);
}
/**
* Get scan data.
*
* @return array
*/
private function get_scan_data(): array {
$site_scan = OVKIT_Site_Scan::instance()->get();
$ts = $site_scan['_timestamp'] ?? 0;
$groups = array(
'needs_attention' => array(
'label' => __( 'Needs attention', 'ovanap-admin-toolkit' ),
'items' => array(),
'open' => true,
),
'recommended' => array(
'label' => __( 'Recommended', 'ovanap-admin-toolkit' ),
'items' => array(),
'open' => true,
),
'all_good' => array(
'label' => __( 'All good', 'ovanap-admin-toolkit' ),
'items' => array(),
'open' => false,
),
'not_configured' => array(
'label' => __( 'Not configured', 'ovanap-admin-toolkit' ),
'items' => array(),
'open' => false,
),
);
if ( $ts ) {
foreach ( $site_scan as $key => $item ) {
if ( strpos( $key, '_' ) === 0 || ! is_array( $item ) ) {
continue;
}
$st = $item['status'] ?? 'missing';
if ( 'conflict' === $st ) {
$groups['needs_attention']['items'][] = $item;
} elseif ( 'warning' === $st ) {
$groups['recommended']['items'][] = $item;
} elseif ( 'ok' === $st ) {
$groups['all_good']['items'][] = $item;
} else {
$groups['not_configured']['items'][] = $item;
}
}
}
return array(
'ts' => $ts,
'groups' => $groups,
'conflict_count' => count( $groups['needs_attention']['items'] ),
'warning_count' => count( $groups['recommended']['items'] ),
'ok_count' => count( $groups['all_good']['items'] ),
);
}
/**
* Sorted groups.
*
* @param array $grouped Grouped.
* @param array $scan Scan.
* @return array
*/
private function sorted_groups( array $grouped, array $scan ): array {
$risk_order = array(
'risky' => 0,
'advanced' => 1,
'recommended' => 2,
'safe' => 3,
);
$sorted = array();
foreach ( $grouped as $section => $features ) {
$arr = $features;
usort(
$arr,
function ( $a, $b ) use ( $scan, $risk_order ) {
// Primary: priority field (lowest number first).
$pa = (int) ( $a['priority'] ?? 99 );
$pb = (int) ( $b['priority'] ?? 99 );
if ( $pa !== $pb ) {
return $pa - $pb;
}
// Tiebreaker 1: detected features first.
$det_a = ! empty( $scan[ $a['key'] ]['detected'] ) ? 0 : 1;
$det_b = ! empty( $scan[ $b['key'] ]['detected'] ) ? 0 : 1;
if ( $det_a !== $det_b ) {
return $det_a - $det_b;
}
// Tiebreaker 2: by risk level.
$ra = $risk_order[ $a['risk'] ?? 'safe' ] ?? 3;
$rb = $risk_order[ $b['risk'] ?? 'safe' ] ?? 3;
return $ra - $rb;
}
);
$sorted[ $section ] = $arr;
}
return $sorted;
}
/**
* Audience badge.
*
* @param string $audience Audience.
* @return string
*/
private function audience_badge( string $audience ): string {
if ( 'woocommerce' === $audience ) {
return '' . esc_html__( 'WooCommerce', 'ovanap-admin-toolkit' ) . '';
}
return '';
}
// Shared HTML helpers.
/**
* Render header.
*
* @param bool $show_health_check Show health check.
*/
private function render_header( bool $show_health_check = false ): void {
?>
'check',
'warning' => 'alert',
'conflict' => 'x',
'missing' => 'search',
);
// Build summary counts for collapsed mode.
$conflict_c = count( $groups['needs_attention']['items'] ?? array() );
$warning_c = count( $groups['recommended']['items'] ?? array() );
$ok_c = count( $groups['all_good']['items'] ?? array() );
$sum_parts = array();
if ( $conflict_c ) {
/* translators: %d: number of critical issues */
$sum_parts[] = sprintf( _n( '%d critical', '%d critical', $conflict_c, 'ovanap-admin-toolkit' ), $conflict_c );
}
if ( $warning_c ) {
/* translators: %d: number of recommended items */
$sum_parts[] = sprintf( _n( '%d recommended', '%d recommended', $warning_c, 'ovanap-admin-toolkit' ), $warning_c );
}
if ( $ok_c ) {
/* translators: %d: number of passing items */
$sum_parts[] = sprintf( _n( '%d passing', '%d passing', $ok_c, 'ovanap-admin-toolkit' ), $ok_c );
}
$summary_str = $sum_parts ? implode( ' · ', $sum_parts ) : __( 'No issues found', 'ovanap-admin-toolkit' );
?>
$group ) :
if ( empty( $group['items'] ) ) {
continue;
}
$open = $all_open ? true : $group['open'];
?>
>
svg( $scan_icons[ $st ] ?? 'alert' ), $this->svg_allowlist() ); ?>
svg_allowlist();
$badge_al = array( 'span' => array( 'class' => true ) );
$wc_active = class_exists( 'WooCommerce' );
foreach ( $sorted_groups as $section => $features ) :
$slug = $section_slugs[ $section ] ?? '';
?>
svg( $f['icon'] ), $svg_al ); ?>
risk_badge( $risk ), $badge_al ); ?>
audience_badge( $audience );
if ( $ab ) {
echo wp_kses( $ab, $badge_al );
}
?>
get_scan_data();
$features = OVKIT_Feature_Registry::instance()->get_all();
$settings = get_option( OVKIT_OPTION, array() );
// Match render_features() visibility: WC-only features hidden when WooCommerce is inactive.
$wc_active = class_exists( 'WooCommerce' );
$visible_features = array_filter(
$features,
function ( $f ) use ( $wc_active ) {
$audience = $f['audience'] ?? 'everyone';
return ! ( 'woocommerce' === $audience && ! $wc_active );
}
);
$total_features = count( $visible_features );
// Count enabled the same way render_features() does: setting truthy AND key present in registry.
$enabled_count = count(
array_filter(
$settings,
function ( $value, $key ) use ( $visible_features ) {
return ! empty( $value ) && isset( $visible_features[ $key ] );
},
ARRAY_FILTER_USE_BOTH
)
);
$conflict_count = $scan_data['conflict_count'];
$scan_url = admin_url( 'admin.php?page=ovkit-scan' );
// Tracking status: GTM configured?
$gtm_enabled = ! empty( $settings['track_gtm_enable'] ) && ! empty( trim( (string) ( $settings['track_gtm_id'] ?? '' ) ) );
// Activity status: most recent log entry timestamp (log is stored newest-first).
$log = OVKIT_Activity_Log::get_log();
$last_ts = isset( $log[0]['time'] ) ? (int) $log[0]['time'] : 0;
$svg_al = $this->svg_allowlist();
?>
render_header( true ); ?>
svg_large( 'alert' ), $svg_al ); ?>
0 ) {
$state = 'attention';
/* translators: %d: number of issues */
$msg = sprintf( _n( '%d problem needs your attention', '%d problems need your attention', $conflict_count, 'ovanap-admin-toolkit' ), $conflict_count );
$sub = '';
$link = __( 'See what to do →', 'ovanap-admin-toolkit' );
} elseif ( $warning_count > 0 ) {
$state = 'recommend';
/* translators: %d: number of recommendations */
$msg = sprintf( _n( 'No problems found, %d recommendation', 'No problems found, %d recommendations', $warning_count, 'ovanap-admin-toolkit' ), $warning_count );
$sub = '';
$link = __( 'See recommendations →', 'ovanap-admin-toolkit' );
} else {
$state = 'ok';
$msg = __( 'No problems found in the last site scan', 'ovanap-admin-toolkit' );
$sub = '';
$link = __( 'Scan again →', 'ovanap-admin-toolkit' );
}
if ( $scan_data['ts'] ) {
/* translators: 1: enabled feature count, 2: total feature count, 3: time of the last scan */
$sub = sprintf( __( '%1$d of %2$d switches on · last scan %3$s', 'ovanap-admin-toolkit' ), $enabled_count, $total_features, $this->relative_time( (int) $scan_data['ts'] ) );
}
?>
svg_large( 'ok' === $state ? 'check' : 'alert' ), $svg_al ); ?>
0 ) {
/* translators: %d: number of issues */
$health_status = sprintf( _n( '%d problem', '%d problems', $conflict_count, 'ovanap-admin-toolkit' ), $conflict_count );
} elseif ( $warning_count > 0 ) {
/* translators: %d: number of recommendations */
$health_status = sprintf( _n( '%d recommendation', '%d recommendations', $warning_count, 'ovanap-admin-toolkit' ), $warning_count );
} else {
$health_status = __( 'No problems found', 'ovanap-admin-toolkit' );
}
$activity_status = $last_ts
? sprintf( /* translators: %s: relative time like "2 hours ago" */ __( 'Last change: %s', 'ovanap-admin-toolkit' ), $this->relative_time( $last_ts ) )
: __( 'No changes yet', 'ovanap-admin-toolkit' );
$nav_cards = array(
array(
'url' => admin_url( 'admin.php?page=ovkit-features' ),
'icon' => 'zap',
'title' => __( 'Features', 'ovanap-admin-toolkit' ),
'desc' => __( 'Toggle performance, security, cleanup and more', 'ovanap-admin-toolkit' ),
'status' => $features_status,
),
array(
'url' => admin_url( 'admin.php?page=ovkit-tracking' ),
'icon' => 'chart',
'title' => __( 'Tracking', 'ovanap-admin-toolkit' ),
'desc' => __( 'GTM, GA4, Meta Pixel, TikTok Pixel setup', 'ovanap-admin-toolkit' ),
'status' => $tracking_status,
),
array(
'url' => $scan_url,
'icon' => 'shield',
'title' => __( 'Site Scan', 'ovanap-admin-toolkit' ),
'desc' => __( 'Full environment scan results', 'ovanap-admin-toolkit' ),
'status' => $health_status,
),
array(
'url' => admin_url( 'admin.php?page=ovkit-log' ),
'icon' => 'clock',
'title' => __( 'Activity', 'ovanap-admin-toolkit' ),
'desc' => __( 'Change history and rollback', 'ovanap-admin-toolkit' ),
'status' => $activity_status,
),
);
?>
get_grouped();
$settings = get_option( OVKIT_OPTION, array() );
$scan = OVKIT_Scanner::instance()->get_cached();
$log = OVKIT_Activity_Log::get_formatted();
$section_slugs = array(
'Performance' => 'perf',
'Security' => 'sec',
'Cleanup' => 'clean',
'Admin UI' => 'admin',
'Tracking' => 'track',
'Frontend' => 'front',
'Maintenance' => 'maint',
);
$sorted_groups = $this->sorted_groups( $grouped, $scan );
// Compute tab counts using the same visibility rules as render_feature_rows()
// (WC-only features hidden when WooCommerce is inactive).
$wc_active = class_exists( 'WooCommerce' );
$count_all = 0;
$count_quick = 0;
$count_det = 0;
$count_act = 0;
foreach ( $sorted_groups as $features ) {
foreach ( $features as $f ) {
$audience = $f['audience'] ?? 'everyone';
if ( 'woocommerce' === $audience && ! $wc_active ) {
continue;
}
++$count_all;
$key = $f['key'];
$risk = $f['risk'] ?? 'safe';
$level = $f['level'] ?? 'standard';
if ( ( 'safe' === $risk || 'recommended' === $risk ) && 'beginner' === $level ) {
++$count_quick;
}
if ( ! empty( $scan[ $key ]['detected'] ) ) {
++$count_det;
}
if ( ! empty( $settings[ $key ] ) ) {
++$count_act;
}
}
}
?>
render_header(); ?>
$slug ) : ?>
render_feature_rows( $sorted_groups, $settings, $scan, $section_slugs ); ?>
render_save_footer( count( $log ) ); ?>
get_grouped();
$settings = get_option( OVKIT_OPTION, array() );
$scan = OVKIT_Scanner::instance()->get_cached();
$log = OVKIT_Activity_Log::get_formatted();
$section_slugs = array( 'Tracking' => 'track' );
$tracking_features = $grouped['Tracking'] ?? array();
$sorted_groups = $this->sorted_groups( array( 'Tracking' => $tracking_features ), $scan );
?>
render_header(); ?>
render_feature_rows( $sorted_groups, $settings, $scan, $section_slugs ); ?>
render_save_footer( count( $log ) ); ?>
get_scan_data();
?>
render_header(); ?>
render_scan_panel( $scan_data['groups'], $scan_data['ts'], true ); ?>