Files
prautoblogger/templates/admin/pipeline-settings-page.php
PRAutoBlogger Engineer baae383c76 feat: Pipeline Settings M4 — generation history list + per-step I/O drill-down (v0.26.0)
CEO ask #5 complete: browsable paginated run list + full input/output inspection
per pipeline stage.

## What was built

### Generations list (prautoblogger-gen-history)
Hidden admin page (options.php-parent, priority 14) showing all pipeline runs newest
first, 20 per page. Each row: article title (linked to dossier when a post exists),
start date, status chip, distinct model(s), settled cost, duration. Queries bounded
(LIMIT/OFFSET) — no unbounded SELECT *.

New files:
- includes/admin/class-gen-history-query.php — paginated run list + per-run I/O queries
- includes/admin/class-gen-history-page.php  — hidden admin page + asset enqueue
- assets/css/gen-history.css, assets/js/gen-history.js — table + drill-down UI
- templates/admin/gen-history-page.php       — HTML template

### Per-step input + output drill-down (Stage I/O)
'Stage I/O' toggle opens an AJAX-loaded inline panel. For each generation_log stage:
  - input_system: system-role message content from generation_log.request_json
  - input_user: last user-role message from request_json (rendered instruction)
  - output: model response from run_stages.meta_json.output
  - output_pruned: true when meta_json present but output key absent (retention prune)

New AJAX endpoint: prautoblogger_gen_run_io (manage_options + nonce)
New file: includes/ajax/class-gen-run-io-handler.php

### gen-log INPUT/OUTPUT coverage verified
- INPUT: generation_log.request_json stores the chat body WITHOUT the Authorization
  header (Request_Recorder captures body after build_body(), before headers are added).
  System + user message content extracted and surfaced.
- OUTPUT: run_stages.meta_json.output is the LLM response text (Run_Stage_State::done()).
  Image/log-only stages (image_a, image_b, llm_research, image_prompt_rewrite) have no
  run_stages row; output is null, NOT pruned. Pruned outputs are flagged honestly.

### Dossier as entry point
Runs with a linked post show a 'Dossier' button deep-linking into the existing dossier
(which renders full per-stage I/O with editorial context). Stage I/O is complementary —
works for failed/orphan runs and provides the same data in a lighter inline UI.

### Stage-list-driven
Stage labels via Stage_Display_Map::label() — Phase 2b curate/seo appear automatically.

### Dead prautoblogger_pipeline_history endpoint
Not dead — it IS used by pipeline-settings.js for prompt version history (M3 feature).
Left in place. No dangling endpoint.

## M3 P2 sweep
- Removed dead data-preview-nonce and data-diff-nonce HTML attrs from
  pipeline-settings-prompt-panel.php (JS uses prabPipeline.*Nonce from
  wp_localize_script, not data-* attrs — confirmed by grep).
- Removed 3 redundant wp_create_nonce() calls from class-pipeline-settings-renderer.php
  (preview_nonce, history_nonce, diff_nonce keys that fed the now-removed dead attrs).
- Updated CONTEXT.md: added M3 + M4 terminology tables.
- Updated ARCHITECTURE.md: added ajax/ section to file tree; M3/M4 design notes.
- Updated INFORMATION-ARCHITECTURE.md: prautoblogger-gen-history entry.

## Security
- manage_options cap + prautoblogger_gen_run_io nonce on the AJAX endpoint.
- All output esc_html()/esc_url() server-side before JSON-encoding.
- JS renders prompt/response text via textContent — never innerHTML.
- No writes from history UI; request_json never contains auth headers.

## Version: v0.25.0 → v0.26.0

Agent-Session: pipeline-settings-m4
2026-06-23 06:57:54 +00:00

207 lines
8.8 KiB
PHP

<?php
/**
* Pipeline Settings page template (M3).
*
* Variables injected by PRAutoBlogger_Pipeline_Settings_Renderer::render():
* array $view['steps'] -- ordered step definitions from Step_Map.
* array $view['active_step'] -- the currently selected step definition.
* array $view['save_result'] -- {status, message} from the save handler.
* string $view['nonce_field'] -- nonce field name.
* string $view['nonce_action'] -- nonce action string.
* string $view['page_slug'] -- pipeline page slug.
* array $view['step_data'] -- assembled view data for the active step.
* float $view['monthly_spend'] -- current month's LLM spend in USD.
* float $view['budget'] -- configured monthly budget cap in USD.
* array $view['global_fields'] -- field defs + current values for global context.
* string $view['step_context'] -- context id matching the active step, or null.
* array $view['step_fields'] -- field defs + current values for active step context.
* string $view['preview_action'] -- (M3) AJAX action name for preview.
* string $view['history_action'] -- (M3) AJAX action name for history.
* string $view['diff_action'] -- (M3) AJAX action name for diff.
*
* @see admin/class-pipeline-settings-renderer.php -- Provides $view.
* @see templates/admin/pipeline-settings-prompt-panel.php -- Prompt editor partial.
* @see templates/admin/pipeline-settings-step-options.php -- Step option fields partial.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$steps = $view['steps'];
$active_step = $view['active_step'];
$save_result = $view['save_result'];
$nonce_field = $view['nonce_field'];
$nonce_action = $view['nonce_action'];
$page_slug = $view['page_slug'];
$step_data = $view['step_data'];
$monthly_spend = $view['monthly_spend'];
$budget = $view['budget'];
$global_fields = $view['global_fields'];
$step_context = $view['step_context'];
$step_fields = $view['step_fields'];
$base_url = admin_url( 'admin.php?page=' . rawurlencode( $page_slug ) );
?>
<div class="wrap ab-wrap pab-pipeline-wrap">
<div class="ab-header">
<div class="ab-header-left">
<span class="dashicons dashicons-networking ab-header-icon"></span>
<div>
<h1 class="ab-header-title"><?php esc_html_e( 'Pipeline Settings', 'prautoblogger' ); ?></h1>
<span class="ab-header-version">v<?php echo esc_html( PRAUTOBLOGGER_VERSION ); ?></span>
</div>
</div>
<div class="ab-header-actions">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=prautoblogger-settings' ) ); ?>" class="ab-btn ab-btn-outline">
<?php esc_html_e( '← All Settings', 'prautoblogger' ); ?>
</a>
</div>
</div>
<div class="ab-stats-bar">
<div class="ab-stat">
<span class="ab-stat-label"><?php esc_html_e( 'Monthly Spend', 'prautoblogger' ); ?></span>
<span class="ab-stat-value">$<?php echo esc_html( number_format( $monthly_spend, 2 ) ); ?> <small>/ $<?php echo esc_html( number_format( $budget, 2 ) ); ?></small></span>
</div>
<div class="ab-stat">
<span class="ab-stat-label"><?php esc_html_e( 'Prompts Table', 'prautoblogger' ); ?></span>
<span class="ab-stat-value">
<?php if ( PRAutoBlogger_Prompt_Registry::is_available() ) : ?>
<span class="pab-badge pab-badge-ok"><?php esc_html_e( 'Available', 'prautoblogger' ); ?></span>
<?php else : ?>
<span class="pab-badge pab-badge-warn"><?php esc_html_e( 'Unavailable — using defaults', 'prautoblogger' ); ?></span>
<?php endif; ?>
</span>
</div>
</div>
<?php if ( 'saved' === $save_result['status'] ) : ?>
<div class="ab-save-notice notice notice-success is-dismissible">
<span class="dashicons dashicons-saved"></span> <?php echo esc_html( $save_result['message'] ); ?>
</div>
<?php elseif ( 'error' === $save_result['status'] ) : ?>
<div class="ab-save-notice notice notice-error is-dismissible">
<span class="dashicons dashicons-warning"></span> <?php echo esc_html( $save_result['message'] ); ?>
</div>
<?php endif; ?>
<?php
// -- Global Content Context block (editable -- M2) -----------------
$context = 'global';
$section_title = __( 'Global Content Context', 'prautoblogger' );
$step_id = 'global';
$fields = $global_fields;
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/pipeline-settings-step-options.php';
?>
<div class="pab-pipeline-layout">
<nav class="pab-step-rail" aria-label="<?php esc_attr_e( 'Pipeline steps', 'prautoblogger' ); ?>">
<?php foreach ( $steps as $i => $step ) : ?>
<?php $is_active = $step['id'] === $active_step['id']; ?>
<a href="<?php echo esc_url( add_query_arg( 'step', $step['id'], $base_url ) ); ?>"
class="pab-step-btn <?php echo $is_active ? 'pab-step-btn--active' : ''; ?>"
aria-current="<?php echo $is_active ? 'page' : 'false'; ?>">
<span class="pab-step-num"><?php echo esc_html( (string) ( $i + 1 ) ); ?></span>
<span class="dashicons <?php echo esc_attr( $step['icon'] ); ?> pab-step-icon"></span>
<span class="pab-step-label"><?php echo esc_html( $step['label'] ); ?></span>
</a>
<?php endforeach; ?>
</nav>
<div class="pab-step-panel">
<div class="pab-panel-header">
<h2><?php echo esc_html( $active_step['label'] ); ?></h2>
<?php if ( ! empty( $active_step['description'] ) ) : ?>
<p class="pab-panel-desc"><?php echo esc_html( $active_step['description'] ); ?></p>
<?php endif; ?>
</div>
<?php
// -- Step option fields (M2) ----------------------------------------
if ( null !== $step_context && ! empty( $step_fields ) ) :
$context = $step_context;
$section_title = __( 'Step Settings', 'prautoblogger' );
$step_id = $active_step['id'];
$fields = $step_fields;
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/pipeline-settings-step-options.php';
endif;
?>
<?php if ( ! empty( $step_data['model_option'] ) ) : ?>
<div class="pab-section">
<h3 class="pab-section-title"><?php esc_html_e( 'Model', 'prautoblogger' ); ?></h3>
<form method="post"
action="<?php echo esc_url( add_query_arg( 'step', $active_step['id'], $base_url ) ); ?>"
class="pab-model-form">
<?php wp_nonce_field( $nonce_action, $nonce_field ); ?>
<input type="hidden" name="pipeline_action" value="save_model" />
<input type="hidden" name="model_option" value="<?php echo esc_attr( $step_data['model_option'] ); ?>" />
<div class="pab-model-picker-wrap">
<?php
$renderer = new PRAutoBlogger_Pipeline_Settings_Renderer();
$renderer->render_model_picker(
$step_data['model_option'],
$step_data['model_value'],
array( 'capability' => $active_step['capability'] ?? 'text→text' )
);
?>
</div>
<p class="pab-field-note">
<?php esc_html_e( 'Changing the model here also updates the corresponding field in Settings → AI Models.', 'prautoblogger' ); ?>
</p>
<button type="submit" class="ab-btn ab-btn-secondary ab-btn-sm">
<?php esc_html_e( 'Save Model', 'prautoblogger' ); ?>
</button>
</form>
</div>
<?php endif; ?>
<?php if ( ! empty( $step_data['params'] ) ) : ?>
<div class="pab-section">
<h3 class="pab-section-title"><?php esc_html_e( 'Parameters', 'prautoblogger' ); ?></h3>
<table class="pab-params-table">
<?php foreach ( $step_data['params'] as $param_name => $param_value ) : ?>
<tr>
<th><?php echo esc_html( $param_name ); ?></th>
<td><code class="pab-mono"><?php echo is_array( $param_value ) ? esc_html( wp_json_encode( $param_value ) ) : esc_html( (string) $param_value ); ?></code></td>
</tr>
<?php endforeach; ?>
</table>
<p class="pab-field-note">
<?php esc_html_e( 'Parameters are defined in the prompt registry defaults. Editing params is a Phase 2b feature.', 'prautoblogger' ); ?>
</p>
</div>
<?php endif; ?>
<?php
if ( ! empty( $step_data['system'] ) ) :
$panel = $step_data['system'];
$panel_title = __( 'System Instructions', 'prautoblogger' );
$step_id = $active_step['id'];
$is_writer = ( 'writer' === $step_id );
$writing_mode = (string) get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/pipeline-settings-prompt-panel.php';
endif;
?>
<?php
foreach ( $step_data['agent_panels'] as $key => $panel ) :
$panel_title = sprintf(
/* translators: %s = registry key e.g. 'content.draft' */
__( 'Prompt: %s', 'prautoblogger' ),
$key
);
$step_id = $active_step['id'];
$is_writer = ( 'writer' === $step_id );
$writing_mode = (string) get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/pipeline-settings-prompt-panel.php';
endforeach;
?>
</div>
</div>
</div>