'GET', 'callback' => [ __CLASS__, 'rest_assignments' ], 'permission_callback' => function () { return current_user_can( 'read' ); // any logged-in user; the // join filters by their id }, 'args' => [ 'since' => [ 'type' => 'string', 'required' => false ], ], ] ); // LEAF half — manual fetch trigger (callable by admin from the page). register_rest_route( 'sa-orch/v1', '/leaf/fetch-now', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'rest_fetch_now' ], 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ] ); } public static function register_menu() { add_submenu_page( SA_Orch_Admin::PARENT_SLUG, 'Remote Roots', 'Remote Roots', SA_Orch_Admin::CAP, self::SUBMENU_SLUG, [ __CLASS__, 'render_page' ] ); } /* =================================================================== * ROOT HALF — assignments endpoint * =================================================================== */ public static function rest_assignments( WP_REST_Request $req ) { global $wpdb; $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return new WP_REST_Response( [ 'error' => 'unauthenticated', 'message' => 'No authenticated user resolved.', ], 401 ); } // Filter by 'since' (last_modified > since); optional. $since = $req->get_param( 'since' ); $where_since = ''; $params = [ $user_id ]; if ( $since && preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/', (string) $since ) ) { $where_since = " AND t.updated_at > %s"; $params[] = str_replace( 'T', ' ', rtrim( (string) $since, 'Z' ) ); } // Fluent Boards assignment join: fbs_relations.object_type = 'task_assignee', // object_id = task_id, foreign_id = wp user_id. $sql = " SELECT t.id AS root_task_id, t.board_id, t.title, t.description, t.stage_id, t.status, t.priority, t.created_at, t.updated_at, t.last_completed_at, b.title AS board_title, bt.title AS stage_title FROM {$wpdb->prefix}fbs_relations r JOIN {$wpdb->prefix}fbs_tasks t ON r.object_id = t.id LEFT JOIN {$wpdb->prefix}fbs_boards b ON t.board_id = b.id LEFT JOIN {$wpdb->prefix}fbs_board_terms bt ON t.stage_id = bt.id WHERE r.object_type = 'task_assignee' AND r.foreign_id = %d AND t.archived_at IS NULL {$where_since} ORDER BY t.updated_at DESC LIMIT 100 "; $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A ); if ( $rows === null ) { return new WP_REST_Response( [ 'error' => 'query_failed', 'message' => 'Database query failed: ' . $wpdb->last_error, ], 500 ); } // Trim description for transport efficiency. Full description fetched // on demand by leaf when dev clicks into a specific assignment. $out = []; foreach ( $rows as $r ) { $desc = (string) $r['description']; $desc_excerpt = function_exists( 'mb_substr' ) ? mb_substr( wp_strip_all_tags( $desc ), 0, 800 ) : substr( wp_strip_all_tags( $desc ), 0, 800 ); $out[] = [ 'root_task_id' => (int) $r['root_task_id'], 'board_id' => (int) $r['board_id'], 'board_title' => (string) $r['board_title'], 'stage_id' => (int) $r['stage_id'], 'stage_title' => (string) $r['stage_title'], 'status' => (string) $r['status'], 'priority' => (string) $r['priority'], 'title' => (string) $r['title'], 'description_excerpt' => $desc_excerpt, 'created_at' => (string) $r['created_at'], 'updated_at' => (string) $r['updated_at'], 'last_completed_at' => $r['last_completed_at'], ]; } return new WP_REST_Response( [ 'actor' => [ 'wp_user_id' => $user_id, 'login' => ( $u = get_user_by( 'id', $user_id ) ) ? $u->user_login : null, ], 'count' => count( $out ), 'fetched_at' => current_time( 'mysql', true ), 'assignments' => $out, ], 200 ); } /* =================================================================== * LEAF HALF — fetch logic * =================================================================== */ /** * Fetch assignments from the configured root. * Returns either a parsed payload or a WP_Error. */ public static function fetch_remote_assignments() { $cfg = get_option( self::OPTION_KEY, [] ); if ( empty( $cfg['url'] ) || empty( $cfg['app_user'] ) || empty( $cfg['app_password'] ) ) { return new WP_Error( 'not_configured', 'Remote root not configured. Set URL, username, and application password first.' ); } $url = rtrim( (string) $cfg['url'], '/' ) . '/wp-json/sa-orch/v1/root/assignments'; $auth_header = 'Basic ' . base64_encode( $cfg['app_user'] . ':' . str_replace( ' ', '', $cfg['app_password'] ) ); $resp = wp_remote_get( $url, [ 'headers' => [ 'Authorization' => $auth_header ], 'timeout' => self::REST_TIMEOUT, ] ); if ( is_wp_error( $resp ) ) { return $resp; } $code = (int) wp_remote_retrieve_response_code( $resp ); $body = (string) wp_remote_retrieve_body( $resp ); if ( $code < 200 || $code >= 300 ) { return new WP_Error( 'remote_error', "HTTP $code from root: " . substr( $body, 0, 240 ) ); } $payload = json_decode( $body, true ); if ( ! is_array( $payload ) ) { return new WP_Error( 'parse_error', 'Root response was not valid JSON: ' . substr( $body, 0, 240 ) ); } // Cache the last fetch in option for the admin page render. update_option( 'sa_orch_remote_root_last_fetch', [ 'at' => current_time( 'mysql', true ), 'payload' => $payload, ], false /* not autoloaded */ ); return $payload; } public static function rest_fetch_now( WP_REST_Request $req ) { $r = self::fetch_remote_assignments(); if ( is_wp_error( $r ) ) { return new WP_REST_Response( [ 'error' => $r->get_error_code(), 'message' => $r->get_error_message(), ], 502 ); } return new WP_REST_Response( $r, 200 ); } /* =================================================================== * LEAF HALF — admin page render * =================================================================== */ public static function render_page() { if ( ! current_user_can( SA_Orch_Admin::CAP ) ) wp_die( 'Insufficient permissions.' ); $error = ''; $notice = ''; // Handle save if ( isset( $_POST['sa_orch_save_root'] ) && check_admin_referer( 'sa_orch_save_root' ) ) { $cfg = [ 'url' => esc_url_raw( trim( wp_unslash( $_POST['root_url'] ?? '' ) ) ), 'app_user' => sanitize_text_field( wp_unslash( $_POST['app_user'] ?? '' ) ), 'app_password' => trim( wp_unslash( $_POST['app_password'] ?? '' ) ), ]; update_option( self::OPTION_KEY, $cfg, false ); $notice = 'Remote root saved.'; } // Handle fetch button if ( isset( $_POST['sa_orch_fetch_now'] ) && check_admin_referer( 'sa_orch_fetch_now' ) ) { $r = self::fetch_remote_assignments(); if ( is_wp_error( $r ) ) { $error = 'Fetch failed: ' . $r->get_error_message(); } else { $notice = 'Fetched ' . (int) $r['count'] . ' assignment(s).'; } } $cfg = get_option( self::OPTION_KEY, [] ); $last_fetch = get_option( 'sa_orch_remote_root_last_fetch', [] ); $payload = $last_fetch['payload'] ?? null; $assignments = is_array( $payload ) ? ( $payload['assignments'] ?? [] ) : []; echo '
'; echo '

Remote Roots

'; echo '

Pull assignments from a remote SA-Orchestration root (e.g., the dev root). Leaf-initiated; outbound HTTPS only.

'; if ( $error !== '' ) echo '

' . esc_html( $error ) . '

'; if ( $notice !== '' ) echo '

' . esc_html( $notice ) . '

'; // Configuration form echo '

Configuration (single root, MVP)

'; echo '
'; wp_nonce_field( 'sa_orch_save_root' ); echo ''; echo ''; echo ''; echo ''; echo '
Root URL
App Username
App Password'; echo '

Generate at {root}/wp-admin/profile.php → Application Passwords. Stored in wp_options (not autoloaded). Revoke from same page anytime.

'; echo '

'; echo '
'; // Fetch button echo '

Pull Assignments

'; echo '
'; wp_nonce_field( 'sa_orch_fetch_now' ); echo '

'; if ( ! empty( $last_fetch['at'] ) ) { echo ' Last fetched: ' . esc_html( $last_fetch['at'] ) . ' UTC'; } echo '

'; echo '
'; // Render assignments if ( $payload && isset( $payload['count'] ) ) { $actor = $payload['actor'] ?? []; echo '

Assignments (' . (int) $payload['count'] . ')

'; echo '

Authenticated to root as: ' . esc_html( $actor['login'] ?? '' ) . ' (wp_user_id=' . (int) ( $actor['wp_user_id'] ?? 0 ) . ')

'; if ( empty( $assignments ) ) { echo '

No assignments found for this user on the configured root.

'; } else { echo ''; echo ''; echo ''; foreach ( $assignments as $a ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ( ! empty( $a['description_excerpt'] ) ) { echo ''; } } echo '
Root Task IDBoardStageStatusPriorityTitleUpdated
' . (int) $a['root_task_id'] . '' . esc_html( $a['board_title'] ) . ' (#' . (int) $a['board_id'] . ')' . esc_html( $a['stage_title'] ) . '' . esc_html( $a['status'] ) . '' . esc_html( $a['priority'] ) . '' . esc_html( $a['title'] ) . '' . esc_html( $a['updated_at'] ) . '
excerpt: ' . esc_html( substr( $a['description_excerpt'], 0, 400 ) ) . ( strlen( $a['description_excerpt'] ) > 400 ? '...' : '' ) . '
'; } } echo '
'; } }