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
This commit is contained in:
204
assets/js/gen-history.js
Normal file
204
assets/js/gen-history.js
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Generation History page — Stage I/O drill-down (M4).
|
||||
*
|
||||
* Handles the "Stage I/O" toggle button on each run row:
|
||||
* 1. On first click: POST to prautoblogger_gen_run_io, receive stage
|
||||
* data, render the per-stage accordion (input_system, input_user,
|
||||
* output).
|
||||
* 2. On subsequent clicks: toggle the row's visibility (no re-fetch).
|
||||
*
|
||||
* SECURITY: Renders all text via textContent — NEVER innerHTML for
|
||||
* prompt/response content. The nonce is passed via wp_localize_script
|
||||
* as prabGenHistory.nonce (not a data-* attribute).
|
||||
*
|
||||
* @see ajax/class-gen-run-io-handler.php -- AJAX endpoint.
|
||||
* @see admin/class-gen-history-page.php -- Localises prabGenHistory config.
|
||||
*/
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
/** @type {{ ajaxUrl: string, action: string, nonce: string, strings: Object }} */
|
||||
var cfg = window.prabGenHistory || {};
|
||||
|
||||
/**
|
||||
* Render a single stage's I/O block.
|
||||
*
|
||||
* @param {Object} stage Stage data from AJAX response.
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function renderStage( stage ) {
|
||||
var wrap = document.createElement( 'div' );
|
||||
wrap.className = 'prab-io-stage';
|
||||
|
||||
// Header.
|
||||
var header = document.createElement( 'div' );
|
||||
header.className = 'prab-io-stage__header';
|
||||
|
||||
var label = document.createElement( 'span' );
|
||||
label.className = 'prab-io-stage__label';
|
||||
label.textContent = stage.label || stage.stage;
|
||||
header.appendChild( label );
|
||||
|
||||
var meta = document.createElement( 'span' );
|
||||
meta.className = 'prab-io-stage__meta';
|
||||
var costStr = stage.estimated_cost ? '$' + stage.estimated_cost.toFixed( 6 ) : '';
|
||||
var tokStr = stage.prompt_tokens
|
||||
? stage.prompt_tokens + ' in / ' + stage.completion_tokens + ' out tokens'
|
||||
: '';
|
||||
var metaParts = [ stage.model, tokStr, costStr ].filter( Boolean );
|
||||
meta.textContent = metaParts.join( ' · ' );
|
||||
header.appendChild( meta );
|
||||
|
||||
var statusChip = document.createElement( 'span' );
|
||||
statusChip.className = 'prab-chip prab-chip--' + ( stage.response_status || '' );
|
||||
statusChip.textContent = stage.response_status || '';
|
||||
header.appendChild( statusChip );
|
||||
|
||||
wrap.appendChild( header );
|
||||
|
||||
// Body: system input, user input, output.
|
||||
var body = document.createElement( 'div' );
|
||||
body.className = 'prab-io-stage__body';
|
||||
|
||||
body.appendChild( renderSection(
|
||||
/* translators: label for system prompt section */
|
||||
cfg.strings && cfg.strings.systemLabel || 'System Prompt (Input)',
|
||||
stage.input_system,
|
||||
! stage.input_system ? ( cfg.strings.noInput || '[No input stored]' ) : null
|
||||
) );
|
||||
|
||||
body.appendChild( renderSection(
|
||||
/* translators: label for user instruction section */
|
||||
cfg.strings && cfg.strings.userLabel || 'Assembled Instruction (Input)',
|
||||
stage.input_user,
|
||||
! stage.input_user ? ( cfg.strings.noInput || '[No input stored]' ) : null
|
||||
) );
|
||||
|
||||
var outputMissing = null;
|
||||
if ( stage.output_pruned ) {
|
||||
outputMissing = cfg.strings.pruned || '[Output pruned by retention policy]';
|
||||
} else if ( stage.output === null ) {
|
||||
outputMissing = cfg.strings.noOutput || '[No output stored for this stage]';
|
||||
}
|
||||
body.appendChild( renderSection(
|
||||
cfg.strings && cfg.strings.outputLabel || 'Model Response (Output)',
|
||||
stage.output,
|
||||
outputMissing
|
||||
) );
|
||||
|
||||
if ( stage.error_message ) {
|
||||
body.appendChild( renderSection( 'Error', stage.error_message, null ) );
|
||||
}
|
||||
|
||||
wrap.appendChild( body );
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a labelled text section.
|
||||
*
|
||||
* @param {string} title Section heading.
|
||||
* @param {string|null} text Content text (null when missing).
|
||||
* @param {string|null} missingMsg Message to show when text is null.
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
function renderSection( title, text, missingMsg ) {
|
||||
var section = document.createElement( 'div' );
|
||||
section.className = 'prab-io-section';
|
||||
|
||||
var heading = document.createElement( 'div' );
|
||||
heading.className = 'prab-io-section__title';
|
||||
heading.textContent = title;
|
||||
section.appendChild( heading );
|
||||
|
||||
var content = document.createElement( 'pre' );
|
||||
if ( text ) {
|
||||
content.className = 'prab-io-section__text';
|
||||
// textContent — never innerHTML — for security.
|
||||
content.textContent = text;
|
||||
} else {
|
||||
content.className = 'prab-io-section__text prab-io-section__text--missing';
|
||||
content.textContent = missingMsg || '';
|
||||
}
|
||||
section.appendChild( content );
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and render stage I/O for a run.
|
||||
*
|
||||
* @param {string} runId Pipeline run UUID.
|
||||
* @param {HTMLElement} panel The panel container element to populate.
|
||||
*/
|
||||
function fetchRunIO( runId, panel ) {
|
||||
var fd = new FormData();
|
||||
fd.append( 'action', cfg.action || 'prautoblogger_gen_run_io' );
|
||||
fd.append( 'nonce', cfg.nonce || '' );
|
||||
fd.append( 'run_id', runId );
|
||||
|
||||
fetch( cfg.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' } )
|
||||
.then( function ( r ) { return r.json(); } )
|
||||
.then( function ( resp ) {
|
||||
panel.innerHTML = '';
|
||||
if ( ! resp.success ) {
|
||||
var err = document.createElement( 'p' );
|
||||
err.textContent = ( resp.data && resp.data.message ) || cfg.strings.error;
|
||||
panel.appendChild( err );
|
||||
return;
|
||||
}
|
||||
var stages = resp.data.stages || [];
|
||||
if ( 0 === stages.length ) {
|
||||
var none = document.createElement( 'p' );
|
||||
none.textContent = cfg.strings.noStages || 'No stage entries found.';
|
||||
panel.appendChild( none );
|
||||
return;
|
||||
}
|
||||
stages.forEach( function ( s ) {
|
||||
panel.appendChild( renderStage( s ) );
|
||||
} );
|
||||
// If dossier URL available, append a link.
|
||||
var run = resp.data.run || {};
|
||||
if ( run.dossier_url ) {
|
||||
var link = document.createElement( 'p' );
|
||||
link.style.marginTop = '12px';
|
||||
var a = document.createElement( 'a' );
|
||||
a.href = run.dossier_url;
|
||||
a.textContent = 'Open full Article Dossier →';
|
||||
link.appendChild( a );
|
||||
panel.appendChild( link );
|
||||
}
|
||||
} )
|
||||
.catch( function () {
|
||||
panel.innerHTML = '';
|
||||
var err = document.createElement( 'p' );
|
||||
err.textContent = cfg.strings.error || 'Request failed.';
|
||||
panel.appendChild( err );
|
||||
} );
|
||||
}
|
||||
|
||||
/** Wire "Stage I/O" toggle buttons on DOMContentLoaded. */
|
||||
document.addEventListener( 'DOMContentLoaded', function () {
|
||||
var buttons = document.querySelectorAll( '.prab-gen-history__io-toggle' );
|
||||
buttons.forEach( function ( btn ) {
|
||||
var runId = btn.dataset.runId;
|
||||
var ioRow = document.getElementById( 'prab-io-' + runId );
|
||||
if ( ! ioRow ) {
|
||||
return;
|
||||
}
|
||||
var panel = ioRow.querySelector( '.prab-gen-history__io-panel' );
|
||||
var loaded = false;
|
||||
|
||||
btn.addEventListener( 'click', function () {
|
||||
if ( ioRow.hidden ) {
|
||||
ioRow.hidden = false;
|
||||
if ( ! loaded ) {
|
||||
loaded = true;
|
||||
fetchRunIO( runId, panel );
|
||||
}
|
||||
} else {
|
||||
ioRow.hidden = true;
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} )();
|
||||
Reference in New Issue
Block a user