feat: Pipeline board M5 — Mission Brief (list + right-rail inspector) (#175, v0.27.0)
Reworks the board into Direction C Mission Brief: status-grouped run list + persistent inspector reusing M4 Stage I/O; stage-list-driven; preserves New Article + polling + filters; column-limit setting restored. QA APPROVE 1034e19; CI green. Agent-Session: pipeline-board-m5-merge
This commit is contained in:
@@ -4,25 +4,29 @@ declare(strict_types=1);
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Supplies card data for the kanban board page from existing DB tables.
|
||||
* Supplies run-list data for the Mission Brief board (M5).
|
||||
*
|
||||
* Orchestrates the four board columns:
|
||||
* Orchestrates the four status sections:
|
||||
* Generating | In Review | Published | Failed
|
||||
*
|
||||
* Generation-log queries (Generating, Failed) are delegated to
|
||||
* PRAutoBlogger_Board_Gen_Log_Query. WP_Query columns (In Review, Published)
|
||||
* are handled here.
|
||||
* M5 additions (v0.27.0): each card now includes `run_stages_summary`
|
||||
* -- a lightweight array of { stage, status } entries derived from the
|
||||
* run_stages table, used by the board's dot-rail (stage-progress indicator).
|
||||
* The inspector's full I/O is fetched separately via Board_Inspector_Handler
|
||||
* on row-click (not loaded up-front for every card).
|
||||
*
|
||||
* M2: cards now include `dossier_url` so board.js can deep-link to the
|
||||
* Article Dossier instead of the old per-column destinations.
|
||||
* Generation-log queries (Generating, Failed) are delegated to
|
||||
* PRAutoBlogger_Board_Gen_Log_Query. WP_Query sections (In Review, Published)
|
||||
* are handled here.
|
||||
*
|
||||
* No schema changes -- all data comes from tables that already exist.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger_Board_Page::on_ajax_board_status().
|
||||
* Dependencies: PRAutoBlogger_Board_Gen_Log_Query, PRAutoBlogger_Dossier_Page,
|
||||
* WordPress WP_Query, get_option().
|
||||
* Dependencies: PRAutoBlogger_Board_Gen_Log_Query, PRAutoBlogger_Board_Stage_Dots,
|
||||
* PRAutoBlogger_Dossier_Page, WordPress WP_Query, get_option().
|
||||
*
|
||||
* @see admin/class-board-gen-log-query.php -- Raw gen_log queries (Generating/Failed).
|
||||
* @see admin/class-board-stage-dots.php -- Stage dot-rail enrichment (M5).
|
||||
* @see admin/class-board-page.php -- Calls get_board_snapshot().
|
||||
* @see admin/class-dossier-page.php -- Provides url_for_post() for deep-links.
|
||||
* @see ARCHITECTURE.md -- Database schema.
|
||||
@@ -56,6 +60,12 @@ class PRAutoBlogger_Board_Data_Provider {
|
||||
$published = $this->get_published_cards();
|
||||
$failed = $this->gen_log_query->get_failed_cards();
|
||||
|
||||
// Enrich cards with lightweight stage dots.
|
||||
$generating = PRAutoBlogger_Board_Stage_Dots::enrich( $generating );
|
||||
$in_review = PRAutoBlogger_Board_Stage_Dots::enrich( $in_review );
|
||||
$published = PRAutoBlogger_Board_Stage_Dots::enrich( $published );
|
||||
$failed = PRAutoBlogger_Board_Stage_Dots::enrich( $failed );
|
||||
|
||||
return array(
|
||||
'generating' => $generating,
|
||||
'in_review' => $in_review,
|
||||
@@ -85,7 +95,6 @@ class PRAutoBlogger_Board_Data_Provider {
|
||||
|
||||
/**
|
||||
* Cards for generated draft posts awaiting human review.
|
||||
* M2: cards include dossier_url for deep-linking.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
@@ -132,7 +141,6 @@ class PRAutoBlogger_Board_Data_Provider {
|
||||
|
||||
/**
|
||||
* Cards for published posts within the configured window.
|
||||
* M2: cards include dossier_url for deep-linking.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
@@ -187,10 +195,8 @@ class PRAutoBlogger_Board_Data_Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 'human_modified' on each card in ONE batched query (v0.20.0 —
|
||||
* CPO product AC: human-modified runs are visually distinct at the
|
||||
* run-list level). No per-card queries; self-healing on a missing
|
||||
* table/column (flag simply stays false).
|
||||
* Set 'human_modified' on each card in ONE batched query (v0.20.0).
|
||||
* No per-card queries; self-healing on a missing table/column.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $cards Column cards.
|
||||
* @return array<int, array<string, mixed>> Cards with the flag set.
|
||||
|
||||
@@ -4,27 +4,35 @@ declare(strict_types=1);
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Registers and renders the kanban board -- the primary landing screen for PRAutoBlogger.
|
||||
* Registers and renders the Mission Brief board -- the primary landing screen.
|
||||
*
|
||||
* Board columns: Generating | In Review | Published | Failed.
|
||||
* Cards live-update via AJAX polling at a settings-backed interval (default 5s),
|
||||
* backing off to 2x when no active generating run is detected.
|
||||
* M5 (v0.27.0): replaces the kanban column layout with a vertical run list
|
||||
* grouped by status section (Generating | In Review | Published | Failed),
|
||||
* plus a persistent right-rail inspector. Selecting a row fetches full per-stage
|
||||
* I/O via PRAutoBlogger_Board_Inspector_Handler (AJAX) and populates the rail
|
||||
* without navigating away. The dossier remains the deep-dive destination.
|
||||
*
|
||||
* Card click-throughs (M1):
|
||||
* - Generating -> Activity Log filtered to run context
|
||||
* - In Review -> Review Queue
|
||||
* - Published -> Post edit screen
|
||||
* - Failed -> Activity Log
|
||||
*
|
||||
* M2 will rewire all click-throughs to the Article Dossier page.
|
||||
* Board capabilities preserved from M4:
|
||||
* - New Article action (links to Ideas Browser).
|
||||
* - Per-run dossier deep-link (click row or rail "Open dossier" button).
|
||||
* - Status sections with counts.
|
||||
* - Live AJAX polling with backoff (prautoblogger_board_status).
|
||||
* - Published-window filter (prautoblogger_board_published_window_days).
|
||||
* - Poll-interval setting (prautoblogger_board_poll_interval).
|
||||
* - Human-modified badge.
|
||||
* - Stalled/failed rows read RED (never softened).
|
||||
* - Error banner on poll failure.
|
||||
* - Empty state per section.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: PRAutoBlogger_Board_Data_Provider, wp_localize_script.
|
||||
* Dependencies: PRAutoBlogger_Board_Data_Provider, PRAutoBlogger_Board_Inspector_Handler,
|
||||
* wp_localize_script.
|
||||
*
|
||||
* @see admin/class-board-data-provider.php -- Supplies card data.
|
||||
* @see templates/admin/board-page.php -- HTML template.
|
||||
* @see assets/js/board.js -- Polling + DOM updates.
|
||||
* @see ARCHITECTURE.md -- Section: Board (kanban dashboard).
|
||||
* @see admin/class-board-data-provider.php -- Supplies run-list snapshot.
|
||||
* @see ajax/class-board-inspector-handler.php -- Inspector AJAX (per-run stage I/O).
|
||||
* @see templates/admin/board-page.php -- HTML template (Mission Brief layout).
|
||||
* @see assets/js/board.js -- Polling + inspector rail JS.
|
||||
* @see ARCHITECTURE.md -- §Board (M5 Mission Brief).
|
||||
*/
|
||||
class PRAutoBlogger_Board_Page {
|
||||
|
||||
@@ -33,7 +41,7 @@ class PRAutoBlogger_Board_Page {
|
||||
/** AJAX action name for board polling. */
|
||||
public const AJAX_ACTION = 'prautoblogger_board_status';
|
||||
|
||||
/** Nonce action string for board AJAX. */
|
||||
/** Nonce action string for board AJAX (polling + inspector share this nonce). */
|
||||
public const NONCE_ACTION = 'prautoblogger_board';
|
||||
|
||||
/**
|
||||
@@ -48,7 +56,7 @@ class PRAutoBlogger_Board_Page {
|
||||
* $admin_page_hooks['prautoblogger-settings'] before add_submenu_page()
|
||||
* runs here, so get_plugin_page_hookname() resolves to the correct
|
||||
* 'prautoblogger_page_prautoblogger-board' suffix rather than the fallback
|
||||
* 'admin_page_prautoblogger-board'. See ARCHITECTURE.md Section: Board.
|
||||
* 'admin_page_prautoblogger-board'. See ARCHITECTURE.md §Board.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -70,8 +78,7 @@ class PRAutoBlogger_Board_Page {
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- intentional submenu reorder.
|
||||
global $submenu;
|
||||
if ( isset( $submenu['prautoblogger-settings'] ) ) {
|
||||
$items = $submenu['prautoblogger-settings'];
|
||||
// Find the Board entry and move it to position 0.
|
||||
$items = $submenu['prautoblogger-settings'];
|
||||
$board_idx = null;
|
||||
foreach ( $items as $i => $item ) {
|
||||
if ( isset( $item[2] ) && self::PAGE_SLUG === $item[2] ) {
|
||||
@@ -82,7 +89,6 @@ class PRAutoBlogger_Board_Page {
|
||||
if ( null !== $board_idx && $board_idx > 0 ) {
|
||||
$board_item = $items[ $board_idx ];
|
||||
unset( $items[ $board_idx ] );
|
||||
// Prepend board item at position 0 while preserving remaining order.
|
||||
$submenu['prautoblogger-settings'] = array_merge( array( $board_item ), array_values( $items ) );
|
||||
}
|
||||
}
|
||||
@@ -101,7 +107,7 @@ class PRAutoBlogger_Board_Page {
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue board-specific JS + CSS and localize the poller config.
|
||||
* Enqueue board-specific JS + CSS and localize the poller + inspector config.
|
||||
*
|
||||
* Called via `admin_enqueue_scripts` action, gated to this page's hook suffix.
|
||||
*
|
||||
@@ -128,50 +134,52 @@ class PRAutoBlogger_Board_Page {
|
||||
PRAUTOBLOGGER_VERSION,
|
||||
true
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'prautoblogger-board-generate',
|
||||
PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/board-generate.js',
|
||||
array( 'jquery', 'prautoblogger-board' ),
|
||||
PRAUTOBLOGGER_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
$poll_interval = max( 3, (int) get_option( 'prautoblogger_board_poll_interval', 5 ) );
|
||||
$published_days = max( 1, (int) get_option( 'prautoblogger_board_published_window_days', 7 ) );
|
||||
$poll_interval = max( 3, (int) get_option( 'prautoblogger_board_poll_interval', 5 ) );
|
||||
$published_days = max( 1, (int) get_option( 'prautoblogger_board_published_window_days', 7 ) );
|
||||
|
||||
wp_localize_script(
|
||||
'prautoblogger-board',
|
||||
'prabBoard',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
|
||||
'generateNonce' => wp_create_nonce( 'prautoblogger_generate_now' ),
|
||||
'pollInterval' => $poll_interval * 1000,
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
|
||||
'pollInterval' => $poll_interval * 1000,
|
||||
'publishedWindowDays' => $published_days,
|
||||
'action' => self::AJAX_ACTION,
|
||||
'strings' => array(
|
||||
'generating' => __( 'Generating', 'prautoblogger' ),
|
||||
'inReview' => __( 'In Review', 'prautoblogger' ),
|
||||
'published' => __( 'Published', 'prautoblogger' ),
|
||||
'failed' => __( 'Failed', 'prautoblogger' ),
|
||||
'empty' => __( 'Nothing here yet.', 'prautoblogger' ),
|
||||
'cost' => __( 'Cost', 'prautoblogger' ),
|
||||
'view' => __( 'View', 'prautoblogger' ),
|
||||
'edit' => __( 'Edit', 'prautoblogger' ),
|
||||
'viewLog' => __( 'View Log', 'prautoblogger' ),
|
||||
'pollError' => __( 'Board update failed -- retrying.', 'prautoblogger' ),
|
||||
'humanModified' => __( 'Human-modified', 'prautoblogger' ),
|
||||
'newArticle' => __( 'New Article', 'prautoblogger' ),
|
||||
'generatingBtn' => __( 'Generating...', 'prautoblogger' ),
|
||||
'genStarted' => __( 'Generation started. Board will update automatically.', 'prautoblogger' ),
|
||||
'genError' => __( 'Generation failed to start. Check the Activity Log.', 'prautoblogger' ),
|
||||
'action' => self::AJAX_ACTION,
|
||||
'inspectorAction' => PRAutoBlogger_Board_Inspector_Handler::ACTION,
|
||||
'ideasUrl' => admin_url( 'admin.php?page=prautoblogger-ideas' ),
|
||||
'strings' => array(
|
||||
'generating' => __( 'Generating', 'prautoblogger' ),
|
||||
'inReview' => __( 'In review', 'prautoblogger' ),
|
||||
'published' => __( 'Published', 'prautoblogger' ),
|
||||
'failed' => __( 'Failed', 'prautoblogger' ),
|
||||
'stalled' => __( 'Stalled', 'prautoblogger' ),
|
||||
'empty' => __( 'Nothing here yet.', 'prautoblogger' ),
|
||||
'cost' => __( 'Cost', 'prautoblogger' ),
|
||||
'view' => __( 'View', 'prautoblogger' ),
|
||||
'edit' => __( 'Edit', 'prautoblogger' ),
|
||||
'viewLog' => __( 'View log', 'prautoblogger' ),
|
||||
'pollError' => __( 'Board update failed — retrying.', 'prautoblogger' ),
|
||||
'humanModified' => __( 'Human-modified', 'prautoblogger' ),
|
||||
'inspectorEmpty' => __( 'Select an article to preview its pipeline.', 'prautoblogger' ),
|
||||
'inspectorLoad' => __( 'Loading…', 'prautoblogger' ),
|
||||
'inspectorError' => __( 'Could not load run details.', 'prautoblogger' ),
|
||||
'openDossier' => __( 'Open dossier →', 'prautoblogger' ),
|
||||
'newArticle' => __( 'New article', 'prautoblogger' ),
|
||||
'input' => __( 'Input', 'prautoblogger' ),
|
||||
'output' => __( 'Output', 'prautoblogger' ),
|
||||
'pruned' => __( '[pruned — past retention window]', 'prautoblogger' ),
|
||||
'noOutput' => __( '[no output recorded]', 'prautoblogger' ),
|
||||
'totalCost' => __( 'Total cost', 'prautoblogger' ),
|
||||
'tokens' => __( 'tok', 'prautoblogger' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: return the current board snapshot (all four columns).
|
||||
* AJAX handler: return the current board snapshot (all four sections).
|
||||
*
|
||||
* Nonce: `prautoblogger_board`. Requires `manage_options` capability.
|
||||
*
|
||||
|
||||
94
includes/admin/class-board-stage-dots.php
Normal file
94
includes/admin/class-board-stage-dots.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Enriches board cards with lightweight run_stages dot-rail data (M5).
|
||||
*
|
||||
* Extracted from PRAutoBlogger_Board_Data_Provider to stay under the
|
||||
* 300-line cap. Performs ONE batched run_stages query for all cards
|
||||
* in a section, never one query per card.
|
||||
*
|
||||
* Each card gains `run_stages_summary`: an ordered array of
|
||||
* { stage, status } where status is one of:
|
||||
* 'done' | 'active' | 'failed' | 'pending'
|
||||
* Used by the board.js stage dot-rail. The inspector's full I/O is
|
||||
* fetched separately on row-click via Board_Inspector_Handler.
|
||||
*
|
||||
* Falls back gracefully when the run_stages table is unavailable or a
|
||||
* card has no run_id (generating cards from the transient path).
|
||||
*
|
||||
* Triggered by: PRAutoBlogger_Board_Data_Provider::get_board_snapshot().
|
||||
* Dependencies: PRAutoBlogger_Run_Stage_State (table probe), WordPress $wpdb.
|
||||
*
|
||||
* @see admin/class-board-data-provider.php -- Calls enrich().
|
||||
* @see ARCHITECTURE.md -- §Board (M5 Mission Brief).
|
||||
*/
|
||||
class PRAutoBlogger_Board_Stage_Dots {
|
||||
|
||||
/**
|
||||
* Enrich a set of cards with run_stages dot-rail summaries.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $cards Cards to enrich.
|
||||
* @return array<int, array<string, mixed>> Cards with run_stages_summary set.
|
||||
*/
|
||||
public static function enrich( array $cards ): array {
|
||||
// Initialise empty summary on every card.
|
||||
foreach ( $cards as $i => $card ) {
|
||||
$cards[ $i ]['run_stages_summary'] = array();
|
||||
}
|
||||
|
||||
if ( ! PRAutoBlogger_Run_Stage_State::is_available() ) {
|
||||
return $cards;
|
||||
}
|
||||
|
||||
$run_ids = array();
|
||||
foreach ( $cards as $card ) {
|
||||
if ( ! empty( $card['run_id'] ) ) {
|
||||
$run_ids[] = (string) $card['run_id'];
|
||||
}
|
||||
}
|
||||
if ( empty( $run_ids ) ) {
|
||||
return $cards;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = PRAutoBlogger_Run_Stage_State::table_name();
|
||||
$placeholders = implode( ',', array_fill( 0, count( $run_ids ), '%s' ) );
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- $placeholders is a fixed %s list.
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT run_id, stage, status FROM {$table} WHERE run_id IN ({$placeholders}) ORDER BY id ASC",
|
||||
$run_ids
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! is_array( $rows ) ) {
|
||||
return $cards;
|
||||
}
|
||||
|
||||
// Index by run_id.
|
||||
$by_run = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$rid = (string) ( $row['run_id'] ?? '' );
|
||||
if ( '' === $rid ) {
|
||||
continue;
|
||||
}
|
||||
$by_run[ $rid ][] = array(
|
||||
'stage' => (string) ( $row['stage'] ?? '' ),
|
||||
'status' => (string) ( $row['status'] ?? 'pending' ),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $cards as $i => $card ) {
|
||||
$rid = (string) ( $card['run_id'] ?? '' );
|
||||
if ( '' !== $rid && isset( $by_run[ $rid ] ) ) {
|
||||
$cards[ $i ]['run_stages_summary'] = $by_run[ $rid ];
|
||||
}
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
}
|
||||
155
includes/ajax/class-board-inspector-handler.php
Normal file
155
includes/ajax/class-board-inspector-handler.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* AJAX handler: per-run stage inspector for the Mission Brief board (M5).
|
||||
*
|
||||
* What: Returns the full per-stage I/O breakdown for a given run_id,
|
||||
* structured for the board's right-rail inspector. Reuses
|
||||
* PRAutoBlogger_Gen_History_Query::get_run_io() (the M4 data layer)
|
||||
* and PRAutoBlogger_Gen_History_Query::get_run_meta() verbatim --
|
||||
* no new DB queries.
|
||||
*
|
||||
* The inspector surfaces prompts and responses. SECURITY is enforced here:
|
||||
* - Authorization headers are NEVER logged (provider build_body() is
|
||||
* captured before header injection -- see ARCHITECTURE.md §Request_Recorder).
|
||||
* - manage_options capability required.
|
||||
* - Nonce: 'prautoblogger_board' (shared with board AJAX, same page).
|
||||
* - All string values esc_html'd before JSON output; JS renders via
|
||||
* textContent, never innerHTML (structurally prevents XSS).
|
||||
* - Read-only -- no writes.
|
||||
*
|
||||
* Triggered by: board.js on run-row click (right-rail fetch).
|
||||
* Dependencies: PRAutoBlogger_Gen_History_Query, PRAutoBlogger_Board_Page,
|
||||
* PRAutoBlogger_Stage_Display_Map, PRAutoBlogger_Dossier_Page.
|
||||
*
|
||||
* @see admin/class-board-page.php -- Registers this handler; supplies nonce.
|
||||
* @see admin/class-gen-history-query.php -- Data layer (get_run_io, get_run_meta).
|
||||
* @see core/class-stage-display-map.php -- Stage label resolution.
|
||||
* @see assets/js/board.js -- Client trigger (row-click inspector fetch).
|
||||
* @see ARCHITECTURE.md -- §Board (M5 Mission Brief).
|
||||
*/
|
||||
class PRAutoBlogger_Board_Inspector_Handler {
|
||||
|
||||
/** AJAX action identifier. */
|
||||
public const ACTION = 'prautoblogger_board_inspector';
|
||||
|
||||
/**
|
||||
* Register wp-ajax hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_hooks(): void {
|
||||
add_action( 'wp_ajax_' . self::ACTION, array( __CLASS__, 'handle' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the inspector AJAX request.
|
||||
*
|
||||
* POST fields:
|
||||
* nonce string wp_nonce for 'prautoblogger_board'.
|
||||
* run_id string Pipeline run UUID.
|
||||
*
|
||||
* Success response JSON:
|
||||
* {
|
||||
* run: { run_id, status, settled_usd, started_at, post_id,
|
||||
* post_title, dossier_url },
|
||||
* stages: [
|
||||
* { stage, label, model, agent_role, prompt_tokens,
|
||||
* completion_tokens, estimated_cost, response_status,
|
||||
* error_message, input_system, input_user,
|
||||
* output, output_pruned }
|
||||
* ],
|
||||
* cost_total: float
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function handle(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ),
|
||||
403
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
check_ajax_referer( PRAutoBlogger_Board_Page::NONCE_ACTION, 'nonce' );
|
||||
|
||||
$run_id = isset( $_POST['run_id'] )
|
||||
? sanitize_text_field( wp_unslash( $_POST['run_id'] ) )
|
||||
: '';
|
||||
|
||||
if ( '' === $run_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Missing run_id.', 'prautoblogger' ) ), 400 );
|
||||
}
|
||||
|
||||
$query = new PRAutoBlogger_Gen_History_Query();
|
||||
$run_meta = $query->get_run_meta( $run_id );
|
||||
|
||||
if ( null === $run_meta ) {
|
||||
// For runs that exist only in gen_log (no runs table row), build minimal meta.
|
||||
$run_meta = array(
|
||||
'run_id' => $run_id,
|
||||
'status' => 'unknown',
|
||||
'settled_usd' => 0,
|
||||
'started_at' => '',
|
||||
'finished_at' => '',
|
||||
'post_id' => 0,
|
||||
'post_title' => '',
|
||||
);
|
||||
}
|
||||
|
||||
$post_id = (int) ( $run_meta['post_id'] ?? 0 );
|
||||
$dossier_url = $post_id > 0
|
||||
? PRAutoBlogger_Dossier_Page::url_for_post( $post_id )
|
||||
: '';
|
||||
|
||||
$run_payload = array(
|
||||
'run_id' => esc_html( (string) ( $run_meta['run_id'] ?? '' ) ),
|
||||
'status' => esc_html( (string) ( $run_meta['status'] ?? '' ) ),
|
||||
'settled_usd' => round( (float) ( $run_meta['settled_usd'] ?? 0 ), 6 ),
|
||||
'started_at' => esc_html( (string) ( $run_meta['started_at'] ?? '' ) ),
|
||||
'post_id' => $post_id,
|
||||
'post_title' => esc_html( (string) ( $run_meta['post_title'] ?? '' ) ),
|
||||
'dossier_url' => esc_url( $dossier_url ),
|
||||
);
|
||||
|
||||
$raw_stages = $query->get_run_io( $run_id );
|
||||
$stages = array();
|
||||
$cost_total = 0.0;
|
||||
|
||||
foreach ( $raw_stages as $stage ) {
|
||||
$cost_total += (float) ( $stage['estimated_cost'] ?? 0 );
|
||||
$stages[] = array(
|
||||
'stage' => esc_html( (string) ( $stage['stage'] ?? '' ) ),
|
||||
'label' => esc_html( PRAutoBlogger_Stage_Display_Map::label( (string) ( $stage['stage'] ?? '' ) ) ),
|
||||
'model' => esc_html( (string) ( $stage['model'] ?? '' ) ),
|
||||
'agent_role' => esc_html( (string) ( $stage['agent_role'] ?? '' ) ),
|
||||
'prompt_tokens' => (int) ( $stage['prompt_tokens'] ?? 0 ),
|
||||
'completion_tokens' => (int) ( $stage['completion_tokens'] ?? 0 ),
|
||||
'estimated_cost' => round( (float) ( $stage['estimated_cost'] ?? 0 ), 6 ),
|
||||
'response_status' => esc_html( (string) ( $stage['response_status'] ?? '' ) ),
|
||||
'error_message' => esc_html( (string) ( $stage['error_message'] ?? '' ) ),
|
||||
// JS renders via textContent -- no innerHTML XSS risk.
|
||||
'input_system' => null !== $stage['input_system']
|
||||
? esc_html( (string) $stage['input_system'] ) : null,
|
||||
'input_user' => null !== $stage['input_user']
|
||||
? esc_html( (string) $stage['input_user'] ) : null,
|
||||
'output' => null !== $stage['output']
|
||||
? esc_html( (string) $stage['output'] ) : null,
|
||||
'output_pruned' => (bool) ( $stage['output_pruned'] ?? false ),
|
||||
);
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'run' => $run_payload,
|
||||
'stages' => $stages,
|
||||
'cost_total' => round( $cost_total, 6 ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,9 @@ class PRAutoBlogger {
|
||||
|
||||
// v0.26.0 (M4): generation run I/O drill-down.
|
||||
PRAutoBlogger_Gen_Run_IO_Handler::register_hooks();
|
||||
|
||||
// v0.27.0 (M5): board inspector rail AJAX.
|
||||
PRAutoBlogger_Board_Inspector_Handler::register_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user