RcmEmailAutomation / sa-orchestration /includes /class-sa-actor-admin.php
NujacsaintS's picture
reseed
f51c224
Raw
History Blame Contribute Delete
13.8 kB
<?php
defined( 'ABSPATH' ) || exit;
/**
* Sleeper Agents admin submenu — list / create / archive actors.
*
* Single page with view modes:
* ?page=sa-orchestration-actors → list (default)
* ?page=sa-orchestration-actors&view=create → create form
* ?page=sa-orchestration-actors&view=detail&id=X → detail
*
* POST actions (admin-post-style, handled via admin_init when on this page):
* sa_actor_action=create → create new actor; redirect to list
* sa_actor_action=archive&id=X → archive actor; redirect to list
* sa_actor_action=unarchive&id=X → unarchive actor; redirect to list
*
* Per the locked doctrine: actors created here are non-binding identity
* records. They cannot progress Boards, accept gates, or write to
* Asterion. This admin surface is the deliberate human-arbitrated path
* for instantiating those identities.
*/
class SA_Orch_Actor_Admin {
const SUBMENU_SLUG = 'sa-orchestration-actors';
public static function register_menu() {
add_submenu_page(
SA_Orch_Admin::PARENT_SLUG,
'Sleeper Agents',
'Sleeper Agents',
SA_Orch_Admin::CAP,
self::SUBMENU_SLUG,
[ __CLASS__, 'render_page' ]
);
}
/** Catch POST actions on the submenu page before rendering. */
public static function maybe_handle_action() {
if ( ! is_admin() ) return;
if ( ! current_user_can( SA_Orch_Admin::CAP ) ) return;
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
if ( $page !== self::SUBMENU_SLUG ) return;
$action = isset( $_POST['sa_actor_action'] ) ? sanitize_text_field( wp_unslash( $_POST['sa_actor_action'] ) ) : '';
if ( $action === '' ) return;
check_admin_referer( 'sa_actor_form', 'sa_actor_nonce' );
if ( $action === 'create' ) {
$data = [
'name' => isset( $_POST['actor_name'] ) ? sanitize_text_field( wp_unslash( $_POST['actor_name'] ) ) : '',
'type' => isset( $_POST['actor_type'] ) ? sanitize_text_field( wp_unslash( $_POST['actor_type'] ) ) : '',
'persona' => isset( $_POST['actor_persona'] ) ? sanitize_textarea_field( wp_unslash( $_POST['actor_persona'] ) ) : '',
'scope' => isset( $_POST['actor_scope'] ) ? sanitize_textarea_field( wp_unslash( $_POST['actor_scope'] ) ) : '',
];
$result = SA_Orch_Actor_Registry::create_actor( $data );
if ( is_wp_error( $result ) ) {
$msg = 'error&msg=' . rawurlencode( $result->get_error_message() );
} else {
$msg = 'created&id=' . (int) $result;
}
wp_safe_redirect( admin_url( 'admin.php?page=' . self::SUBMENU_SLUG . '&notice=' . $msg ) );
exit;
}
if ( $action === 'archive' || $action === 'unarchive' ) {
$id = isset( $_POST['actor_id'] ) ? (int) $_POST['actor_id'] : 0;
if ( $id > 0 ) {
if ( $action === 'archive' ) SA_Orch_Actor_Registry::archive_actor( $id );
if ( $action === 'unarchive' ) SA_Orch_Actor_Registry::unarchive_actor( $id );
}
wp_safe_redirect( admin_url( 'admin.php?page=' . self::SUBMENU_SLUG . '&notice=' . $action . 'd' ) );
exit;
}
}
/** Single-page render with view dispatching. */
public static function render_page() {
if ( ! current_user_can( SA_Orch_Admin::CAP ) ) {
wp_die( __( 'Insufficient permissions.', 'sa-orchestration' ) );
}
$view = isset( $_GET['view'] ) ? sanitize_text_field( wp_unslash( $_GET['view'] ) ) : 'list';
echo '<div class="wrap sa-actors-wrap">';
echo '<h1>Sleeper Agents <span class="sa-actors-tag">identity-bound roles · non-binding</span></h1>';
echo self::inline_styles();
self::render_notice();
if ( $view === 'create' ) {
self::render_create_form();
} elseif ( $view === 'detail' ) {
self::render_detail();
} else {
self::render_list();
}
echo '</div>'; // .wrap
}
private static function render_notice() {
if ( empty( $_GET['notice'] ) ) return;
$notice = sanitize_text_field( wp_unslash( $_GET['notice'] ) );
$msg = isset( $_GET['msg'] ) ? sanitize_text_field( wp_unslash( $_GET['msg'] ) ) : '';
$kind = 'success'; $body = '';
if ( $notice === 'created' ) {
$body = 'Actor created.';
} elseif ( $notice === 'archived' ) {
$body = 'Actor archived.';
} elseif ( $notice === 'unarchived' ) {
$body = 'Actor unarchived.';
} elseif ( $notice === 'error' ) {
$kind = 'error';
$body = $msg !== '' ? $msg : 'Action failed.';
}
if ( $body !== '' ) {
echo '<div class="notice notice-' . esc_attr( $kind ) . ' is-dismissible"><p>' . esc_html( $body ) . '</p></div>';
}
}
private static function render_list() {
$include_archived = ! empty( $_GET['show_archived'] );
$actors = SA_Orch_Actor_Registry::list_actors( $include_archived );
$create_url = admin_url( 'admin.php?page=' . self::SUBMENU_SLUG . '&view=create' );
$toggle_archived_url = add_query_arg( 'show_archived', $include_archived ? '0' : '1' );
echo '<p class="sa-actors-meta">Actors are persistent identity-bound roles. They may speak, draft, advise, observe, coordinate. They may not progress Boards, accept gates, mutate Asterion, or act silently.</p>';
echo '<p>';
echo '<a href="' . esc_url( $create_url ) . '" class="button button-primary">+ Create Actor</a> &nbsp; ';
echo '<a href="' . esc_url( $toggle_archived_url ) . '" class="button">' . ( $include_archived ? 'Hide archived' : 'Show archived' ) . '</a>';
echo '</p>';
echo '<table class="wp-list-table widefat fixed striped sa-actors-table">';
echo '<thead><tr>';
echo '<th>Name</th><th>Type</th><th>Owner</th><th>Status</th><th>Persona (excerpt)</th><th>Created</th><th>Actions</th>';
echo '</tr></thead><tbody>';
if ( empty( $actors ) ) {
echo '<tr><td colspan="7" class="sa-actors-empty">No actors yet. <a href="' . esc_url( $create_url ) . '">Create the first one</a> with intent.</td></tr>';
} else {
foreach ( $actors as $u ) {
$a = SA_Orch_Actor_Registry::get_actor( (int) $u->ID );
if ( ! $a ) continue;
$detail_url = admin_url( 'admin.php?page=' . self::SUBMENU_SLUG . '&view=detail&id=' . $a['user_id'] );
$persona_excerpt = function_exists( 'mb_substr' ) ? mb_substr( $a['persona'], 0, 80 ) : substr( $a['persona'], 0, 80 );
$status_label = $a['is_active'] ? '<span class="sa-actors-status-active">active</span>' : '<span class="sa-actors-status-archived">archived</span>';
echo '<tr>';
echo '<td><strong><a href="' . esc_url( $detail_url ) . '">' . esc_html( $a['name'] ) . '</a></strong><br><small class="sa-actors-login">' . esc_html( $a['login'] ) . '</small></td>';
echo '<td>' . esc_html( $a['type'] ) . '</td>';
echo '<td>' . esc_html( $a['owner_login'] ?: '—' ) . '</td>';
echo '<td>' . $status_label . '</td>'; // status_label is pre-built; safe
echo '<td>' . esc_html( $persona_excerpt ) . ( strlen( $a['persona'] ) > 80 ? '…' : '' ) . '</td>';
echo '<td>' . esc_html( $a['created_at'] ) . '</td>';
echo '<td>';
if ( $a['is_active'] ) {
self::render_post_button( 'archive', $a['user_id'], 'Archive' );
} else {
self::render_post_button( 'unarchive', $a['user_id'], 'Unarchive' );
}
echo '</td>';
echo '</tr>';
}
}
echo '</tbody></table>';
}
private static function render_post_button( string $action, int $actor_id, string $label ) {
$url = admin_url( 'admin.php?page=' . self::SUBMENU_SLUG );
echo '<form method="post" action="' . esc_url( $url ) . '" style="display:inline;">';
wp_nonce_field( 'sa_actor_form', 'sa_actor_nonce' );
echo '<input type="hidden" name="sa_actor_action" value="' . esc_attr( $action ) . '" />';
echo '<input type="hidden" name="actor_id" value="' . esc_attr( (string) $actor_id ) . '" />';
echo '<button type="submit" class="button button-small">' . esc_html( $label ) . '</button>';
echo '</form>';
}
private static function render_create_form() {
$list_url = admin_url( 'admin.php?page=' . self::SUBMENU_SLUG );
echo '<h2>Create Actor</h2>';
echo '<p class="sa-actors-meta">Creation discipline: actors must be named with intent, have a defined role, and a scoped purpose. No "random AI assistant" — only purposeful, identity-bound roles.</p>';
echo '<form method="post" action="' . esc_url( $list_url ) . '" class="sa-actors-form">';
wp_nonce_field( 'sa_actor_form', 'sa_actor_nonce' );
echo '<input type="hidden" name="sa_actor_action" value="create" />';
echo '<table class="form-table"><tbody>';
echo '<tr><th><label for="actor_name">Name <span class="required">*</span></label></th><td><input id="actor_name" name="actor_name" type="text" class="regular-text" required placeholder="e.g. Bridget — Reconciliation" /></td></tr>';
echo '<tr><th><label for="actor_type">Type <span class="required">*</span></label></th><td><input id="actor_type" name="actor_type" type="text" class="regular-text" required placeholder="e.g. reconciliation, designer, lawyer, dev" /></td></tr>';
echo '<tr><th><label for="actor_persona">Persona <span class="required">*</span></label></th><td><textarea id="actor_persona" name="actor_persona" rows="4" class="large-text" required placeholder="Voice + behavior. What this actor is for. e.g. \'System-level coordination actor. Monitors shared objects, highlights divergence, suggests reconciliation.\'"></textarea></td></tr>';
echo '<tr><th><label for="actor_scope">Scope</label></th><td><textarea id="actor_scope" name="actor_scope" rows="3" class="large-text" placeholder="What surfaces / objects this actor may comment on. e.g. \'cross-board observations\', \'fluent-support tickets\', \'fluent-boards tasks tagged urgent\'"></textarea></td></tr>';
echo '</tbody></table>';
echo '<p>';
echo '<button type="submit" class="button button-primary">Create Actor</button> &nbsp; ';
echo '<a href="' . esc_url( $list_url ) . '" class="button">Cancel</a>';
echo '</p>';
echo '</form>';
}
private static function render_detail() {
$list_url = admin_url( 'admin.php?page=' . self::SUBMENU_SLUG );
$id = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0;
$a = $id > 0 ? SA_Orch_Actor_Registry::get_actor( $id ) : null;
if ( ! $a ) {
echo '<p class="sa-actors-empty">Actor not found. <a href="' . esc_url( $list_url ) . '">Back to list</a></p>';
return;
}
echo '<h2>' . esc_html( $a['name'] ) . ' <small class="sa-actors-login">' . esc_html( $a['login'] ) . '</small></h2>';
echo '<p><a href="' . esc_url( $list_url ) . '">&larr; Back to list</a></p>';
echo '<table class="form-table"><tbody>';
echo '<tr><th>Type</th> <td>' . esc_html( $a['type'] ) . '</td></tr>';
echo '<tr><th>Status</th> <td>' . ( $a['is_active'] ? 'active' : 'archived' ) . '</td></tr>';
echo '<tr><th>Owner</th> <td>' . esc_html( $a['owner_login'] ?: '—' ) . ' (user_id=' . (int) $a['owner_user_id'] . ')</td></tr>';
echo '<tr><th>Persona</th> <td><pre class="sa-actors-pre">' . esc_html( $a['persona'] ) . '</pre></td></tr>';
echo '<tr><th>Scope</th> <td><pre class="sa-actors-pre">' . esc_html( $a['scope'] ?: '—' ) . '</pre></td></tr>';
echo '<tr><th>Subscription tier</th> <td>' . esc_html( $a['subscription_tier'] ?: '— (reserved for future)' ) . '</td></tr>';
echo '<tr><th>Created</th> <td>' . esc_html( $a['created_at'] ) . '</td></tr>';
echo '</tbody></table>';
echo '<h3>Activity trail</h3>';
echo '<p class="sa-actors-meta">Actor-attributed comments and drafts will appear here once the invocation surface is implemented (deferred to a future Board). For now: empty by design.</p>';
}
private static function inline_styles(): string {
return '<style>
.sa-actors-wrap h1 .sa-actors-tag { font-size:11px; padding:2px 6px; background:#f0f0f1; color:#646970; border-radius:3px; vertical-align:middle; font-weight:normal; margin-left:8px; }
.sa-actors-meta { color:#646970; }
.sa-actors-table .sa-actors-login { color:#646970; font-family:Consolas,Monaco,monospace; font-size:11px; }
.sa-actors-status-active { color:#1e4620; background:#edf7ed; padding:2px 6px; border-radius:3px; font-size:11px; }
.sa-actors-status-archived { color:#646970; background:#f0f0f1; padding:2px 6px; border-radius:3px; font-size:11px; }
.sa-actors-empty { color:#646970; font-style:italic; }
.sa-actors-pre { background:#f6f7f7; padding:10px; border-radius:4px; white-space:pre-wrap; max-width:680px; }
.sa-actors-form .required { color:#d63638; }
</style>';
}
}