<?php
/**
 * AVI Control Center — admin : Strategic Knowledge (Qdrant modules experts) + Chat Monitor (FastAPI / SQL).
 *
 * @package Avision
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Page d'administration reliée au backend FastAPI (/admin/*) avec X-Admin-API-Key.
 */
class Avision_Control_Center {

	/** Slug menu admin (éviter tab=chat : collision possible avec d’autres extensions → onglet monitor). */
	const PAGE_SLUG = 'avi-avi-control-center';

	/** Préfixe commun des transients cache Qdrant (liste scroll / pages) — purge groupée via « Vider le cache ». */
	const QDRANT_CACHE_PREFIX = 'avision_qdrant_';

	/**
	 * Supprime toutes les options transient dont le nom (sans préfixe WP) commence par avision_qdrant_.
	 *
	 * @return int Nombre de lignes supprimées (approximatif : transient + timeout).
	 */
	private static function delete_transients_prefixed_avision_qdrant() {
		global $wpdb;
		if ( ! isset( $wpdb->options ) ) {
			return 0;
		}
		$base = self::QDRANT_CACHE_PREFIX;
		$like_transient = $wpdb->esc_like( '_transient_' . $base ) . '%';
		$like_timeout   = $wpdb->esc_like( '_transient_timeout_' . $base ) . '%';
		$n1             = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_transient ) );
		$n2             = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_timeout ) );
		return $n1 + $n2;
	}

	/**
	 * GET /admin/qdrant/points avec cache transient (clé avision_qdrant_points_{md5 query}).
	 *
	 * @param array<string, mixed> $query limit, offset, …
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function api_get_qdrant_points_cached( array $query ) {
		if ( ! function_exists( 'get_transient' ) || ! function_exists( 'set_transient' ) ) {
			return self::api_get( '/admin/admin/qdrant/points', $query );
		}
		$q = $query;
		ksort( $q );
		$tkey = self::QDRANT_CACHE_PREFIX . 'points_' . md5( wp_json_encode( $q, JSON_UNESCAPED_UNICODE ) );
		$cached = get_transient( $tkey );
		if ( is_array( $cached ) && array_key_exists( 'ok', $cached ) && array_key_exists( 'code', $cached ) ) {
			return $cached;
		}
		$res = self::api_get( '/admin/admin/qdrant/points', $query );
		if ( $res['ok'] ) {
			set_transient( $tkey, $res, 2 * MINUTE_IN_SECONDS );
		}
		return $res;
	}

	/**
	 * @return void
	 */
	public static function init() {
		add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) );
		add_action( 'wp_ajax_avision_cc_brain_tool', array( __CLASS__, 'ajax_run_brain_tool' ) );
		add_action( 'wp_ajax_avision_cc_qdrant_filter', array( __CLASS__, 'ajax_qdrant_filter' ) );
		add_action( 'wp_ajax_avision_cc_qdrant_search_chunks', array( __CLASS__, 'ajax_qdrant_search_chunks' ) );
		add_action( 'wp_ajax_avision_cc_qdrant_get_point', array( __CLASS__, 'ajax_qdrant_get_point' ) );
		add_action( 'wp_ajax_avision_cc_knowledge_preview_simulate', array( __CLASS__, 'ajax_knowledge_preview_simulate' ) );
		add_action( 'wp_ajax_avision_cc_purge_cache', array( __CLASS__, 'ajax_purge_litespeed_cache' ) );
		add_action( 'wp_ajax_avision_cc_dashboard_refresh', array( __CLASS__, 'ajax_dashboard_refresh' ) );
		add_action( 'wp_ajax_avision_cc_qi_run_baseline', array( __CLASS__, 'ajax_qi_run_baseline' ) );
		add_action( 'wp_ajax_avision_cc_qi_save_baseline', array( __CLASS__, 'ajax_qi_save_baseline' ) );
		add_action( 'wp_ajax_avision_qi_run_full', array( __CLASS__, 'ajax_qi_run_full' ) );
		add_action( 'wp_ajax_avision_lqa_run', array( __CLASS__, 'ajax_lqa_run' ) );
		add_action( 'wp_ajax_avision_cc_watcher_run', array( __CLASS__, 'ajax_watcher_run' ) );
		add_action( 'wp_ajax_avision_cc_watcher_log', array( __CLASS__, 'ajax_watcher_log' ) );
		add_action( 'wp_ajax_avision_hc_run_full', array( 'Avision_Health_Check', 'ajax_run_full_check' ) );
		add_action( 'wp_ajax_avision_hc_run_fast', array( 'Avision_Health_Check', 'ajax_run_fast_check' ) );
		add_action( 'wp_ajax_avision_smoke_run', array( 'Avision_Health_Check', 'ajax_run_smoke_test' ) );
		add_action( 'wp_ajax_avision_run_code_health_check', array( __CLASS__, 'ajax_run_code_health_check' ) );
		add_action( 'wp_ajax_avision_integration_tests_run', array( __CLASS__, 'ajax_integration_tests_run' ) );
		add_action( 'wp_ajax_avision_rollback_file', array( __CLASS__, 'ajax_rollback_file' ) );
		add_action( 'wp_ajax_avision_cron_guard_status', array( __CLASS__, 'ajax_cron_guard_status' ) );
		add_action( 'wp_ajax_avision_cron_guard_install', array( __CLASS__, 'ajax_cron_guard_install' ) );
		add_action( 'wp_ajax_avision_cc_get_file_info', array( __CLASS__, 'ajax_cc_get_file_info' ) );
		add_action( 'wp_ajax_avision_cc_save_code_guard_alerts', array( __CLASS__, 'ajax_cc_save_code_guard_alerts' ) );
		add_action( 'wp_ajax_avision_cc_test_code_guard_alert', array( __CLASS__, 'ajax_cc_test_code_guard_alert' ) );
		add_action( 'wp_ajax_avision_security_run_scan', array( __CLASS__, 'ajax_security_run_scan' ) );
		add_action( 'wp_ajax_avision_security_save_alerts', array( __CLASS__, 'ajax_security_save_alerts' ) );
		add_action( 'wp_ajax_avision_security_save_settings', array( __CLASS__, 'ajax_security_save_settings' ) );
		add_action( 'wp_ajax_avision_security_test_alert', array( __CLASS__, 'ajax_security_test_alert' ) );
		add_action( 'wp_ajax_avision_api_test_all', array( __CLASS__, 'ajax_api_test_all' ) );
		add_action( 'wp_ajax_avision_api_test_alert', array( __CLASS__, 'ajax_api_test_alert' ) );
		add_action( 'wp_ajax_avision_api_check_onboarding', array( __CLASS__, 'ajax_api_check_onboarding' ) );
		add_action( 'wp_ajax_avision_api_force_cron', array( __CLASS__, 'ajax_api_force_cron' ) );
		add_action( 'wp_ajax_avision_api_live_test', array( __CLASS__, 'ajax_api_live_test' ) );
		add_action( 'wp_ajax_avision_fleet_debug_run', array( __CLASS__, 'ajax_fleet_debug_run' ) );
	}

	/**
	 * @return void
	 */
	public static function register_menu() {
		add_menu_page(
			'AVI Control Center',
			'AVI Control Center',
			'manage_options',
			self::PAGE_SLUG,
			array( __CLASS__, 'render' ),
			'dashicons-superhero',
			58
		);
	}

	/**
	 * URL de base FastAPI (sans slash final).
	 *
	 * @return string
	 */
	private static function backend_base_url() {
		if ( defined( 'AVISION_BACKEND_INTERNAL_URL' ) && is_string( AVISION_BACKEND_INTERNAL_URL ) && AVISION_BACKEND_INTERNAL_URL !== '' ) {
			return rtrim( AVISION_BACKEND_INTERNAL_URL, '/' );
		}
		$opt = function_exists( 'get_option' ) ? get_option( 'avision_fastapi_base_url', '' ) : '';
		$opt = is_string( $opt ) ? trim( $opt ) : '';
		if ( $opt !== '' ) {
			return rtrim( $opt, '/' );
		}
		return 'http://127.0.0.1:8001';
	}

	/**
	 * Clé secrète alignée sur ADMIN_API_KEY du backend.
	 *
	 * @return string
	 */
	private static function admin_api_key() {
		if ( defined( 'AVISION_ADMIN_API_KEY' ) && is_string( AVISION_ADMIN_API_KEY ) && AVISION_ADMIN_API_KEY !== '' ) {
			return (string) AVISION_ADMIN_API_KEY;
		}
		$opt = function_exists( 'get_option' ) ? get_option( 'avision_admin_api_key', '' ) : '';
		return is_string( $opt ) ? $opt : '';
	}

	/**
	 * Racine du dépôt « public » (WordPress + avision-backend au même niveau que wp-content).
	 * Surcharge : constante AVISION_REPO_PUBLIC_ROOT (chemin absolu du dossier public).
	 *
	 * @return string Chemin avec séparateur final.
	 */
	private static function repo_public_root() {
		if ( defined( 'AVISION_REPO_PUBLIC_ROOT' ) && is_string( AVISION_REPO_PUBLIC_ROOT ) && AVISION_REPO_PUBLIC_ROOT !== '' ) {
			$r = wp_normalize_path( AVISION_REPO_PUBLIC_ROOT );
			return trailingslashit( $r );
		}
		return trailingslashit( wp_normalize_path( ABSPATH ) );
	}

	/**
	 * Ligne de commande Python pour exécuter un script (shell_exec / 2>&1).
	 * Surcharge : AVISION_PYTHON_BIN = chemin absolu vers python.exe (sinon py -3 sous Windows, python3 ailleurs).
	 *
	 * @param string $script_abs Chemin absolu du .py.
	 * @return string
	 */
	private static function brain_python_cmdline( $script_abs ) {
		$script_abs = (string) $script_abs;
		$q_script     = escapeshellarg( $script_abs );
		if ( defined( 'AVISION_PYTHON_BIN' ) && is_string( AVISION_PYTHON_BIN ) && AVISION_PYTHON_BIN !== '' ) {
			return escapeshellarg( AVISION_PYTHON_BIN ) . ' ' . $q_script . ' 2>&1';
		}
		if ( stripos( (string) PHP_OS, 'WIN' ) === 0 ) {
			return escapeshellarg( 'py' ) . ' -3 ' . $q_script . ' 2>&1';
		}
		return escapeshellarg( 'python3' ) . ' ' . $q_script . ' 2>&1';
	}

	/**
	 * Résout le chemin absolu d’un script pipeline cerveau (liste blanche).
	 *
	 * Ordre de résolution : constante AVISION_BACKEND_DATA_DIR (définir dans wp-config.php si besoin,
	 * ex. define( 'AVISION_BACKEND_DATA_DIR', '/root/avision-backend/data' );), puis /root/avision-backend/data/,
	 * puis dépôt relatif à repo_public_root().
	 *
	 * @param string $basename build_master_brain.py
	 * @return string|\WP_Error
	 */
	private static function resolve_brain_script_path( $basename ) {
		$allowed = array(
			'build_master_brain.py',
		);
		if ( ! in_array( $basename, $allowed, true ) ) {
			return new \WP_Error( 'avision_brain_script', __( 'Script non autorisé.', 'avision-dashboard' ) );
		}
		$public = untrailingslashit( self::repo_public_root() );
		$public_candidate = $public . DIRECTORY_SEPARATOR . 'avision-backend' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . $basename;

		$real      = false;
		$candidate = $public_candidate;

		if ( defined( 'AVISION_BACKEND_DATA_DIR' ) && is_string( AVISION_BACKEND_DATA_DIR ) && AVISION_BACKEND_DATA_DIR !== '' && is_dir( AVISION_BACKEND_DATA_DIR ) ) {
			$avision_backend_data_candidate = rtrim( AVISION_BACKEND_DATA_DIR, '/\\' ) . '/' . $basename;
			if ( file_exists( $avision_backend_data_candidate ) ) {
				$rp = @realpath( $avision_backend_data_candidate );
				if ( $rp !== false && is_file( $rp ) ) {
					$real      = $rp;
					$candidate = $avision_backend_data_candidate;
				}
			}
		}
		if ( $real === false ) {
			$hardcoded = '/root/avision-backend/data/' . $basename;
			if ( file_exists( $hardcoded ) ) {
				$rp = @realpath( $hardcoded );
				if ( $rp !== false && is_file( $rp ) ) {
					$real      = $rp;
					$candidate = $hardcoded;
				}
			}
		}
		if ( $real === false ) {
			$rp = @realpath( $public_candidate );
			if ( $rp !== false && is_file( $rp ) ) {
				$real      = $rp;
				$candidate = $public_candidate;
			}
		}
		if ( $real === false || ! is_file( $real ) ) {
			return new \WP_Error(
				'avision_brain_missing',
				sprintf(
					/* translators: %s: expected filesystem path. */
					__( 'Script introuvable : %s', 'avision-dashboard' ),
					$candidate
				)
			);
		}
		$data_dir = @realpath( dirname( $real ) );
		if ( $data_dir === false ) {
			return new \WP_Error( 'avision_brain_dir', __( 'Dossier avision-backend/data introuvable.', 'avision-dashboard' ) );
		}
		$norm_script = wp_normalize_path( $real );
		$norm_base   = wp_normalize_path( $data_dir );
		if ( strpos( $norm_script, trailingslashit( $norm_base ) ) !== 0 ) {
			return new \WP_Error( 'avision_brain_path', __( 'Chemin script hors périmètre sécurisé.', 'avision-dashboard' ) );
		}
		return $real;
	}

	/**
	 * AJAX admin : exécute build_master_brain.py (shell_exec si disponible).
	 *
	 * @return void
	 */
	public static function ajax_run_brain_tool() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ),
				403
			);
		}
		check_ajax_referer( 'avision_cc_intelligence', 'nonce' );
		$tool = isset( $_POST['tool'] ) ? sanitize_key( (string) wp_unslash( $_POST['tool'] ) ) : '';
		$map  = array(
			'build_brain' => 'build_master_brain.py',
		);
		if ( ! isset( $map[ $tool ] ) ) {
			wp_send_json_error( array( 'message' => __( 'Action inconnue.', 'avision-dashboard' ) ), 400 );
		}
		if ( ! function_exists( 'shell_exec' ) || ! is_callable( 'shell_exec' ) ) {
			wp_send_json_error(
				array(
					'message' => __(
						'La fonction shell_exec est désactivée sur cet hébergement. Activez-la ou lancez les scripts Python en local.',
						'avision-dashboard'
					),
				),
				500
			);
		}
		$path = self::resolve_brain_script_path( $map[ $tool ] );
		if ( is_wp_error( $path ) ) {
			wp_send_json_error( array( 'message' => $path->get_error_message() ), 500 );
		}
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 600 );
		}
		$cmd  = self::brain_python_cmdline( $path );
		$raw  = shell_exec( $cmd );
		$log  = is_string( $raw ) ? $raw : '';
		$max  = 512000;
		if ( strlen( $log ) > $max ) {
			$log = substr( $log, 0, $max ) . "\n… [sortie tronquée]";
		}
		wp_send_json_success(
			array(
				'log' => $log,
				'tool' => $tool,
			)
		);
	}

	/**
	 * Liens d’interface Control Center : base wp-admin/admin.php issue de siteurl (Réglages généraux), schéma admin.
	 * Pas de home_url() ni de domaine www codé en dur — évite les déconnexions www / non-www.
	 *
	 * @param array<string, string|int|float> $query Paramètres (page, tab, qdrant_offset, …).
	 * @return string
	 */
	private static function control_center_admin_screen_url( array $query ) {
		if ( ! function_exists( 'site_url' ) || ! function_exists( 'add_query_arg' ) ) {
			return '';
		}
		$base = site_url( 'wp-admin/admin.php', 'admin' );
		return (string) add_query_arg( $query, $base );
	}

	/**
	 * @param string               $path  Chemin absolu API (ex. /admin/qdrant/points).
	 * @param array<string, mixed> $query Query string.
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function api_get( $path, $query = array() ) {
		$base = self::backend_base_url();
		$url  = $base . $path;
		if ( ! empty( $query ) ) {
			$url .= ( strpos( $url, '?' ) !== false ? '&' : '?' ) . http_build_query( $query );
		}
		$key = self::admin_api_key();
		$headers = array(
			'Accept'           => 'application/json',
			'X-Admin-API-Key' => $key,
		);
		if ( strpos( $url, '127.0.0.1' ) === false && stripos( $url, 'localhost' ) === false ) {
			$headers['Host'] = 'engine.avision.org';
		}
		$res = wp_remote_get(
			$url,
			array(
				'timeout' => 45,
				'headers' => $headers,
			)
		);
		return self::normalize_response( $res );
	}

	/**
	 * @param string               $path Chemin absolu API.
	 * @param array<string, mixed> $json Corps JSON.
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function api_post_json( $path, $json ) {
		$base = self::backend_base_url();
		$url  = $base . $path;
		$key  = self::admin_api_key();
		$headers = array(
			'Content-Type'     => 'application/json; charset=utf-8',
			'Accept'           => 'application/json',
			'X-Admin-API-Key' => $key,
		);
		if ( strpos( $url, '127.0.0.1' ) === false && stripos( $url, 'localhost' ) === false ) {
			$headers['Host'] = 'engine.avision.org';
		}
		$res = wp_remote_post(
			$url,
			array(
				'timeout' => 90,
				'headers' => $headers,
				'body'    => wp_json_encode( $json ),
			)
		);
		return self::normalize_response( $res );
	}

	/**
	 * Requête HTTP DELETE vers l’API FastAPI (ex. suppression point Qdrant).
	 *
	 * @param string $path Chemin absolu API (ex. /admin/qdrant/points/123).
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function api_delete( $path ) {
		$base = self::backend_base_url();
		$url  = $base . $path;
		$key  = self::admin_api_key();
		$headers = array(
			'Accept'           => 'application/json',
			'X-Admin-API-Key' => $key,
		);
		if ( strpos( $url, '127.0.0.1' ) === false && stripos( $url, 'localhost' ) === false ) {
			$headers['Host'] = 'engine.avision.org';
		}
		$res = wp_remote_request(
			$url,
			array(
				'timeout' => 45,
				'method'  => 'DELETE',
				'headers' => $headers,
			)
		);
		return self::normalize_response( $res );
	}

	/**
	 * @param array|\WP_Error $res Réponse wp_remote_*.
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function normalize_response( $res ) {
		if ( is_wp_error( $res ) ) {
			return array(
				'ok'    => false,
				'code'  => 0,
				'body'  => '',
				'data'  => null,
				'error' => $res->get_error_message(),
			);
		}
		$code = (int) wp_remote_retrieve_response_code( $res );
		$body = (string) wp_remote_retrieve_body( $res );
		$data = json_decode( $body, true );
		return array(
			'ok'    => $code >= 200 && $code < 300,
			'code'  => $code,
			'body'  => $body,
			'data'  => is_array( $data ) ? $data : null,
			'error' => $code >= 400 ? ( is_array( $data ) && isset( $data['detail'] ) ? wp_json_encode( $data['detail'] ) : $body ) : '',
		);
	}

	/**
	 * En-têtes pour GET/POST /api/v1/knowledge/* (Authorization Bearer = même secret que X-Admin-API-Key).
	 *
	 * @param bool $with_json_content_type Ajouter Content-Type JSON (POST).
	 * @return array<string, string>
	 */
	private static function knowledge_v1_bearer_headers( $with_json_content_type = false ) {
		$key = self::admin_api_key();
		$headers = array(
			'Accept'        => 'application/json',
			'Authorization' => 'Bearer ' . $key,
		);
		if ( $with_json_content_type ) {
			$headers['Content-Type'] = 'application/json; charset=utf-8';
		}
		$base = self::backend_base_url();
		if ( strpos( $base, '127.0.0.1' ) === false && stripos( $base, 'localhost' ) === false ) {
			$headers['Host'] = 'engine.avision.org';
		}
		return $headers;
	}

	/**
	 * GET JSON sur une route /api/v1/knowledge/… (Bearer).
	 *
	 * @param string $path Chemin absolu (ex. /api/v1/knowledge/stats).
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function knowledge_v1_get( $path ) {
		$url = self::backend_base_url() . $path;
		$res = wp_remote_get(
			$url,
			array(
				'timeout' => 45,
				'headers' => self::knowledge_v1_bearer_headers( false ),
			)
		);
		return self::normalize_response( $res );
	}

	/**
	 * POST JSON sur une route /api/v1/knowledge/… (Bearer).
	 *
	 * @param string               $path Chemin absolu.
	 * @param array<string, mixed> $json Corps.
	 * @return array{ok: bool, code: int, body: string, data: mixed|null, error: string}
	 */
	private static function knowledge_v1_post_json( $path, array $json ) {
		$url = self::backend_base_url() . $path;
		$res = wp_remote_post(
			$url,
			array(
				'timeout' => 90,
				'headers' => self::knowledge_v1_bearer_headers( true ),
				'body'    => wp_json_encode( $json ),
			)
		);
		return self::normalize_response( $res );
	}

	/**
	 * Lit un entier depuis un payload stats (plusieurs clés possibles selon version API).
	 *
	 * @param array<string, mixed>|null $d Données JSON.
	 * @param array<int, string>        $keys Clés candidates.
	 * @param int                       $def Valeur par défaut.
	 * @return int
	 */
	private static function knowledge_stats_int( $d, array $keys, $def = 0 ) {
		if ( ! is_array( $d ) ) {
			return $def;
		}
		foreach ( $keys as $k ) {
			if ( isset( $d[ $k ] ) && is_numeric( $d[ $k ] ) ) {
				return (int) $d[ $k ];
			}
		}
		return $def;
	}

	/**
	 * Clé filtre / badge source document (metadata.doc_type).
	 *
	 * @param array<string, mixed> $meta Métadonnées point Qdrant.
	 * @return string cms|legacy
	 */
	private static function kb_row_doc_source_key( array $meta ) {
		$dt = isset( $meta['doc_type'] ) ? strtoupper( trim( (string) $meta['doc_type'] ) ) : '';
		return ( $dt === 'EXPERT_SEO' ) ? 'cms' : 'legacy';
	}

	/**
	 * Extrait les champs CMS V2 depuis un payload Qdrant (plat ou metadata).
	 *
	 * @param array<string, mixed> $pl Payload racine.
	 * @return array<string, mixed>
	 */
	private static function kb_qdrant_payload_to_cms_fields( array $pl ) {
		$meta = isset( $pl['metadata'] ) && is_array( $pl['metadata'] ) ? $pl['metadata'] : array();
		$content = isset( $pl['content'] ) ? (string) $pl['content'] : '';
		if ( $content === '' && isset( $pl['text'] ) ) {
			$content = (string) $pl['text'];
		}
		$sc = isset( $pl['strategic_category'] ) ? strtoupper( trim( (string) $pl['strategic_category'] ) ) : '';
		if ( $sc === '' && isset( $meta['strategic_category'] ) ) {
			$sc = strtoupper( trim( (string) $meta['strategic_category'] ) );
		}
		$ut = isset( $pl['user_type'] ) ? sanitize_key( (string) $pl['user_type'] ) : '';
		if ( $ut === '' && isset( $pl['dashboard_type'] ) ) {
			$ut = sanitize_key( (string) $pl['dashboard_type'] );
		}
		if ( $ut === '' && isset( $meta['user_type'] ) ) {
			$ut = sanitize_key( (string) $meta['user_type'] );
		}
		$min_level = isset( $pl['min_level'] ) ? (int) $pl['min_level'] : 0;
		if ( $min_level < 1 && isset( $meta['min_level'] ) ) {
			$min_level = (int) $meta['min_level'];
		}
		$cat = isset( $pl['category'] ) ? strtoupper( trim( (string) $pl['category'] ) ) : '';
		if ( $cat === '' && isset( $meta['category'] ) ) {
			$cat = strtoupper( trim( (string) $meta['category'] ) );
		}
		return array(
			'content'            => $content,
			'strategic_category' => $sc,
			'user_type'          => $ut,
			'min_level'          => $min_level,
			'category'           => $cat,
		);
	}

	/**
	 * HTML interne du tbody du tableau Qdrant (lignes uniquement).
	 *
	 * @param array<int, mixed> $points Liste des points API.
	 * @return string
	 */
	private static function kb_render_points_tbody_inner_html( $points ) {
		$html = '';
		if ( ! is_array( $points ) || empty( $points ) ) {
			return '<tr><td colspan="9">' . esc_html__( 'Aucun point ou erreur de connexion.', 'avision-dashboard' ) . '</td></tr>';
		}
		foreach ( $points as $p ) {
			if ( ! is_array( $p ) ) {
				continue;
			}
			$id       = isset( $p['id'] ) ? $p['id'] : '';
			$txt      = isset( $p['text'] ) ? (string) $p['text'] : '';
			$meta_arr = isset( $p['metadata'] ) && is_array( $p['metadata'] ) ? $p['metadata'] : array();
			$meta     = wp_json_encode( $meta_arr, JSON_UNESCAPED_UNICODE );
			$opt_key  = self::kb_resolve_option_key_from_metadata( $meta_arr );
			$role     = self::kb_role_fonction_from_metadata( $meta_arr, $opt_key );
			$badge    = self::kb_target_badge_html( $meta_arr );
			$tgt_key  = self::kb_target_filter_key_from_metadata( $meta_arr );
			$kb_cat   = self::kb_category_from_metadata( $meta_arr );
			$tgt_raw  = self::kb_target_raw_from_metadata( $meta_arr );
			$strat_c  = isset( $p['strategic_category'] ) ? strtoupper( trim( (string) $p['strategic_category'] ) ) : '';
			$doc_key   = self::kb_row_doc_source_key( $meta_arr );
			$doc_badge = ( $doc_key === 'cms' )
				? '<span class="avi-doc-badge-cms">CMS</span>'
				: '<span class="avi-doc-badge-legacy">Legacy</span>';

			$preview_len = 200;
			$txt_len     = function_exists( 'mb_strlen' ) ? mb_strlen( $txt ) : strlen( $txt );
			$is_long_txt = $txt_len > $preview_len;
			$txt_preview = $txt;
			if ( $is_long_txt ) {
				$txt_preview = function_exists( 'mb_substr' ) ? mb_substr( $txt, 0, $preview_len ) : substr( $txt, 0, $preview_len );
			}

			$search_blob = strtolower( (string) $id . ' ' . $opt_key . ' ' . $role . ' ' . $kb_cat . ' ' . $strat_c . ' ' . $tgt_raw . ' ' . $txt . ' ' . $meta );
			$search_blob = substr( $search_blob, 0, 4000 );

			$html .= '<tr class="avi-kb-row" data-avi-kb-search="' . esc_attr( $search_blob ) . '" data-avi-kb-target="' . esc_attr( $tgt_key ) . '" data-avi-kb-category="' . esc_attr( strtolower( $kb_cat ) ) . '" data-avi-kb-doc="' . esc_attr( $doc_key ) . '">';
			$html .= '<td><code>' . esc_html( (string) $id ) . '</code></td>';
			$html .= '<td>' . $doc_badge . '</td>';
			$html .= '<td>' . $badge . '</td>';
			$html .= '<td class="avi-kb-strat"><code>' . esc_html( $strat_c !== '' ? $strat_c : '—' ) . '</code></td>';
			$html .= '<td><code>' . esc_html( $kb_cat !== '' ? $kb_cat : '—' ) . '</code></td>';
			$html .= '<td><span class="avi-kb-role">' . esc_html( $role ) . '</span>';
			if ( $opt_key !== '' ) {
				$html .= '<span class="avi-kb-role-key"><code>' . esc_html( $opt_key ) . '</code></span>';
			}
			$html .= '</td>';
			$html .= '<td class="avi-kb-text-cell">';
			$html .= '<span class="avi-kb-text-short">' . esc_html( $txt_preview ) . ( $is_long_txt ? '…' : '' ) . '</span>';
			if ( $is_long_txt ) {
				$html .= '<span class="avi-kb-text-long" style="display:none;">' . esc_html( $txt ) . '</span>';
				$html .= ' <button type="button" class="button button-small avi-kb-toggle-text" aria-expanded="false">' . esc_html__( '👁️ Voir tout', 'avision-dashboard' ) . '</button>';
			}
			$html .= '</td>';
			$html .= '<td><pre style="white-space:pre-wrap;max-height:120px;overflow:auto;font-size:11px;">' . esc_html( $meta ) . '</pre></td>';
			$html .= '<td>';
			$avi_kb_del_confirm = esc_js( __( 'Supprimer ce point ?', 'avision-dashboard' ) );
			$html .= '<form method="post" class="avi-kb-delete-form" style="margin:0;display:inline">';
			$html .= wp_nonce_field( 'avision_cc_delete_point', '_wpnonce', false, false );
			$html .= '<input type="hidden" name="avision_cc_delete_point" value="1" />';
			$html .= '<input type="hidden" name="cc_delete_point_id" value="' . esc_attr( (string) $id ) . '" />';
			$html .= '<button type="submit" class="button button-small avi-kb-btn-delete" onclick="return confirm(\'' . $avi_kb_del_confirm . '\');">' . esc_html__( 'Supprimer', 'avision-dashboard' ) . '</button>';
			$html .= '</form></td>';
			$html .= '</tr>';
		}
		return $html;
	}

	/**
	 * Journalise une injection CMS V2 dans avi_ai_interactions.
	 *
	 * @param string $mode new|update.
	 * @param string $strategic_category Cat. stratégique.
	 * @param string $category Cat. pédagogique.
	 * @param string $content Contenu brut.
	 * @param string $point_id Id point Qdrant (réponse ou existant).
	 * @return void
	 */
	private static function log_injection_avi_ai_interactions( $mode, $strategic_category, $category, $content, $point_id ) {
		global $wpdb;
		$table = $wpdb->prefix . 'avi_ai_interactions';
		$question = 'INJECTION:' . $mode . '|' . $strategic_category . '|' . $category;
		$answer   = substr( $content, 0, 500 );
		$payload  = array(
			'mode'     => $mode,
			'chars'    => strlen( $content ),
			'point_id' => $point_id,
		);
		$wpdb->insert(
			$table,
			array(
				'user_id'          => get_current_user_id(),
				'question'         => $question,
				'answer'           => $answer,
				'level_context'    => 'inject_v2',
				'admin_correction' => wp_json_encode( $payload ),
				'created_at'       => current_time( 'mysql' ),
			),
			array( '%d', '%s', '%s', '%s', '%s', '%s' )
		);
	}

	/**
	 * AJAX : filtres serveur GET /admin/qdrant/points.
	 *
	 * @return void
	 */
	public static function ajax_qdrant_filter() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_knowledge', 'nonce' );
		$category            = isset( $_POST['category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['category'] ) ) ) : '';
		$user_type           = isset( $_POST['user_type'] ) ? sanitize_key( (string) wp_unslash( $_POST['user_type'] ) ) : '';
		$strategic_category  = isset( $_POST['strategic_category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['strategic_category'] ) ) ) : '';
		$query               = array(
			'limit'  => 50,
			'offset' => 0,
		);
		if ( $category !== '' ) {
			$query['category'] = $category;
		}
		if ( $user_type !== '' && $user_type !== 'system' ) {
			$query['user_type'] = $user_type;
		}
		if ( $strategic_category !== '' ) {
			$query['strategic_category'] = $strategic_category;
		}
		$res = self::api_get( '/admin/admin/qdrant/points', $query );
		if ( ! $res['ok'] ) {
			wp_send_json_error(
				array(
					'message' => $res['error'] !== '' ? $res['error'] : ( 'HTTP ' . $res['code'] ),
				),
				502
			);
		}
		$points = ( is_array( $res['data'] ) && isset( $res['data']['points'] ) && is_array( $res['data']['points'] ) ) ? $res['data']['points'] : array();
		$html_rows   = self::kb_render_points_tbody_inner_html( $points );
		$total       = ( is_array( $res['data'] ) && isset( $res['data']['total_estimated'] ) && $res['data']['total_estimated'] !== null ) ? (int) $res['data']['total_estimated'] : 0;
		$next_offset = ( is_array( $res['data'] ) && ! empty( $res['data']['next_offset'] ) ) ? (string) $res['data']['next_offset'] : '';
		wp_send_json_success(
			array(
				'html_rows'   => $html_rows,
				'total'       => $total,
				'next_offset' => $next_offset,
			)
		);
	}

	/**
	 * AJAX : recherche dense GET /admin/qdrant/search (max 5 résultats affichés).
	 *
	 * @return void
	 */
	public static function ajax_qdrant_search_chunks() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_knowledge', 'nonce' );
		$q = isset( $_POST['query'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['query'] ) ) : '';
		if ( strlen( $q ) < 3 ) {
			wp_send_json_error( array( 'message' => __( 'Minimum 3 caractères.', 'avision-dashboard' ) ), 400 );
		}
		$res = self::api_get( '/admin/admin/qdrant/search', array( 'query' => $q ) );
		if ( ! $res['ok'] ) {
			wp_send_json_error(
				array(
					'message' => $res['error'] !== '' ? $res['error'] : ( 'HTTP ' . $res['code'] ),
				),
				502
			);
		}
		$results = array();
		if ( is_array( $res['data'] ) && isset( $res['data']['results'] ) && is_array( $res['data']['results'] ) ) {
			$n = 0;
			foreach ( $res['data']['results'] as $row ) {
				if ( $n >= 5 ) {
					break;
				}
				if ( ! is_array( $row ) ) {
					continue;
				}
				$rid = isset( $row['id'] ) ? $row['id'] : '';
				$pl  = isset( $row['payload'] ) && is_array( $row['payload'] ) ? $row['payload'] : array();
				$txt = isset( $pl['content'] ) ? (string) $pl['content'] : '';
				if ( $txt === '' && isset( $pl['text'] ) ) {
					$txt = (string) $pl['text'];
				}
				$snippet = function_exists( 'mb_substr' ) ? mb_substr( $txt, 0, 80 ) : substr( $txt, 0, 80 );
				$results[] = array(
					'id'      => $rid,
					'snippet' => $snippet,
				);
				$n++;
			}
		}
		wp_send_json_success( array( 'results' => $results ) );
	}

	/**
	 * AJAX : GET /admin/qdrant/point/{id} pour préremplissage CMS V2.
	 *
	 * @return void
	 */
	public static function ajax_qdrant_get_point() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_knowledge', 'nonce' );
		$pid = isset( $_POST['point_id'] ) ? trim( (string) wp_unslash( $_POST['point_id'] ) ) : '';
		if ( $pid === '' ) {
			wp_send_json_error( array( 'message' => __( 'ID manquant.', 'avision-dashboard' ) ), 400 );
		}
		$path = '/admin/qdrant/point/' . rawurlencode( $pid );
		$res  = self::api_get( $path, array() );
		if ( ! $res['ok'] ) {
			wp_send_json_error(
				array(
					'message' => $res['error'] !== '' ? $res['error'] : ( 'HTTP ' . $res['code'] ),
				),
				502
			);
		}
		$pl = ( is_array( $res['data'] ) && isset( $res['data']['payload'] ) && is_array( $res['data']['payload'] ) ) ? $res['data']['payload'] : array();
		$f  = self::kb_qdrant_payload_to_cms_fields( $pl );
		$preview80 = '';
		$c = isset( $f['content'] ) ? (string) $f['content'] : '';
		if ( $c !== '' ) {
			$preview80 = function_exists( 'mb_substr' ) ? mb_substr( $c, 0, 80 ) : substr( $c, 0, 80 );
		}
		wp_send_json_success(
			array(
				'fields'     => $f,
				'preview_80' => $preview80,
				'point_id'   => $pid,
			)
		);
	}

	/**
	 * AJAX : POST /api/v1/knowledge/simulate (prévisualisation injection).
	 *
	 * @return void
	 */
	public static function ajax_knowledge_preview_simulate() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_knowledge', 'nonce' );
		$content    = isset( $_POST['content'] ) ? wp_unslash( (string) $_POST['content'] ) : '';
		$sc         = isset( $_POST['strategic_category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['strategic_category'] ) ) ) : '';
		$ut         = isset( $_POST['user_type'] ) ? sanitize_key( (string) wp_unslash( $_POST['user_type'] ) ) : '';
		$cat        = isset( $_POST['category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['category'] ) ) ) : '';
		$min_level  = isset( $_POST['min_level'] ) ? (int) $_POST['min_level'] : 0;
		$test_level = isset( $_POST['test_level'] ) ? (int) $_POST['test_level'] : 1;
		if ( $test_level < 1 || $test_level > 21 ) {
			$test_level = 1;
		}
		$tq = isset( $_POST['test_query'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['test_query'] ) ) : '';
		if ( trim( $tq ) === '' ) {
			$tq = __( 'Contrôle qualité du chunk avant injection.', 'avision-dashboard' );
		}
		$len       = function_exists( 'mb_strlen' ) ? mb_strlen( $content ) : strlen( $content );
		$allow_sc  = array( 'REPUTATION', 'LOCAL_VISIBILITY', 'ROI_AGENCE' );
		$allow_ut  = array( 'core', 'b2b', 'b2c' );
		$allow_cat = array( 'GBP_RULES', 'STRATEGY', 'PERFORMANCE', 'IDENTITY' );
		if ( $len < 50 ) {
			wp_send_json_error( array( 'message' => __( 'Contenu expert : minimum 50 caractères.', 'avision-dashboard' ) ), 400 );
		}
		if ( ! in_array( $sc, $allow_sc, true ) || ! in_array( $ut, $allow_ut, true ) || ! in_array( $cat, $allow_cat, true ) || $min_level < 1 || $min_level > 21 ) {
			wp_send_json_error( array( 'message' => __( 'Valeurs stratégiques ou niveau invalides.', 'avision-dashboard' ) ), 400 );
		}
		$dashboard_type = ( $ut === 'b2b' ) ? 'b2b' : 'b2c';
		$sim_body       = array(
			'raw_text'       => $content,
			'test_query'     => $tq,
			'dashboard_type' => $dashboard_type,
			'current_level'  => $test_level,
		);
		$sim = self::knowledge_v1_post_json( '/api/v1/knowledge/simulate', $sim_body );
		if ( $sim['ok'] && is_array( $sim['data'] ) ) {
			$sim_txt = '';
			foreach ( array( 'simulated_response', 'response', 'answer', 'simulated_text', 'output' ) as $sk ) {
				if ( ! empty( $sim['data'][ $sk ] ) && is_string( $sim['data'][ $sk ] ) ) {
					$sim_txt = $sim['data'][ $sk ];
					break;
				}
			}
			if ( $sim_txt === '' && ! empty( $sim['data']['data'] ) && is_string( $sim['data']['data'] ) ) {
				$sim_txt = $sim['data']['data'];
			}
			$html  = '<div class="avi-cms-preview-result" style="background:#f0f8ff;border-left:4px solid #0073aa;padding:12px;margin:12px 0;">';
			$html .= '<strong>' . esc_html__( '🧠 Réponse simulée (prévisualisation)', 'avision-dashboard' ) . '</strong>';
			$html .= '<div style="margin-top:8px;">' . nl2br( esc_html( $sim_txt !== '' ? $sim_txt : wp_json_encode( $sim['data'], JSON_UNESCAPED_UNICODE ) ) ) . '</div>';
			$html .= '<p class="description" style="margin-bottom:0;">' . esc_html__( 'Validez avec « Confirmer injection » pour écrire dans Qdrant.', 'avision-dashboard' ) . '</p></div>';
			wp_send_json_success( array( 'html' => $html ) );
		}
		wp_send_json_error(
			array(
				'message' => $sim['error'] !== '' ? $sim['error'] : ( 'HTTP ' . $sim['code'] ),
			),
			502
		);
	}

	/**
	 * GET /health (sans auth admin).
	 *
	 * @return array{ok: bool, code: int}
	 */
	private static function http_backend_health() {
		$url = self::backend_base_url() . '/health';
		$res = wp_remote_get(
			$url,
			array(
				'timeout' => 8,
				'headers' => array( 'Accept' => 'application/json' ),
			)
		);
		if ( is_wp_error( $res ) ) {
			return array( 'ok' => false, 'code' => 0 );
		}
		$code = (int) wp_remote_retrieve_response_code( $res );
		return array(
			'ok'   => $code >= 200 && $code < 300,
			'code' => $code,
		);
	}

	/**
	 * Questions baseline QI (référence wp_option avi_qi_baseline_questions).
	 *
	 * @return array<int, array<string, mixed>>
	 */
	private static function qi_default_baseline_questions() {
		return array(
			array(
				'id'          => 'qi_001',
				'label'       => 'B2C — Guardian Mode',
				'question'    => 'What does Guardian Mode do for my Google listing?',
				'user_type'   => 'b2c',
				'expected_kw' => array( 'shield', 'protect', 'monitor', 'automatic' ),
				'min_level'   => 4,
			),
			array(
				'id'          => 'qi_002',
				'label'       => 'B2C — Score faible',
				'question'    => 'My score is 42/100, what should I do first?',
				'user_type'   => 'b2c',
				'expected_kw' => array( 'level', 'category', 'foundation', 'priority' ),
				'min_level'   => 1,
			),
			array(
				'id'          => 'qi_003',
				'label'       => 'B2C — Blocage 72h',
				'question'    => 'Why is my listing locked for 72 hours?',
				'user_type'   => 'b2c',
				'expected_kw' => array( 'Google', 'validate', 'signal', 'natural' ),
				'min_level'   => 1,
			),
			array(
				'id'          => 'qi_004',
				'label'       => 'B2C — Growth Radar',
				'question'    => 'How does Growth Radar help me find keywords?',
				'user_type'   => 'b2c',
				'expected_kw' => array( 'keyword', 'local', 'search', 'weekly', 'volume' ),
				'min_level'   => 1,
			),
			array(
				'id'          => 'qi_005',
				'label'       => 'B2B — ROI Guardian',
				'question'    => 'What is the ROI of activating Guardian for my agency clients?',
				'user_type'   => 'b2b',
				'expected_kw' => array( 'ROI', 'protection', 'automated', 'fleet' ),
				'min_level'   => 4,
			),
			array(
				'id'          => 'qi_006',
				'label'       => 'B2B — Fleet management',
				'question'    => 'How do I manage 50 client listings from one dashboard?',
				'user_type'   => 'b2b',
				'expected_kw' => array( 'agency', 'fleet', 'bulk', 'dashboard' ),
				'min_level'   => 1,
			),
			array(
				'id'          => 'qi_007',
				'label'       => 'B2B — Wholesale pricing',
				'question'    => 'What is wholesale pricing for Guardian?',
				'user_type'   => 'b2b',
				'expected_kw' => array( 'wholesale', 'tier', 'pricing', 'location' ),
				'min_level'   => 1,
			),
			array(
				'id'          => 'qi_008',
				'label'       => 'B2B — Plans comparison',
				'question'    => 'What is the difference between Starter Pro and Elite Pro?',
				'user_type'   => 'b2b',
				'expected_kw' => array( 'radar', 'weekly', 'daily', 'keyword', 'bundle' ),
				'min_level'   => 1,
			),
		);
	}

	/**
	 * Initialise avi_qi_baseline_questions si absent.
	 *
	 * @return void
	 */
	private static function ensure_qi_baseline_questions_option() {
		$cur = get_option( 'avi_qi_baseline_questions', false );
		if ( $cur === false || ! is_array( $cur ) || count( $cur ) === 0 ) {
			update_option( 'avi_qi_baseline_questions', self::qi_default_baseline_questions(), false );
		}
	}

	/**
	 * AJAX : purge LiteSpeed (commande whitelist uniquement).
	 *
	 * @return void
	 */
	public static function ajax_purge_litespeed_cache() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_dashboard', 'nonce' );
		if ( ! function_exists( 'shell_exec' ) || ! is_callable( 'shell_exec' ) ) {
			wp_send_json_error( array( 'message' => __( 'shell_exec indisponible.', 'avision-dashboard' ) ), 500 );
		}
		$whitelist = 'wp litespeed-purge all --allow-root --path=/home/avision.org/public_html 2>&1';
		$raw       = shell_exec( $whitelist );
		$log       = is_string( $raw ) ? $raw : '';
		wp_send_json_success( array( 'log' => $log ) );
	}

	/**
	 * AJAX : rafraîchir santé + stats knowledge.
	 *
	 * @return void
	 */
	public static function ajax_dashboard_refresh() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_dashboard', 'nonce' );
		$h = self::http_backend_health();
		$s = self::knowledge_v1_get( '/api/v1/knowledge/stats' );
		wp_send_json_success(
			array(
				'health_ok' => $h['ok'],
				'health_code' => isset( $h['code'] ) ? $h['code'] : 0,
				'stats'     => ( $s['ok'] && is_array( $s['data'] ) ) ? $s['data'] : null,
				'stats_error' => $s['ok'] ? '' : ( $s['error'] ?: ( 'HTTP ' . $s['code'] ) ),
			)
		);
	}

	/**
	 * AJAX : exécute les 8 tests QI baseline.
	 *
	 * @return void
	 */
	public static function ajax_qi_run_baseline() {
		global $wpdb;
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_intelligence', 'nonce' );
		self::ensure_qi_baseline_questions_option();
		$questions = get_option( 'avi_qi_baseline_questions', array() );
		if ( ! is_array( $questions ) ) {
			$questions = self::qi_default_baseline_questions();
		}
		$results      = array();
		$scores       = array();
		$table_ai     = $wpdb->prefix . 'avi_ai_interactions';
		$uid          = get_current_user_id();

		foreach ( $questions as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$qid    = isset( $row['id'] ) ? (string) $row['id'] : '';
			$qtext  = isset( $row['question'] ) ? (string) $row['question'] : '';
			$ut     = isset( $row['user_type'] ) ? sanitize_key( (string) $row['user_type'] ) : 'b2c';
			$min_lv = isset( $row['min_level'] ) ? (int) $row['min_level'] : 1;
			$kws    = isset( $row['expected_kw'] ) && is_array( $row['expected_kw'] ) ? $row['expected_kw'] : array();
			if ( $qtext === '' ) {
				continue;
			}
			$db_type = ( $ut === 'b2b' ) ? 'b2b' : 'b2c';
			$body    = array(
				'raw_text'       => $qtext,
				'test_query'     => $qtext,
				'dashboard_type' => $db_type,
				'current_level'  => max( 1, min( 21, $min_lv ) ),
			);
			$sim = self::knowledge_v1_post_json( '/api/v1/knowledge/simulate', $body );
			$sim_txt = '';
			if ( $sim['ok'] && is_array( $sim['data'] ) ) {
				foreach ( array( 'simulated_response', 'response', 'answer', 'simulated_text', 'output' ) as $sk ) {
					if ( ! empty( $sim['data'][ $sk ] ) && is_string( $sim['data'][ $sk ] ) ) {
						$sim_txt = $sim['data'][ $sk ];
						break;
					}
				}
			}
			if ( $sim_txt === '' && is_array( $sim['data'] ) && ! empty( $sim['data']['data'] ) && is_string( $sim['data']['data'] ) ) {
				$sim_txt = $sim['data']['data'];
			}
			$low     = strtolower( $sim_txt );
			$nk      = count( $kws );
			$hits    = 0;
			foreach ( $kws as $kw ) {
				if ( ! is_string( $kw ) || $kw === '' ) {
					continue;
				}
				if ( strpos( $low, strtolower( $kw ) ) !== false ) {
					$hits++;
				}
			}
			$pct = ( $nk > 0 ) ? (int) round( ( $hits / $nk ) * 100 ) : 0;
			$scores[] = $pct;
			$status_l = '❌';
			if ( $pct >= 75 ) {
				$status_l = '✅';
			} elseif ( $pct >= 50 ) {
				$status_l = '⚠️';
			}
			$results[] = array(
				'id'       => $qid,
				'score_pct' => $pct,
				'status'   => $status_l,
				'response' => $sim_txt,
				'error'    => $sim['ok'] ? '' : ( $sim['error'] ?: ( 'HTTP ' . $sim['code'] ) ),
			);
			$wpdb->insert(
				$table_ai,
				array(
					'user_id'          => $uid,
					'question'         => 'QI_BASELINE:' . $qid,
					'answer'           => substr( $sim_txt, 0, 500 ),
					'level_context'    => 'qi_baseline',
					'admin_correction' => wp_json_encode( array( 'score_pct' => $pct, 'hits' => $hits, 'total_kw' => $nk ) ),
					'created_at'       => current_time( 'mysql' ),
				),
				array( '%d', '%s', '%s', '%s', '%s', '%s' )
			);
		}
		$global_avg = count( $scores ) > 0 ? (int) round( array_sum( $scores ) / count( $scores ) ) : 0;
		wp_send_json_success(
			array(
				'results'      => $results,
				'score_global' => $global_avg,
			)
		);
	}

	/**
	 * AJAX : sauvegarde le dernier run baseline (JSON).
	 *
	 * @return void
	 */
	public static function ajax_qi_save_baseline() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_intelligence', 'nonce' );
		$payload = isset( $_POST['payload'] ) ? wp_unslash( (string) $_POST['payload'] ) : '';
		if ( $payload === '' ) {
			wp_send_json_error( array( 'message' => __( 'Payload vide.', 'avision-dashboard' ) ), 400 );
		}
		$decoded = json_decode( $payload, true );
		if ( ! is_array( $decoded ) ) {
			wp_send_json_error( array( 'message' => __( 'JSON invalide.', 'avision-dashboard' ) ), 400 );
		}
		$wrap = array(
			'saved_at' => current_time( 'mysql' ),
			'data'     => $decoded,
		);
		update_option( 'avi_qi_last_baseline', wp_json_encode( $wrap, JSON_UNESCAPED_UNICODE ), false );
		wp_send_json_success( array( 'saved_at' => $wrap['saved_at'] ) );
	}

	/**
	 * AJAX : exécute test_qi_baseline.py (22 questions) et parse la sortie texte.
	 *
	 * @return void
	 */
	public static function ajax_qi_run_full() {
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}

		$script = '/root/avision-backend/scripts/test_qi_baseline.py';
		$venv   = '/root/avision-backend/venv/bin/python3';
		if ( ! file_exists( $script ) || ! file_exists( $venv ) ) {
			wp_send_json_error( array( 'message' => 'Script ou venv Python introuvable.' ), 500 );
		}

		$cmd = 'cd /root/avision-backend && ' . escapeshellarg( $venv ) . ' ' . escapeshellarg( $script ) . ' 2>&1';
		$raw = shell_exec( $cmd );

		$lines      = $raw ? explode( "\n", trim( (string) $raw ) ) : array();
		$results    = array();
		$b2c_pass   = 0;
		$b2c_total  = 0;
		$b2b_pass   = 0;
		$b2b_total  = 0;
		$total_pass = 0;
		$total_total = 0;

		foreach ( $lines as $line ) {
			$line = trim( $line );
			if ( preg_match( '/\[(PASS|FAIL)\] Q(\d+): (.+?) \| score=(\d+)% \| found=\[([^\]]*)\] \| missing=\[([^\]]*)\]/', $line, $m ) ) {
				$n           = (int) $m[2];
				$results[]   = array(
					'n'        => $n,
					'status'   => $m[1],
					'question' => $m[3],
					'score'    => (int) $m[4],
					'found'    => $m[5],
					'missing'  => $m[6],
					'type'     => $n <= 10 ? 'b2c' : 'b2b',
				);
			}
			if ( preg_match( '/B2C: (\d+)\/(\d+)/', $line, $m ) ) {
				$b2c_pass  = (int) $m[1];
				$b2c_total = (int) $m[2];
			}
			if ( preg_match( '/B2B: (\d+)\/(\d+)/', $line, $m ) ) {
				$b2b_pass  = (int) $m[1];
				$b2b_total = (int) $m[2];
			}
			if ( preg_match( '/TOTAL QI: (\d+)\/(\d+)/', $line, $m ) ) {
				$total_pass  = (int) $m[1];
				$total_total = (int) $m[2];
			}
		}

		$score_pct = $total_total > 0 ? (int) round( ( $total_pass / $total_total ) * 100 ) : 0;

		$result = array(
			'timestamp'   => current_time( 'mysql' ),
			'score_pct'   => $score_pct,
			'total_pass'  => $total_pass,
			'total_total' => $total_total,
			'b2c_pass'    => $b2c_pass,
			'b2c_total'   => $b2c_total,
			'b2b_pass'    => $b2b_pass,
			'b2b_total'   => $b2b_total,
			'results'     => $results,
			'trigger'     => 'manual',
			'raw_tail'    => substr( (string) $raw, -3000 ),
		);

		update_option( 'avi_qi_monitor_last', $result, false );

		$history = get_option( 'avi_qi_monitor_history', array() );
		if ( ! is_array( $history ) ) {
			$history = array();
		}
		array_unshift(
			$history,
			array(
				'timestamp' => $result['timestamp'],
				'score_pct' => $score_pct,
				'b2c'       => $b2c_pass . '/' . $b2c_total,
				'b2b'       => $b2b_pass . '/' . $b2b_total,
				'trigger'   => 'manual',
			)
		);
		$history = array_slice( $history, 0, 14 );
		update_option( 'avi_qi_monitor_history', $history, false );

		wp_send_json_success( $result );
	}

	/**
	 * Live Chat QA — teste le vrai tunnel WordPress → FastAPI via proxy_chat_to_fastapi_params().
	 *
	 * @return void
	 */
	public static function ajax_lqa_run() {
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}

		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 300 );
		}

		$api_file = defined( 'AVISION_PATH' ) ? AVISION_PATH . 'includes/class-avision-api.php' : '';
		if ( $api_file !== '' && is_readable( $api_file ) ) {
			require_once $api_file;
		}

		$test_questions = array(
			array(
				'id'       => 'lqa_b2c_01',
				'type'     => 'b2c',
				'message'  => 'What is my visibility score and what does it mean?',
				'keywords' => array( 'score', 'visibility', 'google', 'local' ),
				'level'    => 2,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_02',
				'type'     => 'b2c',
				'message'  => 'What should I do at Level 1?',
				'keywords' => array( 'category', 'primary' ),
				'level'    => 1,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_03',
				'type'     => 'b2c',
				'message'  => 'How does Guardian Mode protect my business?',
				'keywords' => array( 'guardian', 'shield', 'protect', 'monitor' ),
				'level'    => 3,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_04',
				'type'     => 'b2c',
				'message'  => 'How can I get more Google reviews?',
				'keywords' => array( 'review', 'customer', 'google', 'ask' ),
				'level'    => 4,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_05',
				'type'     => 'b2c',
				'message'  => 'What is the Growth Radar and how does it help me?',
				'keywords' => array( 'radar', 'keyword', 'search', 'local' ),
				'level'    => 5,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_06',
				'type'     => 'b2c',
				'message'  => 'How long does it take to see results with Avision?',
				'keywords' => array( 'week' ),
				'level'    => 2,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_07',
				'type'     => 'b2c',
				'message'  => 'My listing was suspended, what should I do?',
				'keywords' => array( 'suspend', 'reinstate' ),
				'level'    => 2,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2c_08',
				'type'     => 'b2c',
				'message'  => 'Why are my Google photos not showing up?',
				'keywords' => array( 'photo', 'reject', 'gbp', 'pending' ),
				'level'    => 4,
				'sector'   => 'RESTAURANT',
			),
			array(
				'id'       => 'lqa_b2b_01',
				'type'     => 'b2b',
				'message'  => 'How do I prioritize which listings to activate Guardian on first?',
				'keywords' => array( 'score', 'priority', 'fleet', 'guardian' ),
				'level'    => null,
				'sector'   => 'SERVICE',
			),
			array(
				'id'       => 'lqa_b2b_02',
				'type'     => 'b2b',
				'message'  => 'How does the agency dashboard track fleet performance?',
				'keywords' => array( 'fleet', 'dashboard', 'kpi', 'score' ),
				'level'    => null,
				'sector'   => 'SERVICE',
			),
			array(
				'id'       => 'lqa_b2b_03',
				'type'     => 'b2b',
				'message'  => 'How do I activate Guardian on multiple listings at once?',
				'keywords' => array( 'bulk', 'guardian', 'activation', 'fleet' ),
				'level'    => null,
				'sector'   => 'SERVICE',
			),
			array(
				'id'       => 'lqa_b2b_04',
				'type'     => 'b2b',
				'message'  => 'How does wholesale tier pricing work for agencies?',
				'keywords' => array( 'tier', 'wholesale', 'location', 'price' ),
				'level'    => null,
				'sector'   => 'SERVICE',
			),
		);

		$results   = array();
		$pass      = 0;
		$fail_rag  = 0;
		$fail_kw   = 0;
		$fail_orch = 0;

		$api = null;
		if ( class_exists( '\Avision\Avision_API' ) ) {
			$api = new \Avision\Avision_API();
			if ( method_exists( $api, 'init' ) ) {
				$api->init();
			}
		}

		foreach ( $test_questions as $q ) {
			$q_type = isset( $q['type'] ) ? (string) $q['type'] : 'b2c';
			$params = array(
				'message'        => isset( $q['message'] ) ? (string) $q['message'] : '',
				'dashboard_type' => $q_type === 'b2b' ? 'b2b' : 'b2c',
				'language'       => 'en',
				'score'          => $q_type === 'b2c' ? 45 : 0,
				'sector'         => isset( $q['sector'] ) ? (string) $q['sector'] : '',
				'business_name'  => 'AVI QA Test',
				'city'           => 'London',
				'profile_id'     => '',
				'user_id'        => '1',
				'chatHistory'    => array(),
			);
			if ( isset( $q['level'] ) && $q['level'] !== null ) {
				$params['currentLevel'] = (int) $q['level'];
				$params['level']        = (int) $q['level'];
			}
			$fleet_kpis_lqa = array();
			if ( $q_type === 'b2b' ) {
				$fleet_kpis_lqa = array(
					'managed_listings_count' => 5,
					'agency_tier'            => 1,
					'critical_count'         => 1,
					'visibility_tier'      => 'agency_tier1',
					'google_connected'       => true,
				);
				if ( class_exists( '\Avision\Avision_API' ) && method_exists( '\Avision\Avision_API', 'enrich_fleet_kpis_with_profiles_public' ) ) {
					$fleet_kpis_lqa = \Avision\Avision_API::enrich_fleet_kpis_with_profiles_public(
						$fleet_kpis_lqa,
						1
					);
				}
				$params['fleet_kpis'] = $fleet_kpis_lqa;
			}

			$response = array( 'success' => false, 'error' => 'Avision_API unavailable' );
			if ( $api !== null ) {
				$response = $api->proxy_chat_to_fastapi_params( $params );
				// Délai anti-race-condition entre questions LQA (FastAPI context isolation)
				usleep( 300000 ); // 0.3s
			}

			$reply      = '';
			$rag_used   = null;
			$orch_path  = null;
			$model_used = null;
			$http_ok    = false;
			$err_msg    = '';

			if ( is_wp_error( $response ) ) {
				$err_msg = $response->get_error_message();
			} elseif ( $response instanceof \WP_REST_Response ) {
				$data = $response->get_data();
				if ( is_array( $data ) ) {
					$reply      = isset( $data['reply'] ) ? (string) $data['reply'] : '';
					$rag_used   = isset( $data['rag_used'] ) ? $data['rag_used'] : null;
					$orch_path  = isset( $data['orchestrator_path'] ) ? $data['orchestrator_path'] : null;
					$model_used = isset( $data['model'] ) ? $data['model'] : null;
					$http_ok    = $reply !== '';
				}
			} elseif ( is_array( $response ) ) {
				if ( empty( $response['success'] ) && isset( $response['error'] ) ) {
					$err_msg = is_string( $response['error'] ) ? $response['error'] : 'Chat error';
				}
				$data = $response;
				if ( isset( $data['data'] ) && is_array( $data['data'] ) ) {
					$data = array_merge( $data, $data['data'] );
				}
				$reply      = isset( $data['reply'] ) ? (string) $data['reply'] : '';
				$rag_used   = isset( $data['rag_used'] ) ? $data['rag_used'] : null;
				$orch_path  = isset( $data['orchestrator_path'] ) ? $data['orchestrator_path'] : null;
				$model_used = isset( $data['model'] ) ? $data['model'] : null;
				$http_ok    = $reply !== '';
			}

			$reply_lower = strtolower( wp_strip_all_tags( $reply ) );
			$found_kw    = array();
			$missing_kw  = array();
			$keywords    = isset( $q['keywords'] ) && is_array( $q['keywords'] ) ? $q['keywords'] : array();
			foreach ( $keywords as $kw ) {
				$kw = (string) $kw;
				if ( $kw !== '' && strpos( $reply_lower, strtolower( $kw ) ) !== false ) {
					$found_kw[] = $kw;
				} else {
					$missing_kw[] = $kw;
				}
			}
			$kw_count = count( $keywords );
			$kw_score = $kw_count > 0 ? (int) round( ( count( $found_kw ) / $kw_count ) * 100 ) : 0;

			$check_rag   = ( (int) $rag_used === 1 || $rag_used === true );
			$orch_str    = is_string( $orch_path ) ? $orch_path : '';
			$check_orch  = ( $orch_str !== '' && strpos( strtolower( $orch_str ), 'legacy' ) === false );
			$model_str   = is_string( $model_used ) ? $model_used : '';
			$check_model = ( $model_str !== '' && strpos( $model_str, 'orchestrator_nlg' ) !== false );
			$check_kw    = $kw_score >= 50;
			$check_http  = $http_ok;

			$check_guardian    = 'n/a';
			$check_radar       = 'n/a';
			$check_fiche       = 'n/a';
			$check_not_generic = 'n/a';
			if ( $q_type === 'b2b' ) {
				$check_guardian = ( strpos( $reply_lower, 'guardian' ) !== false ) ? 'pass' : 'fail';
				$check_radar    = ( strpos( $reply_lower, 'radar' ) !== false || strpos( $reply_lower, 'hebdo' ) !== false || strpos( $reply_lower, 'mensuel' ) !== false ) ? 'pass' : 'fail';
				$check_fiche_ok = false;
				if ( isset( $fleet_kpis_lqa['top_priority_profiles'] ) && is_array( $fleet_kpis_lqa['top_priority_profiles'] ) ) {
					foreach ( $fleet_kpis_lqa['top_priority_profiles'] as $_fp ) {
						$fn = strtolower( str_replace( array( ' ', '-' ), '', $_fp['business_name'] ?? '' ) );
						if ( $fn && strpos( str_replace( array( ' ', '-' ), '', $reply_lower ), $fn ) !== false ) {
							$check_fiche_ok = true;
							break;
						}
					}
				}
				$check_fiche       = $check_fiche_ok ? 'pass' : 'fail';
				$check_not_generic = ( strpos( $reply_lower, 'fleet data is loading' ) === false && strpos( $reply_lower, 'ensure all information is accurate' ) === false ) ? 'pass' : 'fail';
			}

			$status = ( $check_http && $check_kw ) ? 'PASS' : 'FAIL';
			if ( $status === 'PASS' ) {
				$pass++;
			}
			if ( ! $check_rag ) {
				$fail_rag++;
			}
			if ( ! $check_kw ) {
				$fail_kw++;
			}
			if ( ! $check_orch ) {
				$fail_orch++;
			}

			$reply_excerpt = function_exists( 'mb_substr' )
				? mb_substr( wp_strip_all_tags( $reply ), 0, 150 )
				: substr( wp_strip_all_tags( $reply ), 0, 150 );

			$results[] = array(
				'id'            => isset( $q['id'] ) ? (string) $q['id'] : '',
				'type'          => $q_type,
				'message'       => isset( $q['message'] ) ? (string) $q['message'] : '',
				'status'        => $status,
				'kw_score'      => $kw_score,
				'found_kw'      => $found_kw,
				'missing_kw'    => $missing_kw,
				'rag_used'      => $rag_used,
				'orch_path'     => is_string( $orch_path ) ? $orch_path : '',
				'model_used'    => is_string( $model_used ) ? $model_used : '',
				'reply_excerpt' => $reply_excerpt,
				'check_rag'     => $check_rag,
				'check_orch'    => $check_orch,
				'check_model'   => $check_model,
				'check_http'        => $check_http,
				'check_guardian'    => $q_type === 'b2b' ? $check_guardian : 'n/a',
				'check_radar'       => $q_type === 'b2b' ? $check_radar : 'n/a',
				'check_fiche'       => $q_type === 'b2b' ? $check_fiche : 'n/a',
				'check_not_generic' => $q_type === 'b2b' ? $check_not_generic : 'n/a',
				'error'             => $err_msg,
			);
		}

		$total     = count( $test_questions );
		$score_pct = $total > 0 ? (int) round( ( $pass / $total ) * 100 ) : 0;

		$result = array(
			'timestamp' => current_time( 'mysql' ),
			'score_pct' => $score_pct,
			'pass'      => $pass,
			'total'     => $total,
			'fail_rag'  => $fail_rag,
			'fail_kw'   => $fail_kw,
			'fail_orch' => $fail_orch,
			'results'   => $results,
		);

		update_option( 'avi_lqa_last_result', $result, false );

		$history = get_option( 'avi_lqa_history', array() );
		if ( ! is_array( $history ) ) {
			$history = array();
		}
		array_unshift(
			$history,
			array(
				'timestamp' => $result['timestamp'],
				'score_pct' => $score_pct,
				'pass'      => $pass,
				'total'     => $total,
				'fail_rag'  => $fail_rag,
			)
		);
		$history = array_slice( $history, 0, 10 );
		update_option( 'avi_lqa_history', $history, false );

		if ( $score_pct < 80 ) {
			$email      = get_option( 'avi_code_guard_alert_email', 'contact@avision.org' );
			$recipients = array_unique(
				array_filter(
					array(
						is_string( $email ) ? $email : 'contact@avision.org',
						'toniosbh@gmail.com',
					)
				)
			);
			$subject = '[AVI Live QA] Score degrade — ' . $score_pct . '% — ' . current_time( 'mysql' );
			$body    = "Le Live Chat QA a detecte des problemes sur le tunnel AVI.\n\n"
				. 'Score : ' . $score_pct . '% (' . $pass . '/' . $total . ")\n"
				. 'RAG non utilise : ' . $fail_rag . " question(s)\n"
				. 'Orchestrateur legacy : ' . $fail_orch . " question(s)\n"
				. 'Mots-cles manquants : ' . $fail_kw . " question(s)\n\n"
				. admin_url( 'admin.php?page=avi-avi-control-center&tab=qi_monitor' );
			foreach ( $recipients as $to ) {
				wp_mail( $to, $subject, $body );
			}
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX : exécute le watcher FastAPI (proxy).
	 *
	 * @return void
	 */
	public static function ajax_watcher_run() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_intelligence', 'nonce' );
		$key = self::admin_api_key();
		if ( ! is_string( $key ) || trim( $key ) === '' ) {
			wp_send_json_error( array( 'message' => __( 'Clé admin manquante.', 'avision-dashboard' ) ), 400 );
		}
		$url = 'http://127.0.0.1:8001/admin/admin/watcher/run';
		$res = wp_remote_post(
			$url,
			array(
				'timeout' => 310,
				'headers' => array(
					'Content-Type' => 'application/json; charset=utf-8',
					'X-Admin-Key'  => $key,
				),
				'body'    => wp_json_encode( array(), JSON_UNESCAPED_UNICODE ),
			)
		);
		if ( is_wp_error( $res ) ) {
			wp_send_json_error( array( 'message' => $res->get_error_message() ), 502 );
		}
		$code = (int) wp_remote_retrieve_response_code( $res );
		$body = (string) wp_remote_retrieve_body( $res );
		$data = json_decode( $body, true );
		if ( $code < 200 || $code >= 300 ) {
			wp_send_json_error( array( 'message' => 'HTTP ' . $code, 'body' => $body ), $code );
		}
		wp_send_json_success( is_array( $data ) ? $data : array( 'raw' => $body ) );
	}

	/**
	 * AJAX : lit les dernières lignes du log watcher (proxy local filesystem).
	 *
	 * @return void
	 */
	public static function ajax_watcher_log() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		check_ajax_referer( 'avision_cc_intelligence', 'nonce' );

		$paths = array(
			'/root/avision-backend/logs/watcher.log',
		);
		$log = '';
		foreach ( $paths as $p ) {
			if ( is_readable( $p ) ) {
				$log = (string) @file_get_contents( $p );
				break;
			}
		}
		if ( $log === '' ) {
			wp_send_json_error( array( 'message' => 'log_not_found' ), 404 );
		}
		$lines = preg_split( "/\r\n|\n|\r/", $log );
		$lines = is_array( $lines ) ? $lines : array();
		$tail  = array_slice( $lines, -20 );
		wp_send_json_success( array( 'lines' => $tail ) );
	}

	/**
	 * Onglet Tableau de bord (santé, stats, injections récentes, actions).
	 *
	 * @return void
	 */
	private static function render_dashboard_tab() {
		global $wpdb;
		$h           = self::http_backend_health();
		$stats_res   = self::knowledge_v1_get( '/api/v1/knowledge/stats' );
		$d_st        = ( $stats_res['ok'] && is_array( $stats_res['data'] ) ) ? $stats_res['data'] : null;
		$total_pts   = self::knowledge_stats_int( $d_st, array( 'total_points', 'total' ) );
		$cms_inj     = self::knowledge_stats_int( $d_st, array( 'cms_injected', 'cms_chunks', 'expert_seo_count' ) );
		$legacy_pts  = self::knowledge_stats_int( $d_st, array( 'legacy_points', 'legacy_count' ) );
		$by_sc       = ( is_array( $d_st ) && isset( $d_st['by_strategic_category'] ) && is_array( $d_st['by_strategic_category'] ) ) ? $d_st['by_strategic_category'] : array();
		$by_ut       = ( is_array( $d_st ) && isset( $d_st['by_user_type'] ) && is_array( $d_st['by_user_type'] ) ) ? $d_st['by_user_type'] : array();
		$rep_n       = isset( $by_sc['REPUTATION'] ) && is_numeric( $by_sc['REPUTATION'] ) ? (int) $by_sc['REPUTATION'] : 0;
		$loc_n       = isset( $by_sc['LOCAL_VISIBILITY'] ) && is_numeric( $by_sc['LOCAL_VISIBILITY'] ) ? (int) $by_sc['LOCAL_VISIBILITY'] : 0;
		$roi_n       = isset( $by_sc['ROI_AGENCE'] ) && is_numeric( $by_sc['ROI_AGENCE'] ) ? (int) $by_sc['ROI_AGENCE'] : 0;
		$b2c_n       = isset( $by_ut['b2c'] ) && is_numeric( $by_ut['b2c'] ) ? (int) $by_ut['b2c'] : 0;
		$b2b_n       = isset( $by_ut['b2b'] ) && is_numeric( $by_ut['b2b'] ) ? (int) $by_ut['b2b'] : 0;
		$core_n      = isset( $by_ut['core'] ) && is_numeric( $by_ut['core'] ) ? (int) $by_ut['core'] : 0;
		$key_ok      = self::admin_api_key() !== '';
		$nonce_dash  = wp_create_nonce( 'avision_cc_dashboard' );
		$t_ai        = $wpdb->prefix . 'avi_ai_interactions';
		$last_inj    = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, user_id, question, answer, level_context, created_at FROM {$t_ai} WHERE level_context IN (%s, %s) ORDER BY created_at DESC LIMIT 5",
				'inject_v2',
				'qi_baseline'
			),
			ARRAY_A
		);
		if ( ! is_array( $last_inj ) ) {
			$last_inj = array();
		}

		echo '<h2 style="margin-top:0;">' . esc_html__( 'Tableau de bord', 'avision-dashboard' ) . '</h2>';

		echo '<div class="avi-cc-dash-grid">';
		echo '<div class="avi-cc-dash-card"><h3>' . esc_html__( 'Backend FastAPI', 'avision-dashboard' ) . '</h3>';
		if ( $h['ok'] ) {
			echo '<p><span class="avi-cc-badge avi-cc-badge--ok">' . esc_html__( 'Opérationnel', 'avision-dashboard' ) . '</span></p>';
					} else {
			echo '<p><span class="avi-cc-badge avi-cc-badge--err">' . esc_html__( 'Hors ligne', 'avision-dashboard' ) . '</span></p>';
		}
		echo '</div>';

		echo '<div class="avi-cc-dash-card"><h3>' . esc_html__( 'Qdrant (total points)', 'avision-dashboard' ) . '</h3>';
		echo '<p class="avi-cc-dash-big">' . esc_html( (string) $total_pts ) . '</p></div>';

		echo '<div class="avi-cc-dash-card"><h3>' . esc_html__( 'Backend URL', 'avision-dashboard' ) . '</h3>';
		echo '<p><code>' . esc_html( self::backend_base_url() ) . '</code></p></div>';

		echo '<div class="avi-cc-dash-card"><h3>' . esc_html__( 'Clé Admin', 'avision-dashboard' ) . '</h3>';
		if ( $key_ok ) {
			echo '<p><span class="avi-cc-badge avi-cc-badge--ok">' . esc_html__( 'Définie ✅', 'avision-dashboard' ) . '</span></p>';
		} else {
			echo '<p><span class="avi-cc-badge avi-cc-badge--err">' . esc_html__( 'Manquante ❌', 'avision-dashboard' ) . '</span></p>';
		}
		echo '</div>';
		echo '</div>';

		echo '<h3>' . esc_html__( 'Stats mémoire AVI', 'avision-dashboard' ) . '</h3>';
		echo '<table class="widefat striped avi-cc-table"><tbody>';
		echo '<tr><th>' . esc_html__( 'Total chunks', 'avision-dashboard' ) . '</th><td>' . esc_html( (string) $total_pts ) . '</td></tr>';
		echo '<tr><th>REPUTATION / LOCAL_VISIBILITY / ROI_AGENCE</th><td>' . esc_html( (string) $rep_n ) . ' / ' . esc_html( (string) $loc_n ) . ' / ' . esc_html( (string) $roi_n ) . '</td></tr>';
		echo '<tr><th>B2C / B2B / CORE</th><td>' . esc_html( (string) $b2c_n ) . ' / ' . esc_html( (string) $b2b_n ) . ' / ' . esc_html( (string) $core_n ) . '</td></tr>';
		echo '<tr><th>' . esc_html__( 'Chunks CMS v2 (EXPERT_SEO)', 'avision-dashboard' ) . '</th><td>' . esc_html( (string) $cms_inj ) . '</td></tr>';
		echo '<tr><th>' . esc_html__( 'Legacy (sans métadonnées CMS)', 'avision-dashboard' ) . '</th><td>' . esc_html( (string) $legacy_pts ) . '</td></tr>';
		echo '</tbody></table>';

		echo '<h3>' . esc_html__( 'Dernières injections', 'avision-dashboard' ) . '</h3>';
		echo '<table class="widefat striped avi-cc-table"><thead><tr>';
		echo '<th>' . esc_html__( 'Date', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Type', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Aperçu', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Utilisateur', 'avision-dashboard' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $last_inj as $lr ) {
			if ( ! is_array( $lr ) ) {
				continue;
			}
			$ap = isset( $lr['answer'] ) ? (string) $lr['answer'] : '';
			if ( function_exists( 'mb_substr' ) ) {
				$ap = mb_substr( $ap, 0, 80 );
				} else {
				$ap = substr( $ap, 0, 80 );
			}
			echo '<tr>';
			echo '<td>' . esc_html( isset( $lr['created_at'] ) ? (string) $lr['created_at'] : '' ) . '</td>';
			echo '<td>' . esc_html( isset( $lr['level_context'] ) ? (string) $lr['level_context'] : '' ) . '</td>';
			echo '<td>' . esc_html( $ap ) . ( strlen( (string) ( $lr['answer'] ?? '' ) ) > 80 ? '…' : '' ) . '</td>';
			echo '<td>' . esc_html( isset( $lr['user_id'] ) ? (string) $lr['user_id'] : '' ) . '</td>';
			echo '</tr>';
		}
		if ( empty( $last_inj ) ) {
			echo '<tr><td colspan="4">' . esc_html__( 'Aucune entrée.', 'avision-dashboard' ) . '</td></tr>';
		}
		echo '</tbody></table>';

		echo '<p class="avi-cc-quick-actions">';
		echo '<button type="button" class="button" id="avi-cc-purge-litespeed">' . esc_html__( '🔄 Purger cache LiteSpeed', 'avision-dashboard' ) . '</button> ';
		echo '<button type="button" class="button button-primary" id="avi-cc-dashboard-refresh">' . esc_html__( '📊 Actualiser stats', 'avision-dashboard' ) . '</button>';
		echo '</p>';
		echo '<pre id="avi-cc-purge-log" class="avi-cc-purge-log" style="display:none;max-height:120px;overflow:auto;background:#f6f7f7;padding:8px;"></pre>';

		$err_gen = wp_json_encode( __( 'Erreur AJAX.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		printf(
			'<script>jQuery(function($){
var nd=%1$s;
$("#avi-cc-purge-litespeed").on("click",function(){
	$.post(ajaxurl,{action:"avision_cc_purge_cache",nonce:nd}).done(function(r){
		if(r&&r.success&&r.data&&typeof r.data.log==="string"){
			$("#avi-cc-purge-log").show().text(r.data.log);
			return;
		}
		alert(%2$s);
	}).fail(function(){alert(%2$s);});
});
$("#avi-cc-dashboard-refresh").on("click",function(){
	$.post(ajaxurl,{action:"avision_cc_dashboard_refresh",nonce:nd}).done(function(r){
		if(r&&r.success){location.reload();return;}
		alert(%2$s);
	}).fail(function(){alert(%2$s);});
});
});</script>',
			wp_json_encode( $nonce_dash, JSON_UNESCAPED_UNICODE ),
			$err_gen
		);
	}

	/**
	 * @return void
	 */
	public static function render() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Accès refusé.', 'avision-dashboard' ) );
		}

		$tab = isset( $_GET['tab'] ) ? sanitize_key( (string) wp_unslash( $_GET['tab'] ) ) : 'dashboard';
		if ( $tab === 'chat' ) {
			$tab = 'monitor';
		}
		$tabs_allowed = array( 'dashboard', 'knowledge', 'intelligence', 'monitor', 'qi_monitor', 'google_updates', 'announcements', 'health', 'dashboard_health', 'code_guard', 'security', 'api_monitor', 'integration_tests' );
		if ( ! in_array( $tab, $tabs_allowed, true ) ) {
			$tab = 'dashboard';
		}

		// --- POST delete Qdrant (DELETE FastAPI) ---
		if ( ! empty( $_POST['avision_cc_delete_point'] ) && isset( $_POST['_wpnonce'] ) ) {
			check_admin_referer( 'avision_cc_delete_point' );
			$del_id = isset( $_POST['cc_delete_point_id'] ) ? trim( (string) wp_unslash( $_POST['cc_delete_point_id'] ) ) : '';
			if ( $del_id === '' ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'ID de point manquant.', 'avision-dashboard' ) . '</p></div>';
			} else {
				$path_del = '/admin/qdrant/points/' . rawurlencode( $del_id );
				$del      = self::api_delete( $path_del );
				if ( $del['ok'] ) {
					self::delete_transients_prefixed_avision_qdrant();
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Point Qdrant supprimé.', 'avision-dashboard' ) . '</p></div>';
				} else {
					echo '<div class="notice notice-error"><p>' . esc_html( $del['error'] ?: ( 'HTTP ' . $del['code'] ) ) . '</p></div>';
				}
			}
		}

		// --- POST vider cache transients Qdrant (liste GET /admin/qdrant/points) ---
		if ( ! empty( $_POST['avision_cc_clear_qdrant_cache'] ) && isset( $_POST['_wpnonce'] ) ) {
			check_admin_referer( 'avision_cc_clear_qdrant_cache' );
			$n = self::delete_transients_prefixed_avision_qdrant();
			echo '<div class="notice notice-success is-dismissible"><p>';
			echo esc_html(
				sprintf(
					/* translators: %d: number of DB rows removed (transient + timeout rows). */
					__( 'Cache Qdrant vidé (%d entrée(s) d’options supprimées). Rechargez la liste.', 'avision-dashboard' ),
					$n
				)
			);
			echo '</p></div>';
		}

		// --- POST injection niche B2B → Qdrant (upsert FastAPI, pas de clé admin en JS) ---
		if ( ! empty( $_POST['avision_cc_niche_inject'] ) && isset( $_POST['_wpnonce'] ) ) {
			check_admin_referer( 'avision_cc_niche_inject' );
			$niche_title   = isset( $_POST['niche_title'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['niche_title'] ) ) : '';
			$niche_content = isset( $_POST['niche_content'] ) ? sanitize_textarea_field( (string) wp_unslash( $_POST['niche_content'] ) ) : '';
			if ( $niche_title === '' || $niche_content === '' ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'Titre et contenu sont obligatoires.', 'avision-dashboard' ) . '</p></div>';
			} else {
				$chunk  = '[B2B niche expertise: ' . $niche_title . ']' . "\n\n" . $niche_content;
				$nsc    = isset( $_POST['niche_strategic_category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['niche_strategic_category'] ) ) ) : 'B2B';
				$allow_n = array( 'GBP', 'B2B', 'ROI', 'HEATMAP', 'WHOLESALE' );
				if ( ! in_array( $nsc, $allow_n, true ) ) {
					$nsc = 'B2B';
				}
				$body_n = array(
					'content'            => $chunk,
					'strategic_category' => $nsc,
					'metadata'           => array(
						'user_type' => 'b2b',
						'category'  => 'STRATEGY',
						'source'    => 'niche_injector',
						'niche'     => $niche_title,
					),
					'embed'              => true,
				);
				$up_n = self::api_post_json( '/admin/qdrant/upsert', $body_n );
				if ( $up_n['ok'] ) {
					self::delete_transients_prefixed_avision_qdrant();
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Expertise niche synchronisée vers Qdrant.', 'avision-dashboard' ) . '</p></div>';
				} else {
					echo '<div class="notice notice-error"><p>' . esc_html( $up_n['error'] ?: ( 'HTTP ' . $up_n['code'] ) ) . '</p></div>';
				}
			}
		}

		$base = self::backend_base_url();
		$key  = self::admin_api_key();
		if ( $key === '' ) {
			echo '<div class="notice notice-warning"><p>' . esc_html__( 'Définissez AVISION_ADMIN_API_KEY dans wp-config.php ou l’option avision_admin_api_key (identique à ADMIN_API_KEY du backend).', 'avision-dashboard' ) . '</p></div>';
		}

		echo '<div class="wrap"><h1>AVI Control Center</h1>';
		echo '<p class="description">' . esc_html__( 'Backend FastAPI : ', 'avision-dashboard' ) . '<code>' . esc_html( $base ) . '</code></p>';

		echo '<style id="avi-control-center-styles">
#avi-control-center-wrap .avi-cc-dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;margin:16px 0;}
#avi-control-center-wrap .avi-cc-dash-card{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.06);}
#avi-control-center-wrap .avi-cc-dash-card h3{margin:0 0 8px;font-size:14px;color:#1d2327;}
#avi-control-center-wrap .avi-cc-dash-big{font-size:28px;font-weight:700;margin:0;}
#avi-control-center-wrap .nav-tab-wrapper{border-bottom:1px solid #c3c4c7;}
#avi-control-center-wrap .nav-tab.nav-tab-active{border-bottom:3px solid #0073aa;margin-bottom:-1px;}
#avi-control-center-wrap table.widefat.striped.avi-cc-table tbody tr:nth-child(odd){background:rgba(0,0,0,.02);}
#avi-control-center-wrap table.widefat.striped.avi-cc-table tbody tr:hover{background:rgba(0,115,170,.06);}
#avi-control-center-wrap .avi-cc-badge{display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600;}
#avi-control-center-wrap .avi-cc-badge--ok{background:#46b450;color:#fff;}
#avi-control-center-wrap .avi-cc-badge--warn{background:#ffb900;color:#1d2327;}
#avi-control-center-wrap .avi-cc-badge--err{background:#dc3232;color:#fff;}
#avi-control-center-wrap .avi-cc-quick-actions{margin-top:16px;}
</style>';
		echo '<div id="avi-control-center-wrap">';

		$url_d = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'dashboard',
			)
		);
		$url_k = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'knowledge',
			)
		);
		$url_m = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'monitor',
			)
		);
		$url_i = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'intelligence',
			)
		);
		$url_qm = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'qi_monitor',
			)
		);
		$url_g = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'google_updates',
			)
		);
		$url_ann = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'announcements',
			)
		);
		$url_h = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'health',
			)
		);
		$url_dh = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'dashboard_health',
			)
		);
		$url_cg = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'code_guard',
			)
		);
		$url_it = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'integration_tests',
			)
		);
		$url_sec = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'security',
			)
		);
		$url_api = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'api_monitor',
			)
		);
		echo '<h2 class="nav-tab-wrapper">';
		echo '<a href="' . esc_url( $url_d ) . '" class="nav-tab' . ( $tab === 'dashboard' ? ' nav-tab-active' : '' ) . '">' . esc_html__( 'Tableau de bord', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_k ) . '" class="nav-tab' . ( $tab === 'knowledge' ? ' nav-tab-active' : '' ) . '">' . esc_html__( 'Base de connaissance', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_i ) . '" class="nav-tab' . ( $tab === 'intelligence' ? ' nav-tab-active' : '' ) . '">' . esc_html__( 'Cerveau AVI', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_m ) . '" class="nav-tab' . ( $tab === 'monitor' ? ' nav-tab-active' : '' ) . '">' . esc_html__( 'Chat Monitor', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_qm ) . '" class="nav-tab' . ( $tab === 'qi_monitor' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🧠 QI Monitor', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_g ) . '" class="nav-tab' . ( $tab === 'google_updates' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🌐 Google Updates', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_ann ) . '" class="nav-tab' . ( $tab === 'announcements' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '📢 Announcements', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_h ) . '" class="nav-tab' . ( $tab === 'health' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🩺 Health Check', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_dh ) . '" class="nav-tab' . ( $tab === 'dashboard_health' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🛡️ Dashboard Health', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_cg ) . '" class="nav-tab' . ( $tab === 'code_guard' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🛡️ Code Guard', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_it ) . '" class="nav-tab' . ( $tab === 'integration_tests' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🔬 Integration Tests', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_sec ) . '" class="nav-tab' . ( $tab === 'security' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🛡️ Security', 'avision-dashboard' ) . '</a>';
		echo '<a href="' . esc_url( $url_api ) . '" class="nav-tab' . ( $tab === 'api_monitor' ? ' nav-tab-active' : '' ) . '">' . esc_html__( '🔌 API & Crons', 'avision-dashboard' ) . '</a>';
		echo '</h2>';

		switch ( $tab ) {
			case 'dashboard':
				self::render_dashboard_tab();
				break;
			case 'knowledge':
				self::render_knowledge_tab();
				break;
			case 'intelligence':
				self::render_intelligence_tab();
				break;
			case 'monitor':
				self::render_chat_monitor_tab();
				break;
			case 'qi_monitor':
				self::render_qi_monitor_tab();
				break;
			case 'google_updates':
				self::render_google_updates_tab();
				break;
			case 'announcements':
				self::render_announcements_tab();
				break;
			case 'health':
				self::render_health_tab();
				break;
			case 'dashboard_health':
				if ( class_exists( 'Avision_Dashboard_Health' ) ) {
					Avision_Dashboard_Health::render_tab();
				} else {
					echo '<div class="notice notice-error"><p>Avision_Dashboard_Health non chargé.</p></div>';
				}
				break;
			case 'code_guard':
				self::render_code_guard_tab();
				break;
			case 'integration_tests':
				self::render_integration_tests_tab();
				break;
			case 'security':
				self::render_security_tab();
				break;
			case 'api_monitor':
				self::render_api_monitor_tab();
				break;
			default:
				self::render_dashboard_tab();
				break;
		}

		echo '</div></div>';
	}

	/**
	 * @return void
	 */
	private static function render_health_tab() {
		$results_full = class_exists( 'Avision_Health_Check' ) ? Avision_Health_Check::get_last_result( 'full' ) : null;
		$results_fast = class_exists( 'Avision_Health_Check' ) ? Avision_Health_Check::get_last_result( 'fast' ) : null;
		$results        = is_array( $results_full ) ? $results_full : null;
		$last_for_cron  = is_array( $results_full ) ? $results_full : ( is_array( $results_fast ) ? $results_fast : null );

		$nonce = wp_create_nonce( 'avision_cc_hc' );
		$token = class_exists( 'Avision_Health_Check' ) ? Avision_Health_Check::get_token() : '';
		$b2c_url = home_url( '/premium-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=starter_pro';
		$b2b_url = home_url( '/agency-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=agency_t1';

		$score = ( $results && isset( $results['score'] ) ) ? (int) $results['score'] : 0;
		$max = ( $results && isset( $results['max_score'] ) ) ? (int) $results['max_score'] : 0;
		$pct = ( $max > 0 ) ? (int) round( ( $score / $max ) * 100 ) : 0;
		if ( $pct < 0 ) {
			$pct = 0;
		}
		if ( $pct > 100 ) {
			$pct = 100;
		}

		$dash = 2 * pi() * 30;
		$dash = is_float( $dash ) ? $dash : 188.5;
		$dasharray = round( (float) $dash, 2 );
		$dashoffset = round( (float) $dasharray * ( 1 - ( $pct / 100 ) ), 2 );

		$full_ts = ( is_array( $results_full ) && isset( $results_full['timestamp'] ) ) ? (int) $results_full['timestamp'] : 0;
		$fast_ts = ( is_array( $results_fast ) && isset( $results_fast['timestamp'] ) ) ? (int) $results_fast['timestamp'] : 0;

		$history = get_option( 'avision_hc_history', array() );
		if ( ! is_array( $history ) ) {
			$history = array();
		}
		$history7 = array_slice( $history, 0, 7 );

		echo '<div id="avi-health-tab-wrap">';
		echo '<style>
#avi-health-tab-wrap{--avi-ok:#1D9E75;--avi-ok-bg:#E1F5EE;--avi-ok-text:#085041;--avi-warn:#BA7517;--avi-warn-bg:#FAEEDA;--avi-warn-text:#633806;--avi-fail:#A32D2D;--avi-fail-bg:#FCEBEB;--avi-fail-text:#501313;--avi-navy:#0A1628;--avi-purple:#7F77DD;--avi-card-bg:#ffffff;--avi-border:rgba(0,0,0,0.08);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;}
#avi-health-tab-wrap .ahc-topbar{background:linear-gradient(135deg,var(--avi-navy),#111c33);border-radius:12px;padding:18px 18px;display:flex;gap:18px;align-items:center;justify-content:space-between;border:1px solid rgba(255,255,255,0.06);}
#avi-health-tab-wrap .ahc-left{display:flex;align-items:center;gap:16px;}
#avi-health-tab-wrap .ahc-gauge{width:76px;height:76px;flex:0 0 auto;position:relative;}
#avi-health-tab-wrap .ahc-gauge svg{display:block;}
#avi-health-tab-wrap .ahc-gauge .ahc-score{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-weight:700;line-height:1;}
#avi-health-tab-wrap .ahc-gauge .ahc-score small{opacity:.85;font-weight:600;font-size:11px;margin-top:4px;}
#avi-health-tab-wrap .ahc-meta{color:rgba(255,255,255,0.85);font-size:12px;line-height:1.35;}
#avi-health-tab-wrap .ahc-meta code{background:rgba(255,255,255,0.08);color:#fff;padding:2px 6px;border-radius:6px;}
#avi-health-tab-wrap .ahc-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end;}
#avi-health-tab-wrap .ahc-btn{border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.08);color:#fff;border-radius:10px;padding:10px 12px;font-weight:700;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:8px;}
#avi-health-tab-wrap .ahc-btn:hover{background:rgba(255,255,255,0.14);}
#avi-health-tab-wrap .ahc-btn.primary{background:rgba(127,119,221,0.26);border-color:rgba(127,119,221,0.5);}
#avi-health-tab-wrap .ahc-fastbar{margin-top:14px;background:#fff;border:1px solid var(--avi-border);border-radius:12px;padding:12px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;}
#avi-health-tab-wrap .ahc-fastbar .ahc-fast-items{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}
#avi-health-tab-wrap .ahc-dot{width:10px;height:10px;border-radius:999px;display:inline-block;vertical-align:middle;margin-right:6px;}
#avi-health-tab-wrap .dot-ok{background:var(--avi-ok);}
#avi-health-tab-wrap .dot-warn{background:var(--avi-warn);}
#avi-health-tab-wrap .dot-fail{background:var(--avi-fail);}
#avi-health-tab-wrap .ahc-pill{border-radius:999px;padding:6px 10px;font-weight:800;font-size:12px;border:1px solid var(--avi-border);background:#fff;color:#111;}
#avi-health-tab-wrap .ok-pill{background:var(--avi-ok-bg);color:var(--avi-ok-text);border-color:rgba(29,158,117,0.25);}
#avi-health-tab-wrap .warn-pill{background:var(--avi-warn-bg);color:var(--avi-warn-text);border-color:rgba(186,117,23,0.25);}
#avi-health-tab-wrap .fail-pill{background:var(--avi-fail-bg);color:var(--avi-fail-text);border-color:rgba(163,45,45,0.25);}
#avi-health-tab-wrap .ahc-grid{margin-top:14px;display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px;}
#avi-health-tab-wrap .ahc-card{background:var(--avi-card-bg);border:1px solid var(--avi-border);border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.04);}
#avi-health-tab-wrap .ahc-card-head{padding:14px 14px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;gap:10px;}
#avi-health-tab-wrap .ahc-card-title{display:flex;align-items:center;gap:10px;}
#avi-health-tab-wrap .ahc-icon{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-weight:900;background:#f3f4f6;color:#111;border:1px solid var(--avi-border);}
#avi-health-tab-wrap .ahc-card-title strong{display:block;font-size:14px;color:#0b1220;}
#avi-health-tab-wrap .ahc-card-title span{display:block;font-size:12px;color:#5b677a;margin-top:2px;}
#avi-health-tab-wrap .ahc-chevron{transition:transform .18s ease;color:#667085;font-weight:900;}
#avi-health-tab-wrap .ahc-card.open .ahc-chevron{transform:rotate(180deg);}
#avi-health-tab-wrap .ahc-card-body{padding:0 14px 14px;}
#avi-health-tab-wrap .ahc-section{border:1px solid var(--avi-border);border-radius:8px;margin-top:12px;overflow:hidden;background:#fff;}
#avi-health-tab-wrap .ahc-sec-head{padding:10px 12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;background:#fbfbfc;}
#avi-health-tab-wrap .ahc-sec-head .lbl{font-weight:900;letter-spacing:0.08em;font-size:11px;color:#4a5568;text-transform:uppercase;}
#avi-health-tab-wrap .ahc-sec-head .meta{display:flex;align-items:center;gap:10px;}
#avi-health-tab-wrap .ahc-sec-body{padding:10px 12px;}
#avi-health-tab-wrap .ahc-sec-body[style*="display:none"]{padding:0 12px;}
#avi-health-tab-wrap .ahc-bar{height:3px;background:#eef1f5;border-radius:999px;overflow:hidden;margin:8px 0 10px;}
#avi-health-tab-wrap .ahc-bar > i{display:block;height:100%;background:var(--avi-purple);width:0%;}
#avi-health-tab-wrap .ahc-item{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:9px 0;border-top:1px dashed rgba(0,0,0,0.08);}
#avi-health-tab-wrap .ahc-item:first-child{border-top:0;}
#avi-health-tab-wrap .ahc-item .left{display:flex;gap:10px;align-items:flex-start;}
#avi-health-tab-wrap .ahc-st{width:22px;height:22px;border-radius:999px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:900;flex:0 0 auto;}
#avi-health-tab-wrap .st-ok{background:var(--avi-ok-bg);color:var(--avi-ok-text);}
#avi-health-tab-wrap .st-warn{background:var(--avi-warn-bg);color:var(--avi-warn-text);border-radius:7px;}
#avi-health-tab-wrap .st-fail{background:var(--avi-fail-bg);color:var(--avi-fail-text);}
#avi-health-tab-wrap .ahc-item .txt strong{display:block;font-size:13px;color:#111827;}
#avi-health-tab-wrap .ahc-item .txt small{display:block;font-size:12px;color:#667085;margin-top:2px;}
#avi-health-tab-wrap .ahc-item .val{font-weight:900;color:#111827;white-space:nowrap;}
#avi-health-tab-wrap .ahc-card-actions{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;}
#avi-health-tab-wrap .ahc-card-actions a{border:1px solid var(--avi-border);background:#fff;border-radius:10px;padding:8px 10px;font-weight:900;text-decoration:none;color:#0b1220;}
#avi-health-tab-wrap .ahc-card-actions a:hover{border-color:rgba(127,119,221,0.55);box-shadow:0 1px 0 rgba(127,119,221,0.12);}
#avi-health-tab-wrap .ahc-icon.hc-icon-purple{background:rgba(127,119,221,0.18);color:var(--avi-purple);border-color:rgba(127,119,221,0.35);}
#avi-health-tab-wrap .ahc-cron-gauge-strip{padding:0 14px 12px;margin-top:-4px;}
#avi-health-tab-wrap .ahc-cron-gauge-track{height:8px;border-radius:999px;background:#eef1f5;overflow:hidden;}
#avi-health-tab-wrap .ahc-cron-gauge-fill{height:100%;border-radius:999px;min-width:0;}
#avi-health-tab-wrap .ahc-cron-gauge-fill.g-ok{background:var(--avi-ok);}
#avi-health-tab-wrap .ahc-cron-gauge-fill.g-warn{background:var(--avi-warn);}
#avi-health-tab-wrap .ahc-cron-gauge-fill.g-fail{background:var(--avi-fail);}
#avi-health-tab-wrap .ahc-cron-gauge-label{font-size:11px;color:#667085;margin-top:6px;line-height:1.35;}
#avi-health-tab-wrap .ahc-history{margin-top:16px;background:#fff;border:1px solid var(--avi-border);border-radius:12px;padding:14px;}
#avi-health-tab-wrap .ahc-history h3{margin:0 0 10px;font-size:14px;}
#avi-health-tab-wrap .ahc-history table{width:100%;border-collapse:collapse;}
#avi-health-tab-wrap .ahc-history th,#avi-health-tab-wrap .ahc-history td{padding:10px 8px;border-bottom:1px solid rgba(0,0,0,0.06);text-align:left;font-size:12px;}
#avi-health-tab-wrap .ahc-empty{margin-top:14px;background:#fff;border:1px dashed rgba(0,0,0,0.18);border-radius:12px;padding:18px;text-align:center;}
#avi-health-tab-wrap .ahc-empty p{margin:0;color:#475467;font-size:13px;}
#avi-health-tab-wrap .ahc-empty .big{display:inline-flex;align-items:center;gap:10px;margin-top:12px;background:var(--avi-purple);color:#fff;border:0;border-radius:12px;padding:12px 16px;font-weight:900;cursor:pointer;text-decoration:none;}
</style>';

		echo '<script>var aviHcNonce = \'' . esc_js( $nonce ) . '\';</script>';

		echo '<div class="ahc-topbar">';
		echo '<div class="ahc-left">';
		if ( $results ) {
			echo '<div class="ahc-gauge">';
			echo '<svg viewBox="0 0 76 76" width="76" height="76" aria-hidden="true">';
			echo '<circle cx="38" cy="38" r="30" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="8"></circle>';
			echo '<circle cx="38" cy="38" r="30" fill="none" stroke="rgba(127,119,221,0.95)" stroke-width="8" stroke-linecap="round" stroke-dasharray="' . esc_attr( (string) $dasharray ) . '" stroke-dashoffset="' . esc_attr( (string) $dashoffset ) . '" transform="rotate(-90 38 38)"></circle>';
			echo '</svg>';
			echo '<div class="ahc-score">' . esc_html( (string) $score ) . '/' . esc_html( (string) $max ) . '<small>score</small></div>';
			echo '</div>';
		} else {
			echo '<div class="ahc-gauge">';
			echo '<svg viewBox="0 0 76 76" width="76" height="76" aria-hidden="true">';
			echo '<circle cx="38" cy="38" r="30" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="8"></circle>';
			echo '</svg>';
			echo '<div class="ahc-score">—<small>score</small></div>';
			echo '</div>';
		}
		echo '<div class="ahc-meta">';
		echo '<div><strong style="color:#fff;">AVI Health Check</strong></div>';
		echo '<div>Dernier <code>full</code> : ' . esc_html( $full_ts ? gmdate( 'Y-m-d H:i:s', $full_ts ) . ' UTC' : '—' ) . '</div>';
		echo '<div>Dernier <code>fast</code> : ' . esc_html( $fast_ts ? gmdate( 'Y-m-d H:i:s', $fast_ts ) . ' UTC' : '—' ) . '</div>';
		echo '</div>';
		echo '</div>';

		echo '<div class="ahc-actions">';
		echo '<button class="ahc-btn primary" id="ahcRunFull">Full Check (~20s)</button>';
		echo '<button class="ahc-btn" id="ahcRunFast">Fast Check (~5s)</button>';
		echo '<a class="ahc-btn" href="' . esc_url( $b2c_url ) . '" target="_blank" rel="noopener">Simuler B2C</a>';
		echo '<a class="ahc-btn" href="' . esc_url( $b2b_url ) . '" target="_blank" rel="noopener">Simuler B2B</a>';
		echo '</div>';
		echo '</div>';

		// FAST CHECK BAR
		echo '<div class="ahc-fastbar">';
		echo '<div class="ahc-fast-items">';
		echo '<strong style="display:flex;align-items:center;gap:8px;"><span style="font-size:14px;">⚡</span> Fast</strong>';
		if ( is_array( $results_fast ) && isset( $results_fast['categories'] ) && is_array( $results_fast['categories'] ) ) {
			foreach ( $results_fast['categories'] as $k => $c ) {
				if ( ! is_array( $c ) ) {
					continue;
				}
				$cs = isset( $c['score'] ) ? (int) $c['score'] : 0;
				$cm = isset( $c['max'] ) ? (int) $c['max'] : 0;
				$st = ( $cm > 0 && $cs === $cm ) ? 'ok' : ( ( $cs > 0 ) ? 'warn' : 'fail' );
				$dot = $st === 'ok' ? 'dot-ok' : ( $st === 'warn' ? 'dot-warn' : 'dot-fail' );
				$lbl = isset( $c['label'] ) ? (string) $c['label'] : (string) $k;
				echo '<span><i class="ahc-dot ' . esc_attr( $dot ) . '"></i>' . esc_html( $lbl ) . '</span>';
			}
		} else {
			echo '<span style="color:#667085;">Aucun fast check</span>';
		}
		echo '</div>';
		$fs = ( is_array( $results_fast ) && isset( $results_fast['score'], $results_fast['max_score'] ) ) ? (int) $results_fast['score'] . '/' . (int) $results_fast['max_score'] : '—';
		echo '<span class="ahc-pill">' . esc_html( $fs ) . '</span>';
		echo '</div>';

		// EMPTY STATE
		if ( ! $results ) {
			echo '<div class="ahc-empty">';
			echo '<p><strong>Aucun check exécuté</strong> — cliquez Full Check.</p>';
			echo '<p>Durée estimée : <strong>20 secondes</strong></p>';
			echo '<a class="big" href="#" onclick="document.getElementById(\'ahcRunFull\').click();return false;">▶ Lancer le premier Health Check</a>';
			echo '</div>';
			echo '</div>';
			self::render_health_tab_js();
			return;
		}

		// GRILLE DES CARTES (6 catégories)
		echo '<div class="ahc-grid">';
		$cats = isset( $results['categories'] ) && is_array( $results['categories'] ) ? $results['categories'] : array();
		foreach ( $cats as $ck => $cat ) {
			if ( ! is_array( $cat ) ) {
				continue;
			}
			$clabel = isset( $cat['label'] ) ? (string) $cat['label'] : (string) $ck;
			$cs = isset( $cat['score'] ) ? (int) $cat['score'] : 0;
			$cm = isset( $cat['max'] ) ? (int) $cat['max'] : 0;
			$pill = ( $cm > 0 && $cs === $cm ) ? 'ok-pill' : ( ( $cs > 0 ) ? 'warn-pill' : 'fail-pill' );
			$open = ( $cm > 0 && $cs < $cm ) ? ' open' : '';
			echo '<div class="ahc-card' . esc_attr( $open ) . '" data-cat="' . esc_attr( (string) $ck ) . '">';
			echo '<div class="ahc-card-head" onclick="ahcToggleCard(this)">';
			echo '<div class="ahc-card-title">';
			if ( (string) $ck === 'cron_capacity' ) {
				echo '<div class="ahc-icon hc-icon-purple" aria-hidden="true">⏱</div>';
			} else {
				echo '<div class="ahc-icon">' . esc_html( strtoupper( substr( $clabel, 0, 1 ) ) ) . '</div>';
			}
			echo '<div><strong>' . esc_html( $clabel ) . '</strong><span>' . esc_html( $cs . ' / ' . $cm ) . '</span></div>';
			echo '</div>';
			echo '<div style="display:flex;align-items:center;gap:10px;">';
			echo '<span class="ahc-pill ' . esc_attr( $pill ) . '">' . esc_html( $cs . '/' . $cm ) . '</span>';
			echo '<span class="ahc-chevron">⌄</span>';
			echo '</div>';
			echo '</div>';

			if ( (string) $ck === 'cron_capacity' ) {
				$cron_gauge = null;
				if ( isset( $cat['hc_gauge'] ) && is_array( $cat['hc_gauge'] ) ) {
					$cron_gauge = $cat['hc_gauge'];
				} elseif ( is_array( $last_for_cron ) && isset( $last_for_cron['categories']['cron_capacity']['hc_gauge'] ) && is_array( $last_for_cron['categories']['cron_capacity']['hc_gauge'] ) ) {
					$cron_gauge = $last_for_cron['categories']['cron_capacity']['hc_gauge'];
				}
				if ( is_array( $cron_gauge ) ) {
					$up = isset( $cron_gauge['utilization_pct'] ) ? (int) $cron_gauge['utilization_pct'] : 0;
					$ga = isset( $cron_gauge['guardian_active'] ) ? (int) $cron_gauge['guardian_active'] : 0;
					$cp = isset( $cron_gauge['capacity_per_hour'] ) ? (int) $cron_gauge['capacity_per_hour'] : 0;
					$bar_w = min( 100, max( 0, $up ) );
					$gcls  = ( $up < 80 ) ? 'g-ok' : ( ( $up < 100 ) ? 'g-warn' : 'g-fail' );
					echo '<div class="ahc-cron-gauge-strip">';
					echo '<div class="ahc-cron-gauge-track"><div class="ahc-cron-gauge-fill ' . esc_attr( $gcls ) . '" style="width:' . esc_attr( (string) $bar_w ) . '%"></div></div>';
					echo '<div class="ahc-cron-gauge-label">' . esc_html( $ga . ' / ' . $cp . ' profils · ' . $up . '%' ) . '</div>';
					echo '</div>';
				}
			}

			$body_style = ( $open !== '' ) ? '' : ' style="display:none"';
			echo '<div class="ahc-card-body"' . $body_style . '>';
			$sections = isset( $cat['sections'] ) && is_array( $cat['sections'] ) ? $cat['sections'] : array();
			foreach ( $sections as $sec ) {
				if ( ! is_array( $sec ) ) {
					continue;
				}
				$sl = isset( $sec['label'] ) ? (string) $sec['label'] : 'Section';
				$ss = isset( $sec['score'] ) ? (int) $sec['score'] : 0;
				$sm = isset( $sec['max'] ) ? (int) $sec['max'] : 0;
				$sec_open = ( $sm > 0 && $ss < $sm && $open !== '' ) ? '' : ' style="display:none"';
				$w = ( $sm > 0 ) ? (int) round( ( $ss / $sm ) * 100 ) : 0;
				if ( $w < 0 ) {
					$w = 0;
				}
				if ( $w > 100 ) {
					$w = 100;
				}
				echo '<div class="ahc-section">';
				echo '<div class="ahc-sec-head" onclick="ahcToggleSection(this)">';
				echo '<div class="lbl">' . esc_html( $sl ) . '</div>';
				echo '<div class="meta"><span class="ahc-pill">' . esc_html( $ss . '/' . $sm ) . '</span><span class="ahc-chevron">⌄</span></div>';
				echo '</div>';
				echo '<div class="ahc-sec-body"' . $sec_open . '>';
				echo '<div class="ahc-bar"><i style="width:' . esc_attr( (string) $w ) . '%"></i></div>';
				$items = isset( $sec['items'] ) && is_array( $sec['items'] ) ? $sec['items'] : array();
				foreach ( $items as $it ) {
					if ( ! is_array( $it ) ) {
						continue;
					}
					$il = isset( $it['label'] ) ? (string) $it['label'] : '';
					$st = isset( $it['status'] ) ? (string) $it['status'] : 'warn';
					$detail = isset( $it['detail'] ) ? (string) $it['detail'] : '';
					$val = isset( $it['val'] ) ? (string) $it['val'] : '';
					$cls = $st === 'ok' ? 'st-ok' : ( $st === 'fail' ? 'st-fail' : 'st-warn' );
					$ic = $st === 'ok' ? '✓' : ( $st === 'fail' ? '✗' : '⚠' );
					echo '<div class="ahc-item">';
					echo '<div class="left"><div class="ahc-st ' . esc_attr( $cls ) . '">' . esc_html( $ic ) . '</div>';
					echo '<div class="txt"><strong>' . esc_html( $il ) . '</strong><small>' . esc_html( $detail ) . '</small></div></div>';
					echo '<div class="val">' . esc_html( $val ) . '</div>';
					echo '</div>';
				}
				echo '</div>';
				echo '</div>';
			}

			// Boutons simulation bas de carte.
			if ( (string) $ck === 'premium_dom' ) {
				$sim1 = home_url( '/premium-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=starter_pro';
				$sim2 = home_url( '/premium-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=elite_pro';
				echo '<div class="ahc-card-actions">';
				echo '<a href="' . esc_url( $sim1 ) . '" target="_blank" rel="noopener">Simuler starter_pro</a>';
				echo '<a href="' . esc_url( $sim2 ) . '" target="_blank" rel="noopener">Simuler elite_pro</a>';
				echo '</div>';
			} elseif ( (string) $ck === 'agency_dom' ) {
				$sim1 = home_url( '/agency-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=agency_t1';
				$sim2 = home_url( '/agency-dashboard/' ) . '?avi_hc_bypass=' . rawurlencode( $token ) . '&avi_plan=agency_t3';
				echo '<div class="ahc-card-actions">';
				echo '<a href="' . esc_url( $sim1 ) . '" target="_blank" rel="noopener">Simuler agency_t1</a>';
				echo '<a href="' . esc_url( $sim2 ) . '" target="_blank" rel="noopener">Simuler agency_t3</a>';
				echo '</div>';
			}

			echo '</div>'; // card-body
			echo '</div>'; // card
		}
		echo '</div>'; // grid

		// HISTORIQUE
		echo '<div class="ahc-history">';
		echo '<h3>Historique des 7 derniers Full Checks</h3>';
		if ( empty( $history7 ) ) {
			echo '<p style="margin:0;color:#667085;">Aucun historique.</p>';
		} else {
			echo '<table><thead><tr><th>Date (UTC)</th><th>Score</th><th>Status global</th></tr></thead><tbody>';
			foreach ( $history7 as $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}
				$ts = isset( $row['timestamp'] ) ? (int) $row['timestamp'] : 0;
				$sc = isset( $row['score'] ) ? (int) $row['score'] : 0;
				$mx = isset( $row['max_score'] ) ? (int) $row['max_score'] : 0;
				$st = ( $mx > 0 && $sc === $mx ) ? 'OK' : ( ( $sc > 0 ) ? 'WARN' : 'FAIL' );
				$cls = $st === 'OK' ? 'ok-pill' : ( $st === 'FAIL' ? 'fail-pill' : 'warn-pill' );
				echo '<tr>';
				echo '<td>' . esc_html( $ts ? gmdate( 'Y-m-d H:i:s', $ts ) : '—' ) . '</td>';
				echo '<td>' . esc_html( $sc . '/' . $mx ) . '</td>';
				echo '<td><span class="ahc-pill ' . esc_attr( $cls ) . '">' . esc_html( $st ) . '</span></td>';
				echo '</tr>';
			}
			echo '</tbody></table>';
		}
		echo '</div>';

		echo '</div>';
		self::render_health_tab_js();
	}

	/**
	 * @return void
	 */
	private static function render_health_tab_js() {
		echo '<script>
function ahcToggleCard(el){
	var card = el && el.closest ? el.closest(".ahc-card") : null;
	if(!card) return;
	var body = card.querySelector(".ahc-card-body");
	if(!body) return;
	var open = body.style.display !== "none";
	body.style.display = open ? "none" : "";
	if(open){ card.classList.remove("open"); } else { card.classList.add("open"); }
}
function ahcToggleSection(el){
	var sec = el && el.closest ? el.closest(".ahc-section") : null;
	if(!sec) return;
	var body = sec.querySelector(".ahc-sec-body");
	if(!body) return;
	body.style.display = (body.style.display === "none" || body.getAttribute("style")==="display:none") ? "" : "none";
}
function ahcRun(action){
	var btnFull = document.getElementById("ahcRunFull");
	var btnFast = document.getElementById("ahcRunFast");
	var btn = action==="avision_hc_run_full" ? btnFull : btnFast;
	if(btn){ btn.disabled = true; btn.textContent = "⏳ Exécution..."; }
	var fd = new FormData();
	fd.append("action", action);
	fd.append("nonce", (typeof aviHcNonce!=="undefined") ? aviHcNonce : (typeof aviCcNonce!=="undefined") ? aviCcNonce : "");
	fetch(ajaxurl, {method:"POST", body: fd, credentials:"same-origin"}).then(function(r){return r.json();}).then(function(j){
		if(j && j.success){ location.reload(); return; }
		alert("Health Check: erreur AJAX");
		location.reload();
	}).catch(function(){ alert("Health Check: erreur réseau"); location.reload(); });
}
document.addEventListener("DOMContentLoaded", function(){
	var btnFull = document.getElementById("ahcRunFull");
	var btnFast = document.getElementById("ahcRunFast");
	if(btnFull){ btnFull.addEventListener("click", function(e){ e.preventDefault(); ahcRun("avision_hc_run_full"); }); }
	if(btnFast){ btnFast.addEventListener("click", function(e){ e.preventDefault(); ahcRun("avision_hc_run_fast"); }); }
	// Auto-ouvrir cartes contenant des fails.
	var cards = document.querySelectorAll("#avi-health-tab-wrap .ahc-card");
	cards.forEach(function(c){
		var pill = c.querySelector(".ahc-pill");
		if(pill && pill.classList.contains("fail-pill")){
			var body = c.querySelector(".ahc-card-body");
			if(body){ body.style.display = ""; c.classList.add("open"); }
		}
	});
});
</script>';
	}

	/**
	 * Carte clé d’option WP / source Qdrant → libellé métier (Knowledge Base).
	 *
	 * @return array<string, string>
	 */
	private static function kb_option_key_role_labels() {
		return array(
			'avi_prompt_system'                 => 'Cerveau Central (Identité)',
			'avi_kc_general_suspensions'        => 'Protocole Crise (Suspension)',
			'avi_prompt_b2b_welcome'            => 'Accueil Agence',
			'avi_prompt_b2b_welcome_en'        => 'Accueil Agence (EN)',
			'avi_prompt_b2b_welcome_es'        => 'Accueil Agence (ES)',
			'avi_prompt_b2b_global'            => 'Savoir global B2B',
			'avi_prompt_b2c_welcome'           => 'Accueil Coach B2C',
			'avi_prompt_b2c_global'            => 'Savoir global B2C',
			'avi_prompt_b2c_encyclopedia'      => 'Encyclopédie B2C',
			'avi_prompt_b2c_hotel'             => 'Lexique vertical Hôtel',
			'avi_prompt_b2c_restaurant'        => 'Lexique vertical Restaurant',
			'avi_prompt_b2c_sab'               => 'Lexique vertical SAB',
			'avi_kc_restricted_words'          => 'Mots tabous / règles SEO',
			'gbp_master_knowledge'             => 'Base maître GBP (6 piliers)',
			'b2b_master_knowledge'             => 'Base maître Agence B2B',
			'avi_knowledge_levels_memory'      => 'Mémoire niveaux (JSON)',
			'avi_knowledge_guardian_memory'    => 'Mémoire Mode Gardien',
			'avi_knowledge_notifications_memory' => 'Mémoire notifications',
			'encyclopedie_dashboard'           => 'Encyclopédie Dashboard',
			'lexique_officiel'                 => 'Lexique officiel AVI',
			'rag_chunk'                        => 'Chunk RAG (import)',
			'manual_upsert'                    => 'Saisie manuelle (Control Center)',
		);
	}

	/**
	 * Extrait une clé « option WP » ou source canonique depuis les métadonnées Qdrant.
	 *
	 * @param array<string, mixed> $meta Métadonnées point.
	 * @return string
	 */
	private static function kb_resolve_option_key_from_metadata( array $meta ) {
		$candidates = array( 'wp_option', 'option', 'option_name', 'source', 'origin', 'chunk_source' );
		foreach ( $candidates as $k ) {
			if ( empty( $meta[ $k ] ) || ! is_string( $meta[ $k ] ) ) {
				continue;
			}
			$v = trim( $meta[ $k ] );
			if ( $v !== '' ) {
				return sanitize_key( $v );
			}
		}
		return '';
	}

	/**
	 * Libellé « Rôle / fonction » à partir de la clé (wp_option ou équivalent).
	 *
	 * @param string $key Clé normalisée.
	 * @return string
	 */
	private static function kb_role_label_from_option_key( $key ) {
		$key = is_string( $key ) ? sanitize_key( $key ) : '';
		if ( $key === '' ) {
			return __( '— (non renseigné)', 'avision-dashboard' );
		}
		$map = self::kb_option_key_role_labels();
		if ( isset( $map[ $key ] ) ) {
			return $map[ $key ];
		}
		if ( strpos( $key, 'avi_kc_' ) === 0 ) {
			return __( 'Bloc KC : ', 'avision-dashboard' ) . $key;
		}
		if ( strpos( $key, 'avi_prompt_' ) === 0 ) {
			return __( 'Prompt système : ', 'avision-dashboard' ) . $key;
		}
		return __( 'Document : ', 'avision-dashboard' ) . $key;
	}

	/**
	 * Chaîne « cible » brute depuis le payload Qdrant (priorité payload canonique).
	 *
	 * Ordre : target → cible_tableau → user_type (rétrocompatibilité).
	 *
	 * @param array<string, mixed> $meta Métadonnées.
	 * @return string Minuscules, sans espaces superflus (peut être vide).
	 */
	private static function kb_target_raw_from_metadata( array $meta ) {
		foreach ( array( 'target', 'cible_tableau', 'user_type' ) as $k ) {
			if ( empty( $meta[ $k ] ) || ! is_string( $meta[ $k ] ) ) {
				continue;
			}
			$v = trim( $meta[ $k ] );
			if ( $v !== '' ) {
				return strtolower( $v );
			}
		}
		return '';
	}

	/**
	 * Normalise la cible pour badge, filtre et data-attributes : core | b2c | b2b | system.
	 *
	 * @param array<string, mixed> $meta Métadonnées.
	 * @return string
	 */
	private static function kb_normalize_target_key_from_metadata( array $meta ) {
		$raw = self::kb_target_raw_from_metadata( $meta );
		if ( $raw === '' ) {
			return 'core';
		}
		$fold = $raw;
		if ( function_exists( 'remove_accents' ) ) {
			$fold = strtolower( remove_accents( $fold ) );
		}
		if ( $fold === 'b2c' || $fold === 'consumer' ) {
			return 'b2c';
		}
		if ( $fold === 'b2b' || $fold === 'agency' || strpos( $fold, 'b2b' ) !== false ) {
			return 'b2b';
		}
		if ( $fold === 'system' || $fold === 'systeme' || 0 === strpos( $fold, 'system' ) ) {
			return 'system';
		}
		if ( $fold === 'core' ) {
			return 'core';
		}
		return 'core';
	}

	/**
	 * Badge HTML « Cible » (CORE / B2C / B2B / SYSTÈME) depuis metadata.target (fallback cible_tableau, user_type).
	 *
	 * @param array<string, mixed> $meta Métadonnées.
	 * @return string HTML (déjà échappé côté attributs ; libellés échappés).
	 */
	private static function kb_target_badge_html( array $meta ) {
		$key     = self::kb_normalize_target_key_from_metadata( $meta );
		$raw_tgt = self::kb_target_raw_from_metadata( $meta );
		if ( $key === 'b2c' ) {
			$label = 'B2C';
			$class = 'avi-kb-badge avi-kb-badge--b2c';
		} elseif ( $key === 'b2b' ) {
			$label = 'B2B';
			$class = 'avi-kb-badge avi-kb-badge--b2b';
		} elseif ( $key === 'system' ) {
			$label = __( 'SYSTÈME', 'avision-dashboard' );
			$class = 'avi-kb-badge avi-kb-badge--system';
		} elseif ( $key === 'core' ) {
			$class = 'avi-kb-badge avi-kb-badge--core';
			if ( $raw_tgt === '' || $raw_tgt === 'core' ) {
				$label = 'CORE';
			} else {
				$label = strtoupper( function_exists( 'mb_substr' ) ? mb_substr( $raw_tgt, 0, 12 ) : substr( $raw_tgt, 0, 12 ) );
			}
		} else {
			$label = 'CORE';
			$class = 'avi-kb-badge avi-kb-badge--core';
		}
		return '<span class="' . esc_attr( $class ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * Catégorie Knowledge (IDENTITY / STRATEGY / PROCEDURE / DASHBOARD) depuis metadata.category.
	 *
	 * @param array<string, mixed> $meta Métadonnées du point.
	 * @return string Code en majuscules ou chaîne vide.
	 */
	private static function kb_category_from_metadata( array $meta ) {
		if ( ! empty( $meta['category'] ) && is_string( $meta['category'] ) ) {
			return strtoupper( trim( $meta['category'] ) );
		}
		return '';
	}

	/**
	 * Clé filtre « Cible » (cohérente avec les badges) pour data-attribute / menu déroulant.
	 *
	 * @param array<string, mixed> $meta Métadonnées.
	 * @return string core|b2c|b2b|system
	 */
	private static function kb_target_filter_key_from_metadata( array $meta ) {
		return self::kb_normalize_target_key_from_metadata( $meta );
	}

	/**
	 * Libellé « Rôle / fonction » : role_fonction du payload si présent, sinon carte wp_option.
	 *
	 * @param array<string, mixed> $meta Métadonnées.
	 * @param string               $opt_key Clé option résolue (fallback).
	 * @return string
	 */
	private static function kb_role_fonction_from_metadata( array $meta, $opt_key ) {
		if ( ! empty( $meta['role_fonction'] ) && is_string( $meta['role_fonction'] ) ) {
			$v = trim( $meta['role_fonction'] );
			if ( $v !== '' ) {
				return $v;
			}
		}
		return self::kb_role_label_from_option_key( $opt_key );
	}

	/**
	 * Bloc « Injecter une expertise » (niche B2B) : envoi serveur vers FastAPI /admin/qdrant/upsert.
	 *
	 * @return void
	 */
	private static function render_niche_injector() {
		echo '<details class="avi-niche-details" style="margin:16px 0;max-width:920px;">';
		echo '<summary style="cursor:pointer;font-weight:600;padding:8px 0;">' . esc_html__( '▶ Injection avancée par niche (GBP, B2B, ROI…)', 'avision-dashboard' ) . '</summary>';
		echo '<div class="avi-niche-box" style="margin-top:8px;">';
		echo '<h3>' . esc_html__( '🎾 Créer un module expert (niche)', 'avision-dashboard' ) . '</h3>';
		echo '<p class="description">' . esc_html__( 'Chaque upsert crée un segment Qdrant étiqueté par module stratégique (GBP, B2B, ROI, HEATMAP, WHOLESALE).', 'avision-dashboard' ) . '</p>';
		echo '<form method="post" action="" id="avi-niche-inject-form">';
		wp_nonce_field( 'avision_cc_niche_inject' );
		echo '<input type="hidden" name="avision_cc_niche_inject" value="1" />';
		echo '<p><label for="niche_title">' . esc_html__( 'Secteur (ex. TENNIS)', 'avision-dashboard' ) . '</label><br />';
		echo '<input type="text" name="niche_title" id="niche_title" class="regular-text" required placeholder="' . esc_attr__( 'TENNIS', 'avision-dashboard' ) . '" autocomplete="off" /></p>';
		echo '<p><label for="niche_strategic_category">' . esc_html__( 'Module expert', 'avision-dashboard' ) . '</label><br />';
		echo '<select name="niche_strategic_category" id="niche_strategic_category">';
		$niche_opts = array(
			'B2B'       => 'B2B',
			'GBP'       => 'GBP',
			'ROI'       => 'ROI',
			'HEATMAP'   => 'HEATMAP',
			'WHOLESALE' => 'WHOLESALE',
		);
		foreach ( $niche_opts as $ov => $ol ) {
			echo '<option value="' . esc_attr( $ov ) . '"' . selected( 'B2B', $ov, false ) . '>' . esc_html( $ol ) . '</option>';
		}
		echo '</select></p>';
		echo '<p><label for="niche_content">' . esc_html__( 'Conseil stratégique B2B', 'avision-dashboard' ) . '</label><br />';
		echo '<textarea name="niche_content" id="niche_content" class="large-text" rows="6" required placeholder="' . esc_attr__( 'Texte détaillé pour le Knowledge Center…', 'avision-dashboard' ) . '"></textarea></p>';
		submit_button( __( 'Synchroniser au Cerveau', 'avision-dashboard' ), 'primary', 'submit', false, array( 'id' => 'avi-niche-sync-btn' ) );
		echo '</form>';
		echo '</div>';
		echo '</details>';
	}

	/**
	 * [CMS-V2-AUDIT]
	 * 1) Signature formulaire upsert historique : POST avec champ hidden avision_cc_upsert=1,
	 *    nonce action avision_cc_upsert (wp_nonce_field), champs cc_point_id, cc_strategic_category,
	 *    cc_content, cc_metadata. Traitement dans render() (pas ici) : corps JSON envoyé à
	 *    api_post_json( '/admin/qdrant/upsert', $body ) avec content, metadata (objet), strategic_category,
	 *    embed=true, id optionnel — pas de champs user_type / min_level / category CMS.
	 * 2) Endpoint effectif de ce formulaire : /admin/qdrant/upsert (ancien). Il est distinct de
	 *    /api/v1/knowledge/upsert (CMS Pydantic strict, utilisé uniquement par le formulaire CMS V2 ci-dessous).
	 * 3) Champs HTML visibles : ID optionnel, module expert (select GBP…), contenu (textarea), metadata (JSON).
	 *
	 * Onglet « Knowledge Base » : liste Qdrant + CMS V2 (stats, injection structurée, simulate) + injection directe.
	 *
	 * @return void
	 */
	private static function render_knowledge_tab() {

		if ( isset( $_GET['refresh'] ) && sanitize_text_field( (string) wp_unslash( $_GET['refresh'] ) ) === '1' ) {
			self::delete_transients_prefixed_avision_qdrant();
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Cache Qdrant (transients) vidé — données rechargées.', 'avision-dashboard' ) . '</p></div>';
		}

		if ( ! empty( $_POST['avision_cms_upsert_submit'] ) && isset( $_POST['_wpnonce_cms_upsert'] ) ) {
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_die( esc_html__( 'Accès refusé.', 'avision-dashboard' ) );
			}
			check_admin_referer( 'avision_cc_cms_upsert', '_wpnonce_cms_upsert' );
			$content        = isset( $_POST['content'] ) ? wp_unslash( (string) $_POST['content'] ) : '';
			$sc             = isset( $_POST['strategic_category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['strategic_category'] ) ) ) : '';
			$ut             = isset( $_POST['user_type'] ) ? sanitize_key( (string) wp_unslash( $_POST['user_type'] ) ) : '';
			$cat            = isset( $_POST['category'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_POST['category'] ) ) ) : '';
			$min_level      = isset( $_POST['min_level'] ) ? (int) $_POST['min_level'] : 0;
			$inject_mode    = isset( $_POST['inject_mode'] ) ? sanitize_key( (string) wp_unslash( $_POST['inject_mode'] ) ) : 'new';
			$update_point_id = isset( $_POST['update_point_id'] ) ? trim( (string) wp_unslash( $_POST['update_point_id'] ) ) : '';
			$len            = function_exists( 'mb_strlen' ) ? mb_strlen( $content ) : strlen( $content );
			$allow_sc       = array( 'REPUTATION', 'LOCAL_VISIBILITY', 'ROI_AGENCE' );
			$allow_ut       = array( 'core', 'b2b', 'b2c' );
			$allow_cat      = array( 'GBP_RULES', 'STRATEGY', 'PERFORMANCE', 'IDENTITY' );
			if ( $inject_mode !== 'new' && $inject_mode !== 'update' ) {
				$inject_mode = 'new';
			}
			if ( $inject_mode === 'update' && $update_point_id === '' ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'Mode mise à jour : sélectionnez un chunk existant.', 'avision-dashboard' ) . '</p></div>';
			} elseif ( $len < 50 ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'Contenu expert : minimum 50 caractères.', 'avision-dashboard' ) . '</p></div>';
			} elseif ( ! in_array( $sc, $allow_sc, true ) || ! in_array( $ut, $allow_ut, true ) || ! in_array( $cat, $allow_cat, true ) || $min_level < 1 || $min_level > 21 ) {
				echo '<div class="notice notice-error"><p>' . esc_html__( 'Valeurs stratégiques ou niveau invalides.', 'avision-dashboard' ) . '</p></div>';
			} else {
				$up  = null;
				$pid = '';
				if ( $inject_mode === 'update' ) {
					$meta_up = array(
						'user_type'          => $ut,
						'min_level'          => $min_level,
						'category'           => $cat,
						'doc_type'           => 'EXPERT_SEO',
						'dashboard_type'     => $ut,
						'strategic_category' => $sc,
					);
					$body_ad = array(
						'content'            => $content,
						'strategic_category' => $sc,
						'embed'              => true,
						'metadata'           => $meta_up,
					);
					if ( ctype_digit( $update_point_id ) ) {
						$body_ad['id'] = (int) $update_point_id;
				} else {
						$body_ad['id'] = $update_point_id;
					}
					$up = self::api_post_json( '/admin/qdrant/upsert', $body_ad );
					if ( $up['ok'] ) {
						$pid = $update_point_id;
						if ( is_array( $up['data'] ) && isset( $up['data']['id'] ) ) {
							$pid = (string) $up['data']['id'];
						}
					}
			} else {
				$body = array(
						'content'            => $content,
						'strategic_category' => $sc,
						'user_type'          => $ut,
						'min_level'          => $min_level,
						'category'           => $cat,
				);
				$up = self::knowledge_v1_post_json( '/api/v1/knowledge/upsert', $body );
					if ( $up['ok'] && is_array( $up['data'] ) ) {
						foreach ( array( 'point_id', 'id', 'qdrant_id' ) as $pk ) {
							if ( isset( $up['data'][ $pk ] ) ) {
								$pid = (string) $up['data'][ $pk ];
								break;
							}
						}
					}
				}
				if ( $up !== null && $up['ok'] ) {
					self::delete_transients_prefixed_avision_qdrant();
					self::log_injection_avi_ai_interactions( $inject_mode, $sc, $cat, $content, $pid );
					echo '<div class="notice notice-success is-dismissible"><p>';
					echo esc_html(
						sprintf(
							/* translators: 1: point id, 2: strategic category, 3: user type, 4: min level. */
							__( '✅ Chunk injecté — ID: %1$s | %2$s / %3$s / Level %4$d', 'avision-dashboard' ),
							$pid !== '' ? $pid : '—',
							$sc,
							$ut,
							$min_level
						)
					);
					echo '</p></div>';
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( '📝 Injection loguée', 'avision-dashboard' ) . '</p></div>';
				} elseif ( $up !== null ) {
					echo '<div class="notice notice-error"><p>' . esc_html( $up['error'] ?: ( 'HTTP ' . $up['code'] ) ) . '</p></div>';
				}
			}
		}

		$next = isset( $_GET['qdrant_offset'] ) ? (string) wp_unslash( $_GET['qdrant_offset'] ) : '';
		$query = array( 'limit' => 30 );
		if ( $next !== '' ) {
			$query['offset'] = $next;
		}
		$res        = self::api_get_qdrant_points_cached( $query );
		$stats_res  = self::knowledge_v1_get( '/api/v1/knowledge/stats' );
		$stats_data = ( $stats_res['ok'] && is_array( $stats_res['data'] ) ) ? $stats_res['data'] : null;

		echo '<style>
			.avi-kb-badge{display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.02em;}
			.avi-kb-badge--core{background:#64748b;color:#fff;}
			.avi-kb-badge--b2c{background:#1d4ed8;color:#fff;}
			.avi-kb-badge--b2b{background:#15803d;color:#fff;}
			.avi-kb-badge--system{background:#7c3aed;color:#fff;}
			#avi-kb-filter-wrap{margin:12px 0;display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
			#avi-kb-filter{flex:1;min-width:200px;max-width:min(480px,100%);}
			#avi-kb-target-filter{min-width:140px;}
			#avi-kb-category-filter{min-width:160px;}
			#avi-kb-source-filter{min-width:180px;}
			#avi-kb-strategic-filter{min-width:200px;}
			.avi-kb-wrap .avi-chunk-search-btn{margin-left:6px;}
			.avi-kb-wrap .avi-chunk-search-row{margin:6px 0;}
			.avi-kb-wrap .avi-chunk-pick{cursor:pointer;margin:4px 0;padding:6px 8px;border:1px solid #c3c4c7;border-radius:4px;background:#f6f7f7;max-width:640px;}
			.avi-kb-wrap .avi-chunk-pick:hover{background:#f0f0f1;}
			.avi-cms-v2-form.avi-cms-v2-form #avi-cms-confirm-btn:disabled{opacity:0.55;cursor:not-allowed;}
			.avi-cms-v2-form .avi-cms-preview-box{margin-top:8px;}
			.avi-doc-badge-cms{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#00a32a;color:#fff;}
			.avi-doc-badge-legacy{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#787878;color:#fff;}
			.avi-kb-v2-stats{display:flex;flex-wrap:wrap;gap:12px;margin:16px 0;}
			.avi-kb-v2-stat-card{flex:1;min-width:160px;max-width:280px;border:1px solid #c3c4c7;border-radius:6px;padding:12px 14px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,.04);}
			.avi-kb-v2-stat-card h4{margin:0 0 6px;font-size:13px;color:#1d2327;}
			.avi-kb-v2-stat-val{font-size:22px;font-weight:700;color:#1d2327;}
			.avi-cms-badge-ok{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#00a32a;color:#fff;}
			.avi-cms-badge-warn{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#f97316;color:#fff;}
			.avi-cms-badge-danger{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:#d63638;color:#fff;}
			.avi-cms-v2-box{margin:16px 0;padding:16px;border:1px solid #c3c4c7;background:#fff;max-width:920px;border-radius:4px;}
			.avi-cms-v2-box h2,.avi-cms-v2-box h3{margin-top:0;}
			.avi-kb-role{font-weight:600;color:#1e293b;}
			.avi-kb-role-key{display:block;font-size:11px;color:#64748b;font-weight:400;margin-top:2px;}
			.avi-kb-pagination-nav{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0;}
			.avi-kb-btn-delete{background:#dc2626!important;border-color:#991b1b!important;color:#fff!important;text-shadow:none!important;box-shadow:none!important;}
			.avi-kb-btn-delete:hover,.avi-kb-btn-delete:focus{background:#b91c1c!important;border-color:#7f1d1d!important;color:#fff!important;}
			#avi-kb-points-table th:last-child,#avi-kb-points-table td:last-child{white-space:nowrap;width:1%;}
			.avi-kb-strat code{font-size:11px;}
			.avi-niche-box{margin:16px 0;padding:16px;border:1px solid #c3c4c7;background:#fff;max-width:720px;border-radius:4px;}
			.avi-niche-box h3{margin-top:0;}
			.avi-niche-box input.regular-text{width:100%;max-width:100%;}
			.avi-niche-box textarea{width:100%;max-width:100%;box-sizing:border-box;}
		</style>';

		$cc_kn_nonce = wp_create_nonce( 'avision_cc_knowledge' );
		echo '<div class="avi-kb-wrap" data-avi-cc-nonce="' . esc_attr( $cc_kn_nonce ) . '">';
		echo '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px">';
		echo '<h2 style="margin:0">' . esc_html__( 'Points Qdrant (modules experts)', 'avision-dashboard' ) . '</h2>';
		echo '<form method="post" style="margin:0">';
		wp_nonce_field( 'avision_cc_clear_qdrant_cache' );
		echo '<input type="hidden" name="avision_cc_clear_qdrant_cache" value="1" />';
		submit_button( __( 'Vider le cache', 'avision-dashboard' ), 'secondary', 'submit', false, array( 'id' => 'avision-cc-clear-qdrant-cache' ) );
		echo '</form></div>';
		if ( ! $res['ok'] ) {
			echo '<div class="notice notice-error"><p>' . esc_html( $res['error'] ?: 'Erreur API' ) . '</p></div>';
		}

		$points = ( is_array( $res['data'] ) && isset( $res['data']['points'] ) && is_array( $res['data']['points'] ) ) ? $res['data']['points'] : array();

		echo '<div id="avi-kb-filter-wrap">';
		echo '<label for="avi-kb-filter" class="screen-reader-text">' . esc_html__( 'Filtrer le tableau', 'avision-dashboard' ) . '</label>';
		echo '<input type="search" id="avi-kb-filter" placeholder="' . esc_attr__( 'Rechercher par mots-clés (ID, rôle, texte, métadonnées…)', 'avision-dashboard' ) . '" autocomplete="off" />';
		echo '<label for="avi-kb-target-filter" class="screen-reader-text">' . esc_html__( 'Filtrer par cible', 'avision-dashboard' ) . '</label>';
		echo '<select id="avi-kb-target-filter" autocomplete="off">';
		echo '<option value="">' . esc_html__( 'Toutes les cibles', 'avision-dashboard' ) . '</option>';
		echo '<option value="core">CORE</option>';
		echo '<option value="b2c">B2C</option>';
		echo '<option value="b2b">B2B</option>';
		echo '<option value="system">' . esc_html__( 'SYSTÈME', 'avision-dashboard' ) . '</option>';
		echo '</select>';
		echo '<label for="avi-kb-category-filter" class="screen-reader-text">' . esc_html__( 'Filtrer par catégorie', 'avision-dashboard' ) . '</label>';
		echo '<select id="avi-kb-category-filter" autocomplete="off">';
		echo '<option value="">' . esc_html__( 'Toutes les catégories', 'avision-dashboard' ) . '</option>';
		echo '<option value="IDENTITY">IDENTITY</option>';
		echo '<option value="STRATEGY">STRATEGY</option>';
		echo '<option value="PROCEDURE">PROCEDURE</option>';
		echo '<option value="DASHBOARD">DASHBOARD</option>';
		echo '</select>';
		echo '<label for="avi-kb-source-filter" class="screen-reader-text">' . esc_html__( 'Filtrer par source document', 'avision-dashboard' ) . '</label>';
		echo '<select id="avi-kb-source-filter" autocomplete="off">';
		echo '<option value="">' . esc_html__( 'Toutes les sources', 'avision-dashboard' ) . '</option>';
		echo '<option value="cms">' . esc_html__( 'Source : CMS uniquement', 'avision-dashboard' ) . '</option>';
		echo '<option value="legacy">' . esc_html__( 'Source : Legacy uniquement', 'avision-dashboard' ) . '</option>';
		echo '</select>';
		echo '<label for="avi-kb-strategic-filter" class="screen-reader-text">' . esc_html__( 'Module expert (filtre serveur)', 'avision-dashboard' ) . '</label>';
		echo '<select id="avi-kb-strategic-filter" autocomplete="off">';
		echo '<option value="">' . esc_html__( 'Tous les modules experts', 'avision-dashboard' ) . '</option>';
		echo '<option value="REPUTATION">REPUTATION</option>';
		echo '<option value="LOCAL_VISIBILITY">LOCAL_VISIBILITY</option>';
		echo '<option value="ROI_AGENCE">ROI_AGENCE</option>';
		echo '</select>';
		echo '<button type="button" class="button button-secondary" id="avi-kb-filter-apply-btn">' . esc_html__( '🔍 Appliquer', 'avision-dashboard' ) . '</button>';
		echo '</div>';
		echo '<p id="avi-kb-filter-status" class="description" style="margin:8px 0 0;"></p>';

		echo '<table class="widefat striped" id="avi-kb-points-table"><thead><tr>';
		echo '<th>' . esc_html__( 'ID', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Source', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Cible', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Module expert', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Catégorie', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Rôle / fonction', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Texte (extrait)', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Metadata', 'avision-dashboard' ) . '</th>';
		echo '<th>' . esc_html__( 'Actions', 'avision-dashboard' ) . '</th>';
		echo '</tr></thead><tbody id="avi-kb-points-tbody">';
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML construit avec esc_* dans kb_render_points_tbody_inner_html.
		echo self::kb_render_points_tbody_inner_html( $points );
		echo '</tbody></table>';

		$kb_js_lbl_voir = wp_json_encode( __( '👁️ Voir tout', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$kb_js_lbl_red  = wp_json_encode( __( 'Réduire', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		printf(
			'<script>
(function(){
	var inp=document.getElementById("avi-kb-filter");
	var sel=document.getElementById("avi-kb-target-filter");
	var catSel=document.getElementById("avi-kb-category-filter");
	var srcSel=document.getElementById("avi-kb-source-filter");
	var tb=document.getElementById("avi-kb-points-table");
	if(!inp||!tb)return;
	function run(){
		var q=(inp.value||"").toLowerCase().trim();
		var tgt=(sel&&sel.value)?String(sel.value).toLowerCase():"";
		var cat=(catSel&&catSel.value)?String(catSel.value).toLowerCase():"";
		var src=(srcSel&&srcSel.value)?String(srcSel.value).toLowerCase():"";
		var rows=tb.querySelectorAll("tbody tr.avi-kb-row");
		for(var i=0;i<rows.length;i++){
			var r=rows[i];
			var hay=(r.getAttribute("data-avi-kb-search")||"");
			var rowTgt=(r.getAttribute("data-avi-kb-target")||"core").toLowerCase();
			var rowCat=(r.getAttribute("data-avi-kb-category")||"").toLowerCase();
			var rowDoc=(r.getAttribute("data-avi-kb-doc")||"legacy").toLowerCase();
			var okSearch=!q||hay.indexOf(q)!==-1;
			var okTgt=!tgt||rowTgt===tgt;
			var okCat=!cat||rowCat===cat;
			var okSrc=!src||rowDoc===src;
			r.style.display=(okSearch&&okTgt&&okCat&&okSrc)?"":"none";
		}
	}
	inp.addEventListener("input",run);
	inp.addEventListener("search",run);
	if(sel){sel.addEventListener("change",run);}
	if(catSel){catSel.addEventListener("change",run);}
	if(srcSel){srcSel.addEventListener("change",run);}
	tb.addEventListener("click",function(ev){
		var btn=ev.target.closest(".avi-kb-toggle-text");
		if(!btn)return;
		var cell=btn.closest(".avi-kb-text-cell");
		if(!cell)return;
		var s=cell.querySelector(".avi-kb-text-short");
		var L=cell.querySelector(".avi-kb-text-long");
		if(!s||!L)return;
		var exp=btn.getAttribute("aria-expanded")==="true";
		if(exp){
			s.style.display="";
			L.style.display="none";
			btn.setAttribute("aria-expanded","false");
			btn.textContent=%1$s;
		}else{
			s.style.display="none";
			L.style.display="";
			btn.setAttribute("aria-expanded","true");
			btn.textContent=%2$s;
		}
	});
})();
</script>',
			$kb_js_lbl_voir,
			$kb_js_lbl_red
		);

		$current_offset = isset( $_GET['qdrant_offset'] ) ? (string) wp_unslash( $_GET['qdrant_offset'] ) : '';
		$next_off       = ( is_array( $res['data'] ) && ! empty( $res['data']['next_offset'] ) ) ? (string) $res['data']['next_offset'] : '';

		$url_first = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'knowledge',
			)
		);

		echo '<nav class="avi-kb-pagination-nav" aria-label="' . esc_attr__( 'Pagination Knowledge Base', 'avision-dashboard' ) . '">';
		if ( $current_offset !== '' ) {
			echo '<a class="button" href="' . esc_url( $url_first ) . '">' . esc_html__( '← Page précédente (début)', 'avision-dashboard' ) . '</a>';
		}
		if ( $next_off !== '' ) {
			$url_next = self::control_center_admin_screen_url(
				array(
					'page'          => self::PAGE_SLUG,
					'tab'           => 'knowledge',
					'qdrant_offset' => $next_off,
				)
			);
			echo '<a class="button button-primary" href="' . esc_url( $url_next ) . '">' . esc_html__( 'Page suivante (scroll Qdrant)', 'avision-dashboard' ) . '</a>';
		}
		echo '</nav>';
		echo '</div>';

		$url_refresh_stats = self::control_center_admin_screen_url(
			array(
				'page'    => self::PAGE_SLUG,
				'tab'     => 'knowledge',
				'refresh' => '1',
			)
		);
		$url_cms_form = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'knowledge',
			)
		);

		if ( ! $stats_res['ok'] ) {
			echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'API /api/v1/knowledge/stats inaccessible ou erreur — vérifiez le backend et la clé Bearer (ADMIN_API_KEY).', 'avision-dashboard' ) . ' ';
			echo esc_html( $stats_res['error'] ?: ( 'HTTP ' . $stats_res['code'] ) );
			echo '</p></div>';
		}

		echo '<hr style="margin:24px 0;" />';
		echo '<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px;">';
		echo '<h2 style="margin:0;">' . esc_html__( 'État de la base de connaissances', 'avision-dashboard' ) . '</h2>';
		echo '<a class="button button-secondary" href="' . esc_url( $url_refresh_stats ) . '">' . esc_html__( 'Actualiser', 'avision-dashboard' ) . '</a>';
		echo '</div>';

		$d_st          = $stats_data;
		$total_points  = self::knowledge_stats_int( $d_st, array( 'total_points', 'total' ) );
		$cms_injected  = self::knowledge_stats_int( $d_st, array( 'cms_injected', 'cms_chunks', 'expert_seo_count' ) );
		$legacy_points = self::knowledge_stats_int( $d_st, array( 'legacy_points', 'legacy_count' ) );
		$miss_ut       = self::knowledge_stats_int( $d_st, array( 'user_type_missing', 'missing_user_type', 'manquant' ) );
		$no_min_level  = self::knowledge_stats_int( $d_st, array( 'points_without_min_level', 'without_min_level', 'missing_min_level' ) );
		$by_sc         = ( is_array( $d_st ) && isset( $d_st['by_strategic_category'] ) && is_array( $d_st['by_strategic_category'] ) ) ? $d_st['by_strategic_category'] : array();
		$by_ut         = ( is_array( $d_st ) && isset( $d_st['by_user_type'] ) && is_array( $d_st['by_user_type'] ) ) ? $d_st['by_user_type'] : array();
		$sc_keys       = array( 'REPUTATION', 'LOCAL_VISIBILITY', 'ROI_AGENCE' );
		$manquant_ut   = $miss_ut;
		if ( is_array( $by_ut ) && isset( $by_ut['manquant'] ) && is_numeric( $by_ut['manquant'] ) ) {
			$manquant_ut = max( $manquant_ut, (int) $by_ut['manquant'] );
		}

		echo '<div class="avi-kb-v2-stats">';
		echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html__( 'Total points', 'avision-dashboard' ) . '</h4>';
		echo '<div class="avi-kb-v2-stat-val">' . esc_html( (string) $total_points ) . '</div></div>';
		echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html__( 'Chunks CMS (EXPERT_SEO)', 'avision-dashboard' ) . '</h4>';
		echo '<div class="avi-kb-v2-stat-val"><span class="avi-cms-badge-ok">' . esc_html( (string) $cms_injected ) . '</span></div></div>';
		echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html__( 'Points legacy (sans métadonnées CMS)', 'avision-dashboard' ) . '</h4>';
		echo '<div class="avi-kb-v2-stat-val">';
		if ( $legacy_points > 0 ) {
			echo '<span class="avi-cms-badge-warn">' . esc_html( (string) $legacy_points ) . '</span>';
		} else {
			echo esc_html( (string) $legacy_points );
		}
		echo '</div></div>';
		foreach ( $sc_keys as $sk ) {
			$nv = isset( $by_sc[ $sk ] ) && is_numeric( $by_sc[ $sk ] ) ? (int) $by_sc[ $sk ] : self::knowledge_stats_int( $d_st, array( strtolower( $sk ) . '_count', $sk ), 0 );
			echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html( $sk ) . '</h4>';
			echo '<div class="avi-kb-v2-stat-val">' . esc_html( (string) $nv ) . '</div></div>';
		}
		foreach ( array( 'b2b', 'b2c', 'core' ) as $uk ) {
			$nv = isset( $by_ut[ $uk ] ) && is_numeric( $by_ut[ $uk ] ) ? (int) $by_ut[ $uk ] : self::knowledge_stats_int( $d_st, array( 'user_type_' . $uk, 'count_' . $uk ), 0 );
			echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html( strtoupper( $uk ) ) . '</h4>';
			echo '<div class="avi-kb-v2-stat-val">' . esc_html( (string) $nv ) . '</div></div>';
		}
		echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html__( 'manquant', 'avision-dashboard' ) . '</h4>';
		echo '<div class="avi-kb-v2-stat-val">';
		if ( $manquant_ut > 0 ) {
			echo '<span class="avi-cms-badge-danger">' . esc_html( (string) $manquant_ut ) . '</span>';
		} else {
			echo esc_html( (string) $manquant_ut );
		}
		echo '</div></div>';
		echo '<div class="avi-kb-v2-stat-card"><h4>' . esc_html__( 'Points sans min_level', 'avision-dashboard' ) . '</h4>';
		echo '<div class="avi-kb-v2-stat-val">';
		if ( $no_min_level > 0 ) {
			echo '<span class="avi-cms-badge-danger">' . esc_html( (string) $no_min_level ) . '</span>';
		} else {
			echo esc_html( (string) $no_min_level );
		}
		echo '</div></div>';
		echo '</div>';

		self::render_niche_injector();

		echo '<div class="avi-cms-v2-box avi-kb-wrap" data-avi-cc-nonce="' . esc_attr( $cc_kn_nonce ) . '">';
		echo '<h2>' . esc_html__( 'Injecter un chunk expert (CMS Structuré)', 'avision-dashboard' ) . '</h2>';
		echo '<form method="post" action="' . esc_url( $url_cms_form ) . '" class="widefat avi-cms-v2-form" id="avi-cms-v2-form" style="max-width:920px">';
		wp_nonce_field( 'avision_cc_cms_upsert', '_wpnonce_cms_upsert', false, true );
		echo '<p><label for="avi-inject-mode" class="screen-reader-text">' . esc_html__( 'Mode d’injection', 'avision-dashboard' ) . '</label>';
		echo '<select name="inject_mode" id="avi-inject-mode" style="margin-bottom:12px;font-size:14px;">';
		echo '<option value="new">' . esc_html__( '➕ Nouveau savoir', 'avision-dashboard' ) . '</option>';
		echo '<option value="update">' . esc_html__( '✏️ Mise à jour (chunk existant)', 'avision-dashboard' ) . '</option>';
		echo '</select></p>';
		echo '<div id="avi-chunk-search-wrap" class="avi-chunk-search-wrap" style="display:none;margin-bottom:12px;">';
		echo '<div class="avi-chunk-search-row">';
		echo '<input type="text" id="avi-chunk-search-input" class="regular-text" style="max-width:min(480px,100%)" placeholder="' . esc_attr__( 'Chercher un chunk existant…', 'avision-dashboard' ) . '" autocomplete="off" />';
		echo '<button type="button" class="button avi-chunk-search-btn" id="avi-chunk-search-btn">' . esc_html__( '🔍 Chercher', 'avision-dashboard' ) . '</button></div>';
		echo '<div id="avi-chunk-search-results" class="avi-chunk-search-results" style="margin-top:8px;"></div>';
		echo '<input type="hidden" id="avi-update-point-id" name="update_point_id" value="" />';
		echo '</div>';
		echo '<div id="avi-chunk-diff-wrap" class="avi-chunk-diff-wrap" style="display:none;margin:8px 0;padding:8px;border:1px solid #c3c4c7;border-radius:4px;background:#f6f7f7;max-width:720px;">';
		echo '<span class="description" id="avi-chunk-diff-text"></span></div>';
		echo '<div id="avi-cms-preview-ajax-box" class="avi-cms-preview-box avi-cms-v2-form"></div>';
		echo '<table class="form-table"><tbody>';
		echo '<tr><th><label for="cms_v2_content">' . esc_html__( 'Contenu expert', 'avision-dashboard' ) . '</label></th><td>';
		echo '<textarea name="content" id="cms_v2_content" class="large-text" rows="8" minlength="50" required placeholder="' . esc_attr__( 'Rédigez au moins 50 caractères d’analyse métier (ex. protocole réponse avis 1★, impact Maps, etc.).', 'avision-dashboard' ) . '"></textarea>';
		echo '<p class="description">' . esc_html__( 'Minimum 50 caractères. Écrivez comme un expert métier.', 'avision-dashboard' ) . '</p></td></tr>';
		echo '<tr><th><label for="cms_v2_strategic_category">' . esc_html__( 'Catégorie stratégique', 'avision-dashboard' ) . '</label></th><td>';
		echo '<select name="strategic_category" id="cms_v2_strategic_category" required>';
		foreach ( array( 'REPUTATION', 'LOCAL_VISIBILITY', 'ROI_AGENCE' ) as $sco ) {
			echo '<option value="' . esc_attr( $sco ) . '">' . esc_html( $sco ) . '</option>';
		}
		echo '</select></td></tr>';
		echo '<tr><th><label for="cms_v2_user_type">' . esc_html__( 'Type d’utilisateur', 'avision-dashboard' ) . '</label></th><td>';
		echo '<select name="user_type" id="cms_v2_user_type" required>';
		echo '<option value="core">' . esc_html__( 'core — Universel', 'avision-dashboard' ) . '</option>';
		echo '<option value="b2b">' . esc_html__( 'b2b — Agence', 'avision-dashboard' ) . '</option>';
		echo '<option value="b2c">' . esc_html__( 'b2c — Commerce', 'avision-dashboard' ) . '</option>';
		echo '</select></td></tr>';
		echo '<tr><th><label for="cms_v2_min_level">' . esc_html__( 'Niveau minimum d’accès', 'avision-dashboard' ) . '</label></th><td>';
		echo '<input type="number" name="min_level" id="cms_v2_min_level" value="1" min="1" max="21" required class="small-text" />';
		echo '<p class="description">' . esc_html__( '1 = accessible à tous / 21 = expert confirmé', 'avision-dashboard' ) . '</p></td></tr>';
		echo '<tr><th><label for="cms_v2_category">' . esc_html__( 'Catégorie pédagogique', 'avision-dashboard' ) . '</label></th><td>';
		echo '<select name="category" id="cms_v2_category" required>';
		foreach ( array( 'GBP_RULES', 'STRATEGY', 'PERFORMANCE', 'IDENTITY' ) as $cato ) {
			echo '<option value="' . esc_attr( $cato ) . '">' . esc_html( $cato ) . '</option>';
		}
		echo '</select></td></tr>';
		echo '</tbody></table>';
		echo '<p class="avi-cms-v2-actions" style="margin-top:12px;">';
		echo '<button type="button" class="button button-secondary" id="avi-cms-preview-btn">' . esc_html__( '👁 Prévisualiser', 'avision-dashboard' ) . '</button> ';
		echo '<button type="submit" class="button button-primary" name="avision_cms_upsert_submit" id="avi-cms-confirm-btn" value="1" disabled="disabled">' . esc_html__( '✅ Confirmer injection', 'avision-dashboard' ) . '</button>';
		echo '</p>';
		echo '</form>';
		echo '</div>';
		$avi_kb_filter_msg = wp_json_encode( __( '%d chunks trouvés (filtrés)', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$avi_jq_err        = wp_json_encode( __( 'Erreur réseau ou réponse invalide.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$avi_empty_res     = wp_json_encode( __( 'Aucun résultat.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$avi_diff_prefix   = wp_json_encode( __( 'Contenu actuel :', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		printf(
			'<script>jQuery(function($){
var knonce=%1$s;
var filterMsgTpl=%2$s;
var aviDiffPrefix=%5$s;
var aviEmptyRes=%6$s;
var wrap=document.querySelector(".avi-kb-wrap[data-avi-cc-nonce]")||document.querySelector(".avi-cms-v2-box.avi-kb-wrap");
if(wrap && !knonce){knonce=wrap.getAttribute("data-avi-cc-nonce")||"";}
function aviFmtChunks(n){return filterMsgTpl.replace(/%%d/g,String(parseInt(n,10)||0));}
$("#avi-kb-filter-apply-btn").on("click",function(){
	var tbody=$("#avi-kb-points-tbody");
	var inp=document.getElementById("avi-kb-filter");
	if(!tbody.length)return;
	$.post(ajaxurl,{action:"avision_cc_qdrant_filter",nonce:knonce,category:$("#avi-kb-category-filter").val()||"",user_type:$("#avi-kb-target-filter").val()||"",strategic_category:$("#avi-kb-strategic-filter").val()||""})
		.done(function(r){
			if(r&&r.success&&r.data){
				tbody.html(r.data.html_rows);
				var t=(typeof r.data.total!=="undefined"&&r.data.total!==null)?parseInt(r.data.total,10):0;
				$("#avi-kb-filter-status").text(aviFmtChunks(t));
				if(inp){inp.dispatchEvent(new Event("input"));}
				return;
			}
			var m=(r&&r.data&&r.data.message)?r.data.message:%3$s;
			alert(m);
		}).fail(function(){alert(%3$s);});
});
function aviSetConfirmEnabled(on){var b=document.getElementById("avi-cms-confirm-btn");if(b){b.disabled=!on;}}
function aviClearPreview(){$("#avi-cms-preview-ajax-box").empty();aviSetConfirmEnabled(false);}
$("#avi-cms-preview-btn").on("click",function(){
	aviClearPreview();
	$.post(ajaxurl,{action:"avision_cc_knowledge_preview_simulate",nonce:knonce,content:$("#cms_v2_content").val()||"",strategic_category:$("#cms_v2_strategic_category").val()||"",user_type:$("#cms_v2_user_type").val()||"",category:$("#cms_v2_category").val()||"",min_level:$("#cms_v2_min_level").val()||"1"})
		.done(function(r){
			if(r&&r.success&&r.data&&r.data.html){
				$("#avi-cms-preview-ajax-box").html(r.data.html);
				aviSetConfirmEnabled(true);
				return;
			}
			var m=(r&&r.data&&r.data.message)?r.data.message:%3$s;
			alert(m);
			aviSetConfirmEnabled(false);
		}).fail(function(){alert(%3$s);aviSetConfirmEnabled(false);});
});
$("#avi-cms-v2-form").on("change input","#cms_v2_content,#cms_v2_strategic_category,#cms_v2_user_type,#cms_v2_min_level,#cms_v2_category",function(){aviClearPreview();});
$("#avi-inject-mode").on("change",function(){
	var u=$(this).val()==="update";
	$("#avi-chunk-search-wrap").toggle(u);
	if(!u){
		$("#avi-update-point-id").val("");
		$("#avi-chunk-diff-wrap").hide();
		$("#avi-chunk-search-results").empty();
	}
});
$("#avi-chunk-search-btn").on("click",function(){
	var q=$("#avi-chunk-search-input").val()||"";
	if(q.length<3){alert(%4$s);return;}
	$.post(ajaxurl,{action:"avision_cc_qdrant_search_chunks",nonce:knonce,query:q})
		.done(function(r){
			if(!(r&&r.success&&r.data&&Array.isArray(r.data.results))){alert(%3$s);return;}
			var h="",i,res=r.data.results;
			for(i=0;i<res.length;i++){
				var row=res[i];
				h+="<button type=\\"button\\" class=\\"avi-chunk-pick button\\" data-point-id=\\""+String(row.id).replace(/"/g,"&quot;")+"\\"><code>"+String(row.id)+"</code> — "+$("<div/>").text(row.snippet||"").html()+"</button>";
			}
			$("#avi-chunk-search-results").html(h||("<em>"+aviEmptyRes+"</em>"));
		}).fail(function(){alert(%3$s);});
});
$(document).on("click",".avi-chunk-pick",function(){
	var pid=$(this).attr("data-point-id")||"";
	if(!pid)return;
	$.post(ajaxurl,{action:"avision_cc_qdrant_get_point",nonce:knonce,point_id:pid})
		.done(function(r){
			if(!(r&&r.success&&r.data&&r.data.fields)){alert(%3$s);return;}
			var f=r.data.fields;
			$("#cms_v2_content").val(f.content||"");
			if(f.strategic_category){$("#cms_v2_strategic_category").val(f.strategic_category);}
			if(f.user_type){$("#cms_v2_user_type").val(f.user_type);}
			if(f.min_level>=1&&f.min_level<=21){$("#cms_v2_min_level").val(String(f.min_level));}
			if(f.category){$("#cms_v2_category").val(f.category);}
			$("#avi-update-point-id").val(String(r.data.point_id||pid));
			var p80=r.data.preview_80||"";
			$("#avi-chunk-diff-text").text(aviDiffPrefix+" "+p80+"…");
			$("#avi-chunk-diff-wrap").show();
			aviClearPreview();
		}).fail(function(){alert(%3$s);});
});
})(jQuery);</script>',
			wp_json_encode( $cc_kn_nonce, JSON_UNESCAPED_UNICODE ),
			$avi_kb_filter_msg,
			$avi_jq_err,
			wp_json_encode( __( 'Minimum 3 caractères pour la recherche.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE ),
			$avi_diff_prefix,
			$avi_empty_res
		);
	}

	/**
	 * Libellé B2B / B2C à partir du chat_type SQL (b2b_fleet, b2c, eureka, …).
	 *
	 * @param string|null $chat_type Valeur chat_type.
	 * @return string
	 */
	private static function user_type_label_from_chat_type( $chat_type ) {
		$t = is_string( $chat_type ) ? strtolower( trim( $chat_type ) ) : '';
		if ( $t === '' ) {
			return '—';
		}
		if ( strpos( $t, 'b2b' ) !== false ) {
			return 'B2B';
		}
		return 'B2C';
	}

	/**
	 * Onglet « Cerveau AVI » : reconstruction du cerveau via build_master_brain.py (shell_exec).
	 *
	 * @return void
	 */
	private static function render_intelligence_tab() {
		self::ensure_qi_baseline_questions_option();
		$nonce = wp_create_nonce( 'avision_cc_intelligence' );
		$shell_ok        = function_exists( 'shell_exec' ) && is_callable( 'shell_exec' );
		$build_script_fs = '/root/avision-backend/data/build_master_brain.py';
		if ( ! $shell_ok ) {
			$status = __( 'shell_exec indisponible — contactez l’hébergeur ou exécutez les .py en local.', 'avision-dashboard' );
		} elseif ( file_exists( $build_script_fs ) ) {
			$status = __( '✅ Script build_master_brain.py trouvé', 'avision-dashboard' );
		} else {
			$status = __( '❌ Script introuvable', 'avision-dashboard' );
		}
		echo '<div class="avi-control-card" style="max-width:920px;margin-top:12px">';
		echo '<h2>' . esc_html__( '🧠 Gestion du Cerveau AVI', 'avision-dashboard' ) . '</h2>';
		echo '<p>' . esc_html__( 'Statut actuel :', 'avision-dashboard' ) . ' <strong id="avi-status">' . esc_html( $status ) . '</strong></p>';
		if ( defined( 'AVISION_PYTHON_BIN' ) && is_string( AVISION_PYTHON_BIN ) && AVISION_PYTHON_BIN !== '' ) {
			echo '<p class="description">' . esc_html__( 'Interpréteur : AVISION_PYTHON_BIN (wp-config).', 'avision-dashboard' ) . '</p>';
		} elseif ( stripos( (string) PHP_OS, 'WIN' ) === 0 ) {
			echo '<p class="description">' . esc_html__( 'Interpréteur : py -3 (Windows). Définissez AVISION_PYTHON_BIN pour forcer un chemin.', 'avision-dashboard' ) . '</p>';
		} else {
			echo '<p class="description">' . esc_html__( 'Interpréteur : python3. Définissez AVISION_PYTHON_BIN pour forcer un chemin.', 'avision-dashboard' ) . '</p>';
		}
		echo '<div class="avi-actions" style="display:flex;flex-wrap:wrap;gap:8px;margin:12px 0">';
		echo '<button type="button" class="button button-primary" id="btn-build-brain" data-tool="build_brain">' . esc_html__( '🧠 Reconstruire le Cerveau AVI', 'avision-dashboard' ) . '</button>';
		echo '</div>';
		echo '<p class="description">' . esc_html__( 'Lance build_master_brain.py — reconstruit avi_master_brain.json depuis les sources.', 'avision-dashboard' ) . '</p>';
		echo '<div id="avi-intelligence-root" data-nonce="' . esc_attr( $nonce ) . '" style="display:none" aria-hidden="true"></div>';
		echo '<label for="avi-console" class="screen-reader-text">' . esc_html__( 'Journal d’exécution', 'avision-dashboard' ) . '</label>';
		echo '<pre id="avi-console" style="background:#0a0a0a;color:#0f0;padding:12px;margin-top:16px;font-family:Consolas,Monaco,monospace;font-size:12px;line-height:1.45;min-height:200px;max-height:420px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;border:1px solid #1f2937;border-radius:4px;">';
		echo esc_html( __( '// Les logs s’affichent ici après chaque action.', 'avision-dashboard' ) );
		echo '</pre>';
		echo '</div>';

		$qi_questions = get_option( 'avi_qi_baseline_questions', array() );
		if ( ! is_array( $qi_questions ) || empty( $qi_questions ) ) {
			$qi_questions = self::qi_default_baseline_questions();
		}
		$last_bl_raw = get_option( 'avi_qi_last_baseline', '' );
		$last_saved_at = '';
		if ( is_string( $last_bl_raw ) && $last_bl_raw !== '' ) {
			$dec = json_decode( $last_bl_raw, true );
			if ( is_array( $dec ) && ! empty( $dec['saved_at'] ) ) {
				$last_saved_at = (string) $dec['saved_at'];
			}
		}

		echo '<div class="avi-control-card avi-qi-baseline-card" style="max-width:1100px;margin-top:24px">';
		echo '<h2>' . esc_html__( '🧪 Test QI Baseline — 8 questions fixes', 'avision-dashboard' ) . '</h2>';
		echo '<table class="widefat striped avi-cc-table"><thead><tr>';
		echo '<th>' . esc_html__( 'ID', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Label', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Type', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Mots-clés attendus', 'avision-dashboard' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $qi_questions as $qr ) {
			if ( ! is_array( $qr ) ) {
				continue;
			}
			$kws = isset( $qr['expected_kw'] ) && is_array( $qr['expected_kw'] ) ? implode( ', ', $qr['expected_kw'] ) : '';
			echo '<tr>';
			echo '<td><code>' . esc_html( isset( $qr['id'] ) ? (string) $qr['id'] : '' ) . '</code></td>';
			echo '<td>' . esc_html( isset( $qr['label'] ) ? (string) $qr['label'] : '' ) . '</td>';
			echo '<td>' . esc_html( isset( $qr['user_type'] ) ? strtoupper( (string) $qr['user_type'] ) : '' ) . '</td>';
			echo '<td><span class="description">' . esc_html( $kws ) . '</span></td>';
			echo '</tr>';
		}
		echo '</tbody></table>';
		echo '<p style="margin:12px 0;">';
		echo '<button type="button" class="button button-primary" id="avi-qi-run-baseline">' . esc_html__( '▶ Lancer les 8 tests', 'avision-dashboard' ) . '</button> ';
		echo '<button type="button" class="button" id="avi-qi-save-baseline">' . esc_html__( '💾 Sauvegarder baseline', 'avision-dashboard' ) . '</button> ';
		echo '<button type="button" class="button" id="avi-qi-compare-baseline">' . esc_html__( '📊 Comparer', 'avision-dashboard' ) . '</button>';
		echo '</p>';
		echo '<p class="description" id="avi-qi-saved-info">' . esc_html__( 'Dernier baseline enregistré :', 'avision-dashboard' ) . ' <strong id="avi-qi-saved-at">' . esc_html( $last_saved_at !== '' ? $last_saved_at : '—' ) . '</strong></p>';
		echo '<div id="avi-qi-global-score" style="font-size:32px;font-weight:700;margin:12px 0;"></div>';
		echo '<div id="avi-qi-results-wrap" style="overflow-x:auto;"><table class="widefat striped avi-cc-table" id="avi-qi-results-table" style="display:none;margin-top:8px;"><thead><tr>';
		echo '<th>ID</th><th>' . esc_html__( 'Score %', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Statut', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Réponse simulée', 'avision-dashboard' ) . '</th>';
		echo '</tr></thead><tbody id="avi-qi-results-body"></tbody></table></div>';
		echo '</div>';

		$append_log = wp_json_encode( __( '> Terminé.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$err_generic = wp_json_encode( __( 'Erreur AJAX ou réponse invalide.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE );
		$last_bl_json = wp_json_encode( $last_bl_raw, JSON_UNESCAPED_UNICODE );
		printf(
			'<script>
(function($){
	var nonce=$("#avi-intelligence-root").data("nonce");
	var $c=$("#avi-console");
	function append(t){$c.append(document.createTextNode(t));$c.scrollTop($c[0].scrollHeight);}
	function run(tool,label){
		append(label+"\n");
		$.post(ajaxurl,{action:"avision_cc_brain_tool",nonce:nonce,tool:tool})
			.done(function(r){
				if(r&&r.success&&r.data&&typeof r.data.log==="string"){
					append(r.data.log);
					append("\n"+%1$s+"\n");
					return;
				}
				var msg=(r&&r.data&&r.data.message)?String(r.data.message):%2$s;
				append("\n"+msg+"\n");
			})
			.fail(function(x){
				var msg=%2$s;
				try{var j=x.responseJSON;if(j&&j.data&&j.data.message){msg=String(j.data.message);}}catch(e){}
				append("\n"+msg+"\n");
			});
	}
	$("#btn-build-brain").on("click",function(){
		run("build_brain","> Lancement de build_master_brain.py…\n");
	});
	window.aviLastQiResults=null;
	var savedBaselineJson=%3$s;
	$("#avi-qi-run-baseline").on("click",function(){
		var $btn=$(this);$btn.prop("disabled",true);
		$.post(ajaxurl,{action:"avision_cc_qi_run_baseline",nonce:nonce}).done(function(r){
			$btn.prop("disabled",false);
			if(!(r&&r.success&&r.data)){alert(%2$s);return;}
			window.aviLastQiResults=r.data;
			var g=r.data.score_global||0;
			$("#avi-qi-global-score").text("Score global moyen : "+g+"\\u0025");
			var rows=r.data.results||[],i,tbody="",cell;
			for(i=0;i<rows.length;i++){
				var row=rows[i];
				cell=(row.response||"").substring(0,400)+(String(row.response||"").length>400?"…":"");
				tbody+="<tr><td><code>"+String(row.id)+"</code></td><td>"+String(row.score_pct)+"</td><td>"+String(row.status)+"</td><td>"+$("<div/>").text(cell).html()+"</td></tr>";
			}
			$("#avi-qi-results-body").html(tbody);
			$("#avi-qi-results-table").show();
		}).fail(function(){$btn.prop("disabled",false);alert(%2$s);});
	});
	$("#avi-qi-save-baseline").on("click",function(){
		if(!window.aviLastQiResults){alert(%4$s);return;}
		$.post(ajaxurl,{action:"avision_cc_qi_save_baseline",nonce:nonce,payload:JSON.stringify(window.aviLastQiResults)}).done(function(r){
			if(r&&r.success&&r.data&&r.data.saved_at){
				$("#avi-qi-saved-at").text(r.data.saved_at);
				return;
			}
			alert(%2$s);
		});
	});
	$("#avi-qi-compare-baseline").on("click",function(){
		if(!window.aviLastQiResults){alert(%4$s);return;}
		var cur=window.aviLastQiResults.score_global||0;
		var prev=null;
		try{
			var raw=savedBaselineJson;
			if(raw&&typeof raw==="string"&&raw.length){
				var o=JSON.parse(raw);
				if(o&&o.data&&typeof o.data.score_global!=="undefined"){prev=parseInt(o.data.score_global,10);}
			}
		}catch(e){}
		if(prev===null){alert(%5$s);return;}
		alert(%6$s.replace("PREV",String(prev)).replace("CUR",String(cur)));
	});
})(jQuery);
</script>',
			$append_log,
			$err_generic,
			$last_bl_json,
			wp_json_encode( __( 'Lancez d’abord les 8 tests.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE ),
			wp_json_encode( __( 'Impossible de lire le baseline sauvegardé (score).', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE ),
			wp_json_encode( __( 'Baseline enregistré : PREV — Actuel : CUR', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE )
		);
	}

	/**
	 * Onglet Chat Monitor V2 (historique FastAPI / SQL, filtres RAG, modale détail).
	 *
	 * @return void
	 */
	private static function render_chat_monitor_tab() {
		global $wpdb;

		$f_uid   = isset( $_GET['f_user_id'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_user_id'] ) ) : '';
		$f_type  = isset( $_GET['f_type'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_type'] ) ) : '';
		$f_from  = isset( $_GET['f_date_from'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_date_from'] ) ) : '';
		$f_to    = isset( $_GET['f_date_to'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_date_to'] ) ) : '';
		$f_utype = isset( $_GET['f_user_type'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_user_type'] ) ) : '';
		$f_lang  = isset( $_GET['f_lang'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_lang'] ) ) : '';
		$f_rag   = isset( $_GET['f_rag'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['f_rag'] ) ) : '';

		$table = $wpdb->prefix . 'avi_chat_logs';
		$where = array( '1=1' );
		$args  = array();

		if ( $f_uid !== '' && ctype_digit( $f_uid ) ) {
			$where[] = 'cl.user_id = %d';
			$args[]  = (int) $f_uid;
		}
		if ( $f_type !== '' ) {
			$where[] = 'cl.level_context LIKE %s';
			$args[]  = '%' . $wpdb->esc_like( $f_type ) . '%';
		}
		if ( $f_from !== '' ) {
			$where[] = 'cl.timestamp >= %s';
			$args[]  = $f_from . ' 00:00:00';
		}
		if ( $f_to !== '' ) {
			$where[] = 'cl.timestamp <= %s';
			$args[]  = $f_to . ' 23:59:59';
		}
		if ( $f_utype !== '' ) {
			$where[] = 'cl.user_type = %s';
			$args[]  = $f_utype;
		}
		if ( $f_lang !== '' ) {
			$where[] = 'cl.language = %s';
			$args[]  = $f_lang;
		}
		if ( $f_rag === '1' ) {
			$where[] = 'cl.rag_used = 1';
		} elseif ( $f_rag === '0' ) {
			$where[] = '(cl.rag_used = 0 OR cl.rag_used IS NULL)';
		}

		$sql = 'SELECT cl.* FROM ' . $table . ' cl WHERE ' . implode( ' AND ', $where )
			. ' ORDER BY cl.timestamp DESC LIMIT 100';
		if ( ! empty( $args ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table fixe préfixée, placeholders uniquement dans WHERE.
			$rows = $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
		} else {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$rows = $wpdb->get_results( $sql );
		}
		if ( ! is_array( $rows ) ) {
			$rows = array();
		}

		$form_action = self::control_center_admin_screen_url(
			array(
				'page' => self::PAGE_SLUG,
				'tab'  => 'monitor',
			)
		);
		?>
		<style>
		.avi-monitor-wrap { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
		.avi-monitor-filters { background:#f8f9fc; border:1px solid #e2e8f0; border-radius:8px; padding:16px; margin-bottom:20px; display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end; }
		.avi-monitor-filters label { font-size:12px; font-weight:600; color:#4a5568; display:flex; flex-direction:column; gap:4px; }
		.avi-monitor-filters input, .avi-monitor-filters select { padding:6px 10px; border:1px solid #cbd5e0; border-radius:6px; font-size:13px; }
		.avi-monitor-filters button { background:#1a2b5e; color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
		.avi-monitor-table { width:100%; border-collapse:collapse; font-size:13px; }
		.avi-monitor-table th { background:#1a2b5e; color:#fff; padding:10px 12px; text-align:left; font-weight:600; }
		.avi-monitor-table td { padding:9px 12px; border-bottom:1px solid #edf2f7; vertical-align:top; max-width:200px; }
		.avi-monitor-table tr:hover { background:#f7f9fc; cursor:pointer; }
		.avi-badge { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; font-weight:700; }
		.avi-badge-b2c { background:#dbeafe; color:#1e40af; }
		.avi-badge-b2b { background:#d1fae5; color:#065f46; }
		.avi-badge-rag { background:#ede9fe; color:#5b21b6; }
		.avi-badge-norag { background:#fee2e2; color:#991b1b; }
		.avi-badge-en { background:#fef3c7; color:#92400e; }
		.avi-badge-fr { background:#e0f2fe; color:#0c4a6e; }
		.avi-badge-es { background:#fce7f3; color:#9d174d; }
		.avi-monitor-truncate { max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#4a5568; }
		.avi-monitor-modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.55); z-index:99999; }
		.avi-monitor-modal-overlay.active { display:flex; align-items:center; justify-content:center; }
		.avi-monitor-modal { background:#fff; border-radius:12px; width:90%; max-width:860px; max-height:90vh; overflow-y:auto; box-shadow:0 20px 60px rgba(0,0,0,0.3); }
		.avi-monitor-modal-header { background:#1a2b5e; color:#fff; padding:16px 24px; border-radius:12px 12px 0 0; display:flex; justify-content:space-between; align-items:center; }
		.avi-monitor-modal-header h3 { margin:0; font-size:16px; }
		.avi-monitor-modal-close { background:none; border:none; color:#fff; font-size:22px; cursor:pointer; }
		.avi-monitor-modal-body { padding:24px; }
		.avi-monitor-section { margin-bottom:20px; }
		.avi-monitor-section h4 { font-size:13px; font-weight:700; color:#1a2b5e; text-transform:uppercase; letter-spacing:.05em; margin:0 0 8px; }
		.avi-monitor-section pre { background:#f8f9fc; border:1px solid #e2e8f0; border-radius:6px; padding:12px; font-size:12px; white-space:pre-wrap; word-break:break-all; max-height:200px; overflow-y:auto; margin:0; }
		.avi-monitor-meta-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px,1fr)); gap:10px; }
		.avi-monitor-meta-item { background:#f8f9fc; border-radius:6px; padding:10px; }
		.avi-monitor-meta-item .label { font-size:11px; color:#718096; font-weight:600; }
		.avi-monitor-meta-item .value { font-size:13px; color:#1a202c; font-weight:700; margin-top:2px; }
		.avi-monitor-rag-sources { display:flex; flex-direction:column; gap:6px; }
		.avi-monitor-rag-item { background:#ede9fe; border-left:3px solid #7c3aed; padding:8px 12px; border-radius:0 6px 6px 0; font-size:12px; }
		.avi-monitor-hallucination { background:#fff7ed; border:1px solid #fed7aa; border-radius:6px; padding:12px; font-size:12px; color:#92400e; }
		</style>

		<div class="avi-monitor-wrap">
			<h2 style="color:#1a2b5e;margin-bottom:16px;"><?php echo esc_html__( '💬 Chat Monitor', 'avision-dashboard' ); ?></h2>

			<form method="get" action="<?php echo esc_url( $form_action ); ?>" class="avi-monitor-filters">
				<input type="hidden" name="page" value="<?php echo esc_attr( self::PAGE_SLUG ); ?>" />
				<input type="hidden" name="tab" value="monitor" />
				<label><?php esc_html_e( 'User ID', 'avision-dashboard' ); ?> <input type="text" name="f_user_id" value="<?php echo esc_attr( $f_uid ); ?>" placeholder="<?php echo esc_attr__( 'ex: 110', 'avision-dashboard' ); ?>" style="width:80px;"></label>
				<label><?php esc_html_e( 'Type', 'avision-dashboard' ); ?> <select name="f_user_type">
					<option value=""><?php esc_html_e( 'Tous', 'avision-dashboard' ); ?></option>
					<option value="b2c" <?php selected( $f_utype, 'b2c' ); ?>><?php esc_html_e( 'B2C', 'avision-dashboard' ); ?></option>
					<option value="b2b" <?php selected( $f_utype, 'b2b' ); ?>><?php esc_html_e( 'B2B', 'avision-dashboard' ); ?></option>
				</select></label>
				<label><?php esc_html_e( 'Langue', 'avision-dashboard' ); ?> <select name="f_lang">
					<option value=""><?php esc_html_e( 'Toutes', 'avision-dashboard' ); ?></option>
					<option value="en" <?php selected( $f_lang, 'en' ); ?>>EN</option>
					<option value="fr" <?php selected( $f_lang, 'fr' ); ?>>FR</option>
					<option value="es" <?php selected( $f_lang, 'es' ); ?>>ES</option>
				</select></label>
				<label><?php esc_html_e( 'RAG', 'avision-dashboard' ); ?> <select name="f_rag">
					<option value=""><?php esc_html_e( 'Tous', 'avision-dashboard' ); ?></option>
					<option value="1" <?php selected( $f_rag, '1' ); ?>><?php esc_html_e( 'RAG utilisé', 'avision-dashboard' ); ?></option>
					<option value="0" <?php selected( $f_rag, '0' ); ?>><?php esc_html_e( 'Sans RAG', 'avision-dashboard' ); ?></option>
				</select></label>
				<label><?php esc_html_e( 'Level/context', 'avision-dashboard' ); ?> <input type="text" name="f_type" value="<?php echo esc_attr( $f_type ); ?>" placeholder="<?php echo esc_attr__( 'ex: 1', 'avision-dashboard' ); ?>" style="width:80px;"></label>
				<label><?php esc_html_e( 'Du', 'avision-dashboard' ); ?> <input type="date" name="f_date_from" value="<?php echo esc_attr( $f_from ); ?>"></label>
				<label><?php esc_html_e( 'Au', 'avision-dashboard' ); ?> <input type="date" name="f_date_to" value="<?php echo esc_attr( $f_to ); ?>"></label>
				<button type="submit"><?php esc_html_e( 'Filtrer', 'avision-dashboard' ); ?></button>
			</form>

			<p style="color:#718096;font-size:13px;">
				<?php
				echo esc_html(
					sprintf(
						/* translators: %d: nombre de lignes */
						_n( '%d conversation — cliquez une ligne pour le détail complet', '%d conversations — cliquez une ligne pour le détail complet', count( $rows ), 'avision-dashboard' ),
						count( $rows )
					)
				);
				?>
			</p>

			<table class="avi-monitor-table">
				<thead>
					<tr>
						<th><?php esc_html_e( 'ID', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Date', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'User', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Type', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Lang', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Level', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Modèle', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'RAG', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Question', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Réponse', 'avision-dashboard' ); ?></th>
					</tr>
				</thead>
				<tbody>
				<?php if ( empty( $rows ) ) : ?>
					<tr><td colspan="10" style="text-align:center;color:#718096;padding:24px;"><?php esc_html_e( 'Aucune conversation.', 'avision-dashboard' ); ?></td></tr>
				<?php else : ?>
					<?php
					foreach ( $rows as $row ) :
						if ( ! is_object( $row ) ) {
							continue;
						}
						$utype = isset( $row->user_type ) ? (string) $row->user_type : '';
						$lang  = isset( $row->language ) ? (string) $row->language : '';
						$rag   = isset( $row->rag_used ) ? (int) $row->rag_used : null;
						$model = isset( $row->model_used ) ? (string) $row->model_used : '';
						$model_parts = explode( '/', $model );
						$model_short = '';
						if ( $model !== '' ) {
							$model_short = ( isset( $model_parts[1] ) && $model_parts[1] !== '' ) ? $model_parts[1] : $model;
						} else {
							$model_short = __( '—', 'avision-dashboard' );
						}
						$qtxt  = isset( $row->query ) ? (string) $row->query : '';
						$rtxt  = isset( $row->response ) ? (string) $row->response : '';
						$qtxt_clean = (string) preg_replace( '/^\[SYSTEM:.*?\]\s*/s', '', $qtxt );
						$r_strip    = wp_strip_all_tags( $rtxt );
						$row_json = wp_json_encode( $row, JSON_UNESCAPED_UNICODE );
						if ( false === $row_json ) {
							$row_json = '{}';
						}
						$q_prev   = function_exists( 'mb_substr' ) ? mb_substr( $qtxt_clean, 0, 80 ) : substr( $qtxt_clean, 0, 80 );
						$r_prev   = function_exists( 'mb_substr' ) ? mb_substr( $r_strip, 0, 80 ) : substr( $r_strip, 0, 80 );
						?>
					<tr class="avi-monitor-row" data-row="<?php echo esc_attr( $row_json ); ?>">
						<td style="font-weight:700;color:#1a2b5e;"><?php echo isset( $row->id ) ? (int) $row->id : 0; ?></td>
						<td style="white-space:nowrap;font-size:12px;"><?php echo isset( $row->timestamp ) ? esc_html( (string) $row->timestamp ) : ''; ?></td>
						<td><?php echo isset( $row->user_id ) ? (int) $row->user_id : 0; ?></td>
						<td>
							<?php if ( $utype === 'b2c' ) : ?>
								<span class="avi-badge avi-badge-b2c">B2C</span>
							<?php elseif ( $utype === 'b2b' ) : ?>
								<span class="avi-badge avi-badge-b2b">B2B</span>
							<?php else : ?>
								<span style="color:#a0aec0;"><?php esc_html_e( '—', 'avision-dashboard' ); ?></span>
							<?php endif; ?>
						</td>
						<td>
							<?php if ( $lang !== '' ) : ?>
								<span class="avi-badge avi-badge-<?php echo esc_attr( $lang ); ?>"><?php echo esc_html( strtoupper( $lang ) ); ?></span>
							<?php else : ?>
								<?php esc_html_e( '—', 'avision-dashboard' ); ?>
							<?php endif; ?>
						</td>
						<td><code><?php echo esc_html( isset( $row->level_context ) ? (string) $row->level_context : __( '—', 'avision-dashboard' ) ); ?></code></td>
						<td style="font-size:11px;color:#5a67d8;"><?php echo esc_html( $model_short ); ?></td>
						<td>
							<?php if ( $rag === 1 ) : ?>
								<span class="avi-badge avi-badge-rag">✓ RAG</span>
							<?php elseif ( $rag === 0 ) : ?>
								<span class="avi-badge avi-badge-norag">✗ LLM</span>
							<?php else : ?>
								<span style="color:#a0aec0;">?</span>
							<?php endif; ?>
						</td>
						<td class="avi-monitor-truncate"><?php echo esc_html( $q_prev ); ?></td>
						<td class="avi-monitor-truncate"><?php echo esc_html( $r_prev ); ?></td>
					</tr>
					<?php endforeach; ?>
				<?php endif; ?>
				</tbody>
			</table>
		</div>

		<div class="avi-monitor-modal-overlay" id="aviMonitorModal">
			<div class="avi-monitor-modal">
				<div class="avi-monitor-modal-header">
					<h3 id="aviMonitorModalTitle"><?php esc_html_e( 'Détail conversation', 'avision-dashboard' ); ?></h3>
					<button type="button" class="avi-monitor-modal-close" id="aviMonitorModalClose" aria-label="<?php esc_attr_e( 'Fermer', 'avision-dashboard' ); ?>">✕</button>
				</div>
				<div class="avi-monitor-modal-body" id="aviMonitorModalBody"></div>
			</div>
		</div>

		<script>
		(function() {
			var overlay = document.getElementById('aviMonitorModal');
			var bodyEl  = document.getElementById('aviMonitorModalBody');
			var titleEl = document.getElementById('aviMonitorModalTitle');
			var closeBtn = document.getElementById('aviMonitorModalClose');
			if (!overlay || !bodyEl || !titleEl || !closeBtn) { return; }
			closeBtn.addEventListener('click', function() {
				overlay.classList.remove('active');
			});
			overlay.addEventListener('click', function(e) {
				if (e.target === overlay) { overlay.classList.remove('active'); }
			});
			document.querySelectorAll('.avi-monitor-row').forEach(function(tr) {
				tr.addEventListener('click', function() {
					var raw = this.getAttribute('data-row');
					var row = {};
					try { row = JSON.parse(raw); } catch (err) { return; }

					titleEl.textContent = '<?php echo esc_js( __( 'Conversation', 'avision-dashboard' ) ); ?> #' + row.id + ' — ' + (row.timestamp || '');

					var ragHtml = '';
					if (row.rag_sources) {
						try {
							var chunks = JSON.parse(row.rag_sources);
							if (Array.isArray(chunks) && chunks.length > 0) {
								ragHtml = '<div class="avi-monitor-rag-sources">';
								chunks.forEach(function(c, i) {
									var src = (c && (c.corpus_id || c.source || c.id)) || ('Chunk ' + (i+1));
									var txt = (c && (c.text || c.content)) || '';
									ragHtml += '<div class="avi-monitor-rag-item"><strong>' + String(src).replace(/</g,'&lt;') + '</strong>'
										+ (txt ? '<br><span style="color:#6d28d9;">' + String(txt).substring(0,200) + '…</span>' : '')
										+ '</div>';
								});
								ragHtml += '</div>';
							} else {
								ragHtml = '<em style="color:#a0aec0;"><?php echo esc_js( __( 'Aucune source RAG parseable', 'avision-dashboard' ) ); ?></em>';
							}
						} catch(e) {
							ragHtml = '<pre>' + String(row.rag_sources).substring(0,500).replace(/</g,'&lt;') + '</pre>';
						}
					} else {
						ragHtml = '<div class="avi-monitor-hallucination">⚠️ <?php echo esc_js( __( 'Aucune source RAG détectée — réponse LLM pure (risque hallucination)', 'avision-dashboard' ) ); ?></div>';
					}

					var rawMeta = '';
					if (row.raw_fastapi_meta) {
						try {
							var metaObj = JSON.parse(row.raw_fastapi_meta);
							rawMeta = JSON.stringify(metaObj, null, 2);
						} catch(e) { rawMeta = row.raw_fastapi_meta; }
					}

					var qClean = String(row.query || '').replace(/^\[SYSTEM:.*?\]\s*/s, '');

					bodyEl.innerHTML =
						'<div class="avi-monitor-meta-grid">'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Type', 'avision-dashboard' ) ); ?></div><div class="value">' + String(row.user_type || '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>').toUpperCase() + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Langue', 'avision-dashboard' ) ); ?></div><div class="value">' + String(row.language || '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>').toUpperCase() + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Level/context', 'avision-dashboard' ) ); ?></div><div class="value">' + String(row.level_context || '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>').replace(/</g,'&lt;') + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Profile ID', 'avision-dashboard' ) ); ?></div><div class="value">' + (row.profile_id != null ? String(row.profile_id) : '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>') + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'User ID', 'avision-dashboard' ) ); ?></div><div class="value">' + (row.user_id != null ? String(row.user_id) : '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>') + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Modèle LLM', 'avision-dashboard' ) ); ?></div><div class="value" style="color:#5a67d8;">' + String(row.model_used || '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>').replace(/</g,'&lt;') + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'RAG utilisé', 'avision-dashboard' ) ); ?></div><div class="value">' + (row.rag_used == 1 ? '✅ <?php echo esc_js( __( 'Oui', 'avision-dashboard' ) ); ?>' : (row.rag_used === 0 || row.rag_used === '0' ? '❌ <?php echo esc_js( __( 'Non', 'avision-dashboard' ) ); ?>' : '❓ <?php echo esc_js( __( 'Inconnu', 'avision-dashboard' ) ); ?>')) + '</div></div>'
						+ '<div class="avi-monitor-meta-item"><div class="label"><?php echo esc_js( __( 'Orchestrator path', 'avision-dashboard' ) ); ?></div><div class="value" style="font-size:11px;">' + String(row.orchestrator_path || '<?php echo esc_js( __( '—', 'avision-dashboard' ) ); ?>').replace(/</g,'&lt;') + '</div></div>'
						+ '</div>'

						+ '<div class="avi-monitor-section" style="margin-top:20px;">'
						+ '<h4>❓ <?php echo esc_js( __( 'Question complète', 'avision-dashboard' ) ); ?></h4>'
						+ '<pre>' + qClean.replace(/</g,'&lt;') + '</pre>'
						+ '</div>'

						+ '<div class="avi-monitor-section">'
						+ '<h4>💬 <?php echo esc_js( __( 'Réponse complète AVI', 'avision-dashboard' ) ); ?></h4>'
						+ '<pre>' + String(row.response || '').replace(/</g,'&lt;') + '</pre>'
						+ '</div>'

						+ '<div class="avi-monitor-section">'
						+ '<h4>🧠 <?php echo esc_js( __( 'Sources RAG utilisées', 'avision-dashboard' ) ); ?></h4>'
						+ ragHtml
						+ '</div>'

						+ (rawMeta ? '<div class="avi-monitor-section"><h4>🔍 <?php echo esc_js( __( 'Métadonnées FastAPI brutes', 'avision-dashboard' ) ); ?></h4><pre>' + rawMeta.replace(/</g,'&lt;') + '</pre></div>' : '');

					overlay.classList.add('active');
				});
			});
		})();
		</script>
		<?php
		self::render_fleet_debug_section();
	}

	/**
	 * Fleet Debug Checklist — fin onglet Chat Monitor (enrichissement B2B par agence).
	 *
	 * @return void
	 */
	private static function render_fleet_debug_section() {
		global $wpdb;
		$agencies = array();
		if ( isset( $wpdb ) && is_object( $wpdb ) ) {
			$t_ag = $wpdb->prefix . 'avi_saas_agencies';
			$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t_ag ) );
			if ( $exists === $t_ag ) {
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$rows = $wpdb->get_results(
					"SELECT id, admin_user_id FROM `{$t_ag}` WHERE admin_user_id > 0 ORDER BY id ASC LIMIT 200",
					ARRAY_A
				);
				if ( is_array( $rows ) ) {
					foreach ( $rows as $ag_row ) {
						if ( ! is_array( $ag_row ) ) {
							continue;
						}
						$uid_l = isset( $ag_row['admin_user_id'] ) ? (int) $ag_row['admin_user_id'] : 0;
						$lbl   = 'Agency #' . ( isset( $ag_row['id'] ) ? (int) $ag_row['id'] : 0 );
						if ( $uid_l > 0 && function_exists( 'get_userdata' ) ) {
							$u = get_userdata( $uid_l );
							if ( $u && ! empty( $u->display_name ) ) {
								$lbl = (string) $u->display_name;
							} elseif ( $u && ! empty( $u->user_login ) ) {
								$lbl = (string) $u->user_login;
							}
						}
						$ag_row['agency_label'] = $lbl;
						$agencies[]             = $ag_row;
					}
				}
			}
		}
		$fd_nonce = wp_create_nonce( 'avision_cc_nonce' );
		?>
		<div class="avi-fleet-debug-wrap" style="margin-top:32px;padding-top:24px;border-top:2px solid #e2e8f0;">
			<h2 style="color:#1a2b5e;margin:0 0 8px;"><?php esc_html_e( 'Fleet Debug Checklist', 'avision-dashboard' ); ?></h2>
			<p style="color:#718096;font-size:13px;margin:0 0 16px;"><?php esc_html_e( 'Vérifie top_priority_profiles et tiers (getScoreTier) pour une agence — même enrichissement que proxy-avi / HELLO_INIT.', 'avision-dashboard' ); ?></p>
			<div class="avi-monitor-filters" style="margin-bottom:16px;">
				<label style="font-size:12px;font-weight:600;color:#4a5568;display:flex;flex-direction:column;gap:4px;">
					<?php esc_html_e( 'Agence (admin WP)', 'avision-dashboard' ); ?>
					<select id="avi-fleet-debug-agency" style="min-width:280px;padding:6px 10px;border:1px solid #cbd5e0;border-radius:6px;font-size:13px;">
						<option value=""><?php esc_html_e( '— Choisir une agence —', 'avision-dashboard' ); ?></option>
						<?php
						foreach ( $agencies as $ag_row ) {
							if ( ! is_array( $ag_row ) ) {
								continue;
							}
							$uid = isset( $ag_row['admin_user_id'] ) ? (int) $ag_row['admin_user_id'] : 0;
							if ( $uid <= 0 ) {
								continue;
							}
							$label = isset( $ag_row['agency_label'] ) ? (string) $ag_row['agency_label'] : ( 'Agency #' . ( isset( $ag_row['id'] ) ? (int) $ag_row['id'] : 0 ) );
							echo '<option value="' . esc_attr( (string) $uid ) . '">' . esc_html( $label . ' (user #' . $uid . ')' ) . '</option>';
						}
						?>
					</select>
				</label>
				<button type="button" id="avi-fleet-debug-run" class="button button-primary" style="background:#6366f1;border-color:#6366f1;margin-top:18px;">
					<?php esc_html_e( 'Run Fleet Debug', 'avision-dashboard' ); ?>
				</button>
			</div>
			<div id="avi-fleet-debug-result"></div>
		</div>
		<style>
		.avi-fleet-debug-table { width:100%; border-collapse:collapse; font-size:13px; margin-top:12px; }
		.avi-fleet-debug-table th { background:#1a2b5e; color:#fff; padding:10px 12px; text-align:left; }
		.avi-fleet-debug-table td { padding:9px 12px; border-bottom:1px solid #edf2f7; vertical-align:middle; }
		.avi-fd-tier { display:inline-block; padding:3px 10px; border-radius:12px; font-size:11px; font-weight:700; }
		.avi-fd-tier--critique { background:#fee2e2; color:#991b1b; }
		.avi-fd-tier--fragile { background:#ffedd5; color:#9a3412; }
		.avi-fd-tier--en_progression { background:#fef9c3; color:#854d0e; }
		.avi-fd-tier--solide { background:#dcfce7; color:#166534; }
		.avi-fd-tier--dominant { background:#ede9fe; color:#5b21b6; }
		.avi-fd-summary { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:16px; }
		.avi-fd-pill { background:#f8f9fc; border:1px solid #e2e8f0; border-radius:8px; padding:10px 14px; font-size:12px; }
		.avi-fd-pill strong { display:block; font-size:18px; color:#1a2b5e; }
		</style>
		<script>
		(function() {
			var btn = document.getElementById('avi-fleet-debug-run');
			var sel = document.getElementById('avi-fleet-debug-agency');
			var box = document.getElementById('avi-fleet-debug-result');
			var nonce = <?php echo wp_json_encode( $fd_nonce, JSON_UNESCAPED_UNICODE ); ?>;
			if (!btn || !sel || !box) { return; }
			function esc(s) {
				return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
			}
			function tierBadge(tier) {
				var labels = { critique: 'Critique', fragile: 'Fragile', en_progression: 'En progression', solide: 'Solide', dominant: 'Dominant' };
				var t = String(tier || '').toLowerCase();
				return '<span class="avi-fd-tier avi-fd-tier--' + esc(t) + '">' + esc(labels[t] || t) + '</span>';
			}
			btn.addEventListener('click', function() {
				var uid = parseInt(sel.value, 10) || 0;
				if (uid <= 0) {
					alert(<?php echo wp_json_encode( __( 'Sélectionnez une agence.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE ); ?>);
					return;
				}
				btn.disabled = true;
				box.innerHTML = '<p style="color:#718096;"><?php echo esc_js( __( 'Chargement…', 'avision-dashboard' ) ); ?></p>';
				var fd = new FormData();
				fd.append('action', 'avision_fleet_debug_run');
				fd.append('nonce', nonce);
				fd.append('agency_user_id', String(uid));
				fetch(<?php echo wp_json_encode( admin_url( 'admin-ajax.php' ), JSON_UNESCAPED_UNICODE ); ?>, { method: 'POST', body: fd, credentials: 'same-origin' })
					.then(function(r) { return r.json(); })
					.then(function(res) {
						btn.disabled = false;
						if (!res || !res.success || !res.data) {
							var msg = (res && res.data && res.data.message) ? res.data.message : <?php echo wp_json_encode( __( 'Erreur AJAX.', 'avision-dashboard' ), JSON_UNESCAPED_UNICODE ); ?>;
							box.innerHTML = '<div class="notice notice-error inline"><p>' + esc(msg) + '</p></div>';
							return;
						}
						var d = res.data;
						var tiers = d.tiers || {};
						var profiles = d.top_priority_profiles || [];
						var ok = d.fleet_kpis_ok ? '✅' : '⚠️';
						var html = '<div class="avi-fd-summary">'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'fleet_kpis_ok', 'avision-dashboard' ) ); ?></span><strong>' + ok + '</strong></div>'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'agency_id', 'avision-dashboard' ) ); ?></span><strong>' + esc(d.agency_id) + '</strong></div>'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'Critique ≤45', 'avision-dashboard' ) ); ?></span><strong style="color:#dc2626;">' + esc(tiers.critical) + '</strong></div>'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'Fragile 46–60', 'avision-dashboard' ) ); ?></span><strong style="color:#ea580c;">' + esc(tiers.fragile) + '</strong></div>'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'Solide 76–90', 'avision-dashboard' ) ); ?></span><strong style="color:#16a34a;">' + esc(tiers.solid) + '</strong></div>'
							+ '<div class="avi-fd-pill"><span><?php echo esc_js( __( 'Dominant ≥91', 'avision-dashboard' ) ); ?></span><strong style="color:#7c3aed;">' + esc(tiers.dominant) + '</strong></div>'
							+ '</div>';
						if (!profiles.length) {
							html += '<p style="color:#718096;"><?php echo esc_js( __( 'Aucune fiche top_priority_profiles.', 'avision-dashboard' ) ); ?></p>';
						} else {
							html += '<table class="avi-fleet-debug-table"><thead><tr>'
								+ '<th><?php echo esc_js( __( 'Nom', 'avision-dashboard' ) ); ?></th>'
								+ '<th><?php echo esc_js( __( 'Score', 'avision-dashboard' ) ); ?></th>'
								+ '<th><?php echo esc_js( __( 'Tier', 'avision-dashboard' ) ); ?></th>'
								+ '<th><?php echo esc_js( __( 'Guardian', 'avision-dashboard' ) ); ?></th>'
								+ '<th><?php echo esc_js( __( 'Radar', 'avision-dashboard' ) ); ?></th>'
								+ '<th><?php echo esc_js( __( 'Status', 'avision-dashboard' ) ); ?></th>'
								+ '</tr></thead><tbody>';
							profiles.forEach(function(p) {
								var gd = parseInt(p.guardian_active, 10) === 1 ? 'ON' : 'OFF';
								var st = parseInt(p.guardian_active, 10) === 1 ? '✅' : '⚠️';
								html += '<tr>'
									+ '<td><strong>' + esc(p.business_name) + '</strong></td>'
									+ '<td>' + esc(p.score) + '</td>'
									+ '<td>' + tierBadge(p.tier) + '</td>'
									+ '<td>' + esc(gd) + '</td>'
									+ '<td><code>' + esc(p.radar_plan || 'none') + '</code></td>'
									+ '<td style="font-size:16px;">' + st + '</td>'
									+ '</tr>';
							});
							html += '</tbody></table>';
						}
						box.innerHTML = html;
					})
					.catch(function() {
						btn.disabled = false;
						box.innerHTML = '<div class="notice notice-error inline"><p><?php echo esc_js( __( 'Erreur réseau.', 'avision-dashboard' ) ); ?></p></div>';
					});
			});
		})();
		</script>
		<?php
	}

	/**
	 * Tier score aligné getScoreTier() JS (critique ≤45, fragile 46–60, …).
	 *
	 * @param int $score Score 0–100.
	 * @return string
	 */
	private static function fleet_debug_score_tier_key( $score ) {
		$v = max( 0, min( 100, (int) $score ) );
		if ( $v <= 45 ) {
			return 'critique';
		}
		if ( $v <= 60 ) {
			return 'fragile';
		}
		if ( $v <= 75 ) {
			return 'en_progression';
		}
		if ( $v <= 90 ) {
			return 'solide';
		}
		return 'dominant';
	}

	/**
	 * AJAX Fleet Debug : enrich_fleet_kpis_with_profiles_public + compteurs tiers.
	 *
	 * @return void
	 */
	public static function ajax_fleet_debug_run() {
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Accès refusé.', 'avision-dashboard' ) ), 403 );
		}
		$user_id = isset( $_POST['agency_user_id'] ) ? (int) $_POST['agency_user_id'] : 0;
		if ( $user_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'agency_user_id invalide.', 'avision-dashboard' ) ), 400 );
		}
		$api_file = defined( 'AVISION_PATH' ) ? AVISION_PATH . 'includes/class-avision-api.php' : '';
		if ( $api_file !== '' && is_readable( $api_file ) ) {
			require_once $api_file;
		}
		if ( ! class_exists( 'Avision\\Avision_API' ) || ! method_exists( 'Avision\\Avision_API', 'enrich_fleet_kpis_with_profiles_public' ) ) {
			wp_send_json_error( array( 'message' => __( 'Avision_API::enrich_fleet_kpis_with_profiles_public indisponible.', 'avision-dashboard' ) ), 500 );
		}
		$kpis = \Avision\Avision_API::enrich_fleet_kpis_with_profiles_public( array(), $user_id );
		if ( ! is_array( $kpis ) ) {
			$kpis = array();
		}
		$agency_id = 0;
		if ( function_exists( 'avi_get_agency_id_for_user' ) ) {
			$agency_id = (int) avi_get_agency_id_for_user( $user_id );
		}
		if ( $agency_id <= 0 && function_exists( 'get_user_meta' ) ) {
			$m = get_user_meta( $user_id, 'avi_saas_agency_id', true );
			if ( is_numeric( $m ) ) {
				$agency_id = (int) $m;
			}
		}
		global $wpdb;
		if ( $agency_id <= 0 && isset( $wpdb ) && is_object( $wpdb ) ) {
			$agency_id = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT id FROM {$wpdb->prefix}avi_saas_agencies WHERE admin_user_id = %d LIMIT 1",
					$user_id
				)
			);
		}
		$profiles_raw = isset( $kpis['top_priority_profiles'] ) && is_array( $kpis['top_priority_profiles'] ) ? $kpis['top_priority_profiles'] : array();
		$tier_counts  = array(
			'critical'  => 0,
			'fragile'   => 0,
			'improving' => 0,
			'solid'     => 0,
			'dominant'  => 0,
		);
		$profiles_out = array();
		foreach ( $profiles_raw as $p ) {
			if ( ! is_array( $p ) ) {
				continue;
			}
			$sc   = isset( $p['score'] ) ? (int) $p['score'] : 0;
			$tier = self::fleet_debug_score_tier_key( $sc );
			if ( $tier === 'critique' ) {
				$tier_counts['critical']++;
			} elseif ( $tier === 'fragile' ) {
				$tier_counts['fragile']++;
			} elseif ( $tier === 'en_progression' ) {
				$tier_counts['improving']++;
			} elseif ( $tier === 'solide' ) {
				$tier_counts['solid']++;
			} elseif ( $tier === 'dominant' ) {
				$tier_counts['dominant']++;
			}
			$profiles_out[] = array(
				'id'              => isset( $p['id'] ) ? (int) $p['id'] : 0,
				'business_name'   => isset( $p['business_name'] ) ? (string) $p['business_name'] : '',
				'score'           => $sc,
				'guardian_active' => isset( $p['guardian_active'] ) ? (int) $p['guardian_active'] : 0,
				'radar_plan'      => isset( $p['radar_plan'] ) ? (string) $p['radar_plan'] : 'none',
				'tier'            => $tier,
			);
		}
		$fleet_kpis_ok = ! empty( $profiles_out ) || ( isset( $kpis['managed_listings_count'] ) && (int) $kpis['managed_listings_count'] > 0 );
		wp_send_json_success(
			array(
				'top_priority_profiles' => $profiles_out,
				'tiers'                 => array(
					'critical'  => (int) ( isset( $kpis['critical_listings'] ) ? $kpis['critical_listings'] : ( isset( $kpis['critical_count'] ) ? $kpis['critical_count'] : $tier_counts['critical'] ) ),
					'fragile'   => (int) ( isset( $kpis['fragile_listings'] ) ? $kpis['fragile_listings'] : ( isset( $kpis['fragile_count'] ) ? $kpis['fragile_count'] : $tier_counts['fragile'] ) ),
					'improving' => (int) ( isset( $kpis['improving_listings'] ) ? $kpis['improving_listings'] : ( isset( $kpis['improving_count'] ) ? $kpis['improving_count'] : $tier_counts['improving'] ) ),
					'solid'     => (int) ( isset( $kpis['solid_listings'] ) ? $kpis['solid_listings'] : ( isset( $kpis['solid_count'] ) ? $kpis['solid_count'] : $tier_counts['solid'] ) ),
					'dominant'  => (int) ( isset( $kpis['dominant_listings'] ) ? $kpis['dominant_listings'] : ( isset( $kpis['dominant_count'] ) ? $kpis['dominant_count'] : $tier_counts['dominant'] ) ),
				),
				'fleet_kpis_ok'         => (bool) $fleet_kpis_ok,
				'agency_id'             => (int) $agency_id,
			)
		);
	}

	/**
	 * Onglet Google Updates (veille + validation + injection Qdrant).
	 *
	 * @return void
	 */
	private static function render_google_updates_tab() {
		global $wpdb;

		if ( ! current_user_can( 'manage_options' ) ) {
			echo '<div class="notice notice-error"><p>' . esc_html__( 'Accès refusé.', 'avision-dashboard' ) . '</p></div>';
			return;
		}

		$nonce_action = 'avi_google_updates_action';
		$key          = self::admin_api_key();
		$base         = rtrim( self::backend_base_url(), '/' );

		$tbl_updates = $wpdb->prefix . 'avi_google_updates';
		$tbl_exists  = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl_updates ) );

		// Helpers
		$http_get = static function ( $url, $key ) {
			$args = array(
				'timeout' => 30,
				'headers' => array(
					'X-Admin-Key' => $key,
				),
			);
			return wp_remote_get( $url, $args );
		};
		$http_delete = static function ( $url, $key ) {
			$args = array(
				'timeout' => 60,
				'headers' => array(
					'X-Admin-Key' => $key,
				),
				'method'  => 'DELETE',
			);
			return wp_remote_request( $url, $args );
		};
		$qi_run_backend = static function ( $base, $key ) {
			$url = rtrim( (string) $base, '/' ) . '/admin/admin/qi/run';
			$res = wp_remote_post(
				$url,
				array(
					'timeout' => 360,
					'headers' => array(
						'Content-Type' => 'application/json; charset=utf-8',
						'X-Admin-Key'  => $key,
					),
					'body'    => wp_json_encode( array(), JSON_UNESCAPED_UNICODE ),
				)
			);
			if ( is_wp_error( $res ) ) {
				return array( 'ok' => false, 'error' => $res->get_error_message(), 'data' => null );
			}
			$code = (int) wp_remote_retrieve_response_code( $res );
			$body = (string) wp_remote_retrieve_body( $res );
			if ( $code < 200 || $code >= 300 ) {
				return array( 'ok' => false, 'error' => 'HTTP ' . $code, 'data' => $body );
			}
			$decoded = json_decode( $body, true );
			if ( ! is_array( $decoded ) ) {
				return array( 'ok' => false, 'error' => 'invalid_json', 'data' => $body );
			}
			return array( 'ok' => true, 'error' => '', 'data' => $decoded );
		};
		$reliability_icon_from_corpus = static function ( $corpus_id ) {
			$cid = strtolower( (string) $corpus_id );
			if ( strpos( $cid, 'watcher_gbp' ) !== false ) {
				return '🏛️';
			}
			if ( strpos( $cid, 'watcher_sterling' ) !== false || strpos( $cid, 'brightlocal' ) !== false || strpos( $cid, 'whitespark' ) !== false ) {
				return '🎓';
			}
			return '📝';
		};
		$reliability_icon_from_source_name = static function ( $source_name ) {
			$sn = strtolower( (string) $source_name );
			if ( strpos( $sn, 'google' ) !== false ) {
				return '🏛️';
			}
			if ( strpos( $sn, 'sterling' ) !== false || strpos( $sn, 'brightlocal' ) !== false || strpos( $sn, 'whitespark' ) !== false ) {
				return '🎓';
			}
			return '📝';
		};
		$impact_class = static function ( $impact ) {
			$im = strtolower( trim( (string) $impact ) );
			if ( $im === 'high' ) {
				return 'avi-impact--high';
			}
			if ( $im === 'medium' ) {
				return 'avi-impact--medium';
			}
			return 'avi-impact--low';
		};

		echo '<style>
#avi-control-center-wrap .avi-gu-section{margin:18px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:12px;background:#fff;}
#avi-control-center-wrap .avi-gu-section h3{margin:0 0 12px;font-size:16px;}
#avi-control-center-wrap .avi-impact--high{color:#b91c1c;font-weight:800;}
#avi-control-center-wrap .avi-impact--medium{color:#b45309;font-weight:800;}
#avi-control-center-wrap .avi-impact--low{color:#047857;font-weight:800;}
#avi-control-center-wrap .avi-gu-filters{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;margin:10px 0 14px;}
#avi-control-center-wrap .avi-gu-filters label{font-size:12px;font-weight:700;display:block;margin-bottom:4px;}
#avi-control-center-wrap .avi-gu-filters select{min-width:200px;}
#avi-control-center-wrap .avi-gu-kpi{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 0;}
#avi-control-center-wrap .avi-gu-kpi .badge{display:inline-flex;gap:8px;align-items:center;padding:6px 10px;border-radius:999px;border:1px solid #ddd;background:#fff;font-weight:800;font-size:12px;}
#avi-control-center-wrap .avi-gu-kpi .dot{width:8px;height:8px;border-radius:999px;display:inline-block;}
#avi-control-center-wrap .avi-gu-kpi .dot--pending{background:#f59e0b;}
#avi-control-center-wrap .avi-gu-kpi .dot--approved{background:#3b82f6;}
#avi-control-center-wrap .avi-gu-kpi .dot--injected{background:#10b981;}
#avi-control-center-wrap .avi-gu-score{font-size:28px;font-weight:900;line-height:1;}
#avi-control-center-wrap .avi-gu-score-badge{display:inline-block;margin-left:10px;padding:3px 10px;border-radius:999px;font-weight:900;font-size:12px;}
#avi-control-center-wrap .avi-gu-score--good{background:#dcfce7;color:#166534;}
#avi-control-center-wrap .avi-gu-score--warn{background:#fffbeb;color:#92400e;}
#avi-control-center-wrap .avi-gu-score--bad{background:#fee2e2;color:#991b1b;}
</style>';

		if ( ! is_string( $key ) || trim( $key ) === '' ) {
			echo '<div class="notice notice-warning"><p>' . esc_html__( 'Définissez AVISION_ADMIN_API_KEY (wp-config.php) ou l’option avision_admin_api_key.', 'avision-dashboard' ) . '</p></div>';
		}

		// Actions POST (approve/reject/inject + rollback corpus).
		if ( ! empty( $_POST['avi_gu_action'] ) && isset( $_POST['_wpnonce'] ) ) {
			check_admin_referer( $nonce_action );
			$act = sanitize_key( (string) wp_unslash( $_POST['avi_gu_action'] ) );
			$id  = isset( $_POST['avi_gu_id'] ) ? (int) $_POST['avi_gu_id'] : 0;
			$corpus_id = isset( $_POST['avi_gu_corpus_id'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['avi_gu_corpus_id'] ) ) : '';

			if ( $act === 'rollback_corpus' && $corpus_id !== '' ) {
				$url_del = $base . '/admin/admin/qdrant/corpus/' . rawurlencode( $corpus_id );
				$res_del = $http_delete( $url_del, $key );
				$ok_del  = ! is_wp_error( $res_del ) && (int) wp_remote_retrieve_response_code( $res_del ) >= 200 && (int) wp_remote_retrieve_response_code( $res_del ) < 300;
				if ( $ok_del ) {
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Rollback exécuté.', 'avision-dashboard' ) . '</p></div>';
				} else {
					$msg = is_wp_error( $res_del ) ? $res_del->get_error_message() : ( 'HTTP ' . (int) wp_remote_retrieve_response_code( $res_del ) );
					echo '<div class="notice notice-error"><p>' . esc_html( $msg ) . '</p></div>';
				}
			}

			if ( $id > 0 && $tbl_exists === $tbl_updates ) {
				if ( $act === 'approve' ) {
					$wpdb->update(
						$tbl_updates,
						array( 'status' => 'approved' ),
						array( 'id' => $id ),
						array( '%s' ),
						array( '%d' )
					);
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Update approuvé.', 'avision-dashboard' ) . '</p></div>';
				} elseif ( $act === 'reject' ) {
					$wpdb->delete( $tbl_updates, array( 'id' => $id ), array( '%d' ) );
					echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Update supprimé.', 'avision-dashboard' ) . '</p></div>';
				} elseif ( $act === 'inject' ) {
					$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$tbl_updates} WHERE id = %d LIMIT 1", $id ), ARRAY_A );
					if ( ! is_array( $row ) ) {
						echo '<div class="notice notice-error"><p>' . esc_html__( 'Update introuvable.', 'avision-dashboard' ) . '</p></div>';
					} elseif ( ! is_string( $key ) || trim( $key ) === '' ) {
						echo '<div class="notice notice-error"><p>' . esc_html__( 'Clé admin manquante (AVISION_ADMIN_API_KEY).', 'avision-dashboard' ) . '</p></div>';
					} else {
						// QI avant.
						$qi_before = $qi_run_backend( $base, $key );
						$before_pct = ( $qi_before['ok'] && is_array( $qi_before['data'] ) && isset( $qi_before['data']['score_pct'] ) ) ? (int) $qi_before['data']['score_pct'] : null;

						$title   = isset( $row['title'] ) ? (string) $row['title'] : '';
						$summary = isset( $row['summary'] ) ? (string) $row['summary'] : '';
						$cat     = isset( $row['category'] ) ? (string) $row['category'] : '';
						$impact  = isset( $row['impact'] ) ? (string) $row['impact'] : '';
						$sn      = isset( $row['source_name'] ) ? (string) $row['source_name'] : ( isset( $row['source'] ) ? (string) $row['source'] : '' );
						$su      = isset( $row['source_url'] ) ? (string) $row['source_url'] : '';
						$lang    = isset( $row['lang'] ) ? (string) $row['lang'] : '';
						$cid     = isset( $row['corpus_id'] ) ? (string) $row['corpus_id'] : '';
						if ( trim( $cid ) === '' ) {
							$cid = 'google_updates_' . (string) $id;
						}

						$content = trim( $title . "\n\n" . $summary );
						if ( $sn !== '' || $su !== '' ) {
							$content .= "\n\nSource: " . trim( $sn . ' ' . $su );
						}
						$content_len = function_exists( 'mb_strlen' ) ? mb_strlen( $content ) : strlen( $content );
						if ( $content_len < 50 ) {
							$pad_src = trim( $title );
							if ( $pad_src === '' && ( $sn !== '' || $su !== '' ) ) {
								$pad_src = trim( $sn . ' ' . $su );
							}
							while ( ( function_exists( 'mb_strlen' ) ? mb_strlen( $content ) : strlen( $content ) ) < 50 && $pad_src !== '' ) {
								$content .= ( $content !== '' ? "\n\n" : '' ) . $pad_src;
							}
						}

						$cat_norm = strtoupper( trim( $cat ) );
						$strategic_category = 'LOCAL_VISIBILITY';
						if ( $cat_norm === 'REPUTATION' || strpos( $cat_norm, 'REPUTATION' ) !== false ) {
							$strategic_category = 'REPUTATION';
						} elseif ( strpos( $cat_norm, 'ROI' ) !== false || strpos( $cat_norm, 'WHOLESALE' ) !== false ) {
							$strategic_category = 'ROI_AGENCE';
						} elseif (
							in_array( $cat_norm, array( 'STRATEGY', 'LOCAL_SEO', 'GBP' ), true )
							|| strpos( $cat_norm, 'LOCAL_SEO' ) !== false
							|| strpos( $cat_norm, 'GBP' ) !== false
							|| strpos( $cat_norm, 'STRATEGY' ) !== false
						) {
							$strategic_category = 'LOCAL_VISIBILITY';
						}

						$endpoint = rtrim( self::backend_base_url(), '/' ) . '/api/v1/knowledge/upsert';
						$body = array(
							'content'            => $content,
							'strategic_category' => $strategic_category,
							'user_type'          => 'core',
							'min_level'          => 1,
							'category'           => $cat_norm,
							'metadata'           => array(
								'corpus_id'   => $cid,
								'lang'        => $lang,
								'category'    => $cat,
								'impact'      => $impact,
								'source_name' => $sn,
								'source_url'  => $su,
								'origin'      => 'control_center_google_updates',
								'wp_row_id'   => $id,
							),
							'embed'              => true,
						);
						$res = wp_remote_post(
							$endpoint,
							array(
								'timeout' => 30,
								'headers' => array(
									'Content-Type' => 'application/json; charset=utf-8',
									'X-Admin-Key'  => $key,
								),
								'body'    => wp_json_encode( $body, JSON_UNESCAPED_UNICODE ),
							)
						);
						$ok = ! is_wp_error( $res ) && (int) wp_remote_retrieve_response_code( $res ) >= 200 && (int) wp_remote_retrieve_response_code( $res ) < 300;
						if ( ! $ok ) {
							$msg = is_wp_error( $res ) ? $res->get_error_message() : ( 'HTTP ' . (int) wp_remote_retrieve_response_code( $res ) );
							echo '<div class="notice notice-error"><p>' . esc_html( $msg ) . '</p></div>';
						} else {
							$wpdb->update(
								$tbl_updates,
								array( 'status' => 'injected', 'corpus_id' => $cid ),
								array( 'id' => $id ),
								array( '%s', '%s' ),
								array( '%d' )
							);
							update_user_meta( get_current_user_id(), 'avi_gu_last_corpus_id', $cid );

							// QI après + rollback auto si régression.
							$qi_after = $qi_run_backend( $base, $key );
							$after_pct = ( $qi_after['ok'] && is_array( $qi_after['data'] ) && isset( $qi_after['data']['score_pct'] ) ) ? (int) $qi_after['data']['score_pct'] : null;
							if ( $before_pct !== null && $after_pct !== null && $after_pct < $before_pct ) {
								$url_del = $base . '/admin/admin/qdrant/corpus/' . rawurlencode( $cid );
								$res_del = $http_delete( $url_del, $key );
								$ok_del  = ! is_wp_error( $res_del ) && (int) wp_remote_retrieve_response_code( $res_del ) >= 200 && (int) wp_remote_retrieve_response_code( $res_del ) < 300;
								if ( $ok_del ) {
									$wpdb->update(
										$tbl_updates,
										array( 'status' => 'approved' ),
										array( 'id' => $id ),
										array( '%s' ),
										array( '%d' )
									);
									echo '<div class="notice notice-error"><p>' . esc_html__( 'QI en régression : rollback automatique exécuté.', 'avision-dashboard' ) . '</p></div>';
								} else {
									echo '<div class="notice notice-warning"><p>' . esc_html__( 'QI en régression : rollback automatique a échoué.', 'avision-dashboard' ) . '</p></div>';
								}
							} else {
								echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Injection effectuée.', 'avision-dashboard' ) . '</p></div>';
							}
						}
					}
				}
			}
		}

		// ─────────────────────────────────────────────────────────────
		// SECTION 1 — Corpus injectés
		// ─────────────────────────────────────────────────────────────
		echo '<div class="avi-gu-section">';
		echo '<h3>🧠 ' . esc_html__( 'Mémoire AVI — Corpus injectés dans Qdrant', 'avision-dashboard' ) . '</h3>';

		$corpus_rows = array();
		if ( is_string( $key ) && trim( $key ) !== '' ) {
			$url_points = 'http://127.0.0.1:8001/admin/admin/qdrant/points?limit=200';
			$res_points = $http_get( $url_points, $key );
			$ok_points  = ! is_wp_error( $res_points ) && (int) wp_remote_retrieve_response_code( $res_points ) >= 200 && (int) wp_remote_retrieve_response_code( $res_points ) < 300;
			if ( $ok_points ) {
				$data = json_decode( (string) wp_remote_retrieve_body( $res_points ), true );
				$pts  = ( is_array( $data ) && isset( $data['points'] ) && is_array( $data['points'] ) ) ? $data['points'] : array();
				$by_corpus = array();
				foreach ( $pts as $p ) {
					if ( ! is_array( $p ) ) {
						continue;
					}
					$meta = isset( $p['metadata'] ) && is_array( $p['metadata'] ) ? $p['metadata'] : array();
					$cid  = isset( $meta['corpus_id'] ) ? (string) $meta['corpus_id'] : '';
					if ( $cid === '' ) {
						continue;
					}
					if ( ! isset( $by_corpus[ $cid ] ) ) {
						$by_corpus[ $cid ] = array(
							'corpus_id' => $cid,
							'n'         => 0,
							'category'  => isset( $meta['category'] ) ? (string) $meta['category'] : '',
							'lang'      => isset( $meta['lang'] ) ? (string) $meta['lang'] : '',
						);
					}
					$by_corpus[ $cid ]['n']++;
				}
				$corpus_rows = array_values( $by_corpus );
			}
		}

		echo '<table class="widefat striped avi-cc-table"><thead><tr>';
		echo '<th>corpus_id</th><th>' . esc_html__( 'Nb chunks', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Catégorie', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Lang', 'avision-dashboard' ) . '</th><th>' . esc_html__( 'Actions', 'avision-dashboard' ) . '</th>';
		echo '</tr></thead><tbody>';
		if ( empty( $corpus_rows ) ) {
			echo '<tr><td colspan="5">' . esc_html__( 'Aucun corpus détecté (ou clé admin manquante).', 'avision-dashboard' ) . '</td></tr>';
		} else {
			foreach ( $corpus_rows as $cr ) {
				$cid = isset( $cr['corpus_id'] ) ? (string) $cr['corpus_id'] : '';
				$n   = isset( $cr['n'] ) ? (int) $cr['n'] : 0;
				$cat = isset( $cr['category'] ) ? (string) $cr['category'] : '';
				$lang = isset( $cr['lang'] ) ? (string) $cr['lang'] : '';
				$rel = $reliability_icon_from_corpus( $cid );
				echo '<tr>';
				echo '<td><code>' . esc_html( $cid ) . '</code> <span title="reliability">' . esc_html( $rel ) . '</span></td>';
				echo '<td>' . esc_html( (string) $n ) . '</td>';
				echo '<td><code>' . esc_html( $cat ) . '</code></td>';
				echo '<td><code>' . esc_html( $lang ) . '</code></td>';
				echo '<td>';
				echo '<form method="post" action="" onsubmit="return confirm(\'Rollback ce corpus ?\');" style="display:inline;">';
				wp_nonce_field( $nonce_action );
				echo '<input type="hidden" name="avi_gu_action" value="rollback_corpus" />';
				echo '<input type="hidden" name="avi_gu_corpus_id" value="' . esc_attr( $cid ) . '" />';
				echo '<button type="submit" class="button">🗑️ ' . esc_html__( 'Rollback', 'avision-dashboard' ) . '</button>';
				echo '</form>';
				echo '</td>';
				echo '</tr>';
			}
		}
		echo '</tbody></table>';
		echo '</div>';

		// ─────────────────────────────────────────────────────────────
		// SECTION 2 — Injections en attente
		// ─────────────────────────────────────────────────────────────
		echo '<div class="avi-gu-section">';
		echo '<h3>📥 ' . esc_html__( 'Injections en attente — Validation manuelle', 'avision-dashboard' ) . '</h3>';

		if ( $tbl_exists !== $tbl_updates ) {
			echo '<p><strong>' . esc_html__( 'Table manquante :', 'avision-dashboard' ) . '</strong> <code>' . esc_html( $tbl_updates ) . '</code></p>';
			echo '</div>';
		} else {
			// Watcher — affichage dernier run (watcher_hashes.json).
			$watcher_last_run = '';
			$watcher_hashes_paths = array(
				'/root/avision-backend/data/watcher_hashes.json',
				trailingslashit( untrailingslashit( self::repo_public_root() ) ) . 'avision-backend/data/watcher_hashes.json',
			);
			foreach ( $watcher_hashes_paths as $whp ) {
				if ( is_readable( $whp ) ) {
					$raw = (string) @file_get_contents( $whp );
					$dec = json_decode( $raw, true );
					if ( is_array( $dec ) ) {
						foreach ( array( 'last_run', 'updated_at', 'timestamp', 'generated_at' ) as $k ) {
							if ( ! empty( $dec[ $k ] ) && is_string( $dec[ $k ] ) ) {
								$watcher_last_run = $dec[ $k ];
								break;
							}
						}
					}
					if ( $watcher_last_run === '' ) {
						$mt = @filemtime( $whp );
						if ( $mt ) {
							$watcher_last_run = date_i18n( 'Y-m-d H:i:s', (int) $mt );
						}
					}
					break;
				}
			}

			echo '<div style="margin:12px 0 16px;padding:12px 14px;border:1px solid #e5e7eb;border-radius:10px;background:#f8fafc;">';
			echo '<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;">';
			echo '<div><strong>🔄 ' . esc_html__( 'Watcher — Veille automatique', 'avision-dashboard' ) . '</strong><div style="color:#555;font-size:12px;margin-top:2px;">' . esc_html__( 'Dernier run :', 'avision-dashboard' ) . ' <code>' . esc_html( $watcher_last_run !== '' ? $watcher_last_run : '—' ) . '</code></div></div>';
			echo '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
			echo '<button type="button" class="button button-secondary" id="aviGuWatcherRunBtn">▶ ' . esc_html__( 'Lancer le watcher maintenant', 'avision-dashboard' ) . '</button>';
			echo '<a href="#" id="aviGuWatcherLogLink" class="button">' . esc_html__( '📋 Voir le log', 'avision-dashboard' ) . '</a>';
			echo '</div>';
			echo '</div>';
			echo '<div id="aviGuWatcherResult" style="display:none;margin-top:10px;padding:10px;border:1px dashed #bbb;border-radius:8px;background:#fff;"></div>';
			echo '<pre id="aviGuWatcherLog" style="display:none;margin-top:10px;max-height:260px;overflow:auto;padding:10px;border:1px solid #e5e7eb;border-radius:8px;background:#0b1020;color:#e5e7eb;font-size:12px;line-height:1.4;"></pre>';
			echo '</div>';
			$gu_status = isset( $_GET['gu_status'] ) ? sanitize_key( (string) wp_unslash( $_GET['gu_status'] ) ) : 'all';
			$gu_cat    = isset( $_GET['gu_category'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['gu_category'] ) ) : 'all';
			$gu_rel    = isset( $_GET['gu_reliability'] ) ? sanitize_key( (string) wp_unslash( $_GET['gu_reliability'] ) ) : 'all';
			$gu_orderby = isset( $_GET['gu_orderby'] ) ? sanitize_key( (string) wp_unslash( $_GET['gu_orderby'] ) ) : 'update_date';
			$gu_order   = isset( $_GET['gu_order'] ) ? strtoupper( sanitize_key( (string) wp_unslash( $_GET['gu_order'] ) ) ) : 'DESC';

			$allowed_status = array( 'all', 'pending', 'approved', 'injected' );
			if ( ! in_array( $gu_status, $allowed_status, true ) ) {
				$gu_status = 'all';
			}
			$allowed_rel = array( 'all', 'official', 'expert', 'blog' );
			if ( ! in_array( $gu_rel, $allowed_rel, true ) ) {
				$gu_rel = 'all';
			}
			$allowed_orderby = array(
				'id'         => 'id',
				'category'   => 'category',
				'impact'     => 'impact',
				'status'     => 'status',
				'update_date' => 'update_date',
			);
			if ( ! isset( $allowed_orderby[ $gu_orderby ] ) ) {
				$gu_orderby = 'update_date';
			}
			if ( $gu_order !== 'ASC' && $gu_order !== 'DESC' ) {
				$gu_order = 'DESC';
			}

			// Cat dropdown.
			$cats = array();
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name validated via SHOW TABLES.
			$cat_rows = $wpdb->get_col( "SELECT DISTINCT category FROM {$tbl_updates} WHERE category IS NOT NULL AND TRIM(category) <> '' ORDER BY category ASC" );
			if ( is_array( $cat_rows ) ) {
				foreach ( $cat_rows as $c ) {
					$c = is_string( $c ) ? trim( $c ) : '';
					if ( $c !== '' ) {
						$cats[] = $c;
					}
				}
			}

			// KPIs
			$c_pending  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tbl_updates} WHERE status = 'pending'" );
			$c_approved = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tbl_updates} WHERE status = 'approved'" );
			$c_injected = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$tbl_updates} WHERE status = 'injected'" );
			echo '<div class="avi-gu-kpi">';
			echo '<span class="badge"><span class="dot dot--pending"></span>Pending <strong>' . esc_html( (string) $c_pending ) . '</strong></span>';
			echo '<span class="badge"><span class="dot dot--approved"></span>Approved <strong>' . esc_html( (string) $c_approved ) . '</strong></span>';
			echo '<span class="badge"><span class="dot dot--injected"></span>Injected <strong>' . esc_html( (string) $c_injected ) . '</strong></span>';
			echo '</div>';

			echo '<form method="get" action="" class="avi-gu-filters">';
			echo '<input type="hidden" name="page" value="' . esc_attr( self::PAGE_SLUG ) . '" />';
			echo '<input type="hidden" name="tab" value="google_updates" />';
			echo '<div><label>' . esc_html__( 'Statut', 'avision-dashboard' ) . '</label><select name="gu_status">';
			foreach ( $allowed_status as $st ) {
				$lbl = $st === 'all' ? 'All' : ucfirst( $st );
				echo '<option value="' . esc_attr( $st ) . '"' . selected( $gu_status, $st, false ) . '>' . esc_html( $lbl ) . '</option>';
			}
			echo '</select></div>';

			echo '<div><label>' . esc_html__( 'Catégorie', 'avision-dashboard' ) . '</label><select name="gu_category">';
			echo '<option value="all"' . selected( $gu_cat, 'all', false ) . '>All</option>';
			foreach ( $cats as $c ) {
				echo '<option value="' . esc_attr( $c ) . '"' . selected( $gu_cat, $c, false ) . '>' . esc_html( $c ) . '</option>';
			}
			echo '</select></div>';

			echo '<div><label>' . esc_html__( 'Fiabilité', 'avision-dashboard' ) . '</label><select name="gu_reliability">';
			echo '<option value="all"' . selected( $gu_rel, 'all', false ) . '>All</option>';
			echo '<option value="official"' . selected( $gu_rel, 'official', false ) . '>🏛️ Official</option>';
			echo '<option value="expert"' . selected( $gu_rel, 'expert', false ) . '>🎓 Expert</option>';
			echo '<option value="blog"' . selected( $gu_rel, 'blog', false ) . '>📝 Blog</option>';
			echo '</select></div>';

			echo '<input type="hidden" name="gu_orderby" value="' . esc_attr( $gu_orderby ) . '" />';
			echo '<input type="hidden" name="gu_order" value="' . esc_attr( $gu_order ) . '" />';
			submit_button( __( 'Filtrer', 'avision-dashboard' ), 'secondary', '', false );
			echo '</form>';

			echo '<p style="margin:0 0 10px;color:#444;"><strong>' . esc_html__( 'Fiabilité sources :', 'avision-dashboard' ) . '</strong> 🏛️ Official / 🎓 Expert / 📝 Blog</p>';

			$where_parts = array( '1=1' );
			$where_args  = array();
			if ( $gu_status !== 'all' ) {
				$where_parts[] = 'status = %s';
				$where_args[]  = $gu_status;
			}
			if ( $gu_cat !== 'all' && trim( $gu_cat ) !== '' ) {
				$where_parts[] = 'category = %s';
				$where_args[]  = $gu_cat;
			}
			$where_sql = implode( ' AND ', $where_parts );

			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- ORDER BY column validated via allowlist.
			$sql_list = "SELECT * FROM {$tbl_updates} WHERE {$where_sql} ORDER BY " . $allowed_orderby[ $gu_orderby ] . ' ' . $gu_order . ' LIMIT 200';
			if ( ! empty( $where_args ) ) {
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- placeholders are prepared below.
				$sql_list = $wpdb->prepare( $sql_list, $where_args );
			}
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- sql_list already prepared when args exist, otherwise constant parts only.
			$rows = $wpdb->get_results( $sql_list, ARRAY_A );
			if ( ! is_array( $rows ) ) {
				$rows = array();
			}

			$self_url = self::control_center_admin_screen_url(
				array(
					'page' => self::PAGE_SLUG,
					'tab'  => 'google_updates',
				)
			);
			$mk_sort = static function ( $label, $col ) use ( $self_url, $gu_status, $gu_cat, $gu_rel, $gu_orderby, $gu_order ) {
				$next = ( $gu_orderby === $col && $gu_order === 'ASC' ) ? 'DESC' : 'ASC';
				$url = add_query_arg(
					array(
						'gu_status'      => $gu_status,
						'gu_category'    => $gu_cat,
						'gu_reliability' => $gu_rel,
						'gu_orderby'     => $col,
						'gu_order'       => $next,
					),
					$self_url
				);
				$arrow = '';
				if ( $gu_orderby === $col ) {
					$arrow = $gu_order === 'ASC' ? ' ▲' : ' ▼';
				}
				return '<a href="' . esc_url( $url ) . '" style="text-decoration:none;">' . esc_html( $label . $arrow ) . '</a>';
			};

			echo '<table class="widefat striped avi-cc-table"><thead><tr>';
			echo '<th>' . $mk_sort( '#', 'id' ) . '</th>';
			echo '<th>' . esc_html__( 'Titre', 'avision-dashboard' ) . '</th>';
			echo '<th>' . esc_html__( 'Source', 'avision-dashboard' ) . '</th>';
			echo '<th>' . esc_html__( 'Fiabilité', 'avision-dashboard' ) . '</th>';
			echo '<th>' . $mk_sort( __( 'Catégorie', 'avision-dashboard' ), 'category' ) . '</th>';
			echo '<th>' . $mk_sort( __( 'Impact', 'avision-dashboard' ), 'impact' ) . '</th>';
			echo '<th>' . $mk_sort( __( 'Statut', 'avision-dashboard' ), 'status' ) . '</th>';
			echo '<th>' . $mk_sort( __( 'Date', 'avision-dashboard' ), 'update_date' ) . '</th>';
			echo '<th>' . esc_html__( 'Actions', 'avision-dashboard' ) . '</th>';
			echo '</tr></thead><tbody>';

		foreach ( $rows as $r ) {
			if ( ! is_array( $r ) ) {
				continue;
			}
				$id     = isset( $r['id'] ) ? (int) $r['id'] : 0;
				$title  = isset( $r['title'] ) ? (string) $r['title'] : '';
				$summary = isset( $r['summary'] ) ? (string) $r['summary'] : '';
				$cat    = isset( $r['category'] ) ? (string) $r['category'] : '';
				$impact = isset( $r['impact'] ) ? (string) $r['impact'] : '';
				$status = isset( $r['status'] ) ? strtolower( trim( (string) $r['status'] ) ) : 'pending';
				$date   = isset( $r['update_date'] ) ? (string) $r['update_date'] : ( isset( $r['created_at'] ) ? (string) $r['created_at'] : '' );
				$sn     = isset( $r['source_name'] ) ? (string) $r['source_name'] : '';
				$su     = isset( $r['source_url'] ) ? (string) $r['source_url'] : '';
				$rel_i  = $reliability_icon_from_source_name( $sn );

				// Reliability filter (computed).
				if ( $gu_rel !== 'all' ) {
					if ( $gu_rel === 'official' && $rel_i !== '🏛️' ) {
						continue;
					}
					if ( $gu_rel === 'expert' && $rel_i !== '🎓' ) {
						continue;
					}
					if ( $gu_rel === 'blog' && $rel_i !== '📝' ) {
						continue;
					}
				}

				$sum120 = function_exists( 'mb_substr' ) ? mb_substr( $summary, 0, 120 ) : substr( $summary, 0, 120 );
				if ( strlen( $summary ) > 120 ) {
					$sum120 .= '…';
				}

				$st_icon = $status === 'injected' ? '✔' : ( $status === 'approved' ? '✅' : '⏳' );
			echo '<tr>';
				echo '<td>' . esc_html( (string) $id ) . '</td>';
				echo '<td><strong>' . esc_html( $title ) . '</strong><div style="margin-top:4px;color:#555;font-size:12px;">' . esc_html( $sum120 ) . '</div></td>';
				echo '<td>' . esc_html( $sn ) . ( $su !== '' ? ' <a href="' . esc_url( $su ) . '" target="_blank" rel="noopener noreferrer">🔗</a>' : '' ) . '</td>';
				echo '<td>' . esc_html( $rel_i ) . '</td>';
				echo '<td><code>' . esc_html( $cat ) . '</code></td>';
				echo '<td><span class="' . esc_attr( $impact_class( $impact ) ) . '">' . esc_html( strtoupper( (string) $impact ) ) . '</span></td>';
				echo '<td>' . esc_html( $st_icon . ' ' . $status ) . '</td>';
				echo '<td>' . esc_html( $date ) . '</td>';
				echo '<td>';
				if ( $status === 'pending' ) {
					echo '<form method="post" action="" style="display:inline;margin-right:8px;">';
					wp_nonce_field( $nonce_action );
					echo '<input type="hidden" name="avi_gu_action" value="approve" /><input type="hidden" name="avi_gu_id" value="' . esc_attr( (string) $id ) . '" />';
					echo '<button class="button button-primary" type="submit">✅ ' . esc_html__( 'Approuver', 'avision-dashboard' ) . '</button>';
					echo '</form>';
					echo '<form method="post" action="" style="display:inline;">';
					wp_nonce_field( $nonce_action );
					echo '<input type="hidden" name="avi_gu_action" value="reject" /><input type="hidden" name="avi_gu_id" value="' . esc_attr( (string) $id ) . '" />';
					echo '<button class="button" type="submit">🗑️ ' . esc_html__( 'Supprimer', 'avision-dashboard' ) . '</button>';
					echo '</form>';
				} elseif ( $status === 'approved' ) {
					echo '<form method="post" action="" onsubmit="return confirm(\'Injecter dans Qdrant ?\');" style="display:inline;">';
					wp_nonce_field( $nonce_action );
					echo '<input type="hidden" name="avi_gu_action" value="inject" /><input type="hidden" name="avi_gu_id" value="' . esc_attr( (string) $id ) . '" />';
					echo '<button class="button button-secondary" type="submit">🚀 ' . esc_html__( 'Injecter', 'avision-dashboard' ) . '</button>';
					echo '</form>';
				} else {
					echo '<span style="color:#047857;font-weight:800;">✔ ' . esc_html__( 'Dans Qdrant', 'avision-dashboard' ) . '</span>';
			}
			echo '</td>';
			echo '</tr>';
		}

		if ( empty( $rows ) ) {
				echo '<tr><td colspan="9">' . esc_html__( 'Aucune entrée.', 'avision-dashboard' ) . '</td></tr>';
		}
		echo '</tbody></table>';
			echo '</div>';
		}

		// ─────────────────────────────────────────────────────────────
		// SECTION 3 — Test QI (AJAX existant)
		// ─────────────────────────────────────────────────────────────
		$qi_nonce = wp_create_nonce( 'avision_cc_intelligence' );
		echo '<div class="avi-gu-section" id="aviGuQiSection">';
		echo '<h3>📊 ' . esc_html__( 'Score QI AVI — Anti-régression', 'avision-dashboard' ) . '</h3>';
		echo '<p style="margin:0 0 12px;color:#555;">' . esc_html__( 'Exécutez le baseline avant/après injection pour détecter toute régression.', 'avision-dashboard' ) . '</p>';
		echo '<p style="margin:0 0 12px;">';
		echo '<button type="button" class="button" id="aviGuQiBeforeBtn">▶ ' . esc_html__( 'Test QI Avant', 'avision-dashboard' ) . '</button> ';
		echo '<button type="button" class="button" id="aviGuQiAfterBtn">▶ ' . esc_html__( 'Test QI Après', 'avision-dashboard' ) . '</button> ';
		echo '<button type="button" class="button" id="aviGuQiRollbackBtn" style="display:none;">↩️ ' . esc_html__( 'Rollback dernière injection', 'avision-dashboard' ) . '</button>';
		echo '</p>';
		echo '<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-end;">';
		echo '<div><div style="font-size:12px;font-weight:800;color:#555;">' . esc_html__( 'QI avant', 'avision-dashboard' ) . '</div><div class="avi-gu-score" id="aviGuQiBeforeScore">—</div><span class="avi-gu-score-badge" id="aviGuQiBeforeBadge" style="display:none;"></span></div>';
		echo '<div><div style="font-size:12px;font-weight:800;color:#555;">' . esc_html__( 'QI après', 'avision-dashboard' ) . '</div><div class="avi-gu-score" id="aviGuQiAfterScore">—</div><span class="avi-gu-score-badge" id="aviGuQiAfterBadge" style="display:none;"></span></div>';
		echo '<div><div style="font-size:12px;font-weight:800;color:#555;">' . esc_html__( 'Delta', 'avision-dashboard' ) . '</div><div class="avi-gu-score" id="aviGuQiDelta">—</div></div>';
		echo '</div>';

		$last_corpus = (string) get_user_meta( get_current_user_id(), 'avi_gu_last_corpus_id', true );
		echo '<form method="post" action="" id="aviGuQiRollbackForm" style="display:none;">';
		wp_nonce_field( $nonce_action );
		echo '<input type="hidden" name="avi_gu_action" value="rollback_corpus" />';
		echo '<input type="hidden" name="avi_gu_corpus_id" value="' . esc_attr( $last_corpus ) . '" />';
		echo '</form>';

		echo '<script>jQuery(document).ready(function($){'
			. 'var nonce=' . wp_json_encode( $qi_nonce ) . ';'
			. 'function watcherRun(){'
			. 'var $btn=$("#aviGuWatcherRunBtn");var $res=$("#aviGuWatcherResult");'
			. '$res.hide().text("");'
			. '$btn.prop("disabled",true).text("⏳ Watcher...");'
			. '$.post(ajaxurl,{action:"avision_cc_watcher_run",nonce:nonce}).done(function(r){'
			. '$btn.prop("disabled",false).text("▶ Lancer le watcher maintenant");'
			. 'if(r&&r.success&&r.data){'
			. 'var d=r.data;'
			. 'var out="returncode="+String(d.returncode)+"\\n";'
			. 'out+="stdout:\\n"+String(d.stdout||"")+"\\n\\n";'
			. 'out+="stderr:\\n"+String(d.stderr||"");'
			. '$res.show().text(out);return;'
			. '}'
			. '$res.show().text("Watcher error: "+JSON.stringify(r));'
			. '}).fail(function(){'
			. '$btn.prop("disabled",false).text("▶ Lancer le watcher maintenant");'
			. '$res.show().text("Watcher network error");'
			. '});'
			. '}'
			. 'function watcherLog(e){'
			. 'if(e&&e.preventDefault){e.preventDefault();}'
			. 'var $pre=$("#aviGuWatcherLog");'
			. '$pre.hide().text("Chargement...");'
			. '$.post(ajaxurl,{action:"avision_cc_watcher_log",nonce:nonce}).done(function(r){'
			. 'if(r&&r.success&&r.data&&r.data.lines){$pre.show().text(r.data.lines.join("\\n"));return;}'
			. '$pre.show().text("Log error: "+JSON.stringify(r));'
			. '}).fail(function(){ $pre.show().text("Log network error"); });'
			. '}'
			. 'var before=null, after=null;'
			. 'function badge($el,p){$el.show();$el.removeClass("avi-gu-score--good avi-gu-score--warn avi-gu-score--bad");var cls="avi-gu-score--bad",txt="RED";if(p>=95){cls="avi-gu-score--good";txt="GREEN";}else if(p>=80){cls="avi-gu-score--warn";txt="ORANGE";}$el.addClass(cls);$el.text(txt);}'
			. 'function render(){'
			. 'if(before!==null){$("#aviGuQiBeforeScore").text(String(before)+"%");badge($("#aviGuQiBeforeBadge"),before);}'
			. 'if(after!==null){$("#aviGuQiAfterScore").text(String(after)+"%");badge($("#aviGuQiAfterBadge"),after);}'
			. 'if(before!==null&&after!==null){var d=after-before;var s=(d>=0?("+"+String(d)):String(d))+"%";$("#aviGuQiDelta").text(s+(d<0?" 🔴":" ✅")); if(d<0){$("#aviGuQiRollbackBtn").show();} else {$("#aviGuQiRollbackBtn").hide();}}'
			. '}'
			. 'function runQi(cb){'
			. '$("#aviGuQiBeforeBtn,#aviGuQiAfterBtn").prop("disabled",true).text("⏳ ~3 min...");'
			. '$.post(ajaxurl,{action:"avision_cc_qi_run_baseline",nonce:nonce})'
			. '.done(function(r){'
			. '$("#aviGuQiBeforeBtn").prop("disabled",false).text("▶ Test QI Avant");'
			. '$("#aviGuQiAfterBtn").prop("disabled",false).text("▶ Test QI Après");'
			. 'if(r&&r.success&&r.data&&typeof r.data.score_global!=="undefined"){cb(parseInt(r.data.score_global,10));return;}'
			. 'alert("QI error: "+JSON.stringify(r));'
			. '})'
			. '.fail(function(){'
			. '$("#aviGuQiBeforeBtn").prop("disabled",false).text("▶ Test QI Avant");'
			. '$("#aviGuQiAfterBtn").prop("disabled",false).text("▶ Test QI Après");'
			. 'alert("QI network error");'
			. '});'
			. '}'
			. '$("#aviGuQiBeforeBtn").on("click",function(){runQi(function(p){before=p;render();});});'
			. '$("#aviGuQiAfterBtn").on("click",function(){runQi(function(p){after=p;render();});});'
			. '$("#aviGuQiRollbackBtn").on("click",function(){if(!confirm("Rollback dernière injection ?")){return;} $("#aviGuQiRollbackForm").trigger("submit");});'
			. '$("#aviGuWatcherRunBtn").on("click",function(){watcherRun();});'
			. '$("#aviGuWatcherLogLink").on("click",function(e){watcherLog(e);});'
			. '});</script>';
		echo '</div>';

		// ─────────────────────────────────────────────────────────────
		// SECTION 4 — Idées Blog
		// ─────────────────────────────────────────────────────────────
		echo '<div class="avi-gu-section">';
		echo '<h3>💡 ' . esc_html__( 'Idées Articles — Tendances Google Maps 2026', 'avision-dashboard' ) . '</h3>';
		if ( $tbl_exists !== $tbl_updates ) {
			echo '<p>' . esc_html__( 'Table updates introuvable.', 'avision-dashboard' ) . '</p>';
		} else {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name validated via SHOW TABLES.
			$ideas = $wpdb->get_results( "SELECT id, title, category, source_name, update_date, status FROM {$tbl_updates} WHERE status IN ('approved','injected') ORDER BY update_date DESC LIMIT 8", ARRAY_A );
			if ( ! is_array( $ideas ) ) {
				$ideas = array();
			}
			if ( empty( $ideas ) ) {
				echo '<p style="margin:0;color:#555;">' . esc_html__( 'Aucune idée disponible.', 'avision-dashboard' ) . '</p>';
			} else {
				echo '<ul style="margin:0;padding-left:18px;">';
				foreach ( $ideas as $it ) {
					$id = isset( $it['id'] ) ? (int) $it['id'] : 0;
					$title = isset( $it['title'] ) ? (string) $it['title'] : '';
					$cat = isset( $it['category'] ) ? (string) $it['category'] : '';
					$sn = isset( $it['source_name'] ) ? (string) $it['source_name'] : '';
					$dt = isset( $it['update_date'] ) ? (string) $it['update_date'] : '';
					echo '<li style="margin:0 0 10px;">';
					echo '<strong>' . esc_html( $title ) . '</strong> ';
					if ( $cat !== '' ) {
						echo '<code>' . esc_html( $cat ) . '</code> ';
					}
					echo '<span style="color:#555;">' . esc_html( $sn ) . ' — ' . esc_html( $dt ) . '</span> ';
					echo '<button type="button" class="button" style="margin-left:8px;" onclick="try{navigator.clipboard.writeText(' . wp_json_encode( $title ) . ');this.textContent=\'✅ Copié\';}catch(e){}">📋 ' . esc_html__( 'Copier titre', 'avision-dashboard' ) . '</button>';
					echo '</li>';
				}
				echo '</ul>';
			}
		}
		echo '</div>';
	}

	private static function render_announcements_tab() {
		global $wpdb;
		$table      = $wpdb->prefix . 'avi_banners';
		$tbl_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );

		// Handle form actions.
		if ( isset( $_POST['avi_banner_action'] ) && check_admin_referer( 'avi_banner_nonce' ) ) {
			$action = sanitize_text_field( (string) wp_unslash( $_POST['avi_banner_action'] ) );

			if ( $action === 'create' || $action === 'update' ) {
				$title_raw   = isset( $_POST['banner_title'] ) ? (string) wp_unslash( $_POST['banner_title'] ) : '';
				$msg_raw     = isset( $_POST['banner_message'] ) ? (string) wp_unslash( $_POST['banner_message'] ) : '';
				$badge_text  = isset( $_POST['badge_text'] ) ? (string) wp_unslash( $_POST['badge_text'] ) : 'Info';
				$badge_color = isset( $_POST['badge_color'] ) ? (string) wp_unslash( $_POST['badge_color'] ) : '#3B82F6';
				$text_size   = isset( $_POST['text_size'] ) ? (string) wp_unslash( $_POST['text_size'] ) : 'normal';
				$bold_seg    = isset( $_POST['bold_segments'] ) ? (string) wp_unslash( $_POST['bold_segments'] ) : '';
				$audience    = isset( $_POST['audience'] ) ? (string) wp_unslash( $_POST['audience'] ) : 'both';

				$text_size = in_array( $text_size, array( 'small', 'normal', 'large' ), true ) ? $text_size : 'normal';
				$audience  = in_array( $audience, array( 'b2c', 'b2b', 'both' ), true ) ? $audience : 'both';

				$color = function_exists( 'sanitize_hex_color' ) ? sanitize_hex_color( $badge_color ) : $badge_color;
				if ( ! is_string( $color ) || $color === '' ) {
					$color = '#3B82F6';
				}

				$data = array(
					'title'         => sanitize_text_field( $title_raw ),
					'message'       => sanitize_textarea_field( $msg_raw ),
					'badge_text'    => sanitize_text_field( $badge_text ),
					'badge_color'   => $color,
					'text_size'     => $text_size,
					'bold_segments' => sanitize_textarea_field( $bold_seg ),
					'scrolling'     => isset( $_POST['scrolling'] ) ? 1 : 0,
					'audience'      => $audience,
					'active'        => isset( $_POST['banner_active'] ) ? 1 : 0,
				);

				if ( $action === 'create' ) {
					$wpdb->insert( $table, $data );
				} else {
					$wpdb->update(
						$table,
						$data,
						array( 'id' => (int) ( isset( $_POST['banner_id'] ) ? (string) wp_unslash( $_POST['banner_id'] ) : 0 ) )
					);
				}
			} elseif ( $action === 'delete' ) {
				$wpdb->delete(
					$table,
					array( 'id' => (int) ( isset( $_POST['banner_id'] ) ? (string) wp_unslash( $_POST['banner_id'] ) : 0 ) )
				);
			} elseif ( $action === 'toggle' ) {
				$bid = (int) ( isset( $_POST['banner_id'] ) ? (string) wp_unslash( $_POST['banner_id'] ) : 0 );
				$cur = (int) $wpdb->get_var( $wpdb->prepare( "SELECT active FROM {$table} WHERE id=%d", $bid ) );
				$wpdb->update( $table, array( 'active' => $cur ? 0 : 1 ), array( 'id' => $bid ) );
			}
		}

		$banners = $tbl_exists
			? $wpdb->get_results( "SELECT * FROM {$table} ORDER BY id DESC", ARRAY_A )
			: array();
		?>
		<div class="avi-cc-tab-content" style="padding:24px;">
			<style>
				.avi-cc-tab-content input[type="text"]::placeholder,
				.avi-cc-tab-content textarea::placeholder {
					color: rgba(255, 255, 255, 0.45) !important;
				}
				.avi-cc-tab-content input[type="text"],
				.avi-cc-tab-content textarea {
					color: #ffffff !important;
				}
				.avi-cc-tab-content .form-table th {
					color: #ffffff !important;
					font-weight: 600;
				}
			</style>
			<h2 style="color:#fff;margin-bottom:4px;">📢 Announcements</h2>
			<p style="color:var(--avi-text-muted,#8899AA);margin-bottom:24px;">
				Broadcast banners to B2C members, B2B agencies, or both.
			</p>

			<?php if ( ! $tbl_exists ) : ?>
				<div class="notice notice-error">
					<p>Table <code>avi_banners</code> does not exist. Re-save plugin settings to trigger install.</p>
				</div>
			<?php else : ?>

			<div style="background:#0D1E38;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:24px;margin-bottom:32px;">
				<h3 style="color:#fff;margin:0 0 16px;">➕ New Announcement</h3>
				<form method="post">
					<?php wp_nonce_field( 'avi_banner_nonce' ); ?>
					<input type="hidden" name="avi_banner_action" value="create">

					<table class="form-table" style="color:#C0D0E8;">
						<tr>
							<th>Title</th>
							<td><input type="text" name="banner_title" style="width:100%;background:#0A1628;border:1px solid #2A3A5C;color:#ffffff !important;padding:8px 12px;border-radius:6px;" required></td>
						</tr>
						<tr>
							<th>Message</th>
							<td>
								<textarea name="banner_message" rows="3" style="width:100%;background:#0A1628;border:1px solid #2A3A5C;color:#ffffff !important;padding:8px 12px;border-radius:6px;resize:vertical;"></textarea>
								<p class="description" style="color:#8899AA;">
									Wrap text in **double asterisks** to bold it. Ex: "Your **Guardian** is now active."
								</p>
							</td>
						</tr>
						<tr>
							<th>Badge Text</th>
							<td><input type="text" name="badge_text" value="Info" style="width:200px;background:#0A1628;border:1px solid #2A3A5C;color:#ffffff !important;padding:8px 12px;border-radius:6px;"></td>
						</tr>
						<tr>
							<th>Badge Color</th>
							<td>
								<input type="color" name="badge_color" value="#3B82F6">
								&nbsp;
								<span style="color:#8899AA;font-size:12px;">
									Suggestions:
									<span style="color:#3B82F6">■</span> Info #3B82F6 &nbsp;
									<span style="color:#10B981">■</span> Update #10B981 &nbsp;
									<span style="color:#F59E0B">■</span> Important #F59E0B &nbsp;
									<span style="color:#EF4444">■</span> Alert #EF4444
								</span>
							</td>
						</tr>
						<tr>
							<th>Text Size</th>
							<td>
								<select name="text_size" style="background:#0A1628;border:1px solid #2A3A5C;color:#fff;padding:8px;border-radius:6px;">
									<option value="small">Small</option>
									<option value="normal" selected>Normal</option>
									<option value="large">Large</option>
								</select>
							</td>
						</tr>
						<tr>
							<th>Scrolling text</th>
							<td>
								<label><input type="checkbox" name="scrolling" value="1"> Enable marquee scrolling</label>
							</td>
						</tr>
						<tr>
							<th>Audience</th>
							<td>
								<select name="audience" style="background:#0A1628;border:1px solid #2A3A5C;color:#fff;padding:8px;border-radius:6px;">
									<option value="both">Both (B2C + B2B)</option>
									<option value="b2c">B2C only (Premium members)</option>
									<option value="b2b">B2B only (Agencies)</option>
								</select>
							</td>
						</tr>
						<tr>
							<th>Active</th>
							<td>
								<label><input type="checkbox" name="banner_active" value="1" checked> Publish immediately</label>
							</td>
						</tr>
					</table>
					<button type="submit" class="button button-primary">Create Announcement</button>
				</form>
			</div>

			<h3 style="color:#fff;margin-bottom:16px;">📋 Active Announcements (<?php echo (int) count( $banners ); ?>)</h3>
			<?php if ( empty( $banners ) ) : ?>
				<p style="color:#8899AA;">No announcements yet.</p>
			<?php else : ?>
				<?php foreach ( $banners as $b ) : ?>
					<div style="background:#0D1E38;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:20px;margin-bottom:16px;border-left:4px solid <?php echo esc_attr( (string) $b['badge_color'] ); ?>;">
						<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;">
							<div style="flex:1;">
								<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
									<span style="background:<?php echo esc_attr( (string) $b['badge_color'] ); ?>;color:#fff;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;text-transform:uppercase;letter-spacing:0.5px;">
										<?php echo esc_html( (string) $b['badge_text'] ); ?>
									</span>
									<strong style="color:#fff;"><?php echo esc_html( (string) $b['title'] ); ?></strong>
									<span style="color:#8899AA;font-size:12px;">
										[<?php echo esc_html( (string) $b['audience'] ); ?>]
										<?php echo ! empty( $b['scrolling'] ) ? '🔄 scrolling' : ''; ?>
										<?php echo ! empty( $b['active'] ) ? '' : '⏸ paused'; ?>
									</span>
								</div>
								<p style="color:#C0D0E8;margin:0;font-size:13px;"><?php echo esc_html( (string) $b['message'] ); ?></p>
							</div>
							<div style="display:flex;flex-direction:column;gap:6px;min-width:90px;">
								<form method="post" style="margin:0;">
									<?php wp_nonce_field( 'avi_banner_nonce' ); ?>
									<input type="hidden" name="avi_banner_action" value="toggle">
									<input type="hidden" name="banner_id" value="<?php echo (int) $b['id']; ?>">
									<button type="submit" class="button button-small" style="width:100%;">
										<?php echo ! empty( $b['active'] ) ? '⏸ Pause' : '▶ Activate'; ?>
									</button>
								</form>
								<form method="post" style="margin:0;" onsubmit="return confirm('Delete this announcement?');">
									<?php wp_nonce_field( 'avi_banner_nonce' ); ?>
									<input type="hidden" name="avi_banner_action" value="delete">
									<input type="hidden" name="banner_id" value="<?php echo (int) $b['id']; ?>">
									<button type="submit" class="button button-small" style="width:100%;color:#EF4444;border-color:#EF4444;">🗑 Delete</button>
								</form>
							</div>
						</div>
					</div>
				<?php endforeach; ?>
			<?php endif; ?>
			<?php endif; ?>
		</div>
		<?php
	}


	/**
	 * Onglet QI Monitor — 22 questions test_qi_baseline.py.
	 *
	 * @return void
	 */
	private static function render_qi_monitor_tab() {
		$last    = get_option( 'avi_qi_monitor_last', array() );
		$last    = is_array( $last ) ? $last : array();
		$history = get_option( 'avi_qi_monitor_history', array() );
		$history = is_array( $history ) ? $history : array();
		$nonce   = wp_create_nonce( 'avision_cc_nonce' );

		$log_file  = '/root/avision-backend/logs/qi_history.log';
		$log_lines = array();
		if ( file_exists( $log_file ) && is_readable( $log_file ) ) {
			$all = file( $log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
			if ( is_array( $all ) ) {
				$log_lines = array_slice( array_reverse( $all ), 0, 14 );
			}
		}

		$score     = isset( $last['score_pct'] ) ? (int) $last['score_pct'] : null;
		$b2c_pass  = isset( $last['b2c_pass'] ) ? (int) $last['b2c_pass'] : null;
		$b2c_total = isset( $last['b2c_total'] ) && (int) $last['b2c_total'] > 0 ? (int) $last['b2c_total'] : 10;
		$b2b_pass  = isset( $last['b2b_pass'] ) ? (int) $last['b2b_pass'] : null;
		$b2b_total = isset( $last['b2b_total'] ) && (int) $last['b2b_total'] > 0 ? (int) $last['b2b_total'] : 12;

		$qi_color_class = static function ( $pct ) {
			if ( $pct === null ) {
				return '';
			}
			if ( $pct >= 90 ) {
				return 'green';
			}
			if ( $pct >= 75 ) {
				return 'orange';
			}
			return 'red';
		};

		$qi_color_hex = static function ( $pct ) {
			if ( $pct === null ) {
				return '#718096';
			}
			if ( $pct >= 90 ) {
				return '#38a169';
			}
			if ( $pct >= 75 ) {
				return '#dd6b20';
			}
			return '#e53e3e';
		};

		$b2c_pct = ( $b2c_pass !== null && $b2c_total > 0 ) ? (int) round( ( $b2c_pass / $b2c_total ) * 100 ) : null;
		$b2b_pct = ( $b2b_pass !== null && $b2b_total > 0 ) ? (int) round( ( $b2b_pass / $b2b_total ) * 100 ) : null;
		?>
		<style>
		.qi-wrap { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
		.qi-score-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin-bottom:24px; }
		.qi-score-card { background:#fff; border-radius:12px; padding:20px; text-align:center; box-shadow:0 2px 8px rgba(0,0,0,.08); }
		.qi-score-card .qi-score-val { font-size:36px; font-weight:800; }
		.qi-score-card .qi-score-label { font-size:12px; color:#718096; margin-top:4px; }
		.qi-score-card.green { border-top:4px solid #38a169; }
		.qi-score-card.orange { border-top:4px solid #dd6b20; }
		.qi-score-card.red { border-top:4px solid #e53e3e; }
		.qi-table { width:100%; border-collapse:collapse; font-size:13px; }
		.qi-table th { background:#1a2b5e; color:#fff; padding:10px 12px; text-align:left; }
		.qi-table td { padding:9px 12px; border-bottom:1px solid #edf2f7; }
		.qi-badge { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; font-weight:700; }
		.qi-badge-pass { background:#c6f6d5; color:#276749; }
		.qi-badge-fail { background:#fed7d7; color:#9b2c2c; }
		.qi-badge-b2c { background:#dbeafe; color:#1e40af; }
		.qi-badge-b2b { background:#d1fae5; color:#065f46; }
		.qi-log-line { font-family:monospace; font-size:12px; padding:4px 8px; border-radius:4px; }
		.qi-log-line.ok { background:#f0fff4; color:#276749; }
		.qi-log-line.alert { background:#fff5f5; color:#9b2c2c; }
		.qi-section { background:#fff; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 8px rgba(0,0,0,.08); }
		.qi-section h3 { margin:0 0 16px; font-size:15px; color:#1a2b5e; }
		.lqa-section { background:#fff; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 8px rgba(0,0,0,.08); }
		.lqa-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:12px; }
		.lqa-header h3 { margin:0; font-size:15px; color:#1a2b5e; }
		.lqa-header p { margin:4px 0 0; font-size:12px; color:#718096; }
		.lqa-run-btn { background:#7c3aed !important; border-color:#7c3aed !important; }
		.lqa-score-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:16px; }
		.lqa-score-card { background:#fff; border-radius:8px; padding:14px; text-align:center; }
		.lqa-score-val { font-size:28px; font-weight:800; }
		.lqa-score-label { font-size:11px; color:#718096; }
		.lqa-output { display:none; margin-top:16px; background:#1a202c; color:#e2e8f0; padding:16px; border-radius:8px; font-family:monospace; font-size:12px; max-height:300px; overflow-y:auto; white-space:pre-wrap; }
		.lqa-row-fail { background:#fff5f5 !important; }
		.lqa-model-short { font-size:11px; color:#5a67d8; }
		</style>

		<div class="qi-wrap">
			<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;flex-wrap:wrap;gap:12px;">
				<h2 style="margin:0;color:#1a2b5e;"><?php esc_html_e( '🧠 QI Monitor — AVI Chat Intelligence', 'avision-dashboard' ); ?></h2>
				<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
					<span style="font-size:12px;color:#718096;">
						<?php esc_html_e( '⏰ Auto 8h + 20h UTC', 'avision-dashboard' ); ?>
						<?php if ( ! empty( $last['timestamp'] ) ) : ?>
						— <?php esc_html_e( 'Dernier :', 'avision-dashboard' ); ?> <?php echo esc_html( (string) $last['timestamp'] ); ?>
						<?php endif; ?>
					</span>
					<button type="button" id="qi-run-btn" class="button button-primary">
						<?php esc_html_e( '▶ Lancer test 22Q', 'avision-dashboard' ); ?>
					</button>
				</div>
			</div>

			<div class="qi-score-grid">
				<div class="qi-score-card <?php echo esc_attr( $qi_color_class( $score ) ); ?>">
					<div class="qi-score-val" style="color:<?php echo esc_attr( $qi_color_hex( $score ) ); ?>">
						<?php echo $score !== null ? esc_html( (string) $score . '%' ) : '—'; ?>
					</div>
					<div class="qi-score-label"><?php esc_html_e( 'Score global (22Q)', 'avision-dashboard' ); ?></div>
				</div>
				<div class="qi-score-card <?php echo esc_attr( $qi_color_class( $b2c_pct ) ); ?>">
					<div class="qi-score-val" style="color:<?php echo esc_attr( $qi_color_hex( $b2c_pct ) ); ?>">
						<?php echo $b2c_pct !== null ? esc_html( (string) $b2c_pct . '%' ) : '—'; ?>
					</div>
					<div class="qi-score-label"><?php esc_html_e( 'B2C (10 questions)', 'avision-dashboard' ); ?></div>
				</div>
				<div class="qi-score-card <?php echo esc_attr( $qi_color_class( $b2b_pct ) ); ?>">
					<div class="qi-score-val" style="color:<?php echo esc_attr( $qi_color_hex( $b2b_pct ) ); ?>">
						<?php echo $b2b_pct !== null ? esc_html( (string) $b2b_pct . '%' ) : '—'; ?>
					</div>
					<div class="qi-score-label"><?php esc_html_e( 'B2B Fleet (12 questions)', 'avision-dashboard' ); ?></div>
				</div>
			</div>

			<?php if ( ! empty( $last['results'] ) && is_array( $last['results'] ) ) : ?>
			<div class="qi-section">
				<h3><?php esc_html_e( '📋 Détail des 22 questions — dernier run', 'avision-dashboard' ); ?></h3>
				<table class="qi-table">
					<thead><tr>
						<th>#</th>
						<th><?php esc_html_e( 'Type', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Question', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Score', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Statut', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Mots trouvés', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Manquants', 'avision-dashboard' ); ?></th>
					</tr></thead>
					<tbody>
					<?php
					foreach ( $last['results'] as $r ) :
						if ( ! is_array( $r ) ) {
							continue;
						}
						$r_status = isset( $r['status'] ) ? (string) $r['status'] : '';
						$r_score  = isset( $r['score'] ) ? (int) $r['score'] : 0;
						$r_type   = isset( $r['type'] ) ? (string) $r['type'] : 'b2c';
						$r_n      = isset( $r['n'] ) ? (int) $r['n'] : 0;
						?>
					<tr style="background:<?php echo $r_status === 'PASS' ? '#fff' : '#fff5f5'; ?>;">
						<td style="font-weight:700;color:#1a2b5e;"><?php echo esc_html( (string) $r_n ); ?></td>
						<td>
							<span class="qi-badge qi-badge-<?php echo esc_attr( $r_type ); ?>">
								<?php echo esc_html( strtoupper( $r_type ) ); ?>
							</span>
						</td>
						<td style="max-width:280px;"><?php echo esc_html( isset( $r['question'] ) ? (string) $r['question'] : '' ); ?></td>
						<td style="font-weight:700;color:<?php echo esc_attr( $r_score >= 75 ? '#38a169' : ( $r_score >= 50 ? '#dd6b20' : '#e53e3e' ) ); ?>">
							<?php echo esc_html( (string) $r_score ); ?>%
						</td>
						<td>
							<span class="qi-badge qi-badge-<?php echo $r_status === 'PASS' ? 'pass' : 'fail'; ?>">
								<?php echo $r_status === 'PASS' ? '✅ PASS' : '❌ FAIL'; ?>
							</span>
						</td>
						<td style="font-size:11px;color:#38a169;"><?php echo esc_html( isset( $r['found'] ) ? (string) $r['found'] : '' ); ?></td>
						<td style="font-size:11px;color:#e53e3e;"><?php echo esc_html( isset( $r['missing'] ) ? (string) $r['missing'] : '' ); ?></td>
					</tr>
					<?php endforeach; ?>
					</tbody>
				</table>
			</div>
			<?php endif; ?>

			<?php if ( ! empty( $history ) ) : ?>
			<div class="qi-section">
				<h3><?php esc_html_e( '📊 Historique runs (14 derniers)', 'avision-dashboard' ); ?></h3>
				<table class="qi-table">
					<thead><tr>
						<th><?php esc_html_e( 'Date', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Global', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'B2C', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'B2B', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Trigger', 'avision-dashboard' ); ?></th>
					</tr></thead>
					<tbody>
					<?php
					foreach ( $history as $h ) :
						if ( ! is_array( $h ) ) {
							continue;
						}
						$pct = isset( $h['score_pct'] ) ? (int) $h['score_pct'] : 0;
						$bg  = $pct >= 90 ? '#f0fff4' : ( $pct >= 75 ? '#fffbeb' : '#fff5f5' );
						?>
					<tr style="background:<?php echo esc_attr( $bg ); ?>;">
						<td><?php echo esc_html( isset( $h['timestamp'] ) ? (string) $h['timestamp'] : '' ); ?></td>
						<td style="font-weight:700;color:<?php echo esc_attr( $qi_color_hex( $pct ) ); ?>">
							<?php echo esc_html( (string) $pct ); ?>%
						</td>
						<td><?php echo esc_html( isset( $h['b2c'] ) ? (string) $h['b2c'] : '' ); ?></td>
						<td><?php echo esc_html( isset( $h['b2b'] ) ? (string) $h['b2b'] : '' ); ?></td>
						<td style="font-size:11px;color:#718096;"><?php echo esc_html( isset( $h['trigger'] ) ? (string) $h['trigger'] : '' ); ?></td>
					</tr>
					<?php endforeach; ?>
					</tbody>
				</table>
			</div>
			<?php endif; ?>

			<?php
			$lqa_last    = get_option( 'avi_lqa_last_result', array() );
			$lqa_last    = is_array( $lqa_last ) ? $lqa_last : array();
			$lqa_history = get_option( 'avi_lqa_history', array() );
			$lqa_history = is_array( $lqa_history ) ? $lqa_history : array();
			?>
			<div class="lqa-section qi-section" id="lqa-section">
				<div class="lqa-header">
					<div>
						<h3><?php esc_html_e( '🔴 Live Chat QA — Tunnel WordPress → FastAPI', 'avision-dashboard' ); ?></h3>
						<p>
							<?php esc_html_e( 'Teste le vrai tunnel de production (12 questions B2C+B2B) — vérifie RAG, orchestrateur, mots-clés', 'avision-dashboard' ); ?>
							<?php if ( ! empty( $lqa_last['timestamp'] ) ) : ?>
							— <?php esc_html_e( 'Dernier :', 'avision-dashboard' ); ?> <?php echo esc_html( (string) $lqa_last['timestamp'] ); ?>
							<?php endif; ?>
						</p>
					</div>
					<button type="button" id="lqa-run-btn" class="button button-primary lqa-run-btn">
						<?php esc_html_e( '🔴 Live QA (12Q)', 'avision-dashboard' ); ?>
					</button>
				</div>

				<?php if ( ! empty( $lqa_last['results'] ) && is_array( $lqa_last['results'] ) ) :
					$lqa_score     = isset( $lqa_last['score_pct'] ) ? (int) $lqa_last['score_pct'] : 0;
					$lqa_color     = $lqa_score >= 90 ? '#38a169' : ( $lqa_score >= 75 ? '#dd6b20' : '#e53e3e' );
					$lqa_fail_rag  = isset( $lqa_last['fail_rag'] ) ? (int) $lqa_last['fail_rag'] : 0;
					$lqa_fail_orch = isset( $lqa_last['fail_orch'] ) ? (int) $lqa_last['fail_orch'] : 0;
					$lqa_fail_kw   = isset( $lqa_last['fail_kw'] ) ? (int) $lqa_last['fail_kw'] : 0;
					?>
				<div class="lqa-score-grid">
					<div class="lqa-score-card" style="border-top:3px solid <?php echo esc_attr( $lqa_color ); ?>;">
						<div class="lqa-score-val" style="color:<?php echo esc_attr( $lqa_color ); ?>;"><?php echo esc_html( (string) $lqa_score ); ?>%</div>
						<div class="lqa-score-label"><?php esc_html_e( 'Score global', 'avision-dashboard' ); ?></div>
					</div>
					<div class="lqa-score-card" style="border-top:3px solid <?php echo esc_attr( $lqa_fail_rag === 0 ? '#38a169' : '#e53e3e' ); ?>;">
						<div class="lqa-score-val" style="color:<?php echo esc_attr( $lqa_fail_rag === 0 ? '#38a169' : '#e53e3e' ); ?>;">
							<?php echo $lqa_fail_rag === 0 ? '✅' : esc_html( (string) $lqa_fail_rag ); ?>
						</div>
						<div class="lqa-score-label"><?php esc_html_e( 'Sans RAG', 'avision-dashboard' ); ?></div>
					</div>
					<div class="lqa-score-card" style="border-top:3px solid <?php echo esc_attr( $lqa_fail_orch === 0 ? '#38a169' : '#e53e3e' ); ?>;">
						<div class="lqa-score-val" style="color:<?php echo esc_attr( $lqa_fail_orch === 0 ? '#38a169' : '#e53e3e' ); ?>;">
							<?php echo $lqa_fail_orch === 0 ? '✅' : esc_html( (string) $lqa_fail_orch ); ?>
						</div>
						<div class="lqa-score-label"><?php esc_html_e( 'Legacy path', 'avision-dashboard' ); ?></div>
					</div>
					<div class="lqa-score-card" style="border-top:3px solid <?php echo esc_attr( $lqa_fail_kw === 0 ? '#38a169' : '#e53e3e' ); ?>;">
						<div class="lqa-score-val" style="color:<?php echo esc_attr( $lqa_fail_kw === 0 ? '#38a169' : '#e53e3e' ); ?>;">
							<?php echo $lqa_fail_kw === 0 ? '✅' : esc_html( (string) $lqa_fail_kw ); ?>
						</div>
						<div class="lqa-score-label"><?php esc_html_e( 'Mots-clés KO', 'avision-dashboard' ); ?></div>
					</div>
				</div>

				<table class="qi-table">
					<thead><tr>
						<th><?php esc_html_e( 'Type', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Question', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Statut', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'KW Score', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'RAG', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Orchestrateur', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Modèle', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Guardian', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Radar', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Fiche nommée', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Non générique', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Mots manquants', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Réponse (extrait)', 'avision-dashboard' ); ?></th>
					</tr></thead>
					<tbody>
					<?php
					foreach ( $lqa_last['results'] as $r ) :
						if ( ! is_array( $r ) ) {
							continue;
						}
						$r_status     = isset( $r['status'] ) ? (string) $r['status'] : 'FAIL';
						$r_type       = isset( $r['type'] ) ? (string) $r['type'] : 'b2c';
						$r_kw         = isset( $r['kw_score'] ) ? (int) $r['kw_score'] : 0;
						$r_rag        = isset( $r['rag_used'] ) ? $r['rag_used'] : null;
						$r_orch       = isset( $r['orch_path'] ) ? (string) $r['orch_path'] : '';
						$r_model      = isset( $r['model_used'] ) ? (string) $r['model_used'] : '';
						$r_missing    = isset( $r['missing_kw'] ) && is_array( $r['missing_kw'] ) ? $r['missing_kw'] : array();
						$r_msg        = isset( $r['message'] ) ? (string) $r['message'] : '';
						$r_excerpt    = isset( $r['reply_excerpt'] ) ? (string) $r['reply_excerpt'] : '';
						$r_check_orch = ! empty( $r['check_orch'] );
						$r_chk_guard  = isset( $r['check_guardian'] ) ? (string) $r['check_guardian'] : 'n/a';
						$r_chk_radar  = isset( $r['check_radar'] ) ? (string) $r['check_radar'] : 'n/a';
						$r_chk_fiche  = isset( $r['check_fiche'] ) ? (string) $r['check_fiche'] : 'n/a';
						$r_chk_nogen  = isset( $r['check_not_generic'] ) ? (string) $r['check_not_generic'] : 'n/a';
						$model_parts  = $r_model !== '' ? explode( '/', $r_model ) : array();
						$model_short  = isset( $model_parts[1] ) ? $model_parts[1] : $r_model;
						?>
					<tr class="<?php echo $r_status === 'PASS' ? '' : 'lqa-row-fail'; ?>">
						<td><span class="qi-badge qi-badge-<?php echo esc_attr( $r_type ); ?>"><?php echo esc_html( strtoupper( $r_type ) ); ?></span></td>
						<td style="font-size:12px;max-width:200px;"><?php echo esc_html( function_exists( 'mb_substr' ) ? mb_substr( $r_msg, 0, 60 ) : substr( $r_msg, 0, 60 ) ); ?>…</td>
						<td><span class="qi-badge qi-badge-<?php echo $r_status === 'PASS' ? 'pass' : 'fail'; ?>"><?php echo $r_status === 'PASS' ? '✅ PASS' : '❌ FAIL'; ?></span></td>
						<td style="font-weight:700;color:<?php echo esc_attr( $r_kw >= 75 ? '#38a169' : ( $r_kw >= 50 ? '#dd6b20' : '#e53e3e' ) ); ?>;"><?php echo esc_html( (string) $r_kw ); ?>%</td>
						<td>
							<?php
							if ( (int) $r_rag === 1 || $r_rag === true ) {
								echo '<span style="color:#38a169">✅ RAG</span>';
							} elseif ( (int) $r_rag === 0 ) {
								echo '<span style="color:#e53e3e">❌ LLM</span>';
							} else {
								echo '<span style="color:#a0aec0">?</span>';
							}
							?>
						</td>
						<td style="font-size:11px;color:<?php echo esc_attr( $r_check_orch ? '#38a169' : '#e53e3e' ); ?>;"><?php echo esc_html( $r_orch !== '' ? $r_orch : '—' ); ?></td>
						<td class="lqa-model-short"><?php echo esc_html( $model_short !== '' ? $model_short : '—' ); ?></td>
						<?php
						foreach ( array( $r_chk_guard, $r_chk_radar, $r_chk_fiche, $r_chk_nogen ) as $r_b2b_chk ) :
							if ( $r_b2b_chk === 'n/a' ) :
								?>
						<td><span style="color:#a0aec0;">—</span></td>
								<?php
							elseif ( $r_b2b_chk === 'pass' ) :
								?>
						<td><span class="qi-badge qi-badge-pass">✅</span></td>
								<?php
							else :
								?>
						<td><span class="qi-badge qi-badge-fail">❌</span></td>
								<?php
							endif;
						endforeach;
						?>
						<td style="font-size:11px;color:#e53e3e;"><?php echo esc_html( implode( ', ', $r_missing ) ); ?></td>
						<td style="font-size:11px;color:#4a5568;max-width:200px;"><?php echo esc_html( $r_excerpt ); ?>…</td>
					</tr>
					<?php endforeach; ?>
					</tbody>
				</table>

				<?php if ( ! empty( $lqa_history ) ) : ?>
				<h4 style="font-size:13px;color:#4a5568;margin:16px 0 8px;"><?php esc_html_e( 'Historique Live QA', 'avision-dashboard' ); ?></h4>
				<table class="qi-table">
					<thead><tr>
						<th><?php esc_html_e( 'Date', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Score', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Pass', 'avision-dashboard' ); ?></th>
						<th><?php esc_html_e( 'Sans RAG', 'avision-dashboard' ); ?></th>
					</tr></thead>
					<tbody>
					<?php
					foreach ( $lqa_history as $h ) :
						if ( ! is_array( $h ) ) {
							continue;
						}
						$pct        = isset( $h['score_pct'] ) ? (int) $h['score_pct'] : 0;
						$bg         = $pct >= 90 ? '#f0fff4' : ( $pct >= 75 ? '#fffbeb' : '#fff5f5' );
						$h_fail_rag = isset( $h['fail_rag'] ) ? (int) $h['fail_rag'] : 0;
						?>
					<tr style="background:<?php echo esc_attr( $bg ); ?>;">
						<td><?php echo esc_html( isset( $h['timestamp'] ) ? (string) $h['timestamp'] : '' ); ?></td>
						<td style="font-weight:700;color:<?php echo esc_attr( $qi_color_hex( $pct ) ); ?>;"><?php echo esc_html( (string) $pct ); ?>%</td>
						<td><?php echo esc_html( ( isset( $h['pass'] ) ? (string) (int) $h['pass'] : '0' ) . '/' . ( isset( $h['total'] ) ? (string) (int) $h['total'] : '12' ) ); ?></td>
						<td style="color:<?php echo esc_attr( $h_fail_rag === 0 ? '#38a169' : '#e53e3e' ); ?>;"><?php echo esc_html( (string) $h_fail_rag ); ?></td>
					</tr>
					<?php endforeach; ?>
					</tbody>
				</table>
				<?php endif; ?>
				<?php endif; ?>

				<div id="lqa-output" class="lqa-output"></div>
			</div>

			<?php if ( ! empty( $log_lines ) ) : ?>
			<div class="qi-section">
				<h3><?php esc_html_e( '🕐 Log cron automatique (8h + 20h UTC)', 'avision-dashboard' ); ?></h3>
				<div style="display:flex;flex-direction:column;gap:4px;">
					<?php
					foreach ( $log_lines as $line ) :
						$is_alert = ( strpos( $line, 'ALERT' ) !== false || strpos( $line, 'FAIL' ) !== false );
						?>
					<div class="qi-log-line <?php echo $is_alert ? 'alert' : 'ok'; ?>">
						<?php echo esc_html( $line ); ?>
					</div>
					<?php endforeach; ?>
				</div>
				<p style="font-size:11px;color:#a0aec0;margin:8px 0 0;">
					<?php esc_html_e( 'Alerte auto si QI < 90% → toniosbh@gmail.com + contact@avision.org', 'avision-dashboard' ); ?>
				</p>
			</div>
			<?php endif; ?>

			<div id="qi-run-output" style="display:none;background:#1a202c;color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:12px;max-height:400px;overflow-y:auto;white-space:pre-wrap;margin-top:16px;"></div>
		</div>

		<script>
		(function() {
			var btn = document.getElementById('qi-run-btn');
			var out = document.getElementById('qi-run-output');
			if (!btn || !out) return;
			var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
			var nonce = <?php echo wp_json_encode( $nonce ); ?>;
			btn.addEventListener('click', function() {
				btn.disabled = true;
				btn.textContent = '⏳ Test en cours (~30s)...';
				out.style.display = 'block';
				out.textContent = 'Lancement test QI 22 questions B2C + B2B...\n';
				var fd = new FormData();
				fd.append('action', 'avision_qi_run_full');
				fd.append('nonce', nonce);
				fetch(ajaxurl, {method:'POST', body:fd, credentials:'same-origin'})
					.then(function(r){ return r.json(); })
					.then(function(j) {
						if (j && j.success && j.data) {
							var d = j.data;
							out.textContent = 'Score global : ' + d.score_pct + '%\n'
								+ 'B2C : ' + d.b2c_pass + '/' + d.b2c_total + '\n'
								+ 'B2B : ' + d.b2b_pass + '/' + d.b2b_total + '\n\n';
							if (d.results && d.results.length) {
								d.results.forEach(function(r) {
									var q = r.question ? String(r.question).substring(0, 60) : '';
									out.textContent += '[' + r.status + '] Q' + r.n + ' (' + String(r.type).toUpperCase() + ') '
										+ q + '... | score=' + r.score + '%'
										+ (r.missing ? ' | missing=[' + r.missing + ']' : '') + '\n';
								});
							}
							setTimeout(function(){ location.reload(); }, 2000);
						} else {
							out.textContent = 'Erreur AJAX';
						}
						btn.disabled = false;
						btn.textContent = '▶ Lancer test 22Q';
					})
					.catch(function() {
						out.textContent = 'Erreur réseau';
						btn.disabled = false;
						btn.textContent = '▶ Lancer test 22Q';
					});
			});
		})();
		(function() {
			var btn = document.getElementById('lqa-run-btn');
			var out = document.getElementById('lqa-output');
			if (!btn || !out) return;
			var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
			var nonce = <?php echo wp_json_encode( $nonce ); ?>;
			btn.addEventListener('click', function() {
				btn.disabled = true;
				btn.textContent = '⏳ Live QA en cours (~60s)...';
				out.style.display = 'block';
				out.textContent = 'Test tunnel WordPress → FastAPI sur 12 questions...\n';
				var fd = new FormData();
				fd.append('action', 'avision_lqa_run');
				fd.append('nonce', nonce);
				fetch(ajaxurl, {method:'POST', body:fd, credentials:'same-origin'})
					.then(function(r){ return r.json(); })
					.then(function(j) {
						if (j && j.success && j.data) {
							var d = j.data;
							out.textContent = 'Score : ' + d.score_pct + '% (' + d.pass + '/' + d.total + ')\n'
								+ 'RAG KO : ' + d.fail_rag + ' | Orch legacy : ' + d.fail_orch + ' | KW KO : ' + d.fail_kw + '\n\n';
							if (d.results && d.results.length) {
								d.results.forEach(function(r) {
									var msg = r.message ? String(r.message).substring(0, 50) : '';
									out.textContent += '[' + r.status + '] ' + String(r.type).toUpperCase()
										+ ' | ' + msg + '...'
										+ ' | kw=' + r.kw_score + '%'
										+ ' | rag=' + (r.rag_used === 1 ? '✅' : '❌')
										+ ' | path=' + (r.orch_path || '?')
										+ (r.missing_kw && r.missing_kw.length ? ' | missing=[' + r.missing_kw.join(',') + ']' : '')
										+ '\n';
								});
							}
							setTimeout(function(){ location.reload(); }, 3000);
						} else {
							out.textContent = 'Erreur AJAX : ' + JSON.stringify(j);
						}
						btn.disabled = false;
						btn.textContent = '🔴 Live QA (12Q)';
					})
					.catch(function(e) {
						out.textContent = 'Erreur réseau : ' + e;
						btn.disabled = false;
						btn.textContent = '🔴 Live QA (12Q)';
					});
			});
		})();
		</script>
		<?php
	}
	/* =========================================================================
	 * 🛡️ CODE GUARD TAB
	 * ========================================================================= */

	private static $code_guard_health_script   = '/root/scripts/check_code_health.sh';
	private static $code_guard_rollback_script  = '/root/scripts/rollback_file.sh';
	private static $code_guard_backup_dir       = '/root/backups/critical-files/';
	private static $code_guard_health_log_path  = '/root/backups/code_health.log';

	/**
	 * Fichiers rollback autorisés (N1/N2) — intersection avec le backup réel.
	 *
	 * @return array<int, string>
	 */
	private static function code_guard_rollback_whitelist_filenames() {
		return array(
			'page-premium-dashboard.php',
			'page-agency-dashboard.php',
			'functions.php',
			'class-avision-api.php',
			'class-ajax-router.php',
			'class-avision-stripe-webhook.php',
			'class-avision-dashboard-route-guard.php',
			'avi-stripe-s10b.php',
			'proxy-avi.php',
		);
	}

	/**
	 * Crontab filtré (root) : crontab -u root, puis fallback fichier spool si vide.
	 *
	 * @return string
	 */
	private static function code_guard_get_filtered_crontab_text() {
		$raw = shell_exec( '/usr/bin/sudo -n /root/scripts/read_crontab_www.sh 2>/dev/null' );
		if ( ! is_string( $raw ) || trim( $raw ) === '' ) {
			return '';
		}
		return trim( $raw );
	}

	/**
	 * @param string $backup_date
	 * @return array<string, mixed>
	 */
	private static function code_guard_load_manifest( $backup_date ) {
		$backup_date = sanitize_file_name( (string) $backup_date );
		if ( $backup_date === '' ) {
			return array();
		}
		$manifest_path = rtrim( self::$code_guard_backup_dir, '/' ) . '/' . $backup_date . '/manifest.json';
		if ( ! is_readable( $manifest_path ) ) {
			return array();
		}
		$manifest = json_decode( (string) @file_get_contents( $manifest_path ), true );
		return is_array( $manifest ) ? $manifest : array();
	}

	/**
	 * @param array<string, mixed> $manifest
	 * @return array{trigger:string,sprint:string,file_count:int,modified_count:int}
	 */
	private static function code_guard_manifest_meta( $manifest ) {
		$trigger = '';
		if ( isset( $manifest['trigger'] ) ) {
			$trigger = (string) $manifest['trigger'];
		} elseif ( isset( $manifest['backup_trigger'] ) ) {
			$trigger = (string) $manifest['backup_trigger'];
		}
		$sprint = '';
		if ( isset( $manifest['sprint'] ) ) {
			$sprint = (string) $manifest['sprint'];
		} elseif ( isset( $manifest['context'] ) ) {
			$sprint = (string) $manifest['context'];
		}
		$file_count = 0;
		if ( isset( $manifest['file_count'] ) ) {
			$file_count = (int) $manifest['file_count'];
		} elseif ( isset( $manifest['total_files'] ) ) {
			$file_count = (int) $manifest['total_files'];
		} elseif ( isset( $manifest['files'] ) && is_array( $manifest['files'] ) ) {
			$file_count = count( $manifest['files'] );
		}
		$modified_count = 0;
		if ( isset( $manifest['modified_count'] ) ) {
			$modified_count = (int) $manifest['modified_count'];
		} elseif ( isset( $manifest['modified_since_previous'] ) ) {
			$modified_count = (int) $manifest['modified_since_previous'];
		} elseif ( isset( $manifest['changed_count'] ) ) {
			$modified_count = (int) $manifest['changed_count'];
		} elseif ( isset( $manifest['files'] ) && is_array( $manifest['files'] ) ) {
			foreach ( $manifest['files'] as $f ) {
				if ( ! is_array( $f ) ) {
					continue;
				}
				if ( ! empty( $f['modified'] ) || ! empty( $f['changed'] ) ) {
					$modified_count++;
				}
			}
		}
		return array(
			'trigger'        => $trigger,
			'sprint'         => $sprint,
			'file_count'     => $file_count,
			'modified_count' => $modified_count,
		);
	}

	/**
	 * @param string               $backup_date
	 * @param array<string, mixed> $manifest
	 * @return string
	 */
	private static function code_guard_manifest_dropdown_label( $backup_date, $manifest ) {
		$meta = self::code_guard_manifest_meta( $manifest );
		$parts = array( $backup_date );
		if ( $meta['trigger'] !== '' ) {
			$parts[] = $meta['trigger'];
		}
		if ( $meta['sprint'] !== '' ) {
			$parts[] = $meta['sprint'];
		}
		$fc = $meta['file_count'] > 0 ? $meta['file_count'] : 0;
		$parts[] = $fc . ' fichiers';
		$mod_part = $meta['modified_count'] . ' modifiés';
		if ( $meta['modified_count'] > 0 ) {
			$mod_part .= ' ⚠️';
		}
		$parts[] = $mod_part;
		return implode( ' — ', $parts );
	}

	/**
	 * @param array<string, mixed> $manifest
	 * @return array<int, array<string, string>>
	 */
	private static function code_guard_manifest_changed_files( $manifest ) {
		$out = array();
		if ( ! isset( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
			return $out;
		}
		foreach ( $manifest['files'] as $f ) {
			if ( ! is_array( $f ) ) {
				continue;
			}
			$name = '';
			if ( isset( $f['name'] ) ) {
				$name = (string) $f['name'];
			} elseif ( isset( $f['file'] ) ) {
				$name = (string) $f['file'];
			} elseif ( isset( $f['filename'] ) ) {
				$name = (string) $f['filename'];
			}
			$is_mod = ! empty( $f['modified'] ) || ! empty( $f['changed'] );
			$sha_cur = isset( $f['sha_current'] ) ? (string) $f['sha_current'] : '';
			$sha_bak = isset( $f['sha_backup'] ) ? (string) $f['sha_backup'] : '';
			if ( $sha_cur !== '' && $sha_bak !== '' && $sha_cur !== $sha_bak ) {
				$is_mod = true;
			}
			if ( ! $is_mod ) {
				continue;
			}
			$sha = '';
			if ( isset( $f['sha256'] ) ) {
				$sha = (string) $f['sha256'];
			} elseif ( isset( $f['sha'] ) ) {
				$sha = (string) $f['sha'];
			} elseif ( $sha_cur !== '' ) {
				$sha = $sha_cur;
			}
			$diff = '';
			if ( isset( $f['diff'] ) ) {
				$diff = (string) $f['diff'];
			} elseif ( isset( $f['diff_summary'] ) ) {
				$diff = (string) $f['diff_summary'];
			} elseif ( isset( $f['diff_plus'], $f['diff_minus'] ) ) {
				$diff = '+' . (int) $f['diff_plus'] . ' / -' . (int) $f['diff_minus'];
			}
			$out[] = array(
				'name' => $name,
				'sha'  => $sha,
				'diff' => $diff,
			);
		}
		return $out;
	}

	/**
	 * @param array<string, mixed> $manifest
	 * @param string               $filename
	 * @return array<string, mixed>
	 */
	private static function code_guard_manifest_file_entry( $manifest, $filename ) {
		if ( ! isset( $manifest['files'] ) || ! is_array( $manifest['files'] ) ) {
			return array();
		}
		foreach ( $manifest['files'] as $f ) {
			if ( ! is_array( $f ) ) {
				continue;
			}
			$name = '';
			if ( isset( $f['name'] ) ) {
				$name = (string) $f['name'];
			} elseif ( isset( $f['file'] ) ) {
				$name = (string) $f['file'];
			} elseif ( isset( $f['filename'] ) ) {
				$name = (string) $f['filename'];
			}
			if ( $name === $filename ) {
				return $f;
			}
		}
		return array();
	}

	/**
	 * @return array<int, array{name:string,manifest:array<string,mixed>}>
	 */
	private static function code_guard_get_backup_entries() {
		$backup_dir = rtrim( self::$code_guard_backup_dir, '/' );
		$entries    = array();
		if ( ! is_dir( $backup_dir ) ) {
			return $entries;
		}
		$dirs = glob( $backup_dir . '/*', GLOB_ONLYDIR );
		if ( ! is_array( $dirs ) ) {
			return $entries;
		}
		foreach ( $dirs as $dpath ) {
			$dname    = basename( $dpath );
			$manifest = self::code_guard_load_manifest( $dname );
			$entries[] = array(
				'name'     => $dname,
				'manifest' => $manifest,
			);
		}
		usort(
			$entries,
			function( $a, $b ) {
				return strcmp( $b['name'], $a['name'] );
			}
		);
		return $entries;
	}

	/**
	 * @return string
	 */
	private static function code_guard_latest_backup_date() {
		$entries = self::code_guard_get_backup_entries();
		if ( empty( $entries ) ) {
			return '';
		}
		return (string) $entries[0]['name'];
	}

	/**
	 * @param string $filename
	 * @return string
	 */
	private static function code_guard_prod_path_for_file( $filename ) {
		$theme   = get_template_directory();
		$plugin  = defined( 'AVISION_PATH' ) ? trailingslashit( AVISION_PATH ) : trailingslashit( dirname( dirname( __DIR__ ) ) );
		$map     = array(
			'page-premium-dashboard.php'           => $theme . '/page-premium-dashboard.php',
			'page-agency-dashboard.php'            => $theme . '/page-agency-dashboard.php',
			'functions.php'                        => $theme . '/functions.php',
			'class-avision-api.php'                => $plugin . 'includes/class-avision-api.php',
			'class-ajax-router.php'                => $plugin . 'includes/class-ajax-router.php',
			'class-avision-stripe-webhook.php'       => $plugin . 'includes/class-avision-stripe-webhook.php',
			'class-avision-dashboard-route-guard.php' => $plugin . 'includes/class-avision-dashboard-route-guard.php',
			'avi-stripe-s10b.php'                  => $plugin . 'includes/avi-stripe-s10b.php',
			'proxy-avi.php'                        => ABSPATH . 'proxy-avi.php',
			'legal-fr.php'                         => $plugin . 'templates/pages/legal-fr.php',
			'legal-es.php'                         => $plugin . 'templates/pages/legal-es.php',
			'partners.php'                         => $plugin . 'templates/pages/partners.php',
		);
		return isset( $map[ $filename ] ) ? $map[ $filename ] : '';
	}

	/**
	 * @param string $path
	 * @return int
	 */
	private static function code_guard_count_file_lines( $path ) {
		if ( ! is_readable( $path ) ) {
			return 0;
		}
		$n = 0;
		$fh = @fopen( $path, 'rb' );
		if ( ! $fh ) {
			return 0;
		}
		while ( ! feof( $fh ) ) {
			$chunk = fread( $fh, 8192 );
			if ( $chunk === false ) {
				break;
			}
			$n += substr_count( $chunk, "\n" );
		}
		fclose( $fh );
		return $n + 1;
	}

	/**
	 * @return string
	 */
	private static function code_guard_read_last_hourly_check() {
		$path = self::$code_guard_health_log_path;
		if ( ! is_readable( $path ) ) {
			return '';
		}
		$lines = @file( $path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
		if ( ! is_array( $lines ) || empty( $lines ) ) {
			return '';
		}
		return trim( (string) end( $lines ) );
	}

	/**
	 * @return void
	 */
	private static function render_code_guard_tab() {
		if ( class_exists( 'Avision_Code_Guard_Center' ) ) {
			Avision_Code_Guard_Center::render_tab();
		} else {
			echo '<div class="notice notice-error"><p>' . esc_html__( 'Avision_Code_Guard_Center non chargé.', 'avision-dashboard' ) . '</p></div>';
		}
	}

	/**
	 * Onglet Security — scan plugins WordPress (backdoors / plugins masqués).
	 *
	 * @return void
	 */
	private static function render_security_tab() {
		$nonce = wp_create_nonce( 'avision_cc_nonce' );

		$last = class_exists( 'Avision_Plugin_Security_Scan' )
			? Avision_Plugin_Security_Scan::get_last_result()
			: get_option( 'avision_security_scan_last', null );
		if ( ! is_array( $last ) ) {
			$last = array();
		}

		$history = get_option( 'avision_security_scan_history', array() );
		$history = is_array( $history ) ? $history : array();

		$alerts_enabled = (bool) get_option( 'avi_security_alerts_enabled', 1 );
		$alert_email    = get_option( 'avi_security_alert_email', 'contact@avision.org' );
		$alert_email    = is_string( $alert_email ) ? $alert_email : 'contact@avision.org';

		$whitelist_stored = get_option( 'avi_security_scan_whitelist_plugins', null );
		$whitelist_default = array(
			'avision-dashboard',
			'litespeed-cache',
			'wp-mail-smtp',
			'wpforms-lite',
			'contact-form-7',
		);
		if ( is_array( $whitelist_stored ) && ! empty( $whitelist_stored ) ) {
			$whitelist_lines = implode( "\n", array_map( 'strval', $whitelist_stored ) );
		} else {
			$whitelist_lines = implode( "\n", $whitelist_default );
		}

		$score          = isset( $last['score'] ) ? (int) $last['score'] : null;
		$status         = isset( $last['status'] ) ? (string) $last['status'] : '';
		$ts             = isset( $last['timestamp'] ) ? (int) $last['timestamp'] : 0;
		$findings       = isset( $last['findings'] ) && is_array( $last['findings'] ) ? $last['findings'] : array();
		$hidden_plugins = isset( $last['hidden_plugins'] ) && is_array( $last['hidden_plugins'] ) ? $last['hidden_plugins'] : array();

		$critical_n = 0;
		$warn_n     = 0;
		foreach ( $findings as $f ) {
			if ( ! is_array( $f ) ) {
				continue;
			}
			$sev = isset( $f['severity'] ) ? (string) $f['severity'] : '';
			if ( $sev === 'critical' ) {
				$critical_n++;
			} elseif ( $sev === 'warn' ) {
				$warn_n++;
			}
		}

		$gauge_color = '#16a34a';
		if ( $score !== null && $score < 75 ) {
			$gauge_color = '#dc2626';
		} elseif ( $score !== null && $score < 100 ) {
			$gauge_color = '#d97706';
		}

		$scan_next = wp_next_scheduled( 'avision_security_plugin_scan_cron' );
		?>
		<div class="avi-security-tab">
		<style>
		.avi-security-tab{--avi-navy:#1a1a2e;--avi-violet:#6366f1;--avi-amber:#d97706;}
		.avi-security-tab .sec-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:24px;box-shadow:0 2px 8px rgba(26,26,46,.06)}
		.avi-security-tab .sec-card h3{margin-top:0;color:var(--avi-navy);font-size:16px}
		.avi-security-tab .sec-muted{color:#64748b;font-size:13px}
		.avi-security-tab .sec-gauge-wrap{margin:16px 0}
		.avi-security-tab .sec-gauge-bar{height:14px;background:#e2e8f0;border-radius:8px;overflow:hidden;max-width:420px}
		.avi-security-tab .sec-gauge-fill{height:100%;border-radius:8px;transition:width .3s}
		.avi-security-tab .sec-score-big{font-size:32px;font-weight:700;color:var(--avi-navy)}
		.avi-security-tab .sec-btn{padding:8px 18px;border-radius:8px;border:1px solid var(--avi-violet);background:var(--avi-violet);color:#fff;cursor:pointer;font-size:13px;font-weight:600;margin-right:8px}
		.avi-security-tab .sec-btn:hover{opacity:.92}
		.avi-security-tab .sec-btn:disabled{opacity:.5;cursor:not-allowed}
		.avi-security-tab .sec-btn-amber{border-color:var(--avi-amber);background:var(--avi-amber)}
		.avi-security-tab .sec-table{width:100%;border-collapse:collapse;font-size:13px;margin-top:12px}
		.avi-security-tab .sec-table th,.avi-security-tab .sec-table td{text-align:left;padding:8px 10px;border-bottom:1px solid #f0f0f0;vertical-align:top}
		.avi-security-tab .sec-table th{background:#f8fafc;color:#475569;font-weight:600}
		.avi-security-tab .sec-sev-critical{color:#dc2626;font-weight:700}
		.avi-security-tab .sec-sev-warn{color:#d97706;font-weight:700}
		.avi-security-tab .sec-badge{display:inline-block;padding:6px 12px;border-radius:8px;font-size:13px;font-weight:600}
		.avi-security-tab .sec-badge-ok{background:#dcfce7;color:#166534}
		.avi-security-tab .sec-badge-err{background:#fee2e2;color:#dc2626}
		.avi-security-tab .sec-badge-warn{background:#fef3c7;color:#92400e}
		.avi-security-tab .sec-field{margin-bottom:12px}
		.avi-security-tab .sec-field label{display:block;font-size:12px;color:#64748b;margin-bottom:4px}
		.avi-security-tab .sec-field input[type="email"],.avi-security-tab .sec-field textarea{width:100%;max-width:480px;padding:8px 10px;border:1px solid #cbd5e1;border-radius:8px;font-size:13px}
		.avi-security-tab #sec-msg{margin-top:10px;font-size:13px}
		</style>

		<!-- A) Dernier scan -->
		<div class="sec-card">
			<h3><?php esc_html_e( '🔍 Dernier scan', 'avision-dashboard' ); ?></h3>
			<p class="sec-muted">
				<?php esc_html_e( 'Scan des plugins WordPress (WP_PLUGIN_DIR) — détection backdoors et plugins masqués.', 'avision-dashboard' ); ?>
				<?php
				if ( $scan_next ) {
					echo ' — ' . esc_html__( 'Cron daily : prochain', 'avision-dashboard' ) . ' ' . esc_html( gmdate( 'Y-m-d H:i', $scan_next ) ) . ' UTC';
				}
				?>
			</p>
			<?php if ( $ts > 0 ) : ?>
				<p class="sec-muted"><?php esc_html_e( 'Dernière exécution :', 'avision-dashboard' ); ?> <strong><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $ts ) ); ?> UTC</strong>
					<?php if ( $status !== '' ) : ?>
						— <?php esc_html_e( 'Statut', 'avision-dashboard' ); ?>: <code><?php echo esc_html( $status ); ?></code>
					<?php endif; ?>
				</p>
			<?php else : ?>
				<p class="sec-muted"><?php esc_html_e( 'Aucun scan enregistré.', 'avision-dashboard' ); ?></p>
			<?php endif; ?>

			<?php if ( $score !== null ) : ?>
			<div class="sec-gauge-wrap">
				<div class="sec-score-big" id="sec-score-display"><?php echo esc_html( (string) $score ); ?>/100</div>
				<div class="sec-gauge-bar"><div class="sec-gauge-fill" id="sec-gauge-fill" style="width:<?php echo esc_attr( (string) max( 0, min( 100, $score ) ) ); ?>%;background:<?php echo esc_attr( $gauge_color ); ?>"></div></div>
				<p class="sec-muted" style="margin-top:8px">
					<span class="sec-sev-critical"><?php echo esc_html( (string) $critical_n ); ?> critical</span>
					· <span class="sec-sev-warn"><?php echo esc_html( (string) $warn_n ); ?> warn</span>
				</p>
			</div>
			<?php endif; ?>

			<button type="button" class="sec-btn" id="sec-run-scan"><?php esc_html_e( '🔍 Run Scan Now', 'avision-dashboard' ); ?></button>
			<button type="button" class="sec-btn sec-btn-amber" id="sec-test-alert"><?php esc_html_e( '📧 Test Alert Email', 'avision-dashboard' ); ?></button>
			<span id="sec-scan-spinner" style="display:none;margin-left:8px">⏳</span>
			<div id="sec-msg"></div>
		</div>

		<!-- B) Findings -->
		<div class="sec-card">
			<h3><?php esc_html_e( 'Findings', 'avision-dashboard' ); ?></h3>
			<?php if ( empty( $findings ) ) : ?>
				<span class="sec-badge sec-badge-ok"><?php esc_html_e( '✅ No threats detected', 'avision-dashboard' ); ?></span>
			<?php else : ?>
			<table class="sec-table" id="sec-findings-table">
				<thead><tr>
					<th><?php esc_html_e( 'Plugin', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Fichier', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Ligne', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Pattern', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Sévérité', 'avision-dashboard' ); ?></th>
				</tr></thead>
				<tbody id="sec-findings-body">
				<?php foreach ( $findings as $f ) :
					if ( ! is_array( $f ) ) {
						continue;
					}
					$sev = isset( $f['severity'] ) ? (string) $f['severity'] : 'warn';
					$cls = ( $sev === 'critical' ) ? 'sec-sev-critical' : 'sec-sev-warn';
					?>
				<tr>
					<td><?php echo esc_html( isset( $f['plugin'] ) ? (string) $f['plugin'] : '' ); ?></td>
					<td><code><?php echo esc_html( isset( $f['file'] ) ? (string) $f['file'] : '' ); ?></code></td>
					<td><?php echo isset( $f['line'] ) && (int) $f['line'] > 0 ? esc_html( (string) (int) $f['line'] ) : '—'; ?></td>
					<td><?php echo esc_html( isset( $f['pattern'] ) ? (string) $f['pattern'] : '' ); ?></td>
					<td class="<?php echo esc_attr( $cls ); ?>"><?php echo esc_html( strtoupper( $sev ) ); ?></td>
				</tr>
				<?php endforeach; ?>
				</tbody>
			</table>
			<?php endif; ?>
		</div>

		<!-- C) Plugins cachés -->
		<div class="sec-card">
			<h3><?php esc_html_e( 'Plugins cachés', 'avision-dashboard' ); ?></h3>
			<?php if ( ! empty( $hidden_plugins ) ) : ?>
				<span class="sec-badge sec-badge-err"><?php esc_html_e( 'Hidden plugins detected', 'avision-dashboard' ); ?></span>
				<ul style="margin-top:12px" id="sec-hidden-list">
					<?php foreach ( $hidden_plugins as $hp ) : ?>
						<li><code><?php echo esc_html( (string) $hp ); ?></code></li>
					<?php endforeach; ?>
				</ul>
			<?php else : ?>
				<span class="sec-badge sec-badge-ok" id="sec-hidden-badge"><?php esc_html_e( '✅ All plugins visible in admin', 'avision-dashboard' ); ?></span>
			<?php endif; ?>
		</div>

		<!-- D) Paramètres -->
		<div class="sec-card">
			<h3><?php esc_html_e( 'Paramètres', 'avision-dashboard' ); ?></h3>
			<div class="sec-field">
				<label>
					<input type="checkbox" id="sec-alerts-enabled" value="1" <?php checked( $alerts_enabled ); ?> />
					<?php esc_html_e( 'Enable automatic daily scan alerts', 'avision-dashboard' ); ?>
				</label>
			</div>
			<div class="sec-field">
				<label for="sec-alert-email"><?php esc_html_e( 'Email destinataire alertes', 'avision-dashboard' ); ?></label>
				<input type="email" id="sec-alert-email" value="<?php echo esc_attr( $alert_email ); ?>" />
			</div>
			<div class="sec-field">
				<label for="sec-whitelist"><?php esc_html_e( 'Whitelist plugins (un slug par ligne — critical réduit en warn)', 'avision-dashboard' ); ?></label>
				<textarea id="sec-whitelist" rows="6"><?php echo esc_textarea( $whitelist_lines ); ?></textarea>
			</div>
			<button type="button" class="sec-btn" id="sec-save-alerts"><?php esc_html_e( 'Save alert settings', 'avision-dashboard' ); ?></button>
			<button type="button" class="sec-btn sec-btn-amber" id="sec-save-settings"><?php esc_html_e( 'Save Settings', 'avision-dashboard' ); ?></button>
			<div id="sec-settings-msg" class="sec-muted" style="margin-top:8px"></div>
		</div>

		<!-- E) Historique -->
		<div class="sec-card">
			<h3><?php esc_html_e( 'Historique (10 derniers scans)', 'avision-dashboard' ); ?></h3>
			<?php if ( empty( $history ) ) : ?>
				<p class="sec-muted"><?php esc_html_e( 'Aucun historique.', 'avision-dashboard' ); ?></p>
			<?php else : ?>
			<table class="sec-table" id="sec-history-table">
				<thead><tr>
					<th><?php esc_html_e( 'Date', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Score', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Critical', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Warn', 'avision-dashboard' ); ?></th>
					<th><?php esc_html_e( 'Hidden', 'avision-dashboard' ); ?></th>
				</tr></thead>
				<tbody>
				<?php foreach ( $history as $h ) :
					if ( ! is_array( $h ) ) {
						continue;
					}
					$h_ts   = isset( $h['timestamp'] ) ? (int) $h['timestamp'] : 0;
					$h_crit = isset( $h['critical_count'] ) ? (int) $h['critical_count'] : 0;
					$h_fc   = isset( $h['findings_count'] ) ? (int) $h['findings_count'] : 0;
					$h_warn = isset( $h['warn_count'] ) ? (int) $h['warn_count'] : max( 0, $h_fc - $h_crit );
					$h_hid  = isset( $h['hidden_count'] ) ? (int) $h['hidden_count'] : 0;
					?>
				<tr>
					<td><?php echo $h_ts > 0 ? esc_html( gmdate( 'Y-m-d H:i', $h_ts ) ) : '—'; ?></td>
					<td><strong><?php echo isset( $h['score'] ) ? esc_html( (string) (int) $h['score'] ) : '—'; ?></strong></td>
					<td class="sec-sev-critical"><?php echo esc_html( (string) $h_crit ); ?></td>
					<td class="sec-sev-warn"><?php echo esc_html( (string) $h_warn ); ?></td>
					<td><?php echo esc_html( (string) $h_hid ); ?></td>
				</tr>
				<?php endforeach; ?>
				</tbody>
			</table>
			<?php endif; ?>
		</div>
		</div><!-- .avi-security-tab -->

		<script>
		(function(){
			var nonce = <?php echo wp_json_encode( $nonce ); ?>;
			function secMsg(el, text, ok){
				if(!el) return;
				el.textContent = text;
				el.style.color = ok ? '#16a34a' : '#dc2626';
			}
			function secUpdateUi(data){
				if(!data) return;
				var score = typeof data.score === 'number' ? data.score : parseInt(data.score, 10);
				if(isNaN(score)) score = 0;
				var disp = document.getElementById('sec-score-display');
				var fill = document.getElementById('sec-gauge-fill');
				if(disp) disp.textContent = score + '/100';
				if(fill){
					fill.style.width = Math.max(0, Math.min(100, score)) + '%';
					fill.style.background = score < 75 ? '#dc2626' : (score < 100 ? '#d97706' : '#16a34a');
				}
				var findings = data.findings || [];
				var crit = 0, warn = 0;
				findings.forEach(function(f){
					if(f.severity === 'critical') crit++;
					else if(f.severity === 'warn') warn++;
				});
				var findingsCard = document.getElementById('sec-findings-table');
				var tbody = document.getElementById('sec-findings-body');
				if(findings.length === 0){
					if(findingsCard) findingsCard.style.display = 'none';
				} else if(tbody){
					tbody.innerHTML = '';
					findings.forEach(function(f){
						var tr = document.createElement('tr');
						var sev = (f.severity || 'warn').toUpperCase();
						var cls = f.severity === 'critical' ? 'sec-sev-critical' : 'sec-sev-warn';
						tr.innerHTML = '<td>' + (f.plugin||'') + '</td><td><code>' + (f.file||'') + '</code></td><td>' + (f.line > 0 ? f.line : '—') + '</td><td>' + (f.pattern||'') + '</td><td class="' + cls + '">' + sev + '</td>';
						tbody.appendChild(tr);
					});
					if(findingsCard) findingsCard.style.display = '';
				}
				var hidden = data.hidden_plugins || [];
				var hidBadge = document.getElementById('sec-hidden-badge');
				var hidList = document.getElementById('sec-hidden-list');
				if(hidden.length > 0){
					if(hidBadge) hidBadge.style.display = 'none';
				}
			}
			var runBtn = document.getElementById('sec-run-scan');
			if(runBtn){
				runBtn.addEventListener('click', function(){
					runBtn.disabled = true;
					document.getElementById('sec-scan-spinner').style.display = 'inline';
					var fd = new FormData();
					fd.append('action', 'avision_security_run_scan');
					fd.append('nonce', nonce);
					fetch(ajaxurl, {method:'POST', body:fd})
						.then(function(r){ return r.json(); })
						.then(function(d){
							runBtn.disabled = false;
							document.getElementById('sec-scan-spinner').style.display = 'none';
							if(d.success && d.data){
								secMsg(document.getElementById('sec-msg'), 'Scan terminé — score ' + d.data.score + '/100', true);
								secUpdateUi(d.data);
								setTimeout(function(){ location.reload(); }, 1200);
							} else {
								secMsg(document.getElementById('sec-msg'), d.data && d.data.message ? d.data.message : 'Scan failed', false);
							}
						})
						.catch(function(e){
							runBtn.disabled = false;
							document.getElementById('sec-scan-spinner').style.display = 'none';
							secMsg(document.getElementById('sec-msg'), 'Network error: ' + e.message, false);
						});
				});
			}
			var testBtn = document.getElementById('sec-test-alert');
			if(testBtn){
				testBtn.addEventListener('click', function(){
					testBtn.disabled = true;
					var fd = new FormData();
					fd.append('action', 'avision_security_test_alert');
					fd.append('nonce', nonce);
					fetch(ajaxurl, {method:'POST', body:fd})
						.then(function(r){ return r.json(); })
						.then(function(d){
							testBtn.disabled = false;
							var ok = d.success && d.data && d.data.sent;
							secMsg(document.getElementById('sec-msg'), ok ? 'Test email sent.' : (d.data && d.data.message ? d.data.message : 'Test failed'), ok);
						})
						.catch(function(e){
							testBtn.disabled = false;
							secMsg(document.getElementById('sec-msg'), e.message, false);
						});
				});
			}
			var saveAlerts = document.getElementById('sec-save-alerts');
			if(saveAlerts){
				saveAlerts.addEventListener('click', function(){
					var fd = new FormData();
					fd.append('action', 'avision_security_save_alerts');
					fd.append('nonce', nonce);
					fd.append('enabled', document.getElementById('sec-alerts-enabled').checked ? '1' : '0');
					fd.append('email', document.getElementById('sec-alert-email').value);
					fetch(ajaxurl, {method:'POST', body:fd})
						.then(function(r){ return r.json(); })
						.then(function(d){
							secMsg(document.getElementById('sec-settings-msg'), d.success ? 'Alert settings saved.' : 'Save failed', d.success);
						});
				});
			}
			var saveSettings = document.getElementById('sec-save-settings');
			if(saveSettings){
				saveSettings.addEventListener('click', function(){
					var fd = new FormData();
					fd.append('action', 'avision_security_save_settings');
					fd.append('nonce', nonce);
					fd.append('whitelist', document.getElementById('sec-whitelist').value);
					fetch(ajaxurl, {method:'POST', body:fd})
						.then(function(r){ return r.json(); })
						.then(function(d){
							secMsg(document.getElementById('sec-settings-msg'), d.success ? 'Whitelist saved.' : 'Save failed', d.success);
						});
				});
			}
		})();
		</script>
		<?php
	}

	/* ---- Code Guard AJAX handlers ---- */

	/**
	 * AJAX: Run code health check script.
	 *
	 * @return void
	 */
	public static function ajax_run_code_health_check() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$script = self::$code_guard_health_script;
		if ( ! file_exists( $script ) ) {
			wp_send_json_error( array( 'message' => 'Health check script not found: ' . $script ) );
		}

		$raw = shell_exec( '/usr/bin/sudo -n /root/scripts/run_health_check_www.sh 2>&1' );
		$lines = explode( "\n", str_replace( "\r\n", "\n", str_replace( "\r", "\n", (string) $raw ) ) );
		if ( ! is_array( $lines ) ) {
			$lines = array();
		}

		$ok_count   = 0;
		$err_count  = 0;
		$score_max  = 35;
		$matched    = false;
		foreach ( $lines as $line ) {
			if ( preg_match( '/(?:R[ÉE]SULTAT|RESULTAT)\s*:\s*(\d+)\s*OK\s*\/\s*(\d+)\s*(?:ERREURS|erreurs|ERRORS|errors)?/iu', $line, $m ) ) {
				$ok_count  = (int) $m[1];
				$err_count = (int) $m[2];
				$score_max = max( 1, $ok_count + $err_count );
				$matched   = true;
				break;
			}
		}

		if ( ! $matched ) {
			$ok_count  = 0;
			$err_count = 0;
			$score_max = 35;
		}

		$score = $ok_count;

		$details = array();
		foreach ( $lines as $line ) {
			$t = trim( $line );
			if ( $t === '' ) {
				continue;
			}
			if ( preg_match( '/^-{3,}/', $t ) ) {
				continue;
			}
			if ( stripos( $t, '=== CODE HEALTH CHECK' ) !== false ) {
				continue;
			}
			if ( stripos( $t, '=== RÉSULTAT' ) !== false || stripos( $t, '=== RESULTAT' ) !== false ) {
				continue;
			}
			if ( strpos( $t, '🟢' ) !== false || strpos( $t, '⚠️' ) !== false ) {
				continue;
			}
			if ( preg_match( '/(?:R[ÉE]SULTAT|RESULTAT)\s*:\s*\d+\s*OK\s*\/\s*\d+/iu', $t ) ) {
				continue;
			}

			$has_ok_emoji  = ( strpos( $t, '✅' ) !== false );
			$has_err_emoji = ( strpos( $t, '❌' ) !== false );
			$icon          = '';
			$text_line     = $t;
			$ok_line       = false;

			if ( $has_ok_emoji && ! $has_err_emoji ) {
				$icon      = '✅';
				$text_line = trim( preg_replace( '/✅/u', '', $t, 1 ) );
				$ok_line   = true;
			} elseif ( $has_err_emoji ) {
				$icon      = '❌';
				$text_line = trim( preg_replace( '/❌/u', '', $t, 1 ) );
				$ok_line   = false;
			} else {
				$ok_line = ( strpos( $t, 'PASS' ) !== false || preg_match( '/\bOK\b/i', $t ) );
				if ( strpos( $t, 'FAIL' ) !== false || strpos( $t, '❌' ) !== false || preg_match( '/\bERREUR\b/i', $t ) ) {
					$ok_line = false;
				}
				$icon      = $ok_line ? '✅' : '❌';
				$text_line = $t;
			}

			$details[] = array(
				'icon' => $icon,
				'text' => $text_line,
				'ok'   => $ok_line,
			);
		}

		$result = array(
			'score'      => $score,
			'score_max'  => $score_max,
			'total'      => $score_max,
			'timestamp'  => gmdate( 'Y-m-d H:i:s' ) . ' UTC',
			'details'    => $details,
		);

		update_option( 'avi_code_health_last_result', $result, false );

		$history = get_option( 'avi_code_health_history', array() );
		if ( ! is_array( $history ) ) {
			$history = array();
		}
		$backup_date_hist = self::code_guard_latest_backup_date();
		$manifest_hist    = $backup_date_hist !== '' ? self::code_guard_load_manifest( $backup_date_hist ) : array();
		$meta_hist        = self::code_guard_manifest_meta( $manifest_hist );
		$history[] = array(
			'score'           => $score,
			'score_max'       => $score_max,
			'timestamp'       => $result['timestamp'],
			'backup_date'     => $backup_date_hist,
			'trigger'         => 'manuel',
			'sprint'          => isset( $manifest_hist['sprint'] ) ? (string) $manifest_hist['sprint'] : '',
			'modified_count'  => $meta_hist['modified_count'],
		);
		if ( count( $history ) > 10 ) {
			$history = array_slice( $history, -10 );
		}
		update_option( 'avi_code_health_history', $history, false );

		// --- Alerte email automatique Code Guard ---
		$cg_alerts_enabled = get_option( 'avi_code_guard_alerts_enabled', 0 );
		$cg_score_ok       = isset( $score ) ? intval( $score ) : 0;
		$cg_score_max      = isset( $score_max ) ? intval( $score_max ) : 35;
		$cg_threshold      = ( $cg_score_max > 0 ) ? ( $cg_score_ok / $cg_score_max ) : 1;

		if ( $cg_alerts_enabled && $cg_threshold < 0.85 ) {
			$cg_alert_email   = get_option( 'avi_code_guard_alert_email', 'contact@avision.org' );
			$cg_recipients    = array_unique( array( $cg_alert_email, 'toniosbh@gmail.com' ) );
			$cg_subject       = '[AVI Code Guard] ⚠️ Score dégradé — ' . $cg_score_ok . '/' . $cg_score_max;
			$cg_body          = "Code Guard a détecté un score dégradé sur avision.org.\n\n"
							  . 'Score : ' . $cg_score_ok . ' / ' . $cg_score_max . "\n"
							  . 'Date  : ' . current_time( 'mysql' ) . "\n\n"
							  . "Vérifiez l'onglet Code Guard dans AVI Control Center :\n"
							  . admin_url( 'admin.php?page=avi-avi-control-center&tab=code_guard' );
			foreach ( $cg_recipients as $cg_to ) {
				wp_mail( $cg_to, $cg_subject, $cg_body );
			}
		}
		// --- Fin alerte Code Guard ---

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: Rollback a file from backup (list or restore).
	 *
	 * @return void
	 */
	public static function ajax_rollback_file() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$sub = isset( $_POST['sub_action'] ) ? sanitize_key( (string) wp_unslash( $_POST['sub_action'] ) ) : '';
		$date = isset( $_POST['backup_date'] ) ? sanitize_file_name( (string) wp_unslash( $_POST['backup_date'] ) ) : '';

		$backup_dir = rtrim( self::$code_guard_backup_dir, '/' );
		if ( $date === '' || ! is_dir( $backup_dir . '/' . $date ) ) {
			wp_send_json_error( array( 'message' => 'Invalid backup date.' ) );
		}
		$date_dir = $backup_dir . '/' . $date;
		$php_dir  = $date_dir . '/php';

		if ( $sub === 'list_files' ) {
			$files = array();
			if ( is_dir( $php_dir ) ) {
				foreach ( self::code_guard_rollback_whitelist_filenames() as $wf ) {
					if ( is_file( $php_dir . '/' . $wf ) ) {
						$files[] = $wf;
					}
				}
			}
			wp_send_json_success( array( 'files' => $files ) );
			return;
		}

		if ( $sub === 'rollback' ) {
			$file = isset( $_POST['backup_file'] ) ? sanitize_file_name( (string) wp_unslash( $_POST['backup_file'] ) ) : '';
			$whitelist = self::code_guard_rollback_whitelist_filenames();
			if ( $file === '' || ! in_array( $file, $whitelist, true ) ) {
				wp_send_json_error( array( 'message' => 'Invalid backup file.' ) );
			}
			$full_backup = $php_dir . '/' . $file;
			if ( ! is_file( $full_backup ) ) {
				wp_send_json_error( array( 'message' => 'Backup file not found.' ) );
			}

			$script = self::$code_guard_rollback_script;
			if ( ! file_exists( $script ) ) {
				wp_send_json_error( array( 'message' => 'Rollback script not found: ' . $script ) );
			}

			$output = shell_exec( '/usr/bin/sudo -n /root/scripts/rollback_file.sh ' . escapeshellarg( $file ) . ' ' . escapeshellarg( $date ) . ' 2>&1' );

			$log = array(
				'action'    => 'rollback',
				'file'      => $file,
				'date'      => $date,
				'timestamp' => gmdate( 'Y-m-d H:i:s' ) . ' UTC',
				'output'    => substr( (string) $output, 0, 500 ),
				'user_id'   => get_current_user_id(),
			);
			$rollback_log = get_option( 'avi_code_guard_rollback_log', array() );
			if ( ! is_array( $rollback_log ) ) {
				$rollback_log = array();
			}
			$rollback_log[] = $log;
			if ( count( $rollback_log ) > 50 ) {
				$rollback_log = array_slice( $rollback_log, -50 );
			}
			update_option( 'avi_code_guard_rollback_log', $rollback_log, false );

			wp_send_json_success( array( 'message' => 'Restored ' . $file . ' from ' . $date, 'output' => (string) $output ) );
			return;
		}

		wp_send_json_error( array( 'message' => 'Unknown sub_action.' ) );
	}

	/**
	 * AJAX: Compare prod file vs backup (sizes, SHA, diff lines).
	 *
	 * @return void
	 */
	public static function ajax_cc_get_file_info() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$date = isset( $_POST['backup_date'] ) ? sanitize_file_name( (string) wp_unslash( $_POST['backup_date'] ) ) : '';
		$file = isset( $_POST['backup_file'] ) ? sanitize_file_name( (string) wp_unslash( $_POST['backup_file'] ) ) : '';
		$whitelist = self::code_guard_rollback_whitelist_filenames();
		if ( $date === '' || $file === '' || ! in_array( $file, $whitelist, true ) ) {
			wp_send_json_error( array( 'message' => 'Invalid parameters.' ) );
		}

		$backup_dir = rtrim( self::$code_guard_backup_dir, '/' );
		$backup_path = $backup_dir . '/' . $date . '/php/' . $file;
		if ( ! is_file( $backup_path ) ) {
			wp_send_json_error( array( 'message' => 'Backup file not found.' ) );
		}

		$prod_path = self::code_guard_prod_path_for_file( $file );
		if ( $prod_path === '' || ! is_file( $prod_path ) ) {
			wp_send_json_error( array( 'message' => 'Production file not found.' ) );
		}

		$manifest = self::code_guard_load_manifest( $date );
		$entry    = self::code_guard_manifest_file_entry( $manifest, $file );

		$size_prod   = (int) filesize( $prod_path );
		$size_backup = (int) filesize( $backup_path );
		$lines_prod  = self::code_guard_count_file_lines( $prod_path );
		$lines_backup = self::code_guard_count_file_lines( $backup_path );
		$sha_prod    = hash_file( 'sha256', $prod_path );
		$sha_backup  = hash_file( 'sha256', $backup_path );
		$unchanged   = ( $sha_prod === $sha_backup );

		$diff_plus  = 0;
		$diff_minus = 0;
		if ( isset( $entry['diff_plus'] ) ) {
			$diff_plus = (int) $entry['diff_plus'];
		}
		if ( isset( $entry['diff_minus'] ) ) {
			$diff_minus = (int) $entry['diff_minus'];
		}
		if ( $diff_plus === 0 && $diff_minus === 0 && is_readable( $prod_path ) && is_readable( $backup_path ) ) {
			$diff_out = shell_exec( 'diff -u ' . escapeshellarg( $backup_path ) . ' ' . escapeshellarg( $prod_path ) . ' 2>/dev/null' );
			if ( is_string( $diff_out ) ) {
				foreach ( explode( "\n", $diff_out ) as $dl ) {
					if ( isset( $dl[0] ) && $dl[0] === '+' && ( ! isset( $dl[1] ) || $dl[1] !== '+' ) ) {
						$diff_plus++;
					} elseif ( isset( $dl[0] ) && $dl[0] === '-' && ( ! isset( $dl[1] ) || $dl[1] !== '-' ) ) {
						$diff_minus++;
					}
				}
			}
		}

		$diff_label = '+' . $diff_plus . ' / -' . $diff_minus;
		if ( isset( $entry['diff'] ) && (string) $entry['diff'] !== '' ) {
			$diff_label = (string) $entry['diff'];
		}

		wp_send_json_success(
			array(
				'file'         => $file,
				'size_prod'    => $size_prod,
				'size_backup'  => $size_backup,
				'lines_prod'   => $lines_prod,
				'lines_backup' => $lines_backup,
				'sha_prod'     => $sha_prod,
				'sha_backup'   => $sha_backup,
				'diff_plus'    => $diff_plus,
				'diff_minus'   => $diff_minus,
				'diff_label'   => $diff_label,
				'unchanged'    => $unchanged,
			)
		);
	}

	/**
	 * AJAX: Save Code Guard alert settings.
	 *
	 * @return void
	 */
	public static function ajax_cc_save_code_guard_alerts() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$email = isset( $_POST['email'] ) ? sanitize_email( (string) wp_unslash( $_POST['email'] ) ) : '';
		if ( $email === '' || ! is_email( $email ) ) {
			wp_send_json_error( array( 'message' => 'Invalid email.' ) );
		}
		$enabled = isset( $_POST['enabled'] ) && (string) wp_unslash( $_POST['enabled'] ) === '1';

		update_option( 'avi_code_guard_alert_email', $email, false );
		update_option( 'avi_code_guard_alerts_enabled', $enabled, false );

		wp_send_json_success( array( 'message' => __( 'Paramètres enregistrés.', 'avision-dashboard' ) ) );
	}

	/**
	 * AJAX: Send test Code Guard alert email.
	 *
	 * @return void
	 */
	public static function ajax_cc_test_code_guard_alert() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$email = get_option( 'avi_code_guard_alert_email', 'contact@avision.org' );
		$email = is_string( $email ) ? $email : 'contact@avision.org';
		if ( ! is_email( $email ) ) {
			wp_send_json_error( array( 'message' => 'Invalid alert email.' ) );
		}

		$subject = '[AVI Code Guard] Test alert';
		$body    = "Test alert from AVI Control Center.\nTime: " . gmdate( 'Y-m-d H:i:s' ) . " UTC\n";
		$sent    = wp_mail( $email, $subject, $body );

		if ( ! $sent ) {
			wp_send_json_error( array( 'message' => 'wp_mail failed.' ) );
		}

		wp_send_json_success( array( 'message' => __( 'Email de test envoyé.', 'avision-dashboard' ) ) );
	}

	/**
	 * AJAX: Run plugin security scan.
	 *
	 * @return void
	 */
	public static function ajax_security_run_scan() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		if ( ! class_exists( 'Avision_Plugin_Security_Scan' ) ) {
			wp_send_json_error( array( 'message' => 'Security scan class not loaded.' ) );
		}

		$result = Avision_Plugin_Security_Scan::run_scan( false );
		wp_send_json_success( $result );
	}

	/**
	 * AJAX: Save Security alert settings.
	 *
	 * @return void
	 */
	public static function ajax_security_save_alerts() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$enabled_raw = isset( $_POST['enabled'] ) ? (string) wp_unslash( $_POST['enabled'] ) : '0';
		$enabled     = ( $enabled_raw === '1' || $enabled_raw === 'true' || (int) $enabled_raw === 1 );

		$email = isset( $_POST['email'] ) ? sanitize_email( (string) wp_unslash( $_POST['email'] ) ) : '';
		if ( $email === '' || ! is_email( $email ) ) {
			$email = 'contact@avision.org';
		}

		update_option( 'avi_security_alerts_enabled', $enabled ? 1 : 0, false );
		update_option( 'avi_security_alert_email', $email, false );

		wp_send_json_success( array( 'message' => __( 'Paramètres enregistrés.', 'avision-dashboard' ) ) );
	}

	/**
	 * AJAX: Save Security whitelist plugins.
	 *
	 * @return void
	 */
	public static function ajax_security_save_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$raw = isset( $_POST['whitelist'] ) ? (string) wp_unslash( $_POST['whitelist'] ) : '';
		$lines = preg_split( '/\r\n|\r|\n/', $raw );
		if ( ! is_array( $lines ) ) {
			$lines = array();
		}
		$whitelist = array();
		foreach ( $lines as $line ) {
			$slug = sanitize_key( trim( (string) $line ) );
			if ( $slug !== '' ) {
				$whitelist[] = $slug;
			}
		}
		$whitelist = array_values( array_unique( $whitelist ) );

		update_option( 'avi_security_scan_whitelist_plugins', $whitelist, false );

		wp_send_json_success( array( 'message' => __( 'Whitelist enregistrée.', 'avision-dashboard' ) ) );
	}

	/**
	 * AJAX: Send test Security alert email.
	 *
	 * @return void
	 */
	public static function ajax_security_test_alert() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		if ( ! class_exists( 'Avision_Plugin_Security_Scan' ) ) {
			wp_send_json_error( array( 'message' => 'Security scan class not loaded.' ) );
		}

		$sent = Avision_Plugin_Security_Scan::maybe_send_alert(
			array(
				'score'          => 50,
				'findings'       => array(
					array(
						'plugin'   => 'test',
						'file'     => 'test.php',
						'line'     => 1,
						'pattern'  => 'TEST ALERT',
						'severity' => 'critical',
						'message'  => 'Test alert from Control Center',
					),
				),
				'hidden_plugins' => array(),
				'timestamp'      => time(),
			)
		);

		wp_send_json_success( array( 'sent' => (bool) $sent ) );
	}

	/**
	 * Onglet API & Crons — monitoring APIs, boucliers, live tests.
	 *
	 * @return void
	 */
	private static function render_api_monitor_tab() {
		if ( ! class_exists( 'Avision_Api_Cron_Monitor' ) ) {
			echo '<p>' . esc_html__( 'Avision_Api_Cron_Monitor non chargé.', 'avision-dashboard' ) . '</p>';
			return;
		}

		$nonce       = wp_create_nonce( 'avision_cc_nonce' );
		$ajax_url    = admin_url( 'admin-ajax.php' );
		$ping_last   = get_option( Avision_Api_Cron_Monitor::OPTION_PING_LAST, array() );
		$ping_last   = is_array( $ping_last ) ? $ping_last : array();
		$labels      = Avision_Api_Cron_Monitor::get_api_labels();
		$usage       = Avision_Api_Cron_Monitor::get_api_usage();
		$crons       = Avision_Api_Cron_Monitor::get_all_crons_status();
		$shields     = Avision_Api_Cron_Monitor::get_guardian_shields_status();
		$profiles    = Avision_Api_Cron_Monitor::get_profiles_for_select( 20 );
		$proof_runs  = Avision_Api_Cron_Monitor::get_proof_extractor_flow_runs();
		$profile_cnt = 0;
		global $wpdb;
		if ( isset( $wpdb ) && $wpdb ) {
			$profile_cnt = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}avi_saas_profiles`" );
		}
		$cap_max = Avision_Api_Cron_Monitor::MAX_PROFILE_CAPACITY;
		$default_pid = (int) Avision_Api_Cron_Monitor::PROFILE_LIVE_TEST_ID;
		$live_profiles = array();
		if ( isset( $wpdb ) && $wpdb ) {
			$live_table = $wpdb->prefix . 'avi_saas_profiles';
			$live_rows  = $wpdb->get_results(
				"SELECT id, business_name, city, user_id FROM `{$live_table}` WHERE id NOT IN (340,341,342,343,344,345,346,347,348,349,43) ORDER BY id DESC LIMIT 20",
				ARRAY_A
			);
			$live_profiles = is_array( $live_rows ) ? $live_rows : array();
		}
		?>
		<div class="avi-api-monitor-tab">
		<style>
		.avi-api-monitor-tab{--avi-navy:#1a1a2e;--avi-violet:#6366f1;}
		.avi-api-monitor-tab .am-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:24px;box-shadow:0 2px 8px rgba(26,26,46,.06)}
		.avi-api-monitor-tab .am-card h3{margin-top:0;color:var(--avi-navy);font-size:16px}
		.avi-api-monitor-tab .am-muted{color:#64748b;font-size:13px}
		.avi-api-monitor-tab .am-btn{padding:8px 18px;border-radius:8px;border:1px solid var(--avi-violet);background:var(--avi-violet);color:#fff;cursor:pointer;font-size:13px;font-weight:600;margin-right:8px;margin-top:8px}
		.avi-api-monitor-tab .am-btn:disabled{opacity:.5;cursor:not-allowed}
		.avi-api-monitor-tab .am-table{width:100%;border-collapse:collapse;font-size:13px;margin-top:12px}
		.avi-api-monitor-tab .am-table th,.avi-api-monitor-tab .am-table td{padding:8px 10px;border-bottom:1px solid #f0f0f0;text-align:left;vertical-align:top}
		.avi-api-monitor-tab .am-table th{background:#f8fafc;color:#475569}
		.avi-api-monitor-tab .am-badge{display:inline-block;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}
		.avi-api-monitor-tab .am-badge-ok{background:#dcfce7;color:#166534}
		.avi-api-monitor-tab .am-badge-warn{background:#fef3c7;color:#92400e}
		.avi-api-monitor-tab .am-badge-fail{background:#fee2e2;color:#dc2626}
		.avi-api-monitor-tab .am-flow{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}
		.avi-api-monitor-tab .am-flow-step{padding:8px 12px;background:#f1f5f9;border-radius:8px;font-size:12px}
		.avi-api-monitor-tab .am-pre{background:#1e293b;color:#e2e8f0;padding:12px;border-radius:8px;font-size:11px;max-height:220px;overflow:auto;white-space:pre-wrap}
		.avi-api-monitor-tab .am-progress{height:10px;background:#e2e8f0;border-radius:6px;overflow:hidden;margin:10px 0}
		.avi-api-monitor-tab .am-progress-fill{height:100%;background:linear-gradient(90deg,var(--avi-violet),#8b5cf6);width:0;transition:width .3s}
		.avi-api-monitor-tab .am-cap-bar{height:12px;background:#e2e8f0;border-radius:6px;overflow:hidden;max-width:400px}
		.avi-api-monitor-tab .am-cap-fill{height:100%;background:var(--avi-violet)}
		.avi-api-monitor-tab select{padding:6px 10px;border-radius:6px;border:1px solid #cbd5e1}
		.avi-api-monitor-tab .am-drawer{display:none;margin-top:8px;padding:10px;background:#f8fafc;border-radius:8px;font-size:12px}
		</style>

		<!-- Section 1 — APIs -->
		<div class="am-card">
			<h3><?php esc_html_e( 'État des APIs', 'avision-dashboard' ); ?></h3>
			<p class="am-muted"><?php esc_html_e( 'Dernier ping (cron 5 min + test manuel).', 'avision-dashboard' ); ?></p>
			<button type="button" class="am-btn" id="am-btn-test-all"><?php esc_html_e( '🔄 Test All APIs', 'avision-dashboard' ); ?></button>
			<button type="button" class="am-btn" id="am-btn-test-alert" style="background:#d97706;border-color:#d97706"><?php esc_html_e( '📧 Test Alert', 'avision-dashboard' ); ?></button>
			<table class="am-table" id="am-api-table">
				<thead><tr><th>API</th><th><?php esc_html_e( 'Usage', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Statut', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Dernière vérif', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Latence', 'avision-dashboard' ); ?></th></tr></thead>
				<tbody>
				<?php
				foreach ( $labels as $key => $label ) {
					$row = isset( $ping_last[ $key ] ) && is_array( $ping_last[ $key ] ) ? $ping_last[ $key ] : array();
					$st  = isset( $row['status'] ) ? (string) $row['status'] : 'unknown';
					$badge = 'am-badge-warn';
					$icon  = '🟡';
					if ( $st === 'ok' ) {
						$badge = 'am-badge-ok';
						$icon  = '🟢';
					} elseif ( $st === 'fail' ) {
						$badge = 'am-badge-fail';
						$icon  = '🔴';
					}
					$ts = isset( $row['checked_at'] ) ? (int) $row['checked_at'] : 0;
					$lat = isset( $row['latency_ms'] ) ? (int) $row['latency_ms'] : 0;
					echo '<tr><td>' . esc_html( $label ) . '</td><td>' . esc_html( isset( $usage[ $key ] ) ? $usage[ $key ] : '—' ) . '</td>';
					echo '<td><span class="am-badge ' . esc_attr( $badge ) . '">' . esc_html( $icon . ' ' . strtoupper( $st ) ) . '</span></td>';
					echo '<td>' . ( $ts ? esc_html( gmdate( 'Y-m-d H:i:s', $ts ) . ' UTC' ) : '—' ) . '</td>';
					echo '<td>' . esc_html( $lat ? $lat . ' ms' : '—' ) . '</td></tr>';
				}
				?>
				</tbody>
			</table>
			<div id="am-api-msg" class="am-muted"></div>
		</div>

		<!-- Section 2 — Onboarding -->
		<div class="am-card">
			<h3><?php esc_html_e( 'Onboarding Checks', 'avision-dashboard' ); ?></h3>
			<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
				<input type="text" id="am-onboard-search" placeholder="🔍 Rechercher par ID ou nom..." style="flex:1;border:1px solid #d1d5db;border-radius:6px;padding:7px 12px;font-size:13px;" oninput="aviFilterSelect('am-onboard-profile',this.value)">
			</div>
			<label><?php esc_html_e( 'Profil', 'avision-dashboard' ); ?>
			<select id="am-onboard-profile" style="width:100%;max-width:600px;">
				<?php foreach ( $profiles as $p ) : ?>
					<option value="<?php echo esc_attr( (string) $p['id'] ); ?>" <?php selected( (int) $p['id'], $default_pid ); ?>>
						<?php echo esc_html( '#' . $p['id'] . ' — ' . ( $p['business_name'] ?? '' ) . ' (' . ( $p['city'] ?? '' ) . ')' ); ?>
					</option>
				<?php endforeach; ?>
			</select></label>
			<button type="button" class="am-btn" id="am-btn-onboard"><?php esc_html_e( 'Vérifier', 'avision-dashboard' ); ?></button>
			<div id="am-onboard-results"></div>
		</div>

		<!-- Section 3 — Boucliers -->
		<div class="am-card">
			<h3><?php esc_html_e( '9 Boucliers Guardian', 'avision-dashboard' ); ?></h3>
			<table class="am-table">
				<thead><tr><th><?php esc_html_e( 'Bouclier', 'avision-dashboard' ); ?></th><th>API</th><th>Cron</th><th><?php esc_html_e( 'Fréquence', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Dernière exec', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Profils actifs', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Statut', 'avision-dashboard' ); ?></th></tr></thead>
				<tbody>
				<?php foreach ( $shields as $sh ) : ?>
					<tr class="am-shield-row" data-shield-id="<?php echo esc_attr( $sh['id'] ); ?>" style="cursor:pointer">
						<td><strong><?php echo esc_html( $sh['nom'] ); ?></strong></td>
						<td><?php echo esc_html( $sh['api_used'] ); ?></td>
						<td><code><?php echo esc_html( $sh['cron_hook'] ); ?></code></td>
						<td><?php echo esc_html( $sh['frequency'] ); ?></td>
						<td><?php echo esc_html( $sh['last_run_human'] ); ?></td>
						<td><?php echo esc_html( (string) $sh['profiles_active'] ); ?></td>
						<td><?php echo esc_html( $sh['statut'] ); ?></td>
					</tr>
					<tr class="am-drawer-row" style="display:none"><td colspan="7"><div class="am-drawer"><?php echo esc_html( $sh['bouclier'] . ' — hook: ' . $sh['cron_hook'] ); ?></div></td></tr>
				<?php endforeach; ?>
				</tbody>
			</table>
		</div>

		<!-- Section 4 — Crons -->
		<div class="am-card">
			<h3><?php esc_html_e( 'Tableau Crons', 'avision-dashboard' ); ?></h3>
			<p class="am-muted"><?php esc_html_e( 'Capacité flotte :', 'avision-dashboard' ); ?> <strong><?php echo esc_html( (string) $profile_cnt ); ?> / <?php echo esc_html( (string) $cap_max ); ?></strong></p>
			<div class="am-cap-bar"><div class="am-cap-fill" style="width:<?php echo esc_attr( (string) min( 100, (int) round( ( $profile_cnt / max( 1, $cap_max ) ) * 100 ) ) ); ?>%"></div></div>
			<table class="am-table">
				<thead><tr><th>Hook</th><th><?php esc_html_e( 'Intervalle', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Dernière exec', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Prochaine', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Profils', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Bouclier', 'avision-dashboard' ); ?></th><th><?php esc_html_e( 'Statut', 'avision-dashboard' ); ?></th><th></th></tr></thead>
				<tbody>
				<?php foreach ( $crons as $c ) : ?>
					<tr>
						<td><code><?php echo esc_html( $c['hook'] ); ?></code></td>
						<td><?php echo esc_html( $c['interval'] ); ?></td>
						<td><?php echo esc_html( $c['last_run_human'] ); ?></td>
						<td><?php echo esc_html( $c['next_run_human'] ); ?></td>
						<td><?php echo esc_html( (string) $c['profiles_count'] ); ?></td>
						<td><?php echo esc_html( $c['bouclier_associe'] ); ?></td>
						<td><?php echo esc_html( $c['statut'] ); ?></td>
						<td><button type="button" class="am-btn am-force-cron" data-hook="<?php echo esc_attr( $c['hook'] ); ?>">▶ Force</button></td>
					</tr>
				<?php endforeach; ?>
				</tbody>
			</table>
		</div>

		<!-- Section 5 — Proof Extractor -->
		<div class="am-card">
			<h3><?php esc_html_e( 'Proof Extractor Flow', 'avision-dashboard' ); ?></h3>
			<div class="am-flow">
				<span class="am-flow-step">Review 5★</span> →
				<span class="am-flow-step"><?php esc_html_e( 'Vérif. chars', 'avision-dashboard' ); ?></span> →
				<span class="am-flow-step">OpenRouter</span> →
				<span class="am-flow-step">GBP Post (dry)</span> →
				<span class="am-flow-step">Log</span> →
				<span class="am-flow-step">Email</span>
			</div>
			<?php if ( empty( $proof_runs ) ) : ?>
				<p class="am-muted"><?php esc_html_e( 'Aucune ligne récente dans guardian-creators.log.', 'avision-dashboard' ); ?></p>
			<?php else : ?>
				<table class="am-table">
					<thead><tr><th><?php esc_html_e( 'Run', 'avision-dashboard' ); ?></th><th>Review</th><th>OR</th><th>Post</th><th>Log</th><th>Email</th></tr></thead>
					<tbody>
					<?php foreach ( $proof_runs as $pr ) : ?>
						<tr>
							<td><code><?php echo esc_html( substr( $pr['line'], 0, 80 ) ); ?></code></td>
							<td><?php echo esc_html( $pr['step_review'] ); ?></td>
							<td><?php echo esc_html( $pr['step_openrouter'] ); ?></td>
							<td><?php echo esc_html( $pr['step_post'] ); ?></td>
							<td><?php echo esc_html( $pr['step_log'] ); ?></td>
							<td><?php echo esc_html( $pr['step_email'] ); ?></td>
						</tr>
					<?php endforeach; ?>
					</tbody>
				</table>
			<?php endif; ?>
		</div>

		<!-- Section 6 — Live Tests -->
		<div class="am-card">
			<h3><?php esc_html_e( '🧪 Live Tests', 'avision-dashboard' ); ?></h3>
			<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
				<input type="text" id="am-live-search" placeholder="🔍 Rechercher par ID ou nom..." style="flex:1;border:1px solid #d1d5db;border-radius:6px;padding:7px 12px;font-size:13px;" oninput="aviFilterSelect('am-live-profile',this.value)">
			</div>
			<select id="am-live-profile" style="width:100%;max-width:600px;margin-bottom:8px;">
				<?php foreach ( $live_profiles as $p ) : ?>
					<option value="<?php echo esc_attr( (string) $p['id'] ); ?>"<?php echo (int) $p['id'] === (int) Avision_Api_Cron_Monitor::PROFILE_LIVE_TEST_ID ? ' selected="selected"' : ''; ?>><?php echo esc_html( '#' . $p['id'] . ' — ' . ( $p['business_name'] ?? '' ) . ( !empty($p['city']) ? ' (' . $p['city'] . ')' : '' ) ); ?></option>
				<?php endforeach; ?>
			</select>
			<div style="margin-top:12px">
				<?php
				$tests = array(
					'gbp_scan'         => 'GBP Scan',
					'reviews'          => 'Reviews',
					'keywords'         => 'Keywords',
					'chat'             => 'Chat AVI',
					'proof_extractor'  => 'Proof Extractor',
					'maps_iframe'      => 'Maps iframe',
					'email_brevo'      => 'Email Brevo',
					'oauth_status'     => 'OAuth',
					'competitors'      => 'Competitors DFS',
				);
				foreach ( $tests as $tk => $tl ) {
					echo '<button type="button" class="am-btn am-live-test" data-test="' . esc_attr( $tk ) . '">' . esc_html( $tl ) . '</button>';
				}
				?>
				<button type="button" class="am-btn" id="am-btn-run-all" style="background:#0f766e;border-color:#0f766e"><?php esc_html_e( '🚀 Run All Tests', 'avision-dashboard' ); ?></button>
			</div>
			<div class="am-progress"><div class="am-progress-fill" id="am-live-progress"></div></div>
			<div id="am-live-results"></div>
		</div>

		<script>
		(function(){
			var ajaxUrl = <?php echo wp_json_encode( $ajax_url ); ?>;
			var nonce = <?php echo wp_json_encode( $nonce ); ?>;
			function post(action, data, cb){
				data = data || {};
				data.action = action;
				data.nonce = nonce;
				var fd = new FormData();
				Object.keys(data).forEach(function(k){ fd.append(k, data[k]); });
				fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
					.then(function(r){ return r.json(); })
					.then(cb)
					.catch(function(e){ cb({ success: false, data: { message: String(e) } }); });
			}
			document.getElementById('am-btn-test-all').addEventListener('click', function(){
				var btn = this; btn.disabled = true;
				document.getElementById('am-api-msg').textContent = '…';
				post('avision_api_test_all', {}, function(res){
					btn.disabled = false;
					document.getElementById('am-api-msg').textContent = res.success ? 'OK — recharger la page pour voir le tableau.' : (res.data && res.data.message ? res.data.message : 'Erreur');
					if (res.success) location.reload();
				});
			});
			document.getElementById('am-btn-test-alert').addEventListener('click', function(){
				post('avision_api_test_alert', {}, function(res){
					document.getElementById('am-api-msg').textContent = res.success ? 'Alerte test envoyée.' : 'Échec';
				});
			});
			document.getElementById('am-btn-onboard').addEventListener('click', function(){
				var pid = document.getElementById('am-onboard-profile').value;
				post('avision_api_check_onboarding', { profile_id: pid }, function(res){
					var el = document.getElementById('am-onboard-results');
					if (!res.success || !res.data) { el.innerHTML = '<p class="am-muted">Erreur</p>'; return; }
					var html = '<table class="am-table"><tr><th>Check</th><th>API</th><th>Valeur</th><th>Impact</th><th></th></tr>';
					(res.data.checks || []).forEach(function(c){
						html += '<tr><td>'+c.label+'</td><td>'+c.api+'</td><td><code>'+(c.value||'—')+'</code></td><td>+'+c.score_impact+'%</td><td>'+(c.pass?'✅':'❌')+'</td></tr>';
					});
					html += '</table>';
					el.innerHTML = html;
				});
			});
			document.querySelectorAll('.am-force-cron').forEach(function(b){
				b.addEventListener('click', function(){
					var hook = this.getAttribute('data-hook');
					if (!hook || !confirm('Force run '+hook+' ?')) return;
					post('avision_api_force_cron', { hook: hook }, function(res){
						alert(res.success ? 'Cron déclenché: '+hook : 'Erreur');
					});
				});
			});
			document.querySelectorAll('.am-shield-row').forEach(function(row){
				row.addEventListener('click', function(){
					var next = row.nextElementSibling;
					if (next && next.classList.contains('am-drawer-row')) {
						next.style.display = next.style.display === 'none' ? 'table-row' : 'none';
					}
				});
			});
			function escHtml(s){
				if (s === null || s === undefined) return '';
				return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
			}
			function competitorsFallbackBadge(level){
				level = parseInt(level, 10);
				if (isNaN(level)) level = 0;
				var map = {
					1: { icon: '🟢', label: 'Direct match' },
					2: { icon: '🟡', label: 'City fallback' },
					3: { icon: '🟠', label: 'Generic + neighborhood' },
					4: { icon: '🔴', label: 'Generic + city' },
					0: { icon: '⚫', label: 'No results' }
				};
				var m = map[level] || map[0];
				return '<span class="am-badge am-badge-ok">' + m.icon + ' ' + escHtml(m.label) + '</span>';
			}
			function renderCompetitorsHtml(name, res){
				var d = (res && res.data) ? res.data : {};
				var st = (res.success && d.status === 'ok') ? 'ok' : 'fail';
				var lat = d.latency_ms ? d.latency_ms + ' ms' : '';
				var level = (d.fallback_level !== undefined && d.fallback_level !== null) ? d.fallback_level : 0;
				var kw = d.keyword_used || '';
				var comps = Array.isArray(d.competitors) ? d.competitors : [];
				var raw = d.raw_sample || '';
				var uid = 'am-raw-' + Date.now() + '-' + Math.floor(Math.random() * 10000);
				var html = '<div class="am-live-competitors" style="margin-top:12px">';
				html += '<p><strong>' + escHtml(name) + '</strong> <span class="am-badge am-badge-' + st + '">' + st + '</span> ' + lat + '</p>';
				html += '<p>' + competitorsFallbackBadge(level) + ' <em>' + escHtml(kw) + '</em></p>';
				if (!comps.length) {
					html += '<p class="am-muted">Aucun concurrent trouvé pour cette zone</p>';
				} else {
					html += '<table class="am-table"><thead><tr><th>#</th><th>Nom</th><th>Adresse</th><th>Note</th><th>Position Maps</th></tr></thead><tbody>';
					comps.forEach(function(c, i){
						html += '<tr><td>' + (i + 1) + '</td>';
						html += '<td>' + escHtml(c.name || '') + '</td>';
						html += '<td>' + escHtml(c.address || '') + '</td>';
						html += '<td>' + (c.rating != null && c.rating !== '' ? escHtml(c.rating) : '—') + '</td>';
						html += '<td>' + (c.maps_position != null && c.maps_position !== '' ? escHtml(c.maps_position) : '—') + '</td></tr>';
					});
					html += '</tbody></table>';
				}
				if (raw) {
					html += '<p><button type="button" class="am-btn am-toggle-raw" data-target="' + uid + '" style="margin-top:8px;background:#475569;border-color:#475569">Voir réponse brute</button></p>';
					html += '<pre class="am-pre" id="' + uid + '" style="display:none">' + escHtml(raw) + '</pre>';
				}
				if (d.note) {
					html += '<p class="am-muted">' + escHtml(d.note) + '</p>';
				}
				if (d.error) {
					html += '<p><span class="am-badge am-badge-fail">' + escHtml(d.error) + '</span></p>';
				}
				html += '</div>';
				return html;
			}
			function renderLiveResultHtml(name, res){
				if (name === 'competitors') {
					return renderCompetitorsHtml('Competitors DFS', res);
				}
				var d = (res && res.data) ? res.data : {};
				var st = (res.success && d.status === 'ok') ? 'ok' : 'fail';
				var lat = d.latency_ms ? d.latency_ms + ' ms' : '';
				return '<div style="margin-top:12px"><p><strong>' + escHtml(name) + '</strong> <span class="am-badge am-badge-' + st + '">' + st + '</span> ' + lat + '</p><pre class="am-pre">' + escHtml(JSON.stringify(d, null, 2)) + '</pre></div>';
			}
			function renderLiveResult(name, res){
				var wrap = document.createElement('div');
				wrap.innerHTML = renderLiveResultHtml(name, res);
				while (wrap.firstChild) {
					document.getElementById('am-live-results').appendChild(wrap.firstChild);
				}
			}
			function showLiveLoading(test, label){
				var boxId = 'am-live-box-' + test;
				var old = document.getElementById(boxId);
				if (old) old.remove();
				var box = document.createElement('div');
				box.id = boxId;
				box.style.marginTop = '12px';
				box.innerHTML = '<p><strong>' + escHtml(label || test) + '</strong> <span class="am-muted">Initialising…</span></p>';
				document.getElementById('am-live-results').appendChild(box);
				return boxId;
			}
			function replaceLiveBox(boxId, html){
				var box = document.getElementById(boxId);
				if (box) {
					box.outerHTML = html;
				} else {
					document.getElementById('am-live-results').insertAdjacentHTML('beforeend', html);
				}
			}
			document.getElementById('am-live-results').addEventListener('click', function(e){
				var btn = e.target.closest('.am-toggle-raw');
				if (!btn) return;
				var pre = document.getElementById(btn.getAttribute('data-target'));
				if (!pre) return;
				var show = pre.style.display === 'none';
				pre.style.display = show ? 'block' : 'none';
				btn.textContent = show ? 'Masquer réponse brute' : 'Voir réponse brute';
			});
			document.querySelectorAll('.am-live-test').forEach(function(btn){
				btn.addEventListener('click', function(){
					var test = this.getAttribute('data-test');
					var pid = document.getElementById('am-live-profile').value;
					var label = this.textContent.trim();
					this.disabled = true;
					var boxId = showLiveLoading(test, label);
					post('avision_api_live_test', { test_name: test, profile_id: pid }, function(res){
						btn.disabled = false;
						replaceLiveBox(boxId, renderLiveResultHtml(test, res));
					});
				});
			});
			document.getElementById('am-btn-run-all').addEventListener('click', function(){
				var tests = ['gbp_scan','reviews','keywords','chat','proof_extractor','maps_iframe','email_brevo','oauth_status','competitors'];
				var pid = document.getElementById('am-live-profile').value;
				document.getElementById('am-live-results').innerHTML = '';
				var i = 0, bar = document.getElementById('am-live-progress');
				function next(){
					if (i >= tests.length) { bar.style.width = '100%'; return; }
					bar.style.width = Math.round((i/tests.length)*100)+'%';
					post('avision_api_live_test', { test_name: tests[i], profile_id: pid }, function(res){
						renderLiveResult(tests[i], res);
						i++; next();
					});
				}
				next();
			});
		})();

		// Filtre select par recherche texte
		function aviFilterSelect(selId, q) {
			var sel = document.getElementById(selId);
			if (!sel) return;
			var opts = Array.from(sel.querySelectorAll('option'));
			var ql = q.toLowerCase().trim();
			opts.forEach(function(o) {
				o.hidden = ql && o.text.toLowerCase().indexOf(ql) === -1;
			});
			var first = opts.find(function(o){ return !o.hidden; });
			if (first) sel.value = first.value;
		}
		</script>
		</div>
		<?php
	}

	/**
	 * AJAX: ping toutes les APIs.
	 *
	 * @return void
	 */
	public static function ajax_api_test_all() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		if ( ! class_exists( 'Avision_Api_Cron_Monitor' ) ) {
			wp_send_json_error( array( 'message' => 'Monitor not loaded' ) );
		}
		$result = Avision_Api_Cron_Monitor::run_api_ping();
		wp_send_json_success( $result );
	}

	/**
	 * AJAX: envoyer une alerte API test.
	 *
	 * @return void
	 */
	public static function ajax_api_test_alert() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		if ( class_exists( 'Avision_Api_Cron_Monitor' ) ) {
			Avision_Api_Cron_Monitor::maybe_send_api_alert( 'manual_test', 'fail', array( 'note' => 'Test alert from Control Center' ) );
		}
		wp_send_json_success( array( 'sent' => true ) );
	}

	/**
	 * AJAX: checks onboarding profil.
	 *
	 * @return void
	 */
	public static function ajax_api_check_onboarding() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		$profile_id = isset( $_POST['profile_id'] ) ? (int) wp_unslash( $_POST['profile_id'] ) : 384;
		if ( ! class_exists( 'Avision_Api_Cron_Monitor' ) ) {
			wp_send_json_error( array( 'message' => 'Monitor not loaded' ) );
		}
		wp_send_json_success( Avision_Api_Cron_Monitor::get_onboarding_checks_for_profile( $profile_id ) );
	}

	/**
	 * AJAX: forcer un cron hook.
	 *
	 * @return void
	 */
	public static function ajax_api_force_cron() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		$hook = isset( $_POST['hook'] ) ? sanitize_key( wp_unslash( $_POST['hook'] ) ) : '';
		if ( $hook === '' || ! Avision_Api_Cron_Monitor::is_avi_hook_public( $hook ) ) {
			wp_send_json_error( array( 'message' => 'Invalid hook' ) );
		}
		do_action( $hook );
		$log = get_option( Avision_Api_Cron_Monitor::OPTION_CRON_LAST_RUN, array() );
		if ( ! is_array( $log ) ) {
			$log = array();
		}
		$log[ $hook ] = time();
		update_option( Avision_Api_Cron_Monitor::OPTION_CRON_LAST_RUN, $log, false );
		wp_send_json_success( array( 'hook' => $hook, 'fired' => true ) );
	}

	/**
	 * AJAX: live test unitaire.
	 *
	 * @return void
	 */
	public static function ajax_api_live_test() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Unauthorized' ) );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		$test = isset( $_POST['test_name'] ) ? sanitize_key( wp_unslash( $_POST['test_name'] ) ) : '';
		$pid  = isset( $_POST['profile_id'] ) ? (int) wp_unslash( $_POST['profile_id'] ) : 384;
		if ( ! class_exists( 'Avision_Api_Cron_Monitor' ) ) {
			wp_send_json_error( array( 'message' => 'Monitor not loaded' ) );
		}
		wp_send_json_success( Avision_Api_Cron_Monitor::run_live_test( $test, $pid ) );
	}

	/**
	 * AJAX: Show filtered crontab entries.
	 *
	 * @return void
	 */
	public static function ajax_cron_guard_status() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$filtered_str = self::code_guard_get_filtered_crontab_text();
		$lines = $filtered_str === '' ? array() : preg_split( '/\R/', $filtered_str );
		if ( ! is_array( $lines ) ) {
			$lines = array();
		}
		$ll_join = strtolower( $filtered_str );
		$has_guard = ( strpos( $ll_join, 'check_code_health' ) !== false );

		wp_send_json_success( array(
			'crontab'        => $filtered_str !== '' ? $filtered_str : '(empty)',
			'has_guard_cron' => $has_guard,
			'total_lines'    => count( $lines ),
		) );
	}

	/**
	 * AJAX: Install the 6h guard cron if absent.
	 *
	 * @return void
	 */
	public static function ajax_cron_guard_install() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );

		$script = self::$code_guard_health_script;
		if ( ! file_exists( $script ) ) {
			wp_send_json_error( array( 'message' => 'Health script not found — cannot install cron.' ) );
		}

		$entry = '0 */6 * * * bash ' . $script . ' >> /var/log/avi_code_health.log 2>&1';
		$output = shell_exec( '(crontab -l 2>/dev/null; echo ' . escapeshellarg( $entry ) . ') | sort -u | crontab - 2>&1' );

		$new_cron = self::code_guard_get_filtered_crontab_text();

		wp_send_json_success( array(
			'message'  => 'Guard cron installed (every 6h).',
			'crontab'  => $new_cron !== '' ? $new_cron : '(empty)',
		) );
	}

	/**
	 * Onglet Integration Tests — tests E2E cross-système.
	 */
	private static function render_integration_tests_tab() {
		$nonce     = wp_create_nonce( 'avision_cc_nonce' );
		$last      = get_option( 'avi_integration_tests_last', array() );
		$last      = is_array( $last ) ? $last : array();
		$last_ts   = isset( $last['timestamp'] ) ? (int) $last['timestamp'] : 0;
		$last_res  = isset( $last['output'] ) ? (string) $last['output'] : '';
		$last_pass = isset( $last['pass'] ) ? (int) $last['pass'] : 0;
		$last_fail = isset( $last['fail'] ) ? (int) $last['fail'] : 0;
		?>
		<div class="avi-cc-section" style="margin-top:24px;">
			<h2 style="font-size:1.2rem;margin-bottom:8px;">🔬 Tests d'Intégration AVI</h2>
			<p style="color:#666;margin-bottom:16px;">Vérifie les 18 points critiques du système : endpoints, services, DB, sécurité, règles anti-régression.</p>
			<?php if ( $last_ts ) : ?>
			<p style="font-size:.85rem;color:#888;">Dernier test : <?php echo esc_html( human_time_diff( $last_ts ) ); ?> ago —
				<strong style="color:<?php echo $last_fail > 0 ? '#dc2626' : '#16a34a'; ?>">
					<?php echo esc_html( (string) $last_pass ); ?>/<?php echo esc_html( (string) ( $last_pass + $last_fail ) ); ?> OK
				</strong>
			</p>
			<?php endif; ?>
			<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:20px 24px;margin-bottom:24px;">
				<h3 style="margin:0 0 16px;font-size:1rem;color:#1e293b;">📋 Workflow de déploiement obligatoire</h3>

				<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">

					<div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;">
						<h4 style="margin:0 0 12px;color:#7c3aed;font-size:.9rem;">⬆️ AVANT upload WinSCP</h4>
						<ol style="margin:0;padding-left:18px;font-size:.82rem;line-height:2;">
							<li>Cursor analyse le code <strong>sans modifier</strong></li>
							<li>Cursor implémente le fix</li>
							<li><code>php8.1 -l fichier.php</code> — syntaxe OK</li>
							<li>SSH : <code>avi-predeploy</code> — 10/10 checks</li>
							<li>WinSCP upload</li>
						</ol>
					</div>

					<div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;">
						<h4 style="margin:0 0 12px;color:#059669;font-size:.9rem;">⬇️ APRÈS upload WinSCP</h4>
						<ol style="margin:0;padding-left:18px;font-size:.82rem;line-height:2;">
							<li><code>chown avisi1279:avisi1279 fichier</code></li>
							<li><code>wp litespeed-purge all --allow-root</code></li>
							<li>AVI CC → 🔬 Integration Tests → <strong>18/18</strong></li>
							<li>SSH : <code>avi-snap "description-fix"</code></li>
							<li>SSH : <code>avi-freeze</code> — checksums mis à jour</li>
							<li>SSH : <code>backup_critical_files.sh</code></li>
						</ol>
					</div>

				</div>

				<div style="margin-top:16px;background:#fff;border:1px solid #fca5a5;border-radius:8px;padding:16px;">
					<h4 style="margin:0 0 12px;color:#dc2626;font-size:.9rem;">🚨 Rollbacks en cas de problème</h4>
					<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;font-size:.8rem;">
						<div>
							<strong style="color:#dc2626;">Fichier PHP corrompu</strong><br>
							<code style="display:block;margin-top:4px;background:#f1f5f9;padding:4px 8px;border-radius:4px;font-size:.75rem;">
							cp /root/backups/critical-files/[DATE]/php/[fichier] [dest]<br>
							php8.1 -l [fichier]<br>
							chown avisi1279 [fichier]<br>
							purge cache
							</code>
						</div>
						<div>
							<strong style="color:#dc2626;">Backend main.py corrompu</strong><br>
							<code style="display:block;margin-top:4px;background:#f1f5f9;padding:4px 8px;border-radius:4px;font-size:.75rem;">
							/root/rollback.sh<br><br>
							ou<br><br>
							cp backup/main.py /root/avision-backend/<br>
							systemctl restart avision-backend
							</code>
						</div>
						<div>
							<strong style="color:#dc2626;">Rollback Git complet</strong><br>
							<code style="display:block;margin-top:4px;background:#f1f5f9;padding:4px 8px;border-radius:4px;font-size:.75rem;">
							git log --oneline | head -5<br>
							git checkout v-snap-[date] -- wp-content/[fichier]<br><br>
							⚠️ JAMAIS git reset --hard<br>sur public_html
							</code>
						</div>
					</div>
				</div>

				<div style="margin-top:16px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:12px 16px;">
					<h4 style="margin:0 0 8px;color:#92400e;font-size:.85rem;">⚠️ Règles absolues</h4>
					<ul style="margin:0;padding-left:18px;font-size:.8rem;line-height:1.8;color:#78350f;">
						<li>JAMAIS <code>git reset --hard</code> / <code>git clean</code> sur public_html en prod</li>
						<li>JAMAIS déployer sans <code>avi-predeploy</code> + <code>php8.1 -l</code></li>
						<li>JAMAIS hardcoder <code>sk-or-*</code> dans un fichier PHP</li>
						<li>TOUJOURS <code>chown avisi1279</code> après upload WinSCP</li>
						<li>TOUJOURS purger LiteSpeed après modification PHP/JS</li>
						<li>TOUJOURS <code>avi-snap</code> après fix validé 18/18</li>
					</ul>
				</div>
			</div>
			<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:12px;padding:20px 24px;margin-bottom:24px;">
				<h3 style="margin:0 0 12px;font-size:1rem;color:#dc2626;">🚨 Guide Restauration d'Urgence</h3>
				<p style="font-size:.85rem;color:#7f1d1d;margin:0 0 16px;">En cas de page blanche, hack ou perte de données — procédures SSH à suivre dans l'ordre.</p>

				<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px;">
					<div style="background:#fff;border:1px solid #fca5a5;border-radius:8px;padding:12px;">
						<strong style="color:#dc2626;font-size:.85rem;">🔴 Page blanche / PHP</strong>
						<code style="display:block;margin-top:8px;font-size:.75rem;background:#f9fafb;padding:8px;border-radius:4px;white-space:pre-wrap;">LAST=$(ls -dt /root/backups/critical-files/[0-9]* | head -1)
cp $LAST/php/[fichier] [dest]
php8.1 -l [fichier]
chown avisi1279 [fichier]
wp litespeed-purge all</code>
					</div>
					<div style="background:#fff;border:1px solid #fca5a5;border-radius:8px;padding:12px;">
						<strong style="color:#dc2626;font-size:.85rem;">🔴 DB corrompue</strong>
						<code style="display:block;margin-top:8px;font-size:.75rem;background:#f9fafb;padding:8px;border-radius:4px;white-space:pre-wrap;">LAST=$(ls -dt /root/backups/full/daily/[0-9]* | head -1)
wp db import $LAST/db_avision_*.sql
wp db check
wp litespeed-purge all</code>
					</div>
					<div style="background:#fff;border:1px solid #fca5a5;border-radius:8px;padding:12px;">
						<strong style="color:#dc2626;font-size:.85rem;">🔴 Backend down</strong>
						<code style="display:block;margin-top:8px;font-size:.75rem;background:#f9fafb;padding:8px;border-radius:4px;white-space:pre-wrap;">systemctl restart avision-backend
sleep 3
curl http://localhost:8001/health
# Si KO :
/root/rollback.sh</code>
					</div>
				</div>

				<div style="background:#fff;border:1px solid #fca5a5;border-radius:8px;padding:12px;margin-bottom:12px;">
					<strong style="color:#dc2626;font-size:.85rem;">🔴 Site hacké</strong><br>
					<code style="display:block;margin-top:8px;font-size:.75rem;background:#f9fafb;padding:8px;border-radius:4px;">
LAST=$(ls -dt /root/backups/full/daily/[0-9]* | head -1) &amp;&amp; tar -xzf $LAST/wp-content_*.tar.gz -C / &amp;&amp; chown -R avisi1279:avisi1279 /home/avision.org/public_html/wp-content/ &amp;&amp; wp config shuffle-salts &amp;&amp; wp litespeed-purge all
					</code>
				</div>

				<p style="font-size:.78rem;color:#991b1b;margin:0;">
					📄 Guide complet : <code>/root/RESTAURATION_URGENCE.md</code> —
					aussi disponible sur <a href="https://github.com/toniosbh/avision-saas/blob/master/wp-content/plugins/avision-dashboard/RESTAURATION_URGENCE.md" target="_blank" rel="noopener noreferrer" style="color:#dc2626;">GitHub</a>
				</p>
			</div>
			<button type="button" id="avi-it-run-btn" class="button button-primary" style="margin-bottom:16px;">
				▶ Lancer les tests d'intégration
			</button>
			<div id="avi-it-status" style="margin-bottom:8px;font-weight:600;"></div>
			<pre id="avi-it-output" style="background:#0f172a;color:#e2e8f0;padding:16px;border-radius:8px;max-height:400px;overflow-y:auto;font-size:.82rem;line-height:1.6;white-space:pre-wrap;"><?php echo esc_html( $last_res ); ?></pre>
		</div>
		<script>
		(function() {
			var btn = document.getElementById('avi-it-run-btn');
			var output = document.getElementById('avi-it-output');
			var status = document.getElementById('avi-it-status');
			if (!btn) return;
			btn.addEventListener('click', function() {
				btn.disabled = true;
				btn.textContent = '⏳ Tests en cours…';
				status.textContent = '';
				output.textContent = '⏳ Exécution des tests… (peut prendre 30-60s)';
				var fd = new FormData();
				fd.append('action', 'avision_integration_tests_run');
				fd.append('nonce', '<?php echo esc_js( $nonce ); ?>');
				fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' })
					.then(function(r) { return r.json(); })
					.then(function(res) {
						btn.disabled = false;
						btn.textContent = '▶ Lancer les tests d\'intégration';
						if (res.success && res.data) {
							output.textContent = res.data.output || '';
							var pass = res.data.pass || 0;
							var fail = res.data.fail || 0;
							status.innerHTML = fail > 0
								? '<span style="color:#dc2626;">❌ ' + fail + ' test(s) en échec</span>'
								: '<span style="color:#16a34a;">✅ ' + pass + '/' + (pass+fail) + ' tests passés</span>';
						} else {
							status.innerHTML = '<span style="color:#dc2626;">❌ Erreur AJAX</span>';
							output.textContent = res.data ? (res.data.message || 'Erreur inconnue') : 'Erreur inconnue';
						}
					})
					.catch(function(e) {
						btn.disabled = false;
						btn.textContent = '▶ Lancer les tests d\'intégration';
						status.innerHTML = '<span style="color:#dc2626;">❌ Erreur réseau</span>';
						output.textContent = String(e);
					});
			});
		})();
		</script>
		<?php
	}

	/**
	 * AJAX — Exécuter le script d'intégration.
	 *
	 * @return void
	 */
	public static function ajax_integration_tests_run() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
		}
		check_ajax_referer( 'avision_cc_nonce', 'nonce' );
		$script = WP_CONTENT_DIR . '/avi-scripts/test_integration.sh';
		if ( ! file_exists( $script ) ) {
			wp_send_json_error( array( 'message' => 'Script introuvable : ' . $script ), 500 );
		}
		set_time_limit( 120 );
		$raw = shell_exec( 'bash ' . escapeshellarg( $script ) . ' 2>&1' );
		if ( $raw === null ) {
			wp_send_json_error( array( 'message' => 'shell_exec indisponible ou script en erreur' ), 500 );
		}
		// Parser ✅/❌
		$pass = substr_count( $raw, '✅' );
		$fail = substr_count( $raw, '❌' );
		// Sauvegarder le résultat
		update_option(
			'avi_integration_tests_last',
			array(
				'timestamp' => time(),
				'output'    => $raw,
				'pass'      => $pass,
				'fail'      => $fail,
			),
			false
		);
		wp_send_json_success(
			array(
				'output' => $raw,
				'pass'   => $pass,
				'fail'   => $fail,
			)
		);
	}
}
