## Board 'New Article' button
- templates/admin/board-page.php: add #prab-board-generate-now button + status div
- includes/admin/class-board-page.php: inject generateNonce + i18n strings into prabBoard;
enqueue companion board-generate.js
- assets/js/board-generate.js: NEW – board button AJAX handler (fires
prautoblogger_generate_now, resets board backoff on success)
- assets/js/board.js: applySnapshot() re-enables button when no active runs
## Chained-cron checkpoint machine (retires SSH workaround)
- includes/core/class-generation-checkpoint-runner.php: NEW (295 lines)
Tick 1 (prautoblogger_gen_orchestrate): orchestrate_only() → persist queue → schedule Tick 2
Tick 2..N (prautoblogger_gen_tick): pop one idea → Article_Worker → reschedule or finalize
Budget/halt contract + lock release on all exit paths
- includes/class-executor.php: on_manual_generation() replaced with 1-line delegate to
Generation_Checkpoint_Runner::on_orchestrate_tick() (SSH workaround retired)
- includes/class-prautoblogger.php: register ORCHESTRATE_ACTION + GENERATE_ACTION hooks
- includes/core/class-pipeline-runner.php: expose orchestrate_only() public wrapper
- includes/core/class-pipeline-status.php: add write_initial() + write_error() helpers
## WP-CLI command
- includes/admin/class-wp-cli-commands.php: wp prautoblogger generate → kick_off()
## IA / perf pass (settings-backed pagination + DB indexes)
- includes/admin/class-board-data-provider.php: replace hardcoded posts_per_page=20 with
get_option('prautoblogger_board_column_limit', DEFAULT)
- includes/admin/class-board-gen-log-query.php: replace LIMIT 10/20 literals with settings-backed values
- includes/admin/class-ideas-browser-query.php: NEW – query helpers extracted from Ideas_Browser
(300-line rule split): query_ideas(), get_type_counts(), load_idea_data(),
find_post_by_run_id(), update_idea_stage()
- includes/admin/class-ideas-browser.php: delegates all query work to Ideas_Browser_Query
- includes/class-activator.php: set_default_options() adds board_column_limit + ideas_per_page
- includes/admin/class-admin-page.php: add new settings to numeric sanitization list
- prautoblogger.php: PRAUTOBLOGGER_DEFAULT_BOARD_COLUMN_LIMIT=20, PRAUTOBLOGGER_DEFAULT_IDEAS_PER_PAGE=30
- includes/class-schema-installer.php: indexes on generation_log + analysis_results (db 1.4.0)
- includes/class-pipeline-schema-installer.php: KEY started_at on runs table
## Guard P2s
- scripts/check-constants.php: --self-test mode + inline CONSTANTS_GUARD_WINDOW comment
- .github/workflows/ci.yml: check-constants-self-test step after main constants check
## PHPUnit test
- tests/unit/Core/GenerationCheckpointRunnerTest.php: NEW (227 lines)
4 tests: kick_off schedules+transient, empty-queue finalizes, halted-run aborts+cleanup,
multi-idea pops-one-and-reschedules
## Docs + version
- prautoblogger.php: Version 0.21.0, PRAUTOBLOGGER_DB_VERSION 1.4.0
- ARCHITECTURE.md: §25 Chained-Cron Checkpoint Generation Path
- CHANGELOG.md: v0.21.0 entry
- INFORMATION-ARCHITECTURE.md: NEW – admin pages, entry points, DB tables, options, CLI
- uninstall.php: purge prautoblogger_gen_orchestrate + prautoblogger_gen_tick cron hooks
Agent-Session: phase2-admin-m4-20260614
196 lines
7.0 KiB
PHP
196 lines
7.0 KiB
PHP
<?php
|
||
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.
|
||
*
|
||
* Board columns: Generating | In Review | Published | Failed.
|
||
* Cards live-update via AJAX polling at a settings-backed interval (default 5s),
|
||
* backing off to 2× when no active generating run is detected.
|
||
*
|
||
* 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.
|
||
*
|
||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||
* Dependencies: PRAutoBlogger_Board_Data_Provider, 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 — §Board (kanban dashboard).
|
||
*/
|
||
class PRAutoBlogger_Board_Page {
|
||
|
||
private const PAGE_SLUG = 'prautoblogger-board';
|
||
|
||
/** AJAX action name for board polling. */
|
||
public const AJAX_ACTION = 'prautoblogger_board_status';
|
||
|
||
/** Nonce action string for board AJAX. */
|
||
public const NONCE_ACTION = 'prautoblogger_board';
|
||
|
||
/**
|
||
* Register the Board as the PRIMARY (first) submenu item, overriding the
|
||
* implicit Settings landing page. The trick is: WordPress auto-generates a
|
||
* submenu duplicate of the top-level page; we name it "Board" so the
|
||
* first click goes to the board, not the raw settings page.
|
||
*
|
||
* Registration order note: this method is hooked at admin_menu priority 11
|
||
* so that PRAutoBlogger_Admin_Page::on_register_menu() (priority 10) has
|
||
* already called add_menu_page(). This ensures WordPress has populated
|
||
* $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 §Board.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function on_register_menu(): void {
|
||
// Override the auto-created "PRAutoBlogger" submenu entry with "Board".
|
||
add_submenu_page(
|
||
'prautoblogger-settings',
|
||
__( 'PRAutoBlogger Board', 'prautoblogger' ),
|
||
__( 'Board', 'prautoblogger' ),
|
||
'manage_options',
|
||
self::PAGE_SLUG,
|
||
array( $this, 'render_page' )
|
||
);
|
||
|
||
// Reorder submenu so Board is the FIRST entry (primary click target).
|
||
// WordPress auto-appended a duplicate of the top-level page as submenu
|
||
// index 0; our Board was appended at index 1. We swap them so the
|
||
// top-level PRAutoBlogger click lands on the Board page, not Settings.
|
||
// 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.
|
||
$board_idx = null;
|
||
foreach ( $items as $i => $item ) {
|
||
if ( isset( $item[2] ) && self::PAGE_SLUG === $item[2] ) {
|
||
$board_idx = $i;
|
||
break;
|
||
}
|
||
}
|
||
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 ) );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Render the board page.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function render_page(): void {
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
return;
|
||
}
|
||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/board-page.php';
|
||
}
|
||
|
||
/**
|
||
* Enqueue board-specific JS + CSS and localize the poller config.
|
||
*
|
||
* Called via `admin_enqueue_scripts` action, gated to this page's hook suffix.
|
||
*
|
||
* @param string $hook_suffix Current admin page hook suffix.
|
||
* @return void
|
||
*/
|
||
public function on_enqueue_assets( string $hook_suffix ): void {
|
||
$board_hook = 'prautoblogger_page_' . self::PAGE_SLUG;
|
||
if ( $hook_suffix !== $board_hook ) {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_style(
|
||
'prautoblogger-board',
|
||
PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/board.css',
|
||
array( 'prautoblogger-admin' ),
|
||
PRAUTOBLOGGER_VERSION
|
||
);
|
||
|
||
wp_enqueue_script(
|
||
'prautoblogger-board',
|
||
PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/board.js',
|
||
array( 'jquery' ),
|
||
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 ) );
|
||
|
||
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,
|
||
'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' ),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* AJAX handler: return the current board snapshot (all four columns).
|
||
*
|
||
* Nonce: `prautoblogger_board`. Requires `manage_options` capability.
|
||
*
|
||
* Side effects: DB reads only — no writes.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function on_ajax_board_status(): void {
|
||
check_ajax_referer( self::NONCE_ACTION, 'nonce' );
|
||
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ), 403 );
|
||
return;
|
||
}
|
||
|
||
$provider = new PRAutoBlogger_Board_Data_Provider();
|
||
$snapshot = $provider->get_board_snapshot();
|
||
|
||
wp_send_json_success( $snapshot );
|
||
}
|
||
}
|