'; $title .= 'AllAccessible'; if ($score_label !== null) { $title .= ' ' . esc_html($score_label['text']) . ''; } if ($is_frontend && class_exists('AllAccessible_ContextGuard')) { $skip_reason = AllAccessible_ContextGuard::skip_reason(); if ($skip_reason !== '') { $title .= ' ' . esc_html(sprintf(__('widget not loaded (%s)', 'allaccessible'), $skip_reason)) . ''; } } $bar->add_node(array( 'id' => 'allaccessible', 'title' => $title, 'href' => admin_url('admin.php?page=allaccessible'), 'meta' => array('class' => 'aacb-admin-bar-root'), )); // Frontend-only: per-page actions. if ($is_frontend && $current_url !== '') { $bar->add_node(array( 'id' => 'allaccessible-scan-this', 'parent' => 'allaccessible', 'title' => '' . esc_html__('Scan this page now', 'allaccessible'), 'href' => '#', 'meta' => array( 'class' => 'aacb-bar-scan-trigger', 'onclick' => 'return false;', ), )); $report_url = self::page_report_url($current_url); if ($report_url !== null) { $bar->add_node(array( 'id' => 'allaccessible-page-report', 'parent' => 'allaccessible', 'title' => esc_html__('View page report', 'allaccessible'), 'href' => esc_url($report_url), 'meta' => array('target' => '_blank', 'rel' => 'noopener'), )); } // Separator $bar->add_group(array( 'id' => 'allaccessible-frontend-divider', 'parent' => 'allaccessible', 'meta' => array('class' => 'ab-sub-secondary'), )); } // Always-visible plugin shortcuts. $shortcuts_parent = $is_frontend ? 'allaccessible-frontend-divider' : 'allaccessible'; $bar->add_node(array( 'id' => 'allaccessible-dashboard', 'parent' => $shortcuts_parent, 'title' => esc_html__('Dashboard', 'allaccessible'), 'href' => admin_url('admin.php?page=allaccessible'), )); $bar->add_node(array( 'id' => 'allaccessible-fixes', 'parent' => $shortcuts_parent, 'title' => esc_html__('Audits & Fixes', 'allaccessible'), 'href' => admin_url('admin.php?page=aacb-agentic-fixes'), )); $bar->add_node(array( 'id' => 'allaccessible-settings', 'parent' => $shortcuts_parent, 'title' => esc_html__('Settings', 'allaccessible'), 'href' => admin_url('admin.php?page=allaccessible-account'), )); $bar->add_node(array( 'id' => 'allaccessible-open-app', 'parent' => $shortcuts_parent, 'title' => esc_html__('Open dashboard', 'allaccessible'), 'href' => function_exists('aacb_app_url') ? aacb_app_url() : 'https://app.allaccessible.org/', 'meta' => array('target' => '_blank', 'rel' => 'noopener'), )); } /** * Inline CSS + JS for the toolbar */ public static function enqueue_assets() { if (!is_admin_bar_showing() || !current_user_can('manage_options')) { return; } $is_frontend = !is_admin(); $page_url = $is_frontend ? self::current_frontend_url() : ''; $can_scan = ($is_frontend && $page_url !== ''); $icon_uri = self::ICON_DATA_URI; $scan_icon_uri = "data:image/svg+xml;utf8," . rawurlencode( '' . '' . '' ); $css = <<.ab-item{opacity:.6;pointer-events:none} #wpadminbar .aacb-bar-scan-trigger.is-busy .aacb-bar-scan-icon{animation:aacb-spin 1s linear infinite} @keyframes aacb-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}} CSS; wp_register_style('aacb-admin-bar', false, array(), AACB_VERSION); wp_enqueue_style('aacb-admin-bar'); wp_add_inline_style('aacb-admin-bar', $css); if (!$can_scan) { return; } // JS — bind click on the scan trigger, POST to admin-ajax, toast. $handle = 'aacb-admin-bar-js'; wp_register_script($handle, false, array(), AACB_VERSION, true); wp_enqueue_script($handle); wp_localize_script($handle, 'AACB_AdminBar', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'scanNonce' => wp_create_nonce('aacb_scan_this_page'), 'statusNonce' => wp_create_nonce('aacb_scan_status'), 'scoreNonce' => wp_create_nonce('aacb_page_score'), 'pageUrl' => $page_url, 'pollIntervalMs' => 5000, 'pollMaxMs' => 180000, 'i18n' => array( 'running' => __('Scanning page…', 'allaccessible'), 'done' => __('Scan complete. Score: %d%%', 'allaccessible'), 'doneNoScore' => __('Scan complete. Refresh to see updated score.', 'allaccessible'), 'failed' => __('Scan failed. Try again or check the dashboard.', 'allaccessible'), 'timeout' => __('Scan is still running — check back in a minute.', 'allaccessible'), 'error' => __('Could not start scan: ', 'allaccessible'), 'rate' => __('Scan rate limit reached. Try again later.', 'allaccessible'), ), )); $js = << AACB_AdminBar.pollMaxMs) { clearInterval(timer); if (trigger) trigger.classList.remove('is-busy'); aacbToastUpdate(toast, AACB_AdminBar.i18n.timeout, 'error', false); return; } var fd = new FormData(); fd.append('action', 'aacb_scan_status'); fd.append('_ajax_nonce', AACB_AdminBar.statusNonce); fd.append('job_id', jobId); fd.append('page_url', AACB_AdminBar.pageUrl); fetch(AACB_AdminBar.ajaxUrl, {method:'POST', credentials:'same-origin', body:fd}) .then(function(r){ return r.json().catch(function(){ return null; }); }) .then(function(j){ if (!j || !j.success || !j.data) return; // transient error — keep polling var status = j.data.status; var score = j.data.score; if (status === 'done') { clearInterval(timer); if (trigger) trigger.classList.remove('is-busy'); updateBarBadge(score); document.dispatchEvent(new CustomEvent('aacb:page-score-refresh', { detail: { pageUrl: AACB_AdminBar.pageUrl, score: score } })); var msg = (typeof score === 'number') ? AACB_AdminBar.i18n.done.replace('%d', score) : AACB_AdminBar.i18n.doneNoScore; aacbToastUpdate(toast, msg, 'success', false); } else if (status === 'failed') { clearInterval(timer); if (trigger) trigger.classList.remove('is-busy'); aacbToastUpdate(toast, AACB_AdminBar.i18n.failed, 'error', false); } else { // running / queued — keep toast in progress state aacbToastUpdate(toast, AACB_AdminBar.i18n.running, 'progress', true); } }) .catch(function(){ /* transient — keep polling */ }); }, AACB_AdminBar.pollIntervalMs); } function updateBarBadge(score){ if (typeof score !== 'number') return; var root = document.getElementById('wp-admin-bar-allaccessible'); if (!root) return; var chip = root.querySelector('.aacb-bar-score'); var tier = score >= 90 ? 'good' : (score >= 70 ? 'warn' : 'bad'); if (chip) { chip.textContent = score + '%'; chip.className = 'aacb-bar-score aacb-bar-score--' + tier; } else { var label = root.querySelector('.ab-item'); if (label) { var span = document.createElement('span'); span.className = 'aacb-bar-score aacb-bar-score--' + tier; span.textContent = score + '%'; label.appendChild(document.createTextNode(' ')); label.appendChild(span); } } } function lazyFetchScore(){ var root = document.getElementById('wp-admin-bar-allaccessible'); if (!root) return; // If chip is already painted (admin context with cached aggregation), // no fetch needed. if (root.querySelector('.aacb-bar-score')) return; if (!AACB_AdminBar.pageUrl || !AACB_AdminBar.scoreNonce) return; var fd = new FormData(); fd.append('action', 'aacb_page_score'); fd.append('_ajax_nonce', AACB_AdminBar.scoreNonce); fd.append('page_url', AACB_AdminBar.pageUrl); fetch(AACB_AdminBar.ajaxUrl, {method:'POST', credentials:'same-origin', body:fd}) .then(function(r){ return r.json().catch(function(){ return null; }); }) .then(function(j){ if (j && j.success && j.data && typeof j.data.score === 'number') { updateBarBadge(j.data.score); } // Silent on no-score — admin bar just stays brand-only. }) .catch(function(){ /* silent */ }); } function aacbToast(message, kind, sticky){ var t = document.createElement('div'); t.className = 'aacb-toast aacb-toast--' + kind; t.textContent = message; var bg = (kind === 'success') ? '#0f9d58' : (kind === 'error' ? '#d93025' : '#7c3aed'); t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:100000;padding:12px 20px;border-radius:6px;font:14px/1.4 system-ui,sans-serif;color:#fff;box-shadow:0 6px 24px rgba(0,0,0,.18);transition:opacity .4s;background:'+bg; document.body.appendChild(t); if (!sticky) scheduleDismiss(t); return t; } function aacbToastUpdate(t, message, kind, sticky){ if (!t) return aacbToast(message, kind, sticky); t.textContent = message; t.className = 'aacb-toast aacb-toast--' + kind; var bg = (kind === 'success') ? '#0f9d58' : (kind === 'error' ? '#d93025' : '#7c3aed'); t.style.background = bg; if (!sticky) scheduleDismiss(t); } function scheduleDismiss(t){ setTimeout(function(){ t.style.opacity = '0'; }, 4000); setTimeout(function(){ if (t.parentNode) t.parentNode.removeChild(t); }, 4600); } })(); JS; wp_add_inline_script($handle, $js); } /** * AJAX entry — kicks off a single-page scan via ApiClient. */ public static function ajax_scan_this_page() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'allaccessible'), 'code' => 403), 403); } check_ajax_referer('aacb_scan_this_page'); $page_url = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : ''; if (empty($page_url)) { wp_send_json_error(array('message' => __('Missing page URL.', 'allaccessible'), 'code' => 400), 400); } $client = AllAccessible_ApiClient::get_instance(); $result = $client->trigger_page_scan($page_url, 'wp_admin_bar'); AllAccessible_Debug::api('AdminBar::ajax_scan_this_page', array('page_url' => $page_url), $result); if (is_wp_error($result)) { $data = $result->get_error_data(); $status = is_array($data) && isset($data['status']) ? (int) $data['status'] : 500; wp_send_json_error(array( 'message' => $result->get_error_message(), 'code' => $status, ), $status); } wp_send_json_success(array( 'jobId' => isset($result['jobId']) ? (int) $result['jobId'] : null, 'pageUrl' => $page_url, )); } /** * AJAX poll handler */ public static function ajax_scan_status() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'allaccessible'), 'code' => 403), 403); } check_ajax_referer('aacb_scan_status'); $job_id = isset($_POST['job_id']) ? (int) $_POST['job_id'] : 0; $page_url = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : ''; if ($job_id <= 0) { wp_send_json_error(array('message' => __('Missing job ID.', 'allaccessible'), 'code' => 400), 400); } $client = AllAccessible_ApiClient::get_instance(); $result = $client->get_scan_status($job_id); if (is_wp_error($result)) { $data = $result->get_error_data(); $status = is_array($data) && isset($data['status']) ? (int) $data['status'] : 500; wp_send_json_error(array( 'message' => $result->get_error_message(), 'code' => $status, ), $status); } $job_status = isset($result['job']['status']) ? (string) $result['job']['status'] : 'unknown'; $pages_done = isset($result['job']['pagesDone']) ? (int) $result['job']['pagesDone'] : 0; $pages_failed = isset($result['job']['pagesFailed']) ? (int) $result['job']['pagesFailed'] : 0; $total_pages = isset($result['job']['totalPages']) ? (int) $result['job']['totalPages'] : 0; $score = null; if ($job_status === 'done' && $page_url !== '') { $client->bust_page_caches($page_url); $fresh = $client->get_page_audit(null, $page_url); if (!is_wp_error($fresh) && is_array($fresh)) { if (isset($fresh['overall_score']) && is_numeric($fresh['overall_score'])) { $score = (int) $fresh['overall_score']; } elseif (isset($fresh['score_breakdown']['current']) && is_numeric($fresh['score_breakdown']['current'])) { $score = (int) $fresh['score_breakdown']['current']; } } } if (is_numeric($score) && class_exists('AllAccessible_ReviewNudge')) { AllAccessible_ReviewNudge::mark_moment('first_scored_scan'); } wp_send_json_success(array( 'status' => $job_status, 'pagesDone' => $pages_done, 'pagesFailed' => $pages_failed, 'totalPages' => $total_pages, 'score' => $score, )); } /** * AJAX entry — return the current per-page score for the admin bar. */ public static function ajax_page_score() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Permission denied.', 'code' => 403), 403); } check_ajax_referer('aacb_page_score'); $page_url = isset($_POST['page_url']) ? esc_url_raw(wp_unslash($_POST['page_url'])) : ''; if (empty($page_url)) { wp_send_json_error(array('message' => 'Missing page URL.', 'code' => 400), 400); } $client = AllAccessible_ApiClient::get_instance(); $audit = $client->get_page_audit(null, $page_url); AllAccessible_Debug::api('AdminBar::ajax_page_score', array('page_url' => $page_url), $audit); if (is_wp_error($audit) || !is_array($audit)) { wp_send_json_success(array('score' => null)); } $score = null; if (isset($audit['overall_score']) && is_numeric($audit['overall_score'])) { $score = (int) $audit['overall_score']; } elseif (isset($audit['score_breakdown']['current']) && is_numeric($audit['score_breakdown']['current'])) { $score = (int) $audit['score_breakdown']['current']; } wp_send_json_success(array('score' => $score)); } /* ─── helpers ───────────────────────────────────────────────────── */ /** * Resolve the URL the user is currently viewing on the frontend. */ private static function current_frontend_url() { if (is_singular()) { $id = get_queried_object_id(); if ($id) { $permalink = get_permalink($id); if ($permalink) return $permalink; } } if (is_front_page() || is_home()) { return home_url('/'); } return ''; } /** * Pull a score from a transient if one was warmed by another UI * surface. */ private static function cached_score_label_for_context($is_frontend, $current_url) { $score = null; if ($is_frontend && $current_url !== '') { $key = self::PAGE_SCORE_TRANSIENT_PREFIX . md5($current_url); $cached = get_transient($key); if (is_numeric($cached)) { $score = (int) $cached; } } else { $agg = get_transient('aacb_cache_audit_aggregation_v1'); if (is_array($agg) && isset($agg['overall_score']) && is_numeric($agg['overall_score'])) { $score = (int) $agg['overall_score']; } } if ($score === null) { return null; } $tier = $score >= 90 ? 'good' : ($score >= 70 ? 'warn' : 'bad'); return array('text' => $score . '%', 'tier' => $tier); } /** * Build a "View page report" deep-link for the current frontend URL. */ private static function page_report_url($page_url) { $key = self::PAGE_AUDIT_META_TRANSIENT_PREFIX . md5($page_url); $meta = get_transient($key); if (is_array($meta) && !empty($meta['audit_id']) && !empty($meta['subdomain_id'])) { return sprintf( 'https://app.allaccessible.org/dashboard/audits/%d/audit/%d', (int) $meta['subdomain_id'], (int) $meta['audit_id'] ); } return null; } }