feat: M4 chained-cron checkpoint generation + board button (v0.21.0)

## 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
This commit is contained in:
peptiderepo
2026-06-14 13:33:49 +00:00
parent 326a1af900
commit c7f63b1133
26 changed files with 1145 additions and 250 deletions

View File

@@ -1084,3 +1084,65 @@ PRAutoBlogger and Peptide News both call OpenRouter and may share a single API k
**Important:** If your OpenRouter account has a global spending limit, set each plugin's budget to less than half the total. PRAutoBlogger will hard-stop when its budget is exhausted, but Peptide News currently has no budget enforcement — a spike in news fetches could consume shared quota.
**Future improvement:** A shared `wp_options` key (e.g., `ecosystem_monthly_llm_budget`) that both plugins read, with each plugin reserving its allocation on startup. This requires coordination at the ecosystem level and is tracked as a medium-term goal.
## §25 Chained-Cron Checkpoint Generation Path (v0.21.0, M4)
### Problem
Hostinger shared hosting kills PHP background processes after ~120 s. The former
`on_manual_generation()` path in `Executor` ran the complete orchestrate +
generate pipeline in a single PHP process — meaning a host kill mid-run left the
generation lock held, the run row stuck in `running`, and partially-generated
articles orphaned with no status broadcast.
### Solution: Two-Tick Checkpoint Machine
`PRAutoBlogger_Generation_Checkpoint_Runner` (v0.21.0) splits the pipeline into
independent cron ticks, each of which completes well within Hostinger's limit:
| Tick | WP-Cron action | What happens |
|------|---------------|-------------|
| 1 | `prautoblogger_gen_orchestrate` | Collect sources → analyze → score ideas. Serialize ideas to `prautoblogger_article_queue` option. Schedule Tick 2. |
| 2..N | `prautoblogger_gen_tick` | Pop ONE idea from queue. Call `Article_Worker::generate()`. Persist updated queue. Reschedule next Tick, or finalize. |
If the host kills any tick:
- The idea queue is persisted (option survived the crash).
- The next CRON run picks up naturally (no orphan).
- `Run_Reaper` reclaims truly stuck runs via its daily sweep.
### Entry Points
| Caller | Path |
|--------|------|
| Board "New Article" button | AJAX `prautoblogger_generate_now` → `Generation_Status_Poller::on_ajax_generate_now()` → schedules `prautoblogger_manual_generation` → `Executor::on_manual_generation()` → `kick_off()` |
| WP-CLI | `wp prautoblogger generate` → `WP_CLI_Commands::generate_command()` → `kick_off()` |
| Old cron hook (retained) | `prautoblogger_manual_generation` → `Executor::on_manual_generation()` → `kick_off()` |
### SSH Workaround Retirement
The former `wp eval 'do_action("prautoblogger_manual_generation")'` workaround
fired `on_manual_generation()` synchronously in the CLI PHP process, bypassing:
- Nonce/capability checks (running as CLI user, not WP user)
- Cron-based loopback spawning
- Per-process time limits
This path is retired. `Executor::on_manual_generation()` now delegates immediately
to `Generation_Checkpoint_Runner::on_orchestrate_tick()` (the Tick 1 handler),
which acquires the lock and runs orchestration within its own process. The
`wp prautoblogger generate` WP-CLI command provides the supported CLI path.
### Key State
| Store | Key | Contents |
|-------|-----|---------|
| DB option | `prautoblogger_article_queue` | `{run_id, ideas[], results{}}` — persisted across ticks |
| DB option | `prautoblogger_checkpoint_run_id` | Current run UUID (for finalize/cleanup paths) |
| Transient | `prautoblogger_generation_status` | Status broadcast (polling by board/dossier JS) |
| DB | `prautoblogger_runs` | Per-run ledger with status, cost ceiling |
### Budget / Halt Contract
`Cost_Tracker::is_budget_exceeded()` is checked at the START of Tick 1 and each
generate tick. A halted run (`Run_State::get_status() = 'halted'`) causes the
generate tick to abort without calling `Article_Worker`, clean up the queue, and
release the lock — same guarantee as `Pipeline_Runner`'s queued-article path.