prefix . 'abds_seo_page_scores'; $scans_table = $wpdb->prefix . 'abds_seo_scans'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $page_scores_table/$scans_table are built only from $wpdb->prefix plus a hardcoded literal, never user input, so string interpolation is safe here ($wpdb->prepare() cannot parameterize identifiers anyway). This dashboard is an admin-only screen loaded rarely (one pageview per visit), so the summary aggregates are recomputed on each load rather than cached — the query cost is negligible next to the page's own render cost. $avg_technical = $wpdb->get_var( "SELECT AVG(technical_score) FROM {$page_scores_table}" ); $avg_content = $wpdb->get_var( "SELECT AVG(content_score) FROM {$page_scores_table}" ); $last_scan = $wpdb->get_row( "SELECT * FROM {$scans_table} ORDER BY id DESC LIMIT 1" ); $abds_seo_scanned_pages = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$page_scores_table}" ); // phpcs:enable // Third design pass: "N pages analyzed" is the current total of distinct // scanned pages (page_scores is unique-keyed per post_id, so COUNT(*) // already reflects re-scans in place), not $last_scan->pages_scanned, // which is that one run's own progress counter and can lag behind the // true distinct total between scans. $abds_seo_last_audit_date = null; if ( $last_scan && $last_scan->finished_at ) { $abds_seo_last_audit_date = date_i18n( get_option( 'date_format' ), strtotime( $last_scan->finished_at ) ); } elseif ( $last_scan && $last_scan->started_at ) { $abds_seo_last_audit_date = date_i18n( get_option( 'date_format' ), strtotime( $last_scan->started_at ) ); } $abds_seo_noindex_count = ABDS_SEO_Noindex_Count::noindexed_post_count(); $abds_seo_sitemap = ( new ABDS_SEO_Sitemap_Controller() )->status(); // Ranked by ABDS_SEO_Priority_Service, not by raw score: a broken page // nobody visits matters less than a slightly-off page carrying real // traffic. Passing false forbids an inline GSC call — a cache miss // queues a background refresh instead of stalling this page load. // limit=0 (all) costs the same as any other limit — ranking trims the // already-fully-fetched candidate list, it doesn't re-query per limit — // so this reuses one call for both the "N issues across M pages" // summary and the top-10 table below. $attention = ( new ABDS_SEO_Priority_Service() )->get_attention_list( 0, false ); $abds_seo_issue_count = count( $attention['items'] ); $abds_seo_table_items = array_slice( $attention['items'], 0, 10 ); $abds_seo_license_status = ( new ABDS_SEO_License_Store() )->get_status(); $abds_seo_license_state = (string) ( $abds_seo_license_status['status'] ?? 'none' ); $abds_seo_license_labels = array( 'active' => __( 'License active', 'abds-seo' ), 'expired' => __( 'License expired', 'abds-seo' ), 'revoked' => __( 'License revoked', 'abds-seo' ), 'not_found' => __( 'License not found', 'abds-seo' ), 'site_limit_reached' => __( 'Site limit reached', 'abds-seo' ), 'none' => __( 'License not activated', 'abds-seo' ), ); $abds_seo_license_label = $abds_seo_license_labels[ $abds_seo_license_state ] ?? $abds_seo_license_labels['none']; $abds_seo_settings_url = admin_url( 'admin.php?page=abds-seo-settings' ); // Getting-started checklist (Yoast-pattern): four real, objectively // measurable milestones — no separate "completed" flag to maintain, // each step just reads data this plugin already has. Kept honest: // license activation is real progress toward full value, same as // Yoast's own checklist includes a Premium step, but it's one of // four, not a gate on the other three. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- same admin-only, once-per-pageview rationale as the avg_technical/avg_content queries above; $wpdb->postmeta is core, every value passed through prepare(). $abds_seo_has_keyword = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != ''", '_abds_seo_focus_keyword' ) ) > 0; $abds_seo_has_title_or_desc = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key IN (%s, %s) AND meta_value != ''", '_abds_seo_title', '_abds_seo_description' ) ) > 0; // phpcs:enable $abds_seo_checklist = array( array( 'done' => (bool) $last_scan, 'label' => __( 'Run your first site audit', 'abds-seo' ), 'url' => '#abds-seo-first-scan', ), array( 'done' => $abds_seo_has_keyword, 'label' => __( 'Set a focus keyword on at least one page', 'abds-seo' ), 'url' => admin_url( 'edit.php' ), ), array( 'done' => $abds_seo_has_title_or_desc, 'label' => __( 'Write an SEO title or meta description', 'abds-seo' ), 'url' => admin_url( 'edit.php' ), ), array( 'done' => 'active' === $abds_seo_license_state, 'label' => __( 'Activate your license to unlock Pro features', 'abds-seo' ), 'url' => $abds_seo_settings_url, ), ); $abds_seo_checklist_done = count( array_filter( wp_list_pluck( $abds_seo_checklist, 'done' ) ) ); $abds_seo_checklist_total = count( $abds_seo_checklist ); // The "finishing onboarding" achievement moment (Task B/C wow layer, // ABDS-SEO-TZ-SCORE-WOW-093.md, owner-approved Mockup B as-is): the // checklist card normally disappears the moment it's done (see the // visibility condition below), which would mean nobody ever actually // SEES it finish. This renders it one last time, fully checked off, // with a one-time confetti burst — then flips a site option so it // never shows the completed state again. A real server-tracked flag // (not localStorage, unlike the per-post score-ring celebration in // sidebar.js/classic-meta-box.js) because this is a single site-wide // milestone, not something that varies per post or per browser. $abds_seo_onboarding_just_completed = false; if ( $abds_seo_checklist_done === $abds_seo_checklist_total && ! get_option( 'abds_seo_onboarding_celebrated' ) ) { $abds_seo_onboarding_just_completed = true; update_option( 'abds_seo_onboarding_celebrated', true ); } // The overall score is the focus of the dashboard — the average of the // technical and content scores when both exist, otherwise whichever is // present. Its band drives the substats' colouring; the ring's own // gradient stroke is deliberately fixed poor->ok->good regardless of // band (same choice src/sidebar.js's ScoreRing makes — see that // component's own docblock), so it reads as a living dial, not a flat // per-band colour swap. $abds_seo_tech = ( null !== $avg_technical ) ? (int) round( (float) $avg_technical ) : null; $abds_seo_content = ( null !== $avg_content ) ? (int) round( (float) $avg_content ) : null; if ( null !== $abds_seo_tech && null !== $abds_seo_content ) { $abds_seo_overall = (int) round( ( $abds_seo_tech + $abds_seo_content ) / 2 ); } else { $abds_seo_overall = $abds_seo_tech ?? $abds_seo_content; } $abds_seo_band = 'na'; if ( null !== $abds_seo_overall ) { $abds_seo_band = $abds_seo_overall >= 80 ? 'good' : ( $abds_seo_overall >= 50 ? 'ok' : 'poor' ); } $abds_seo_content_band = 'na'; if ( null !== $abds_seo_content ) { $abds_seo_content_band = $abds_seo_content >= 80 ? 'good' : ( $abds_seo_content >= 50 ? 'ok' : 'poor' ); } $abds_seo_hero_radius = 52; $abds_seo_hero_circumference = 2 * M_PI * $abds_seo_hero_radius; $abds_seo_hero_offset = null !== $abds_seo_overall ? $abds_seo_hero_circumference * ( 1 - $abds_seo_overall / 100 ) : $abds_seo_hero_circumference; ?>