isset($s['misconceptions_open']) && is_numeric($s['misconceptions_open']) ? (int) $s['misconceptions_open'] : null, 'last_audit_score' => isset($s['last_audit_score']) && is_numeric($s['last_audit_score']) ? (int) $s['last_audit_score'] : null, 'last_audit_at' => isset($s['last_audit_at']) ? sanitize_text_field((string) $s['last_audit_at']) : '', 'citations' => isset($s['citations']) && is_numeric($s['citations']) ? (int) $s['citations'] : null, 'prompts' => isset($s['prompts']) && is_numeric($s['prompts']) ? (int) $s['prompts'] : null, 'received_at' => time(), ); update_option(CITAMI_SITE_SUMMARY_OPT, $clean, false); }); function citami_site_summary() { $s = get_option(CITAMI_SITE_SUMMARY_OPT, array()); return is_array($s) ? $s : array(); } /** AI crawler activity in the last N days: hits, pages read (path → hits, bots), bots seen. */ function citami_widget_ai_activity($days = CITAMI_WIDGET_DAYS) { $log = get_option(CITAMI_BOTS_LOG_OPT, array()); $since = time() - $days * DAY_IN_SECONDS; $skip = citami_widget_search_only_bots(); $pages = array(); $bots = array(); $hits = 0; foreach ((array) $log as $e) { if (!is_array($e) || (int) $e['ts'] < $since || in_array($e['bot'], $skip, true)) { continue; } if (isset($e['verified']) && $e['verified'] === false) { continue; // spoofed } $hits++; $path = citami_redirect_normalise_path(isset($e['path']) ? (string) $e['path'] : '/'); if (!isset($pages[$path])) { $pages[$path] = array('path' => $path, 'hits' => 0, 'bots' => array(), 'last' => 0); } $pages[$path]['hits']++; $pages[$path]['bots'][$e['bot']] = true; $pages[$path]['last'] = max($pages[$path]['last'], (int) $e['ts']); $bots[$e['bot']] = isset($bots[$e['bot']]) ? $bots[$e['bot']] + 1 : 1; } uasort($pages, function ($a, $b) { return $b['hits'] <=> $a['hits'] ?: $b['last'] <=> $a['last']; }); foreach ($pages as &$p) { $p['bots'] = array_keys($p['bots']); } unset($p); arsort($bots); return array('days' => $days, 'hits' => $hits, 'pages' => $pages, 'bots' => $bots); } /** Published pages and posts nobody from the AI catalogue has read: the ones with most internal links first. */ function citami_widget_unread_pages(array $read_paths, $limit = 5) { if (!function_exists('citami_links_index')) { return array('total' => 0, 'unread' => 0, 'sample' => array()); } $idx = citami_links_index(); $entries = isset($idx['entries']) && is_array($idx['entries']) ? $idx['entries'] : array(); $inbound = function_exists('citami_links_inbound_counts') ? citami_links_inbound_counts($idx) : array(); $read = array(); foreach ($read_paths as $p) { $read[rtrim($p, '/')] = true; } $unread = array(); foreach ($entries as $e) { if (empty($e['url']) || !empty($e['noindex'])) continue; $path = rtrim((string) wp_parse_url((string) $e['url'], PHP_URL_PATH), '/'); if (isset($read[$path])) continue; $unread[] = array('id' => (int) $e['id'], 'title' => (string) $e['title'], 'url' => (string) $e['url'], 'inbound' => isset($inbound[$e['id']]) ? (int) $inbound[$e['id']] : 0); } usort($unread, function ($a, $b) { return $b['inbound'] <=> $a['inbound'] ?: $a['title'] <=> $b['title']; }); return array('total' => count($entries), 'unread' => count($unread), 'sample' => array_slice($unread, 0, $limit)); } /** Title for a path from the links index, or the path itself. */ function citami_widget_title_for_path($path) { if (function_exists('citami_links_index')) { $idx = citami_links_index(); foreach ((array) (isset($idx['entries']) ? $idx['entries'] : array()) as $e) { if (!empty($e['url']) && rtrim((string) wp_parse_url((string) $e['url'], PHP_URL_PATH), '/') === rtrim($path, '/')) { return (string) $e['title']; } } } return $path === '' ? '/' : $path; } /** Everything the widget shows, in one array (also handy for tests). */ function citami_widget_data() { $activity = citami_widget_ai_activity(); $unread = citami_widget_unread_pages(array_keys($activity['pages'])); $hall = function_exists('citami_404_ai_entries') ? citami_404_ai_entries(5) : array(); $hall_all = function_exists('citami_404_ai_entries') ? count(citami_404_ai_entries(1000)) : 0; $health = function_exists('citami_content_health_snapshot') ? citami_content_health_snapshot() : array(); $truth = isset($health['truth']) && is_array($health['truth']) ? $health['truth'] : array(); $aeo = isset($health['aeo']) && is_array($health['aeo']) ? $health['aeo'] : array(); $summary = citami_site_summary(); return array( 'activity' => $activity, 'unread' => $unread, 'hallucinated' => array('count' => $hall_all, 'list' => $hall), 'aeo_avg' => isset($aeo['avg']) ? $aeo['avg'] : null, 'aeo_scored' => isset($aeo['scored']) ? (int) $aeo['scored'] : 0, 'ai_info' => function_exists('citami_truth_ai_info_enabled') && citami_truth_ai_info_enabled() ? citami_truth_ai_info_url() : '', 'has_sheet' => !empty($truth['has_sheet']), 'truth_errors' => isset($truth['errors']) ? (int) $truth['errors'] : 0, 'about' => isset($truth['about']) && is_array($truth['about']) ? $truth['about'] : null, 'summary' => $summary, 'connected' => citami_get_api_key() !== '', ); } function citami_widget_bot_label($slug) { $cat = function_exists('citami_bots_catalog') ? citami_bots_catalog() : array(); return isset($cat[$slug]['label']) ? $cat[$slug]['label'] : $slug; } function citami_render_ai_widget() { $d = citami_widget_data(); $tab = function ($t) { return admin_url('admin.php?page=citami-connector&tab=' . $t); }; $a = $d['activity']; $s = $d['summary']; $mis = isset($s['misconceptions_open']) && $s['misconceptions_open'] !== null ? (int) $s['misconceptions_open'] : null; // 2.60 — every KPI opens the list behind it, same targets as the Overview KPIs. $overview = function_exists('citami_admin_url') ? citami_admin_url('overview') : admin_url('admin.php?page=citami-connector&tab=overview'); $site_base = function_exists('citami_brand_site') ? citami_brand_site() : citami_get_endpoint(); $mis_href = $mis !== null ? citami_lang_url($site_base . '/dashboard/brand') : $tab('brand'); ?>