>|null */ private ?array $live_connections = null; /** * The screen id `add_menu_page()` gave this page, or '' before it ran. * * Taken from core rather than written out as `toplevel_page_albert`, which * is what the enqueue guard compared against. That literal is core's own * derivation from the menu slug, so restating it is a second copy of a rule * this class does not own: rename the slug and the assets stop loading, * silently, on a page that still renders. * * @since 1.4.0 * @var string */ private string $screen_id = ''; /** * Constructor. * * @param LoggingRepository $logging_repository Ability log repository. * * @since 1.1.0 */ public function __construct( LoggingRepository $logging_repository ) { $this->logging_repository = $logging_repository; } /** * Register WordPress hooks. * * @return void * @since 1.0.0 */ public function register_hooks(): void { add_action( 'wp_ajax_' . Attention::DISMISS_ACTION, [ $this, 'handle_dismiss_attention' ] ); add_action( 'admin_menu', [ $this, 'add_menu_pages' ], Menu::POSITION_DASHBOARD ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Add top-level menu and dashboard page. * * Creates the top-level "Albert" menu with Dashboard as the default page, * then adds "Dashboard" as the first submenu (which replaces the auto-generated one). * * @return void * @since 1.0.0 */ public function add_menu_pages(): void { // Add top-level menu (shows Dashboard by default). $this->screen_id = (string) add_menu_page( __( 'Albert Dashboard', 'albert-ai-butler' ), __( 'Albert', 'albert-ai-butler' ), 'manage_options', $this->page_slug, [ $this, 'render_dashboard_page' ], 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCAyNTYgMjU2Ij48cGF0aCBmaWxsPSIjYTdhYWFkIiBkPSJNNjkuNCAxNC40Yy0uOS44LTEgMy40LTEgMTguOSAwIDE2LjcuMSAxOC4xIDEuMSAxOSAuNi42IDEuNSAxIDIgMXM5LjgtMi4zIDIwLjctNWwxOS44LTUgMi44IDIuOWM3LjggOC4xIDE5LjUgOC4yIDI3LjMuNWwzLjQtMy40IDIwLjEgNS4xYzEyLjggMy4zIDIwLjUgNC45IDIxLjQgNC42LjctLjIgMS42LTEuMSAyLTIgLjMtLjguNi04LjkuNi0xNy45IDAtMTcuOS0uMi0xOS40LTMuMi0xOS43LS45LS4xLTEwLjUgMi0yMS4zIDQuOGwtMTkuNyA1LTIuOC0zLjFjLTMuOS00LjEtNy45LTUuOC0xMy44LTUuOS01LjcgMC0xMC40IDItMTQuMSA2LjJsLTIuNSAyLjgtMTkuNC00LjljLTEwLjYtMi44LTIwLTUtMjAuOS01LS45LjEtMiAuNS0yLjUgMS4xeiIvPjxwYXRoIGZpbGw9IiNhN2FhYWQiIGQ9Ik0yNS4yIDUyLjRjLTYgMy41LTExLjUgNi44LTEyLjIgNy40LTMuMSAyLjgtMy0xLjctMyA5MS4xIDAgNjMuNC4yIDg3LjEuNyA4OC4yIDEuNyAzLjYtNSAzLjQgMTE3LjMgMy40czExNS43LjIgMTE3LjMtMy40Yy41LTEuMS43LTI0LjIuNy04NS43IDAtODIuOCAwLTg0LjItMS4yLTg2LjYtMS4zLTIuNi0yLTMuMy0xNS40LTEzLjgtNC45LTMuOS05LjEtNi44LTkuMy02LjYtLjIuMi0xOS40IDM3LjEtNDIuNyA4MS45LTIzLjIgNDQuOC00My44IDg0LjMtNDUuNyA4Ny44bC0zLjQgNi4zLTQuMS03LjktNDUuOC04Ny44Yy0yMi45LTQzLjktNDEuNy04MC4xLTQyLTgwLjMtLjEtLjEtNS4yIDIuNS0xMS4yIDZ6Ii8+PHBhdGggZmlsbD0iI2E3YWFhZCIgZD0iTTEyNS42IDc2LjdjLTcuOSAxLjUtMTMuNCA5LjgtMTEuNyAxNy44IDIuOCAxMi45IDE5LjQgMTYuNiAyNyA2IDguMS0xMS4yLTEuNy0yNi4zLTE1LjMtMjMuOHpNMTIzLjYgMTI4YTE0LjQgMTQuNCAwIDAgMC04LjIgNy41Yy0zLjEgNi4zLTEuOCAxMy42IDMuMyAxOC4xIDMuMSAyLjcgNS43IDMuNiAxMCAzLjYgNC40IDAgNy42LTEuMiAxMC4zLTMuOSA5LjctOS4zIDQuMS0yNS4yLTkuMy0yNi0yLjQtLjEtNC43LjEtNi4xLjd6Ii8+PC9zdmc+', 80 ); // Add Dashboard submenu (replaces auto-generated first submenu). add_submenu_page( $this->parent_slug, __( 'Dashboard', 'albert-ai-butler' ), __( 'Dashboard', 'albert-ai-butler' ), 'manage_options', $this->page_slug, [ $this, 'render_dashboard_page' ] ); } /** * Enqueue dashboard assets. * * @param string $hook Current admin page hook. * * @return void * @since 1.0.0 */ public function enqueue_assets( string $hook ): void { // Only load on our dashboard page, identified by the screen id core // handed back from add_menu_page() rather than by a copy of it. if ( $this->screen_id === '' || $hook !== $this->screen_id ) { return; } wp_enqueue_style( 'albert-admin', ALBERT_PLUGIN_URL . 'assets/css/admin-settings.css', [ Assets::PRIMITIVES_HANDLE ], Assets::version( 'assets/css/admin-settings.css' ) ); wp_enqueue_script( 'albert-admin-utils', ALBERT_PLUGIN_URL . 'assets/js/albert-admin-utils.js', [], Assets::version( 'assets/js/albert-admin-utils.js' ), true ); // The same script Connections and Settings load, not a Dashboard-specific // one. `admin-dashboard.js` used to exist purely to wire a copy button, // against its own `.albert-copy-btn` class, duplicating the handler in // `admin-settings.js` that every other screen already used. One endpoint // field, one class, one handler. wp_enqueue_script( 'albert-admin', ALBERT_PLUGIN_URL . 'assets/js/admin-settings.js', [ 'albert-admin-utils' ], Assets::version( 'assets/js/admin-settings.js' ), true ); wp_localize_script( 'albert-admin', 'albertAdmin', [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'albert_oauth_nonce' ), 'dismissNonce' => wp_create_nonce( Attention::DISMISS_ACTION ), 'i18n' => [ 'copied' => __( 'Copied!', 'albert-ai-butler' ), 'copyFailed' => __( 'Copy failed', 'albert-ai-butler' ), ], ] ); // The onboarding checklist's "Choose users" button opens the Connections // screen's picker, so the dashboard loads the same script and the same // localised data rather than a copy of either. UserPickerModal::enqueue( 'dashboard' ); ConnectDialog::enqueue(); } /** * Render dashboard page. * * Two states, not one layout with conditional cards (handoff §8.1): an * unfinished setup is a task list, a finished one is a status readout, and * they answer different questions. * * There are deliberately no buttons in the header. "Manage abilities" and * "View connections" are navigation, not actions; the submenu and the tab * row already reach both, and on a finished setup there is no single next * action to promote. The only filled button on the screen belongs to the * current onboarding step, where there genuinely is one thing to do next. * * @return void * @since 1.0.0 */ public function render_dashboard_page(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'albert-ai-butler' ) ); } $has_allowed_users = AllowedUsers::has_any(); $active_connections = count( $this->get_live_connections() ); $has_connections = $active_connections > 0; $setup_complete = $has_allowed_users && $has_connections; $abilities = $this->get_ability_counts(); $mcp_endpoint = McpServer::get_endpoint_url(); ?>
ago( (int) $mine['created_ts'] ) ) ); ?>
| render_status_dot( $activity['status'] ); ?> |
|
, 2: closing wrapping a filter name */
esc_html__( 'An %1$salbert/mcp/external_url%2$s filter returned an address Albert could not use, so this is your site\'s own address instead.', 'albert-ai-butler' ),
'',
''
);
} else {
printf(
/* translators: 1: opening , 2: closing wrapping a filter name */
esc_html__( 'This address comes from the %1$salbert/mcp/external_url%2$s filter, not from your site address.', 'albert-ai-butler' ),
'',
''
);
}
?>