Spaces:
Sleeping
Sleeping
Commit ·
5a14210
1
Parent(s): 93528c6
Pre-stage: roll forward sa-orchestration/ with v0.8.0 MVP leaf-link
Browse filesAdds the new class-sa-leaf-link.php (Board #40 MVP — leaf↔root
assignments link) and updates the bootstrap to register its REST
routes + admin menu. Source-of-truth: SA-Orchestration source repo.
Pre-staged copy here stays current with the source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sa-orchestration/includes/class-sa-leaf-link.php
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
defined( 'ABSPATH' ) || exit;
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Leaf↔Root Link — v0.8.0 MVP
|
| 6 |
+
*
|
| 7 |
+
* Two halves in one class, ship together in every install:
|
| 8 |
+
*
|
| 9 |
+
* ROOT half — REST endpoint /sa-orch/v1/root/assignments
|
| 10 |
+
* Returns Fluent Boards tasks where the authenticated user is
|
| 11 |
+
* an assignee (via fbs_relations object_type='task_assignee').
|
| 12 |
+
* Auth: WP application password (Basic auth) or session nonce.
|
| 13 |
+
*
|
| 14 |
+
* LEAF half — Admin page (SA-Orchestration → Remote Roots)
|
| 15 |
+
* Stores ONE remote root config in wp_options (URL + app
|
| 16 |
+
* password). Manual "Fetch Now" button calls the configured
|
| 17 |
+
* root's /assignments endpoint and renders results in a
|
| 18 |
+
* table on the same admin page.
|
| 19 |
+
*
|
| 20 |
+
* MVP scope (locked):
|
| 21 |
+
* - One root per leaf (multi-root deferred to post-MVP)
|
| 22 |
+
* - No new tables — single wp_option for config
|
| 23 |
+
* - Manual fetch only; no cadence, heartbeat, or cron
|
| 24 |
+
* - No Fluent Support ticket creation; just list assignments on screen
|
| 25 |
+
* - No outbound curated updates; bidirectional sync is post-MVP
|
| 26 |
+
*
|
| 27 |
+
* The closed loop: developer self-assigns on Fluent Boards on the dev root
|
| 28 |
+
* → leaf admin clicks "Fetch Now" → assignment appears in the leaf's
|
| 29 |
+
* Remote Roots admin page.
|
| 30 |
+
*
|
| 31 |
+
* Doctrine: leaf-initiated, always up; the dev root never reaches into
|
| 32 |
+
* the leaf. Per Board #40 invariants.
|
| 33 |
+
*/
|
| 34 |
+
class SA_Orch_Leaf_Link {
|
| 35 |
+
|
| 36 |
+
const SUBMENU_SLUG = 'sa-orchestration-remote-roots';
|
| 37 |
+
const OPTION_KEY = 'sa_orch_remote_root'; // single-root MVP storage
|
| 38 |
+
const REST_TIMEOUT = 15;
|
| 39 |
+
|
| 40 |
+
/* ===================================================================
|
| 41 |
+
* Hooks
|
| 42 |
+
* =================================================================== */
|
| 43 |
+
|
| 44 |
+
public static function register_routes() {
|
| 45 |
+
// ROOT half — serves assignments to authenticated callers.
|
| 46 |
+
register_rest_route( 'sa-orch/v1', '/root/assignments', [
|
| 47 |
+
'methods' => 'GET',
|
| 48 |
+
'callback' => [ __CLASS__, 'rest_assignments' ],
|
| 49 |
+
'permission_callback' => function () {
|
| 50 |
+
return current_user_can( 'read' ); // any logged-in user; the
|
| 51 |
+
// join filters by their id
|
| 52 |
+
},
|
| 53 |
+
'args' => [
|
| 54 |
+
'since' => [ 'type' => 'string', 'required' => false ],
|
| 55 |
+
],
|
| 56 |
+
] );
|
| 57 |
+
|
| 58 |
+
// LEAF half — manual fetch trigger (callable by admin from the page).
|
| 59 |
+
register_rest_route( 'sa-orch/v1', '/leaf/fetch-now', [
|
| 60 |
+
'methods' => 'POST',
|
| 61 |
+
'callback' => [ __CLASS__, 'rest_fetch_now' ],
|
| 62 |
+
'permission_callback' => function () {
|
| 63 |
+
return current_user_can( 'manage_options' );
|
| 64 |
+
},
|
| 65 |
+
] );
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
public static function register_menu() {
|
| 69 |
+
add_submenu_page(
|
| 70 |
+
SA_Orch_Admin::PARENT_SLUG,
|
| 71 |
+
'Remote Roots',
|
| 72 |
+
'Remote Roots',
|
| 73 |
+
SA_Orch_Admin::CAP,
|
| 74 |
+
self::SUBMENU_SLUG,
|
| 75 |
+
[ __CLASS__, 'render_page' ]
|
| 76 |
+
);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* ===================================================================
|
| 80 |
+
* ROOT HALF — assignments endpoint
|
| 81 |
+
* =================================================================== */
|
| 82 |
+
|
| 83 |
+
public static function rest_assignments( WP_REST_Request $req ) {
|
| 84 |
+
global $wpdb;
|
| 85 |
+
|
| 86 |
+
$user_id = get_current_user_id();
|
| 87 |
+
if ( $user_id <= 0 ) {
|
| 88 |
+
return new WP_REST_Response( [
|
| 89 |
+
'error' => 'unauthenticated',
|
| 90 |
+
'message' => 'No authenticated user resolved.',
|
| 91 |
+
], 401 );
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Filter by 'since' (last_modified > since); optional.
|
| 95 |
+
$since = $req->get_param( 'since' );
|
| 96 |
+
$where_since = '';
|
| 97 |
+
$params = [ $user_id ];
|
| 98 |
+
if ( $since && preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$/', (string) $since ) ) {
|
| 99 |
+
$where_since = " AND t.updated_at > %s";
|
| 100 |
+
$params[] = str_replace( 'T', ' ', rtrim( (string) $since, 'Z' ) );
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Fluent Boards assignment join: fbs_relations.object_type = 'task_assignee',
|
| 104 |
+
// object_id = task_id, foreign_id = wp user_id.
|
| 105 |
+
$sql = "
|
| 106 |
+
SELECT
|
| 107 |
+
t.id AS root_task_id,
|
| 108 |
+
t.board_id,
|
| 109 |
+
t.title,
|
| 110 |
+
t.description,
|
| 111 |
+
t.stage_id,
|
| 112 |
+
t.status,
|
| 113 |
+
t.priority,
|
| 114 |
+
t.created_at,
|
| 115 |
+
t.updated_at,
|
| 116 |
+
t.last_completed_at,
|
| 117 |
+
b.title AS board_title,
|
| 118 |
+
bt.title AS stage_title
|
| 119 |
+
FROM {$wpdb->prefix}fbs_relations r
|
| 120 |
+
JOIN {$wpdb->prefix}fbs_tasks t ON r.object_id = t.id
|
| 121 |
+
LEFT JOIN {$wpdb->prefix}fbs_boards b ON t.board_id = b.id
|
| 122 |
+
LEFT JOIN {$wpdb->prefix}fbs_board_terms bt ON t.stage_id = bt.id
|
| 123 |
+
WHERE r.object_type = 'task_assignee'
|
| 124 |
+
AND r.foreign_id = %d
|
| 125 |
+
AND t.archived_at IS NULL
|
| 126 |
+
{$where_since}
|
| 127 |
+
ORDER BY t.updated_at DESC
|
| 128 |
+
LIMIT 100
|
| 129 |
+
";
|
| 130 |
+
|
| 131 |
+
$rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A );
|
| 132 |
+
if ( $rows === null ) {
|
| 133 |
+
return new WP_REST_Response( [
|
| 134 |
+
'error' => 'query_failed',
|
| 135 |
+
'message' => 'Database query failed: ' . $wpdb->last_error,
|
| 136 |
+
], 500 );
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Trim description for transport efficiency. Full description fetched
|
| 140 |
+
// on demand by leaf when dev clicks into a specific assignment.
|
| 141 |
+
$out = [];
|
| 142 |
+
foreach ( $rows as $r ) {
|
| 143 |
+
$desc = (string) $r['description'];
|
| 144 |
+
$desc_excerpt = function_exists( 'mb_substr' )
|
| 145 |
+
? mb_substr( wp_strip_all_tags( $desc ), 0, 800 )
|
| 146 |
+
: substr( wp_strip_all_tags( $desc ), 0, 800 );
|
| 147 |
+
$out[] = [
|
| 148 |
+
'root_task_id' => (int) $r['root_task_id'],
|
| 149 |
+
'board_id' => (int) $r['board_id'],
|
| 150 |
+
'board_title' => (string) $r['board_title'],
|
| 151 |
+
'stage_id' => (int) $r['stage_id'],
|
| 152 |
+
'stage_title' => (string) $r['stage_title'],
|
| 153 |
+
'status' => (string) $r['status'],
|
| 154 |
+
'priority' => (string) $r['priority'],
|
| 155 |
+
'title' => (string) $r['title'],
|
| 156 |
+
'description_excerpt' => $desc_excerpt,
|
| 157 |
+
'created_at' => (string) $r['created_at'],
|
| 158 |
+
'updated_at' => (string) $r['updated_at'],
|
| 159 |
+
'last_completed_at' => $r['last_completed_at'],
|
| 160 |
+
];
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
return new WP_REST_Response( [
|
| 164 |
+
'actor' => [
|
| 165 |
+
'wp_user_id' => $user_id,
|
| 166 |
+
'login' => ( $u = get_user_by( 'id', $user_id ) ) ? $u->user_login : null,
|
| 167 |
+
],
|
| 168 |
+
'count' => count( $out ),
|
| 169 |
+
'fetched_at' => current_time( 'mysql', true ),
|
| 170 |
+
'assignments' => $out,
|
| 171 |
+
], 200 );
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* ===================================================================
|
| 175 |
+
* LEAF HALF — fetch logic
|
| 176 |
+
* =================================================================== */
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Fetch assignments from the configured root.
|
| 180 |
+
* Returns either a parsed payload or a WP_Error.
|
| 181 |
+
*/
|
| 182 |
+
public static function fetch_remote_assignments() {
|
| 183 |
+
$cfg = get_option( self::OPTION_KEY, [] );
|
| 184 |
+
if ( empty( $cfg['url'] ) || empty( $cfg['app_user'] ) || empty( $cfg['app_password'] ) ) {
|
| 185 |
+
return new WP_Error( 'not_configured', 'Remote root not configured. Set URL, username, and application password first.' );
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
$url = rtrim( (string) $cfg['url'], '/' ) . '/wp-json/sa-orch/v1/root/assignments';
|
| 189 |
+
|
| 190 |
+
$auth_header = 'Basic ' . base64_encode( $cfg['app_user'] . ':' . str_replace( ' ', '', $cfg['app_password'] ) );
|
| 191 |
+
|
| 192 |
+
$resp = wp_remote_get( $url, [
|
| 193 |
+
'headers' => [ 'Authorization' => $auth_header ],
|
| 194 |
+
'timeout' => self::REST_TIMEOUT,
|
| 195 |
+
] );
|
| 196 |
+
|
| 197 |
+
if ( is_wp_error( $resp ) ) {
|
| 198 |
+
return $resp;
|
| 199 |
+
}
|
| 200 |
+
$code = (int) wp_remote_retrieve_response_code( $resp );
|
| 201 |
+
$body = (string) wp_remote_retrieve_body( $resp );
|
| 202 |
+
if ( $code < 200 || $code >= 300 ) {
|
| 203 |
+
return new WP_Error( 'remote_error', "HTTP $code from root: " . substr( $body, 0, 240 ) );
|
| 204 |
+
}
|
| 205 |
+
$payload = json_decode( $body, true );
|
| 206 |
+
if ( ! is_array( $payload ) ) {
|
| 207 |
+
return new WP_Error( 'parse_error', 'Root response was not valid JSON: ' . substr( $body, 0, 240 ) );
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Cache the last fetch in option for the admin page render.
|
| 211 |
+
update_option( 'sa_orch_remote_root_last_fetch', [
|
| 212 |
+
'at' => current_time( 'mysql', true ),
|
| 213 |
+
'payload' => $payload,
|
| 214 |
+
], false /* not autoloaded */ );
|
| 215 |
+
|
| 216 |
+
return $payload;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
public static function rest_fetch_now( WP_REST_Request $req ) {
|
| 220 |
+
$r = self::fetch_remote_assignments();
|
| 221 |
+
if ( is_wp_error( $r ) ) {
|
| 222 |
+
return new WP_REST_Response( [
|
| 223 |
+
'error' => $r->get_error_code(),
|
| 224 |
+
'message' => $r->get_error_message(),
|
| 225 |
+
], 502 );
|
| 226 |
+
}
|
| 227 |
+
return new WP_REST_Response( $r, 200 );
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* ===================================================================
|
| 231 |
+
* LEAF HALF — admin page render
|
| 232 |
+
* =================================================================== */
|
| 233 |
+
|
| 234 |
+
public static function render_page() {
|
| 235 |
+
if ( ! current_user_can( SA_Orch_Admin::CAP ) ) wp_die( 'Insufficient permissions.' );
|
| 236 |
+
|
| 237 |
+
$error = '';
|
| 238 |
+
$notice = '';
|
| 239 |
+
|
| 240 |
+
// Handle save
|
| 241 |
+
if ( isset( $_POST['sa_orch_save_root'] ) && check_admin_referer( 'sa_orch_save_root' ) ) {
|
| 242 |
+
$cfg = [
|
| 243 |
+
'url' => esc_url_raw( trim( wp_unslash( $_POST['root_url'] ?? '' ) ) ),
|
| 244 |
+
'app_user' => sanitize_text_field( wp_unslash( $_POST['app_user'] ?? '' ) ),
|
| 245 |
+
'app_password' => trim( wp_unslash( $_POST['app_password'] ?? '' ) ),
|
| 246 |
+
];
|
| 247 |
+
update_option( self::OPTION_KEY, $cfg, false );
|
| 248 |
+
$notice = 'Remote root saved.';
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
// Handle fetch button
|
| 252 |
+
if ( isset( $_POST['sa_orch_fetch_now'] ) && check_admin_referer( 'sa_orch_fetch_now' ) ) {
|
| 253 |
+
$r = self::fetch_remote_assignments();
|
| 254 |
+
if ( is_wp_error( $r ) ) {
|
| 255 |
+
$error = 'Fetch failed: ' . $r->get_error_message();
|
| 256 |
+
} else {
|
| 257 |
+
$notice = 'Fetched ' . (int) $r['count'] . ' assignment(s).';
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
$cfg = get_option( self::OPTION_KEY, [] );
|
| 262 |
+
$last_fetch = get_option( 'sa_orch_remote_root_last_fetch', [] );
|
| 263 |
+
$payload = $last_fetch['payload'] ?? null;
|
| 264 |
+
$assignments = is_array( $payload ) ? ( $payload['assignments'] ?? [] ) : [];
|
| 265 |
+
|
| 266 |
+
echo '<div class="wrap">';
|
| 267 |
+
echo '<h1>Remote Roots</h1>';
|
| 268 |
+
echo '<p>Pull assignments from a remote SA-Orchestration root (e.g., the dev root). Leaf-initiated; outbound HTTPS only.</p>';
|
| 269 |
+
|
| 270 |
+
if ( $error !== '' ) echo '<div class="notice notice-error"><p>' . esc_html( $error ) . '</p></div>';
|
| 271 |
+
if ( $notice !== '' ) echo '<div class="notice notice-success"><p>' . esc_html( $notice ) . '</p></div>';
|
| 272 |
+
|
| 273 |
+
// Configuration form
|
| 274 |
+
echo '<h2>Configuration (single root, MVP)</h2>';
|
| 275 |
+
echo '<form method="post">';
|
| 276 |
+
wp_nonce_field( 'sa_orch_save_root' );
|
| 277 |
+
echo '<table class="form-table">';
|
| 278 |
+
echo '<tr><th>Root URL</th><td><input type="url" name="root_url" class="regular-text" placeholder="https://sleeperagendev.wpenginepowered.com" value="' . esc_attr( $cfg['url'] ?? '' ) . '" required></td></tr>';
|
| 279 |
+
echo '<tr><th>App Username</th><td><input type="text" name="app_user" class="regular-text" placeholder="user@example.com" value="' . esc_attr( $cfg['app_user'] ?? '' ) . '" required></td></tr>';
|
| 280 |
+
echo '<tr><th>App Password</th><td><input type="password" name="app_password" class="regular-text" placeholder="XXXX XXXX XXXX XXXX XXXX XXXX" value="' . esc_attr( $cfg['app_password'] ?? '' ) . '" required>';
|
| 281 |
+
echo '<p class="description">Generate at <code>{root}/wp-admin/profile.php</code> → Application Passwords. Stored in wp_options (not autoloaded). Revoke from same page anytime.</p></td></tr>';
|
| 282 |
+
echo '</table>';
|
| 283 |
+
echo '<p><button type="submit" name="sa_orch_save_root" value="1" class="button">Save Remote Root</button></p>';
|
| 284 |
+
echo '</form>';
|
| 285 |
+
|
| 286 |
+
// Fetch button
|
| 287 |
+
echo '<h2>Pull Assignments</h2>';
|
| 288 |
+
echo '<form method="post">';
|
| 289 |
+
wp_nonce_field( 'sa_orch_fetch_now' );
|
| 290 |
+
echo '<p><button type="submit" name="sa_orch_fetch_now" value="1" class="button button-primary">Fetch Now</button>';
|
| 291 |
+
if ( ! empty( $last_fetch['at'] ) ) {
|
| 292 |
+
echo ' <span style="margin-left:1em;color:#666;">Last fetched: ' . esc_html( $last_fetch['at'] ) . ' UTC</span>';
|
| 293 |
+
}
|
| 294 |
+
echo '</p>';
|
| 295 |
+
echo '</form>';
|
| 296 |
+
|
| 297 |
+
// Render assignments
|
| 298 |
+
if ( $payload && isset( $payload['count'] ) ) {
|
| 299 |
+
$actor = $payload['actor'] ?? [];
|
| 300 |
+
echo '<h2>Assignments (' . (int) $payload['count'] . ')</h2>';
|
| 301 |
+
echo '<p style="color:#666;">Authenticated to root as: <code>' . esc_html( $actor['login'] ?? '' ) . '</code> (wp_user_id=' . (int) ( $actor['wp_user_id'] ?? 0 ) . ')</p>';
|
| 302 |
+
|
| 303 |
+
if ( empty( $assignments ) ) {
|
| 304 |
+
echo '<p>No assignments found for this user on the configured root.</p>';
|
| 305 |
+
} else {
|
| 306 |
+
echo '<table class="widefat striped"><thead><tr>';
|
| 307 |
+
echo '<th>Root Task ID</th><th>Board</th><th>Stage</th><th>Status</th><th>Priority</th><th>Title</th><th>Updated</th>';
|
| 308 |
+
echo '</tr></thead><tbody>';
|
| 309 |
+
foreach ( $assignments as $a ) {
|
| 310 |
+
echo '<tr>';
|
| 311 |
+
echo '<td>' . (int) $a['root_task_id'] . '</td>';
|
| 312 |
+
echo '<td>' . esc_html( $a['board_title'] ) . ' (#' . (int) $a['board_id'] . ')</td>';
|
| 313 |
+
echo '<td>' . esc_html( $a['stage_title'] ) . '</td>';
|
| 314 |
+
echo '<td>' . esc_html( $a['status'] ) . '</td>';
|
| 315 |
+
echo '<td>' . esc_html( $a['priority'] ) . '</td>';
|
| 316 |
+
echo '<td>' . esc_html( $a['title'] ) . '</td>';
|
| 317 |
+
echo '<td>' . esc_html( $a['updated_at'] ) . '</td>';
|
| 318 |
+
echo '</tr>';
|
| 319 |
+
if ( ! empty( $a['description_excerpt'] ) ) {
|
| 320 |
+
echo '<tr><td colspan="7" style="background:#f8f8f8;font-size:11.5px;color:#444;padding:6px 12px;"><em>excerpt:</em> ' . esc_html( substr( $a['description_excerpt'], 0, 400 ) ) . ( strlen( $a['description_excerpt'] ) > 400 ? '...' : '' ) . '</td></tr>';
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
echo '</tbody></table>';
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
echo '</div>';
|
| 328 |
+
}
|
| 329 |
+
}
|
sa-orchestration/sa-orchestration.php
CHANGED
|
@@ -51,6 +51,7 @@ require_once SA_ORCH_DIR . 'includes/class-sa-sara-rest.php';
|
|
| 51 |
require_once SA_ORCH_DIR . 'includes/class-sa-sara-admin.php';
|
| 52 |
require_once SA_ORCH_DIR . 'includes/class-sa-asterion-projection.php'; // Board #30
|
| 53 |
require_once SA_ORCH_DIR . 'includes/class-sa-seed-generator.php'; // Board #26
|
|
|
|
| 54 |
require_once SA_ORCH_DIR . 'includes/class-sa-admin.php';
|
| 55 |
|
| 56 |
register_activation_hook( __FILE__, [ 'SA_Orch_Migrations', 'install' ] );
|
|
@@ -59,10 +60,12 @@ add_action( 'rest_api_init', [ 'SA_Orch_Rest', 'register_routes' ] );
|
|
| 59 |
add_action( 'rest_api_init', [ 'SA_Orch_Sara_Rest', 'register_routes' ] );
|
| 60 |
add_action( 'rest_api_init', [ 'SA_Orch_Asterion_Projection', 'register_routes' ] ); // Board #30
|
| 61 |
add_action( 'rest_api_init', [ 'SA_Orch_Seed_Generator', 'register_routes' ] ); // Board #26
|
|
|
|
| 62 |
add_action( 'admin_menu', [ 'SA_Orch_Admin', 'register_menu' ] );
|
| 63 |
add_action( 'admin_menu', [ 'SA_Orch_Asterion_Explorer', 'register_menu' ] );
|
| 64 |
add_action( 'admin_menu', [ 'SA_Orch_Actor_Admin', 'register_menu' ] );
|
| 65 |
add_action( 'admin_menu', [ 'SA_Orch_Seed_Generator', 'register_menu' ] ); // Board #26
|
|
|
|
| 66 |
add_action( 'init', [ 'SA_Orch_Actor_Registry', 'register_role' ] );
|
| 67 |
add_action( 'admin_init', [ 'SA_Orch_Actor_Admin', 'maybe_handle_action' ] );
|
| 68 |
add_action( 'init', [ 'SA_Orch_Projection_Fluent_Support', 'register_hooks' ] );
|
|
|
|
| 51 |
require_once SA_ORCH_DIR . 'includes/class-sa-sara-admin.php';
|
| 52 |
require_once SA_ORCH_DIR . 'includes/class-sa-asterion-projection.php'; // Board #30
|
| 53 |
require_once SA_ORCH_DIR . 'includes/class-sa-seed-generator.php'; // Board #26
|
| 54 |
+
require_once SA_ORCH_DIR . 'includes/class-sa-leaf-link.php'; // Board #40 v0.8.0 MVP
|
| 55 |
require_once SA_ORCH_DIR . 'includes/class-sa-admin.php';
|
| 56 |
|
| 57 |
register_activation_hook( __FILE__, [ 'SA_Orch_Migrations', 'install' ] );
|
|
|
|
| 60 |
add_action( 'rest_api_init', [ 'SA_Orch_Sara_Rest', 'register_routes' ] );
|
| 61 |
add_action( 'rest_api_init', [ 'SA_Orch_Asterion_Projection', 'register_routes' ] ); // Board #30
|
| 62 |
add_action( 'rest_api_init', [ 'SA_Orch_Seed_Generator', 'register_routes' ] ); // Board #26
|
| 63 |
+
add_action( 'rest_api_init', [ 'SA_Orch_Leaf_Link', 'register_routes' ] ); // Board #40 v0.8.0 MVP
|
| 64 |
add_action( 'admin_menu', [ 'SA_Orch_Admin', 'register_menu' ] );
|
| 65 |
add_action( 'admin_menu', [ 'SA_Orch_Asterion_Explorer', 'register_menu' ] );
|
| 66 |
add_action( 'admin_menu', [ 'SA_Orch_Actor_Admin', 'register_menu' ] );
|
| 67 |
add_action( 'admin_menu', [ 'SA_Orch_Seed_Generator', 'register_menu' ] ); // Board #26
|
| 68 |
+
add_action( 'admin_menu', [ 'SA_Orch_Leaf_Link', 'register_menu' ] ); // Board #40 v0.8.0 MVP
|
| 69 |
add_action( 'init', [ 'SA_Orch_Actor_Registry', 'register_role' ] );
|
| 70 |
add_action( 'admin_init', [ 'SA_Orch_Actor_Admin', 'maybe_handle_action' ] );
|
| 71 |
add_action( 'init', [ 'SA_Orch_Projection_Fluent_Support', 'register_hooks' ] );
|