/** * PRAutoBlogger — Mission Brief Board (M5, v0.27.0) * * Replaces the four-column kanban with a vertical run-list + right-rail * inspector layout. Responsibilities: * * 1. AJAX polling (prautoblogger_board_status) — same backoff logic as M4. * Updates the four status sections in-place without flicker. * 2. Row-click inspector — fetches per-run stage I/O via * prautoblogger_board_inspector and renders the right rail. * 3. Stage I/O expand — toggles per-stage prompt/response text inline * inside the inspector. * * Security: All server data rendered via textContent (never innerHTML for * prompt/response text). escHtml() guards any dynamic HTML concatenation. * * Localised config: `prabBoard` object (ajaxUrl, nonce, pollInterval, * action, inspectorAction, strings) injected by Board_Page::on_enqueue_assets(). * * @see includes/admin/class-board-page.php -- AJAX handler + localize. * @see includes/ajax/class-board-inspector-handler.php -- Inspector AJAX. * @see assets/css/board.css -- Styles. * @see templates/admin/board-page.php -- Server-rendered HTML. */ ( function ( $ ) { 'use strict'; var cfg = window.prabBoard || {}; var baseInterval = cfg.pollInterval || 5000; var maxBackoff = 4; var backoffFactor = 1; var pollTimer = null; var $board = null; var $errorBanner = null; var selectedRunId = null; var inspFetching = false; /** Escape HTML for safe DOM insertion via innerHTML. */ function escHtml( str ) { return $( '
' + escHtml( cfg.strings.empty || '' ) + '
' ); return; } var html = ''; for ( var i = 0; i < cards.length; i++ ) { html += buildRowHtml( cards[ i ], secKey ); } $rows.html( html ); // Re-highlight selected row if it reappears after poll. if ( selectedRunId ) { $rows.find( '[data-run-id="' + selectedRunId + '"]' ).addClass( 'is-selected' ); } // Re-bind row-click for freshly rendered rows. bindRowClicks( $rows ); } /** * Apply snapshot to all four sections. * * @param {Object} snapshot Board snapshot from AJAX. * @returns {void} */ function applySnapshot( snapshot ) { renderSection( 'generating', snapshot.generating || [] ); renderSection( 'in_review', snapshot.in_review || [] ); renderSection( 'published', snapshot.published || [] ); renderSection( 'failed', snapshot.failed || [] ); } // ── Inspector rail ──────────────────────────────────────────────────── /** * Render the inspector rail with a loaded run's data. * * @param {Object} data Response data from Board_Inspector_Handler. * @returns {void} */ function renderInspector( data ) { var run = data.run || {}; var stages = data.stages || []; var total = data.cost_total || 0; var $inner = $( '#prab-inspector-inner' ); $inner.empty(); // Title + chip. var titleText = run.post_title || run.run_id || ''; var $title = $( '' ).text( titleText ); $inner.append( $title ); var statusMod = run.status || 'generating'; var statusMap = { running: 'generating', complete: 'published', error: 'failed', draft: 'in_review', }; var chipMod = statusMap[ statusMod ] || statusMod; var chipLabel = sectionMeta( chipMod ).label; var $chipRow = $( '' ); $chipRow.append( $( '' ).text( chipLabel ) ); $inner.append( $chipRow ); // Stage breakdown. var $stageList = $( '' + escHtml( cfg.strings.inspectorLoad || 'Loading…' ) + '
' ); $.ajax( { url: cfg.ajaxUrl, method: 'POST', data: { action: cfg.inspectorAction, nonce: cfg.nonce, run_id: runId }, success: function ( response ) { inspFetching = false; if ( response && response.success && response.data ) { renderInspector( response.data ); } else { $inner.html( '' + escHtml( cfg.strings.inspectorError || 'Could not load run details.' ) + '
' ); } }, error: function () { inspFetching = false; $inner.html( '' + escHtml( cfg.strings.inspectorError || 'Could not load run details.' ) + '
' ); } } ); } // ── Row click binding ───────────────────────────────────────────────── /** * Bind click + keyboard-activate on run rows within a container. * * @param {jQuery} $container Container to search within. * @returns {void} */ function bindRowClicks( $container ) { $container.find( '.prab-run-row' ).on( 'click keydown', function ( e ) { if ( e.type === 'keydown' && e.which !== 13 && e.which !== 32 ) { return; } e.preventDefault(); var $row = $( this ); var runId = $row.data( 'run-id' ); if ( ! runId ) { return; } // Toggle off if already selected. if ( selectedRunId === String( runId ) ) { selectedRunId = null; $row.removeClass( 'is-selected' ); $( '#prab-inspector-inner' ).html( '' + escHtml( cfg.strings.inspectorEmpty || 'Select an article to preview its pipeline.' ) + '
' ); return; } // Deselect all rows, select this one. $board.find( '.prab-run-row' ).removeClass( 'is-selected' ); $row.addClass( 'is-selected' ); selectedRunId = String( runId ); loadInspector( selectedRunId ); } ); } // ── Polling ─────────────────────────────────────────────────────────── /** Compute next poll delay. */ function nextDelay( hasActiveRuns ) { if ( hasActiveRuns ) { backoffFactor = 1; return baseInterval; } backoffFactor = Math.min( backoffFactor + 1, maxBackoff ); return baseInterval * backoffFactor; } /** Schedule the next poll. */ function schedulePoll( delay ) { clearTimeout( pollTimer ); pollTimer = setTimeout( doPoll, delay ); } /** Execute one poll cycle. */ function doPoll() { $.ajax( { url: cfg.ajaxUrl, method: 'POST', data: { action: cfg.action, nonce: cfg.nonce }, success: function ( response ) { if ( $errorBanner ) { $errorBanner.removeClass( 'is-visible' ); } if ( response && response.success && response.data ) { applySnapshot( response.data ); schedulePoll( nextDelay( !! response.data.has_active_runs ) ); } else { schedulePoll( nextDelay( false ) ); } }, error: function () { if ( $errorBanner ) { $errorBanner.addClass( 'is-visible' ); } schedulePoll( nextDelay( false ) ); } } ); } // ── Init ────────────────────────────────────────────────────────────── $( function () { $board = $( '#prab-board' ); if ( ! $board.length ) { return; } // Error banner. $errorBanner = $( '' ); $errorBanner.text( cfg.strings.pollError || '' ); $board.before( $errorBanner ); // Bind clicks on server-rendered rows. bindRowClicks( $board ); // Start poller. var hasActive = ( $board.find( '[data-section="generating"] .prab-run-row' ).length > 0 ); schedulePoll( nextDelay( hasActive ) ); } ); }( jQuery ) );