- Add kanban board as primary admin landing screen (M1, Phase 2) - Four columns: Generating / In Review / Published / Failed - Live AJAX polling (prautoblogger_board_status), settings-backed interval + window - Fix cost-reporter SQL inline-comment bug in get_daily_spend + get_spend_by_stage - New settings: prautoblogger_board_poll_interval (default 5s), prautoblogger_board_published_window_days (default 7) - PHPUnit: BoardDataProviderTest (8 tests) + CostReporterSqlBugFixTest (3 tests) - Docs: ARCHITECTURE.md §22 + options table + file tree; CHANGELOG v0.19.0; app-map updated Agent-Session: phase2-admin-m1-engineer
690 lines
56 KiB
Markdown
690 lines
56 KiB
Markdown
# PRAutoBlogger — Architecture
|
||
|
||
> **Cross-app context:** decisions that affect multiple plugins (Cloudflare AI Gateway routing, OpenRouter account sharing, the interface pattern, image-generation stack, social distributor choice) are recorded in `Peptide Repo CTO/docs/engineering/decisions/`. The incident runbook for cross-app failure modes is at `Peptide Repo CTO/docs/engineering/INCIDENT-RUNBOOK.md`. Engineer PRAutoBlogger should read both before making decisions that cross plugin boundaries.
|
||
|
||
PRAutoBlogger is a WordPress plugin that monitors social media (starting with Reddit) for recurring questions, complaints, and comparisons in a configured niche, uses LLM agents (via OpenRouter) to generate high-quality blog articles from those insights, runs them through a chief editor agent for quality review, and auto-publishes them on a configurable daily schedule. All collected data and generation metrics are stored for a self-improvement feedback loop.
|
||
|
||
---
|
||
|
||
## Data Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ WP-Cron / Action Scheduler │
|
||
│ (Daily trigger, configurable) │
|
||
└────────────┬────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 1. Source Collector │ Pulls data from enabled sources:
|
||
│ (Reddit + LLM Research)│ - Reddit RSS/Atom (primary) / .json (fallback)
|
||
│ │ - LLM Deep Research (reasoning model)
|
||
│ │ Stores raw data in `ab_source_data` table
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 2. Content Analyzer │ LLM call (cheap model) to detect:
|
||
│ (Analysis Agent) │ - Recurring questions
|
||
│ │ - Complaints / pain points
|
||
│ │ - Product comparisons
|
||
│ │ Stores analysis in `ab_analysis_results` table
|
||
│ │ Outputs ranked article ideas
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 3. Idea Scorer & │ Deduplicates against existing posts
|
||
│ Deduplicator │ Scores ideas by relevance, frequency, freshness
|
||
│ │ Picks top N ideas (N = daily article target)
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 4. Writer Agent │ Configurable pipeline:
|
||
│ (Content Generator) │ Single-pass: one LLM call → complete article
|
||
│ │ Multi-step: outline → draft → polish
|
||
│ │ Uses quality model via OpenRouter
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 5. Chief Editor Agent │ LLM-powered editorial review:
|
||
│ │ - Checks quality, accuracy, tone
|
||
│ │ - Checks SEO (title, headings, keyword density)
|
||
│ │ - Can request rewrites or approve
|
||
│ │ - Approved → publish; Rejected → flag for human
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 6. Publisher │ Creates/updates WordPress post
|
||
│ │ Sets categories, tags, featured image
|
||
│ │ Stores generation metadata in post_meta
|
||
│ │ Logs cost/tokens in `prab_generation_log` table
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 6b. Image Pipeline │ Generates two A/B images in parallel:
|
||
│ (optional, if enabled) │ - Image A: article-driven (featured)
|
||
│ │ - Image B: source-driven (post meta)
|
||
│ │ Provider derived from the saved Image Model
|
||
│ │ via Image_Model_Registry::provider_for().
|
||
│ │ NSFW-blocked slots (CF error code 3030) are
|
||
│ │ retried once with a rule-based fallback
|
||
│ │ prompt via Image_NSFW_Retry; a second block
|
||
│ │ logs WARNING and publishes without that image.
|
||
│ │ v0.17.0: results then pass through the local
|
||
│ │ deterministic Image_Composer (corner mark +
|
||
│ │ branded OG/square variants for Image A; mark
|
||
│ │ only for Image B) with a capability ladder
|
||
│ │ (imagick → GD resize-only → pass-through).
|
||
│ │ Sideloads to media library, logs costs.
|
||
└────────────┬────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ 7. Metrics Collector │ Tracks post-publish performance:
|
||
│ (Separate cron job) │ - WordPress native (views, comments)
|
||
│ │ - GA4 (pageviews, bounce rate, time on page)
|
||
│ │ - Composite "content score"
|
||
│ │ Feeds back into Analysis step for self-improvement
|
||
└─────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## File Tree
|
||
|
||
```
|
||
prautoblogger/
|
||
├── prautoblogger.php # Plugin bootstrap — minimal, only hook registration
|
||
├── uninstall.php # Clean removal of ALL plugin data
|
||
├── readme.txt # WordPress.org standard readme
|
||
├── composer.json # Autoloading (PSR-4) and dependencies
|
||
├── ARCHITECTURE.md # This file
|
||
├── CONVENTIONS.md # Naming patterns, extension guides
|
||
├── CHANGELOG.md # Semantic versioning changelog
|
||
│
|
||
├── assets/
|
||
│ ├── brand/ # Vendored brand PNGs rasterized from peptide-repo-brand SVGs (composer overlays)
|
||
│ │ ├── logo-mark-small-56.png # Corner mark, 1x
|
||
│ │ ├── logo-mark-small-112.png # Corner mark / OG band logo, 2x
|
||
│ │ ├── logo-horizontal-reverse-128.png # Square footer lockup, 1x
|
||
│ │ └── logo-horizontal-reverse-256.png # Square footer lockup, 2x
|
||
│ ├── fonts/ # Bundled OFL faces for deterministic text rendering
|
||
│ │ ├── Poppins-Bold.ttf # Square-card caption face
|
||
│ │ ├── Poppins-SemiBold.ttf # OG band caption face
|
||
│ │ └── OFL.txt # SIL Open Font License
|
||
│ ├── css/
|
||
│ │ ├── admin.css # Admin page styles (wp-admin conventions)
|
||
│ │ ├── board.css # Kanban board styles — warm editorial palette (v0.19.0)
|
||
│ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars)
|
||
│ └── js/
|
||
│ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
||
│ ├── board.js # Board poller: AJAX poll + DOM updates + backoff (v0.19.0)
|
||
│ └── posts-widget.js # React component for frontend post cards (wp.element)
|
||
│
|
||
├── includes/
|
||
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks, delegates execution
|
||
│ ├── class-executor.php # Cron handlers, generation AJAX (start + status), model registry
|
||
│ ├── class-ajax-handlers.php # Non-generation AJAX: images, models, test connections
|
||
│ ├── class-generation-lock.php # DB-level atomic mutex for single-writer generation
|
||
│ ├── class-activator.php # Activation: create DB tables, set defaults, schedule cron
|
||
│ ├── class-deactivator.php # Deactivation: clear cron, cleanup transients
|
||
│ ├── class-autoloader.php # PSR-4-style autoloader for plugin classes
|
||
│ │
|
||
│ ├── admin/
|
||
│ │ ├── class-board-page.php # Kanban board — primary landing screen (v0.19.0)
|
||
│ │ ├── class-board-data-provider.php # Board data: 4-column card snapshots from gen_log + post meta (v0.19.0)
|
||
│ │ ├── class-admin-page.php # Main settings page (tabbed SaaS-style UI)
|
||
│ │ ├── class-settings-fields.php # Declarative settings: sections + core fields (API/Models/Content/Sources)
|
||
│ │ ├── class-settings-fields-extended.php # Operational fields: schedule, publishing, display, analytics, images
|
||
│ │ ├── class-image-model-registry.php # Static list of image generation models for the model picker
|
||
│ │ ├── class-admin-notices.php # Onboarding notices, error alerts, budget warnings
|
||
│ │ ├── class-dashboard-widget.php # WP Dashboard widget showing generation status
|
||
│ │ ├── class-post-metabox.php # Metabox on posts showing generation metadata
|
||
│ │ ├── class-metrics-page.php # Admin page for cost dashboard and content scores
|
||
│ │ ├── class-ideas-browser.php # Browse all analysis results (article ideas)
|
||
│ │ ├── class-review-queue.php # Approve/edit/reject queue for generated drafts
|
||
│ │ └── class-log-viewer.php # Activity log viewer with level filtering
|
||
│ │
|
||
│ ├── core/
|
||
│ │ ├── class-scheduler.php # WP-Cron / Action Scheduler job management
|
||
│ │ ├── class-source-collector.php # Orchestrates data collection from all sources
|
||
│ │ ├── class-content-analyzer.php # LLM-powered analysis of collected social data
|
||
│ │ ├── class-analysis-prompts.php # System/user prompt builders + performance context for analyzer
|
||
│ │ ├── class-idea-scorer.php # Ranks and deduplicates article ideas (delegates to Semantic_Dedup)
|
||
│ │ ├── class-semantic-dedup.php # Embedding-based cosine similarity dedup with keyword fallback
|
||
│ │ ├── class-content-generator.php# Writer agent — manages the generation pipeline
|
||
│ │ ├── class-content-prompts.php # Builds all LLM prompts for content generation (system, stage, linking rules)
|
||
│ │ ├── class-peptide-linker.php # Deterministic post-processor: hyperlinks peptide mentions to /peptides/ pages
|
||
│ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review
|
||
│ │ ├── class-publisher.php # Creates WordPress posts from approved content
|
||
│ │ ├── class-post-assembler.php # Post-creation helpers: taxonomy, log linking, images, sanitization
|
||
│ │ ├── class-image-pipeline.php # Orchestrates A/B image generation (parallel via batch)
|
||
│ │ ├── class-image-prompt-builder.php # Generates visual prompts from article/source data
|
||
│ │ ├── class-image-template-filler.php # Fills the editorial style template's {{ topic_summary }} slot (v0.16.0)
|
||
│ │ ├── interface-image-composer.php # Contract: provider bytes → featured-first variant list (v0.17.0)
|
||
│ │ ├── class-image-composer.php # Composer orchestrator: cached capability probe + degradation ladder
|
||
│ │ ├── class-image-composer-imagick.php # Imagick renderer: corner mark, OG band, square card (WP-free)
|
||
│ │ ├── class-image-composer-canvas.php # Imagick primitives: smoke test, cover-crop, logo, deterministic PNG (WP-free)
|
||
│ │ ├── class-image-composer-editor.php # Resize-only rung via wp_get_image_editor (GD path)
|
||
│ │ ├── class-image-composer-layout.php # Geometry defaults + caption clamp (pure, WP-free)
|
||
│ │ ├── class-image-attacher.php # Sideload + cost log + variant meta-linking + caption prepend
|
||
│ │ ├── class-image-media-sideloader.php # Imports images into WordPress media library
|
||
│ │ ├── class-cost-tracker.php # Logs all API costs, enforces budget limits
|
||
│ │ ├── class-logger.php # Structured logging singleton (error/warning/info/debug)
|
||
│ │ ├── class-article-worker.php # Single-article generation (content + edit + publish)
|
||
│ │ ├── class-pipeline-runner.php # Orchestrates pipeline; chains per-article cron jobs
|
||
│ │ ├── class-ga4-client.php # Google Analytics 4 API client (OAuth + Data API)
|
||
│ │ └── class-metrics-collector.php# Collects post performance data (WP + GA4)
|
||
│ │
|
||
│ ├── providers/
|
||
│ │ ├── interface-llm-provider.php # Contract for any LLM provider
|
||
│ │ ├── class-open-router-provider.php # OpenRouter API implementation (chat completions)
|
||
│ │ ├── class-open-router-embedding-provider.php # Text embeddings for semantic dedup
|
||
│ │ ├── class-open-router-pricing.php # Model pricing lookup and cost estimation
|
||
│ │ ├── interface-source-provider.php # Contract for any social media source
|
||
│ │ ├── class-reddit-json-client.php # Reddit HTTP client — RSS (primary) + .json (fallback)
|
||
│ │ ├── class-llm-research-provider.php # LLM deep research source (reasoning models)
|
||
│ │ ├── class-reddit-provider.php # Reddit data collection orchestrator (RSS primary)
|
||
│ │ ├── interface-image-provider.php # Contract for any image generation provider (incl. batch)
|
||
│ │ ├── class-open-router-image-provider.php # OpenRouter image gen (single + batch dispatch)
|
||
│ │ ├── class-open-router-image-batch.php # Parallel curl_multi execution for batch image gen
|
||
│ │ ├── class-open-router-image-support.php # API key, response parsing, retry/backoff helpers
|
||
│ │ ├── class-open-router-image-pricing.php # Model resolution + per-image cost estimation
|
||
│ │ ├── class-open-router-config.php # API base URL (direct vs AI Gateway)
|
||
│ │ ├── class-open-router-request-builder.php # Header building + Hostinger cURL auth workaround
|
||
│ │ ├── class-runware-image-provider.php # Runware FLUX.1 (default v0.9.0+)
|
||
│ │ ├── class-runware-image-pricing.php # FLUX schnell/dev cost table + resolver
|
||
│ │ ├── class-runware-image-support.php # Key, response parsing, retry, dimension snap
|
||
│ │ ├── class-runware-image-batch.php # True parallel curl_multi batch dispatcher
|
||
│ │ └── (new providers go here — see CONVENTIONS.md)
|
||
│ │
|
||
│ ├── services/
|
||
│ │ ├── interface-model-registry.php # Contract for provider-specific model registries
|
||
│ │ ├── class-open-router-model-registry.php # Fetches, caches, queries the OpenRouter model list
|
||
│ │ ├── class-open-router-model-normalizer.php # Maps raw OpenRouter data → standardized record shape
|
||
│ │ └── (Phase 3: class-runware-model-registry.php)
|
||
│ │
|
||
│ ├── frontend/
|
||
│ │ ├── class-article-typography.php # Inline CSS for font, size, table borders on generated posts
|
||
│ │ └── class-posts-widget.php # [prautoblogger_posts] shortcode + REST endpoint
|
||
│ │
|
||
│ └── models/
|
||
│ ├── class-source-data.php # Value object: raw social media post/comment
|
||
│ ├── class-analysis-result.php # Value object: analyzed topic with scores
|
||
│ ├── class-article-idea.php # Value object: scored article idea
|
||
│ ├── class-content-request.php # Value object: generation request with params
|
||
│ ├── class-editorial-review.php # Value object: editor verdict, scores, revised content
|
||
│ ├── class-generation-log.php # Value object: log entry for a generation run
|
||
│ └── class-content-score.php # Value object: composite performance score
|
||
│
|
||
├── templates/
|
||
│ └── admin/
|
||
│ ├── board-page.php # Kanban board template (4-column editorial layout) (v0.19.0)
|
||
│ ├── settings-page.php # Settings page template (tabbed sidebar layout)
|
||
│ ├── metrics-page.php # Metrics/cost dashboard template
|
||
│ ├── review-queue.php # Review queue table template
|
||
│ ├── log-viewer.php # Activity log viewer template
|
||
│ └── metabox-generation-info.php# Post metabox template
|
||
│
|
||
├── languages/
|
||
│ └── prautoblogger.pot # i18n translation template
|
||
│
|
||
└── tests/
|
||
├── unit/ # PHPUnit tests (mocked dependencies)
|
||
└── integration/ # WordPress integration tests
|
||
```
|
||
|
||
---
|
||
|
||
## Frontend Widget Data Flow
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ WordPress Page/Post │ Contains [prautoblogger_posts] shortcode
|
||
│ (e.g. Home page) │
|
||
└────────────┬─────────────────┘
|
||
│ Shortcode renders:
|
||
│ 1. <div id="prab-posts-root"></div>
|
||
│ 2. Enqueues posts-widget.js + posts-widget.css
|
||
│ 3. Passes config via wp_localize_script
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ React Component (wp.element)│ Mounts into #prab-posts-root
|
||
│ posts-widget.js │ Shows loading skeleton on mount
|
||
└────────────┬─────────────────┘
|
||
│ Fetches asynchronously
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ REST API Endpoint │ GET /wp-json/prautoblogger/v1/posts
|
||
│ class-posts-widget.php │ Queries published posts with
|
||
│ │ _prautoblogger_generated = '1' meta
|
||
│ │ Returns slim JSON (id, title, excerpt,
|
||
│ │ url, date, category, image, word_count)
|
||
│ │ 5-minute Cache-Control header
|
||
└──────────────────────────────┘
|
||
```
|
||
|
||
**Shortcode Attributes**
|
||
|
||
| Attribute | Default | Description |
|
||
|------------|----------------------------------|-------------------------|
|
||
| `count` | `6` | Posts to display (max 12) |
|
||
| `category` | (all) | Filter by category slug |
|
||
| `title` | `"Latest Research & Insights"` | Widget heading |
|
||
| `subtitle` | `"Evidence-based articles..."` | Widget subheading |
|
||
|
||
**CSS Integration**
|
||
|
||
The widget CSS (`posts-widget.css`) uses the Peptide Starter theme's CSS custom properties (`--color-*`, `--spacing-*`, `--text-*`, `--radius-*`, `--transition-*`) with hardcoded fallbacks. This means:
|
||
|
||
- **On sites using Peptide Starter:** widget automatically adapts to light/dark mode via `data-theme`.
|
||
- **On other themes:** fallback values render a sensible dark-themed design.
|
||
|
||
---
|
||
|
||
## Background Generation Flow (v0.3.0)
|
||
|
||
The "Generate Now" button runs the pipeline in a background WP-Cron process to avoid
|
||
Hostinger's 120-second LiteSpeed connection timeout. The frontend polls for status.
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ Admin clicks "Generate Now" │ admin.js sends AJAX to on_ajax_generate_now()
|
||
└────────────┬─────────────────┘
|
||
│ Handler returns immediately (< 1 second):
|
||
│ 1. Writes "running" transient with started timestamp
|
||
│ 2. Schedules one-shot cron: prautoblogger_manual_generation
|
||
│ 3. Fires non-blocking loopback to wp-cron.php
|
||
│ 4. Returns JSON success to browser
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ admin.js starts polling │ setInterval every 5 seconds
|
||
│ on_ajax_generation_status() │ Shows stage text + spinner
|
||
└────────────┬─────────────────┘
|
||
│
|
||
▼ (separate PHP process)
|
||
┌──────────────────────────────┐
|
||
│ on_manual_generation() │ WP-Cron fires this in a new request
|
||
│ (class-executor.php) │ 1. ignore_user_abort(true) — survives HTTP kill
|
||
│ │ 2. set_time_limit(300)
|
||
│ │ 3. Acquires PRAutoBlogger_Generation_Lock
|
||
│ │ 4. Runs PRAutoBlogger_Pipeline_Runner::run()
|
||
│ │ (pipeline broadcasts stage updates to transient)
|
||
│ │ 5. Writes final result to transient
|
||
│ │ 6. Releases lock
|
||
└────────────┬─────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────┐
|
||
│ Poller detects completion │ Reads transient: status = 'complete' or 'error'
|
||
│ Shows result message │ Resets button to idle state
|
||
└──────────────────────────────┘
|
||
```
|
||
|
||
**Fallback detection:** If Hostinger kills the PHP process before it updates the
|
||
transient (despite `ignore_user_abort`), the status endpoint detects the orphan after
|
||
180 seconds by checking for recently-created posts with `_prautoblogger_run_id` meta.
|
||
It also releases any stale generation lock. After 600 seconds (STATUS_TTL), it gives
|
||
up unconditionally and reports a timeout.
|
||
|
||
---
|
||
|
||
## Database Schema
|
||
|
||
### Custom Tables
|
||
|
||
All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||
|
||
#### `ab_source_data` — Raw social media data
|
||
|
||
| Column | Type | Description |
|
||
|---------------|-------------------|--------------------------------------------------|
|
||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||
| source_type | VARCHAR(50) | 'reddit' (extensible for future providers) |
|
||
| source_id | VARCHAR(255) | Platform-specific unique ID (post/comment ID) |
|
||
| subreddit | VARCHAR(255) | Subreddit or channel name (nullable) |
|
||
| title | TEXT | Post/video title |
|
||
| content | LONGTEXT | Post body / comment text / transcript |
|
||
| author | VARCHAR(255) | Original author username |
|
||
| score | INT | Upvotes/likes/engagement metric |
|
||
| comment_count | INT | Number of comments/replies |
|
||
| permalink | VARCHAR(500) | URL to original content |
|
||
| collected_at | DATETIME | When we collected this data |
|
||
| metadata_json | LONGTEXT | Platform-specific extra data (JSON) |
|
||
|
||
- INDEX: `source_type`, `collected_at`
|
||
- UNIQUE INDEX: `source_type`, `source_id`
|
||
|
||
#### `ab_analysis_results` — Analyzed topics and patterns
|
||
|
||
| Column | Type | Description |
|
||
|------------------|-------------------|------------------------------------------------|
|
||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||
| analysis_type | VARCHAR(50) | 'question', 'complaint', 'comparison' |
|
||
| topic | VARCHAR(500) | Detected topic/theme |
|
||
| summary | TEXT | LLM-generated summary of the pattern |
|
||
| frequency | INT | How many source posts mention this |
|
||
| relevance_score | FLOAT | LLM-assigned relevance score (0-1) |
|
||
| source_ids_json | LONGTEXT | JSON array of ab_source_data.id references |
|
||
| analyzed_at | DATETIME | When analysis was performed |
|
||
| metadata_json | LONGTEXT | Extra analysis data (JSON) |
|
||
|
||
- INDEX: `analysis_type`, `relevance_score`
|
||
- INDEX: `analyzed_at`
|
||
|
||
#### `prab_generation_log` — API cost and generation tracking
|
||
|
||
| Column | Type | Description |
|
||
|-------------------|-------------------|-----------------------------------------------|
|
||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||
| post_id | BIGINT UNSIGNED | WordPress post ID (nullable, set after publish)|
|
||
| run_id | VARCHAR(36) | UUID linking all entries from one pipeline run |
|
||
| stage | VARCHAR(50) | 'analysis', 'outline', 'draft', 'edit', 'review' |
|
||
| provider | VARCHAR(50) | 'openrouter' |
|
||
| model | VARCHAR(100) | Model identifier used |
|
||
| prompt_tokens | INT | Input tokens consumed |
|
||
| completion_tokens | INT | Output tokens generated |
|
||
| estimated_cost | DECIMAL(10,6) | Estimated USD cost |
|
||
| request_json | LONGTEXT | Full request payload (for debugging) |
|
||
| response_status | VARCHAR(20) | 'success', 'error', 'timeout' |
|
||
| error_message | TEXT | Error details if failed (nullable) |
|
||
| created_at | DATETIME | When the API call was made |
|
||
|
||
- INDEX: `post_id`
|
||
- INDEX: `run_id`
|
||
- INDEX: `created_at`
|
||
- INDEX: `stage`
|
||
|
||
#### `prab_event_log` — Structured application logging
|
||
|
||
| Column | Type | Description |
|
||
|------------|-------------------|-------------------------------------------------|
|
||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||
| level | VARCHAR(10) | 'error', 'warning', 'info', 'debug' |
|
||
| context | VARCHAR(100) | Origin: 'pipeline', 'reddit', 'ga4', etc. |
|
||
| message | TEXT | Human-readable log message |
|
||
| meta_json | LONGTEXT | Optional structured data (nullable) |
|
||
| created_at | DATETIME | When the event occurred |
|
||
|
||
- INDEX: `level`, `created_at`
|
||
- INDEX: `created_at`
|
||
|
||
#### `ab_content_scores` — Post performance metrics
|
||
|
||
| Column | Type | Description |
|
||
|--------------------|-------------------|----------------------------------------------|
|
||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||
| post_id | BIGINT UNSIGNED | WordPress post ID |
|
||
| pageviews | INT | Total pageviews (WP native or GA4) |
|
||
| avg_time_on_page | FLOAT | Average seconds on page (GA4) |
|
||
| bounce_rate | FLOAT | Bounce rate percentage (GA4) |
|
||
| comment_count | INT | WordPress comment count |
|
||
| composite_score | FLOAT | LLM-computed content quality score (0-100) |
|
||
| score_factors_json | LONGTEXT | JSON breakdown of what contributed to score |
|
||
| measured_at | DATETIME | When metrics were collected |
|
||
|
||
- INDEX: `post_id`
|
||
- INDEX: `measured_at`
|
||
- INDEX: `composite_score`
|
||
|
||
### WordPress Options (`wp_options`)
|
||
|
||
All prefixed with `prautoblogger_`:
|
||
|
||
| Option Key | Description |
|
||
|----------------------------------------|-------------------------------------------------------|
|
||
| `prautoblogger_openrouter_api_key` | Encrypted OpenRouter API key |
|
||
| `prautoblogger_ai_gateway_base_url` | Optional Cloudflare AI Gateway URL (proxies OpenRouter); empty = direct |
|
||
| `prautoblogger_ai_gateway_cache_ttl` | Seconds Cloudflare may cache identical LLM responses (0 = off) |
|
||
| `prautoblogger_ga4_property_id` | Google Analytics 4 property ID |
|
||
| `prautoblogger_ga4_credentials_json` | Encrypted GA4 service account credentials |
|
||
| `prautoblogger_analysis_model` | OpenRouter model for analysis (default: cheap) |
|
||
| `prautoblogger_writing_model` | OpenRouter model for writing (default: quality) |
|
||
| `prautoblogger_editor_model` | OpenRouter model for chief editor (default: quality) |
|
||
| `prautoblogger_daily_article_target` | Number of articles per day (1-10, default: 1) |
|
||
| `prautoblogger_writing_pipeline` | 'single_pass' or 'multi_step' (default: multi_step) |
|
||
| `prautoblogger_niche_description` | Text description of the site's niche |
|
||
| `prautoblogger_target_subreddits` | JSON array of subreddits to monitor |
|
||
| `prautoblogger_monthly_budget_usd` | Monthly API spend limit in USD |
|
||
| `prautoblogger_tone` | Content tone (informational, conversational, etc.) |
|
||
| `prautoblogger_min_word_count` | Minimum article word count (default: 800) |
|
||
| `prautoblogger_max_word_count` | Maximum article word count (default: 2000) |
|
||
| `prautoblogger_topic_exclusions` | JSON array of topics to never write about |
|
||
| `prautoblogger_enabled_sources` | JSON array of active source types |
|
||
| `prautoblogger_auto_publish` | Toggle: auto-publish approved posts (default: '0') |
|
||
| `prautoblogger_default_author` | WP user ID for generated post authorship |
|
||
| `prautoblogger_default_category` | WP category ID fallback for generated posts |
|
||
| `prautoblogger_log_level` | Logging threshold: error/warning/info/debug |
|
||
| `prautoblogger_db_version` | Schema version for migrations |
|
||
| `prautoblogger_schedule_time` | Daily generation time (HH:MM, default: '03:00') |
|
||
| `prautoblogger_image_model` | Image model slug from `Image_Model_Registry::get_models()`; provider is derived from this on save |
|
||
| `prautoblogger_image_provider` | Derived from the chosen model on save (v0.8.0+); not editable in admin UI |
|
||
| `prautoblogger_image_prompt_instructions` | System prompt given to the image rewriter LLM (v0.8.0+); falls back to `Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT` when empty. v0.16.0: the default now instructs the LLM to emit a 1-2 sentence editorial topic/mechanism summary as the SCENE (was a comic gag) |
|
||
| `prautoblogger_image_nsfw_retry` | Toggle: retry NSFW-blocked image slots with a rule-based fallback prompt (default: '1') |
|
||
| `prautoblogger_migrated_image_provider_v080` | One-shot migration flag — auto-heals mismatched provider/model pairs on first admin_init after v0.8.0 upgrade |
|
||
| `prautoblogger_article_font_family` | Font family key: 'default', 'inter', 'georgia', 'merriweather', 'lora', 'open_sans', 'roboto', 'system' |
|
||
| `prautoblogger_article_font_size` | Body font size in px (0 = theme default). Recommended: 16–18. |
|
||
| `prautoblogger_table_borders` | Toggle: add borders/padding/striping to tables (default: '1') |
|
||
| `prautoblogger_image_style_template` | Full image prompt template (v0.16.0+); contains exactly one `{{ topic_summary }}` token filled per-article with the rewriter scene. Default = `PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE` (editorial scientific illustration). Replaces the Style Suffix. Validated on save (single-token, brief A5) |
|
||
| `prautoblogger_image_style_suffix` | DEPRECATED (v0.16.0). Former comic style suffix appended to every image prompt; no longer read by the builder. Mirrored to `prautoblogger_image_style_suffix_deprecated` for one cycle and not deleted yet |
|
||
| `prautoblogger_openrouter_model_registry` | Normalized OpenRouter model list (JSON array, daily refresh, serves as durable cache) |
|
||
| `prautoblogger_openrouter_model_registry_fetched_at` | Unix timestamp of last successful model registry refresh |
|
||
| `prautoblogger_image_compose_enabled` | Toggle: run the deterministic image composer (default '1'; auto-degrades, never blocks publishing) |
|
||
| `prautoblogger_image_compose_variants` | Comma list of composed variants for Image A (default 'og,square'; whitelisted at point of use) |
|
||
| `prautoblogger_image_featured_mark_enabled` | Toggle: subtle corner mark on featured images (default '1') |
|
||
| `prautoblogger_image_compose_capability` | Cached capability probe `{fingerprint, capability}`; fingerprint = PHP version + imagick/gd presence, so it auto-invalidates on host changes |
|
||
| `prautoblogger_board_poll_interval` | Kanban board poll interval in seconds (default 5, min 3). Localized into board.js. |
|
||
| `prautoblogger_board_published_window_days` | Days back to show in the Published column (default 7). |
|
||
|
||
### Post Meta
|
||
|
||
Stored on every PRAutoBlogger-generated post:
|
||
|
||
| Meta Key | Description |
|
||
|---------------------------------------|-----------------------------------------------|
|
||
| `_prautoblogger_generated` | Boolean flag — '1' if generated by plugin |
|
||
| `_prautoblogger_analysis_id` | FK to ab_analysis_results.id |
|
||
| `_prautoblogger_source_ids` | JSON array of source data IDs used |
|
||
| `_prautoblogger_model_used` | Model that generated the content |
|
||
| `_prautoblogger_pipeline_mode` | 'single_pass' or 'multi_step' |
|
||
| `_prautoblogger_total_cost` | Total USD cost for this post |
|
||
| `_prautoblogger_total_tokens` | Total tokens consumed for this post |
|
||
| `_prautoblogger_editor_verdict` | 'approved', 'revised', 'rejected' |
|
||
| `_prautoblogger_editor_notes` | Chief editor's review notes |
|
||
| `_prautoblogger_generated_at` | ISO 8601 timestamp of generation |
|
||
| `_prautoblogger_research_sources` | JSON array of source URLs used |
|
||
| `_prautoblogger_og_image_id` | Attachment ID of the composed 1200×630 OG variant (v0.17.0; the rebuilt SEO stage emits `og:image` from this — key name frozen) |
|
||
| `_prautoblogger_square_image_id` | Attachment ID of the composed 1080×1080 square variant (v0.17.0; stored now, no consumer yet) |
|
||
|
||
Composed-variant **attachment** meta (v0.17.0): every pipeline attachment gets
|
||
`_prautoblogger_image_role` (`featured`/`og`/`square`); OG/square variants also
|
||
get `_prautoblogger_base_attachment_id` pointing at the featured attachment
|
||
they derive from. All keys keep the `_prautoblogger_` prefix, so the uninstall
|
||
prefix-sweep purges them.
|
||
|
||
---
|
||
|
||
## External API Integrations
|
||
|
||
| Service | Purpose | Auth | Rate Limit | Code |
|
||
|---------|---------|------|------------|------|
|
||
| OpenRouter | All LLM calls (analysis, writing, editing) | API key (encrypted in wp_options) | Per-model | `providers/class-open-router-provider.php`, `providers/class-open-router-pricing.php` |
|
||
| Cloudflare AI Gateway (optional) | Transparent proxy in front of OpenRouter — adds response caching, cost logging, rate limiting, provider fallback | Same OpenRouter key; gateway URL in `prautoblogger_ai_gateway_base_url` | Gateway-side quotas | Same provider file; activated when gateway URL option is non-empty |
|
||
| Reddit RSS | Primary Reddit data source — Atom feeds for subreddit hot posts | None (unauthenticated) | No known rate limit; reliable from datacenter IPs | `providers/class-reddit-json-client.php` |
|
||
| Reddit .json | Fallback for posts + only source for comments | None (unauthenticated) | ~10 req/min (datacenter IPs often blocked) | `providers/class-reddit-json-client.php` |
|
||
| Google Analytics 4 | Post performance metrics | OAuth2 service account | Standard GA4 limits | `core/class-ga4-client.php`, `core/class-metrics-collector.php` |
|
||
| Runware (FLUX.1 via runware.ai) | **Default** image backend (v0.9.0+): schnell ~$0.0006/image, dev ~$0.02/image. True parallel generation via curl_multi. **v0.15.0: live model catalog sync** from `/v1/models` endpoint (task-based API); normalized to Image_Model_Registry shape; cached with 24h TTL; on-demand refresh via AJAX. | API key (encrypted in wp_options) | Account-level quotas | `providers/class-runware-image-provider.php`, `providers/class-runware-image-pricing.php`, `providers/class-runware-image-support.php`, `providers/class-runware-image-batch.php`, `providers/class-runware-model-catalog.php` |
|
||
|
||
---
|
||
|
||
## Opik LLM Observability (Optional)
|
||
|
||
**Status:** Wave 1 (v0.12.0) — tracing skeleton. Wave 2 (future) — eval harness with frozen-dataset regression testing.
|
||
|
||
Opik is a cloud-hosted LLM observability platform (Comet) providing:
|
||
- **Per-call tracing:** Every OpenRouter call generates a span with model, tokens, latency metadata.
|
||
- **Trace grouping:** One trace per article generation bundles all spans (draft, QA, image prompt).
|
||
- **Prompt versioning:** Foundation for Wave 2 — compare new prompt revisions against a frozen reference dataset using LLM-as-judge scoring on rubric axes (factual accuracy, style match, hallucination risk).
|
||
|
||
**Architecture:**
|
||
- **Feature flag:** `prautoblogger_opik_enabled` (WP option, default off). When false, zero network traffic to Opik.
|
||
- **Credentials:** API key and workspace from PHP constants `PRAUTOBLOGGER_OPIK_API_KEY`, `PRAUTOBLOGGER_OPIK_WORKSPACE` (defined in wp-config.php). Never stored in DB.
|
||
- **Span lifecycle:**
|
||
1. `Opik_Trace_Context::init_trace()` generates UUID at article-worker start.
|
||
2. Each LLM call wraps with `start_span()` → LLM request → `end_span()` with tokens + response.
|
||
3. `finalize_trace()` + `Opik_Span_Queue::enqueue()` accumulates trace + spans for async dispatch.
|
||
4. `prautoblogger_opik_dispatch` cron action batches spans/traces (max 100 per POST) to `/v1/private/spans/batch`, `/v1/private/traces/batch`.
|
||
- **Retry logic:** Exponential backoff (2s, 4s, 8s) on 5xx errors; permanent abort on 4xx; dropped after 3 attempts per item.
|
||
- **Queue:** WP options-backed; max 1000 items; 12-hour TTL; persists across page reloads for truly async dispatch.
|
||
|
||
**Call sites instrumented (Wave 1):**
|
||
- Draft generation (single-pass or multi-step outline/draft/polish).
|
||
- Editorial review (chief editor LLM call).
|
||
- Image prompt generation (deferred; file at 314 LOC, over limit).
|
||
|
||
**Cost & Performance:**
|
||
- Opik free tier: 25k spans/month. Estimated current usage ~4.2k/month (20 posts/day × 7 calls × 30 days). Headroom to 5× growth.
|
||
- Impact on article generation: async dispatch, zero blocking latency overhead.
|
||
- Wave 2 (eval harness): adds LLM-as-judge calls (~$14/month at Haiku pricing, assuming 2 eval runs/week).
|
||
|
||
**Code structure:**
|
||
```
|
||
includes/services/opik/
|
||
class-opik-client.php — REST client (auth, retry, batch POSTs)
|
||
class-opik-trace-context.php — per-request trace state singleton
|
||
class-opik-span-queue.php — WP options-backed async queue
|
||
class-opik-dispatcher.php — cron handler, batch dispatch
|
||
|
||
includes/admin/
|
||
class-opik-settings.php — admin UI (toggle, project name, status)
|
||
```
|
||
|
||
---
|
||
|
||
## Key Decisions
|
||
|
||
### #1: OpenRouter over direct provider APIs
|
||
Gives access to many models through one API, simplifies provider management, and lets the user switch models without code changes. Trade-off: slight latency overhead and dependency on OpenRouter's availability.
|
||
|
||
### #2: Reddit as first social source (RSS primary, .json fallback)
|
||
Reddit has rich discussion data ideal for identifying recurring questions and pain points. Reddit RSS/Atom feeds are the primary data source — they work reliably from datacenter IPs (Hostinger) where .json endpoints return 403. The .json endpoints serve as a fallback for posts and are the only source for comment fetching (RSS doesn't include comments). Trade-off: RSS lacks engagement metrics (score, comment count), but post titles and content are sufficient for topic analysis.
|
||
|
||
### #3: Chief editor agent instead of human review queue
|
||
The user wants full automation — a second LLM pass reviews quality, SEO, and accuracy before publishing. Posts that fail editorial review are flagged for human intervention rather than published. Trade-off: doubles the LLM cost of the "review" step, but catches quality issues.
|
||
|
||
### #4: Configurable pipeline (single-pass vs. multi-step)
|
||
Some users want cheap/fast, others want high quality. Making this configurable avoids forcing one approach. Trade-off: more code paths to maintain.
|
||
|
||
### #5: Composite content score using LLM
|
||
Rather than just tracking raw metrics, we periodically have the LLM evaluate what made high-performing posts succeed and low-performing posts fail. This feeds back into the analysis and generation prompts. Trade-off: additional API cost for scoring, but enables the self-improvement loop.
|
||
|
||
### #6: Source provider interface for future platforms
|
||
Reddit is the only implemented source, but the interface pattern means adding a new source (YouTube, TikTok, etc.) is one class implementation plus a settings checkbox. We removed the old stub provider files (TikTok, Instagram, YouTube) because dead code confuses AI agents and humans alike — the interface is the contract, not empty classes. Trade-off: slight over-engineering for day one, but pays off immediately at source #2.
|
||
|
||
### #7: Custom tables for high-volume data
|
||
Source data, analysis results, and generation logs are high-write, time-series data. WordPress post_meta and options are wrong for this. Custom tables with proper indexes. Trade-off: more complex activation/uninstall, but correct.
|
||
|
||
### #8: All API keys encrypted at rest
|
||
Using `wp_salt()` as encryption key with OpenSSL. Not bulletproof (salt is in wp-config.php) but significantly better than plaintext in wp_options. Trade-off: adds complexity to option get/set, but necessary for security.
|
||
|
||
### #9: Structured logger instead of raw error_log()
|
||
All application logging flows through `PRAutoBlogger_Logger` singleton, which writes to the `prab_event_log` table and forwards errors/warnings to PHP's `error_log()`. This gives users an in-admin Activity Log page with filtering by level, search, and pagination — much more accessible than server logs. Trade-off: one extra DB write per log entry, but the table is prunable and indexed.
|
||
|
||
### #10: Database-level atomic mutex for generation lock
|
||
Uses `INSERT IGNORE` on wp_options (which has a UNIQUE index on `option_name`) instead of transient-based locking. This eliminates the TOCTOU race condition where two concurrent cron runs could both read the transient as "not locked" before either sets it. Expired locks older than 1 hour are cleaned up first to prevent permanent deadlock.
|
||
|
||
### #11: wp.element for frontend React (no build step)
|
||
The posts widget uses WordPress-bundled React (`wp.element`) with raw `createElement` calls instead of JSX. This avoids requiring a Node.js build pipeline for what is a simple card grid. Trade-off: more verbose component code, but zero tooling dependencies for the plugin consumer.
|
||
|
||
### #12: REST API for frontend data fetching
|
||
The widget fetches posts asynchronously from a dedicated REST endpoint rather than rendering server-side. This keeps initial page load fast and allows the endpoint to be cached independently (5-minute Cache-Control). The permission callback uses a filter (`prautoblogger_rest_posts_public`) so sites can restrict access if needed.
|
||
|
||
### #13: Run-ID based log linking
|
||
Each pipeline execution generates a UUID (`run_id`) that tags every `prab_generation_log` entry. When a post is published, `link_generation_logs()` uses `UPDATE WHERE run_id = X` to associate entries with the `post_id`. This is more reliable than the previous timestamp-window approach, especially in batch runs where multiple posts are generated in quick succession. **v0.8.1+**: `run_id` is also persisted on each post as `_prautoblogger_run_id` post_meta, giving the orphan-research reaper a direct path from run to siblings without walking the gen_log table (see #19).
|
||
|
||
### #19: Daily orphan-research-row reaper (v0.8.1)
|
||
When a pipeline dies between post creation and the final `Post_Assembler::amortize_research_costs()` call (Hostinger exec kill, OOM, LiteSpeed cut, fatal), the shared `llm_research` cost row stays in `prab_generation_log` with `post_id = NULL` and sibling articles understate their true cost in the breakdown popover. A new daily WP-Cron event `prautoblogger_reap_orphan_research_rows` (hooked to `PRAutoBlogger_Research_Reaper::on_cron`, 03:15 server time — 15 min after the primary generation cron) scans for orphan rows older than a 1-hour grace window, looks up sibling posts by `_prautoblogger_run_id` post_meta (fallback: distinct `post_id`s in gen_log for the same run_id), and delegates to the existing idempotent `amortize_research_costs()` on match. Orphans that still have no siblings after 7 days are deleted outright as sunk cost. A one-time activator migration (`prautoblogger_migrated_run_id_meta_v081`) back-populates `_prautoblogger_run_id` on existing posts from gen_log so the meta-primary path works on upgrade. Manual ops trigger: `wp prautoblogger reap-research`.
|
||
|
||
### #14: Reddit RSS replaces PullPush.io (and earlier Reddit OAuth)
|
||
Reddit rejected our OAuth API application (April 2026). We initially switched to PullPush.io, but its index was frequently stale or unavailable. Reddit's RSS/Atom feeds (`/r/{sub}/hot.rss`) proved the most reliable option — they work from datacenter IPs where .json gets 403, require no auth, and have no apparent rate limit. The .json endpoints are kept as a fallback for posts and as the sole source for comment data. Each collected item's metadata includes a `data_source` field (`reddit_rss` or `reddit_json`) for auditability.
|
||
|
||
### #18: OpenRouter model registry — daily refresh, WP option + transient cache, zero-coupling
|
||
|
||
The admin model picker (v1) needs to list OpenRouter models with pricing and capabilities. We fetch `https://openrouter.ai/api/v1/models` (free, unauthenticated) once daily and store the normalized payload in a WP option fronted by a 24h transient. On stale-and-fetch-fails, we serve last-good + surface a warning. The registry class (`class-open-router-model-registry.php`) takes all config via constructor (option name, transient name, endpoint URL) — no PRAUTOBLOGGER_* constants inside the class body. Phase 2 lifts it into a shared Composer package with zero internal edits. Phase 3 adds a parallel `Cloudflare_WorkersAI_Model_Registry` behind the same interface. Capability vocabulary: `text→text`, `text+image→text`, `text+audio→text`, `text→image`, `text→audio`, `text→video`, `text→embedding`. Cost: $0/month.
|
||
|
||
### #20: Editorial illustration prompt + style-template setting (v0.16.0, May 2026)
|
||
|
||
Commit 1 of the in-plugin editorial image pipeline (brief `docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md`). The image style pivots from a single-panel newspaper comic to a **text-free editorial scientific illustration**. Two changes ship here; the deterministic PHP composer is a later commit.
|
||
|
||
1. **Prompt structure.** `Image_Prompt_Builder::REWRITER_SYSTEM_PROMPT` now asks the rewriter LLM for a concise 1-2 sentence topic/mechanism summary (one concrete centered focal subject, no text/people-as-gag/logos) as the SCENE, keeping the short CAPTION line for the unchanged HTML-caption-below-image path. The scene/caption parsing contract (`Image_Scene_Parser`) is unchanged.
|
||
2. **Template substitution.** The old `trim($scene . ' ' . $style_suffix)` concat is replaced by `PRAutoBlogger_Image_Template_Filler::fill($scene)` in all three entry points (`build_article_prompt`, `build_source_prompt`, `build_fallback_prompt`). The filler resolves the active template (admin override → default), strips control chars and clamps the summary length (brief A5), then substitutes the single `{{ topic_summary }}` token. Degradation (brief A5/A6): if the template lacks exactly one token it appends the summary and logs a warning; if the summary is empty it emits the style-only prompt — a blank or token-only prompt is never sent to the provider.
|
||
3. **Setting.** New textarea `prautoblogger_image_style_template` (default `PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_TEMPLATE`) replaces the Style Suffix field. On save it is sanitised with `sanitize_textarea_field` and validated to contain exactly one token (`Image_Template_Filler::sanitize_for_save`). The old `prautoblogger_image_style_suffix` is mirrored to `..._deprecated` for one cycle and not deleted (one-time migration `prautoblogger_migrated_style_template_v0160`).
|
||
|
||
Provider/model are unchanged — only the prompt text changes, so production keeps emitting via Runware FLUX.1 schnell but now produces text-free editorial base images.
|
||
|
||
### #21: Deterministic PHP image composer with capability ladder (v0.17.0, Jun 2026)
|
||
|
||
Commit 2 (and final scope) of the in-plugin editorial image pipeline (#20). Small diffusion
|
||
models cannot render clean text (2026-04-21 A/B finding), so base images are text-free —
|
||
but the one image that travels *without* its HTML caption is the social/OG share image.
|
||
A **deterministic local composer** (`PRAutoBlogger_Image_Composer` behind
|
||
`PRAutoBlogger_Image_Composer_Interface`) now runs between provider bytes and sideload:
|
||
|
||
1. **Variants.** Image A → corner-marked featured (1200×632) + branded OG (1200×630, teal
|
||
band, brand mark, baked caption in Poppins SemiBold) + square card (1080×1080: full base
|
||
downscaled into a 1080×569 top slice — no upscaling — cream caption panel in Poppins
|
||
Bold, teal footer with the horizontal reverse lockup). Image B → corner mark only. The
|
||
variant set is config-driven (`prautoblogger_image_compose_variants`, default `og,square`;
|
||
square has no consumer yet — CEO wants it rendered + stored now). Variants are
|
||
meta-linked via `_prautoblogger_image_role`, `_prautoblogger_base_attachment_id`,
|
||
`_prautoblogger_og_image_id`, `_prautoblogger_square_image_id`. The og key is **frozen**:
|
||
the rebuilt SEO stage (pipeline-redesign §4.8) emits `og:image` from it.
|
||
2. **Capability ladder, never fatal.** Imagick is probed once (PNG format + TTF annotate
|
||
smoke test) and cached in `prautoblogger_image_compose_capability`, fingerprinted by
|
||
PHP version + imagick/gd presence so host flips (hPanel/cagefs — it has happened)
|
||
auto-invalidate the cache. Rungs: imagick → `wp_get_image_editor()` resize-only
|
||
(unbranded, square skipped because it would upscale) → pass-through. Every render is
|
||
try/caught; degradation logs one WARNING per run; the publish always proceeds with the
|
||
editable HTML caption intact. Test/ops override: `prautoblogger_image_compose_capability`
|
||
filter.
|
||
3. **Determinism.** Bundled OFL fonts (no system-font queries), vendored brand PNGs (no
|
||
runtime SVG delegate variance), `stripImage()` + PNG date/time chunks excluded + fixed
|
||
compression → byte-stable output **per environment** (not across ImageMagick versions).
|
||
Composition is local CPU, $0, logged as a duration-only stage on the `image_composer`
|
||
channel — no cost-tracker rows.
|
||
4. **Layout tuning.** Geometry/opacity are constants in
|
||
`PRAutoBlogger_Image_Composer_Layout::defaults()` behind the
|
||
`prautoblogger_image_compose_layout` filter — deliberately not settings; promote only if
|
||
the CEO asks to tune them. The caption is hard-clamped (word-boundary wrap + ellipsis,
|
||
multibyte-safe) even though the rewriter LLM is already constrained to <15 words.
|
||
|
||
### #17: Runware as default image backend (v0.9.0, Apr 2026)
|
||
|
||
After a comic-style A/B round in Apr 2026, FLUX.1 schnell via Runware was chosen as the default image backend. Cost is ~$0.0006/image vs ~$0.039/image for Gemini 2.5 Flash Image via OpenRouter (≈65× cheaper), and the looser schnell fidelity reads as editorial-cartoon style — a feature, not a bug, for our single-panel comic aesthetic. FLUX.1 dev (~$0.02/image, 28 steps) remains available as an opt-in for higher-fidelity runs. The Runware layer mirrors the OpenRouter split (interface + provider + support + pricing + batch), all under the 300-line cap. The batch class dispatches `imageInference` POSTs via `curl_multi_init/exec/select` — wall-clock time for the A/B pair drops to ≈ the slowest single image. A v0.9.0 one-time migration (`prautoblogger_migrated_default_image_v090`) flips sites still on the legacy default; explicit user selections are preserved. Cloudflare Workers AI was removed as an image provider in v0.10.0 (see ADR #16 note below).
|
||
|
||
### #16: Image generation via Cloudflare Workers AI (FLUX.1) — SUPERSEDED v0.10.0
|
||
Kept here for history. Until v0.8.2 the default image backend was FLUX.1 schnell on Cloudflare Workers AI, routed via the Cloudflare AI Gateway for caching + unified telemetry. In v0.9.0 (2026-04-21) Runware FLUX.1 schnell became the default (65× cheaper, see ADR #17). In v0.10.0 (2026-04-21) the Cloudflare Workers AI provider was removed entirely — classes, settings, tests, and registry entries are gone; any CF user is migrated to `runware:100@1` on upgrade via `PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run()`. The Cloudflare AI Gateway in front of OpenRouter (ADR #15) is unaffected — it is a distinct integration.
|
||
|
||
|
||
### #15: Optional Cloudflare AI Gateway in front of OpenRouter
|
||
We already use Cloudflare for DNS/CDN on peptiderepo.com, so layering AI Gateway in front of OpenRouter is zero marginal infrastructure. It gives us response caching (meaningful for repeated classification/scoring calls), a unified cost/latency dashboard, rate limiting, and provider fallback — all of which we would otherwise have to build ourselves to satisfy the CTO cost-tracking rules. Kept as an opt-in URL setting (`prautoblogger_ai_gateway_base_url`) so the plugin still works unchanged out of the box and can be bypassed instantly if the gateway misbehaves. The gateway is a transparent OpenRouter-compatible proxy; no new provider class is needed, and the response parsing path (`usage`, `choices[0].message.content`) is unchanged.
|
||
|
||
---
|
||
|
||
### #20: PHPUnit test infrastructure + WordPress-Core PHPCS compliance (v0.10.1)
|
||
|
||
Tests use Brain\Monkey for WordPress function mocking (no database required). The BaseTestCase singleton handles setup/teardown of Monkey stubs and provides fixture helpers for common data shapes (SourceData, ArticleIdea, GenerationLog, etc.). All WordPress functions used by the plugin are stubbed in BaseTestCase::setUp() so individual test classes only override what they need. In v0.10.1, missing stubs for `post_type_exists()` were added to support PeptideLinker's PR-Core guards. All 1,362 WordPress-Core coding-standards violations (short array syntax, line length, spacing) were auto-fixed via phpcbf and the CI gate was flipped to strict mode — PHPCS failures now block CI. This prevents future regressions on the code style front.
|
||
|
||
|
||
### #22: Kanban board as primary admin landing screen (v0.19.0)
|
||
|
||
The primary admin screen is now a kanban board (Direction C "Editorial Record") with four
|
||
columns: Generating | In Review | Published | Failed. A separate `class-board-page.php`
|
||
registers the `Board` submenu first so it appears before Settings in the nav.
|
||
`class-board-data-provider.php` computes column cards from existing tables
|
||
(`prab_generat |