connect = $connect;
}
/**
* Hook everything up.
*/
public function register_hooks() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_ajax_ael42a_run_checks', array( $this, 'ajax_run_checks' ) );
add_action( 'wp_ajax_ael42a_refresh_vitals', array( $this, 'ajax_refresh_vitals' ) );
add_action( 'wp_ajax_ael42a_disconnect', array( $this, 'ajax_disconnect' ) );
add_action( 'wp_ajax_ael42a_save_settings', array( $this, 'ajax_save_settings' ) );
}
/**
* Register the top-level menu page.
*
* The menu icon is the 42A logo mark as a base64 SVG data URI (WordPress
* only recolors dashicons, so custom SVGs keep their own fills - the
* brand colors read fine on the dark admin sidebar). The wide mark is
* centered in a square viewBox so the 20px menu slot renders it whole.
*/
public function register_menu() {
$menu_icon = 'data:image/svg+xml;base64,' . base64_encode(
''
. ''
. ''
. ' '
. ' '
. ' '
. ' '
. ' '
. ''
. ' '
. ' '
. ' '
. ''
. ' '
. ' '
. ' '
. ' '
);
$this->hook_suffix = add_menu_page(
__( '42A AEL - Agentic Enablement Layer', 'ael-42a' ),
__( '42A AEL', 'ael-42a' ),
'manage_options',
self::SLUG,
array( $this, 'render_page' ),
$menu_icon,
81
);
}
/**
* Enqueue admin assets only on our page.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_assets( $hook ) {
if ( $hook !== $this->hook_suffix ) {
return;
}
wp_enqueue_style(
'ael42a-admin',
AEL42A_URL . 'admin/css/admin.css',
array(),
AEL42A_VERSION
);
wp_enqueue_script(
'ael42a-admin',
AEL42A_URL . 'admin/js/admin.js',
array(),
AEL42A_VERSION,
true
);
wp_localize_script(
'ael42a-admin',
'ael42a',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE ),
'i18n' => array(
'running' => __( 'Running checks...', 'ael-42a' ),
'checkStatus' => __( 'Status', 'ael-42a' ),
'checkName' => __( 'Check', 'ael-42a' ),
'checkDetails' => __( 'Details', 'ael-42a' ),
'copied' => __( 'Copied!', 'ael-42a' ),
'copy' => __( 'Copy', 'ael-42a' ),
'error' => __( 'Something went wrong. Please try again.', 'ael-42a' ),
'saved' => __( 'Settings saved.', 'ael-42a' ),
'confirmDisconnect' => __( 'Disconnect this site from 42A? The vitals report will no longer be available.', 'ael-42a' ),
),
)
);
}
/**
* Render the page shell + active tab view.
*/
public function render_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to access this page.', 'ael-42a' ) );
}
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'agent-layer'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only routing.
if ( ! in_array( $tab, array( 'agent-layer', 'visibility', 'vitals', 'account', 'contact' ), true ) ) {
$tab = 'agent-layer';
}
$base_url = admin_url( 'admin.php?page=' . self::SLUG );
echo '
';
// Branded header: static inline 42A logo mark (no remote assets in
// wp-admin). Kept as the page
so WP core relocates admin notices
// directly below it.
echo '';
if ( isset( $_GET['connected'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only notice.
echo '' . esc_html__( 'Site connected to 42A. Domain verification is in progress.', 'ael-42a' ) . '
';
}
if ( isset( $_GET['connect_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only notice.
$connect_error = sanitize_text_field( wp_unslash( $_GET['connect_error'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only notice.
$connect_error = sanitize_text_field( rawurldecode( $connect_error ) );
echo '' .
esc_html( sprintf( /* translators: %s: error message. */ __( 'Connection failed: %s', 'ael-42a' ), $connect_error ) ) .
'
';
}
echo '';
printf(
'%s ',
esc_url( add_query_arg( 'tab', 'agent-layer', $base_url ) ),
'agent-layer' === $tab ? 'nav-tab-active' : '',
esc_html__( 'Agent Layer', 'ael-42a' )
);
printf(
'%s ',
esc_url( add_query_arg( 'tab', 'visibility', $base_url ) ),
'visibility' === $tab ? 'nav-tab-active' : '',
esc_html__( 'AI Visibility', 'ael-42a' )
);
printf(
'%s ',
esc_url( add_query_arg( 'tab', 'vitals', $base_url ) ),
'vitals' === $tab ? 'nav-tab-active' : '',
esc_html__( 'GEO Vitals', 'ael-42a' )
);
printf(
'%s ',
esc_url( add_query_arg( 'tab', 'account', $base_url ) ),
'account' === $tab ? 'nav-tab-active' : '',
esc_html__( 'Account', 'ael-42a' )
);
printf(
'%s ',
esc_url( add_query_arg( 'tab', 'contact', $base_url ) ),
'contact' === $tab ? 'nav-tab-active' : '',
esc_html__( 'Contact', 'ael-42a' )
);
echo ' ';
// Two-column layout: tab content on the left; while not connected, a
// blurred GEO teaser on the right nudging toward the connect flow.
echo '';
echo '
';
if ( 'vitals' === $tab ) {
$this->render_vitals_tab();
} elseif ( 'visibility' === $tab ) {
$this->render_visibility_tab();
} elseif ( 'account' === $tab ) {
$this->render_account_tab();
} elseif ( 'contact' === $tab ) {
$this->render_contact_tab();
} else {
$this->render_agent_layer_tab();
}
echo '
';
if ( ! $this->connect->is_connected() ) {
$this->render_geo_teaser();
} elseif ( 'agent-layer' === $tab ) {
$this->render_visibility_sidebar( $base_url );
}
echo '
';
echo ' ';
}
/**
* Blurred GEO teaser sidebar (not-connected state only).
*/
private function render_geo_teaser() {
$connect_url = $this->connect->connect_url();
require AEL42A_DIR . 'admin/views/geo-teaser.php';
}
/**
* Live AI-visibility sidebar on the Agent Layer tab (connected state) -
* same slot the blurred teaser occupies before connecting.
*
* @param string $base_url Plugin settings page base URL.
*/
private function render_visibility_sidebar( $base_url ) {
$result = $this->connect->get_visibility_cached();
$visibility = is_wp_error( $result ) ? null : $result;
$visibility_tab_url = add_query_arg( 'tab', 'visibility', $base_url );
$dashboard_url = add_query_arg(
array(
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'admin',
'utm_campaign' => 'ael-sidebar',
),
$this->connect->api()->app_url() . '/dashboard'
);
require AEL42A_DIR . 'admin/views/visibility-sidebar.php';
}
/**
* Agent Layer tab.
*/
private function render_agent_layer_tab() {
$telemetry = new Telemetry();
$sample_post = get_posts(
array(
'post_type' => array( 'page', 'post' ),
'post_status' => 'publish',
'posts_per_page' => 1,
)
);
$markdown_example = ! empty( $sample_post )
? add_query_arg( 'format', 'markdown', get_permalink( $sample_post[0] ) )
: home_url( '/?format=markdown' );
$endpoints = array(
array(
'label' => __( 'Markdown (any page)', 'ael-42a' ),
'url' => $markdown_example,
),
array(
'label' => 'llms.txt',
'url' => home_url( '/llms.txt' ),
),
array(
'label' => 'llms-full.txt',
'url' => home_url( '/llms-full.txt' ),
),
array(
'label' => __( 'MCP endpoint', 'ael-42a' ),
'url' => rest_url( '42a/v1/mcp' ),
),
array(
'label' => __( 'MCP discovery', 'ael-42a' ),
'url' => home_url( '/.well-known/mcp.json' ),
),
);
$traffic = $telemetry->recent_days( 7 );
$bot_traffic = $telemetry->bot_traffic_summary( 7 );
$telemetry_opt_in = $telemetry->is_opted_in();
$full_limit = (int) get_option( 'ael42a_full_limit', Llms_Txt::FULL_ITEM_LIMIT );
require AEL42A_DIR . 'admin/views/agent-layer.php';
}
/**
* AI Visibility tab: read-only dashboard mirror (stats, charts, projects).
*/
private function render_visibility_tab() {
$connect = $this->connect;
$connected = $connect->is_connected();
$visibility = null;
$visibility_error = '';
if ( $connected ) {
$result = $connect->get_visibility_cached();
if ( is_wp_error( $result ) ) {
$visibility_error = $result->get_error_message();
} else {
$visibility = $result;
}
}
$connect_url = $connect->connect_url();
$app_url = $connect->api()->app_url();
$dashboard_url = add_query_arg(
array(
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'admin',
'utm_campaign' => 'ael-visibility',
),
$app_url . '/dashboard'
);
require AEL42A_DIR . 'admin/views/ai-visibility.php';
}
/**
* Account tab: 42A connection management (connect, status, disconnect).
*/
private function render_account_tab() {
$connect = $this->connect;
$connected = $connect->is_connected();
$snapshot = $connect->get_site_snapshot();
$connect_url = $connect->connect_url();
$app_url = $connect->api()->app_url();
$site_origin = home_url( '/' );
$plugin_version = AEL42A_VERSION;
require AEL42A_DIR . 'admin/views/account.php';
}
/**
* Contact tab: direct support email + a client-side mailto composer.
*
* No form data ever touches the server or the 42A platform - the
* "Compose email" button builds a mailto: link in the browser and hands
* off to the admin's own mail client, so it works on every host with no
* SMTP configuration and no risk of silent delivery failures.
*/
private function render_contact_tab() {
$support_email = 'support@42a.ai';
/* translators: %s: site name. */
$default_subject = sprintf( __( '[42A] Support request from %s', 'ael-42a' ), get_bloginfo( 'name' ) );
// Diagnostic footer appended to the email body so support has the basics
// without a back-and-forth. Built server-side (translated labels) and
// passed to JS, which URL-encodes it into the mailto body.
$snapshot = $this->connect->get_site_snapshot();
$org = isset( $snapshot['org'] ) && is_array( $snapshot['org'] ) ? $snapshot['org'] : array();
$brand = isset( $snapshot['brand'] ) && is_array( $snapshot['brand'] ) ? $snapshot['brand'] : array();
$context_lines = array(
/* translators: %s: site URL. */
sprintf( __( 'Site: %s', 'ael-42a' ), home_url( '/' ) ),
/* translators: %s: plugin version. */
sprintf( __( 'Plugin version: %s', 'ael-42a' ), AEL42A_VERSION ),
/* translators: %s: WordPress version. */
sprintf( __( 'WordPress version: %s', 'ael-42a' ), get_bloginfo( 'version' ) ),
);
if ( $this->connect->is_connected() ) {
if ( ! empty( $org['name'] ) ) {
/* translators: %s: 42A organization name. */
$context_lines[] = sprintf( __( 'Organization: %s', 'ael-42a' ), (string) $org['name'] );
}
if ( ! empty( $brand['name'] ) ) {
/* translators: %s: 42A brand name. */
$context_lines[] = sprintf( __( 'Brand: %s', 'ael-42a' ), (string) $brand['name'] );
}
}
$context_block = __( 'Sent from the 42A WordPress plugin', 'ael-42a' ) . "\n" . implode( "\n", $context_lines );
require AEL42A_DIR . 'admin/views/contact.php';
}
/**
* GEO Vitals tab.
*/
private function render_vitals_tab() {
$connect = $this->connect;
$connected = $connect->is_connected();
$snapshot = $connect->get_site_snapshot();
$vitals = null;
$vitals_error = '';
if ( $connected ) {
$result = $connect->get_vitals_cached();
if ( is_wp_error( $result ) ) {
$vitals_error = $result->get_error_message();
} else {
$vitals = $result;
}
}
$connect_url = $connect->connect_url();
$app_url = $connect->api()->app_url();
$upsell_url = add_query_arg(
array(
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'admin',
'utm_campaign' => 'ael-vitals',
),
$app_url
);
require AEL42A_DIR . 'admin/views/vitals.php';
}
// ----------------------------------------------------------------- AJAX.
/**
* Guard shared by every AJAX handler.
*/
private function guard_ajax() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ael-42a' ) ), 403 );
}
check_ajax_referer( self::NONCE, 'nonce' );
}
/**
* Run local vitals checks.
*/
public function ajax_run_checks() {
$this->guard_ajax();
$force = isset( $_POST['force'] ) && '1' === $_POST['force']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing -- compared against fixed string; nonce checked in guard_ajax().
$vitals = new Local_Vitals();
wp_send_json_success(
array(
'checks' => $vitals->run_checks( $force ),
)
);
}
/**
* Clear + refetch the connected vitals report.
*/
public function ajax_refresh_vitals() {
$this->guard_ajax();
delete_transient( Cache::VITALS );
$this->connect->refresh_site_snapshot();
// The snapshot refresh may have discovered a platform-side disconnect
// and wiped local state - report success so the page reloads into the
// reconnect flow instead of showing a dead-end error.
if ( ! $this->connect->is_connected() ) {
wp_send_json_success( array( 'disconnected' => true ) );
}
$result = $this->connect->get_vitals_cached( true );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'vitals' => $result ) );
}
/**
* Disconnect from 42A.
*/
public function ajax_disconnect() {
$this->guard_ajax();
$this->connect->disconnect();
wp_send_json_success( array( 'message' => __( 'Disconnected from 42A.', 'ael-42a' ) ) );
}
/**
* Save settings (telemetry opt-in, llms-full item count).
*/
public function ajax_save_settings() {
$this->guard_ajax();
$opt_in = isset( $_POST['telemetry_opt_in'] ) && '1' === $_POST['telemetry_opt_in'] ? '1' : '0'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing -- compared against fixed string; nonce checked in guard_ajax().
update_option( Telemetry::OPT_IN, $opt_in, false );
if ( isset( $_POST['full_limit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked in guard_ajax().
$limit = max( 1, min( Llms_Txt::FULL_ITEM_LIMIT, (int) $_POST['full_limit'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing -- cast to int; nonce checked in guard_ajax().
update_option( 'ael42a_full_limit', $limit, false );
delete_transient( Cache::LLMS_FULL );
}
wp_send_json_success( array( 'message' => __( 'Settings saved.', 'ael-42a' ) ) );
}
}