- PRAUTOBLOGGER_VERSION constant now matches header (0.10.1)
- CHANGELOG.md updated to accurately reflect Round 3 work:
* OpenRouter test encryption fix (real class round-trip)
* 137 PHPCS violations resolved with targeted ignores + reasons
* Clear accounting of which categories of violations and why ignored
- Added phpcs:ignore for class file naming convention (79 violations)
WordPress.Files.FileName.InvalidClassFileName is a known architectural
issue — class files use short names without prautoblogger- prefix
- Added phpcs:ignore for ->prepare() table interpolation (56 violations)
WordPress.DB.PreparedSQL.InterpolatedNotPrepared: dynamic table names
(prab_generation_logs, prab_cost_logs) require interpolation; ->prepare()
does not support table name placeholders, only column values
- Fixed file-level docblock positioning (2 violations)
prautoblogger.php and uninstall.php: moved declare(strict_types=1) before
file docblock per PHP best practices
Bump version: 0.10.0 → 0.10.1
Updated CHANGELOG.md with Fixed/Changed entries:
- Missing post_type_exists() stub in BaseTestCase
- PHPCS autofix of 1,362 violations across 75 files
- CI gate flipped to strict mode
Updated ARCHITECTURE.md with ADR #20 documenting the test infra
and PHPCS compliance improvements.
Run phpcbf --standard=WordPress-Core across includes/, prautoblogger.php,
and uninstall.php. Fixed 1,362 formatting violations in 75 files:
- Short array syntax: [] → array()
- Line breaking for long function calls
- Consistent indentation
- Function call parameter alignment
No logic changes; purely mechanical formatting.
- prautoblogger.php: plugin header Version + PRAUTOBLOGGER_VERSION
- CHANGELOG.md: Removed/Changed/Note/Cost impact/Migration sections
- ARCHITECTURE.md:
- drop 4 CF provider files from the file tree
- drop 2 CF credential options from the wp_options table
- drop CF Workers AI row from the External API Integrations table
- flip Phase 3 placeholder from CF Workers AI registry to Runware
- supersede ADR #16 with a one-line history note pointing at v0.10.0
- update ADR #17 tail to mention the removal
readme.txt stable tag is stale (0.2.2) and clearly not maintained
inline with real releases — leaving alone in this PR; fix belongs in
a dedicated release-process cleanup.
Wires out the Cloudflare Workers AI image provider across the plugin:
- class-image-pipeline.php: drop 'cloudflare' dispatch branch; Runware
becomes the unrecognised-provider fallback
- class-image-model-registry.php: remove flux-1-schnell/cloudflare row;
update provider-enum docblocks (runware|openrouter only)
- class-settings-fields-extended.php: remove three CF fields
(cloudflare_ai_token, cloudflare_account_id, cf_image_via_gateway);
drop Cloudflare mention from model-picker + NSFW retry descriptions
- class-admin-page.php: remove prautoblogger_cloudflare_ai_token from
the encrypted-options list
- interface-image-provider.php: retarget preamble @see at Runware +
OpenRouter; update get_provider_name() example
- class-image-nsfw-blocked.php: generalise preamble (Runware +
OpenRouter can raise this)
Also cleans stale CF Workers AI mentions in:
class-ajax-handlers.php, interface-model-registry.php,
class-image-nsfw-retry.php, class-open-router-image-pricing.php,
prautoblogger.php.
The Cloudflare AI Gateway path (in class-open-router-config.php,
class-settings-fields.php 'prautoblogger_ai_gateway_base_url',
class-open-router-request-builder.php cache headers) is intentionally
untouched — it fronts OpenRouter and is unrelated to the CF Workers AI
image provider.
New Runware image backend at ~$0.0006/image (FLUX.1 schnell) — 65× cheaper
than Gemini 2.5 Flash Image. Provider mirrors OpenRouter's split: interface
+ provider + support + pricing + batch, all under the 300-line cap. Batch
class uses curl_multi for true parallel A/B dispatch — wall-clock drops to
the slowest single image instead of the sum.
Changes:
- Add PRAutoBlogger_Runware_Image_Provider + Pricing + Support + Batch
- Flip PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER/MODEL to runware:100@1
- Add prautoblogger_runware_api_key settings field (encrypted at rest)
- v0.9.0 one-time migration flips legacy default model + re-derives provider
- Wire runware branch into Image_Pipeline::create_default_provider()
- Add runware:100@1 and runware:101@1 to Image_Model_Registry (defaults top)
- Update image prompt builder: no more anthropomorphized molecules;
prefer concrete human-scale scenes (gym bros, doctors, scientists)
- ARCHITECTURE.md + CHANGELOG.md updated with new integration + decision
- Unit tests for pricing, support, provider; update model registry test
Cost impact at 100 images/day (50 articles × A/B):
- Before (Gemini via OpenRouter): $3.90/day ≈ $117/mo
- After (FLUX schnell via Runware): $0.06/day ≈ $1.80/mo
Net saving: ~$115/mo
* feat(images): route Cloudflare Workers AI via AI Gateway by default
Direct probe on 2026-04-21 confirmed the Cloudflare AI Gateway
route to Workers AI (`gateway.ai.cloudflare.com/.../workers-ai/@cf/...`)
is no longer 403ing — the upstream regression noted in v0.8.0's
ARCHITECTURE.md #16 and in thread image-pipeline seq 6 has been
resolved. Routing via the gateway gives us the same benefits we
already get for OpenRouter LLM calls: response caching, cost/latency
dashboard, rate limiting.
Changes:
- `Cloudflare_Image_Support::build_endpoint_url()` now prefers the
gateway URL derived from the existing `prautoblogger_ai_gateway_base_url`
option. Direct API remains the fallback.
- New private helper `build_gateway_workers_ai_url()` strips the
OpenRouter path suffix from the configured gateway URL and appends
`/workers-ai/{model}`. Returns empty string (fallback to direct) when:
no gateway URL configured, the new toggle is off, or the URL isn't a
valid `https://gateway.ai.cloudflare.com/...` shape.
- New admin toggle `prautoblogger_cf_image_via_gateway` (default on) in
the Images section. Operators can disable to force direct API without
clearing the LLM-path gateway URL.
- ARCHITECTURE.md #16 rewritten to reflect the v0.8.2+ gateway routing.
- CHANGELOG.md v0.8.2 entry under Changed.
Tests (new `CloudflareImageSupportTest`):
- Direct URL when gateway not configured.
- Gateway URL derived correctly when configured + toggle on.
- Toggle off forces direct URL even with gateway configured.
- Malformed gateway URL falls back to direct (defensive).
- `is_nsfw_error()` only matches error code 3030 (pinned separately so
a tolerance regression fails early).
Cost impact: near-zero. Gateway adds a few ms of latency and enables
response caching — could *reduce* cost on identical prompts within the
configurable cache TTL.
Uninstall: new option covered by existing prautoblogger_% LIKE sweep.
Implements Commit 1b (narrow AI-Gateway-migration scope) per thread
image-pipeline seq 7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(cost): seed new toggles in activator defaults; tighten URL stripper
QA review on cd45ae2 surfaced two things; one real, one hallucinated:
- **Real (fixed):** `prautoblogger_cf_image_via_gateway` and v0.8.1's
`prautoblogger_image_nsfw_retry` weren't seeded in
`Activator::set_default_options()`. Belt-and-braces — the `get_option()`
fallback at the read site already defaults to '1', so no user-visible
regression exists, but seeding the options follows the established
pattern for every other plugin setting and makes intent legible to
operators inspecting `wp_options` directly.
- **Hallucinated (rejected):** the reviewer claimed
`CloudflareImageSupportTest` methods 2–5 were empty stubs. Verified
— every test has real `$this->assert*` calls (10 total across the 5
cases). Ignoring. Flagging the false-positive to the CTO per the
qa-review quality-tracking protocol.
Also clarified the docblock + tightened the ternary inside
`build_gateway_workers_ai_url()` — explicit `strrpos` false-check
instead of the `?:` short-circuit, so a no-slash base (defensive edge)
falls through cleanly instead of returning a weirdly-sliced URL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(schedule): interpret `prautoblogger_schedule_time` in site timezone
Per thread scheduled-gen-timezone seq 1: WordPress core calls
`date_default_timezone_set('UTC')` early in bootstrap, so the activator's
`strtotime("tomorrow HH:MM")` was resolving in UTC regardless of the
site's configured timezone. On peptiderepo.com (Asia/Singapore, UTC+8)
a `schedule_time` of `06:00` fired at 14:00 local.
Fold this into the existing v0.8.2 branch (PR #102) since both this fix
and the earlier CF-image-gateway change target the same version and
touch overlapping files (activator, settings-fields-extended).
Changes:
- `Activator::next_daily_generation_timestamp()` — new public helper
that reads `prautoblogger_schedule_time`, constructs a
`DateTimeImmutable('tomorrow', wp_timezone())`, applies the
configured hour/minute, returns a Unix timestamp. Defensive fallback
to the old UTC-interpretation behaviour on any DateTime parse error
so a bad tz string can't brick the schedule.
- `Activator::schedule_cron()` delegates to the helper instead of
calling `strtotime` directly.
- New `Activator::reschedule_daily_in_site_timezone_v082()` — one-shot
migration that clears the stale UTC-scheduled event and re-queues
using the helper. Gated by `prautoblogger_migrated_schedule_tz_v082`
option so it runs exactly once. Logs an INFO line comparing
before/after next_run for auditability.
- `class-prautoblogger.php::on_check_db_version()` invokes the
migration on every admin_init; the one-shot flag handles re-run
guarding (no DB_VERSION bump needed — the migration is independent
of the schema version).
- Settings description for "Generation Time" now says
"...in your site's configured timezone (Settings → General →
Timezone). Current site timezone: <wp_timezone_string()>" instead
of the ambiguous "server timezone" phrasing.
File-size compliance:
- Adding the timezone helpers pushed `class-activator.php` to 352
lines. Extracted the 104-line `create_tables()` SQL to a new
`class-schema-installer.php` (136 lines). Activator now 245 lines,
schema file fully dedicated to dbDelta.
Tests (new):
- `ActivatorScheduleTest` — SGT 06:00 → 22:00 UTC prev day; UTC 06:00
→ 06:00 UTC (no-op case); Asia/Kathmandu UTC+5:45 → 00:15 UTC
(fractional offset); malformed time input doesn't throw.
- `ActivatorMigrationTest` — pre-seeded UTC cron gets cleared and
rescheduled in SGT; second pass is idempotent.
CHANGELOG v0.8.2 entry now also covers the timezone fix under Fixed +
the description wording change under Changed.
Implements thread `scheduled-gen-timezone` seq 1 spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cost): persist run_id on generated posts + back-populate for v0.8.1 reaper
The orphan-research-row reaper (upcoming commit) needs a cheap way to
answer "which posts belong to this run_id?" when amortizing a stranded
`llm_research` row back to its siblings. Relying on the generation_log
post_id column is circular: if the pipeline died before amortize_research
ran, we'd trust the same table we're trying to repair.
- `Publisher::build_meta()` now accepts `?string $run_id` and writes
`_prautoblogger_run_id => $run_id` when non-empty. Null/empty preserves
existing null-safe legacy behaviour.
- `Publisher::create_post()` forwards the $run_id it already receives
into build_meta — one-line plumbing, no new parameters at call sites.
- `Activator::backpopulate_run_id_meta_v081()` runs once per site,
gated by `prautoblogger_migrated_run_id_meta_v081` option. Walks
`wp_prautoblogger_generation_log`, pairs (post_id, min(run_id)),
writes `_prautoblogger_run_id` on any post that doesn't already have
it. Uses `metadata_exists()` so it's a no-op on re-run.
- Activator also schedules the new daily `prautoblogger_reap_orphan_research_rows`
cron at 03:15 — 15 min after the primary generation cron — so live
pipelines have time to finish amortizing cleanly before the reaper scans.
- Deactivator unschedules the new hook alongside the existing cron hooks.
No user-visible behaviour change yet. The reaper handler ships in the
next commit; this one just ensures it will have the data + schedule it
needs when it lands.
Part of the v0.8.1 bundle (thread research-cost-reaper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cost): daily reaper for orphan llm_research rows
Implements the orphan-research-row reaper per thread research-cost-reaper
seq 1. When a pipeline dies between post creation and
`Post_Assembler::amortize_research_costs()` (Hostinger exec kill, OOM,
LiteSpeed cut, fatal), the shared research row stays in
`wp_prautoblogger_generation_log` with `post_id = NULL` and sibling
articles' cost popovers understate their true spend.
New class `PRAutoBlogger_Research_Reaper` owns:
- `on_cron()` — WP-Cron entrypoint hooked to the daily
`prautoblogger_reap_orphan_research_rows` event (already scheduled
by the Activator in the previous commit at 03:15).
- `reap()` — returns a `{reaped, deleted, skipped}` summary. For each
orphan `llm_research` row older than 1 hour (grace window for live
pipelines):
- `_prautoblogger_run_id` post_meta primary lookup.
- Falls back to distinct post_ids in gen_log with the same run_id.
- If ≥1 post found → delegate to the existing idempotent
`Post_Assembler::amortize_research_costs()`. That method inserts
the per-article rows and deletes the original orphan in one pass,
so the reaper is idempotent on re-run.
- If 0 posts found AND orphan is >7 days old → delete outright as
sunk cost with nothing to attribute to.
- Else → skip; try again the next day.
- Never throws. DB errors caught and logged WARNING.
New WP-CLI command `wp prautoblogger reap-research` registered in the
main loader when `WP_CLI` is defined — manual ops trigger that prints
the same summary.
Departure from the CTO's spec: the reap logic lives on
`Research_Reaper` instead of `Post_Assembler`. Placing it on
Post_Assembler pushed that file to 391 lines, over the 300-line cap.
Moving it to the thin class colocates the attribution logic with the
cron/CLI plumbing that triggers it. `amortize_research_costs()` stays
on Post_Assembler — the reaper delegates to it unchanged. Flagged for
CTO review.
Part of the v0.8.1 bundle (thread research-cost-reaper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cost): ResearchReaperTest — five cases per spec
Covers the five scenarios from convo thread research-cost-reaper seq 1:
- test_reaps_orphan_with_matching_run_id_posts — meta lookup returns
two post IDs; reaper delegates to amortize_research_costs which
inserts two rows and deletes the orphan.
- test_skips_orphan_within_grace_period — the SELECT's
`created_at < now - 1h` clause filters fresh orphans at the DB; the
reaper's stats stay zero and no side effects fire.
- test_deletes_orphan_with_no_articles_after_7_days — orphan is 8 days
old, zero siblings via meta or gen_log; reaper deletes outright.
- test_falls_back_to_gen_log_when_post_meta_missing — meta returns
empty (legacy pre-v0.8.1 post); gen_log has two distinct post_ids;
amortize still runs.
- test_idempotent_on_repeat_run — second pass on a cleaned state
returns 0/0/0 with no inserts or deletes.
All DB interaction mocked via $wpdb stub — no real DB round-trips.
Fixtures are driven by property mutations in the test methods so
PHPUnit's "one stub per method" restriction on mocks doesn't bite.
Part of the v0.8.1 bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(v0.8.1): version bump + CHANGELOG + ARCHITECTURE.md for reaper
Bump plugin header and PRAUTOBLOGGER_VERSION 0.8.0 → 0.8.1. Patch
bump — the reaper is internal infrastructure; no user-visible UX
change.
CHANGELOG:
- Fixed: orphan `llm_research` rows now reaped daily.
- Added: WP-CLI `wp prautoblogger reap-research`; `_prautoblogger_run_id`
post_meta is now written by Publisher on every post.
ARCHITECTURE.md:
- Decision #13 (Run-ID based log linking) cross-refs the new post_meta.
- New Decision #19 documents the reaper's scan/amortize/delete policy,
the 1-hour grace window, the 7-day stale cutoff, and the one-shot
activator back-populate migration.
DB schema unchanged — no PRAUTOBLOGGER_DB_VERSION bump.
Final commit of the v0.8.1 bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cost): widen $wpdb mock so ResearchReaperTest actually runs
BaseTestCase::create_mock_wpdb() only adds methods
[prepare, get_var, get_results, insert, query, get_row, update] — the
reaper and amortize_research_costs also call `delete` and `get_col`,
which the default mock can't stub. CI on PR #100 fired all five new
ResearchReaperTest cases as errors at the first `->method('delete')`
call during setUp.
Fix: build a bespoke $wpdb mock in this file's setUp with the full
superset of methods it needs. Self-contained; no change to
BaseTestCase so existing tests aren't affected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cost): filter gen_log inserts from event_log inserts in reaper tests
CI on ec1efee showed the two reap-and-amortize tests failing because
`assertCount(2, $this->inserted_rows)` was seeing 4 rows, not 2. Root
cause: the Logger singleton writes INFO lines via `$wpdb->insert` into
the event_log table — the reaper logs once ("Reaped...") and amortize
logs once ("Amortized LLM research cost...") before the two per-post
gen_log inserts, so the capture ended up with 4 total rows.
Add a `gen_log_inserts()` helper that filters the capture down to rows
whose `__table` contains "generation_log" — the assertions on
attribution count should only measure the actual cost inserts, not
incidental log writes. Two tests updated; logic unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): correct misleading GA4 Property ID description
The GA4 client at includes/core/class-ga4-client.php:85 already wraps the
property_id option with "properties/" in the outbound URL path. The admin
description told users to enter "properties/XXXXXXXXX" — which would have
produced a double-prefixed URL (properties/properties/XXXXXXXXX) and a
guaranteed 404 from the Data API. Description now asks for digits only,
matching actual code behavior.
No code logic change — UX copy + CHANGELOG only. Flagged by CEO while
inspecting the Analytics tab; folded into the in-flight v0.8.1 PR per
CTO call rather than cutting a separate patch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(image-pipeline): surface per-image failures in the event log
Two gaps made posts 650/657 appear to silently "lose" their featured
images on 2026-04-20:
1. The batch provider tagged each failed key with ['error' => ...] but
the ERROR log line it emitted didn't identify which key failed, so
operators couldn't tell image_a from image_b in the event log when
one of two parallel requests died.
2. Image_Pipeline::process_image_a/_b folded the per-key error into
$result['errors'] and returned silently, with no WARNING row. Ops
only learned about the miss when the outer catch in the caller fired
— and on the 2026-04-20 incident that outer catch didn't fire, so
nothing was logged at all except an unrelated pricing warning.
Fix:
- class-open-router-image-batch.php: collect_result() takes the request
key as its first argument and every Logger::error message reads
'OpenRouter batch image gen <class> for key "<key>": <detail>'.
Matches the shape of OpenRouter_Image_Support::log_retry.
- class-image-pipeline.php: process_image_a and process_image_b now
emit a Logger::warning('image_pipeline') row whenever $image_data is
missing or carries an error key, in addition to pushing to
$result['errors']. The warning fires whether or not the outer catch
runs.
Test: ImagePipelineTest::test_partial_batch_failure_emits_warning_log_row
mocks $wpdb, runs the pipeline with image_a erroring, and asserts at
least one warning/image_pipeline row was inserted.
No behavior change beyond logging; cost impact zero.
Part of the v0.8.0 image bundle (issues from thread image-mime-bug).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(images): retry Cloudflare NSFW-blocked prompts with a sanitized fallback
Cloudflare's Workers AI FLUX.1 schnell content filter sometimes rejects
peptide-comic prompts as NSFW (HTTP 400, errors[].code === 3030). On
2026-04-20 this produced a silent publish-without-featured-image for
post #650 ("Semaglutide vs Tirzepatide") — the LLM-rewritten scene
tripped the filter and we had no recovery path.
New behaviour: on a 3030 response, the Cloudflare provider throws a
typed PRAutoBlogger_Image_NSFW_Blocked exception. The batch wrapper
catches it and tags the per-slot result with
`error_type: nsfw_blocked`. Image_Pipeline hands the results to a new
PRAutoBlogger_Image_NSFW_Retry helper, which reruns the blocked slot
once with a sanitized fallback prompt (title + style suffix, no LLM
rewrite). On success the retry result replaces the error entry; on a
second block, a WARNING is logged and the pipeline publishes without
that image — same terminal behaviour we had before, just explicit.
Files:
- New providers/class-image-nsfw-blocked.php — typed exception.
- providers/class-cloudflare-image-support.php — is_nsfw_error() +
throw_if_nsfw() helpers.
- providers/class-cloudflare-image-provider.php — calls throw_if_nsfw
inside the 4xx branch; batch catches the typed exception and marks
the result with error_type.
- core/class-image-prompt-builder.php — new public build_fallback_prompt
shim so the retry layer can reach the rule-based fallback without
going through the LLM path.
- New core/class-image-nsfw-retry.php — single-shot retry helper.
- core/class-image-pipeline.php — invokes the retry between batch
completion and per-slot processing.
- admin/class-settings-fields-extended.php — new toggle
`prautoblogger_image_nsfw_retry` (default on) in the Images section.
Tests:
- CloudflareImageProviderTest — 3030 response throws NSFW exception;
batch tags the per-slot result with `error_type: nsfw_blocked`.
- ImagePipelineTest — retry succeeds when provider returns a good
result on the second call; retry is skipped when the setting is off.
Cost impact: worst-case an extra Cloudflare call per NSFW-blocked slot
(~$0.000834). Current incidence ~2% of generations; ~+$0.00002 per
generation amortized. Immaterial.
Uninstall: new option covered by the existing prautoblogger_% LIKE
sweep in uninstall.php.
Part of the v0.8.0 image bundle (issues from thread image-mime-bug).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(images): expose rewriter system prompt as admin setting
CEO feedback: every generated image is anchoring on the cartoon
peptide-with-arms-and-legs character because the hardcoded rewriter
system prompt both lists "anthropomorphized molecules" first in its
primer and ships a few-shot example that literally shows one. The ask
is to give CEO the lever without a code change.
Adds `prautoblogger_image_prompt_instructions` (textarea) in the
Images section. Default value is the existing REWRITER_SYSTEM_PROMPT
constant verbatim, so behaviour is unchanged on upgrade until someone
edits it. `Image_Prompt_Builder::rewrite_via_llm()` reads the option
at call time via a new `resolve_system_prompt()` helper and falls back
to the constant if the option is blank or whitespace-only, so a saved
empty value cannot brick image generation.
Const visibility flipped from `private` to `public` so the settings
field can reference it directly as the field default.
Tests:
- `test_rewrite_uses_setting_when_present` — option set, assert the
LLM's system message equals the option content.
- `test_rewrite_falls_back_to_default_when_setting_empty` — option
whitespace, assert the LLM's system message equals the constant.
Uninstall: new option covered by the existing prautoblogger_% LIKE
sweep in uninstall.php.
Cost impact: none by default. Future CEO edits will change prompt
length; current default is ~180 tokens, budget impact linear from
there.
Part of the v0.8.0 image bundle (thread image-mime-bug, message 03).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(images): collapse Image Provider + Image Model into a single dropdown
Until now, Image Provider and Image Model were two independent selects
in the admin UI. That let the CEO set Model=flux-1-schnell while
Provider was still openrouter — OpenRouter has no such model so every
image gen on posts 650/657 on 2026-04-20 returned HTTP 400, and the
article published without a featured image.
v0.8.0 removes the Image Provider field from the admin UI entirely.
The provider is now derived from the model's entry in the registry
every time the model is saved. A new
PRAutoBlogger_Image_Model_Registry::provider_for() helper centralises
the mapping, so admin save, the on-upgrade migration, and future
callers all read from one place.
On save, PRAutoBlogger_Admin_Page::sanitize_image_model() validates
the submitted slug against the registry:
- Known slug: persist it, and update `prautoblogger_image_provider` to
the paired provider as a side-effect.
- Unknown slug: add_settings_error with a clear message, and return
the previously-saved value so the site stays on a working model.
A one-shot v0.8.0 migration in
PRAutoBlogger::on_check_db_version() derives the provider from the
currently-saved model on first admin_init after the plugin is updated.
This auto-heals sites that were running mismatched pairs before the
upgrade, without requiring the admin to re-open settings.
`prautoblogger_image_provider` option key is preserved — all existing
reads in the pipeline, cost tracker, and validators continue to work
unchanged.
Tests (Services/ImageModelRegistryTest):
- Every registry entry has an id and a provider in {openrouter, cloudflare}.
- provider_for() returns the paired provider for a known slug.
- provider_for() returns '' for unknown/empty slugs.
Cost impact: none.
Migration idempotent via the `prautoblogger_migrated_image_provider_v080`
flag. Uninstall sweeps it along with every other plugin option.
Part of the v0.8.0 image bundle (thread image-mime-bug, Issue 1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(v0.8.0): version bump + CHANGELOG + ARCHITECTURE.md for image bundle
Bump plugin header and PRAUTOBLOGGER_VERSION 0.7.3 → 0.8.0. Minor bump
because Commit D removes the Image Provider dropdown from admin (a
UX-visible change), not a patch.
CHANGELOG:
- Changed: collapsed Image Provider + Model dropdowns; provider derived.
- Added: Image Prompt Instructions setting; Retry NSFW-Blocked Images
setting.
- Fixed: silent per-image failures in the event log.
ARCHITECTURE.md:
- Stage 6b (Image Pipeline) callout now describes provider derivation
and the NSFW-retry fallback path.
- wp_options table lists the four new/changed keys
(`prautoblogger_image_provider` as derived, new
`prautoblogger_image_prompt_instructions`,
`prautoblogger_image_nsfw_retry`, and the one-shot migration flag
`prautoblogger_migrated_image_provider_v080`).
DB schema unchanged — no PRAUTOBLOGGER_DB_VERSION bump needed.
Final commit of the v0.8.0 image bundle (thread image-mime-bug).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardcoded light-mode hex colors in table styles broke dark mode —
header backgrounds and text were unreadable. Now uses theme CSS custom
properties (--color-border-default, --color-bg-secondary,
--color-text-primary) with light-mode fallbacks.
Bump 0.7.2 → 0.7.3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Theme's global reset strips list-style from all ul/ol — restored
disc/decimal bullets with padding inside .entry-content.
Tables now wrapped in a scrollable .prab-table-wrap div via the_content
filter (priority 99). Added responsive media query for tighter cell
padding on viewports under 600px.
Bump 0.7.1 → 0.7.2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CSS targeted .post-content but the theme template uses .entry-content.
Also removed the _prautoblogger_generated meta check — display settings
should apply site-wide to all posts, not just PRAutoBlogger-generated
ones.
Bump 0.7.0 → 0.7.1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New "Display" admin section with three settings:
- Article Font (7 choices + theme default, Google Fonts loaded on demand)
- Article Font Size (px, 0 = theme default)
- Table Borders (toggle, on by default — adds borders, padding, striping)
Inline CSS injected via wp_head only on singular posts with
_prautoblogger_generated meta — zero impact on other pages.
Extracted Image_Model_Registry to keep settings-fields-extended under
300 lines.
Bump 0.6.3 → 0.7.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser omits checkbox fields from POST when none are checked, so the
prautoblogger_enabled_sources option retained its previous value (with
Reddit). Added a hidden input to the checkboxes renderer so an empty
value is always submitted. Also replaced the hardcoded "Collecting
sources from Reddit…" pipeline status with a dynamic message reflecting
actually-enabled sources.
Bump 0.6.2 → 0.6.3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New Peptide_Linker class scans generated HTML and wraps every mention
of a known peptide in a hyperlink to its /peptides/ database page.
Runs as post-processing in Publisher — no LLM involvement, 100% reliable.
Handles case-insensitive matching, hyphen/space variants, skips existing
<a> tags. Gracefully no-ops if PR Core is not active.
Removed prompt-based peptide linking from Content_Prompts (unreliable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When any peptide from the PR Core database is mentioned in a generated
article, the first mention is now automatically linked to its /peptides/
page. Gracefully degrades if PR Core is not active.
Extracted all prompt construction into Content_Prompts (252 lines),
slimming Content_Generator from 324 → 162 lines. Both under 300.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each idea row now has a Generate button that triggers single-article
generation via a background WP-Cron job. Skips the pipeline's collect/
analyze/score steps and goes straight to Article_Worker with the
selected idea. Button shows real-time stage updates via per-idea status
transients, then swaps to Edit/View links with cost on completion.
Respects the generation lock to prevent concurrent runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Writing instructions were framed as a weak "Additional instructions:"
block at the end of the system prompt. Models prioritized competing
format directives in each stage's user prompt and ignored the style
guide. Now: system prompt wraps instructions as "MANDATORY STYLE GUIDE"
with override language, every stage user prompt reinforces compliance,
polish stage explicitly preserves lists and links, and chief editor
revision preserves structural formatting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Research LLM call runs once per pipeline but was logged with no run_id
or post_id, making it invisible in per-article cost breakdowns. Now the
pipeline's cost tracker flows through Source_Collector to the research
provider, tagging costs with the run_id. After all articles complete,
amortize_research_costs() divides the cost evenly and inserts one row
per article. link_generation_logs() excludes llm_research stage so the
first article doesn't grab the full cost.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New submenu page showing all article ideas from the content analyzer.
Displays title, type, relevance score, frequency, key points, and
keywords with type filtering and pagination. Gives live visibility
into the idea pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New data source that sends a configurable research prompt to a
reasoning-capable model (Grok 4.1 Fast, DeepSeek-R1, etc.) to identify
trending topics, content gaps, and emerging questions. Findings feed
into the content analyzer alongside Reddit data, combining real-time
community signals with deep knowledge-base research.
New admin settings: Research Model picker, Research Prompt textarea,
and LLM Deep Research toggle in Enabled Sources.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables reasoning-capable models (Grok 4.1 Fast, DeepSeek-R1, etc.)
via new admin settings: Enable Reasoning toggle and Reasoning Effort
selector. Provider auto-injects reasoning params when enabled; callers
can override per-call via options array. Response parser captures
reasoning_tokens and reasoning_content for visibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GLM 4.7 Flash was timing out at 120s on large analysis prompts.
Parse failure errors now log the raw LLM response at error level
(not debug) so the root cause is always visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cost column is now clickable — hovering shows a per-step breakdown
with stage name, model, token count, and cost for every LLM call.
Pure CSS popover, no JS dependency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New toggle in Images settings controls whether the source-data-based
second image is generated. Disabling saves one image gen call + one
LLM prompt rewrite per article. Defaults to enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two new textareas in Content settings let admins provide custom
instructions for the analysis and chief editor LLM stages, matching
the Writing Instructions field added in v0.4.4. All three pipeline
stages now have user-controllable system prompt customization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New textarea in Content settings lets admins provide custom instructions
appended to the writer's system prompt. Steers style, structure, voice,
and formatting without editing code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Models like GPT 5.1 Nano ignore response_format: json_object and wrap
their JSON in ```json fences or add preamble text. New Json_Extractor
utility tries: (1) direct decode, (2) strip markdown fences, (3) find
outermost { ... } substring. Applied to analyzer and chief editor
parsers. Raw response logged on parse failure for debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Title column was compressed to ~30px making post titles unreadable.
Give title 35% min-width with word-wrap, trim model columns to 100px
with text-overflow ellipsis, cost column to 55px.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.4.1 — Three new columns on the WordPress Posts admin page showing
which LLM wrote each article, which model generated its featured image,
and the total API cost. Model names shortened for compact display with
full ID in tooltip. Cost queries the generation_log table, skipping
the DB hit entirely for non-generated posts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes in one:
1. Duplicate image: prepend_caption_to_post() was embedding the full
image + caption as a <figure> block, but the theme already displays
the featured image. Now only the caption text is prepended — no
second copy of the image.
2. Hardcoded model names: removed all FLUX.1/FLUX.2 references from
model-agnostic code (pipeline, prompt builder, sideloader, OpenRouter
provider, interface, admin labels). The Cloudflare provider still
references FLUX since it specifically wraps that model family.
3. Test alignment: OpenRouterImagePricingTest was asserting against
removed models (flux-2-pro, seedream). Updated to match current
COST_PER_IMAGE entries (Gemini Flash, GPT-5 Image).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Captions were being baked into the image by the FLUX model, causing
text to get cut off at the edges. Now the LLM generates scene and
caption separately — the scene drives image gen (with explicit
"no text in the image" instruction), and the caption is inserted
as a styled <figure>/<figcaption> HTML block at the top of the post.
Changes:
- rewrite_via_llm() returns array{scene, caption} instead of string
- build_article_prompt() and build_source_prompt() return {prompt, caption}
- Style suffix v4: removes "caption text" instruction, adds "no text" directive
- Image pipeline destructures prompt/caption, stores caption in attachment meta
- prepend_caption_to_post() inserts figure+figcaption HTML into post content
- Deduplicated generate_image_a/b into shared generate_single_image()
- One-time v4 migration auto-updates existing style suffix on deploy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hostinger's LiteSpeed web server kills any HTTP connection that runs
longer than 120 seconds with a 500 "Request Timeout" error. The full
pipeline (Reddit fetch → topic scoring → article generation → editorial
pass → image gen ×2) takes 120-180s, causing "Request failed: error"
on every manual generation attempt.
Architecture change:
- Generate Now AJAX handler now schedules a one-shot WP-Cron event
and returns immediately (~100ms response time)
- New prautoblogger_manual_generation cron action runs the pipeline
in a separate PHP process free from the connection timeout
- Pipeline progress is written to a transient at each stage
- New prautoblogger_generation_status AJAX endpoint lets the frontend
poll progress every 5 seconds
- Frontend auto-resumes polling on page load if a generation is running
- Direct wp-cron.php loopback ensures cron fires even if DISABLE_WP_CRON
is set (common on Hostinger)
Version bump: 0.2.9 → 0.3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace photography-style prompts with a comedy writer system prompt
that generates Far Side-style single-panel comic concepts with punchy
captions. The LLM rewriter now outputs a comic scene + caption line,
and the style suffix renders as classic black-ink newspaper comic on
yellowed newsprint.
Includes v3 style migration to auto-replace both the old infomercial
and the short-lived photography style in wp_options. Bumped max_tokens
to 180 to accommodate scene + caption output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes:
1. Image attachment: moved set_post_thumbnail() and Image B post meta
into generate_and_attach_images() so they persist immediately after
each image generates. Previously the caller set them after both images
completed — if the process timed out (common with GPT-5 Image at
~30s/image), the featured image was never attached and Image B was
never attempted.
2. Image prompts: replaced FLUX.1-optimized rewriter system prompt and
90s infomercial style suffix with prompts optimized for Gemini Flash
Image and GPT-5 Image. New style: premium scientific lifestyle
photography with golds/teals color grading. Includes one-time migration
to update stored style suffix.
Also logs when Image B is skipped due to missing source data (previously
silent), helping diagnose why some posts only get one image.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues found in activity logs:
1. Image model IDs stale — FLUX and Seedream removed from OpenRouter.
Updated to current models: Gemini 2.5/3.1 Flash Image, Gemini 3 Pro
Image, GPT-5 Image/Mini. Default changed to gemini-2.5-flash-image.
2. openrouter/free router wraps upstream 429 errors in HTTP 200 body
with error object. Both text and image response parsers now detect
this pattern and surface a proper RuntimeException.
3. Image request used modalities: ["image"] but Gemini needs
["image", "text"]. Updated to include both.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PHP 8.3 TypeError and Error extend \Throwable but NOT \Exception,
so catch(\Exception) silently drops them — explaining why the
generation pipeline fails at the publishing step with no activity
log entry. Now every catch in the generation path catches
\Throwable and logs the exception class name for diagnostics.
Bump to v0.2.7.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _wp_http_referer hidden field didn't include the tab parameter, so
WordPress redirected to the default (API Keys) tab after every save.
Now the JS syncs the referer with the active tab on both tab switch and
page load. Bumped to v0.2.6 to bust JS cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The field renderer still checked for the old 'cloudflare_image' capability
but the setting was changed to 'image_generation'. Updated to resolve
model name/price from the shared get_image_models() list. Bumped to
v0.2.5 to bust browser JS/CSS cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces Cloudflare Workers AI (FLUX.1 schnell) as the default image
provider with OpenRouter, which offers FLUX.2 Pro, Seedream 3.0,
Gemini Flash Image, and GPT Image 1 at comparable or better pricing.
- New provider class (3-file split: provider, pricing, support) reuses
the existing OpenRouter API key and Cloudflare AI Gateway
- Admin settings: new Image Provider dropdown (openrouter/cloudflare),
model picker shows all available image models from both providers
- Image pipeline auto-selects the configured provider at construction
- Cost logging now uses actual model name instead of hardcoded flux-1-schnell
- Default model changed to black-forest-labs/flux-2-pro ($0.03/image)
- Unit tests for pricing resolution and provider response parsing
Cost impact: FLUX.2 Pro at $0.03/image vs schnell at $0.0007/image —
~43x more expensive per image, but dramatically better quality. At 2
images/article × 1 article/day = ~$1.80/month. Seedream 3.0 ($0.003)
available as a budget alternative.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The image model popup shows 0 models because LiteSpeed/browser cache
serves the old model-picker.js (pre-cloudflare_image support) since
the version query string didn't change between commits.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements image-provider interface + Cloudflare Workers AI adapter with pricing and validator. Independent QA verdict: APPROVE on 52fd11e (qa-reviews/prautoblogger/2026-04-15-52fd11e2ce97dd9e659df5cc87ec1382a668bec1.md). All 4 CTO blockers resolved (PHPUnit retry mocks, XSS hardening on error strings, PHPCS on new files, 300-line cap via support-class extraction). PHPCS backlog (586 errors across 38 pre-existing files) remains on continue-on-error; Workstream C material.
The previous deploy served stale admin.js from browser cache because
the version string (used as cache-buster query param) was unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address CTO audit findings for PRAutoBlogger:
- CI pipeline now runs PHPCS (WordPress standards) and PHPUnit before deploy
- Removed TikTok/Instagram/YouTube stub providers (dead code, not registered)
- Rewrote PublisherTest with real behavioral assertions (post_status, metadata,
taxonomy, log linking, hooks) instead of method-existence checks
- Added cross-system LLM budget coordination docs to ARCHITECTURE.md
- Bumped version to 0.2.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PullPush.io index was frequently stale/unavailable. Reddit RSS/Atom feeds
work reliably from datacenter IPs (Hostinger) where .json gets 403.
- RSS is now the primary source for post fetching
- .json endpoints kept as fallback for posts and sole source for comments
- Deleted class-pull-push-client.php
- Updated admin UI, test connection handler, docs, and ARCHITECTURE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite encryption with "enc:" prefix to permanently eliminate
the double-encryption bug on settings save
- Add one-time migration to clear legacy non-prefixed encrypted values
- Rewrite content analyzer prompts to request N distinct diverse topics
across 5 categories (question, complaint, comparison, news, guide)
- Pass target_count*2 from pipeline to analyzer for headroom
- Add skip_dedup flag so manual Generate Now always produces content
- Generate Now button now sends force=1 to clear stale locks
- Rename OpenRouter provider files to PSR-4 convention (hyphenated)
- Switch default models to google/gemini-2.5-flash-lite
- Clean up reddit_debug log channel to standard 'reddit'
- Bump version to 0.2.1
Cost: ~$0.003/article with Gemini 2.0 Flash, ~34s pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Reddit OAuth fields (Client ID/Secret) from admin settings
- Add source status indicator showing PullPush.io + .json fallback health
- Add configurable cache TTL, time window, and posts-per-subreddit settings
- Update Test Connections to report which Reddit source is active
- Bump version 0.1.0 → 0.2.0 across plugin header, constant, and readme
- Switch deploy pipeline from zip/REST API to SSH rsync with --delete
- Add LiteSpeed cache purge step to prevent stale OPcache after deploy
- Update CHANGELOG.md and readme.txt for 0.2.0 release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>