Initial commit: PRAutoBlogger v0.1.0 with frontend posts widget
This commit is contained in:
98
.github/mu-plugins/autoblogger-deploy-receiver.php → .github/mu-plugins/prautoblogger-deploy-receiver.php
vendored
Executable file → Normal file
98
.github/mu-plugins/autoblogger-deploy-receiver.php → .github/mu-plugins/prautoblogger-deploy-receiver.php
vendored
Executable file → Normal file
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: AutoBlogger Deploy Receiver
|
||||
* Plugin Name: PRAutoBlogger Deploy Receiver
|
||||
* Description: REST endpoint that accepts authenticated plugin zip uploads for automated deployment.
|
||||
* Version: 1.0.0
|
||||
* Author: AutoBlogger CI/CD
|
||||
* Author: PRAutoBlogger CI/CD
|
||||
*
|
||||
* INSTALLATION:
|
||||
* 1. Upload this file to wp-content/mu-plugins/autoblogger-deploy-receiver.php
|
||||
* 1. Upload this file to wp-content/mu-plugins/prautoblogger-deploy-receiver.php
|
||||
* 2. Add to wp-config.php (above "That's all, stop editing!"):
|
||||
* define('AUTOBLOGGER_DEPLOY_KEY', 'your-random-64-char-token');
|
||||
* define('PRAUTOBLOGGER_DEPLOY_KEY', 'your-random-64-char-token');
|
||||
* 3. Generate a key: php -r "echo bin2hex(random_bytes(32));"
|
||||
*
|
||||
* SECURITY:
|
||||
* - Deploy key validated with hash_equals() (timing-safe comparison).
|
||||
* - Only accepts zip files containing autoblogger/autoblogger.php.
|
||||
* - Only accepts zip files containing prautoblogger/prautoblogger.php.
|
||||
* - Backs up current plugin before extraction; rolls back on failure.
|
||||
* - Rate limited: max 1 deploy per 60 seconds.
|
||||
* - All actions logged to PHP error_log for audit trail.
|
||||
@@ -25,34 +25,34 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
// Hard requirement: deploy key must be defined in wp-config.php.
|
||||
if ( ! defined( 'AUTOBLOGGER_DEPLOY_KEY' ) || '' === AUTOBLOGGER_DEPLOY_KEY ) {
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEPLOY_KEY' ) || '' === PRAUTOBLOGGER_DEPLOY_KEY ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'rest_api_init', 'autoblogger_deploy_register_route' );
|
||||
add_action( 'rest_api_init', 'prautoblogger_deploy_register_route' );
|
||||
|
||||
/**
|
||||
* Register the deploy REST route.
|
||||
*/
|
||||
function autoblogger_deploy_register_route(): void {
|
||||
register_rest_route( 'autoblogger-deploy/v1', '/deploy', [
|
||||
function prautoblogger_deploy_register_route(): void {
|
||||
register_rest_route( 'prautoblogger-deploy/v1', '/deploy', [
|
||||
'methods' => 'POST',
|
||||
'callback' => 'autoblogger_deploy_handle',
|
||||
'permission_callback' => 'autoblogger_deploy_check_auth',
|
||||
'callback' => 'prautoblogger_deploy_handle',
|
||||
'permission_callback' => 'prautoblogger_deploy_check_auth',
|
||||
] );
|
||||
|
||||
// Health check endpoint (GET) — useful for monitoring.
|
||||
register_rest_route( 'autoblogger-deploy/v1', '/status', [
|
||||
register_rest_route( 'prautoblogger-deploy/v1', '/status', [
|
||||
'methods' => 'GET',
|
||||
'callback' => function () {
|
||||
return [
|
||||
'status' => 'ready',
|
||||
'plugin' => is_dir( WP_PLUGIN_DIR . '/autoblogger' ) ? 'installed' : 'not_installed',
|
||||
'active' => is_plugin_active( 'autoblogger/autoblogger.php' ),
|
||||
'version' => autoblogger_deploy_get_installed_version(),
|
||||
'plugin' => is_dir( WP_PLUGIN_DIR . '/prautoblogger' ) ? 'installed' : 'not_installed',
|
||||
'active' => is_plugin_active( 'prautoblogger/prautoblogger.php' ),
|
||||
'version' => prautoblogger_deploy_get_installed_version(),
|
||||
];
|
||||
},
|
||||
'permission_callback' => 'autoblogger_deploy_check_auth',
|
||||
'permission_callback' => 'prautoblogger_deploy_check_auth',
|
||||
] );
|
||||
}
|
||||
|
||||
@@ -60,20 +60,20 @@ function autoblogger_deploy_register_route(): void {
|
||||
* Validate the deploy key from the request header.
|
||||
* Uses hash_equals() for timing-safe comparison.
|
||||
*/
|
||||
function autoblogger_deploy_check_auth( WP_REST_Request $request ): bool {
|
||||
function prautoblogger_deploy_check_auth( WP_REST_Request $request ): bool {
|
||||
$key = $request->get_header( 'X-Deploy-Key' );
|
||||
if ( null === $key || '' === $key ) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals( (string) AUTOBLOGGER_DEPLOY_KEY, $key );
|
||||
return hash_equals( (string) PRAUTOBLOGGER_DEPLOY_KEY, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a deploy request: receive zip, validate, extract, activate.
|
||||
*/
|
||||
function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response|WP_Error {
|
||||
function prautoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response|WP_Error {
|
||||
// Rate limit: 1 deploy per 60 seconds.
|
||||
$lock = get_transient( 'autoblogger_deploy_lock' );
|
||||
$lock = get_transient( 'prautoblogger_deploy_lock' );
|
||||
if ( false !== $lock ) {
|
||||
return new WP_Error(
|
||||
'rate_limited',
|
||||
@@ -81,12 +81,12 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
[ 'status' => 429 ]
|
||||
);
|
||||
}
|
||||
set_transient( 'autoblogger_deploy_lock', time(), 60 );
|
||||
set_transient( 'prautoblogger_deploy_lock', time(), 60 );
|
||||
|
||||
// 1. Validate uploaded file exists.
|
||||
$files = $request->get_file_params();
|
||||
if ( empty( $files['plugin']['tmp_name'] ) || ! is_uploaded_file( $files['plugin']['tmp_name'] ) ) {
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
return new WP_Error( 'no_file', 'No plugin zip uploaded.', [ 'status' => 400 ] );
|
||||
}
|
||||
|
||||
@@ -95,13 +95,13 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
// 2. Validate it's a real zip and contains our plugin.
|
||||
$zip = new ZipArchive();
|
||||
if ( true !== $zip->open( $tmp_file ) ) {
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
return new WP_Error( 'invalid_zip', 'Uploaded file is not a valid zip.', [ 'status' => 400 ] );
|
||||
}
|
||||
|
||||
$has_main_file = false;
|
||||
for ( $i = 0; $i < $zip->numFiles; $i++ ) {
|
||||
if ( 'autoblogger/autoblogger.php' === $zip->getNameIndex( $i ) ) {
|
||||
if ( 'prautoblogger/prautoblogger.php' === $zip->getNameIndex( $i ) ) {
|
||||
$has_main_file = true;
|
||||
break;
|
||||
}
|
||||
@@ -109,40 +109,40 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
|
||||
if ( ! $has_main_file ) {
|
||||
$zip->close();
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
return new WP_Error(
|
||||
'invalid_plugin',
|
||||
'Zip must contain autoblogger/autoblogger.php at root.',
|
||||
'Zip must contain prautoblogger/prautoblogger.php at root.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Read version from the incoming zip before extraction.
|
||||
$incoming_main = $zip->getFromName( 'autoblogger/autoblogger.php' );
|
||||
$incoming_main = $zip->getFromName( 'prautoblogger/prautoblogger.php' );
|
||||
$incoming_version = 'unknown';
|
||||
if ( $incoming_main && preg_match( '/Version:\s*([0-9]+\.[0-9]+\.[0-9]+)/', $incoming_main, $m ) ) {
|
||||
$incoming_version = $m[1];
|
||||
}
|
||||
|
||||
$plugin_dir = WP_PLUGIN_DIR . '/autoblogger';
|
||||
$plugin_dir = WP_PLUGIN_DIR . '/prautoblogger';
|
||||
$backup_dir = null;
|
||||
|
||||
// 4. Deactivate plugin before file operations (prevent fatal errors).
|
||||
$was_active = is_plugin_active( 'autoblogger/autoblogger.php' );
|
||||
$was_active = is_plugin_active( 'prautoblogger/prautoblogger.php' );
|
||||
if ( $was_active ) {
|
||||
deactivate_plugins( 'autoblogger/autoblogger.php', true );
|
||||
deactivate_plugins( 'prautoblogger/prautoblogger.php', true );
|
||||
}
|
||||
|
||||
// 5. Backup current installation if it exists.
|
||||
if ( is_dir( $plugin_dir ) ) {
|
||||
$backup_dir = WP_PLUGIN_DIR . '/autoblogger-backup-' . time();
|
||||
$backup_dir = WP_PLUGIN_DIR . '/prautoblogger-backup-' . time();
|
||||
if ( ! rename( $plugin_dir, $backup_dir ) ) {
|
||||
$zip->close();
|
||||
// Re-activate if we deactivated.
|
||||
if ( $was_active ) {
|
||||
activate_plugin( 'autoblogger/autoblogger.php' );
|
||||
activate_plugin( 'prautoblogger/prautoblogger.php' );
|
||||
}
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
return new WP_Error( 'backup_failed', 'Could not backup current plugin.', [ 'status' => 500 ] );
|
||||
}
|
||||
}
|
||||
@@ -151,30 +151,30 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
$extract_ok = $zip->extractTo( WP_PLUGIN_DIR );
|
||||
$zip->close();
|
||||
|
||||
if ( ! $extract_ok || ! file_exists( $plugin_dir . '/autoblogger.php' ) ) {
|
||||
if ( ! $extract_ok || ! file_exists( $plugin_dir . '/prautoblogger.php' ) ) {
|
||||
// Rollback: restore backup.
|
||||
if ( $backup_dir && is_dir( $backup_dir ) ) {
|
||||
rename( $backup_dir, $plugin_dir );
|
||||
}
|
||||
if ( $was_active ) {
|
||||
activate_plugin( 'autoblogger/autoblogger.php' );
|
||||
activate_plugin( 'prautoblogger/prautoblogger.php' );
|
||||
}
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
error_log( '[AutoBlogger Deploy] Extraction failed. Rolled back to previous version.' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
error_log( '[PRAutoBlogger Deploy] Extraction failed. Rolled back to previous version.' );
|
||||
return new WP_Error( 'extract_failed', 'Zip extraction failed. Rolled back.', [ 'status' => 500 ] );
|
||||
}
|
||||
|
||||
// 7. Activate plugin.
|
||||
$activation_result = activate_plugin( 'autoblogger/autoblogger.php' );
|
||||
$activation_result = activate_plugin( 'prautoblogger/prautoblogger.php' );
|
||||
if ( is_wp_error( $activation_result ) ) {
|
||||
// Activation failed — rollback.
|
||||
autoblogger_deploy_rmdir_recursive( $plugin_dir );
|
||||
prautoblogger_deploy_rmdir_recursive( $plugin_dir );
|
||||
if ( $backup_dir && is_dir( $backup_dir ) ) {
|
||||
rename( $backup_dir, $plugin_dir );
|
||||
activate_plugin( 'autoblogger/autoblogger.php' );
|
||||
activate_plugin( 'prautoblogger/prautoblogger.php' );
|
||||
}
|
||||
delete_transient( 'autoblogger_deploy_lock' );
|
||||
error_log( '[AutoBlogger Deploy] Activation failed: ' . $activation_result->get_error_message() . '. Rolled back.' );
|
||||
delete_transient( 'prautoblogger_deploy_lock' );
|
||||
error_log( '[PRAutoBlogger Deploy] Activation failed: ' . $activation_result->get_error_message() . '. Rolled back.' );
|
||||
return new WP_Error(
|
||||
'activation_failed',
|
||||
'Plugin activation failed: ' . $activation_result->get_error_message() . '. Rolled back.',
|
||||
@@ -184,12 +184,12 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
|
||||
// 8. Clean up backup.
|
||||
if ( $backup_dir && is_dir( $backup_dir ) ) {
|
||||
autoblogger_deploy_rmdir_recursive( $backup_dir );
|
||||
prautoblogger_deploy_rmdir_recursive( $backup_dir );
|
||||
}
|
||||
|
||||
$old_version = autoblogger_deploy_get_installed_version();
|
||||
$old_version = prautoblogger_deploy_get_installed_version();
|
||||
error_log( sprintf(
|
||||
'[AutoBlogger Deploy] Successfully deployed v%s (was: %s) at %s',
|
||||
'[PRAutoBlogger Deploy] Successfully deployed v%s (was: %s) at %s',
|
||||
$incoming_version,
|
||||
$old_version ?: 'fresh install',
|
||||
gmdate( 'Y-m-d H:i:s' )
|
||||
@@ -198,15 +198,15 @@ function autoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response
|
||||
return new WP_REST_Response( [
|
||||
'success' => true,
|
||||
'version' => $incoming_version,
|
||||
'message' => "AutoBlogger v{$incoming_version} deployed and activated.",
|
||||
'message' => "PRAutoBlogger v{$incoming_version} deployed and activated.",
|
||||
], 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently installed plugin version from the main file header.
|
||||
*/
|
||||
function autoblogger_deploy_get_installed_version(): string {
|
||||
$main_file = WP_PLUGIN_DIR . '/autoblogger/autoblogger.php';
|
||||
function prautoblogger_deploy_get_installed_version(): string {
|
||||
$main_file = WP_PLUGIN_DIR . '/prautoblogger/prautoblogger.php';
|
||||
if ( ! file_exists( $main_file ) ) {
|
||||
return '';
|
||||
}
|
||||
@@ -217,7 +217,7 @@ function autoblogger_deploy_get_installed_version(): string {
|
||||
/**
|
||||
* Recursively delete a directory.
|
||||
*/
|
||||
function autoblogger_deploy_rmdir_recursive( string $dir ): void {
|
||||
function prautoblogger_deploy_rmdir_recursive( string $dir ): void {
|
||||
if ( ! is_dir( $dir ) ) {
|
||||
return;
|
||||
}
|
||||
28
.github/workflows/deploy.yml
vendored
28
.github/workflows/deploy.yml
vendored
@@ -1,17 +1,17 @@
|
||||
# AutoBlogger CI/CD Pipeline
|
||||
# PRAutoBlogger CI/CD Pipeline
|
||||
#
|
||||
# Triggers on every push to main. Runs PHP lint and coding standards checks,
|
||||
# packages the plugin into a clean zip (excluding dev files), and deploys it
|
||||
# to the live WordPress site via the deploy-receiver mu-plugin REST endpoint.
|
||||
#
|
||||
# Required GitHub Secrets:
|
||||
# DEPLOY_URL — https://peptiderepo.com/wp-json/autoblogger-deploy/v1/deploy
|
||||
# DEPLOY_KEY — The token matching AUTOBLOGGER_DEPLOY_KEY in wp-config.php
|
||||
# DEPLOY_URL — https://peptiderepo.com/wp-json/prautoblogger-deploy/v1/deploy
|
||||
# DEPLOY_KEY — The token matching PRAUTOBLOGGER_DEPLOY_KEY in wp-config.php
|
||||
#
|
||||
# @see .github/mu-plugins/autoblogger-deploy-receiver.php — Server-side endpoint.
|
||||
# @see .github/mu-plugins/prautoblogger-deploy-receiver.php — Server-side endpoint.
|
||||
# @see scripts/deploy.sh — Manual deploy script.
|
||||
|
||||
name: Deploy AutoBlogger
|
||||
name: Deploy PRAutoBlogger
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -106,22 +106,22 @@ jobs:
|
||||
- name: Read plugin version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" autoblogger.php)
|
||||
VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" prautoblogger.php)
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Plugin version: $VERSION"
|
||||
|
||||
- name: Build plugin zip
|
||||
run: |
|
||||
# Create a clean build directory
|
||||
mkdir -p build/autoblogger
|
||||
mkdir -p build/prautoblogger
|
||||
|
||||
# Copy only production files (exclude dev/CI artifacts)
|
||||
rsync -av --exclude-from='.distignore' ./ build/autoblogger/
|
||||
rsync -av --exclude-from='.distignore' ./ build/prautoblogger/
|
||||
|
||||
# Create the zip
|
||||
cd build
|
||||
zip -r "autoblogger-${{ steps.version.outputs.version }}.zip" autoblogger/
|
||||
ls -lh "autoblogger-${{ steps.version.outputs.version }}.zip"
|
||||
zip -r "prautoblogger-${{ steps.version.outputs.version }}.zip" prautoblogger/
|
||||
ls -lh "prautoblogger-${{ steps.version.outputs.version }}.zip"
|
||||
|
||||
- name: Deploy to WordPress
|
||||
env:
|
||||
@@ -129,9 +129,9 @@ jobs:
|
||||
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
ZIP_PATH="build/autoblogger-${VERSION}.zip"
|
||||
ZIP_PATH="build/prautoblogger-${VERSION}.zip"
|
||||
|
||||
echo "Deploying AutoBlogger v${VERSION} to production..."
|
||||
echo "Deploying PRAutoBlogger v${VERSION} to production..."
|
||||
|
||||
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST "${DEPLOY_URL}" \
|
||||
@@ -154,6 +154,6 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: autoblogger-${{ steps.version.outputs.version }}
|
||||
path: build/autoblogger-${{ steps.version.outputs.version }}.zip
|
||||
name: prautoblogger-${{ steps.version.outputs.version }}
|
||||
path: build/prautoblogger-${{ steps.version.outputs.version }}.zip
|
||||
retention-days: 30
|
||||
|
||||
158
ARCHITECTURE.md
158
ARCHITECTURE.md
@@ -1,6 +1,6 @@
|
||||
# AutoBlogger — Architecture
|
||||
# PRAutoBlogger — Architecture
|
||||
|
||||
AutoBlogger 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@ AutoBlogger is a WordPress plugin that monitors social media (starting with Redd
|
||||
│ 6. Publisher │ Creates/updates WordPress post
|
||||
│ │ Sets categories, tags, featured image
|
||||
│ │ Stores generation metadata in post_meta
|
||||
│ │ Logs cost/tokens in `ab_generation_log` table
|
||||
│ │ Logs cost/tokens in `prab_generation_log` table
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -75,8 +75,8 @@ AutoBlogger is a WordPress plugin that monitors social media (starting with Redd
|
||||
## File Tree
|
||||
|
||||
```
|
||||
autoblogger/
|
||||
├── autoblogger.php # Plugin bootstrap — minimal, only hook registration
|
||||
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
|
||||
@@ -86,12 +86,14 @@ autoblogger/
|
||||
│
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── admin.css # Admin page styles (wp-admin conventions)
|
||||
│ │ ├── admin.css # Admin page styles (wp-admin conventions)
|
||||
│ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars)
|
||||
│ └── js/
|
||||
│ └── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
||||
│ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
||||
│ └── posts-widget.js # React component for frontend post cards (wp.element)
|
||||
│
|
||||
├── includes/
|
||||
│ ├── class-autoblogger.php # Main orchestrator — registers all hooks via loader
|
||||
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks via loader
|
||||
│ ├── 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
|
||||
@@ -131,6 +133,9 @@ autoblogger/
|
||||
│ │ ├── class-instagram-provider.php # Instagram — stub/future implementation
|
||||
│ │ └── class-youtube-provider.php # YouTube — stub/future implementation
|
||||
│ │
|
||||
│ ├── frontend/
|
||||
│ │ └── 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
|
||||
@@ -149,7 +154,7 @@ autoblogger/
|
||||
│ └── metabox-generation-info.php # Post metabox template
|
||||
│
|
||||
├── languages/
|
||||
│ └── autoblogger.pot # i18n translation template
|
||||
│ └── prautoblogger.pot # i18n translation template
|
||||
│
|
||||
└── tests/
|
||||
├── unit/ # PHPUnit tests (mocked dependencies)
|
||||
@@ -158,11 +163,56 @@ autoblogger/
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Custom Tables
|
||||
|
||||
All tables use `$wpdb->prefix` + `autoblogger_` prefix.
|
||||
All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||
|
||||
#### `ab_source_data` — Raw social media data
|
||||
| Column | Type | Description |
|
||||
@@ -197,7 +247,7 @@ All tables use `$wpdb->prefix` + `autoblogger_` prefix.
|
||||
| INDEX | analysis_type, relevance_score |
|
||||
| INDEX | analyzed_at |
|
||||
|
||||
#### `ab_generation_log` — API cost and generation tracking
|
||||
#### `prab_generation_log` — API cost and generation tracking
|
||||
| Column | Type | Description |
|
||||
|------------------|-------------------|-----------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
@@ -218,7 +268,7 @@ All tables use `$wpdb->prefix` + `autoblogger_` prefix.
|
||||
| INDEX | created_at |
|
||||
| INDEX | stage |
|
||||
|
||||
#### `ab_event_log` — Structured application logging
|
||||
#### `prab_event_log` — Structured application logging
|
||||
| Column | Type | Description |
|
||||
|------------------|-------------------|-----------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
@@ -248,53 +298,53 @@ All tables use `$wpdb->prefix` + `autoblogger_` prefix.
|
||||
|
||||
### WordPress Options (`wp_options`)
|
||||
|
||||
All prefixed with `autoblogger_`:
|
||||
All prefixed with `prautoblogger_`:
|
||||
|
||||
| Option Key | Description |
|
||||
|-------------------------------------|------------------------------------------------------|
|
||||
| `autoblogger_openrouter_api_key` | Encrypted OpenRouter API key |
|
||||
| `autoblogger_reddit_client_id` | Reddit OAuth client ID |
|
||||
| `autoblogger_reddit_client_secret` | Encrypted Reddit client secret |
|
||||
| `autoblogger_reddit_access_token` | Reddit OAuth access token (transient-backed) |
|
||||
| `autoblogger_ga4_property_id` | Google Analytics 4 property ID |
|
||||
| `autoblogger_ga4_credentials_json` | Encrypted GA4 service account credentials |
|
||||
| `autoblogger_analysis_model` | OpenRouter model for analysis (default: cheap) |
|
||||
| `autoblogger_writing_model` | OpenRouter model for writing (default: quality) |
|
||||
| `autoblogger_editor_model` | OpenRouter model for chief editor (default: quality) |
|
||||
| `autoblogger_daily_article_target` | Number of articles per day (1-10, default: 1) |
|
||||
| `autoblogger_writing_pipeline` | 'single_pass' or 'multi_step' (default: multi_step) |
|
||||
| `autoblogger_niche_description` | Text description of the site's niche |
|
||||
| `autoblogger_target_subreddits` | JSON array of subreddits to monitor |
|
||||
| `autoblogger_monthly_budget_usd` | Monthly API spend limit in USD |
|
||||
| `autoblogger_tone` | Content tone (informational, conversational, etc.) |
|
||||
| `autoblogger_min_word_count` | Minimum article word count (default: 800) |
|
||||
| `autoblogger_max_word_count` | Maximum article word count (default: 2000) |
|
||||
| `autoblogger_topic_exclusions` | JSON array of topics to never write about |
|
||||
| `autoblogger_enabled_sources` | JSON array of active source types |
|
||||
| `autoblogger_auto_publish` | Toggle: auto-publish approved posts (default: '0') |
|
||||
| `autoblogger_default_author` | WP user ID for generated post authorship |
|
||||
| `autoblogger_default_category` | WP category ID fallback for generated posts |
|
||||
| `autoblogger_log_level` | Logging threshold: error/warning/info/debug |
|
||||
| `autoblogger_db_version` | Schema version for migrations |
|
||||
| `autoblogger_schedule_time` | Daily generation time (HH:MM, default: '03:00') |
|
||||
| `prautoblogger_openrouter_api_key` | Encrypted OpenRouter API key |
|
||||
| `prautoblogger_reddit_client_id` | Reddit OAuth client ID |
|
||||
| `prautoblogger_reddit_client_secret` | Encrypted Reddit client secret |
|
||||
| `prautoblogger_reddit_access_token` | Reddit OAuth access token (transient-backed) |
|
||||
| `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') |
|
||||
|
||||
### Post Meta
|
||||
|
||||
Stored on every AutoBlogger-generated post:
|
||||
Stored on every PRAutoBlogger-generated post:
|
||||
|
||||
| Meta Key | Description |
|
||||
|---------------------------------------|----------------------------------------------|
|
||||
| `_autoblogger_generated` | Boolean flag — '1' if generated by plugin |
|
||||
| `_autoblogger_analysis_id` | FK to ab_analysis_results.id |
|
||||
| `_autoblogger_source_ids` | JSON array of source data IDs used |
|
||||
| `_autoblogger_model_used` | Model that generated the content |
|
||||
| `_autoblogger_pipeline_mode` | 'single_pass' or 'multi_step' |
|
||||
| `_autoblogger_total_cost` | Total USD cost for this post |
|
||||
| `_autoblogger_total_tokens` | Total tokens consumed for this post |
|
||||
| `_autoblogger_editor_verdict` | 'approved', 'revised', 'rejected' |
|
||||
| `_autoblogger_editor_notes` | Chief editor's review notes |
|
||||
| `_autoblogger_generated_at` | ISO 8601 timestamp of generation |
|
||||
| `_autoblogger_research_sources` | JSON array of source URLs used |
|
||||
| `_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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -326,8 +376,12 @@ Stored on every AutoBlogger-generated post:
|
||||
|
||||
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 `Autoblogger_Logger` singleton, which writes to the `ab_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.
|
||||
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. **Run-ID based log linking.** Each pipeline execution generates a UUID (`run_id`) that tags every `ab_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.
|
||||
12. **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.
|
||||
|
||||
13. **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.
|
||||
|
||||
11. **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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to AutoBlogger will be documented in this file.
|
||||
All notable changes to PRAutoBlogger will be documented in this file.
|
||||
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project uses [Semantic Versioning](https://semver.org/).
|
||||
@@ -20,7 +20,7 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
||||
- Metrics collector: WordPress native + GA4 integration + composite content scoring.
|
||||
- Admin settings page with sections for API keys, models, content, sources, schedule, and analytics.
|
||||
- Metrics dashboard showing monthly spend, budget utilization, cost by stage, and daily spend.
|
||||
- Post metabox showing generation metadata on AutoBlogger-generated posts.
|
||||
- Post metabox showing generation metadata on PRAutoBlogger-generated posts.
|
||||
- Admin notices for missing API keys, budget warnings, and configuration hints.
|
||||
- Stub providers for TikTok, Instagram, and YouTube (future implementation).
|
||||
- Encrypted storage for API keys using AES-256-CBC with wp_salt().
|
||||
|
||||
103
CONVENTIONS.md
103
CONVENTIONS.md
@@ -1,31 +1,31 @@
|
||||
# AutoBlogger — Conventions
|
||||
# PRAutoBlogger — Conventions
|
||||
|
||||
This document codifies the naming patterns, coding conventions, and step-by-step guides for extending AutoBlogger. Update this file whenever a new pattern is introduced.
|
||||
This document codifies the naming patterns, coding conventions, and step-by-step guides for extending PRAutoBlogger. Update this file whenever a new pattern is introduced.
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### PHP Classes
|
||||
- **Format:** `Autoblogger_{PascalCase}` (e.g., `Autoblogger_Content_Generator`)
|
||||
- **Format:** `PRAutoBlogger_{PascalCase}` (e.g., `PRAutoBlogger_Content_Generator`)
|
||||
- **File naming:** `class-{kebab-case}.php` (e.g., `class-content-generator.php`)
|
||||
- **Interfaces:** `interface-{kebab-case}.php` with `Autoblogger_{Name}_Interface` class name
|
||||
- **Interfaces:** `interface-{kebab-case}.php` with `PRAutoBlogger_{Name}_Interface` class name
|
||||
|
||||
### Hooks (Actions & Filters)
|
||||
- **Prefix:** All hooks start with `autoblogger_`
|
||||
- **Actions:** `autoblogger_{event}` (e.g., `autoblogger_before_content_generation`)
|
||||
- **Filters:** `autoblogger_filter_{what}` (e.g., `autoblogger_filter_article_idea_score`)
|
||||
- **Hook callbacks registered in:** `class-autoblogger.php` (central loader), never in the classes themselves
|
||||
- **Prefix:** All hooks start with `prautoblogger_`
|
||||
- **Actions:** `prautoblogger_{event}` (e.g., `prautoblogger_before_content_generation`)
|
||||
- **Filters:** `prautoblogger_filter_{what}` (e.g., `prautoblogger_filter_article_idea_score`)
|
||||
- **Hook callbacks registered in:** `class-prautoblogger.php` (central loader), never in the classes themselves
|
||||
|
||||
### Database
|
||||
- **Table prefix:** `{$wpdb->prefix}autoblogger_` (e.g., `wp_autoblogger_source_data`)
|
||||
- **Option prefix:** `autoblogger_` (e.g., `autoblogger_openrouter_api_key`)
|
||||
- **Post meta prefix:** `_autoblogger_` (underscore prefix hides from custom fields UI)
|
||||
- **Transient prefix:** `autoblogger_` (e.g., `autoblogger_reddit_token`)
|
||||
- **Table prefix:** `{$wpdb->prefix}prautoblogger_` (e.g., `wp_prautoblogger_source_data`)
|
||||
- **Option prefix:** `prautoblogger_` (e.g., `prautoblogger_openrouter_api_key`)
|
||||
- **Post meta prefix:** `_prautoblogger_` (underscore prefix hides from custom fields UI)
|
||||
- **Transient prefix:** `prautoblogger_` (e.g., `prautoblogger_reddit_token`)
|
||||
|
||||
### Constants
|
||||
- **Format:** `AUTOBLOGGER_{UPPER_SNAKE}` (e.g., `AUTOBLOGGER_MAX_RETRIES`)
|
||||
- **Defined in:** `autoblogger.php` (bootstrap file) for global constants
|
||||
- **Format:** `PRAUTOBLOGGER_{UPPER_SNAKE}` (e.g., `PRAUTOBLOGGER_MAX_RETRIES`)
|
||||
- **Defined in:** `prautoblogger.php` (bootstrap file) for global constants
|
||||
|
||||
### Hook Callback Methods
|
||||
- **Action callbacks:** Prefixed with `on_` (e.g., `on_daily_generation_triggered`)
|
||||
@@ -36,12 +36,12 @@ This document codifies the naming patterns, coding conventions, and step-by-step
|
||||
## Error Handling & Logging
|
||||
|
||||
### Structured Logger
|
||||
All logging uses `Autoblogger_Logger` (singleton) instead of raw `error_log()`. The logger writes to the `ab_event_log` custom table and forwards errors/warnings to PHP's `error_log()` for server-level monitoring. The verbosity threshold is user-configurable via Settings → Publishing → Log Level.
|
||||
All logging uses `PRAutoBlogger_Logger` (singleton) instead of raw `error_log()`. The logger writes to the `prab_event_log` custom table and forwards errors/warnings to PHP's `error_log()` for server-level monitoring. The verbosity threshold is user-configurable via Settings → Publishing → Log Level.
|
||||
|
||||
**Never call `error_log()` directly.** Always use the Logger:
|
||||
|
||||
```php
|
||||
$logger = Autoblogger_Logger::instance();
|
||||
$logger = PRAutoBlogger_Logger::instance();
|
||||
$logger->error( 'API call failed: ' . $error_message, 'openrouter' );
|
||||
$logger->warning( 'Budget nearing limit.', 'cost-tracker' );
|
||||
$logger->info( 'Pipeline complete: 3 posts generated.', 'pipeline' );
|
||||
@@ -63,9 +63,9 @@ The second argument to every log call is a short context tag identifying the ori
|
||||
### What gets shown to the user
|
||||
- API key issues → admin notice (persistent until resolved)
|
||||
- Budget exceeded → admin notice + email to site admin
|
||||
- Generation failures → logged to `ab_generation_log` with `response_status = 'error'`
|
||||
- Generation failures → logged to `prab_generation_log` with `response_status = 'error'`
|
||||
- Generation failures also shown in the admin dashboard widget
|
||||
- All log entries visible in AutoBlogger → Activity Log (filterable by level)
|
||||
- All log entries visible in PRAutoBlogger → Activity Log (filterable by level)
|
||||
|
||||
### What gets retried
|
||||
- LLM API calls: exponential backoff, max 3 retries, then fail loudly
|
||||
@@ -82,15 +82,15 @@ The second argument to every log call is a short context tag identifying the ori
|
||||
## How To: Add a New LLM Provider
|
||||
|
||||
1. Create `includes/providers/class-{name}-provider.php`
|
||||
2. Implement `Autoblogger_LLM_Provider_Interface` (see `interface-llm-provider.php`)
|
||||
2. Implement `PRAutoBlogger_LLM_Provider_Interface` (see `interface-llm-provider.php`)
|
||||
3. Required methods: `send_chat_completion()`, `get_available_models()`, `estimate_cost()`
|
||||
4. Add provider option to the settings page in `admin/class-admin-page.php`
|
||||
5. Register the provider in `class-autoblogger.php` loader
|
||||
5. Register the provider in `class-prautoblogger.php` loader
|
||||
6. Update ARCHITECTURE.md external API integrations table
|
||||
|
||||
### Interface contract:
|
||||
```php
|
||||
interface Autoblogger_LLM_Provider_Interface {
|
||||
interface PRAutoBlogger_LLM_Provider_Interface {
|
||||
public function send_chat_completion(array $messages, string $model, array $options = []): array;
|
||||
public function get_available_models(): array;
|
||||
public function estimate_cost(string $model, int $prompt_tokens, int $completion_tokens): float;
|
||||
@@ -103,17 +103,17 @@ interface Autoblogger_LLM_Provider_Interface {
|
||||
## How To: Add a New Source Provider (Social Platform)
|
||||
|
||||
1. Create `includes/providers/class-{platform}-provider.php`
|
||||
2. Implement `Autoblogger_Source_Provider_Interface` (see `interface-source-provider.php`)
|
||||
2. Implement `PRAutoBlogger_Source_Provider_Interface` (see `interface-source-provider.php`)
|
||||
3. Required methods: `collect_data()`, `get_source_type()`, `validate_credentials()`
|
||||
4. Add API credentials fields to the settings page in `admin/class-admin-page.php`
|
||||
5. Register the provider in `class-autoblogger.php` loader
|
||||
5. Register the provider in `class-prautoblogger.php` loader
|
||||
6. Add the source type string to the `ab_source_data.source_type` allowed values
|
||||
7. Update ARCHITECTURE.md file tree and external API integrations table
|
||||
|
||||
### Interface contract:
|
||||
```php
|
||||
interface Autoblogger_Source_Provider_Interface {
|
||||
public function collect_data(array $config): array; // Returns Autoblogger_Source_Data[]
|
||||
interface PRAutoBlogger_Source_Provider_Interface {
|
||||
public function collect_data(array $config): array; // Returns PRAutoBlogger_Source_Data[]
|
||||
public function get_source_type(): string; // e.g., 'reddit', 'tiktok'
|
||||
public function validate_credentials(): bool;
|
||||
public function get_rate_limit_status(): array;
|
||||
@@ -128,17 +128,17 @@ interface Autoblogger_Source_Provider_Interface {
|
||||
2. Format: add an entry to the appropriate section array:
|
||||
```php
|
||||
[
|
||||
'id' => 'autoblogger_new_setting',
|
||||
'label' => __('Setting Label', 'autoblogger'),
|
||||
'id' => 'prautoblogger_new_setting',
|
||||
'label' => __('Setting Label', 'prautoblogger'),
|
||||
'type' => 'text', // text, number, select, textarea, checkbox, password
|
||||
'default' => '',
|
||||
'description' => __('Help text shown below the field.', 'autoblogger'),
|
||||
'description' => __('Help text shown below the field.', 'prautoblogger'),
|
||||
'sanitize' => 'sanitize_text_field', // sanitization callback
|
||||
]
|
||||
```
|
||||
3. The settings page renderer handles registration, rendering, and sanitization automatically from this array.
|
||||
4. Access the setting anywhere with: `get_option('autoblogger_new_setting', $default)`
|
||||
5. For encrypted settings (API keys): use `Autoblogger_Encryption::encrypt()` / `decrypt()`
|
||||
4. Access the setting anywhere with: `get_option('prautoblogger_new_setting', $default)`
|
||||
5. For encrypted settings (API keys): use `PRAutoBlogger_Encryption::encrypt()` / `decrypt()`
|
||||
|
||||
---
|
||||
|
||||
@@ -146,7 +146,7 @@ interface Autoblogger_Source_Provider_Interface {
|
||||
|
||||
The writing pipeline is an ordered array of stages. Each stage is a class method on `Content_Generator`.
|
||||
|
||||
1. Add the stage method to `class-content-generator.php`: `protected function stage_{name}(Autoblogger_Content_Request $request): string`
|
||||
1. Add the stage method to `class-content-generator.php`: `protected function stage_{name}(PRAutoBlogger_Content_Request $request): string`
|
||||
2. Register the stage in the `get_pipeline_stages()` method for the appropriate mode (single_pass or multi_step)
|
||||
3. Each stage receives the accumulated context and returns its output
|
||||
4. Each stage logs its cost via `Cost_Tracker::log_api_call()`
|
||||
@@ -154,6 +154,45 @@ The writing pipeline is an ordered array of stages. Each stage is a class method
|
||||
|
||||
---
|
||||
|
||||
## How To: Add a New Frontend Component
|
||||
|
||||
Frontend components use `wp.element` (WordPress-bundled React) — no JSX, no build step.
|
||||
|
||||
1. Create the PHP class in `includes/frontend/class-{name}.php`:
|
||||
- Register a shortcode via `add_shortcode()` in an `on_register_shortcode()` method
|
||||
- Register a REST endpoint if the component needs async data (in `on_register_rest_route()`)
|
||||
- Enqueue JS (`wp-element` dependency) and CSS via `wp_enqueue_script/style()`
|
||||
- Pass config to JS via `wp_localize_script()`
|
||||
|
||||
2. Create JS in `assets/js/{name}.js`:
|
||||
- Use `wp.element.createElement` (aliased as `el`) for rendering
|
||||
- Import hooks: `var useState = wp.element.useState;`
|
||||
- Read config from `window.{localizedObjectName}`
|
||||
- Handle all states: loading, loaded, empty, error
|
||||
- Mount into the shortcode's `<div id="...">` mount point
|
||||
|
||||
3. Create CSS in `assets/css/{name}.css`:
|
||||
- Use Peptide Starter theme CSS custom properties with fallbacks: `var(--color-text-primary, #f9fafb)`
|
||||
- Prefix all classes with `prab-` to avoid collisions
|
||||
- Include responsive breakpoints, focus-visible states, and loading skeletons
|
||||
|
||||
4. Wire hooks in `class-prautoblogger.php`:
|
||||
- Instantiate the PHP class in `register_frontend_hooks()`
|
||||
- Register shortcode on `init`, REST routes on `rest_api_init`
|
||||
|
||||
5. Add `'frontend/'` to the autoloader's `$directories` array (already done for the first component)
|
||||
|
||||
6. Update ARCHITECTURE.md file tree and add a data flow section
|
||||
|
||||
### REST endpoint conventions:
|
||||
- Namespace: `prautoblogger/v1`
|
||||
- Permission callback: use a filter hook for extensibility (e.g., `prautoblogger_rest_{name}_public`)
|
||||
- Sanitize all params with `sanitize_callback` in arg registration
|
||||
- Return only the fields the frontend needs — no unnecessary data
|
||||
- Set `Cache-Control` header for cacheable responses
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Rules
|
||||
|
||||
1. **`declare(strict_types=1);`** in every PHP file
|
||||
@@ -162,7 +201,7 @@ The writing pipeline is an ordered array of stages. Each stage is a class method
|
||||
4. **Every class has a 3-question preamble docblock:** What / Who triggers / Dependencies
|
||||
5. **Every public method has a docblock** with `@param`, `@return`, and side effects
|
||||
6. **`@see` references** at the top of files that participate in multi-class flows
|
||||
7. **All strings translatable** via `__()` / `_e()` with `autoblogger` text domain
|
||||
7. **All strings translatable** via `__()` / `_e()` with `prautoblogger` text domain
|
||||
8. **No magic methods** without explicit justification documented in code
|
||||
9. **No `echo` of raw data** — always escape with `esc_html()`, `esc_attr()`, etc.
|
||||
10. **Nonce on every form/AJAX** — `wp_nonce_field()` / `check_ajax_referer()`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* AutoBlogger admin styles — modern SaaS-inspired UI.
|
||||
* PRAutoBlogger admin styles — modern SaaS-inspired UI.
|
||||
*
|
||||
* Design language: clean cards, subtle shadows, accent color #4f46e5 (indigo),
|
||||
* neutral grays from WordPress admin palette. Follows wp-admin conventions
|
||||
@@ -72,13 +72,13 @@
|
||||
.ab-text-danger { color: var(--ab-danger); }
|
||||
|
||||
/* ── Status message ──────────────────────────────────────────────────── */
|
||||
#autoblogger-status-message {
|
||||
#prautoblogger-status-message {
|
||||
margin: 16px 0; padding: 12px 16px; border-radius: var(--ab-radius);
|
||||
border-left: 4px solid var(--ab-success); background: var(--ab-card-bg);
|
||||
font-size: 13px; box-shadow: var(--ab-shadow);
|
||||
}
|
||||
#autoblogger-status-message.error { border-left-color: var(--ab-danger); }
|
||||
#autoblogger-status-message.hidden { display: none; }
|
||||
#prautoblogger-status-message.error { border-left-color: var(--ab-danger); }
|
||||
#prautoblogger-status-message.hidden { display: none; }
|
||||
|
||||
/* ── Layout: sidebar tabs + content ──────────────────────────────────── */
|
||||
.ab-layout {
|
||||
@@ -176,48 +176,48 @@ input[type="time"].ab-input { max-width: 160px; }
|
||||
}
|
||||
|
||||
/* ── Metrics page cards (preserved from before) ──────────────────────── */
|
||||
.autoblogger-cards { display: flex; gap: 16px; margin: 20px 0; flex-wrap: wrap; }
|
||||
.autoblogger-card { background: var(--ab-card-bg); border: 1px solid var(--ab-border); border-radius: var(--ab-radius); padding: 20px; min-width: 200px; flex: 1; }
|
||||
.autoblogger-card h3 { margin: 0 0 8px; font-size: 13px; color: var(--ab-text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.autoblogger-card-value { font-size: 28px; font-weight: 600; color: var(--ab-text); line-height: 1.2; }
|
||||
.autoblogger-card-value-small { font-size: 18px; }
|
||||
.autoblogger-card-sub { margin-top: 4px; font-size: 13px; color: var(--ab-text-muted); }
|
||||
.autoblogger-card-warning { border-left: 4px solid var(--ab-warning); }
|
||||
.autoblogger-card-danger { border-left: 4px solid var(--ab-danger); }
|
||||
.autoblogger-budget-bar { margin-top: 12px; height: 8px; background: var(--ab-bg); border-radius: 4px; overflow: hidden; }
|
||||
.autoblogger-budget-fill { height: 100%; background: var(--ab-primary); border-radius: 4px; transition: width 0.3s ease; }
|
||||
.autoblogger-card-warning .autoblogger-budget-fill { background: var(--ab-warning); }
|
||||
.autoblogger-card-danger .autoblogger-budget-fill { background: var(--ab-danger); }
|
||||
.prautoblogger-cards { display: flex; gap: 16px; margin: 20px 0; flex-wrap: wrap; }
|
||||
.prautoblogger-card { background: var(--ab-card-bg); border: 1px solid var(--ab-border); border-radius: var(--ab-radius); padding: 20px; min-width: 200px; flex: 1; }
|
||||
.prautoblogger-card h3 { margin: 0 0 8px; font-size: 13px; color: var(--ab-text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.prautoblogger-card-value { font-size: 28px; font-weight: 600; color: var(--ab-text); line-height: 1.2; }
|
||||
.prautoblogger-card-value-small { font-size: 18px; }
|
||||
.prautoblogger-card-sub { margin-top: 4px; font-size: 13px; color: var(--ab-text-muted); }
|
||||
.prautoblogger-card-warning { border-left: 4px solid var(--ab-warning); }
|
||||
.prautoblogger-card-danger { border-left: 4px solid var(--ab-danger); }
|
||||
.prautoblogger-budget-bar { margin-top: 12px; height: 8px; background: var(--ab-bg); border-radius: 4px; overflow: hidden; }
|
||||
.prautoblogger-budget-fill { height: 100%; background: var(--ab-primary); border-radius: 4px; transition: width 0.3s ease; }
|
||||
.prautoblogger-card-warning .prautoblogger-budget-fill { background: var(--ab-warning); }
|
||||
.prautoblogger-card-danger .prautoblogger-budget-fill { background: var(--ab-danger); }
|
||||
|
||||
/* ── Metabox / verdict (shared) ──────────────────────────────────────── */
|
||||
.autoblogger-metabox p { margin: 6px 0; font-size: 12px; }
|
||||
.autoblogger-verdict-approved { color: var(--ab-success); font-weight: 600; }
|
||||
.autoblogger-verdict-revised { color: var(--ab-warning); font-weight: 600; }
|
||||
.autoblogger-verdict-rejected { color: var(--ab-danger); font-weight: 600; }
|
||||
.autoblogger-verdict-pending { color: var(--ab-text-muted); font-weight: 600; }
|
||||
.prautoblogger-metabox p { margin: 6px 0; font-size: 12px; }
|
||||
.prautoblogger-verdict-approved { color: var(--ab-success); font-weight: 600; }
|
||||
.prautoblogger-verdict-revised { color: var(--ab-warning); font-weight: 600; }
|
||||
.prautoblogger-verdict-rejected { color: var(--ab-danger); font-weight: 600; }
|
||||
.prautoblogger-verdict-pending { color: var(--ab-text-muted); font-weight: 600; }
|
||||
|
||||
/* ── Metrics tables ──────────────────────────────────────────────────── */
|
||||
.autoblogger-metrics .widefat { margin: 10px 0 30px; }
|
||||
.prautoblogger-metrics .widefat { margin: 10px 0 30px; }
|
||||
|
||||
/* ── Review queue ────────────────────────────────────────────────────── */
|
||||
.autoblogger-review-queue .autoblogger-queue-toolbar {
|
||||
.prautoblogger-review-queue .prautoblogger-queue-toolbar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin: 15px 0; padding: 10px 12px; background: var(--ab-card-bg);
|
||||
border: 1px solid var(--ab-border); border-radius: var(--ab-radius);
|
||||
}
|
||||
.autoblogger-review-queue .autoblogger-queue-count { margin-left: auto; color: var(--ab-text-muted); font-size: 13px; }
|
||||
.autoblogger-review-queue .autoblogger-queue-table .check-column { width: 30px; padding: 10px 0 10px 10px; }
|
||||
.autoblogger-review-queue .autoblogger-queue-actions { white-space: nowrap; }
|
||||
.autoblogger-review-queue .autoblogger-queue-actions .button { margin-right: 4px; }
|
||||
.autoblogger-review-queue .autoblogger-editor-notes { margin-top: 4px; font-size: 12px; color: var(--ab-text-muted); font-style: italic; }
|
||||
.autoblogger-review-queue .autoblogger-empty-queue { margin: 40px 0; text-align: center; color: var(--ab-text-muted); font-size: 14px; }
|
||||
.autoblogger-review-queue .autoblogger-pagination { margin: 20px 0; text-align: center; }
|
||||
.autoblogger-review-queue .autoblogger-pagination .page-numbers { display: inline-block; padding: 4px 10px; margin: 0 2px; text-decoration: none; }
|
||||
.autoblogger-review-queue .autoblogger-pagination .page-numbers.current { background: var(--ab-primary); color: #fff; border-radius: 3px; }
|
||||
.prautoblogger-review-queue .prautoblogger-queue-count { margin-left: auto; color: var(--ab-text-muted); font-size: 13px; }
|
||||
.prautoblogger-review-queue .prautoblogger-queue-table .check-column { width: 30px; padding: 10px 0 10px 10px; }
|
||||
.prautoblogger-review-queue .prautoblogger-queue-actions { white-space: nowrap; }
|
||||
.prautoblogger-review-queue .prautoblogger-queue-actions .button { margin-right: 4px; }
|
||||
.prautoblogger-review-queue .prautoblogger-editor-notes { margin-top: 4px; font-size: 12px; color: var(--ab-text-muted); font-style: italic; }
|
||||
.prautoblogger-review-queue .prautoblogger-empty-queue { margin: 40px 0; text-align: center; color: var(--ab-text-muted); font-size: 14px; }
|
||||
.prautoblogger-review-queue .prautoblogger-pagination { margin: 20px 0; text-align: center; }
|
||||
.prautoblogger-review-queue .prautoblogger-pagination .page-numbers { display: inline-block; padding: 4px 10px; margin: 0 2px; text-decoration: none; }
|
||||
.prautoblogger-review-queue .prautoblogger-pagination .page-numbers.current { background: var(--ab-primary); color: #fff; border-radius: 3px; }
|
||||
|
||||
#autoblogger-queue-status { margin: 10px 0; padding: 10px 15px; border-left: 4px solid var(--ab-success); background: var(--ab-card-bg); }
|
||||
#autoblogger-queue-status.error { border-left-color: var(--ab-danger); }
|
||||
#autoblogger-queue-status.hidden { display: none; }
|
||||
#prautoblogger-queue-status { margin: 10px 0; padding: 10px 15px; border-left: 4px solid var(--ab-success); background: var(--ab-card-bg); }
|
||||
#prautoblogger-queue-status.error { border-left-color: var(--ab-danger); }
|
||||
#prautoblogger-queue-status.hidden { display: none; }
|
||||
|
||||
/* ── Log viewer ──────────────────────────────────────────────────────── */
|
||||
.ab-log-filters { display: flex; align-items: center; justify-content: space-between; margin: 16px 0; gap: 12px; flex-wrap: wrap; }
|
||||
@@ -251,9 +251,9 @@ input[type="time"].ab-input { max-width: 160px; }
|
||||
.ab-log-meta-toggle:hover { color: var(--ab-primary); }
|
||||
.ab-log-meta { display: none; margin: 8px 0 0; padding: 10px; background: var(--ab-bg); border-radius: 4px; font-size: 12px; overflow-x: auto; max-height: 200px; }
|
||||
|
||||
#autoblogger-log-status { margin: 10px 0; padding: 10px 15px; border-left: 4px solid var(--ab-success); background: var(--ab-card-bg); border-radius: 4px; }
|
||||
#autoblogger-log-status.error { border-left-color: var(--ab-danger); }
|
||||
#autoblogger-log-status.hidden { display: none; }
|
||||
#prautoblogger-log-status { margin: 10px 0; padding: 10px 15px; border-left: 4px solid var(--ab-success); background: var(--ab-card-bg); border-radius: 4px; }
|
||||
#prautoblogger-log-status.error { border-left-color: var(--ab-danger); }
|
||||
#prautoblogger-log-status.hidden { display: none; }
|
||||
|
||||
.ab-log-pagination { margin: 20px 0; text-align: center; }
|
||||
.ab-log-pagination .page-numbers { display: inline-block; padding: 4px 10px; margin: 0 2px; text-decoration: none; }
|
||||
@@ -268,6 +268,6 @@ input[type="time"].ab-input { max-width: 160px; }
|
||||
.ab-panel { padding: 20px 16px; }
|
||||
.ab-header { flex-direction: column; gap: 12px; align-items: flex-start; }
|
||||
.ab-stats-bar { flex-direction: column; }
|
||||
.autoblogger-cards { flex-direction: column; }
|
||||
.autoblogger-review-queue .autoblogger-queue-toolbar { flex-wrap: wrap; }
|
||||
.prautoblogger-cards { flex-direction: column; }
|
||||
.prautoblogger-review-queue .prautoblogger-queue-toolbar { flex-wrap: wrap; }
|
||||
}
|
||||
|
||||
349
assets/css/posts-widget.css
Normal file
349
assets/css/posts-widget.css
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* PRAutoBlogger Posts Widget — Frontend Styles
|
||||
*
|
||||
* Uses CSS custom properties from the Peptide Starter theme so the widget
|
||||
* automatically adapts to light/dark mode via data-theme on <html>.
|
||||
*
|
||||
* Theme variable mapping:
|
||||
* --color-text-primary → headings, titles
|
||||
* --color-text-secondary → body text, excerpts
|
||||
* --color-text-tertiary → muted text (dates, reading time)
|
||||
* --color-bg-primary → page background
|
||||
* --color-bg-secondary → card background
|
||||
* --color-bg-tertiary → card hover, placeholder backgrounds
|
||||
* --color-border-default → card borders, dividers
|
||||
* --color-border-subtle → lighter borders
|
||||
* --color-brand-primary → accent (links, hover states, focus rings)
|
||||
* --radius-md → card border radius (8px)
|
||||
* --transition-normal → hover/focus transitions (200ms)
|
||||
*
|
||||
* All classes prefixed with `prab-` to avoid collisions with theme/plugins.
|
||||
*
|
||||
* @see includes/frontend/class-posts-widget.php — PHP class that enqueues this.
|
||||
* @see assets/js/posts-widget.js — React component that uses these classes.
|
||||
* @see peptide-starter/style.css — Theme CSS with variable definitions.
|
||||
*/
|
||||
|
||||
/* ===== Widget Container ===== */
|
||||
.prab-posts-widget {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-4xl, 80px) var(--spacing-md, 16px);
|
||||
}
|
||||
|
||||
.prab-widget-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-2xl, 48px);
|
||||
}
|
||||
|
||||
.prab-widget-header h2 {
|
||||
font-size: var(--text-h2, 30px);
|
||||
font-weight: var(--text-h2-weight, 700);
|
||||
line-height: var(--text-h2-lh, 1.25);
|
||||
letter-spacing: var(--text-h2-ls, -0.015em);
|
||||
color: var(--color-text-primary, #f9fafb);
|
||||
margin-bottom: var(--spacing-sm, 8px);
|
||||
}
|
||||
|
||||
.prab-widget-header p {
|
||||
font-size: var(--text-body-lg, 15px);
|
||||
color: var(--color-text-secondary, #94a3b8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== Grid Layout ===== */
|
||||
.prab-posts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--spacing-lg, 24px);
|
||||
}
|
||||
|
||||
/* ===== Post Card ===== */
|
||||
.prab-post-card {
|
||||
background: var(--color-bg-secondary, #1f2937);
|
||||
border: 1px solid var(--color-border-default, #374151);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition-normal, 200ms ease-out),
|
||||
transform var(--transition-normal, 200ms ease-out),
|
||||
box-shadow var(--transition-normal, 200ms ease-out);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.prab-post-card:hover {
|
||||
border-color: var(--color-text-tertiary, #9ca3af);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.prab-post-card:focus-visible {
|
||||
outline: var(--focus-width, 2px) solid var(--focus-color, var(--color-brand-primary, #3b82f6));
|
||||
outline-offset: var(--focus-offset, 2px);
|
||||
box-shadow: var(--focus-shadow, 0 0 0 4px rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
/* Featured image area */
|
||||
.prab-post-image {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: var(--color-bg-tertiary, #374151);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prab-post-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform var(--transition-slow, 300ms ease-in-out);
|
||||
}
|
||||
|
||||
.prab-post-card:hover .prab-post-image img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* Placeholder for posts without featured images */
|
||||
.prab-post-image--placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.prab-post-image--placeholder svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
opacity: 0.15;
|
||||
color: var(--color-text-primary, #f9fafb);
|
||||
}
|
||||
|
||||
/* Card body */
|
||||
.prab-post-body {
|
||||
padding: var(--spacing-md, 16px) var(--spacing-lg, 24px) var(--spacing-lg, 24px);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Category badge + date row */
|
||||
.prab-post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
margin-bottom: var(--spacing-md, 16px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prab-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-size: var(--text-label, 11px);
|
||||
font-weight: var(--text-label-weight, 500);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Category badge colors — these are semantic and stay consistent in both modes */
|
||||
.prab-badge--guides { color: var(--color-status-success, #10b981); background: rgba(16, 185, 129, 0.12); }
|
||||
.prab-badge--solutions { color: #8b5cf6; background: rgba(139, 92, 246, 0.12); }
|
||||
.prab-badge--comparisons { color: var(--color-status-warning, #f59e0b); background: rgba(245, 158, 11, 0.12); }
|
||||
.prab-badge--articles { color: var(--color-brand-primary, #3b82f6); background: rgba(59, 130, 246, 0.12); }
|
||||
.prab-badge--default { color: var(--color-text-tertiary, #9ca3af); background: rgba(148, 163, 184, 0.12); }
|
||||
|
||||
.prab-post-date {
|
||||
font-size: var(--text-body-sm, 12px);
|
||||
color: var(--color-text-tertiary, #9ca3af);
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.prab-post-title {
|
||||
font-size: var(--text-h5, 17px);
|
||||
font-weight: var(--text-h5-weight, 700);
|
||||
line-height: var(--text-h5-lh, 1.4);
|
||||
color: var(--color-text-primary, #f9fafb);
|
||||
margin-bottom: var(--spacing-sm, 8px);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
transition: color var(--transition-normal, 200ms ease-out);
|
||||
}
|
||||
|
||||
.prab-post-card:hover .prab-post-title {
|
||||
color: var(--color-brand-primary, #3b82f6);
|
||||
}
|
||||
|
||||
/* Excerpt */
|
||||
.prab-post-excerpt {
|
||||
font-size: var(--text-body-md, 13px);
|
||||
color: var(--color-text-secondary, #d1d5db);
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Footer: reading time + arrow */
|
||||
.prab-post-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-md, 16px);
|
||||
padding-top: var(--spacing-md, 16px);
|
||||
border-top: 1px solid var(--color-border-default, #374151);
|
||||
}
|
||||
|
||||
.prab-reading-time {
|
||||
font-size: var(--text-body-sm, 12px);
|
||||
color: var(--color-text-tertiary, #9ca3af);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.prab-reading-time svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.prab-read-more {
|
||||
font-size: var(--text-body-md, 13px);
|
||||
font-weight: 500;
|
||||
color: var(--color-brand-primary, #3b82f6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: gap var(--transition-normal, 200ms ease-out);
|
||||
}
|
||||
|
||||
.prab-post-card:hover .prab-read-more {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.prab-read-more svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* ===== View All Link ===== */
|
||||
.prab-view-all {
|
||||
text-align: center;
|
||||
margin-top: var(--spacing-2xl, 48px);
|
||||
}
|
||||
|
||||
.prab-view-all a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding: var(--spacing-md, 16px) var(--spacing-xl, 32px);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-default, #374151);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
color: var(--color-text-primary, #f9fafb);
|
||||
font-size: var(--text-body-md, 13px);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: border-color var(--transition-normal, 200ms ease-out),
|
||||
background var(--transition-normal, 200ms ease-out);
|
||||
}
|
||||
|
||||
.prab-view-all a:hover {
|
||||
border-color: var(--color-brand-primary, #3b82f6);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.prab-view-all a:focus-visible {
|
||||
outline: var(--focus-width, 2px) solid var(--focus-color, var(--color-brand-primary, #3b82f6));
|
||||
outline-offset: var(--focus-offset, 2px);
|
||||
box-shadow: var(--focus-shadow, 0 0 0 4px rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
/* ===== Loading Skeleton ===== */
|
||||
.prab-skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-bg-secondary, #1f2937) 25%,
|
||||
var(--color-bg-tertiary, #374151) 50%,
|
||||
var(--color-bg-secondary, #1f2937) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: prab-shimmer 1.5s infinite;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
@keyframes prab-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.prab-skeleton-image { height: 180px; border-radius: var(--radius-md, 8px) var(--radius-md, 8px) 0 0; }
|
||||
.prab-skeleton-badge { width: 60px; height: 20px; }
|
||||
.prab-skeleton-title { height: 22px; width: 85%; margin-bottom: 8px; }
|
||||
.prab-skeleton-title-2 { height: 22px; width: 60%; margin-bottom: 12px; }
|
||||
.prab-skeleton-text { height: 16px; width: 100%; margin-bottom: 6px; }
|
||||
.prab-skeleton-text-2 { height: 16px; width: 75%; }
|
||||
|
||||
/* ===== Empty State ===== */
|
||||
.prab-empty-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-4xl, 80px) var(--spacing-md, 16px);
|
||||
background: var(--color-bg-secondary, #1f2937);
|
||||
border: 1px solid var(--color-border-default, #374151);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
}
|
||||
|
||||
.prab-empty-state svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: var(--color-text-tertiary, #9ca3af);
|
||||
margin-bottom: var(--spacing-md, 16px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.prab-empty-state h3 {
|
||||
font-size: var(--text-h4, 20px);
|
||||
font-weight: var(--text-h4-weight, 600);
|
||||
color: var(--color-text-primary, #f9fafb);
|
||||
margin-bottom: var(--spacing-sm, 8px);
|
||||
}
|
||||
|
||||
.prab-empty-state p {
|
||||
font-size: var(--text-body-md, 13px);
|
||||
color: var(--color-text-secondary, #d1d5db);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== Error State ===== */
|
||||
.prab-error-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2xl, 48px) var(--spacing-md, 16px);
|
||||
color: var(--color-text-secondary, #d1d5db);
|
||||
font-size: var(--text-body-md, 13px);
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 479px) {
|
||||
.prab-posts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.prab-widget-header h2 {
|
||||
font-size: var(--text-h3, 24px);
|
||||
}
|
||||
.prab-posts-widget {
|
||||
padding: var(--spacing-2xl, 48px) var(--spacing-md, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) and (max-width: 767px) {
|
||||
.prab-posts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* AutoBlogger admin JavaScript.
|
||||
* PRAutoBlogger admin JavaScript.
|
||||
*
|
||||
* Handles: tab switching, "Generate Now", "Test Connections", review queue actions.
|
||||
* Uses vanilla jQuery (WordPress admin already loads it).
|
||||
*
|
||||
* @see admin/class-admin-page.php — Localizes autobloggerAdmin object.
|
||||
* @see admin/class-admin-page.php — Localizes prautobloggerAdmin object.
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var config = window.autobloggerAdmin || {};
|
||||
var config = window.prautobloggerAdmin || {};
|
||||
|
||||
/*
|
||||
* ── Settings page: tab switching ───────────────────────────────────
|
||||
@@ -44,7 +44,7 @@
|
||||
* @param {string} type 'success' or 'error'.
|
||||
*/
|
||||
function showStatus(message, type) {
|
||||
var $el = $('#autoblogger-status-message');
|
||||
var $el = $('#prautoblogger-status-message');
|
||||
$el.removeClass('hidden error')
|
||||
.text(message);
|
||||
if (type === 'error') {
|
||||
@@ -55,7 +55,7 @@
|
||||
/**
|
||||
* Handle "Generate Now" button click.
|
||||
*/
|
||||
$(document).on('click', '#autoblogger-generate-now', function () {
|
||||
$(document).on('click', '#prautoblogger-generate-now', function () {
|
||||
var $btn = $(this);
|
||||
if ($btn.prop('disabled')) return;
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'autoblogger_generate_now',
|
||||
action: 'prautoblogger_generate_now',
|
||||
nonce: config.generateNonce
|
||||
},
|
||||
timeout: 300000, // 5 minutes — generation can be slow.
|
||||
@@ -100,7 +100,7 @@
|
||||
/**
|
||||
* Handle "Test Connections" button click.
|
||||
*/
|
||||
$(document).on('click', '#autoblogger-test-connection', function () {
|
||||
$(document).on('click', '#prautoblogger-test-connection', function () {
|
||||
var $btn = $(this);
|
||||
if ($btn.prop('disabled')) return;
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'autoblogger_test_connection',
|
||||
action: 'prautoblogger_test_connection',
|
||||
nonce: config.testNonce,
|
||||
service: 'all'
|
||||
},
|
||||
@@ -149,7 +149,7 @@
|
||||
* @param {string} type 'success' or 'error'.
|
||||
*/
|
||||
function showQueueStatus(message, type) {
|
||||
var $el = $('#autoblogger-queue-status');
|
||||
var $el = $('#prautoblogger-queue-status');
|
||||
$el.removeClass('hidden error').text(message);
|
||||
if (type === 'error') {
|
||||
$el.addClass('error');
|
||||
@@ -159,7 +159,7 @@
|
||||
/**
|
||||
* Handle inline "Approve" button click on review queue.
|
||||
*/
|
||||
$(document).on('click', '.autoblogger-approve-btn', function () {
|
||||
$(document).on('click', '.prautoblogger-approve-btn', function () {
|
||||
var $btn = $(this);
|
||||
var postId = $btn.data('post-id');
|
||||
if ($btn.prop('disabled')) return;
|
||||
@@ -170,7 +170,7 @@
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'autoblogger_approve_post',
|
||||
action: 'prautoblogger_approve_post',
|
||||
nonce: config.reviewNonce,
|
||||
post_id: postId
|
||||
},
|
||||
@@ -194,7 +194,7 @@
|
||||
/**
|
||||
* Handle inline "Reject" button click on review queue.
|
||||
*/
|
||||
$(document).on('click', '.autoblogger-reject-btn', function () {
|
||||
$(document).on('click', '.prautoblogger-reject-btn', function () {
|
||||
var $btn = $(this);
|
||||
var postId = $btn.data('post-id');
|
||||
if ($btn.prop('disabled')) return;
|
||||
@@ -207,7 +207,7 @@
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'autoblogger_reject_post',
|
||||
action: 'prautoblogger_reject_post',
|
||||
nonce: config.reviewNonce,
|
||||
post_id: postId
|
||||
},
|
||||
@@ -231,8 +231,8 @@
|
||||
/**
|
||||
* Handle "Select All" checkbox on review queue.
|
||||
*/
|
||||
$(document).on('change', '#autoblogger-select-all', function () {
|
||||
$('input[name="autoblogger_post_ids[]"]').prop('checked', this.checked);
|
||||
$(document).on('change', '#prautoblogger-select-all', function () {
|
||||
$('input[name="prautoblogger_post_ids[]"]').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -246,7 +246,7 @@
|
||||
});
|
||||
|
||||
/** Clear old logs button. */
|
||||
$(document).on('click', '#autoblogger-clear-logs', function () {
|
||||
$(document).on('click', '#prautoblogger-clear-logs', function () {
|
||||
var $btn = $(this);
|
||||
if ($btn.prop('disabled')) return;
|
||||
if (!confirm('Delete log entries older than 30 days?')) return;
|
||||
@@ -257,10 +257,10 @@
|
||||
$.ajax({
|
||||
url: config.ajaxUrl,
|
||||
method: 'POST',
|
||||
data: { action: 'autoblogger_clear_logs', nonce: nonce, days: 30 },
|
||||
data: { action: 'prautoblogger_clear_logs', nonce: nonce, days: 30 },
|
||||
timeout: 15000,
|
||||
success: function (response) {
|
||||
var $status = $('#autoblogger-log-status');
|
||||
var $status = $('#prautoblogger-log-status');
|
||||
if (response.success) {
|
||||
$status.removeClass('hidden error').text(response.data.message);
|
||||
setTimeout(function () { window.location.reload(); }, 1200);
|
||||
|
||||
350
assets/js/posts-widget.js
Normal file
350
assets/js/posts-widget.js
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* PRAutoBlogger Posts Widget — React Frontend Component
|
||||
*
|
||||
* Uses wp.element (WordPress-bundled React) — no build step needed.
|
||||
* Fetches posts from the plugin's REST API and renders a card grid.
|
||||
*
|
||||
* @see includes/frontend/class-posts-widget.php — Registers the shortcode and REST endpoint.
|
||||
* @see assets/css/posts-widget.css — Styles for all widget states.
|
||||
*/
|
||||
|
||||
/* global wp, prabPostsWidget */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var el = wp.element.createElement;
|
||||
var useState = wp.element.useState;
|
||||
var useEffect = wp.element.useEffect;
|
||||
|
||||
/**
|
||||
* SVG icon components — inline to avoid external dependencies.
|
||||
*/
|
||||
var ClockIcon = function () {
|
||||
return el('svg', {
|
||||
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2'
|
||||
},
|
||||
el('circle', { cx: '12', cy: '12', r: '10' }),
|
||||
el('polyline', { points: '12 6 12 12 16 14' })
|
||||
);
|
||||
};
|
||||
|
||||
var ArrowIcon = function () {
|
||||
return el('svg', {
|
||||
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2'
|
||||
},
|
||||
el('path', { d: 'M5 12h14M12 5l7 7-7 7' })
|
||||
);
|
||||
};
|
||||
|
||||
var BookIcon = function () {
|
||||
return el('svg', {
|
||||
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '1.5'
|
||||
},
|
||||
el('path', { d: 'M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25' })
|
||||
);
|
||||
};
|
||||
|
||||
var DocumentIcon = function () {
|
||||
return el('svg', {
|
||||
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '1.5'
|
||||
},
|
||||
el('path', { d: 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z' })
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Map category slug to a CSS modifier class.
|
||||
*
|
||||
* @param {string} categorySlug The category slug from the REST API.
|
||||
* @return {string} CSS class suffix.
|
||||
*/
|
||||
function getCategoryClass(categorySlug) {
|
||||
var slug = (categorySlug || '').toLowerCase();
|
||||
var map = {
|
||||
guides: 'guides',
|
||||
solutions: 'solutions',
|
||||
comparisons: 'comparisons',
|
||||
articles: 'articles'
|
||||
};
|
||||
return map[slug] || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date string to "Mon DD, YYYY".
|
||||
*
|
||||
* @param {string} dateStr ISO 8601 date string.
|
||||
* @return {string} Formatted date.
|
||||
*/
|
||||
function formatDate(dateStr) {
|
||||
var date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Recent';
|
||||
}
|
||||
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate reading time from word count.
|
||||
*
|
||||
* @param {number} wordCount Number of words.
|
||||
* @return {number} Minutes to read (minimum 1).
|
||||
*/
|
||||
function readingTime(wordCount) {
|
||||
return Math.max(1, Math.round((wordCount || 0) / 250));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML tags from a string.
|
||||
*
|
||||
* Uses a DOMParser to safely parse HTML without executing scripts.
|
||||
* Falls back to regex stripping if DOMParser is unavailable.
|
||||
*
|
||||
* @param {string} html HTML string.
|
||||
* @return {string} Plain text.
|
||||
*/
|
||||
function stripHtml(html) {
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
if (typeof DOMParser !== 'undefined') {
|
||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.textContent || '';
|
||||
}
|
||||
// Fallback: regex strip (less accurate but safe).
|
||||
return html.replace(/<[^>]*>/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Single post card component.
|
||||
*/
|
||||
var PostCard = function (props) {
|
||||
var post = props.post;
|
||||
var catClass = getCategoryClass(post.category_slug);
|
||||
var minutes = readingTime(post.word_count);
|
||||
var excerpt = stripHtml(post.excerpt);
|
||||
|
||||
// Choose placeholder icon based on category
|
||||
var PlaceholderIcon = post.category_slug === 'guides' ? BookIcon : DocumentIcon;
|
||||
|
||||
return el('a', {
|
||||
href: post.url || '#',
|
||||
className: 'prab-post-card',
|
||||
'aria-label': post.title
|
||||
},
|
||||
// Image area
|
||||
post.featured_image
|
||||
? el('div', { className: 'prab-post-image' },
|
||||
el('img', {
|
||||
src: post.featured_image,
|
||||
alt: post.title || '',
|
||||
loading: 'lazy',
|
||||
decoding: 'async'
|
||||
})
|
||||
)
|
||||
: el('div', { className: 'prab-post-image prab-post-image--placeholder' },
|
||||
el(PlaceholderIcon)
|
||||
),
|
||||
|
||||
// Body
|
||||
el('div', { className: 'prab-post-body' },
|
||||
// Meta row
|
||||
el('div', { className: 'prab-post-meta' },
|
||||
post.category_name
|
||||
? el('span', {
|
||||
className: 'prab-badge prab-badge--' + catClass
|
||||
}, post.category_name)
|
||||
: null,
|
||||
el('span', { className: 'prab-post-date' }, formatDate(post.date))
|
||||
),
|
||||
|
||||
// Title
|
||||
el('h3', { className: 'prab-post-title' }, post.title),
|
||||
|
||||
// Excerpt
|
||||
el('p', { className: 'prab-post-excerpt' }, excerpt),
|
||||
|
||||
// Footer
|
||||
el('div', { className: 'prab-post-footer' },
|
||||
el('span', { className: 'prab-reading-time' },
|
||||
el(ClockIcon),
|
||||
minutes + ' min read'
|
||||
),
|
||||
el('span', { className: 'prab-read-more' },
|
||||
'Read article',
|
||||
el(ArrowIcon)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Loading skeleton card.
|
||||
*/
|
||||
var SkeletonCard = function () {
|
||||
return el('div', { className: 'prab-post-card', style: { pointerEvents: 'none' } },
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-image' }),
|
||||
el('div', { className: 'prab-post-body' },
|
||||
el('div', { className: 'prab-post-meta' },
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-badge' })
|
||||
),
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-title' }),
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-title-2' }),
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-text', style: { marginTop: '8px' } }),
|
||||
el('div', { className: 'prab-skeleton prab-skeleton-text-2' })
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty state component.
|
||||
*/
|
||||
var EmptyState = function () {
|
||||
return el('div', { className: 'prab-empty-state' },
|
||||
el(DocumentIcon),
|
||||
el('h3', null, 'No articles yet'),
|
||||
el('p', null, 'New research articles will appear here once they\'re published. Check back soon!')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Error state component.
|
||||
*/
|
||||
var ErrorState = function (props) {
|
||||
return el('div', { className: 'prab-error-state' },
|
||||
el('p', null, props.message || 'Unable to load articles. Please try again later.')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main widget component.
|
||||
*
|
||||
* Fetches posts from the REST API on mount and renders the appropriate state.
|
||||
*/
|
||||
var PostsWidget = function (props) {
|
||||
var config = props.config || {};
|
||||
var count = config.count || 6;
|
||||
var title = config.title || 'Latest Research & Insights';
|
||||
var subtitle = config.subtitle || 'Evidence-based articles on peptides, protocols, and emerging research';
|
||||
var category = config.category || '';
|
||||
var archiveUrl = config.archiveUrl || '';
|
||||
|
||||
var stateArr = useState('loading');
|
||||
var state = stateArr[0];
|
||||
var setState = stateArr[1];
|
||||
|
||||
var postsArr = useState([]);
|
||||
var posts = postsArr[0];
|
||||
var setPosts = postsArr[1];
|
||||
|
||||
var errorArr = useState('');
|
||||
var error = errorArr[0];
|
||||
var setError = errorArr[1];
|
||||
|
||||
useEffect(function () {
|
||||
if (!config.restUrl) {
|
||||
setError('Widget misconfigured: REST URL not available.');
|
||||
setState('error');
|
||||
return;
|
||||
}
|
||||
|
||||
var url = config.restUrl + '?per_page=' + count;
|
||||
if (category) {
|
||||
url += '&category=' + encodeURIComponent(category);
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
setPosts(data);
|
||||
setState(data.length > 0 ? 'loaded' : 'empty');
|
||||
})
|
||||
.catch(function (err) {
|
||||
setError(err.message);
|
||||
setState('error');
|
||||
});
|
||||
}, []); // Fetch only on mount; results persist for page lifetime.
|
||||
|
||||
// Build children array
|
||||
var children = [];
|
||||
|
||||
// Header
|
||||
children.push(
|
||||
el('div', { className: 'prab-widget-header', key: 'header' },
|
||||
el('h2', null, title),
|
||||
el('p', null, subtitle)
|
||||
)
|
||||
);
|
||||
|
||||
// Content based on state
|
||||
if (state === 'loading') {
|
||||
var skeletons = [];
|
||||
for (var i = 0; i < Math.min(count, 3); i++) {
|
||||
skeletons.push(el(SkeletonCard, { key: 'skeleton-' + i }));
|
||||
}
|
||||
children.push(el('div', { className: 'prab-posts-grid', key: 'grid' }, skeletons));
|
||||
|
||||
} else if (state === 'empty') {
|
||||
children.push(el(EmptyState, { key: 'empty' }));
|
||||
|
||||
} else if (state === 'error') {
|
||||
children.push(el(ErrorState, { key: 'error', message: error }));
|
||||
|
||||
} else {
|
||||
// Loaded
|
||||
var cards = posts.map(function (post) {
|
||||
return el(PostCard, { key: post.id, post: post });
|
||||
});
|
||||
children.push(el('div', { className: 'prab-posts-grid', key: 'grid' }, cards));
|
||||
|
||||
// "View all" link
|
||||
if (archiveUrl) {
|
||||
children.push(
|
||||
el('div', { className: 'prab-view-all', key: 'viewall' },
|
||||
el('a', { href: archiveUrl },
|
||||
'View all articles',
|
||||
el(ArrowIcon)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return el('div', { className: 'prab-posts-widget' }, children);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mount the widget into the DOM.
|
||||
*
|
||||
* Reads configuration from the global `prabPostsWidget` object
|
||||
* set by wp_localize_script in the PHP class.
|
||||
*/
|
||||
function mount() {
|
||||
var root = document.getElementById('prab-posts-root');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
var config = window.prabPostsWidget || {};
|
||||
wp.element.render(
|
||||
el(PostsWidget, { config: config }),
|
||||
root
|
||||
);
|
||||
}
|
||||
|
||||
// Mount when DOM is ready.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mount);
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "peptiderepo/autoblogger",
|
||||
"name": "peptiderepo/prautoblogger",
|
||||
"description": "WordPress plugin that auto-researches and publishes blog content using AI.",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-2.0-or-later",
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Displays admin notices for onboarding, errors, and budget warnings.
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() on `admin_notices`.
|
||||
* Dependencies: Autoblogger_Cost_Tracker (for budget warnings).
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_notices`.
|
||||
* Dependencies: PRAutoBlogger_Cost_Tracker (for budget warnings).
|
||||
*
|
||||
* @see class-autoblogger.php — Registers the hook.
|
||||
* @see class-prautoblogger.php — Registers the hook.
|
||||
*/
|
||||
class Autoblogger_Admin_Notices {
|
||||
class PRAutoBlogger_Admin_Notices {
|
||||
|
||||
/**
|
||||
* Display relevant admin notices.
|
||||
@@ -32,16 +32,16 @@ class Autoblogger_Admin_Notices {
|
||||
* @return void
|
||||
*/
|
||||
private function check_api_key_notice(): void {
|
||||
$api_key = get_option( 'autoblogger_openrouter_api_key', '' );
|
||||
$api_key = get_option( 'prautoblogger_openrouter_api_key', '' );
|
||||
if ( '' !== $api_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-warning is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
|
||||
esc_html__( 'AutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'autoblogger' ),
|
||||
esc_url( admin_url( 'admin.php?page=autoblogger-settings' ) ),
|
||||
esc_html__( 'Configure now', 'autoblogger' )
|
||||
esc_html__( 'PRAutoBlogger: OpenRouter API key is not configured. Content generation is disabled.', 'prautoblogger' ),
|
||||
esc_url( admin_url( 'admin.php?page=prautoblogger-settings' ) ),
|
||||
esc_html__( 'Configure now', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,20 +51,20 @@ class Autoblogger_Admin_Notices {
|
||||
* @return void
|
||||
*/
|
||||
private function check_budget_notice(): void {
|
||||
$cost_tracker = new Autoblogger_Cost_Tracker();
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
$utilization = $cost_tracker->get_budget_utilization();
|
||||
|
||||
if ( $utilization >= 100.0 ) {
|
||||
printf(
|
||||
'<div class="notice notice-error"><p>%s</p></div>',
|
||||
esc_html__( 'AutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'autoblogger' )
|
||||
esc_html__( 'PRAutoBlogger: Monthly API budget EXCEEDED. Content generation is paused until next month or budget increase.', 'prautoblogger' )
|
||||
);
|
||||
} elseif ( $utilization >= 80.0 ) {
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s</p></div>',
|
||||
esc_html( sprintf(
|
||||
/* translators: %s: budget utilization percentage */
|
||||
__( 'AutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'autoblogger' ),
|
||||
__( 'PRAutoBlogger: Monthly API budget is at %.0f%%. Consider increasing the budget or reducing daily article count.', 'prautoblogger' ),
|
||||
$utilization
|
||||
) )
|
||||
);
|
||||
@@ -77,20 +77,20 @@ class Autoblogger_Admin_Notices {
|
||||
* @return void
|
||||
*/
|
||||
private function check_subreddit_notice(): void {
|
||||
$subreddits = json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true );
|
||||
$subreddits = json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true );
|
||||
if ( ! empty( $subreddits ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show on our settings page.
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || 'toplevel_page_autoblogger-settings' !== $screen->id ) {
|
||||
if ( ! $screen || 'toplevel_page_prautoblogger-settings' !== $screen->id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
|
||||
esc_html__( 'AutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'autoblogger' )
|
||||
esc_html__( 'PRAutoBlogger: No subreddits configured. Add target subreddits in Source Configuration to start collecting data.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Registers and renders the main AutoBlogger settings page in wp-admin.
|
||||
* Registers and renders the main PRAutoBlogger settings page in wp-admin.
|
||||
*
|
||||
* Uses the WordPress Settings API for option registration, sanitization, and rendering.
|
||||
* Settings are defined as a declarative array — adding a new setting is one array entry.
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() registers menu and settings hooks.
|
||||
* Dependencies: Autoblogger_Encryption (for API key storage), Autoblogger_Settings_Fields.
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() registers menu and settings hooks.
|
||||
* Dependencies: PRAutoBlogger_Encryption (for API key storage), PRAutoBlogger_Settings_Fields.
|
||||
*
|
||||
* @see class-autoblogger.php — Registers hooks that call this class.
|
||||
* @see class-prautoblogger.php — Registers hooks that call this class.
|
||||
* @see class-settings-fields.php — Defines all settings fields and sections.
|
||||
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
|
||||
*/
|
||||
class Autoblogger_Admin_Page {
|
||||
class PRAutoBlogger_Admin_Page {
|
||||
|
||||
private const PAGE_SLUG = 'autoblogger-settings';
|
||||
private const OPTION_GROUP = 'autoblogger_settings_group';
|
||||
private const PAGE_SLUG = 'prautoblogger-settings';
|
||||
private const OPTION_GROUP = 'prautoblogger_settings_group';
|
||||
|
||||
/** Register the top-level AutoBlogger menu item. */
|
||||
/** Register the top-level PRAutoBlogger menu item. */
|
||||
public function on_register_menu(): void {
|
||||
add_menu_page(
|
||||
__( 'AutoBlogger', 'autoblogger' ),
|
||||
__( 'AutoBlogger', 'autoblogger' ),
|
||||
__( 'PRAutoBlogger', 'prautoblogger' ),
|
||||
__( 'PRAutoBlogger', 'prautoblogger' ),
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
[ $this, 'render_page' ],
|
||||
@@ -34,15 +34,15 @@ class Autoblogger_Admin_Page {
|
||||
|
||||
/** Register all settings with the WordPress Settings API. */
|
||||
public function on_register_settings(): void {
|
||||
$sections = Autoblogger_Settings_Fields::get_sections();
|
||||
$fields = Autoblogger_Settings_Fields::get_fields();
|
||||
$sections = PRAutoBlogger_Settings_Fields::get_sections();
|
||||
$fields = PRAutoBlogger_Settings_Fields::get_fields();
|
||||
|
||||
foreach ( $sections as $section_id => $section ) {
|
||||
add_settings_section( $section_id, $section['title'], '__return_empty_string', self::PAGE_SLUG );
|
||||
}
|
||||
|
||||
foreach ( $fields as $field ) {
|
||||
$section = $field['section'] ?? 'autoblogger_api';
|
||||
$section = $field['section'] ?? 'prautoblogger_api';
|
||||
register_setting( self::OPTION_GROUP, $field['id'], [
|
||||
'type' => $field['wp_type'] ?? 'string',
|
||||
'sanitize_callback' => [ $this, 'sanitize_field' ],
|
||||
@@ -52,30 +52,30 @@ class Autoblogger_Admin_Page {
|
||||
}
|
||||
}
|
||||
|
||||
/** Enqueue admin CSS and JS on all AutoBlogger admin pages. */
|
||||
/** Enqueue admin CSS and JS on all PRAutoBlogger admin pages. */
|
||||
public function on_enqueue_assets( string $hook_suffix ): void {
|
||||
$pages = [
|
||||
'toplevel_page_' . self::PAGE_SLUG,
|
||||
'autoblogger_page_autoblogger-metrics',
|
||||
'autoblogger_page_autoblogger-review-queue',
|
||||
'autoblogger_page_autoblogger-logs',
|
||||
'prautoblogger_page_prautoblogger-metrics',
|
||||
'prautoblogger_page_prautoblogger-review-queue',
|
||||
'prautoblogger_page_prautoblogger-logs',
|
||||
];
|
||||
if ( ! in_array( $hook_suffix, $pages, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], AUTOBLOGGER_VERSION );
|
||||
wp_enqueue_script( 'autoblogger-admin', AUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], AUTOBLOGGER_VERSION, true );
|
||||
wp_enqueue_style( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/admin.css', [], PRAUTOBLOGGER_VERSION );
|
||||
wp_enqueue_script( 'prautoblogger-admin', PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/admin.js', [ 'jquery' ], PRAUTOBLOGGER_VERSION, true );
|
||||
|
||||
wp_localize_script( 'autoblogger-admin', 'autobloggerAdmin', [
|
||||
wp_localize_script( 'prautoblogger-admin', 'prautobloggerAdmin', [
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'generateNonce' => wp_create_nonce( 'autoblogger_generate_now' ),
|
||||
'testNonce' => wp_create_nonce( 'autoblogger_test_connection' ),
|
||||
'reviewNonce' => wp_create_nonce( 'autoblogger_review_queue' ),
|
||||
'generatingText' => __( 'Generating...', 'autoblogger' ),
|
||||
'generateText' => __( 'Generate Now', 'autoblogger' ),
|
||||
'testingText' => __( 'Testing...', 'autoblogger' ),
|
||||
'testText' => __( 'Test Connections', 'autoblogger' ),
|
||||
'generateNonce' => wp_create_nonce( 'prautoblogger_generate_now' ),
|
||||
'testNonce' => wp_create_nonce( 'prautoblogger_test_connection' ),
|
||||
'reviewNonce' => wp_create_nonce( 'prautoblogger_review_queue' ),
|
||||
'generatingText' => __( 'Generating...', 'prautoblogger' ),
|
||||
'generateText' => __( 'Generate Now', 'prautoblogger' ),
|
||||
'testingText' => __( 'Testing...', 'prautoblogger' ),
|
||||
'testText' => __( 'Test Connections', 'prautoblogger' ),
|
||||
] );
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ class Autoblogger_Admin_Page {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
$sections = Autoblogger_Settings_Fields::get_sections();
|
||||
$fields = Autoblogger_Settings_Fields::get_fields();
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
|
||||
$sections = PRAutoBlogger_Settings_Fields::get_sections();
|
||||
$fields = PRAutoBlogger_Settings_Fields::get_fields();
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/settings-page.php';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +102,7 @@ class Autoblogger_Admin_Page {
|
||||
$value = get_option( $args['id'], $default );
|
||||
|
||||
if ( 'password' === $type && '' !== $value ) {
|
||||
$decrypted = Autoblogger_Encryption::decrypt( $value );
|
||||
$decrypted = PRAutoBlogger_Encryption::decrypt( $value );
|
||||
$value = '' !== $decrypted ? '••••••••' : '';
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class Autoblogger_Admin_Page {
|
||||
|
||||
case 'password':
|
||||
printf( '<input type="password" id="%s" name="%s" value="" class="ab-input" placeholder="%s" autocomplete="off" />',
|
||||
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'autoblogger' ) : '' );
|
||||
$id, $id, '' !== $value ? esc_attr__( 'Saved (enter new value to change)', 'prautoblogger' ) : '' );
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
@@ -152,11 +152,11 @@ class Autoblogger_Admin_Page {
|
||||
break;
|
||||
|
||||
case 'author_select':
|
||||
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
|
||||
wp_dropdown_users( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto (first admin) —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select' ] );
|
||||
break;
|
||||
|
||||
case 'category_select':
|
||||
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'autoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
|
||||
wp_dropdown_categories( [ 'name' => $id, 'id' => $id, 'selected' => absint( $value ), 'show_option_none' => __( '— Auto-assign by type —', 'prautoblogger' ), 'option_none_value' => '0', 'class' => 'ab-select', 'hide_empty' => false ] );
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -181,13 +181,13 @@ class Autoblogger_Admin_Page {
|
||||
$option_name = substr( $filter, strlen( 'sanitize_option_' ) );
|
||||
}
|
||||
|
||||
$encrypted = [ 'autoblogger_openrouter_api_key', 'autoblogger_reddit_client_secret', 'autoblogger_ga4_credentials_json' ];
|
||||
$encrypted = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_reddit_client_secret', 'prautoblogger_ga4_credentials_json' ];
|
||||
if ( in_array( $option_name, $encrypted, true ) ) {
|
||||
if ( '' === $value ) { return get_option( $option_name, '' ); }
|
||||
return Autoblogger_Encryption::encrypt( sanitize_text_field( $value ) );
|
||||
return PRAutoBlogger_Encryption::encrypt( sanitize_text_field( $value ) );
|
||||
}
|
||||
|
||||
$json_fields = [ 'autoblogger_target_subreddits', 'autoblogger_topic_exclusions', 'autoblogger_enabled_sources' ];
|
||||
$json_fields = [ 'prautoblogger_target_subreddits', 'prautoblogger_topic_exclusions', 'prautoblogger_enabled_sources' ];
|
||||
if ( in_array( $option_name, $json_fields, true ) ) {
|
||||
if ( is_array( $value ) ) { return wp_json_encode( array_values( array_map( 'sanitize_text_field', $value ) ) ); }
|
||||
if ( is_string( $value ) && '[' !== substr( trim( $value ), 0, 1 ) ) {
|
||||
@@ -197,7 +197,7 @@ class Autoblogger_Admin_Page {
|
||||
return sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
$numeric = [ 'autoblogger_daily_article_target', 'autoblogger_monthly_budget_usd', 'autoblogger_min_word_count', 'autoblogger_max_word_count', 'autoblogger_default_author', 'autoblogger_default_category' ];
|
||||
$numeric = [ 'prautoblogger_daily_article_target', 'prautoblogger_monthly_budget_usd', 'prautoblogger_min_word_count', 'prautoblogger_max_word_count', 'prautoblogger_default_author', 'prautoblogger_default_category' ];
|
||||
if ( in_array( $option_name, $numeric, true ) ) {
|
||||
return is_numeric( $value ) ? $value : 0;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* WordPress Dashboard widget showing AutoBlogger generation status at a glance.
|
||||
* WordPress Dashboard widget showing PRAutoBlogger generation status at a glance.
|
||||
*
|
||||
* Displays: last generation time, articles generated today, monthly cost,
|
||||
* and budget utilization.
|
||||
*
|
||||
* Triggered by: Could be registered via wp_dashboard_setup hook.
|
||||
* Dependencies: Autoblogger_Cost_Tracker.
|
||||
* Dependencies: PRAutoBlogger_Cost_Tracker.
|
||||
*
|
||||
* @see class-autoblogger.php — Can register this on wp_dashboard_setup.
|
||||
* @see class-prautoblogger.php — Can register this on wp_dashboard_setup.
|
||||
*/
|
||||
class Autoblogger_Dashboard_Widget {
|
||||
class PRAutoBlogger_Dashboard_Widget {
|
||||
|
||||
/**
|
||||
* Register the dashboard widget.
|
||||
@@ -25,8 +25,8 @@ class Autoblogger_Dashboard_Widget {
|
||||
}
|
||||
|
||||
wp_add_dashboard_widget(
|
||||
'autoblogger_status',
|
||||
__( 'AutoBlogger Status', 'autoblogger' ),
|
||||
'prautoblogger_status',
|
||||
__( 'PRAutoBlogger Status', 'prautoblogger' ),
|
||||
[ $this, 'render_widget' ]
|
||||
);
|
||||
}
|
||||
@@ -37,15 +37,15 @@ class Autoblogger_Dashboard_Widget {
|
||||
* @return void
|
||||
*/
|
||||
public function render_widget(): void {
|
||||
$cost_tracker = new Autoblogger_Cost_Tracker();
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
$monthly_spend = $cost_tracker->get_monthly_spend();
|
||||
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
|
||||
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
|
||||
$utilization = $cost_tracker->get_budget_utilization();
|
||||
|
||||
$next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
$next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
|
||||
|
||||
printf( '<p><strong>%s</strong> $%.2f / $%.2f (%.0f%%)</p>',
|
||||
esc_html__( 'Monthly Spend:', 'autoblogger' ),
|
||||
esc_html__( 'Monthly Spend:', 'prautoblogger' ),
|
||||
$monthly_spend,
|
||||
$budget,
|
||||
$utilization
|
||||
@@ -53,15 +53,15 @@ class Autoblogger_Dashboard_Widget {
|
||||
|
||||
if ( false !== $next_run ) {
|
||||
printf( '<p><strong>%s</strong> %s</p>',
|
||||
esc_html__( 'Next Generation:', 'autoblogger' ),
|
||||
esc_html__( 'Next Generation:', 'prautoblogger' ),
|
||||
esc_html( wp_date( 'M j, Y g:i A', $next_run ) )
|
||||
);
|
||||
}
|
||||
|
||||
printf(
|
||||
'<p><a href="%s" class="button button-secondary">%s</a></p>',
|
||||
esc_url( admin_url( 'admin.php?page=autoblogger-metrics' ) ),
|
||||
esc_html__( 'View Full Metrics', 'autoblogger' )
|
||||
esc_url( admin_url( 'admin.php?page=prautoblogger-metrics' ) ),
|
||||
esc_html__( 'View Full Metrics', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,29 +6,29 @@ declare(strict_types=1);
|
||||
*
|
||||
* Provides level filtering (error/warning/info/debug), text search,
|
||||
* pagination, and a one-click "Clear Old Logs" action. Reads from the
|
||||
* ab_event_log table via Autoblogger_Logger::query().
|
||||
* prab_event_log table via PRAutoBlogger_Logger::query().
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: Autoblogger_Logger.
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: PRAutoBlogger_Logger.
|
||||
*
|
||||
* @see core/class-logger.php — Writes log entries and provides query().
|
||||
* @see templates/admin/log-viewer.php — Renders the HTML.
|
||||
* @see class-autoblogger.php — Registers the AJAX clear action.
|
||||
* @see class-prautoblogger.php — Registers the AJAX clear action.
|
||||
*/
|
||||
class Autoblogger_Log_Viewer {
|
||||
class PRAutoBlogger_Log_Viewer {
|
||||
|
||||
/**
|
||||
* Register the log viewer submenu page under AutoBlogger.
|
||||
* Register the log viewer submenu page under PRAutoBlogger.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_menu(): void {
|
||||
add_submenu_page(
|
||||
'autoblogger-settings',
|
||||
__( 'Activity Log', 'autoblogger' ),
|
||||
__( 'Activity Log', 'autoblogger' ),
|
||||
'prautoblogger-settings',
|
||||
__( 'Activity Log', 'prautoblogger' ),
|
||||
__( 'Activity Log', 'prautoblogger' ),
|
||||
'manage_options',
|
||||
'autoblogger-logs',
|
||||
'prautoblogger-logs',
|
||||
[ $this, 'render_page' ]
|
||||
);
|
||||
}
|
||||
@@ -47,36 +47,36 @@ class Autoblogger_Log_Viewer {
|
||||
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
|
||||
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
|
||||
|
||||
$result = Autoblogger_Logger::query( $level, $search, $paged, 50 );
|
||||
$result = PRAutoBlogger_Logger::query( $level, $search, $paged, 50 );
|
||||
$rows = $result['rows'];
|
||||
$total = $result['total'];
|
||||
$total_pages = (int) ceil( $total / 50 );
|
||||
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/log-viewer.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: clear logs older than 30 days.
|
||||
*
|
||||
* Side effects: deletes rows from ab_event_log.
|
||||
* Side effects: deletes rows from prab_event_log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_ajax_clear_logs(): void {
|
||||
check_ajax_referer( 'autoblogger_clear_logs', 'nonce' );
|
||||
check_ajax_referer( 'prautoblogger_clear_logs', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
$days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 30;
|
||||
$deleted = Autoblogger_Logger::prune( $days );
|
||||
$deleted = PRAutoBlogger_Logger::prune( $days );
|
||||
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf(
|
||||
/* translators: %d: number of entries deleted */
|
||||
__( '%d log entries cleared.', 'autoblogger' ),
|
||||
__( '%d log entries cleared.', 'prautoblogger' ),
|
||||
$deleted
|
||||
),
|
||||
] );
|
||||
|
||||
@@ -7,26 +7,26 @@ declare(strict_types=1);
|
||||
* Shows: monthly spend, daily cost chart, spend by pipeline stage, budget
|
||||
* utilization, top/bottom performing posts, and recent generation logs.
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: Autoblogger_Cost_Tracker.
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: PRAutoBlogger_Cost_Tracker.
|
||||
*
|
||||
* @see class-autoblogger.php — Registers the hook.
|
||||
* @see class-prautoblogger.php — Registers the hook.
|
||||
* @see core/class-cost-tracker.php — Provides cost data.
|
||||
*/
|
||||
class Autoblogger_Metrics_Page {
|
||||
class PRAutoBlogger_Metrics_Page {
|
||||
|
||||
/**
|
||||
* Register the metrics submenu page under AutoBlogger.
|
||||
* Register the metrics submenu page under PRAutoBlogger.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_menu(): void {
|
||||
add_submenu_page(
|
||||
'autoblogger-settings',
|
||||
__( 'Metrics & Costs', 'autoblogger' ),
|
||||
__( 'Metrics & Costs', 'autoblogger' ),
|
||||
'prautoblogger-settings',
|
||||
__( 'Metrics & Costs', 'prautoblogger' ),
|
||||
__( 'Metrics & Costs', 'prautoblogger' ),
|
||||
'manage_options',
|
||||
'autoblogger-metrics',
|
||||
'prautoblogger-metrics',
|
||||
[ $this, 'render_page' ]
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,6 @@ class Autoblogger_Metrics_Page {
|
||||
return;
|
||||
}
|
||||
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metrics-page.php';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Adds a metabox to posts showing AutoBlogger generation metadata.
|
||||
* Adds a metabox to posts showing PRAutoBlogger generation metadata.
|
||||
*
|
||||
* Only appears on posts that were generated by AutoBlogger (checked via
|
||||
* _autoblogger_generated post meta).
|
||||
* Only appears on posts that were generated by PRAutoBlogger (checked via
|
||||
* _prautoblogger_generated post meta).
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() on `add_meta_boxes`.
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `add_meta_boxes`.
|
||||
* Dependencies: WordPress post meta API.
|
||||
*
|
||||
* @see class-autoblogger.php — Registers the hook.
|
||||
* @see class-prautoblogger.php — Registers the hook.
|
||||
* @see core/class-publisher.php — Writes the metadata this class displays.
|
||||
*/
|
||||
class Autoblogger_Post_Metabox {
|
||||
class PRAutoBlogger_Post_Metabox {
|
||||
|
||||
/**
|
||||
* Register the metabox for the post editor.
|
||||
@@ -27,13 +27,13 @@ class Autoblogger_Post_Metabox {
|
||||
}
|
||||
|
||||
global $post;
|
||||
if ( ! $post || '1' !== get_post_meta( $post->ID, '_autoblogger_generated', true ) ) {
|
||||
if ( ! $post || '1' !== get_post_meta( $post->ID, '_prautoblogger_generated', true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_meta_box(
|
||||
'autoblogger_generation_info',
|
||||
__( 'AutoBlogger Generation Info', 'autoblogger' ),
|
||||
'prautoblogger_generation_info',
|
||||
__( 'PRAutoBlogger Generation Info', 'prautoblogger' ),
|
||||
[ $this, 'render_metabox' ],
|
||||
'post',
|
||||
'side',
|
||||
@@ -49,6 +49,6 @@ class Autoblogger_Post_Metabox {
|
||||
* @return void
|
||||
*/
|
||||
public function render_metabox( \WP_Post $post ): void {
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/metabox-generation-info.php';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,27 @@ declare(strict_types=1);
|
||||
* Provides approve (publish), edit (opens WP editor), and reject (trash)
|
||||
* actions on each draft. Supports bulk approve/reject via checkboxes.
|
||||
*
|
||||
* Triggered by: Autoblogger::register_admin_hooks() on `admin_menu`.
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: WordPress WP_Query, post meta API.
|
||||
*
|
||||
* @see class-autoblogger.php — Registers the hook and AJAX handlers.
|
||||
* @see class-prautoblogger.php — Registers the hook and AJAX handlers.
|
||||
* @see core/class-publisher.php — Creates the drafts this page displays.
|
||||
* @see templates/admin/review-queue.php — Renders the HTML.
|
||||
*/
|
||||
class Autoblogger_Review_Queue {
|
||||
class PRAutoBlogger_Review_Queue {
|
||||
|
||||
/**
|
||||
* Register the review queue submenu page under AutoBlogger.
|
||||
* Register the review queue submenu page under PRAutoBlogger.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_menu(): void {
|
||||
$hook = add_submenu_page(
|
||||
'autoblogger-settings',
|
||||
__( 'Review Queue', 'autoblogger' ),
|
||||
__( 'Review Queue', 'autoblogger' ),
|
||||
'prautoblogger-settings',
|
||||
__( 'Review Queue', 'prautoblogger' ),
|
||||
__( 'Review Queue', 'prautoblogger' ),
|
||||
'manage_options',
|
||||
'autoblogger-review-queue',
|
||||
'prautoblogger-review-queue',
|
||||
[ $this, 'render_page' ]
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ class Autoblogger_Review_Queue {
|
||||
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
|
||||
$query = $this->get_pending_posts( $paged );
|
||||
|
||||
include AUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/review-queue.php';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +67,7 @@ class Autoblogger_Review_Queue {
|
||||
'paged' => $paged,
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => '_autoblogger_generated',
|
||||
'key' => '_prautoblogger_generated',
|
||||
'value' => '1',
|
||||
],
|
||||
],
|
||||
@@ -87,19 +87,19 @@ class Autoblogger_Review_Queue {
|
||||
* @return void
|
||||
*/
|
||||
public function on_handle_bulk_actions(): void {
|
||||
if ( ! isset( $_POST['autoblogger_bulk_action'] ) ) {
|
||||
if ( ! isset( $_POST['prautoblogger_bulk_action'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_admin_referer( 'autoblogger_review_queue_bulk', 'autoblogger_review_nonce' );
|
||||
check_admin_referer( 'prautoblogger_review_queue_bulk', 'prautoblogger_review_nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = sanitize_text_field( wp_unslash( $_POST['autoblogger_bulk_action'] ) );
|
||||
$post_ids = isset( $_POST['autoblogger_post_ids'] ) && is_array( $_POST['autoblogger_post_ids'] )
|
||||
? array_map( 'absint', $_POST['autoblogger_post_ids'] )
|
||||
$action = sanitize_text_field( wp_unslash( $_POST['prautoblogger_bulk_action'] ) );
|
||||
$post_ids = isset( $_POST['prautoblogger_post_ids'] ) && is_array( $_POST['prautoblogger_post_ids'] )
|
||||
? array_map( 'absint', $_POST['prautoblogger_post_ids'] )
|
||||
: [];
|
||||
|
||||
if ( empty( $post_ids ) ) {
|
||||
@@ -115,7 +115,7 @@ class Autoblogger_Review_Queue {
|
||||
if ( 'approve' === $action ) {
|
||||
$result = wp_update_post( [ 'ID' => $post_id, 'post_status' => 'publish' ], true );
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
|
||||
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
|
||||
$count++;
|
||||
}
|
||||
} elseif ( 'reject' === $action ) {
|
||||
@@ -128,9 +128,9 @@ class Autoblogger_Review_Queue {
|
||||
|
||||
$redirect = add_query_arg(
|
||||
[
|
||||
'page' => 'autoblogger-review-queue',
|
||||
'autoblogger_bulk_done' => $count,
|
||||
'autoblogger_bulk_type' => $action,
|
||||
'page' => 'prautoblogger-review-queue',
|
||||
'prautoblogger_bulk_done' => $count,
|
||||
'prautoblogger_bulk_type' => $action,
|
||||
],
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
@@ -147,16 +147,16 @@ class Autoblogger_Review_Queue {
|
||||
* @return void
|
||||
*/
|
||||
public function on_ajax_approve_post(): void {
|
||||
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
|
||||
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
|
||||
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,8 +167,8 @@ class Autoblogger_Review_Queue {
|
||||
return;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_autoblogger_approved_at', gmdate( 'c' ) );
|
||||
wp_send_json_success( [ 'message' => __( 'Post published.', 'autoblogger' ), 'post_id' => $post_id ] );
|
||||
update_post_meta( $post_id, '_prautoblogger_approved_at', gmdate( 'c' ) );
|
||||
wp_send_json_success( [ 'message' => __( 'Post published.', 'prautoblogger' ), 'post_id' => $post_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,25 +179,25 @@ class Autoblogger_Review_Queue {
|
||||
* @return void
|
||||
*/
|
||||
public function on_ajax_reject_post(): void {
|
||||
check_ajax_referer( 'autoblogger_review_queue', 'nonce' );
|
||||
check_ajax_referer( 'prautoblogger_review_queue', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'autoblogger' ) ] );
|
||||
wp_send_json_error( [ 'message' => __( 'Invalid post ID.', 'prautoblogger' ) ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$result = wp_trash_post( $post_id );
|
||||
if ( false === $result || null === $result ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'autoblogger' ) ] );
|
||||
wp_send_json_error( [ 'message' => __( 'Failed to reject post.', 'prautoblogger' ) ] );
|
||||
return;
|
||||
}
|
||||
|
||||
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'autoblogger' ), 'post_id' => $post_id ] );
|
||||
wp_send_json_success( [ 'message' => __( 'Post rejected.', 'prautoblogger' ), 'post_id' => $post_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Declarative settings fields and sections for the AutoBlogger admin page.
|
||||
* Declarative settings fields and sections for the PRAutoBlogger admin page.
|
||||
*
|
||||
* Centralizes all field and section definitions in one place, making it trivial
|
||||
* to add new settings (just add one array entry). Decoupled from page rendering logic.
|
||||
*
|
||||
* Triggered by: Autoblogger_Admin_Page::on_register_settings() calls static methods here.
|
||||
* Triggered by: PRAutoBlogger_Admin_Page::on_register_settings() calls static methods here.
|
||||
* Dependencies: None — contains only data definitions and static methods.
|
||||
*
|
||||
* @see admin/class-admin-page.php — Calls get_sections() and get_fields() to register settings.
|
||||
* @see CONVENTIONS.md — "How To: Add a New Admin Setting".
|
||||
*/
|
||||
class Autoblogger_Settings_Fields {
|
||||
class PRAutoBlogger_Settings_Fields {
|
||||
|
||||
/**
|
||||
* Get all settings sections. Each section maps to a tab in the admin UI.
|
||||
@@ -22,40 +22,40 @@ class Autoblogger_Settings_Fields {
|
||||
*/
|
||||
public static function get_sections(): array {
|
||||
return [
|
||||
'autoblogger_api' => [
|
||||
'title' => __( 'API Keys', 'autoblogger' ),
|
||||
'prautoblogger_api' => [
|
||||
'title' => __( 'API Keys', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-admin-network',
|
||||
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'autoblogger' ),
|
||||
'description' => __( 'Connect your external services. Keys are encrypted at rest.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_models' => [
|
||||
'title' => __( 'AI Models', 'autoblogger' ),
|
||||
'prautoblogger_models' => [
|
||||
'title' => __( 'AI Models', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-superhero-alt',
|
||||
'description' => __( 'Choose which models power each stage of the pipeline.', 'autoblogger' ),
|
||||
'description' => __( 'Choose which models power each stage of the pipeline.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_content' => [
|
||||
'title' => __( 'Content', 'autoblogger' ),
|
||||
'prautoblogger_content' => [
|
||||
'title' => __( 'Content', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-edit-large',
|
||||
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'autoblogger' ),
|
||||
'description' => __( 'Control tone, length, pipeline mode, and topic guardrails.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_sources' => [
|
||||
'title' => __( 'Sources', 'autoblogger' ),
|
||||
'prautoblogger_sources' => [
|
||||
'title' => __( 'Sources', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-rss',
|
||||
'description' => __( 'Configure where AutoBlogger finds trending topics.', 'autoblogger' ),
|
||||
'description' => __( 'Configure where PRAutoBlogger finds trending topics.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_schedule' => [
|
||||
'title' => __( 'Schedule & Budget', 'autoblogger' ),
|
||||
'prautoblogger_schedule' => [
|
||||
'title' => __( 'Schedule & Budget', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-calendar-alt',
|
||||
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'autoblogger' ),
|
||||
'description' => __( 'Set daily generation schedule, volume, and spending limits.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_publishing' => [
|
||||
'title' => __( 'Publishing', 'autoblogger' ),
|
||||
'prautoblogger_publishing' => [
|
||||
'title' => __( 'Publishing', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-megaphone',
|
||||
'description' => __( 'Control how generated content is published.', 'autoblogger' ),
|
||||
'description' => __( 'Control how generated content is published.', 'prautoblogger' ),
|
||||
],
|
||||
'autoblogger_analytics' => [
|
||||
'title' => __( 'Analytics', 'autoblogger' ),
|
||||
'prautoblogger_analytics' => [
|
||||
'title' => __( 'Analytics', 'prautoblogger' ),
|
||||
'icon' => 'dashicons-chart-area',
|
||||
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'autoblogger' ),
|
||||
'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'prautoblogger' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -69,222 +69,222 @@ class Autoblogger_Settings_Fields {
|
||||
return [
|
||||
// ── API Keys ────────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_openrouter_api_key',
|
||||
'label' => __( 'OpenRouter API Key', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_openrouter_api_key',
|
||||
'label' => __( 'OpenRouter API Key', 'prautoblogger' ),
|
||||
'type' => 'password',
|
||||
'section' => 'autoblogger_api',
|
||||
'description' => __( 'Get your key at openrouter.ai/keys', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_api',
|
||||
'description' => __( 'Get your key at openrouter.ai/keys', 'prautoblogger' ),
|
||||
'icon' => '🔑',
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_reddit_client_id',
|
||||
'label' => __( 'Reddit Client ID', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_reddit_client_id',
|
||||
'label' => __( 'Reddit Client ID', 'prautoblogger' ),
|
||||
'type' => 'text',
|
||||
'section' => 'autoblogger_api',
|
||||
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_api',
|
||||
'description' => __( 'Create a script app at reddit.com/prefs/apps', 'prautoblogger' ),
|
||||
'icon' => '🤖',
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_reddit_client_secret',
|
||||
'label' => __( 'Reddit Client Secret', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_reddit_client_secret',
|
||||
'label' => __( 'Reddit Client Secret', 'prautoblogger' ),
|
||||
'type' => 'password',
|
||||
'section' => 'autoblogger_api',
|
||||
'section' => 'prautoblogger_api',
|
||||
],
|
||||
|
||||
// ── AI Models ───────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_analysis_model',
|
||||
'label' => __( 'Analysis Model', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_analysis_model',
|
||||
'label' => __( 'Analysis Model', 'prautoblogger' ),
|
||||
'type' => 'text',
|
||||
'section' => 'autoblogger_models',
|
||||
'default' => AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
|
||||
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'autoblogger' ),
|
||||
'badge' => __( 'Low cost', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_models',
|
||||
'default' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
|
||||
'description' => __( 'Used for source analysis. Pick a cheap, fast model.', 'prautoblogger' ),
|
||||
'badge' => __( 'Low cost', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_writing_model',
|
||||
'label' => __( 'Writing Model', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_writing_model',
|
||||
'label' => __( 'Writing Model', 'prautoblogger' ),
|
||||
'type' => 'text',
|
||||
'section' => 'autoblogger_models',
|
||||
'default' => AUTOBLOGGER_DEFAULT_WRITING_MODEL,
|
||||
'description' => __( 'Used for article generation. Quality matters here.', 'autoblogger' ),
|
||||
'badge' => __( 'Quality', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_models',
|
||||
'default' => PRAUTOBLOGGER_DEFAULT_WRITING_MODEL,
|
||||
'description' => __( 'Used for article generation. Quality matters here.', 'prautoblogger' ),
|
||||
'badge' => __( 'Quality', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_editor_model',
|
||||
'label' => __( 'Editor Model', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_editor_model',
|
||||
'label' => __( 'Editor Model', 'prautoblogger' ),
|
||||
'type' => 'text',
|
||||
'section' => 'autoblogger_models',
|
||||
'default' => AUTOBLOGGER_DEFAULT_EDITOR_MODEL,
|
||||
'description' => __( 'Used for the chief editor review pass.', 'autoblogger' ),
|
||||
'badge' => __( 'Quality', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_models',
|
||||
'default' => PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL,
|
||||
'description' => __( 'Used for the chief editor review pass.', 'prautoblogger' ),
|
||||
'badge' => __( 'Quality', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Content ─────────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_niche_description',
|
||||
'label' => __( 'Niche Description', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_niche_description',
|
||||
'label' => __( 'Niche Description', 'prautoblogger' ),
|
||||
'type' => 'textarea',
|
||||
'section' => 'autoblogger_content',
|
||||
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_content',
|
||||
'description' => __( 'Describe your site\'s niche. Guides content analysis and generation.', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_tone',
|
||||
'label' => __( 'Content Tone', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_tone',
|
||||
'label' => __( 'Content Tone', 'prautoblogger' ),
|
||||
'type' => 'select',
|
||||
'section' => 'autoblogger_content',
|
||||
'section' => 'prautoblogger_content',
|
||||
'default' => 'informational',
|
||||
'options' => [
|
||||
'informational' => __( 'Informational', 'autoblogger' ),
|
||||
'conversational' => __( 'Conversational', 'autoblogger' ),
|
||||
'professional' => __( 'Professional', 'autoblogger' ),
|
||||
'casual' => __( 'Casual', 'autoblogger' ),
|
||||
'authoritative' => __( 'Authoritative', 'autoblogger' ),
|
||||
'informational' => __( 'Informational', 'prautoblogger' ),
|
||||
'conversational' => __( 'Conversational', 'prautoblogger' ),
|
||||
'professional' => __( 'Professional', 'prautoblogger' ),
|
||||
'casual' => __( 'Casual', 'prautoblogger' ),
|
||||
'authoritative' => __( 'Authoritative', 'prautoblogger' ),
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_writing_pipeline',
|
||||
'label' => __( 'Writing Pipeline', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_writing_pipeline',
|
||||
'label' => __( 'Writing Pipeline', 'prautoblogger' ),
|
||||
'type' => 'select',
|
||||
'section' => 'autoblogger_content',
|
||||
'section' => 'prautoblogger_content',
|
||||
'default' => 'multi_step',
|
||||
'options' => [
|
||||
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'autoblogger' ),
|
||||
'single_pass' => __( 'Single-pass (one LLM call)', 'autoblogger' ),
|
||||
'multi_step' => __( 'Multi-step (outline → draft → polish)', 'prautoblogger' ),
|
||||
'single_pass' => __( 'Single-pass (one LLM call)', 'prautoblogger' ),
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_min_word_count',
|
||||
'label' => __( 'Min Word Count', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_min_word_count',
|
||||
'label' => __( 'Min Word Count', 'prautoblogger' ),
|
||||
'type' => 'number',
|
||||
'section' => 'autoblogger_content',
|
||||
'section' => 'prautoblogger_content',
|
||||
'default' => 800,
|
||||
'min' => 200,
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_max_word_count',
|
||||
'label' => __( 'Max Word Count', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_max_word_count',
|
||||
'label' => __( 'Max Word Count', 'prautoblogger' ),
|
||||
'type' => 'number',
|
||||
'section' => 'autoblogger_content',
|
||||
'section' => 'prautoblogger_content',
|
||||
'default' => 2000,
|
||||
'min' => 500,
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_topic_exclusions',
|
||||
'label' => __( 'Topic Exclusions', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_topic_exclusions',
|
||||
'label' => __( 'Topic Exclusions', 'prautoblogger' ),
|
||||
'type' => 'textarea',
|
||||
'section' => 'autoblogger_content',
|
||||
'description' => __( 'Comma-separated topics to never write about.', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_content',
|
||||
'description' => __( 'Comma-separated topics to never write about.', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Sources ─────────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_target_subreddits',
|
||||
'label' => __( 'Target Subreddits', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_target_subreddits',
|
||||
'label' => __( 'Target Subreddits', 'prautoblogger' ),
|
||||
'type' => 'textarea',
|
||||
'section' => 'autoblogger_sources',
|
||||
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_sources',
|
||||
'description' => __( 'Comma-separated, without r/. E.g.: peptides, Nootropics, biohackers', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_enabled_sources',
|
||||
'label' => __( 'Enabled Sources', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_enabled_sources',
|
||||
'label' => __( 'Enabled Sources', 'prautoblogger' ),
|
||||
'type' => 'checkboxes',
|
||||
'section' => 'autoblogger_sources',
|
||||
'section' => 'prautoblogger_sources',
|
||||
'options' => [
|
||||
'reddit' => __( 'Reddit', 'autoblogger' ),
|
||||
'tiktok' => __( 'TikTok (coming soon)', 'autoblogger' ),
|
||||
'instagram' => __( 'Instagram (coming soon)', 'autoblogger' ),
|
||||
'youtube' => __( 'YouTube (coming soon)', 'autoblogger' ),
|
||||
'reddit' => __( 'Reddit', 'prautoblogger' ),
|
||||
'tiktok' => __( 'TikTok (coming soon)', 'prautoblogger' ),
|
||||
'instagram' => __( 'Instagram (coming soon)', 'prautoblogger' ),
|
||||
'youtube' => __( 'YouTube (coming soon)', 'prautoblogger' ),
|
||||
],
|
||||
'default' => '["reddit"]',
|
||||
'description' => __( 'Select which platforms to monitor for topics.', 'autoblogger' ),
|
||||
'description' => __( 'Select which platforms to monitor for topics.', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Schedule & Budget ───────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_daily_article_target',
|
||||
'label' => __( 'Articles per Day', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_daily_article_target',
|
||||
'label' => __( 'Articles per Day', 'prautoblogger' ),
|
||||
'type' => 'number',
|
||||
'section' => 'autoblogger_schedule',
|
||||
'section' => 'prautoblogger_schedule',
|
||||
'default' => 1,
|
||||
'min' => 1,
|
||||
'max' => 10,
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_schedule_time',
|
||||
'label' => __( 'Generation Time', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_schedule_time',
|
||||
'label' => __( 'Generation Time', 'prautoblogger' ),
|
||||
'type' => 'time',
|
||||
'section' => 'autoblogger_schedule',
|
||||
'section' => 'prautoblogger_schedule',
|
||||
'default' => '03:00',
|
||||
'description' => __( 'Daily generation runs at this time (server timezone).', 'autoblogger' ),
|
||||
'description' => __( 'Daily generation runs at this time (server timezone).', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_monthly_budget_usd',
|
||||
'label' => __( 'Monthly Budget (USD)', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_monthly_budget_usd',
|
||||
'label' => __( 'Monthly Budget (USD)', 'prautoblogger' ),
|
||||
'type' => 'number',
|
||||
'section' => 'autoblogger_schedule',
|
||||
'section' => 'prautoblogger_schedule',
|
||||
'default' => 50,
|
||||
'min' => 0,
|
||||
'step' => '0.01',
|
||||
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'autoblogger' ),
|
||||
'description' => __( 'Hard stop when reached. 0 = unlimited.', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Publishing ──────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_auto_publish',
|
||||
'label' => __( 'Auto-Publish', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_auto_publish',
|
||||
'label' => __( 'Auto-Publish', 'prautoblogger' ),
|
||||
'type' => 'toggle',
|
||||
'section' => 'autoblogger_publishing',
|
||||
'section' => 'prautoblogger_publishing',
|
||||
'default' => '0',
|
||||
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'autoblogger' ),
|
||||
'description' => __( 'Automatically publish editor-approved posts. When off, all posts go to the Review Queue as drafts.', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_default_author',
|
||||
'label' => __( 'Default Author', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_default_author',
|
||||
'label' => __( 'Default Author', 'prautoblogger' ),
|
||||
'type' => 'author_select',
|
||||
'section' => 'autoblogger_publishing',
|
||||
'section' => 'prautoblogger_publishing',
|
||||
'default' => 0,
|
||||
'description' => __( 'WordPress user assigned as author of generated posts.', 'autoblogger' ),
|
||||
'description' => __( 'WordPress user assigned as author of generated posts.', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_default_category',
|
||||
'label' => __( 'Default Category', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_default_category',
|
||||
'label' => __( 'Default Category', 'prautoblogger' ),
|
||||
'type' => 'category_select',
|
||||
'section' => 'autoblogger_publishing',
|
||||
'section' => 'prautoblogger_publishing',
|
||||
'default' => 0,
|
||||
'description' => __( 'Fallback category for generated posts.', 'autoblogger' ),
|
||||
'description' => __( 'Fallback category for generated posts.', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Logging ─────────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_log_level',
|
||||
'label' => __( 'Log Level', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_log_level',
|
||||
'label' => __( 'Log Level', 'prautoblogger' ),
|
||||
'type' => 'select',
|
||||
'section' => 'autoblogger_publishing',
|
||||
'section' => 'prautoblogger_publishing',
|
||||
'default' => 'info',
|
||||
'options' => [
|
||||
'error' => __( 'Error — only failures', 'autoblogger' ),
|
||||
'warning' => __( 'Warning — errors + warnings', 'autoblogger' ),
|
||||
'info' => __( 'Info — key events (recommended)', 'autoblogger' ),
|
||||
'debug' => __( 'Debug — everything (verbose)', 'autoblogger' ),
|
||||
'error' => __( 'Error — only failures', 'prautoblogger' ),
|
||||
'warning' => __( 'Warning — errors + warnings', 'prautoblogger' ),
|
||||
'info' => __( 'Info — key events (recommended)', 'prautoblogger' ),
|
||||
'debug' => __( 'Debug — everything (verbose)', 'prautoblogger' ),
|
||||
],
|
||||
'description' => __( 'Controls detail level in the Activity Log.', 'autoblogger' ),
|
||||
'description' => __( 'Controls detail level in the Activity Log.', 'prautoblogger' ),
|
||||
],
|
||||
|
||||
// ── Analytics ───────────────────────────────────────────────
|
||||
[
|
||||
'id' => 'autoblogger_ga4_property_id',
|
||||
'label' => __( 'GA4 Property ID', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_ga4_property_id',
|
||||
'label' => __( 'GA4 Property ID', 'prautoblogger' ),
|
||||
'type' => 'text',
|
||||
'section' => 'autoblogger_analytics',
|
||||
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_analytics',
|
||||
'description' => __( 'Format: properties/XXXXXXXXX. Leave blank to skip GA4.', 'prautoblogger' ),
|
||||
],
|
||||
[
|
||||
'id' => 'autoblogger_ga4_credentials_json',
|
||||
'label' => __( 'Service Account JSON', 'autoblogger' ),
|
||||
'id' => 'prautoblogger_ga4_credentials_json',
|
||||
'label' => __( 'Service Account JSON', 'prautoblogger' ),
|
||||
'type' => 'password',
|
||||
'section' => 'autoblogger_analytics',
|
||||
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'autoblogger' ),
|
||||
'section' => 'prautoblogger_analytics',
|
||||
'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'prautoblogger' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ declare(strict_types=1);
|
||||
* Handles plugin activation: creates database tables, sets default options,
|
||||
* and schedules the initial cron job.
|
||||
*
|
||||
* Triggered by: WordPress `register_activation_hook` in autoblogger.php.
|
||||
* Triggered by: WordPress `register_activation_hook` in prautoblogger.php.
|
||||
* Dependencies: WordPress $wpdb, dbDelta().
|
||||
*
|
||||
* @see class-deactivator.php — Reverses transient/cron setup on deactivation.
|
||||
* @see ARCHITECTURE.md — Database schema definitions.
|
||||
*/
|
||||
class Autoblogger_Activator {
|
||||
class PRAutoBlogger_Activator {
|
||||
|
||||
/**
|
||||
* Run activation tasks.
|
||||
@@ -30,7 +30,7 @@ class Autoblogger_Activator {
|
||||
self::schedule_cron();
|
||||
|
||||
// Store the DB version so we can run migrations on future updates.
|
||||
update_option( 'autoblogger_db_version', AUTOBLOGGER_DB_VERSION, false );
|
||||
update_option( 'prautoblogger_db_version', PRAUTOBLOGGER_DB_VERSION, false );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ class Autoblogger_Activator {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$prefix = $wpdb->prefix . 'autoblogger_';
|
||||
$prefix = $wpdb->prefix . 'prautoblogger_';
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
@@ -156,21 +156,21 @@ class Autoblogger_Activator {
|
||||
*/
|
||||
private static function set_default_options(): void {
|
||||
$defaults = [
|
||||
'autoblogger_analysis_model' => AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
|
||||
'autoblogger_writing_model' => AUTOBLOGGER_DEFAULT_WRITING_MODEL,
|
||||
'autoblogger_editor_model' => AUTOBLOGGER_DEFAULT_EDITOR_MODEL,
|
||||
'autoblogger_daily_article_target' => 1,
|
||||
'autoblogger_writing_pipeline' => 'multi_step',
|
||||
'autoblogger_niche_description' => '',
|
||||
'autoblogger_target_subreddits' => '[]',
|
||||
'autoblogger_monthly_budget_usd' => 50.00,
|
||||
'autoblogger_tone' => 'informational',
|
||||
'autoblogger_min_word_count' => 800,
|
||||
'autoblogger_max_word_count' => 2000,
|
||||
'autoblogger_topic_exclusions' => '[]',
|
||||
'autoblogger_enabled_sources' => '["reddit"]',
|
||||
'autoblogger_schedule_time' => '03:00',
|
||||
'autoblogger_log_level' => 'info',
|
||||
'prautoblogger_analysis_model' => PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL,
|
||||
'prautoblogger_writing_model' => PRAUTOBLOGGER_DEFAULT_WRITING_MODEL,
|
||||
'prautoblogger_editor_model' => PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL,
|
||||
'prautoblogger_daily_article_target' => 1,
|
||||
'prautoblogger_writing_pipeline' => 'multi_step',
|
||||
'prautoblogger_niche_description' => '',
|
||||
'prautoblogger_target_subreddits' => '[]',
|
||||
'prautoblogger_monthly_budget_usd' => 50.00,
|
||||
'prautoblogger_tone' => 'informational',
|
||||
'prautoblogger_min_word_count' => 800,
|
||||
'prautoblogger_max_word_count' => 2000,
|
||||
'prautoblogger_topic_exclusions' => '[]',
|
||||
'prautoblogger_enabled_sources' => '["reddit"]',
|
||||
'prautoblogger_schedule_time' => '03:00',
|
||||
'prautoblogger_log_level' => 'info',
|
||||
];
|
||||
|
||||
foreach ( $defaults as $key => $value ) {
|
||||
@@ -181,8 +181,8 @@ class Autoblogger_Activator {
|
||||
/**
|
||||
* Schedule the daily generation cron event if not already scheduled.
|
||||
*
|
||||
* The custom 'autoblogger_six_hours' schedule is normally registered via
|
||||
* the `cron_schedules` filter in class-autoblogger.php. However, activation
|
||||
* The custom 'prautoblogger_six_hours' schedule is normally registered via
|
||||
* the `cron_schedules` filter in class-prautoblogger.php. However, activation
|
||||
* runs before `plugins_loaded`, so the filter may not be registered yet.
|
||||
* We register it inline here to ensure it exists at scheduling time.
|
||||
*
|
||||
@@ -191,36 +191,36 @@ class Autoblogger_Activator {
|
||||
* @return void
|
||||
*/
|
||||
private static function schedule_cron(): void {
|
||||
if ( ! wp_next_scheduled( 'autoblogger_daily_generation' ) ) {
|
||||
if ( ! wp_next_scheduled( 'prautoblogger_daily_generation' ) ) {
|
||||
// Schedule for the configured time tomorrow.
|
||||
$time_str = get_option( 'autoblogger_schedule_time', '03:00' );
|
||||
$time_str = get_option( 'prautoblogger_schedule_time', '03:00' );
|
||||
$parts = explode( ':', $time_str );
|
||||
$hour = isset( $parts[0] ) ? absint( $parts[0] ) : 3;
|
||||
$minute = isset( $parts[1] ) ? absint( $parts[1] ) : 0;
|
||||
$timestamp = strtotime( "tomorrow {$hour}:{$minute}" );
|
||||
|
||||
if ( false !== $timestamp ) {
|
||||
wp_schedule_event( $timestamp, 'daily', 'autoblogger_daily_generation' );
|
||||
wp_schedule_event( $timestamp, 'daily', 'prautoblogger_daily_generation' );
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the six-hour schedule exists before referencing it.
|
||||
// Activation can fire before plugins_loaded, so the cron_schedules filter
|
||||
// in class-autoblogger.php may not be hooked yet.
|
||||
// in class-prautoblogger.php may not be hooked yet.
|
||||
$schedules = wp_get_schedules();
|
||||
if ( ! isset( $schedules['autoblogger_six_hours'] ) ) {
|
||||
if ( ! isset( $schedules['prautoblogger_six_hours'] ) ) {
|
||||
add_filter( 'cron_schedules', static function ( array $scheds ): array {
|
||||
$scheds['autoblogger_six_hours'] = [
|
||||
$scheds['prautoblogger_six_hours'] = [
|
||||
'interval' => 6 * HOUR_IN_SECONDS,
|
||||
'display' => __( 'Every Six Hours', 'autoblogger' ),
|
||||
'display' => __( 'Every Six Hours', 'prautoblogger' ),
|
||||
];
|
||||
return $scheds;
|
||||
} );
|
||||
}
|
||||
|
||||
// Schedule a separate metrics collection job (runs every 6 hours).
|
||||
if ( ! wp_next_scheduled( 'autoblogger_collect_metrics' ) ) {
|
||||
wp_schedule_event( time() + HOUR_IN_SECONDS, 'autoblogger_six_hours', 'autoblogger_collect_metrics' );
|
||||
if ( ! wp_next_scheduled( 'prautoblogger_collect_metrics' ) ) {
|
||||
wp_schedule_event( time() + HOUR_IN_SECONDS, 'prautoblogger_six_hours', 'prautoblogger_collect_metrics' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PSR-4-inspired autoloader for AutoBlogger classes.
|
||||
* PSR-4-inspired autoloader for PRAutoBlogger classes.
|
||||
*
|
||||
* Maps class names like `Autoblogger_Content_Generator` to file paths like
|
||||
* Maps class names like `PRAutoBlogger_Content_Generator` to file paths like
|
||||
* `includes/core/class-content-generator.php` by scanning known subdirectories.
|
||||
*
|
||||
* Triggered by: PHP's spl_autoload when a class is first referenced.
|
||||
* Dependencies: None — this is loaded before anything else.
|
||||
*
|
||||
* @see autoblogger.php — Registers this autoloader at boot time.
|
||||
* @see prautoblogger.php — Registers this autoloader at boot time.
|
||||
*/
|
||||
class Autoblogger_Autoloader {
|
||||
class PRAutoBlogger_Autoloader {
|
||||
|
||||
/**
|
||||
* Directories to scan for class files, relative to the plugin's includes/ dir.
|
||||
@@ -25,6 +25,7 @@ class Autoblogger_Autoloader {
|
||||
'core/',
|
||||
'providers/',
|
||||
'models/',
|
||||
'frontend/',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,7 @@ class Autoblogger_Autoloader {
|
||||
/**
|
||||
* Attempt to load a class file.
|
||||
*
|
||||
* Converts `Autoblogger_Content_Generator` → `class-content-generator.php`
|
||||
* Converts `PRAutoBlogger_Content_Generator` → `class-content-generator.php`
|
||||
* and searches each registered subdirectory under includes/.
|
||||
*
|
||||
* @param string $class_name Fully qualified class name.
|
||||
@@ -48,12 +49,12 @@ class Autoblogger_Autoloader {
|
||||
*/
|
||||
public static function autoload( string $class_name ): void {
|
||||
// Only handle our own classes.
|
||||
if ( 0 !== strpos( $class_name, 'Autoblogger' ) ) {
|
||||
if ( 0 !== strpos( $class_name, 'PRAutoBlogger' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file_name = self::class_to_filename( $class_name );
|
||||
$base_dir = AUTOBLOGGER_PLUGIN_DIR . 'includes/';
|
||||
$base_dir = PRAUTOBLOGGER_PLUGIN_DIR . 'includes/';
|
||||
|
||||
foreach ( self::$directories as $directory ) {
|
||||
$file_path = $base_dir . $directory . $file_name;
|
||||
@@ -67,16 +68,16 @@ class Autoblogger_Autoloader {
|
||||
/**
|
||||
* Convert a class name to its expected filename.
|
||||
*
|
||||
* Autoblogger_Content_Generator → class-content-generator.php
|
||||
* Autoblogger_LLM_Provider_Interface → interface-llm-provider.php
|
||||
* PRAutoBlogger_Content_Generator → class-content-generator.php
|
||||
* PRAutoBlogger_LLM_Provider_Interface → interface-llm-provider.php
|
||||
*
|
||||
* @param string $class_name The class name to convert.
|
||||
*
|
||||
* @return string The expected filename.
|
||||
*/
|
||||
private static function class_to_filename( string $class_name ): string {
|
||||
// Remove the Autoblogger_ prefix.
|
||||
$name = (string) preg_replace( '/^Autoblogger_/', '', $class_name );
|
||||
// Remove the PRAutoBlogger_ prefix.
|
||||
$name = (string) preg_replace( '/^PRAutoBlogger_/', '', $class_name );
|
||||
|
||||
// Determine file prefix: interface- or class-.
|
||||
$prefix = 'class-';
|
||||
|
||||
@@ -7,13 +7,13 @@ declare(strict_types=1);
|
||||
* Note: does NOT delete data or options — that happens in uninstall.php.
|
||||
* Deactivation should be reversible (user can reactivate and pick up where they left off).
|
||||
*
|
||||
* Triggered by: WordPress `register_deactivation_hook` in autoblogger.php.
|
||||
* Triggered by: WordPress `register_deactivation_hook` in prautoblogger.php.
|
||||
* Dependencies: None.
|
||||
*
|
||||
* @see class-activator.php — Sets up what this class tears down.
|
||||
* @see uninstall.php — Permanent data removal on plugin deletion.
|
||||
*/
|
||||
class Autoblogger_Deactivator {
|
||||
class PRAutoBlogger_Deactivator {
|
||||
|
||||
/**
|
||||
* Run deactivation tasks.
|
||||
@@ -34,8 +34,8 @@ class Autoblogger_Deactivator {
|
||||
*/
|
||||
private static function clear_cron(): void {
|
||||
$hooks = [
|
||||
'autoblogger_daily_generation',
|
||||
'autoblogger_collect_metrics',
|
||||
'prautoblogger_daily_generation',
|
||||
'prautoblogger_collect_metrics',
|
||||
];
|
||||
|
||||
foreach ( $hooks as $hook ) {
|
||||
@@ -55,9 +55,9 @@ class Autoblogger_Deactivator {
|
||||
*/
|
||||
private static function clear_transients(): void {
|
||||
$transients = [
|
||||
'autoblogger_reddit_token',
|
||||
'autoblogger_generation_lock',
|
||||
'autoblogger_openrouter_models',
|
||||
'prautoblogger_reddit_token',
|
||||
'prautoblogger_generation_lock',
|
||||
'prautoblogger_openrouter_models',
|
||||
];
|
||||
|
||||
foreach ( $transients as $transient ) {
|
||||
|
||||
@@ -13,7 +13,7 @@ declare(strict_types=1);
|
||||
*
|
||||
* @see admin/class-admin-page.php — Uses this to store/retrieve API keys.
|
||||
*/
|
||||
class Autoblogger_Encryption {
|
||||
class PRAutoBlogger_Encryption {
|
||||
|
||||
private const METHOD = 'aes-256-cbc';
|
||||
|
||||
@@ -35,7 +35,7 @@ class Autoblogger_Encryption {
|
||||
|
||||
$ciphertext = openssl_encrypt( $plaintext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
if ( false === $ciphertext ) {
|
||||
Autoblogger_Logger::instance()->error( 'Encryption failed: ' . openssl_error_string(), 'encryption' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Encryption failed: ' . openssl_error_string(), 'encryption' );
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class Autoblogger_Encryption {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$raw = base64_decode( $encrypted, true );
|
||||
if ( false === $raw || strlen( $raw ) <= $iv_len ) {
|
||||
Autoblogger_Logger::instance()->error( 'Decryption failed: invalid ciphertext format.', 'encryption' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Decryption failed: invalid ciphertext format.', 'encryption' );
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class Autoblogger_Encryption {
|
||||
|
||||
$plaintext = openssl_decrypt( $ciphertext, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
if ( false === $plaintext ) {
|
||||
Autoblogger_Logger::instance()->error( 'Decryption failed: ' . openssl_error_string(), 'encryption' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Decryption failed: ' . openssl_error_string(), 'encryption' );
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
140
includes/class-autoblogger.php → includes/class-prautoblogger.php
Executable file → Normal file
140
includes/class-autoblogger.php → includes/class-prautoblogger.php
Executable file → Normal file
@@ -2,21 +2,21 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Main orchestrator for the AutoBlogger plugin.
|
||||
* Main orchestrator for the PRAutoBlogger plugin.
|
||||
*
|
||||
* Registers all WordPress hooks (actions and filters) and wires up dependencies.
|
||||
* This is the ONLY place hooks are registered — individual classes contain only
|
||||
* business logic, not hook registration.
|
||||
*
|
||||
* Triggered by: autoblogger() singleton in autoblogger.php, called on `plugins_loaded`.
|
||||
* Dependencies: All other plugin classes (loaded via autoloader), Autoblogger_Pipeline_Runner.
|
||||
* Triggered by: prautoblogger() singleton in prautoblogger.php, called on `plugins_loaded`.
|
||||
* Dependencies: All other plugin classes (loaded via autoloader), PRAutoBlogger_Pipeline_Runner.
|
||||
*
|
||||
* @see autoblogger.php — Plugin bootstrap that instantiates this class.
|
||||
* @see prautoblogger.php — Plugin bootstrap that instantiates this class.
|
||||
* @see core/class-pipeline-runner.php — Executes the generation pipeline.
|
||||
* @see ARCHITECTURE.md — Data flow diagram showing how classes interact.
|
||||
* @see CONVENTIONS.md — Hook naming conventions.
|
||||
*/
|
||||
class Autoblogger {
|
||||
class PRAutoBlogger {
|
||||
|
||||
/**
|
||||
* Whether the plugin has been initialized.
|
||||
@@ -52,15 +52,18 @@ class Autoblogger {
|
||||
// Cron hooks — always registered so WP-Cron can fire them.
|
||||
$this->register_cron_hooks();
|
||||
|
||||
// Frontend hooks — shortcode and REST API for public-facing widget.
|
||||
$this->register_frontend_hooks();
|
||||
|
||||
// AJAX hooks — always registered so admin AJAX works.
|
||||
$this->register_ajax_hooks();
|
||||
|
||||
/**
|
||||
* Fires after AutoBlogger has finished registering all hooks.
|
||||
* Fires after PRAutoBlogger has finished registering all hooks.
|
||||
*
|
||||
* Useful for add-ons that need to hook into AutoBlogger's lifecycle.
|
||||
* Useful for add-ons that need to hook into PRAutoBlogger's lifecycle.
|
||||
*/
|
||||
do_action( 'autoblogger_loaded' );
|
||||
do_action( 'prautoblogger_loaded' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,53 +72,71 @@ class Autoblogger {
|
||||
* @return void
|
||||
*/
|
||||
private function register_admin_hooks(): void {
|
||||
$admin_page = new Autoblogger_Admin_Page();
|
||||
$admin_page = new PRAutoBlogger_Admin_Page();
|
||||
add_action( 'admin_menu', [ $admin_page, 'on_register_menu' ] );
|
||||
add_action( 'admin_init', [ $admin_page, 'on_register_settings' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $admin_page, 'on_enqueue_assets' ] );
|
||||
|
||||
$notices = new Autoblogger_Admin_Notices();
|
||||
$notices = new PRAutoBlogger_Admin_Notices();
|
||||
add_action( 'admin_notices', [ $notices, 'on_display_notices' ] );
|
||||
|
||||
$metabox = new Autoblogger_Post_Metabox();
|
||||
$metabox = new PRAutoBlogger_Post_Metabox();
|
||||
add_action( 'add_meta_boxes', [ $metabox, 'on_register_metabox' ] );
|
||||
|
||||
$metrics_page = new Autoblogger_Metrics_Page();
|
||||
$metrics_page = new PRAutoBlogger_Metrics_Page();
|
||||
add_action( 'admin_menu', [ $metrics_page, 'on_register_menu' ] );
|
||||
|
||||
$dashboard_widget = new Autoblogger_Dashboard_Widget();
|
||||
$dashboard_widget = new PRAutoBlogger_Dashboard_Widget();
|
||||
add_action( 'wp_dashboard_setup', [ $dashboard_widget, 'on_register_widget' ] );
|
||||
|
||||
$review_queue = new Autoblogger_Review_Queue();
|
||||
$review_queue = new PRAutoBlogger_Review_Queue();
|
||||
add_action( 'admin_menu', [ $review_queue, 'on_register_menu' ] );
|
||||
|
||||
$log_viewer = new Autoblogger_Log_Viewer();
|
||||
$log_viewer = new PRAutoBlogger_Log_Viewer();
|
||||
add_action( 'admin_menu', [ $log_viewer, 'on_register_menu' ] );
|
||||
|
||||
// Block false update notifications from wordpress.org — our plugin name
|
||||
// collides with a different "PRAutoBlogger" in the plugin directory.
|
||||
add_filter( 'site_transient_update_plugins', [ $this, 'filter_block_false_updates' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register frontend hooks (shortcode and REST API for posts widget).
|
||||
*
|
||||
* The shortcode renders a mount-point div; the React component hydrates it
|
||||
* on the client side by fetching posts from the REST endpoint.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_frontend_hooks(): void {
|
||||
$posts_widget = new PRAutoBlogger_Posts_Widget();
|
||||
add_action( 'init', [ $posts_widget, 'on_register_shortcode' ] );
|
||||
add_action( 'rest_api_init', [ $posts_widget, 'on_register_rest_route' ] );
|
||||
}
|
||||
|
||||
/** Register cron-triggered hooks for scheduled generation and metrics. */
|
||||
private function register_cron_hooks(): void {
|
||||
add_action( 'autoblogger_daily_generation', [ $this, 'on_daily_generation' ] );
|
||||
add_action( 'autoblogger_collect_metrics', [ $this, 'on_collect_metrics' ] );
|
||||
add_action( 'prautoblogger_daily_generation', [ $this, 'on_daily_generation' ] );
|
||||
add_action( 'prautoblogger_collect_metrics', [ $this, 'on_collect_metrics' ] );
|
||||
}
|
||||
|
||||
/** Register AJAX handlers for admin actions. */
|
||||
private function register_ajax_hooks(): void {
|
||||
add_action( 'wp_ajax_autoblogger_generate_now', [ $this, 'on_ajax_generate_now' ] );
|
||||
add_action( 'wp_ajax_autoblogger_test_connection', [ $this, 'on_ajax_test_connection' ] );
|
||||
add_action( 'wp_ajax_prautoblogger_generate_now', [ $this, 'on_ajax_generate_now' ] );
|
||||
add_action( 'wp_ajax_prautoblogger_test_connection', [ $this, 'on_ajax_test_connection' ] );
|
||||
|
||||
$review_queue = new Autoblogger_Review_Queue();
|
||||
add_action( 'wp_ajax_autoblogger_approve_post', [ $review_queue, 'on_ajax_approve_post' ] );
|
||||
add_action( 'wp_ajax_autoblogger_reject_post', [ $review_queue, 'on_ajax_reject_post' ] );
|
||||
$review_queue = new PRAutoBlogger_Review_Queue();
|
||||
add_action( 'wp_ajax_prautoblogger_approve_post', [ $review_queue, 'on_ajax_approve_post' ] );
|
||||
add_action( 'wp_ajax_prautoblogger_reject_post', [ $review_queue, 'on_ajax_reject_post' ] );
|
||||
|
||||
$log_viewer = new Autoblogger_Log_Viewer();
|
||||
add_action( 'wp_ajax_autoblogger_clear_logs', [ $log_viewer, 'on_ajax_clear_logs' ] );
|
||||
$log_viewer = new PRAutoBlogger_Log_Viewer();
|
||||
add_action( 'wp_ajax_prautoblogger_clear_logs', [ $log_viewer, 'on_ajax_clear_logs' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database needs migration after plugin update.
|
||||
*
|
||||
* Compares stored DB version with current AUTOBLOGGER_DB_VERSION.
|
||||
* Compares stored DB version with current PRAUTOBLOGGER_DB_VERSION.
|
||||
* If different, re-runs activation to apply schema changes via dbDelta.
|
||||
*
|
||||
* Side effects: may update database schema and db_version option.
|
||||
@@ -123,9 +144,9 @@ class Autoblogger {
|
||||
* @return void
|
||||
*/
|
||||
public function on_check_db_version(): void {
|
||||
$stored_version = get_option( 'autoblogger_db_version', '0' );
|
||||
if ( version_compare( $stored_version, AUTOBLOGGER_DB_VERSION, '<' ) ) {
|
||||
Autoblogger_Activator::activate();
|
||||
$stored_version = get_option( 'prautoblogger_db_version', '0' );
|
||||
if ( version_compare( $stored_version, PRAUTOBLOGGER_DB_VERSION, '<' ) ) {
|
||||
PRAutoBlogger_Activator::activate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +158,32 @@ class Autoblogger {
|
||||
* @return array<string, array{interval: int, display: string}> Modified schedules.
|
||||
*/
|
||||
public function filter_add_cron_schedules( array $schedules ): array {
|
||||
$schedules['autoblogger_six_hours'] = [
|
||||
$schedules['prautoblogger_six_hours'] = [
|
||||
'interval' => 6 * HOUR_IN_SECONDS,
|
||||
'display' => __( 'Every Six Hours', 'autoblogger' ),
|
||||
'display' => __( 'Every Six Hours', 'prautoblogger' ),
|
||||
];
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block false update notifications from wordpress.org.
|
||||
*
|
||||
* Our plugin slug may collide with a different plugin in the directory.
|
||||
* This filter removes any update notice that wordpress.org might push
|
||||
* for our plugin basename, since we manage updates via our own CI/CD.
|
||||
*
|
||||
* @param object $transient The update_plugins transient data.
|
||||
*
|
||||
* @return object Modified transient with our plugin removed from updates.
|
||||
*/
|
||||
public function filter_block_false_updates( $transient ) {
|
||||
$plugin_basename = PRAUTOBLOGGER_PLUGIN_BASENAME;
|
||||
if ( isset( $transient->response[ $plugin_basename ] ) ) {
|
||||
unset( $transient->response[ $plugin_basename ] );
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the daily generation cron event.
|
||||
*
|
||||
@@ -155,14 +195,14 @@ class Autoblogger {
|
||||
*/
|
||||
public function on_daily_generation(): void {
|
||||
if ( ! $this->acquire_generation_lock() ) {
|
||||
Autoblogger_Logger::instance()->info( 'Daily generation skipped: already running (lock held).', 'scheduler' );
|
||||
PRAutoBlogger_Logger::instance()->info( 'Daily generation skipped: already running (lock held).', 'scheduler' );
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
( new Autoblogger_Pipeline_Runner() )->run();
|
||||
( new PRAutoBlogger_Pipeline_Runner() )->run();
|
||||
} catch ( \Exception $e ) {
|
||||
Autoblogger_Logger::instance()->error( 'Daily generation FAILED: ' . $e->getMessage(), 'scheduler' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Daily generation FAILED: ' . $e->getMessage(), 'scheduler' );
|
||||
} finally {
|
||||
$this->release_generation_lock();
|
||||
}
|
||||
@@ -177,10 +217,10 @@ class Autoblogger {
|
||||
*/
|
||||
public function on_collect_metrics(): void {
|
||||
try {
|
||||
$metrics = new Autoblogger_Metrics_Collector();
|
||||
$metrics = new PRAutoBlogger_Metrics_Collector();
|
||||
$metrics->collect_all();
|
||||
} catch ( \Exception $e ) {
|
||||
Autoblogger_Logger::instance()->error( 'Metrics collection FAILED: ' . $e->getMessage(), 'metrics' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Metrics collection FAILED: ' . $e->getMessage(), 'metrics' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,23 +235,23 @@ class Autoblogger {
|
||||
* @return void
|
||||
*/
|
||||
public function on_ajax_generate_now(): void {
|
||||
check_ajax_referer( 'autoblogger_generate_now', 'nonce' );
|
||||
check_ajax_referer( 'prautoblogger_generate_now', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->acquire_generation_lock() ) {
|
||||
wp_send_json_error( [ 'message' => __( 'A generation run is already in progress.', 'autoblogger' ) ] );
|
||||
wp_send_json_error( [ 'message' => __( 'A generation run is already in progress.', 'prautoblogger' ) ] );
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$results = ( new Autoblogger_Pipeline_Runner() )->run();
|
||||
$results = ( new PRAutoBlogger_Pipeline_Runner() )->run();
|
||||
wp_send_json_success( $results );
|
||||
} catch ( \Exception $e ) {
|
||||
Autoblogger_Logger::instance()->error( 'Manual generation FAILED: ' . $e->getMessage(), 'pipeline' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Manual generation FAILED: ' . $e->getMessage(), 'pipeline' );
|
||||
wp_send_json_error( [ 'message' => $e->getMessage() ] );
|
||||
} finally {
|
||||
$this->release_generation_lock();
|
||||
@@ -220,9 +260,9 @@ class Autoblogger {
|
||||
|
||||
/** AJAX handler: test API connections (OpenRouter, Reddit). */
|
||||
public function on_ajax_test_connection(): void {
|
||||
check_ajax_referer( 'autoblogger_test_connection', 'nonce' );
|
||||
check_ajax_referer( 'prautoblogger_test_connection', 'nonce' );
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'autoblogger' ) ], 403 );
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'prautoblogger' ) ], 403 );
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,16 +270,16 @@ class Autoblogger {
|
||||
$results = [];
|
||||
|
||||
if ( 'openrouter' === $service || 'all' === $service ) {
|
||||
$llm = new Autoblogger_OpenRouter_Provider();
|
||||
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
||||
$results['openrouter'] = $llm->validate_credentials()
|
||||
? [ 'status' => 'ok', 'message' => __( 'OpenRouter connected.', 'autoblogger' ) ]
|
||||
: [ 'status' => 'error', 'message' => __( 'OpenRouter connection failed. Check your API key.', 'autoblogger' ) ];
|
||||
? [ 'status' => 'ok', 'message' => __( 'OpenRouter connected.', 'prautoblogger' ) ]
|
||||
: [ 'status' => 'error', 'message' => __( 'OpenRouter connection failed. Check your API key.', 'prautoblogger' ) ];
|
||||
}
|
||||
if ( 'reddit' === $service || 'all' === $service ) {
|
||||
$reddit = new Autoblogger_Reddit_Provider();
|
||||
$reddit = new PRAutoBlogger_Reddit_Provider();
|
||||
$results['reddit'] = $reddit->validate_credentials()
|
||||
? [ 'status' => 'ok', 'message' => __( 'Reddit API connected.', 'autoblogger' ) ]
|
||||
: [ 'status' => 'error', 'message' => __( 'Reddit connection failed. Check client ID/secret.', 'autoblogger' ) ];
|
||||
? [ 'status' => 'ok', 'message' => __( 'Reddit API connected.', 'prautoblogger' ) ]
|
||||
: [ 'status' => 'error', 'message' => __( 'Reddit connection failed. Check client ID/secret.', 'prautoblogger' ) ];
|
||||
}
|
||||
wp_send_json_success( $results );
|
||||
}
|
||||
@@ -255,7 +295,7 @@ class Autoblogger {
|
||||
private function acquire_generation_lock(): bool {
|
||||
global $wpdb;
|
||||
|
||||
$lock_name = 'autoblogger_generation_lock';
|
||||
$lock_name = 'prautoblogger_generation_lock';
|
||||
$now = (string) time();
|
||||
|
||||
// Clean up expired locks older than 1 hour — prevents permanent deadlock.
|
||||
@@ -290,7 +330,7 @@ class Autoblogger {
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name = %s",
|
||||
'autoblogger_generation_lock'
|
||||
'prautoblogger_generation_lock'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -8,21 +8,21 @@ declare(strict_types=1);
|
||||
* Can approve, request revisions (and produce them), or reject content.
|
||||
* Rejected content is saved as a draft for human review.
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 5).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 5).
|
||||
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
|
||||
*
|
||||
* @see core/class-content-generator.php — Produces the content we review.
|
||||
* @see core/class-publisher.php — Publishes approved content.
|
||||
* @see ARCHITECTURE.md — Data flow step 5.
|
||||
*/
|
||||
class Autoblogger_Chief_Editor {
|
||||
class PRAutoBlogger_Chief_Editor {
|
||||
|
||||
private Autoblogger_LLM_Provider_Interface $llm;
|
||||
private Autoblogger_Cost_Tracker $cost_tracker;
|
||||
private PRAutoBlogger_LLM_Provider_Interface $llm;
|
||||
private PRAutoBlogger_Cost_Tracker $cost_tracker;
|
||||
|
||||
public function __construct(
|
||||
Autoblogger_LLM_Provider_Interface $llm,
|
||||
Autoblogger_Cost_Tracker $cost_tracker
|
||||
PRAutoBlogger_LLM_Provider_Interface $llm,
|
||||
PRAutoBlogger_Cost_Tracker $cost_tracker
|
||||
) {
|
||||
$this->llm = $llm;
|
||||
$this->cost_tracker = $cost_tracker;
|
||||
@@ -38,13 +38,13 @@ class Autoblogger_Chief_Editor {
|
||||
* Side effects: LLM API call(s), cost logging.
|
||||
*
|
||||
* @param string $content The generated HTML content.
|
||||
* @param Autoblogger_Article_Idea $idea The original article idea.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
|
||||
*
|
||||
* @return Autoblogger_Editorial_Review
|
||||
* @return PRAutoBlogger_Editorial_Review
|
||||
*/
|
||||
public function review( string $content, Autoblogger_Article_Idea $idea ): Autoblogger_Editorial_Review {
|
||||
$model = get_option( 'autoblogger_editor_model', AUTOBLOGGER_DEFAULT_EDITOR_MODEL );
|
||||
$niche = get_option( 'autoblogger_niche_description', '' );
|
||||
public function review( string $content, PRAutoBlogger_Article_Idea $idea ): PRAutoBlogger_Editorial_Review {
|
||||
$model = get_option( 'prautoblogger_editor_model', PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL );
|
||||
$niche = get_option( 'prautoblogger_niche_description', '' );
|
||||
|
||||
$system_prompt = $this->build_system_prompt( $niche );
|
||||
$user_prompt = $this->build_review_prompt( $content, $idea );
|
||||
@@ -114,11 +114,11 @@ class Autoblogger_Chief_Editor {
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param Autoblogger_Article_Idea $idea
|
||||
* @param PRAutoBlogger_Article_Idea $idea
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function build_review_prompt( string $content, Autoblogger_Article_Idea $idea ): string {
|
||||
private function build_review_prompt( string $content, PRAutoBlogger_Article_Idea $idea ): string {
|
||||
return sprintf(
|
||||
"Review this article draft:\n\n" .
|
||||
"ORIGINAL BRIEF:\n" .
|
||||
@@ -142,14 +142,14 @@ class Autoblogger_Chief_Editor {
|
||||
*
|
||||
* @param string $content JSON response from LLM.
|
||||
*
|
||||
* @return Autoblogger_Editorial_Review
|
||||
* @return PRAutoBlogger_Editorial_Review
|
||||
*/
|
||||
private function parse_review_response( string $content ): Autoblogger_Editorial_Review {
|
||||
private function parse_review_response( string $content ): PRAutoBlogger_Editorial_Review {
|
||||
$data = json_decode( $content, true );
|
||||
|
||||
if ( ! is_array( $data ) || ! isset( $data['verdict'] ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
|
||||
return new Autoblogger_Editorial_Review( [
|
||||
PRAutoBlogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
|
||||
return new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'rejected',
|
||||
'notes' => 'Editor response could not be parsed.',
|
||||
'quality_score' => 0.0,
|
||||
@@ -163,7 +163,7 @@ class Autoblogger_Chief_Editor {
|
||||
$verdict = 'rejected';
|
||||
}
|
||||
|
||||
return new Autoblogger_Editorial_Review( [
|
||||
return new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => $verdict,
|
||||
'notes' => sanitize_textarea_field( $data['notes'] ?? '' ),
|
||||
'revised_content' => isset( $data['revised_content'] ) ? wp_kses_post( $data['revised_content'] ) : null,
|
||||
|
||||
@@ -8,7 +8,7 @@ declare(strict_types=1);
|
||||
* Uses a cheap model (configurable) to keep analysis costs low.
|
||||
* Batches source data into manageable chunks for the LLM context window.
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 2).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 2).
|
||||
* Dependencies: LLM_Provider_Interface, Cost_Tracker, WordPress $wpdb.
|
||||
*
|
||||
* @see providers/interface-llm-provider.php — LLM used for analysis.
|
||||
@@ -16,14 +16,14 @@ declare(strict_types=1);
|
||||
* @see core/class-idea-scorer.php — Consumes analysis results.
|
||||
* @see ARCHITECTURE.md — Data flow step 2.
|
||||
*/
|
||||
class Autoblogger_Content_Analyzer {
|
||||
class PRAutoBlogger_Content_Analyzer {
|
||||
|
||||
private Autoblogger_LLM_Provider_Interface $llm;
|
||||
private Autoblogger_Cost_Tracker $cost_tracker;
|
||||
private PRAutoBlogger_LLM_Provider_Interface $llm;
|
||||
private PRAutoBlogger_Cost_Tracker $cost_tracker;
|
||||
|
||||
public function __construct(
|
||||
Autoblogger_LLM_Provider_Interface $llm,
|
||||
Autoblogger_Cost_Tracker $cost_tracker
|
||||
PRAutoBlogger_LLM_Provider_Interface $llm,
|
||||
PRAutoBlogger_Cost_Tracker $cost_tracker
|
||||
) {
|
||||
$this->llm = $llm;
|
||||
$this->cost_tracker = $cost_tracker;
|
||||
@@ -37,18 +37,18 @@ class Autoblogger_Content_Analyzer {
|
||||
*
|
||||
* Side effects: LLM API call, database reads/writes.
|
||||
*
|
||||
* @return Autoblogger_Analysis_Result[] Detected patterns.
|
||||
* @return PRAutoBlogger_Analysis_Result[] Detected patterns.
|
||||
*/
|
||||
public function analyze_recent_data(): array {
|
||||
$source_items = $this->fetch_recent_source_data();
|
||||
|
||||
if ( empty( $source_items ) ) {
|
||||
Autoblogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
|
||||
PRAutoBlogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
|
||||
return [];
|
||||
}
|
||||
|
||||
$niche = get_option( 'autoblogger_niche_description', '' );
|
||||
$model = get_option( 'autoblogger_analysis_model', AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
|
||||
$niche = get_option( 'prautoblogger_niche_description', '' );
|
||||
$model = get_option( 'prautoblogger_analysis_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
|
||||
|
||||
// Build the source data summary for the LLM (trim to avoid huge contexts).
|
||||
$summary = $this->build_source_summary( $source_items );
|
||||
@@ -98,7 +98,7 @@ class Autoblogger_Content_Analyzer {
|
||||
*/
|
||||
private function fetch_recent_source_data(): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_source_data';
|
||||
$table = $wpdb->prefix . 'prautoblogger_source_data';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
return $wpdb->get_results(
|
||||
@@ -153,7 +153,7 @@ class Autoblogger_Content_Analyzer {
|
||||
*/
|
||||
private function get_performance_context(): string {
|
||||
global $wpdb;
|
||||
$scores_table = $wpdb->prefix . 'autoblogger_content_scores';
|
||||
$scores_table = $wpdb->prefix . 'prautoblogger_content_scores';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$top_posts = $wpdb->get_results(
|
||||
@@ -239,12 +239,12 @@ class Autoblogger_Content_Analyzer {
|
||||
* @param string $content LLM response content (JSON).
|
||||
* @param array<int, array<string, mixed>> $source_items Original source data for ID mapping.
|
||||
*
|
||||
* @return Autoblogger_Analysis_Result[]
|
||||
* @return PRAutoBlogger_Analysis_Result[]
|
||||
*/
|
||||
private function parse_analysis_response( string $content, array $source_items ): array {
|
||||
$data = json_decode( $content, true );
|
||||
if ( ! isset( $data['patterns'] ) || ! is_array( $data['patterns'] ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ class Autoblogger_Content_Analyzer {
|
||||
$results = [];
|
||||
|
||||
foreach ( $data['patterns'] as $pattern ) {
|
||||
$results[] = new Autoblogger_Analysis_Result( [
|
||||
$results[] = new PRAutoBlogger_Analysis_Result( [
|
||||
'analysis_type' => sanitize_text_field( $pattern['type'] ?? 'question' ),
|
||||
'topic' => sanitize_text_field( $pattern['topic'] ?? '' ),
|
||||
'summary' => sanitize_textarea_field( $pattern['summary'] ?? '' ),
|
||||
@@ -276,13 +276,13 @@ class Autoblogger_Content_Analyzer {
|
||||
*
|
||||
* Side effects: database inserts.
|
||||
*
|
||||
* @param Autoblogger_Analysis_Result[] $results
|
||||
* @param PRAutoBlogger_Analysis_Result[] $results
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function store_results( array $results ): void {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_analysis_results';
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
|
||||
foreach ( $results as $result ) {
|
||||
$row = $result->to_db_row();
|
||||
|
||||
@@ -8,21 +8,21 @@ declare(strict_types=1);
|
||||
* - single_pass: One LLM call produces the complete article (cheaper, faster).
|
||||
* - multi_step: Outline → Draft → Polish pipeline (higher quality, more cost).
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 4).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 4).
|
||||
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
|
||||
*
|
||||
* @see core/class-chief-editor.php — Reviews output of this class.
|
||||
* @see providers/interface-llm-provider.php — LLM used for generation.
|
||||
* @see ARCHITECTURE.md — Data flow step 4.
|
||||
*/
|
||||
class Autoblogger_Content_Generator {
|
||||
class PRAutoBlogger_Content_Generator {
|
||||
|
||||
private Autoblogger_LLM_Provider_Interface $llm;
|
||||
private Autoblogger_Cost_Tracker $cost_tracker;
|
||||
private PRAutoBlogger_LLM_Provider_Interface $llm;
|
||||
private PRAutoBlogger_Cost_Tracker $cost_tracker;
|
||||
|
||||
public function __construct(
|
||||
Autoblogger_LLM_Provider_Interface $llm,
|
||||
Autoblogger_Cost_Tracker $cost_tracker
|
||||
PRAutoBlogger_LLM_Provider_Interface $llm,
|
||||
PRAutoBlogger_Cost_Tracker $cost_tracker
|
||||
) {
|
||||
$this->llm = $llm;
|
||||
$this->cost_tracker = $cost_tracker;
|
||||
@@ -35,23 +35,23 @@ class Autoblogger_Content_Generator {
|
||||
*
|
||||
* Side effects: LLM API calls (1-3 depending on mode), cost logging.
|
||||
*
|
||||
* @param Autoblogger_Article_Idea $idea The scored idea to generate content for.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for.
|
||||
*
|
||||
* @return string Generated HTML content.
|
||||
*
|
||||
* @throws \RuntimeException On generation failure.
|
||||
*/
|
||||
public function generate( Autoblogger_Article_Idea $idea ): string {
|
||||
$mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
|
||||
public function generate( PRAutoBlogger_Article_Idea $idea ): string {
|
||||
$mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
|
||||
|
||||
$request = new Autoblogger_Content_Request(
|
||||
$request = new PRAutoBlogger_Content_Request(
|
||||
$idea,
|
||||
$mode,
|
||||
get_option( 'autoblogger_tone', 'informational' ),
|
||||
absint( get_option( 'autoblogger_min_word_count', 800 ) ),
|
||||
absint( get_option( 'autoblogger_max_word_count', 2000 ) ),
|
||||
get_option( 'autoblogger_niche_description', '' ),
|
||||
json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: []
|
||||
get_option( 'prautoblogger_tone', 'informational' ),
|
||||
absint( get_option( 'prautoblogger_min_word_count', 800 ) ),
|
||||
absint( get_option( 'prautoblogger_max_word_count', 2000 ) ),
|
||||
get_option( 'prautoblogger_niche_description', '' ),
|
||||
json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: []
|
||||
);
|
||||
|
||||
if ( 'single_pass' === $mode ) {
|
||||
@@ -64,12 +64,12 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Single-pass generation: one LLM call produces the complete article.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
*
|
||||
* @return string Generated HTML content.
|
||||
*/
|
||||
private function generate_single_pass( Autoblogger_Content_Request $request ): string {
|
||||
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string {
|
||||
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
$idea = $request->get_idea();
|
||||
|
||||
$system = $this->build_writer_system_prompt( $request );
|
||||
@@ -102,12 +102,12 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Multi-step generation: Outline → Draft → Polish.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
*
|
||||
* @return string Generated HTML content.
|
||||
*/
|
||||
private function generate_multi_step( Autoblogger_Content_Request $request ): string {
|
||||
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string {
|
||||
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
|
||||
// Step 1: Generate outline.
|
||||
$outline = $this->stage_outline( $request, $model );
|
||||
@@ -124,12 +124,12 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Multi-step stage 1: Generate an article outline.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
* @param string $model
|
||||
*
|
||||
* @return string The outline text.
|
||||
*/
|
||||
private function stage_outline( Autoblogger_Content_Request $request, string $model ): string {
|
||||
private function stage_outline( PRAutoBlogger_Content_Request $request, string $model ): string {
|
||||
$idea = $request->get_idea();
|
||||
|
||||
$prompt = sprintf(
|
||||
@@ -168,13 +168,13 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Multi-step stage 2: Write the full draft from the outline.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
* @param string $outline
|
||||
* @param string $model
|
||||
*
|
||||
* @return string Draft HTML content.
|
||||
*/
|
||||
private function stage_draft( Autoblogger_Content_Request $request, string $outline, string $model ): string {
|
||||
private function stage_draft( PRAutoBlogger_Content_Request $request, string $outline, string $model ): string {
|
||||
$prompt = sprintf(
|
||||
"Using this outline, write the full blog post in HTML format.\n\n" .
|
||||
"OUTLINE:\n%s\n\n" .
|
||||
@@ -213,13 +213,13 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Multi-step stage 3: Polish and refine the draft.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
* @param string $draft
|
||||
* @param string $model
|
||||
*
|
||||
* @return string Polished HTML content.
|
||||
*/
|
||||
private function stage_polish( Autoblogger_Content_Request $request, string $draft, string $model ): string {
|
||||
private function stage_polish( PRAutoBlogger_Content_Request $request, string $draft, string $model ): string {
|
||||
$prompt = "Review and polish this blog post draft. Improve:\n" .
|
||||
"1. Flow and readability\n" .
|
||||
"2. SEO optimization (headings, keyword placement)\n" .
|
||||
@@ -249,11 +249,11 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Build the system prompt shared across all writing stages.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function build_writer_system_prompt( Autoblogger_Content_Request $request ): string {
|
||||
private function build_writer_system_prompt( PRAutoBlogger_Content_Request $request ): string {
|
||||
$niche = $request->get_niche_description();
|
||||
$prompt = "You are an expert blog writer";
|
||||
if ( '' !== $niche ) {
|
||||
@@ -268,11 +268,11 @@ class Autoblogger_Content_Generator {
|
||||
/**
|
||||
* Build the user prompt for single-pass generation.
|
||||
*
|
||||
* @param Autoblogger_Content_Request $request
|
||||
* @param PRAutoBlogger_Content_Request $request
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function build_single_pass_prompt( Autoblogger_Content_Request $request ): string {
|
||||
private function build_single_pass_prompt( PRAutoBlogger_Content_Request $request ): string {
|
||||
$idea = $request->get_idea();
|
||||
|
||||
return sprintf(
|
||||
|
||||
@@ -11,15 +11,15 @@ declare(strict_types=1);
|
||||
*
|
||||
* Triggered by: Every class that makes an LLM API call (Content_Analyzer,
|
||||
* Content_Generator, Chief_Editor, Metrics_Collector).
|
||||
* Dependencies: WordPress $wpdb, Autoblogger_OpenRouter_Provider (for cost estimation).
|
||||
* Dependencies: WordPress $wpdb, PRAutoBlogger_OpenRouter_Provider (for cost estimation).
|
||||
*
|
||||
* @see core/class-content-analyzer.php — Calls log_api_call() after analysis.
|
||||
* @see core/class-content-generator.php — Calls log_api_call() after each stage.
|
||||
* @see core/class-chief-editor.php — Calls log_api_call() after review.
|
||||
* @see admin/class-metrics-page.php — Displays cost data from this class.
|
||||
* @see ARCHITECTURE.md — ab_generation_log table schema.
|
||||
* @see ARCHITECTURE.md — prab_generation_log table schema.
|
||||
*/
|
||||
class Autoblogger_Cost_Tracker {
|
||||
class PRAutoBlogger_Cost_Tracker {
|
||||
|
||||
/**
|
||||
* Running cost for the current pipeline execution.
|
||||
@@ -59,7 +59,7 @@ class Autoblogger_Cost_Tracker {
|
||||
/**
|
||||
* Log an API call with its token usage and estimated cost.
|
||||
*
|
||||
* Side effects: database insert into ab_generation_log.
|
||||
* Side effects: database insert into prab_generation_log.
|
||||
*
|
||||
* @param int|null $post_id WordPress post ID (null during generation, set later).
|
||||
* @param string $stage Pipeline stage: 'analysis', 'outline', 'draft', 'polish', 'review'.
|
||||
@@ -82,12 +82,12 @@ class Autoblogger_Cost_Tracker {
|
||||
string $response_status = 'success',
|
||||
string $error_message = ''
|
||||
): void {
|
||||
$llm = new Autoblogger_OpenRouter_Provider();
|
||||
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
||||
$cost = $llm->estimate_cost( $model, $prompt_tokens, $completion_tokens );
|
||||
|
||||
$this->current_run_cost += $cost;
|
||||
|
||||
$log_entry = new Autoblogger_Generation_Log( [
|
||||
$log_entry = new PRAutoBlogger_Generation_Log( [
|
||||
'post_id' => $post_id,
|
||||
'run_id' => $this->run_id,
|
||||
'stage' => $stage,
|
||||
@@ -102,13 +102,13 @@ class Autoblogger_Cost_Tracker {
|
||||
] );
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_generation_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert( $table, $log_entry->to_db_row() );
|
||||
|
||||
if ( false === $wpdb->insert_id ) {
|
||||
Autoblogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class Autoblogger_Cost_Tracker {
|
||||
* @return bool True if monthly spend >= configured budget.
|
||||
*/
|
||||
public function is_budget_exceeded(): bool {
|
||||
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
|
||||
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
|
||||
if ( $budget <= 0 ) {
|
||||
// Budget of 0 means no limit.
|
||||
return false;
|
||||
@@ -141,7 +141,7 @@ class Autoblogger_Cost_Tracker {
|
||||
*/
|
||||
public function get_monthly_spend(): float {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_generation_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
|
||||
|
||||
@@ -174,7 +174,7 @@ class Autoblogger_Cost_Tracker {
|
||||
*/
|
||||
public function get_daily_spend( int $days = 30 ): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_generation_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
$start_date = gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
|
||||
|
||||
@@ -209,7 +209,7 @@ class Autoblogger_Cost_Tracker {
|
||||
*/
|
||||
public function get_spend_by_stage( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_generation_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$results = $wpdb->get_results(
|
||||
@@ -245,7 +245,7 @@ class Autoblogger_Cost_Tracker {
|
||||
* @return float Percentage (0-100+). Can exceed 100 if overspent.
|
||||
*/
|
||||
public function get_budget_utilization(): float {
|
||||
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
|
||||
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
|
||||
if ( $budget <= 0 ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ declare(strict_types=1);
|
||||
* Handles OAuth2 service account authentication and the GA4 Data API.
|
||||
* Decoupled from metrics collection logic for testability and reusability.
|
||||
*
|
||||
* Triggered by: Autoblogger_Metrics_Collector::fetch_ga4_data() calls methods here.
|
||||
* Dependencies: Autoblogger_Encryption (for credentials decryption), wp_remote_post/get().
|
||||
* Triggered by: PRAutoBlogger_Metrics_Collector::fetch_ga4_data() calls methods here.
|
||||
* Dependencies: PRAutoBlogger_Encryption (for credentials decryption), wp_remote_post/get().
|
||||
*
|
||||
* @see core/class-metrics-collector.php — Instantiates this class.
|
||||
* @see ARCHITECTURE.md — External API integrations table.
|
||||
*/
|
||||
class Autoblogger_GA4_Client {
|
||||
class PRAutoBlogger_GA4_Client {
|
||||
|
||||
/**
|
||||
* Fetch Google Analytics 4 data for given posts.
|
||||
@@ -27,17 +27,17 @@ class Autoblogger_GA4_Client {
|
||||
* @return array<int, array{pageviews: int, avg_time_on_page: float, bounce_rate: float}>
|
||||
*/
|
||||
public function fetch_data( array $posts ): array {
|
||||
$property_id = get_option( 'autoblogger_ga4_property_id', '' );
|
||||
$property_id = get_option( 'prautoblogger_ga4_property_id', '' );
|
||||
if ( '' === $property_id ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$credentials_encrypted = get_option( 'autoblogger_ga4_credentials_json', '' );
|
||||
$credentials_encrypted = get_option( 'prautoblogger_ga4_credentials_json', '' );
|
||||
if ( '' === $credentials_encrypted ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$credentials = Autoblogger_Encryption::decrypt( $credentials_encrypted );
|
||||
$credentials = PRAutoBlogger_Encryption::decrypt( $credentials_encrypted );
|
||||
if ( '' === $credentials ) {
|
||||
return [];
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class Autoblogger_GA4_Client {
|
||||
// Using the REST API directly since we don't want a full Google SDK dependency.
|
||||
$access_token = $this->get_access_token( $credentials );
|
||||
if ( '' === $access_token ) {
|
||||
Autoblogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in AutoBlogger → Settings.', 'ga4' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in PRAutoBlogger → Settings.', 'ga4' );
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -94,14 +94,14 @@ class Autoblogger_GA4_Client {
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
|
||||
return [];
|
||||
}
|
||||
|
||||
$ga4_status = wp_remote_retrieve_response_code( $response );
|
||||
if ( 200 !== $ga4_status ) {
|
||||
$ga4_body = wp_remote_retrieve_body( $response );
|
||||
Autoblogger_Logger::instance()->error(
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'GA4 API request failed (HTTP %d): %s', $ga4_status, substr( $ga4_body, 0, 500 ) ),
|
||||
'ga4'
|
||||
);
|
||||
@@ -159,7 +159,7 @@ class Autoblogger_GA4_Client {
|
||||
|
||||
$key = openssl_pkey_get_private( $creds['private_key'] );
|
||||
if ( false === $key ) {
|
||||
Autoblogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ class Autoblogger_GA4_Client {
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class Autoblogger_GA4_Client {
|
||||
|
||||
if ( 200 !== $token_status || ! isset( $data['access_token'] ) ) {
|
||||
$error_desc = $data['error_description'] ?? $data['error'] ?? 'unknown';
|
||||
Autoblogger_Logger::instance()->error(
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'GA4: OAuth token exchange failed (HTTP %d): %s', $token_status, $error_desc ),
|
||||
'ga4'
|
||||
);
|
||||
|
||||
@@ -8,25 +8,25 @@ declare(strict_types=1);
|
||||
* scores ideas by relevance, frequency, and freshness, then returns
|
||||
* the top N ideas matching the daily target.
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 3).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 3).
|
||||
* Dependencies: WordPress $wpdb, WP_Query.
|
||||
*
|
||||
* @see core/class-content-analyzer.php — Produces the analysis results we consume.
|
||||
* @see core/class-content-generator.php — Consumes the ranked ideas we produce.
|
||||
* @see ARCHITECTURE.md — Data flow step 3.
|
||||
*/
|
||||
class Autoblogger_Idea_Scorer {
|
||||
class PRAutoBlogger_Idea_Scorer {
|
||||
|
||||
/**
|
||||
* Score, deduplicate, and rank article ideas.
|
||||
*
|
||||
* @param Autoblogger_Analysis_Result[] $analysis_results Raw analysis output.
|
||||
* @param PRAutoBlogger_Analysis_Result[] $analysis_results Raw analysis output.
|
||||
* @param int $target_count Number of ideas to return.
|
||||
*
|
||||
* @return Autoblogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
|
||||
* @return PRAutoBlogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
|
||||
*/
|
||||
public function score_and_rank( array $analysis_results, int $target_count ): array {
|
||||
$exclusions = json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: [];
|
||||
$exclusions = json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: [];
|
||||
$ideas = [];
|
||||
|
||||
foreach ( $analysis_results as $result ) {
|
||||
@@ -42,7 +42,7 @@ class Autoblogger_Idea_Scorer {
|
||||
|
||||
$metadata = $result->get_metadata() ?? [];
|
||||
|
||||
$ideas[] = new Autoblogger_Article_Idea( [
|
||||
$ideas[] = new PRAutoBlogger_Article_Idea( [
|
||||
'topic' => $result->get_topic(),
|
||||
'article_type' => $this->map_type( $result->get_analysis_type() ),
|
||||
'suggested_title' => $metadata['suggested_title'] ?? $result->get_topic(),
|
||||
@@ -56,7 +56,7 @@ class Autoblogger_Idea_Scorer {
|
||||
}
|
||||
|
||||
// Sort by score descending.
|
||||
usort( $ideas, static function ( Autoblogger_Article_Idea $a, Autoblogger_Article_Idea $b ): int {
|
||||
usort( $ideas, static function ( PRAutoBlogger_Article_Idea $a, PRAutoBlogger_Article_Idea $b ): int {
|
||||
return $b->get_score() <=> $a->get_score();
|
||||
} );
|
||||
|
||||
@@ -68,11 +68,11 @@ class Autoblogger_Idea_Scorer {
|
||||
*
|
||||
* Factors: LLM relevance score (50%), frequency (30%), recency bonus (20%).
|
||||
*
|
||||
* @param Autoblogger_Analysis_Result $result
|
||||
* @param PRAutoBlogger_Analysis_Result $result
|
||||
*
|
||||
* @return float Score between 0 and 1.
|
||||
*/
|
||||
private function compute_score( Autoblogger_Analysis_Result $result ): float {
|
||||
private function compute_score( PRAutoBlogger_Analysis_Result $result ): float {
|
||||
$relevance = min( $result->get_relevance_score(), 1.0 );
|
||||
$frequency = min( $result->get_frequency() / 10.0, 1.0 );
|
||||
|
||||
@@ -114,7 +114,7 @@ class Autoblogger_Idea_Scorer {
|
||||
$query = new \WP_Query( [
|
||||
'post_type' => 'post',
|
||||
'post_status' => [ 'publish', 'draft', 'pending' ],
|
||||
'meta_key' => '_autoblogger_generated',
|
||||
'meta_key' => '_prautoblogger_generated',
|
||||
'meta_value' => '1',
|
||||
's' => $topic,
|
||||
'posts_per_page' => 1,
|
||||
|
||||
@@ -6,7 +6,7 @@ declare(strict_types=1);
|
||||
*
|
||||
* Writes log entries to a custom database table and optionally forwards
|
||||
* to PHP's error_log() for server-level monitoring. The log level is
|
||||
* user-configurable via Settings → AutoBlogger → General.
|
||||
* user-configurable via Settings → PRAutoBlogger → General.
|
||||
*
|
||||
* Levels (ascending verbosity): error → warning → info → debug.
|
||||
* Setting the level to "info" captures error, warning, and info — not debug.
|
||||
@@ -15,10 +15,10 @@ declare(strict_types=1);
|
||||
* Dependencies: WordPress $wpdb.
|
||||
*
|
||||
* @see admin/class-log-viewer.php — Reads and displays log entries.
|
||||
* @see class-activator.php — Creates the ab_event_log table.
|
||||
* @see class-activator.php — Creates the prab_event_log table.
|
||||
* @see ARCHITECTURE.md — Logging architecture section.
|
||||
*/
|
||||
class Autoblogger_Logger {
|
||||
class PRAutoBlogger_Logger {
|
||||
|
||||
/** Log levels, lower = more severe. */
|
||||
public const LEVEL_ERROR = 0;
|
||||
@@ -47,7 +47,7 @@ class Autoblogger_Logger {
|
||||
private int $threshold;
|
||||
|
||||
private function __construct() {
|
||||
$setting = get_option( 'autoblogger_log_level', 'info' );
|
||||
$setting = get_option( 'prautoblogger_log_level', 'info' );
|
||||
$this->threshold = self::LEVEL_MAP[ $setting ] ?? self::LEVEL_INFO;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,11 @@ class Autoblogger_Logger {
|
||||
|
||||
// Always forward errors to PHP error_log for server-level monitoring.
|
||||
if ( $level <= self::LEVEL_WARNING ) {
|
||||
error_log( sprintf( '[AutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
|
||||
error_log( sprintf( '[PRAutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_event_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_event_log';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert( $table, [
|
||||
@@ -155,7 +155,7 @@ class Autoblogger_Logger {
|
||||
int $per_page = 50
|
||||
): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_event_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_event_log';
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
@@ -207,7 +207,7 @@ class Autoblogger_Logger {
|
||||
*/
|
||||
public static function prune( int $days = 30 ): int {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_event_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_event_log';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
return (int) $wpdb->query(
|
||||
|
||||
@@ -5,21 +5,21 @@ declare(strict_types=1);
|
||||
* Collects post performance metrics from WordPress native data and Google Analytics 4.
|
||||
*
|
||||
* Runs on a separate cron schedule (every 6 hours) to gather pageviews, bounce rate,
|
||||
* time on page, and comments for AutoBlogger-generated posts. Computes a composite
|
||||
* time on page, and comments for PRAutoBlogger-generated posts. Computes a composite
|
||||
* "content score" that feeds back into the analysis engine for self-improvement.
|
||||
*
|
||||
* Triggered by: WP-Cron hook `autoblogger_collect_metrics` (every 6 hours).
|
||||
* Dependencies: WordPress $wpdb, Autoblogger_GA4_Client (for GA4 API calls).
|
||||
* Triggered by: WP-Cron hook `prautoblogger_collect_metrics` (every 6 hours).
|
||||
* Dependencies: WordPress $wpdb, PRAutoBlogger_GA4_Client (for GA4 API calls).
|
||||
*
|
||||
* @see core/class-ga4-client.php — Handles GA4 API authentication and requests.
|
||||
* @see core/class-content-analyzer.php — Reads scores for self-improvement loop.
|
||||
* @see admin/class-metrics-page.php — Displays collected metrics.
|
||||
* @see ARCHITECTURE.md — Data flow step 7.
|
||||
*/
|
||||
class Autoblogger_Metrics_Collector {
|
||||
class PRAutoBlogger_Metrics_Collector {
|
||||
|
||||
/**
|
||||
* Collect metrics for all AutoBlogger-generated posts.
|
||||
* Collect metrics for all PRAutoBlogger-generated posts.
|
||||
*
|
||||
* Gathers WordPress native metrics (comments) and GA4 data (if configured),
|
||||
* then computes composite scores.
|
||||
@@ -34,7 +34,7 @@ class Autoblogger_Metrics_Collector {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ga4_client = new Autoblogger_GA4_Client();
|
||||
$ga4_client = new PRAutoBlogger_GA4_Client();
|
||||
$ga4_data = $ga4_client->fetch_data( $posts );
|
||||
$scored = 0;
|
||||
|
||||
@@ -54,12 +54,12 @@ class Autoblogger_Metrics_Collector {
|
||||
$scored++;
|
||||
}
|
||||
|
||||
Autoblogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
|
||||
PRAutoBlogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
|
||||
return $scored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AutoBlogger-generated published posts.
|
||||
* Get all PRAutoBlogger-generated published posts.
|
||||
*
|
||||
* @return \WP_Post[]
|
||||
*/
|
||||
@@ -67,7 +67,7 @@ class Autoblogger_Metrics_Collector {
|
||||
$query = new \WP_Query( [
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_autoblogger_generated',
|
||||
'meta_key' => '_prautoblogger_generated',
|
||||
'meta_value' => '1',
|
||||
'posts_per_page' => 100,
|
||||
'orderby' => 'date',
|
||||
@@ -89,7 +89,7 @@ class Autoblogger_Metrics_Collector {
|
||||
|
||||
// Post may have been deleted between the WP_Query and this call.
|
||||
if ( null === $post ) {
|
||||
Autoblogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
|
||||
PRAutoBlogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -160,9 +160,9 @@ class Autoblogger_Metrics_Collector {
|
||||
*/
|
||||
private function store_score( int $post_id, array $wp_metrics, array $ga_metrics, array $score_data ): void {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_content_scores';
|
||||
$table = $wpdb->prefix . 'prautoblogger_content_scores';
|
||||
|
||||
$entry = new Autoblogger_Content_Score( [
|
||||
$entry = new PRAutoBlogger_Content_Score( [
|
||||
'post_id' => $post_id,
|
||||
'pageviews' => $ga_metrics['pageviews'] ?? 0,
|
||||
'avg_time_on_page' => $ga_metrics['avg_time_on_page'] ?? 0.0,
|
||||
|
||||
@@ -7,14 +7,14 @@ declare(strict_types=1);
|
||||
* Orchestrates: Budget check → Collect source data → Analyze patterns →
|
||||
* Score ideas → Generate content → Editorial review → Publish.
|
||||
*
|
||||
* Triggered by: Autoblogger::on_daily_generation() and Autoblogger::on_ajax_generate_now().
|
||||
* Triggered by: PRAutoBlogger::on_daily_generation() and PRAutoBlogger::on_ajax_generate_now().
|
||||
* Dependencies: All core content classes (Collector, Analyzer, Scorer, Generator, Editor, Publisher).
|
||||
*
|
||||
* @see class-autoblogger.php — Instantiates this class and calls run().
|
||||
* @see class-prautoblogger.php — Instantiates this class and calls run().
|
||||
* @see core/class-cost-tracker.php — Tracks cumulative API costs throughout the run.
|
||||
* @see ARCHITECTURE.md — Data flow diagram showing pipeline steps.
|
||||
*/
|
||||
class Autoblogger_Pipeline_Runner {
|
||||
class PRAutoBlogger_Pipeline_Runner {
|
||||
|
||||
/**
|
||||
* Execute the full content generation pipeline.
|
||||
@@ -29,7 +29,7 @@ class Autoblogger_Pipeline_Runner {
|
||||
* @throws \RuntimeException If budget is exceeded or critical step fails.
|
||||
*/
|
||||
public function run(): array {
|
||||
$cost_tracker = new Autoblogger_Cost_Tracker();
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
|
||||
// Tag all log entries in this run with a unique ID so link_generation_logs()
|
||||
// can accurately attribute costs to the correct post in batch runs.
|
||||
@@ -38,35 +38,35 @@ class Autoblogger_Pipeline_Runner {
|
||||
// Hard stop if monthly budget is exceeded.
|
||||
if ( $cost_tracker->is_budget_exceeded() ) {
|
||||
throw new \RuntimeException(
|
||||
__( 'Monthly API budget exceeded. Generation halted.', 'autoblogger' )
|
||||
__( 'Monthly API budget exceeded. Generation halted.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
$target_count = absint( get_option( 'autoblogger_daily_article_target', 1 ) );
|
||||
$target_count = absint( get_option( 'prautoblogger_daily_article_target', 1 ) );
|
||||
|
||||
// Step 1: Collect source data.
|
||||
$collector = new Autoblogger_Source_Collector();
|
||||
$collector = new PRAutoBlogger_Source_Collector();
|
||||
$collector->collect_from_all_sources();
|
||||
|
||||
// Step 2: Analyze collected data for patterns.
|
||||
$llm = new Autoblogger_OpenRouter_Provider();
|
||||
$analyzer = new Autoblogger_Content_Analyzer( $llm, $cost_tracker );
|
||||
$llm = new PRAutoBlogger_OpenRouter_Provider();
|
||||
$analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker );
|
||||
$analysis = $analyzer->analyze_recent_data();
|
||||
|
||||
// Step 3: Score and deduplicate ideas.
|
||||
$scorer = new Autoblogger_Idea_Scorer();
|
||||
$scorer = new PRAutoBlogger_Idea_Scorer();
|
||||
$ideas = $scorer->score_and_rank( $analysis, $target_count );
|
||||
|
||||
if ( empty( $ideas ) ) {
|
||||
Autoblogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
|
||||
PRAutoBlogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
|
||||
return [ 'generated' => 0, 'published' => 0, 'rejected' => 0, 'cost' => 0.0 ];
|
||||
}
|
||||
|
||||
// Step 4 & 5: Generate content and run through editor for each idea.
|
||||
$generator = new Autoblogger_Content_Generator( $llm, $cost_tracker );
|
||||
$editor = new Autoblogger_Chief_Editor( $llm, $cost_tracker );
|
||||
$publisher = new Autoblogger_Publisher();
|
||||
$auto_publish = in_array( get_option( 'autoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
|
||||
$generator = new PRAutoBlogger_Content_Generator( $llm, $cost_tracker );
|
||||
$editor = new PRAutoBlogger_Chief_Editor( $llm, $cost_tracker );
|
||||
$publisher = new PRAutoBlogger_Publisher();
|
||||
$auto_publish = in_array( get_option( 'prautoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
|
||||
|
||||
$generated = 0;
|
||||
$published = 0;
|
||||
@@ -75,7 +75,7 @@ class Autoblogger_Pipeline_Runner {
|
||||
foreach ( $ideas as $idea ) {
|
||||
// Re-check budget before each article — stop early if we hit the limit.
|
||||
if ( $cost_tracker->is_budget_exceeded() ) {
|
||||
Autoblogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
|
||||
PRAutoBlogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,17 +103,17 @@ class Autoblogger_Pipeline_Runner {
|
||||
} else {
|
||||
$publisher->save_as_draft( $content, $idea, $review, $cost_tracker->get_run_id() );
|
||||
$rejected++;
|
||||
Autoblogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
|
||||
PRAutoBlogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
Autoblogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
|
||||
// Continue with next idea — don't let one failure stop the batch.
|
||||
}
|
||||
}
|
||||
|
||||
$total_cost = $cost_tracker->get_current_run_cost();
|
||||
|
||||
Autoblogger_Logger::instance()->info(
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Pipeline complete: %d generated, %d published, %d rejected. Cost: $%.4f', $generated, $published, $rejected, $total_cost ),
|
||||
'pipeline'
|
||||
);
|
||||
|
||||
@@ -7,14 +7,14 @@ declare(strict_types=1);
|
||||
* Handles both publishing (editor-approved) and saving as draft (editor-rejected).
|
||||
* Stores all generation metadata as post_meta for transparency and auditability.
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 6).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 6).
|
||||
* Dependencies: WordPress wp_insert_post(), post meta API.
|
||||
*
|
||||
* @see core/class-chief-editor.php — Produces the editorial review we consume.
|
||||
* @see core/class-content-generator.php — Produces the content we publish.
|
||||
* @see ARCHITECTURE.md — Data flow step 6.
|
||||
*/
|
||||
class Autoblogger_Publisher {
|
||||
class PRAutoBlogger_Publisher {
|
||||
|
||||
/**
|
||||
* Publish an editor-approved article.
|
||||
@@ -25,8 +25,8 @@ class Autoblogger_Publisher {
|
||||
* Side effects: Creates a WordPress post, writes post_meta.
|
||||
*
|
||||
* @param string $content The final HTML content.
|
||||
* @param Autoblogger_Article_Idea $idea The original article idea.
|
||||
* @param Autoblogger_Editorial_Review $review The editor's review.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
|
||||
* @param PRAutoBlogger_Editorial_Review $review The editor's review.
|
||||
* @param string|null $run_id Pipeline run ID for log linking.
|
||||
*
|
||||
* @return int The created post ID.
|
||||
@@ -35,8 +35,8 @@ class Autoblogger_Publisher {
|
||||
*/
|
||||
public function publish(
|
||||
string $content,
|
||||
Autoblogger_Article_Idea $idea,
|
||||
Autoblogger_Editorial_Review $review,
|
||||
PRAutoBlogger_Article_Idea $idea,
|
||||
PRAutoBlogger_Editorial_Review $review,
|
||||
?string $run_id = null
|
||||
): int {
|
||||
return $this->create_post( $content, $idea, $review, 'publish', $run_id );
|
||||
@@ -46,8 +46,8 @@ class Autoblogger_Publisher {
|
||||
* Save an editor-rejected article as a draft for human review.
|
||||
*
|
||||
* @param string $content The generated HTML content (pre-revision).
|
||||
* @param Autoblogger_Article_Idea $idea The original article idea.
|
||||
* @param Autoblogger_Editorial_Review $review The editor's review with rejection notes.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
|
||||
* @param PRAutoBlogger_Editorial_Review $review The editor's review with rejection notes.
|
||||
* @param string|null $run_id Pipeline run ID for log linking.
|
||||
*
|
||||
* @return int The created post ID.
|
||||
@@ -56,8 +56,8 @@ class Autoblogger_Publisher {
|
||||
*/
|
||||
public function save_as_draft(
|
||||
string $content,
|
||||
Autoblogger_Article_Idea $idea,
|
||||
Autoblogger_Editorial_Review $review,
|
||||
PRAutoBlogger_Article_Idea $idea,
|
||||
PRAutoBlogger_Editorial_Review $review,
|
||||
?string $run_id = null
|
||||
): int {
|
||||
return $this->create_post( $content, $idea, $review, 'draft', $run_id );
|
||||
@@ -67,8 +67,8 @@ class Autoblogger_Publisher {
|
||||
* Create a WordPress post with generation metadata.
|
||||
*
|
||||
* @param string $content HTML content.
|
||||
* @param Autoblogger_Article_Idea $idea Article idea.
|
||||
* @param Autoblogger_Editorial_Review $review Editorial review.
|
||||
* @param PRAutoBlogger_Article_Idea $idea Article idea.
|
||||
* @param PRAutoBlogger_Editorial_Review $review Editorial review.
|
||||
* @param string $post_status 'publish' or 'draft'.
|
||||
* @param string|null $run_id Pipeline run ID for log linking.
|
||||
*
|
||||
@@ -78,8 +78,8 @@ class Autoblogger_Publisher {
|
||||
*/
|
||||
private function create_post(
|
||||
string $content,
|
||||
Autoblogger_Article_Idea $idea,
|
||||
Autoblogger_Editorial_Review $review,
|
||||
PRAutoBlogger_Article_Idea $idea,
|
||||
PRAutoBlogger_Editorial_Review $review,
|
||||
string $post_status,
|
||||
?string $run_id = null
|
||||
): int {
|
||||
@@ -93,15 +93,15 @@ class Autoblogger_Publisher {
|
||||
];
|
||||
|
||||
/**
|
||||
* Fires before an AutoBlogger post is created.
|
||||
* Fires before an PRAutoBlogger post is created.
|
||||
*
|
||||
* Listeners registered in: class-autoblogger.php (main loader).
|
||||
* Listeners registered in: class-prautoblogger.php (main loader).
|
||||
*
|
||||
* @param array $post_data Post data array.
|
||||
* @param Autoblogger_Article_Idea $idea The article idea.
|
||||
* @param Autoblogger_Editorial_Review $review The editorial review.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The article idea.
|
||||
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
|
||||
*/
|
||||
$post_data = apply_filters( 'autoblogger_filter_post_data', $post_data, $idea, $review );
|
||||
$post_data = apply_filters( 'prautoblogger_filter_post_data', $post_data, $idea, $review );
|
||||
|
||||
$post_id = wp_insert_post( $post_data, true );
|
||||
|
||||
@@ -109,7 +109,7 @@ class Autoblogger_Publisher {
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %s: WordPress error message */
|
||||
__( 'Failed to create post: %s', 'autoblogger' ),
|
||||
__( 'Failed to create post: %s', 'prautoblogger' ),
|
||||
$post_id->get_error_message()
|
||||
)
|
||||
);
|
||||
@@ -121,20 +121,20 @@ class Autoblogger_Publisher {
|
||||
// Update generation log entries to reference this post.
|
||||
$this->link_generation_logs( $post_id, $run_id );
|
||||
|
||||
Autoblogger_Logger::instance()->info(
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Post created: ID=%d, status=%s, title="%s"', $post_id, $post_status, $idea->get_suggested_title() ),
|
||||
'publisher'
|
||||
);
|
||||
|
||||
/**
|
||||
* Fires after an AutoBlogger post is successfully created.
|
||||
* Fires after an PRAutoBlogger post is successfully created.
|
||||
*
|
||||
* @param int $post_id The new post ID.
|
||||
* @param string $post_status 'publish' or 'draft'.
|
||||
* @param Autoblogger_Article_Idea $idea The article idea.
|
||||
* @param Autoblogger_Editorial_Review $review The editorial review.
|
||||
* @param PRAutoBlogger_Article_Idea $idea The article idea.
|
||||
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
|
||||
*/
|
||||
do_action( 'autoblogger_post_created', $post_id, $post_status, $idea, $review );
|
||||
do_action( 'prautoblogger_post_created', $post_id, $post_status, $idea, $review );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
@@ -142,32 +142,32 @@ class Autoblogger_Publisher {
|
||||
/**
|
||||
* Build post_meta array for generation metadata.
|
||||
*
|
||||
* @param Autoblogger_Article_Idea $idea
|
||||
* @param Autoblogger_Editorial_Review $review
|
||||
* @param PRAutoBlogger_Article_Idea $idea
|
||||
* @param PRAutoBlogger_Editorial_Review $review
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function build_meta(
|
||||
Autoblogger_Article_Idea $idea,
|
||||
Autoblogger_Editorial_Review $review
|
||||
PRAutoBlogger_Article_Idea $idea,
|
||||
PRAutoBlogger_Editorial_Review $review
|
||||
): array {
|
||||
$pipeline_mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
|
||||
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
$pipeline_mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
|
||||
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
|
||||
|
||||
return [
|
||||
'_autoblogger_generated' => '1',
|
||||
'_autoblogger_analysis_id' => $idea->get_analysis_id(),
|
||||
'_autoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
|
||||
'_autoblogger_model_used' => $model,
|
||||
'_autoblogger_pipeline_mode' => $pipeline_mode,
|
||||
'_autoblogger_editor_verdict' => $review->get_verdict(),
|
||||
'_autoblogger_editor_notes' => $review->get_notes(),
|
||||
'_autoblogger_quality_score' => $review->get_quality_score(),
|
||||
'_autoblogger_seo_score' => $review->get_seo_score(),
|
||||
'_autoblogger_generated_at' => gmdate( 'c' ),
|
||||
'_autoblogger_topic' => $idea->get_topic(),
|
||||
'_autoblogger_article_type' => $idea->get_article_type(),
|
||||
'_autoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
|
||||
'_prautoblogger_generated' => '1',
|
||||
'_prautoblogger_analysis_id' => $idea->get_analysis_id(),
|
||||
'_prautoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
|
||||
'_prautoblogger_model_used' => $model,
|
||||
'_prautoblogger_pipeline_mode' => $pipeline_mode,
|
||||
'_prautoblogger_editor_verdict' => $review->get_verdict(),
|
||||
'_prautoblogger_editor_notes' => $review->get_notes(),
|
||||
'_prautoblogger_quality_score' => $review->get_quality_score(),
|
||||
'_prautoblogger_seo_score' => $review->get_seo_score(),
|
||||
'_prautoblogger_generated_at' => gmdate( 'c' ),
|
||||
'_prautoblogger_topic' => $idea->get_topic(),
|
||||
'_prautoblogger_article_type' => $idea->get_article_type(),
|
||||
'_prautoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -175,11 +175,11 @@ class Autoblogger_Publisher {
|
||||
* Assign category and tags to a generated post.
|
||||
*
|
||||
* @param int $post_id
|
||||
* @param Autoblogger_Article_Idea $idea
|
||||
* @param PRAutoBlogger_Article_Idea $idea
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function assign_taxonomy_terms( int $post_id, Autoblogger_Article_Idea $idea ): void {
|
||||
private function assign_taxonomy_terms( int $post_id, PRAutoBlogger_Article_Idea $idea ): void {
|
||||
$type_category_map = [
|
||||
'guide' => 'Guides',
|
||||
'solution' => 'Solutions',
|
||||
@@ -222,7 +222,7 @@ class Autoblogger_Publisher {
|
||||
*/
|
||||
private function link_generation_logs( int $post_id, ?string $run_id = null ): void {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_generation_log';
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
|
||||
if ( null !== $run_id && '' !== $run_id ) {
|
||||
// Precise linking via run_id — no risk of cross-article misattribution.
|
||||
@@ -253,7 +253,7 @@ class Autoblogger_Publisher {
|
||||
* @return int WordPress user ID.
|
||||
*/
|
||||
private function get_default_author_id(): int {
|
||||
$author_id = absint( get_option( 'autoblogger_default_author', 0 ) );
|
||||
$author_id = absint( get_option( 'prautoblogger_default_author', 0 ) );
|
||||
if ( $author_id > 0 ) {
|
||||
return $author_id;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ declare(strict_types=1);
|
||||
* collect_data() method, and stores results in the ab_source_data table.
|
||||
* Handles deduplication via UNIQUE index on (source_type, source_id).
|
||||
*
|
||||
* Triggered by: Autoblogger::run_generation_pipeline() (step 1).
|
||||
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 1).
|
||||
* Dependencies: Source provider implementations, WordPress $wpdb.
|
||||
*
|
||||
* @see providers/interface-source-provider.php — Interface providers implement.
|
||||
* @see providers/class-reddit-provider.php — Primary source provider.
|
||||
* @see ARCHITECTURE.md — Data flow step 1.
|
||||
*/
|
||||
class Autoblogger_Source_Collector {
|
||||
class PRAutoBlogger_Source_Collector {
|
||||
|
||||
/**
|
||||
* Collect data from all enabled source providers and store in database.
|
||||
@@ -25,7 +25,7 @@ class Autoblogger_Source_Collector {
|
||||
* @return int Total number of new records inserted.
|
||||
*/
|
||||
public function collect_from_all_sources(): int {
|
||||
$enabled = json_decode( get_option( 'autoblogger_enabled_sources', '["reddit"]' ), true );
|
||||
$enabled = json_decode( get_option( 'prautoblogger_enabled_sources', '["reddit"]' ), true );
|
||||
if ( ! is_array( $enabled ) ) {
|
||||
$enabled = [ 'reddit' ];
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class Autoblogger_Source_Collector {
|
||||
foreach ( $enabled as $source_type ) {
|
||||
$provider = $this->get_provider( $source_type );
|
||||
if ( null === $provider ) {
|
||||
Autoblogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
|
||||
PRAutoBlogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ class Autoblogger_Source_Collector {
|
||||
$inserted = $this->store_data( $data );
|
||||
$total_inserted += $inserted;
|
||||
|
||||
Autoblogger_Logger::instance()->info(
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Collected %d items from %s (%d new).', count( $data ), $source_type, $inserted ),
|
||||
'collector'
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
Autoblogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
|
||||
PRAutoBlogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
|
||||
// Continue with other sources — one failure shouldn't stop everything.
|
||||
}
|
||||
}
|
||||
@@ -64,13 +64,13 @@ class Autoblogger_Source_Collector {
|
||||
*
|
||||
* Side effects: database inserts.
|
||||
*
|
||||
* @param Autoblogger_Source_Data[] $items
|
||||
* @param PRAutoBlogger_Source_Data[] $items
|
||||
*
|
||||
* @return int Number of new records inserted.
|
||||
*/
|
||||
private function store_data( array $items ): int {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'autoblogger_source_data';
|
||||
$table = $wpdb->prefix . 'prautoblogger_source_data';
|
||||
$inserted = 0;
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
@@ -110,11 +110,11 @@ class Autoblogger_Source_Collector {
|
||||
*
|
||||
* @param string $source_type Source type identifier.
|
||||
*
|
||||
* @return Autoblogger_Source_Provider_Interface|null
|
||||
* @return PRAutoBlogger_Source_Provider_Interface|null
|
||||
*/
|
||||
private function get_provider( string $source_type ): ?Autoblogger_Source_Provider_Interface {
|
||||
private function get_provider( string $source_type ): ?PRAutoBlogger_Source_Provider_Interface {
|
||||
$providers = [
|
||||
'reddit' => Autoblogger_Reddit_Provider::class,
|
||||
'reddit' => PRAutoBlogger_Reddit_Provider::class,
|
||||
// Future providers: 'tiktok', 'instagram', 'youtube'
|
||||
];
|
||||
|
||||
@@ -123,7 +123,7 @@ class Autoblogger_Source_Collector {
|
||||
*
|
||||
* @param array<string, string> $providers Map of source_type => class name.
|
||||
*/
|
||||
$providers = apply_filters( 'autoblogger_filter_source_providers', $providers );
|
||||
$providers = apply_filters( 'prautoblogger_filter_source_providers', $providers );
|
||||
|
||||
if ( ! isset( $providers[ $source_type ] ) ) {
|
||||
return null;
|
||||
@@ -134,7 +134,7 @@ class Autoblogger_Source_Collector {
|
||||
// Guard against unloadable provider classes (e.g. stub providers not yet
|
||||
// autoloadable, or classes removed during development).
|
||||
if ( ! class_exists( $class ) ) {
|
||||
Autoblogger_Logger::instance()->error(
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'Provider class "%s" for source type "%s" does not exist.', $class, $source_type ),
|
||||
'collector'
|
||||
);
|
||||
@@ -154,7 +154,7 @@ class Autoblogger_Source_Collector {
|
||||
private function get_config_for_source( string $source_type ): array {
|
||||
if ( 'reddit' === $source_type ) {
|
||||
return [
|
||||
'subreddits' => json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true ) ?: [],
|
||||
'subreddits' => json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true ) ?: [],
|
||||
'limit' => 25,
|
||||
'time_filter' => 'day',
|
||||
'include_comments' => true,
|
||||
|
||||
256
includes/frontend/class-posts-widget.php
Normal file
256
includes/frontend/class-posts-widget.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Registers the [prautoblogger_posts] shortcode and its supporting REST API endpoint.
|
||||
*
|
||||
* This is PRAutoBlogger's first public-facing frontend feature. It renders a React-powered
|
||||
* card grid of generated posts, fetching data asynchronously from a lightweight REST
|
||||
* endpoint to keep the initial page load fast.
|
||||
*
|
||||
* Triggered by: Main orchestrator (class-prautoblogger.php) on `init` and `rest_api_init`.
|
||||
* Dependencies: wp.element (WordPress-bundled React), posts-widget.js, posts-widget.css.
|
||||
*
|
||||
* @see class-prautoblogger.php — Hook registration for this class.
|
||||
* @see assets/js/posts-widget.js — React component that renders the cards.
|
||||
* @see assets/css/posts-widget.css — Frontend styles matching the Peptide Repo theme.
|
||||
* @see ARCHITECTURE.md — Data flow for frontend widget.
|
||||
*/
|
||||
class PRAutoBlogger_Posts_Widget {
|
||||
|
||||
/**
|
||||
* REST API namespace for the widget endpoint.
|
||||
*/
|
||||
private const REST_NAMESPACE = 'prautoblogger/v1';
|
||||
|
||||
/**
|
||||
* REST API route for fetching posts.
|
||||
*/
|
||||
private const REST_ROUTE = '/posts';
|
||||
|
||||
/**
|
||||
* Maximum posts allowed per request (hard cap for safety).
|
||||
*/
|
||||
private const MAX_POSTS_PER_REQUEST = 12;
|
||||
|
||||
/**
|
||||
* Register the [prautoblogger_posts] shortcode.
|
||||
*
|
||||
* Called on `init` by the main orchestrator.
|
||||
*
|
||||
* Side effects: registers a WordPress shortcode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_shortcode(): void {
|
||||
add_shortcode( 'prautoblogger_posts', [ $this, 'render_shortcode' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST API endpoint for fetching posts.
|
||||
*
|
||||
* Called on `rest_api_init` by the main orchestrator.
|
||||
*
|
||||
* Side effects: registers a WordPress REST route.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_rest_route(): void {
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
self::REST_ROUTE,
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'handle_rest_request' ],
|
||||
'permission_callback' => function () {
|
||||
/**
|
||||
* Filter whether the posts widget REST endpoint is publicly accessible.
|
||||
*
|
||||
* By default, this is true — the endpoint only serves published posts.
|
||||
* Return false to require authentication (e.g., for private/staging sites).
|
||||
*
|
||||
* @param bool $is_public Whether the endpoint is publicly accessible.
|
||||
*/
|
||||
return (bool) apply_filters( 'prautoblogger_rest_posts_public', true );
|
||||
},
|
||||
'args' => [
|
||||
'per_page' => [
|
||||
'type' => 'integer',
|
||||
'default' => 6,
|
||||
'minimum' => 1,
|
||||
'maximum' => self::MAX_POSTS_PER_REQUEST,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
'category' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the shortcode output.
|
||||
*
|
||||
* Outputs a mount-point div and enqueues the React script + CSS.
|
||||
* The React component hydrates the div on the client side.
|
||||
*
|
||||
* Shortcode attributes:
|
||||
* count — Number of posts to display (default 6, max 12).
|
||||
* category — Filter by category slug (default empty = all).
|
||||
* title — Widget heading (default "Latest Research & Insights").
|
||||
* subtitle — Widget subheading.
|
||||
*
|
||||
* @param array<string, string>|string $atts Shortcode attributes.
|
||||
*
|
||||
* @return string HTML mount-point div.
|
||||
*/
|
||||
public function render_shortcode( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
[
|
||||
'count' => '6',
|
||||
'category' => '',
|
||||
'title' => __( 'Latest Research & Insights', 'prautoblogger' ),
|
||||
'subtitle' => __( 'Evidence-based articles on peptides, protocols, and emerging research', 'prautoblogger' ),
|
||||
],
|
||||
$atts,
|
||||
'prautoblogger_posts'
|
||||
);
|
||||
|
||||
$count = min( absint( $atts['count'] ), self::MAX_POSTS_PER_REQUEST );
|
||||
if ( $count < 1 ) {
|
||||
$count = 6;
|
||||
}
|
||||
|
||||
$this->enqueue_frontend_assets( $count, $atts );
|
||||
|
||||
return '<div id="prab-posts-root"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend CSS and JS for the widget.
|
||||
*
|
||||
* Uses wp.element (WordPress-bundled React) as a dependency — no build step needed.
|
||||
* Passes configuration to JS via wp_localize_script.
|
||||
*
|
||||
* Side effects: enqueues styles and scripts.
|
||||
*
|
||||
* @param int $count Number of posts to fetch.
|
||||
* @param array<string, string> $atts Shortcode attributes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function enqueue_frontend_assets( int $count, array $atts ): void {
|
||||
$version = defined( 'PRAUTOBLOGGER_VERSION' ) ? PRAUTOBLOGGER_VERSION : '0.1.0';
|
||||
|
||||
wp_enqueue_style(
|
||||
'prautoblogger-posts-widget',
|
||||
PRAUTOBLOGGER_PLUGIN_URL . 'assets/css/posts-widget.css',
|
||||
[],
|
||||
$version
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'prautoblogger-posts-widget',
|
||||
PRAUTOBLOGGER_PLUGIN_URL . 'assets/js/posts-widget.js',
|
||||
[ 'wp-element' ], // WordPress-bundled React.
|
||||
$version,
|
||||
true // Load in footer for performance.
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'prautoblogger-posts-widget',
|
||||
'prabPostsWidget',
|
||||
[
|
||||
'restUrl' => esc_url_raw( rest_url( self::REST_NAMESPACE . self::REST_ROUTE ) ),
|
||||
'count' => $count,
|
||||
'category' => sanitize_text_field( $atts['category'] ),
|
||||
'title' => sanitize_text_field( $atts['title'] ),
|
||||
'subtitle' => sanitize_text_field( $atts['subtitle'] ),
|
||||
'archiveUrl' => esc_url( get_post_type_archive_link( 'post' ) ?: '' ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the REST API request for posts.
|
||||
*
|
||||
* Returns a slim JSON array of published posts generated by PRAutoBlogger.
|
||||
* Each post includes only the fields the frontend needs — no unnecessary data.
|
||||
*
|
||||
* @param \WP_REST_Request $request The incoming REST request.
|
||||
*
|
||||
* @return \WP_REST_Response JSON response with post data.
|
||||
*/
|
||||
public function handle_rest_request( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$per_page = min( absint( $request->get_param( 'per_page' ) ), self::MAX_POSTS_PER_REQUEST );
|
||||
if ( $per_page < 1 ) {
|
||||
$per_page = 6;
|
||||
}
|
||||
$category = sanitize_text_field( $request->get_param( 'category' ) ?? '' );
|
||||
|
||||
$query_args = [
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $per_page,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => '_prautoblogger_generated',
|
||||
'value' => '1',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Optional category filter.
|
||||
if ( ! empty( $category ) ) {
|
||||
$query_args['category_name'] = $category;
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = [];
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
$posts[] = $this->format_post_for_response( $post );
|
||||
}
|
||||
|
||||
$response = new \WP_REST_Response( $posts, 200 );
|
||||
|
||||
// Cache for 5 minutes — posts don't change frequently.
|
||||
$response->header( 'Cache-Control', 'public, max-age=300' );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single post for the REST API response.
|
||||
*
|
||||
* Returns only the fields the React component needs to render a card.
|
||||
*
|
||||
* @param \WP_Post $post WordPress post object.
|
||||
*
|
||||
* @return array<string, mixed> Formatted post data.
|
||||
*/
|
||||
private function format_post_for_response( \WP_Post $post ): array {
|
||||
$categories = get_the_category( $post->ID );
|
||||
$category_name = ! empty( $categories ) ? $categories[0]->name : '';
|
||||
$category_slug = ! empty( $categories ) ? $categories[0]->slug : '';
|
||||
$featured_image = get_the_post_thumbnail_url( $post->ID, 'medium_large' );
|
||||
$word_count = str_word_count( wp_strip_all_tags( $post->post_content ) );
|
||||
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'title' => get_the_title( $post ),
|
||||
'excerpt' => get_the_excerpt( $post ),
|
||||
'url' => get_permalink( $post ),
|
||||
'date' => get_the_date( 'c', $post ),
|
||||
'category_name' => $category_name,
|
||||
'category_slug' => $category_slug,
|
||||
'featured_image' => $featured_image ?: '',
|
||||
'word_count' => $word_count,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
* @see core/class-content-analyzer.php — Creates these from source data + LLM analysis.
|
||||
* @see core/class-idea-scorer.php — Ranks these into article ideas.
|
||||
*/
|
||||
class Autoblogger_Analysis_Result {
|
||||
class PRAutoBlogger_Analysis_Result {
|
||||
|
||||
private int $id;
|
||||
private string $analysis_type;
|
||||
|
||||
@@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
* @see core/class-idea-scorer.php — Creates and ranks these.
|
||||
* @see core/class-content-generator.php — Uses these as generation input.
|
||||
*/
|
||||
class Autoblogger_Article_Idea {
|
||||
class PRAutoBlogger_Article_Idea {
|
||||
|
||||
private string $topic;
|
||||
private string $article_type;
|
||||
|
||||
@@ -12,9 +12,9 @@ declare(strict_types=1);
|
||||
*
|
||||
* @see core/class-content-generator.php — Creates and consumes these.
|
||||
*/
|
||||
class Autoblogger_Content_Request {
|
||||
class PRAutoBlogger_Content_Request {
|
||||
|
||||
private Autoblogger_Article_Idea $idea;
|
||||
private PRAutoBlogger_Article_Idea $idea;
|
||||
private string $pipeline_mode;
|
||||
private string $tone;
|
||||
private int $min_word_count;
|
||||
@@ -23,7 +23,7 @@ class Autoblogger_Content_Request {
|
||||
private array $topic_exclusions;
|
||||
|
||||
public function __construct(
|
||||
Autoblogger_Article_Idea $idea,
|
||||
PRAutoBlogger_Article_Idea $idea,
|
||||
string $pipeline_mode,
|
||||
string $tone,
|
||||
int $min_word_count,
|
||||
@@ -40,7 +40,7 @@ class Autoblogger_Content_Request {
|
||||
$this->topic_exclusions = $topic_exclusions;
|
||||
}
|
||||
|
||||
public function get_idea(): Autoblogger_Article_Idea {
|
||||
public function get_idea(): PRAutoBlogger_Article_Idea {
|
||||
return $this->idea;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
* @see core/class-metrics-collector.php — Creates these from WP + GA4 data.
|
||||
* @see core/class-content-analyzer.php — Reads past scores to refine analysis.
|
||||
*/
|
||||
class Autoblogger_Content_Score {
|
||||
class PRAutoBlogger_Content_Score {
|
||||
|
||||
private int $id;
|
||||
private int $post_id;
|
||||
|
||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
* @see core/class-chief-editor.php — Creates these after reviewing content.
|
||||
* @see core/class-publisher.php — Uses verdict to decide publish vs. draft.
|
||||
*/
|
||||
class Autoblogger_Editorial_Review {
|
||||
class PRAutoBlogger_Editorial_Review {
|
||||
|
||||
private string $verdict;
|
||||
private string $notes;
|
||||
|
||||
@@ -7,10 +7,10 @@ declare(strict_types=1);
|
||||
* Triggered by: Cost_Tracker creates these; Metrics_Page displays them.
|
||||
* Dependencies: None — pure data object.
|
||||
*
|
||||
* @see core/class-cost-tracker.php — Writes these to ab_generation_log table.
|
||||
* @see core/class-cost-tracker.php — Writes these to prab_generation_log table.
|
||||
* @see admin/class-metrics-page.php — Reads and displays these.
|
||||
*/
|
||||
class Autoblogger_Generation_Log {
|
||||
class PRAutoBlogger_Generation_Log {
|
||||
|
||||
private int $id;
|
||||
private ?int $post_id;
|
||||
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
* @see providers/interface-source-provider.php — Providers return arrays of these.
|
||||
* @see core/class-content-analyzer.php — Consumes these for pattern detection.
|
||||
*/
|
||||
class Autoblogger_Source_Data {
|
||||
class PRAutoBlogger_Source_Data {
|
||||
|
||||
private int $id;
|
||||
private string $source_type;
|
||||
|
||||
@@ -12,16 +12,16 @@ declare(strict_types=1);
|
||||
*
|
||||
* @see interface-source-provider.php — Interface this class implements.
|
||||
*/
|
||||
class Autoblogger_Instagram_Provider implements Autoblogger_Source_Provider_Interface {
|
||||
class PRAutoBlogger_Instagram_Provider implements PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @return Autoblogger_Source_Data[]
|
||||
* @return PRAutoBlogger_Source_Data[]
|
||||
* @throws \RuntimeException Always — not yet implemented.
|
||||
*/
|
||||
public function collect_data( array $config ): array {
|
||||
throw new \RuntimeException(
|
||||
__( 'Instagram source provider is not yet implemented. Remove "instagram" from enabled sources.', 'autoblogger' )
|
||||
__( 'Instagram source provider is not yet implemented. Remove "instagram" from enabled sources.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ declare(strict_types=1);
|
||||
* Manages pricing tables, model lists, and cost estimation. Decoupled from
|
||||
* the request logic for easy updates and testing.
|
||||
*
|
||||
* Triggered by: Autoblogger_OpenRouter_Provider calls pricing methods.
|
||||
* Triggered by: PRAutoBlogger_OpenRouter_Provider calls pricing methods.
|
||||
* Dependencies: None — pure data + calculation, no external calls by default.
|
||||
*
|
||||
* @see providers/class-openrouter-provider.php — Instantiates this class.
|
||||
* @see ARCHITECTURE.md — Model selection flow.
|
||||
*/
|
||||
class Autoblogger_OpenRouter_Pricing {
|
||||
class PRAutoBlogger_OpenRouter_Pricing {
|
||||
|
||||
/**
|
||||
* Known model pricing (per million tokens, in USD).
|
||||
@@ -41,7 +41,7 @@ class Autoblogger_OpenRouter_Pricing {
|
||||
* @return array<int, array{id: string, name: string, context_length: int, pricing: array{prompt: float, completion: float}}>
|
||||
*/
|
||||
public function get_available_models(): array {
|
||||
$cached = get_transient( 'autoblogger_openrouter_models' );
|
||||
$cached = get_transient( 'prautoblogger_openrouter_models' );
|
||||
if ( false !== $cached && is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class Autoblogger_OpenRouter_Pricing {
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
Autoblogger_Logger::instance()->warning( 'Failed to fetch OpenRouter models list.', 'openrouter' );
|
||||
PRAutoBlogger_Logger::instance()->warning( 'Failed to fetch OpenRouter models list.', 'openrouter' );
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class Autoblogger_OpenRouter_Pricing {
|
||||
}
|
||||
}
|
||||
|
||||
set_transient( 'autoblogger_openrouter_models', $models, DAY_IN_SECONDS );
|
||||
set_transient( 'prautoblogger_openrouter_models', $models, DAY_IN_SECONDS );
|
||||
return $models;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class Autoblogger_OpenRouter_Pricing {
|
||||
|
||||
if ( null === $pricing ) {
|
||||
// Unknown model — use a conservative estimate.
|
||||
Autoblogger_Logger::instance()->warning( "Unknown model pricing for '{$model}'. Using conservative estimate.", 'openrouter' );
|
||||
PRAutoBlogger_Logger::instance()->warning( "Unknown model pricing for '{$model}'. Using conservative estimate.", 'openrouter' );
|
||||
$pricing = [ 'prompt' => 10.0, 'completion' => 30.0 ];
|
||||
}
|
||||
|
||||
@@ -128,10 +128,10 @@ class Autoblogger_OpenRouter_Pricing {
|
||||
* @return string Decrypted API key, or empty string if not set.
|
||||
*/
|
||||
private function get_api_key(): string {
|
||||
$encrypted = get_option( 'autoblogger_openrouter_api_key', '' );
|
||||
$encrypted = get_option( 'prautoblogger_openrouter_api_key', '' );
|
||||
if ( '' === $encrypted ) {
|
||||
return '';
|
||||
}
|
||||
return Autoblogger_Encryption::decrypt( $encrypted );
|
||||
return PRAutoBlogger_Encryption::decrypt( $encrypted );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ declare(strict_types=1);
|
||||
* separate provider integrations.
|
||||
*
|
||||
* Triggered by: Content_Analyzer, Content_Generator, Chief_Editor, Metrics_Collector.
|
||||
* Dependencies: Autoblogger_Encryption (for API key decryption), wp_remote_post(), Autoblogger_OpenRouter_Pricing.
|
||||
* Dependencies: PRAutoBlogger_Encryption (for API key decryption), wp_remote_post(), PRAutoBlogger_OpenRouter_Pricing.
|
||||
*
|
||||
* @see interface-llm-provider.php — Interface this class implements.
|
||||
* @see class-openrouter-pricing.php — Pricing and model list lookups.
|
||||
* @see class-cost-tracker.php — Called after every request to log token usage.
|
||||
* @see ARCHITECTURE.md — Data flow diagram showing where this fits.
|
||||
*/
|
||||
class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interface {
|
||||
class PRAutoBlogger_OpenRouter_Provider implements PRAutoBlogger_LLM_Provider_Interface {
|
||||
|
||||
private const API_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||
|
||||
@@ -51,7 +51,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
$api_key = $this->get_api_key();
|
||||
if ( '' === $api_key ) {
|
||||
throw new \RuntimeException(
|
||||
__( 'OpenRouter API key is not configured. Go to AutoBlogger → Settings.', 'autoblogger' )
|
||||
__( 'OpenRouter API key is not configured. Go to PRAutoBlogger → Settings.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,16 +72,16 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
|
||||
$last_error = '';
|
||||
|
||||
for ( $attempt = 1; $attempt <= AUTOBLOGGER_MAX_RETRIES; $attempt++ ) {
|
||||
for ( $attempt = 1; $attempt <= PRAUTOBLOGGER_MAX_RETRIES; $attempt++ ) {
|
||||
$response = wp_remote_post(
|
||||
self::API_BASE_URL . '/chat/completions',
|
||||
[
|
||||
'timeout' => AUTOBLOGGER_API_TIMEOUT_SECONDS,
|
||||
'timeout' => PRAUTOBLOGGER_API_TIMEOUT_SECONDS,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
'HTTP-Referer' => home_url(),
|
||||
'X-Title' => 'AutoBlogger WordPress Plugin',
|
||||
'X-Title' => 'PRAutoBlogger WordPress Plugin',
|
||||
],
|
||||
'body' => wp_json_encode( $body ),
|
||||
]
|
||||
@@ -89,13 +89,13 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$last_error = $response->get_error_message();
|
||||
Autoblogger_Logger::instance()->warning(
|
||||
sprintf( 'OpenRouter request failed (attempt %d/%d): %s', $attempt, AUTOBLOGGER_MAX_RETRIES, $last_error ),
|
||||
PRAutoBlogger_Logger::instance()->warning(
|
||||
sprintf( 'OpenRouter request failed (attempt %d/%d): %s', $attempt, PRAUTOBLOGGER_MAX_RETRIES, $last_error ),
|
||||
'openrouter'
|
||||
);
|
||||
|
||||
if ( $attempt < AUTOBLOGGER_MAX_RETRIES ) {
|
||||
$delay = AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
|
||||
if ( $attempt < PRAUTOBLOGGER_MAX_RETRIES ) {
|
||||
$delay = PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
|
||||
sleep( (int) $delay );
|
||||
}
|
||||
continue;
|
||||
@@ -108,17 +108,17 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
// Rate limited or server error — retry.
|
||||
if ( $status_code >= 429 || $status_code >= 500 ) {
|
||||
$last_error = sprintf( 'HTTP %d: %s', $status_code, $body_raw );
|
||||
Autoblogger_Logger::instance()->warning(
|
||||
sprintf( 'OpenRouter HTTP %d (attempt %d/%d): %s', $status_code, $attempt, AUTOBLOGGER_MAX_RETRIES, substr( $body_raw, 0, 500 ) ),
|
||||
PRAutoBlogger_Logger::instance()->warning(
|
||||
sprintf( 'OpenRouter HTTP %d (attempt %d/%d): %s', $status_code, $attempt, PRAUTOBLOGGER_MAX_RETRIES, substr( $body_raw, 0, 500 ) ),
|
||||
'openrouter'
|
||||
);
|
||||
|
||||
if ( $attempt < AUTOBLOGGER_MAX_RETRIES ) {
|
||||
if ( $attempt < PRAUTOBLOGGER_MAX_RETRIES ) {
|
||||
// Respect Retry-After header if present.
|
||||
$retry_after = wp_remote_retrieve_header( $response, 'retry-after' );
|
||||
$delay = $retry_after
|
||||
? min( (int) $retry_after, 60 )
|
||||
: AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
|
||||
: PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS * pow( 2, $attempt - 1 );
|
||||
sleep( (int) $delay );
|
||||
}
|
||||
continue;
|
||||
@@ -132,7 +132,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code, %s: error message */
|
||||
__( 'OpenRouter API error (HTTP %1$d): %2$s', 'autoblogger' ),
|
||||
__( 'OpenRouter API error (HTTP %1$d): %2$s', 'prautoblogger' ),
|
||||
$status_code,
|
||||
$error_msg
|
||||
)
|
||||
@@ -142,7 +142,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
// Success — parse response.
|
||||
if ( ! isset( $data['choices'][0]['message']['content'] ) ) {
|
||||
throw new \RuntimeException(
|
||||
__( 'OpenRouter returned unexpected response format.', 'autoblogger' )
|
||||
__( 'OpenRouter returned unexpected response format.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %d: max retries, %s: last error message */
|
||||
__( 'OpenRouter API failed after %1$d attempts. Last error: %2$s', 'autoblogger' ),
|
||||
AUTOBLOGGER_MAX_RETRIES,
|
||||
__( 'OpenRouter API failed after %1$d attempts. Last error: %2$s', 'prautoblogger' ),
|
||||
PRAUTOBLOGGER_MAX_RETRIES,
|
||||
$last_error
|
||||
)
|
||||
);
|
||||
@@ -179,7 +179,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
* @return array<int, array{id: string, name: string, context_length: int, pricing: array{prompt: float, completion: float}}>
|
||||
*/
|
||||
public function get_available_models(): array {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
return $pricing->get_available_models();
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
* @return float Estimated cost in USD.
|
||||
*/
|
||||
public function estimate_cost( string $model, int $prompt_tokens, int $completion_tokens ): float {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
return $pricing->estimate_cost( $model, $prompt_tokens, $completion_tokens );
|
||||
}
|
||||
|
||||
@@ -244,10 +244,10 @@ class Autoblogger_OpenRouter_Provider implements Autoblogger_LLM_Provider_Interf
|
||||
* @return string Decrypted API key, or empty string if not set.
|
||||
*/
|
||||
private function get_api_key(): string {
|
||||
$encrypted = get_option( 'autoblogger_openrouter_api_key', '' );
|
||||
$encrypted = get_option( 'prautoblogger_openrouter_api_key', '' );
|
||||
if ( '' === $encrypted ) {
|
||||
return '';
|
||||
}
|
||||
return Autoblogger_Encryption::decrypt( $encrypted );
|
||||
return PRAutoBlogger_Encryption::decrypt( $encrypted );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ declare(strict_types=1);
|
||||
* Encapsulates OAuth2 token management, rate limit tracking, and HTTP methods
|
||||
* for the Reddit API. Decoupled from collection logic for testability.
|
||||
*
|
||||
* Triggered by: Autoblogger_Reddit_Provider calls fetch methods here.
|
||||
* Dependencies: Autoblogger_Encryption (for client secret), wp_remote_post/get().
|
||||
* Triggered by: PRAutoBlogger_Reddit_Provider calls fetch methods here.
|
||||
* Dependencies: PRAutoBlogger_Encryption (for client secret), wp_remote_post/get().
|
||||
*
|
||||
* @see providers/class-reddit-provider.php — Instantiates this class.
|
||||
* @see ARCHITECTURE.md — External API integrations table.
|
||||
*/
|
||||
class Autoblogger_Reddit_API_Client {
|
||||
class PRAutoBlogger_Reddit_API_Client {
|
||||
|
||||
private const AUTH_URL = 'https://www.reddit.com/api/v1/access_token';
|
||||
private const API_BASE = 'https://oauth.reddit.com';
|
||||
private const USER_AGENT = 'AutoBlogger/0.1.0 (WordPress Plugin)';
|
||||
private const USER_AGENT = 'PRAutoBlogger/0.1.0 (WordPress Plugin)';
|
||||
|
||||
/**
|
||||
* Get an OAuth2 access token, using cached transient if available.
|
||||
@@ -32,15 +32,15 @@ class Autoblogger_Reddit_API_Client {
|
||||
*/
|
||||
public function get_access_token( bool $force_refresh = false ): string {
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_transient( 'autoblogger_reddit_token' );
|
||||
$cached = get_transient( 'prautoblogger_reddit_token' );
|
||||
if ( false !== $cached && is_string( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$client_id = get_option( 'autoblogger_reddit_client_id', '' );
|
||||
$client_secret = Autoblogger_Encryption::decrypt(
|
||||
get_option( 'autoblogger_reddit_client_secret', '' )
|
||||
$client_id = get_option( 'prautoblogger_reddit_client_id', '' );
|
||||
$client_secret = PRAutoBlogger_Encryption::decrypt(
|
||||
get_option( 'prautoblogger_reddit_client_secret', '' )
|
||||
);
|
||||
|
||||
if ( '' === $client_id || '' === $client_secret ) {
|
||||
@@ -63,25 +63,25 @@ class Autoblogger_Reddit_API_Client {
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Reddit OAuth failed: ' . $response->get_error_message(), 'reddit' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth failed: ' . $response->get_error_message(), 'reddit' );
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Reddit OAuth HTTP ' . wp_remote_retrieve_response_code( $response ), 'reddit' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth HTTP ' . wp_remote_retrieve_response_code( $response ), 'reddit' );
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( ! isset( $data['access_token'] ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Reddit OAuth response missing access_token.', 'reddit' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Reddit OAuth response missing access_token.', 'reddit' );
|
||||
return '';
|
||||
}
|
||||
|
||||
$token = $data['access_token'];
|
||||
$expires = isset( $data['expires_in'] ) ? (int) $data['expires_in'] - 60 : 3540;
|
||||
|
||||
set_transient( 'autoblogger_reddit_token', $token, $expires );
|
||||
set_transient( 'prautoblogger_reddit_token', $token, $expires );
|
||||
|
||||
return $token;
|
||||
}
|
||||
@@ -179,7 +179,7 @@ class Autoblogger_Reddit_API_Client {
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Autoblogger_Logger::instance()->error( 'Reddit API GET failed: ' . $response->get_error_message(), 'reddit' );
|
||||
PRAutoBlogger_Logger::instance()->error( 'Reddit API GET failed: ' . $response->get_error_message(), 'reddit' );
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ class Autoblogger_Reddit_API_Client {
|
||||
$rate_reset = wp_remote_retrieve_header( $response, 'x-ratelimit-reset' );
|
||||
|
||||
if ( '' !== $rate_remaining ) {
|
||||
set_transient( 'autoblogger_reddit_rate_limit', [
|
||||
set_transient( 'prautoblogger_reddit_rate_limit', [
|
||||
'remaining' => (int) $rate_remaining,
|
||||
'limit' => (int) $rate_limit + (int) $rate_remaining,
|
||||
'resets_at' => gmdate( 'Y-m-d H:i:s', time() + (int) $rate_reset ),
|
||||
@@ -198,7 +198,7 @@ class Autoblogger_Reddit_API_Client {
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( 200 !== $status_code ) {
|
||||
Autoblogger_Logger::instance()->error( sprintf( 'Reddit API HTTP %d: %s', $status_code, substr( wp_remote_retrieve_body( $response ), 0, 300 ) ), 'reddit' );
|
||||
PRAutoBlogger_Logger::instance()->error( sprintf( 'Reddit API HTTP %d: %s', $status_code, substr( wp_remote_retrieve_body( $response ), 0, 300 ) ), 'reddit' );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ declare(strict_types=1);
|
||||
* storing them in the ab_source_data table.
|
||||
*
|
||||
* Triggered by: Source_Collector calls collect_data() on each enabled provider.
|
||||
* Dependencies: Autoblogger_Reddit_API_Client (for HTTP calls and auth).
|
||||
* Dependencies: PRAutoBlogger_Reddit_API_Client (for HTTP calls and auth).
|
||||
*
|
||||
* @see interface-source-provider.php — Interface this class implements.
|
||||
* @see providers/class-reddit-api-client.php — HTTP client for Reddit API.
|
||||
* @see core/class-source-collector.php — Orchestrates collection from all providers.
|
||||
* @see ARCHITECTURE.md — External API integrations table.
|
||||
*/
|
||||
class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interface {
|
||||
class PRAutoBlogger_Reddit_Provider implements PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
/**
|
||||
* Collect posts and comments from configured subreddits.
|
||||
@@ -34,16 +34,16 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
* comments_per_post?: int,
|
||||
* } $config Collection configuration.
|
||||
*
|
||||
* @return Autoblogger_Source_Data[]
|
||||
* @return PRAutoBlogger_Source_Data[]
|
||||
*
|
||||
* @throws \RuntimeException On authentication failure or API error.
|
||||
*/
|
||||
public function collect_data( array $config ): array {
|
||||
$api_client = new Autoblogger_Reddit_API_Client();
|
||||
$api_client = new PRAutoBlogger_Reddit_API_Client();
|
||||
$token = $api_client->get_access_token();
|
||||
if ( '' === $token ) {
|
||||
throw new \RuntimeException(
|
||||
__( 'Reddit authentication failed. Check client ID and secret in settings.', 'autoblogger' )
|
||||
__( 'Reddit authentication failed. Check client ID and secret in settings.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
$posts = $api_client->fetch_posts( $token, $subreddit, $limit, $time_filter );
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$collected[] = new Autoblogger_Source_Data( [
|
||||
$collected[] = new PRAutoBlogger_Source_Data( [
|
||||
'source_type' => 'reddit',
|
||||
'source_id' => 't3_' . $post['id'],
|
||||
'subreddit' => $subreddit,
|
||||
@@ -92,7 +92,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
);
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
$collected[] = new Autoblogger_Source_Data( [
|
||||
$collected[] = new PRAutoBlogger_Source_Data( [
|
||||
'source_type' => 'reddit',
|
||||
'source_id' => 't1_' . $comment['id'],
|
||||
'subreddit' => $subreddit,
|
||||
@@ -115,7 +115,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
// Log but continue with other subreddits.
|
||||
Autoblogger_Logger::instance()->error(
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'Reddit: Failed to collect from r/%s: %s', $subreddit, $e->getMessage() ),
|
||||
'reddit'
|
||||
);
|
||||
@@ -140,7 +140,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_credentials(): bool {
|
||||
$api_client = new Autoblogger_Reddit_API_Client();
|
||||
$api_client = new PRAutoBlogger_Reddit_API_Client();
|
||||
$token = $api_client->get_access_token( true );
|
||||
return '' !== $token;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
public function get_rate_limit_status(): array {
|
||||
// Reddit rate limits are tracked per-response via headers.
|
||||
// We store the last seen values in a transient.
|
||||
$status = get_transient( 'autoblogger_reddit_rate_limit' );
|
||||
$status = get_transient( 'prautoblogger_reddit_rate_limit' );
|
||||
if ( is_array( $status ) ) {
|
||||
return $status;
|
||||
}
|
||||
@@ -171,7 +171,7 @@ class Autoblogger_Reddit_Provider implements Autoblogger_Source_Provider_Interfa
|
||||
* @return string[]
|
||||
*/
|
||||
private function get_configured_subreddits(): array {
|
||||
$json = get_option( 'autoblogger_target_subreddits', '[]' );
|
||||
$json = get_option( 'prautoblogger_target_subreddits', '[]' );
|
||||
$list = json_decode( $json, true );
|
||||
return is_array( $list ) ? $list : [];
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ declare(strict_types=1);
|
||||
* @see interface-source-provider.php — Interface this class implements.
|
||||
* @see ARCHITECTURE.md — Planned source providers.
|
||||
*/
|
||||
class Autoblogger_Tiktok_Provider implements Autoblogger_Source_Provider_Interface {
|
||||
class PRAutoBlogger_Tiktok_Provider implements PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @return Autoblogger_Source_Data[]
|
||||
* @return PRAutoBlogger_Source_Data[]
|
||||
* @throws \RuntimeException Always — not yet implemented.
|
||||
*/
|
||||
public function collect_data( array $config ): array {
|
||||
throw new \RuntimeException(
|
||||
__( 'TikTok source provider is not yet implemented. Remove "tiktok" from enabled sources.', 'autoblogger' )
|
||||
__( 'TikTok source provider is not yet implemented. Remove "tiktok" from enabled sources.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,16 @@ declare(strict_types=1);
|
||||
*
|
||||
* @see interface-source-provider.php — Interface this class implements.
|
||||
*/
|
||||
class Autoblogger_Youtube_Provider implements Autoblogger_Source_Provider_Interface {
|
||||
class PRAutoBlogger_Youtube_Provider implements PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @return Autoblogger_Source_Data[]
|
||||
* @return PRAutoBlogger_Source_Data[]
|
||||
* @throws \RuntimeException Always — not yet implemented.
|
||||
*/
|
||||
public function collect_data( array $config ): array {
|
||||
throw new \RuntimeException(
|
||||
__( 'YouTube source provider is not yet implemented. Remove "youtube" from enabled sources.', 'autoblogger' )
|
||||
__( 'YouTube source provider is not yet implemented. Remove "youtube" from enabled sources.', 'prautoblogger' )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
* @see CONVENTIONS.md — "How To: Add a New LLM Provider".
|
||||
* @see ARCHITECTURE.md — External API integrations table.
|
||||
*/
|
||||
interface Autoblogger_LLM_Provider_Interface {
|
||||
interface PRAutoBlogger_LLM_Provider_Interface {
|
||||
|
||||
/**
|
||||
* Send a chat completion request to the LLM.
|
||||
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
* @see core/class-source-collector.php — Orchestrates collection from all providers.
|
||||
* @see CONVENTIONS.md — "How To: Add a New Source Provider".
|
||||
*/
|
||||
interface Autoblogger_Source_Provider_Interface {
|
||||
interface PRAutoBlogger_Source_Provider_Interface {
|
||||
|
||||
/**
|
||||
* Collect posts/comments from this social platform.
|
||||
@@ -23,7 +23,7 @@ interface Autoblogger_Source_Provider_Interface {
|
||||
* keywords?: string[],
|
||||
* } $config Source-specific configuration.
|
||||
*
|
||||
* @return Autoblogger_Source_Data[] Array of collected data objects.
|
||||
* @return PRAutoBlogger_Source_Data[] Array of collected data objects.
|
||||
*
|
||||
* @throws \RuntimeException On API error after retries exhausted.
|
||||
*/
|
||||
|
||||
55
autoblogger.php → prautoblogger.php
Executable file → Normal file
55
autoblogger.php → prautoblogger.php
Executable file → Normal file
@@ -2,11 +2,11 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* AutoBlogger — Automated Content Research & Publishing
|
||||
* PRAutoBlogger — Automated Content Research & Publishing
|
||||
*
|
||||
* @wordpress-plugin
|
||||
* Plugin Name: AutoBlogger
|
||||
* Plugin URI: https://peptiderepo.com/autoblogger
|
||||
* Plugin Name: PRAutoBlogger
|
||||
* Plugin URI: https://peptiderepo.com/prautoblogger
|
||||
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.0
|
||||
@@ -15,7 +15,7 @@ declare(strict_types=1);
|
||||
* Author URI: https://peptiderepo.com
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: autoblogger
|
||||
* Text Domain: prautoblogger
|
||||
* Domain Path: /languages
|
||||
*
|
||||
* @see ARCHITECTURE.md — Full data flow and file tree
|
||||
@@ -35,23 +35,23 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
| and limits without magic strings.
|
||||
*/
|
||||
|
||||
define( 'AUTOBLOGGER_VERSION', '0.1.0' );
|
||||
define( 'AUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'AUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'AUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'AUTOBLOGGER_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'AUTOBLOGGER_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.1.0' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
||||
// API and retry limits — named constants instead of magic numbers.
|
||||
define( 'AUTOBLOGGER_MAX_RETRIES', 3 );
|
||||
define( 'AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
define( 'AUTOBLOGGER_API_TIMEOUT_SECONDS', 120 );
|
||||
define( 'AUTOBLOGGER_CACHE_TTL_SECONDS', 3600 );
|
||||
define( 'PRAUTOBLOGGER_MAX_RETRIES', 3 );
|
||||
define( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
define( 'PRAUTOBLOGGER_API_TIMEOUT_SECONDS', 120 );
|
||||
define( 'PRAUTOBLOGGER_CACHE_TTL_SECONDS', 3600 );
|
||||
|
||||
// Default models for OpenRouter (user can override in settings).
|
||||
define( 'AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL', 'anthropic/claude-3.5-haiku' );
|
||||
define( 'AUTOBLOGGER_DEFAULT_WRITING_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
define( 'AUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL', 'anthropic/claude-3.5-haiku' );
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_WRITING_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -59,8 +59,13 @@ define( 'AUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require_once AUTOBLOGGER_PLUGIN_DIR . 'includes/class-autoloader.php';
|
||||
Autoblogger_Autoloader::register();
|
||||
require_once PRAUTOBLOGGER_PLUGIN_DIR . 'includes/class-autoloader.php';
|
||||
PRAutoBlogger_Autoloader::register();
|
||||
|
||||
// The main orchestrator class is loaded explicitly because its CamelCase name
|
||||
// (PRAutoBlogger) doesn't map cleanly to a filename via the autoloader's
|
||||
// kebab-case convention (it would look for class-pr-auto-blogger.php).
|
||||
require_once PRAUTOBLOGGER_PLUGIN_DIR . 'includes/class-prautoblogger.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -68,8 +73,8 @@ Autoblogger_Autoloader::register();
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
register_activation_hook( __FILE__, [ 'Autoblogger_Activator', 'activate' ] );
|
||||
register_deactivation_hook( __FILE__, [ 'Autoblogger_Deactivator', 'deactivate' ] );
|
||||
register_activation_hook( __FILE__, [ 'PRAutoBlogger_Activator', 'activate' ] );
|
||||
register_deactivation_hook( __FILE__, [ 'PRAutoBlogger_Deactivator', 'deactivate' ] );
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -82,17 +87,17 @@ register_deactivation_hook( __FILE__, [ 'Autoblogger_Deactivator', 'deactivate'
|
||||
/**
|
||||
* Returns the singleton instance of the main plugin class.
|
||||
*
|
||||
* @return Autoblogger
|
||||
* @return PRAutoBlogger
|
||||
*/
|
||||
function autoblogger(): Autoblogger {
|
||||
function prautoblogger(): PRAutoBlogger {
|
||||
static $instance = null;
|
||||
if ( null === $instance ) {
|
||||
$instance = new Autoblogger();
|
||||
$instance = new PRAutoBlogger();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
// Initialize on plugins_loaded so all dependencies are available.
|
||||
add_action( 'plugins_loaded', static function (): void {
|
||||
autoblogger()->run();
|
||||
prautoblogger()->run();
|
||||
} );
|
||||
10
readme.txt
10
readme.txt
@@ -1,4 +1,4 @@
|
||||
=== AutoBlogger ===
|
||||
=== PRAutoBlogger ===
|
||||
Contributors: peptiderepo
|
||||
Tags: ai, content generation, blogging, seo, automation
|
||||
Requires at least: 6.0
|
||||
@@ -12,7 +12,7 @@ Automatically researches trending topics from social media and generates SEO-fri
|
||||
|
||||
== Description ==
|
||||
|
||||
AutoBlogger monitors social media platforms (starting with Reddit) for recurring questions, complaints, and product comparisons in your niche. It uses AI to analyze these patterns, generate well-structured blog articles, run them through an editorial review agent, and publish them on a configurable daily schedule.
|
||||
PRAutoBlogger monitors social media platforms (starting with Reddit) for recurring questions, complaints, and product comparisons in your niche. It uses AI to analyze these patterns, generate well-structured blog articles, run them through an editorial review agent, and publish them on a configurable daily schedule.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
@@ -30,9 +30,9 @@ Uses OpenRouter for access to models from Anthropic, OpenAI, Google, Meta, and m
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `autoblogger` folder to the `/wp-content/plugins/` directory.
|
||||
1. Upload the `prautoblogger` folder to the `/wp-content/plugins/` directory.
|
||||
2. Activate the plugin through the 'Plugins' menu in WordPress.
|
||||
3. Go to AutoBlogger > Settings to configure your API keys and preferences.
|
||||
3. Go to PRAutoBlogger > Settings to configure your API keys and preferences.
|
||||
4. Add your OpenRouter API key and Reddit app credentials.
|
||||
5. Configure your target subreddits and content preferences.
|
||||
6. Click "Test Connections" to verify setup.
|
||||
@@ -50,7 +50,7 @@ By default, articles go through the chief editor agent. Articles that pass revie
|
||||
|
||||
= How do I set up Reddit API access? =
|
||||
|
||||
Go to reddit.com/prefs/apps, create a "script" type application. Copy the client ID and secret into the AutoBlogger settings page.
|
||||
Go to reddit.com/prefs/apps, create a "script" type application. Copy the client ID and secret into the PRAutoBlogger settings page.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# AutoBlogger Manual Deploy Script
|
||||
# PRAutoBlogger Manual Deploy Script
|
||||
#
|
||||
# Packages the plugin into a clean zip and deploys it to WordPress via the
|
||||
# deploy-receiver REST endpoint. Use this for hotfixes or local deploys when
|
||||
@@ -16,7 +16,7 @@
|
||||
# If not set, the script will look for a .env file in the project root.
|
||||
#
|
||||
# @see .github/workflows/deploy.yml — Automated pipeline (same deploy step).
|
||||
# @see .github/mu-plugins/autoblogger-deploy-receiver.php — Server endpoint.
|
||||
# @see ARCHITECTURE.md — Deploy architecture (Code Snippets snippet #8 on server).
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
@@ -53,8 +53,8 @@ fi
|
||||
|
||||
# ── Read version ────────────────────────────────────────────────────────────
|
||||
|
||||
VERSION=$(grep -oP 'Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+' "$PROJECT_ROOT/autoblogger.php" || echo "0.0.0")
|
||||
info "AutoBlogger v${VERSION}"
|
||||
VERSION=$(grep -oP 'Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+' "$PROJECT_ROOT/prautoblogger.php" || echo "0.0.0")
|
||||
info "PRAutoBlogger v${VERSION}"
|
||||
|
||||
# ── PHP lint check ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -77,8 +77,8 @@ info "Lint passed."
|
||||
# ── Package ─────────────────────────────────────────────────────────────────
|
||||
|
||||
BUILD_DIR=$(mktemp -d)
|
||||
PLUGIN_DIR="$BUILD_DIR/autoblogger"
|
||||
ZIP_PATH="$BUILD_DIR/autoblogger-${VERSION}.zip"
|
||||
PLUGIN_DIR="$BUILD_DIR/prautoblogger"
|
||||
ZIP_PATH="$BUILD_DIR/prautoblogger-${VERSION}.zip"
|
||||
|
||||
info "Packaging plugin..."
|
||||
|
||||
@@ -110,7 +110,7 @@ rsync -a \
|
||||
"$PROJECT_ROOT/" "$PLUGIN_DIR/"
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
zip -rq "$ZIP_PATH" autoblogger/
|
||||
zip -rq "$ZIP_PATH" prautoblogger/
|
||||
|
||||
ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1)
|
||||
info "Built ${ZIP_PATH} (${ZIP_SIZE})"
|
||||
@@ -157,4 +157,4 @@ fi
|
||||
# ── Cleanup ─────────────────────────────────────────────────────────────────
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
info "Done. AutoBlogger v${VERSION} is live."
|
||||
info "Done. PRAutoBlogger v${VERSION} is live."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Log viewer template — displays structured application log entries.
|
||||
*
|
||||
* Variables from Autoblogger_Log_Viewer::render_page():
|
||||
* Variables from PRAutoBlogger_Log_Viewer::render_page():
|
||||
* $rows — array of log entry rows.
|
||||
* $total — total matching entries.
|
||||
* $total_pages — total pages.
|
||||
@@ -17,30 +17,30 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$base_url = admin_url( 'admin.php?page=autoblogger-logs' );
|
||||
$base_url = admin_url( 'admin.php?page=prautoblogger-logs' );
|
||||
$level_counts = [
|
||||
'all' => __( 'All', 'autoblogger' ),
|
||||
'error' => __( 'Errors', 'autoblogger' ),
|
||||
'warning' => __( 'Warnings', 'autoblogger' ),
|
||||
'info' => __( 'Info', 'autoblogger' ),
|
||||
'debug' => __( 'Debug', 'autoblogger' ),
|
||||
'all' => __( 'All', 'prautoblogger' ),
|
||||
'error' => __( 'Errors', 'prautoblogger' ),
|
||||
'warning' => __( 'Warnings', 'prautoblogger' ),
|
||||
'info' => __( 'Info', 'prautoblogger' ),
|
||||
'debug' => __( 'Debug', 'prautoblogger' ),
|
||||
];
|
||||
?>
|
||||
<div class="wrap ab-wrap">
|
||||
<div class="ab-header">
|
||||
<div class="ab-header-left">
|
||||
<span class="dashicons dashicons-list-view ab-header-icon"></span>
|
||||
<h1 class="ab-header-title"><?php esc_html_e( 'Activity Log', 'autoblogger' ); ?></h1>
|
||||
<h1 class="ab-header-title"><?php esc_html_e( 'Activity Log', 'prautoblogger' ); ?></h1>
|
||||
</div>
|
||||
<div class="ab-header-actions">
|
||||
<button type="button" id="autoblogger-clear-logs" class="ab-btn ab-btn-outline" data-nonce="<?php echo esc_attr( wp_create_nonce( 'autoblogger_clear_logs' ) ); ?>">
|
||||
<button type="button" id="prautoblogger-clear-logs" class="ab-btn ab-btn-outline" data-nonce="<?php echo esc_attr( wp_create_nonce( 'prautoblogger_clear_logs' ) ); ?>">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
<?php esc_html_e( 'Clear Logs (30d+)', 'autoblogger' ); ?>
|
||||
<?php esc_html_e( 'Clear Logs (30d+)', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="autoblogger-log-status" class="hidden"></div>
|
||||
<div id="prautoblogger-log-status" class="hidden"></div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="ab-log-filters">
|
||||
@@ -54,10 +54,10 @@ $level_counts = [
|
||||
</div>
|
||||
|
||||
<form method="get" action="<?php echo esc_url( $base_url ); ?>" class="ab-log-search">
|
||||
<input type="hidden" name="page" value="autoblogger-logs" />
|
||||
<input type="hidden" name="page" value="prautoblogger-logs" />
|
||||
<input type="hidden" name="level" value="<?php echo esc_attr( $level ); ?>" />
|
||||
<input type="text" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search logs...', 'autoblogger' ); ?>" class="ab-input ab-log-search-input" />
|
||||
<button type="submit" class="ab-btn ab-btn-outline ab-btn-small"><?php esc_html_e( 'Search', 'autoblogger' ); ?></button>
|
||||
<input type="text" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search logs...', 'prautoblogger' ); ?>" class="ab-input ab-log-search-input" />
|
||||
<button type="submit" class="ab-btn ab-btn-outline ab-btn-small"><?php esc_html_e( 'Search', 'prautoblogger' ); ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ $level_counts = [
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: total count */
|
||||
esc_html__( '%s entries found', 'autoblogger' ),
|
||||
esc_html__( '%s entries found', 'prautoblogger' ),
|
||||
'<strong>' . esc_html( number_format( $total ) ) . '</strong>'
|
||||
);
|
||||
?>
|
||||
@@ -75,16 +75,16 @@ $level_counts = [
|
||||
<?php if ( empty( $rows ) ) : ?>
|
||||
<div class="ab-log-empty">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<p><?php esc_html_e( 'No log entries match your filters.', 'autoblogger' ); ?></p>
|
||||
<p><?php esc_html_e( 'No log entries match your filters.', 'prautoblogger' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped ab-log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ab-log-col-level"><?php esc_html_e( 'Level', 'autoblogger' ); ?></th>
|
||||
<th class="ab-log-col-time"><?php esc_html_e( 'Time', 'autoblogger' ); ?></th>
|
||||
<th class="ab-log-col-context"><?php esc_html_e( 'Context', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Message', 'autoblogger' ); ?></th>
|
||||
<th class="ab-log-col-level"><?php esc_html_e( 'Level', 'prautoblogger' ); ?></th>
|
||||
<th class="ab-log-col-time"><?php esc_html_e( 'Time', 'prautoblogger' ); ?></th>
|
||||
<th class="ab-log-col-context"><?php esc_html_e( 'Context', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Message', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -105,7 +105,7 @@ $level_counts = [
|
||||
<td>
|
||||
<?php echo esc_html( $row['message'] ?? '' ); ?>
|
||||
<?php if ( ! empty( $row['meta_json'] ) ) : ?>
|
||||
<button type="button" class="ab-log-meta-toggle" title="<?php esc_attr_e( 'Show details', 'autoblogger' ); ?>">
|
||||
<button type="button" class="ab-log-meta-toggle" title="<?php esc_attr_e( 'Show details', 'prautoblogger' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
</button>
|
||||
<pre class="ab-log-meta"><?php echo esc_html( wp_json_encode( json_decode( $row['meta_json'], true ), JSON_PRETTY_PRINT ) ); ?></pre>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Post metabox template showing AutoBlogger generation metadata.
|
||||
* Post metabox template showing PRAutoBlogger generation metadata.
|
||||
*
|
||||
* @see admin/class-post-metabox.php — Renders this template.
|
||||
*
|
||||
@@ -12,63 +12,63 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'topic' => get_post_meta( $post->ID, '_autoblogger_topic', true ),
|
||||
'type' => get_post_meta( $post->ID, '_autoblogger_article_type', true ),
|
||||
'model' => get_post_meta( $post->ID, '_autoblogger_model_used', true ),
|
||||
'pipeline' => get_post_meta( $post->ID, '_autoblogger_pipeline_mode', true ),
|
||||
'verdict' => get_post_meta( $post->ID, '_autoblogger_editor_verdict', true ),
|
||||
'editor_notes' => get_post_meta( $post->ID, '_autoblogger_editor_notes', true ),
|
||||
'quality_score' => get_post_meta( $post->ID, '_autoblogger_quality_score', true ),
|
||||
'seo_score' => get_post_meta( $post->ID, '_autoblogger_seo_score', true ),
|
||||
'generated_at' => get_post_meta( $post->ID, '_autoblogger_generated_at', true ),
|
||||
'keywords' => get_post_meta( $post->ID, '_autoblogger_target_keywords', true ),
|
||||
'topic' => get_post_meta( $post->ID, '_prautoblogger_topic', true ),
|
||||
'type' => get_post_meta( $post->ID, '_prautoblogger_article_type', true ),
|
||||
'model' => get_post_meta( $post->ID, '_prautoblogger_model_used', true ),
|
||||
'pipeline' => get_post_meta( $post->ID, '_prautoblogger_pipeline_mode', true ),
|
||||
'verdict' => get_post_meta( $post->ID, '_prautoblogger_editor_verdict', true ),
|
||||
'editor_notes' => get_post_meta( $post->ID, '_prautoblogger_editor_notes', true ),
|
||||
'quality_score' => get_post_meta( $post->ID, '_prautoblogger_quality_score', true ),
|
||||
'seo_score' => get_post_meta( $post->ID, '_prautoblogger_seo_score', true ),
|
||||
'generated_at' => get_post_meta( $post->ID, '_prautoblogger_generated_at', true ),
|
||||
'keywords' => get_post_meta( $post->ID, '_prautoblogger_target_keywords', true ),
|
||||
];
|
||||
?>
|
||||
<div class="autoblogger-metabox">
|
||||
<div class="prautoblogger-metabox">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Generated:', 'autoblogger' ); ?></strong>
|
||||
<?php echo esc_html( $meta['generated_at'] ?: __( 'Unknown', 'autoblogger' ) ); ?>
|
||||
<strong><?php esc_html_e( 'Generated:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( $meta['generated_at'] ?: __( 'Unknown', 'prautoblogger' ) ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Topic:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Topic:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( $meta['topic'] ?: '—' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Type:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Type:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( ucfirst( $meta['type'] ?: 'article' ) ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Model:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Model:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( $meta['model'] ?: '—' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Pipeline:', 'autoblogger' ); ?></strong>
|
||||
<?php echo esc_html( 'multi_step' === $meta['pipeline'] ? __( 'Multi-step', 'autoblogger' ) : __( 'Single-pass', 'autoblogger' ) ); ?>
|
||||
<strong><?php esc_html_e( 'Pipeline:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( 'multi_step' === $meta['pipeline'] ? __( 'Multi-step', 'prautoblogger' ) : __( 'Single-pass', 'prautoblogger' ) ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Editor Verdict:', 'autoblogger' ); ?></strong>
|
||||
<span class="autoblogger-verdict-<?php echo esc_attr( $meta['verdict'] ?: 'unknown' ); ?>">
|
||||
<?php echo esc_html( ucfirst( $meta['verdict'] ?: __( 'Unknown', 'autoblogger' ) ) ); ?>
|
||||
<strong><?php esc_html_e( 'Editor Verdict:', 'prautoblogger' ); ?></strong>
|
||||
<span class="prautoblogger-verdict-<?php echo esc_attr( $meta['verdict'] ?: 'unknown' ); ?>">
|
||||
<?php echo esc_html( ucfirst( $meta['verdict'] ?: __( 'Unknown', 'prautoblogger' ) ) ); ?>
|
||||
</span>
|
||||
</p>
|
||||
<?php if ( $meta['quality_score'] ) : ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Quality:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Quality:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( number_format( (float) $meta['quality_score'] * 100, 0 ) ); ?>%
|
||||
|
|
||||
<strong><?php esc_html_e( 'SEO:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'SEO:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( number_format( (float) $meta['seo_score'] * 100, 0 ) ); ?>%
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ( $meta['editor_notes'] ) : ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Editor Notes:', 'autoblogger' ); ?></strong><br>
|
||||
<strong><?php esc_html_e( 'Editor Notes:', 'prautoblogger' ); ?></strong><br>
|
||||
<?php echo esc_html( $meta['editor_notes'] ); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ( $meta['keywords'] ) : ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Keywords:', 'autoblogger' ); ?></strong>
|
||||
<strong><?php esc_html_e( 'Keywords:', 'prautoblogger' ); ?></strong>
|
||||
<?php echo esc_html( implode( ', ', json_decode( $meta['keywords'], true ) ?: [] ) ); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -9,9 +9,9 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$cost_tracker = new Autoblogger_Cost_Tracker();
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
$monthly_spend = $cost_tracker->get_monthly_spend();
|
||||
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
|
||||
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
|
||||
$utilization = $cost_tracker->get_budget_utilization();
|
||||
$daily_spend = $cost_tracker->get_daily_spend( 30 );
|
||||
|
||||
@@ -19,64 +19,64 @@ $first_of_month = gmdate( 'Y-m-01' );
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
$stage_breakdown = $cost_tracker->get_spend_by_stage( $first_of_month, $today );
|
||||
?>
|
||||
<div class="wrap autoblogger-metrics">
|
||||
<h1><?php esc_html_e( 'AutoBlogger — Metrics & Costs', 'autoblogger' ); ?></h1>
|
||||
<div class="wrap prautoblogger-metrics">
|
||||
<h1><?php esc_html_e( 'PRAutoBlogger — Metrics & Costs', 'prautoblogger' ); ?></h1>
|
||||
|
||||
<!-- Budget Overview Cards -->
|
||||
<div class="autoblogger-cards">
|
||||
<div class="autoblogger-card">
|
||||
<h3><?php esc_html_e( 'Monthly Spend', 'autoblogger' ); ?></h3>
|
||||
<div class="autoblogger-card-value">
|
||||
<div class="prautoblogger-cards">
|
||||
<div class="prautoblogger-card">
|
||||
<h3><?php esc_html_e( 'Monthly Spend', 'prautoblogger' ); ?></h3>
|
||||
<div class="prautoblogger-card-value">
|
||||
$<?php echo esc_html( number_format( $monthly_spend, 4 ) ); ?>
|
||||
</div>
|
||||
<div class="autoblogger-card-sub">
|
||||
<div class="prautoblogger-card-sub">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: budget amount */
|
||||
esc_html__( 'of $%s budget', 'autoblogger' ),
|
||||
esc_html__( 'of $%s budget', 'prautoblogger' ),
|
||||
esc_html( number_format( $budget, 2 ) )
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="autoblogger-card <?php echo $utilization >= 100 ? 'autoblogger-card-danger' : ( $utilization >= 80 ? 'autoblogger-card-warning' : '' ); ?>">
|
||||
<h3><?php esc_html_e( 'Budget Used', 'autoblogger' ); ?></h3>
|
||||
<div class="autoblogger-card-value">
|
||||
<div class="prautoblogger-card <?php echo $utilization >= 100 ? 'prautoblogger-card-danger' : ( $utilization >= 80 ? 'prautoblogger-card-warning' : '' ); ?>">
|
||||
<h3><?php esc_html_e( 'Budget Used', 'prautoblogger' ); ?></h3>
|
||||
<div class="prautoblogger-card-value">
|
||||
<?php echo esc_html( number_format( $utilization, 1 ) ); ?>%
|
||||
</div>
|
||||
<div class="autoblogger-budget-bar">
|
||||
<div class="autoblogger-budget-fill" style="width: <?php echo esc_attr( (string) min( 100, $utilization ) ); ?>%"></div>
|
||||
<div class="prautoblogger-budget-bar">
|
||||
<div class="prautoblogger-budget-fill" style="width: <?php echo esc_attr( (string) min( 100, $utilization ) ); ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="autoblogger-card">
|
||||
<h3><?php esc_html_e( 'Next Generation', 'autoblogger' ); ?></h3>
|
||||
<div class="autoblogger-card-value autoblogger-card-value-small">
|
||||
<div class="prautoblogger-card">
|
||||
<h3><?php esc_html_e( 'Next Generation', 'prautoblogger' ); ?></h3>
|
||||
<div class="prautoblogger-card-value prautoblogger-card-value-small">
|
||||
<?php
|
||||
$next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
$next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
|
||||
echo false !== $next_run
|
||||
? esc_html( wp_date( 'M j, g:i A', $next_run ) )
|
||||
: esc_html__( 'Not scheduled', 'autoblogger' );
|
||||
: esc_html__( 'Not scheduled', 'prautoblogger' );
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost by Stage -->
|
||||
<h2><?php esc_html_e( 'Cost by Pipeline Stage (This Month)', 'autoblogger' ); ?></h2>
|
||||
<h2><?php esc_html_e( 'Cost by Pipeline Stage (This Month)', 'prautoblogger' ); ?></h2>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Stage', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'API Calls', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Total Tokens', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Cost (USD)', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Stage', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'API Calls', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Total Tokens', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Cost (USD)', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $stage_breakdown ) ) : ?>
|
||||
<tr><td colspan="4"><?php esc_html_e( 'No data yet.', 'autoblogger' ); ?></td></tr>
|
||||
<tr><td colspan="4"><?php esc_html_e( 'No data yet.', 'prautoblogger' ); ?></td></tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $stage_breakdown as $stage => $data ) : ?>
|
||||
<tr>
|
||||
@@ -91,17 +91,17 @@ $stage_breakdown = $cost_tracker->get_spend_by_stage( $first_of_month, $today );
|
||||
</table>
|
||||
|
||||
<!-- Daily Spend Chart (simple HTML table — upgrade to Chart.js later) -->
|
||||
<h2><?php esc_html_e( 'Daily Spend (Last 30 Days)', 'autoblogger' ); ?></h2>
|
||||
<h2><?php esc_html_e( 'Daily Spend (Last 30 Days)', 'prautoblogger' ); ?></h2>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Date', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Cost (USD)', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Date', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Cost (USD)', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $daily_spend ) ) : ?>
|
||||
<tr><td colspan="2"><?php esc_html_e( 'No data yet.', 'autoblogger' ); ?></td></tr>
|
||||
<tr><td colspan="2"><?php esc_html_e( 'No data yet.', 'prautoblogger' ); ?></td></tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( array_reverse( $daily_spend, true ) as $date => $cost ) : ?>
|
||||
<tr>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Review queue template — lists generated drafts awaiting editorial approval.
|
||||
*
|
||||
* Variables available from Autoblogger_Review_Queue::render_page():
|
||||
* Variables available from PRAutoBlogger_Review_Queue::render_page():
|
||||
* $query (WP_Query) — the pending draft posts.
|
||||
*
|
||||
* @see admin/class-review-queue.php — Renders this template.
|
||||
@@ -12,11 +12,11 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$bulk_done = isset( $_GET['autoblogger_bulk_done'] ) ? absint( $_GET['autoblogger_bulk_done'] ) : 0;
|
||||
$bulk_type = isset( $_GET['autoblogger_bulk_type'] ) ? sanitize_text_field( wp_unslash( $_GET['autoblogger_bulk_type'] ) ) : '';
|
||||
$bulk_done = isset( $_GET['prautoblogger_bulk_done'] ) ? absint( $_GET['prautoblogger_bulk_done'] ) : 0;
|
||||
$bulk_type = isset( $_GET['prautoblogger_bulk_type'] ) ? sanitize_text_field( wp_unslash( $_GET['prautoblogger_bulk_type'] ) ) : '';
|
||||
?>
|
||||
<div class="wrap autoblogger-review-queue">
|
||||
<h1><?php esc_html_e( 'AutoBlogger — Review Queue', 'autoblogger' ); ?></h1>
|
||||
<div class="wrap prautoblogger-review-queue">
|
||||
<h1><?php esc_html_e( 'PRAutoBlogger — Review Queue', 'prautoblogger' ); ?></h1>
|
||||
|
||||
<?php if ( $bulk_done > 0 && '' !== $bulk_type ) : ?>
|
||||
<div class="notice notice-success is-dismissible">
|
||||
@@ -25,13 +25,13 @@ $bulk_type = isset( $_GET['autoblogger_bulk_type'] ) ? sanitize_text_field( wp_u
|
||||
if ( 'approve' === $bulk_type ) {
|
||||
printf(
|
||||
/* translators: %d: number of posts */
|
||||
esc_html( _n( '%d post published.', '%d posts published.', $bulk_done, 'autoblogger' ) ),
|
||||
esc_html( _n( '%d post published.', '%d posts published.', $bulk_done, 'prautoblogger' ) ),
|
||||
$bulk_done
|
||||
);
|
||||
} else {
|
||||
printf(
|
||||
/* translators: %d: number of posts */
|
||||
esc_html( _n( '%d post rejected.', '%d posts rejected.', $bulk_done, 'autoblogger' ) ),
|
||||
esc_html( _n( '%d post rejected.', '%d posts rejected.', $bulk_done, 'prautoblogger' ) ),
|
||||
$bulk_done
|
||||
);
|
||||
}
|
||||
@@ -40,71 +40,71 @@ $bulk_type = isset( $_GET['autoblogger_bulk_type'] ) ? sanitize_text_field( wp_u
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="autoblogger-queue-status" class="autoblogger-status-message hidden"></div>
|
||||
<div id="prautoblogger-queue-status" class="prautoblogger-status-message hidden"></div>
|
||||
|
||||
<?php if ( ! $query->have_posts() ) : ?>
|
||||
<div class="autoblogger-empty-queue">
|
||||
<p><?php esc_html_e( 'No generated drafts awaiting review. New posts will appear here after the next generation run.', 'autoblogger' ); ?></p>
|
||||
<div class="prautoblogger-empty-queue">
|
||||
<p><?php esc_html_e( 'No generated drafts awaiting review. New posts will appear here after the next generation run.', 'prautoblogger' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<form method="post" id="autoblogger-review-form">
|
||||
<?php wp_nonce_field( 'autoblogger_review_queue_bulk', 'autoblogger_review_nonce' ); ?>
|
||||
<form method="post" id="prautoblogger-review-form">
|
||||
<?php wp_nonce_field( 'prautoblogger_review_queue_bulk', 'prautoblogger_review_nonce' ); ?>
|
||||
|
||||
<div class="autoblogger-queue-toolbar">
|
||||
<div class="prautoblogger-queue-toolbar">
|
||||
<label>
|
||||
<input type="checkbox" id="autoblogger-select-all" />
|
||||
<?php esc_html_e( 'Select All', 'autoblogger' ); ?>
|
||||
<input type="checkbox" id="prautoblogger-select-all" />
|
||||
<?php esc_html_e( 'Select All', 'prautoblogger' ); ?>
|
||||
</label>
|
||||
<button type="submit" name="autoblogger_bulk_action" value="approve" class="button button-primary">
|
||||
<?php esc_html_e( 'Approve Selected', 'autoblogger' ); ?>
|
||||
<button type="submit" name="prautoblogger_bulk_action" value="approve" class="button button-primary">
|
||||
<?php esc_html_e( 'Approve Selected', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
<button type="submit" name="autoblogger_bulk_action" value="reject" class="button">
|
||||
<?php esc_html_e( 'Reject Selected', 'autoblogger' ); ?>
|
||||
<button type="submit" name="prautoblogger_bulk_action" value="reject" class="button">
|
||||
<?php esc_html_e( 'Reject Selected', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
<span class="autoblogger-queue-count">
|
||||
<span class="prautoblogger-queue-count">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of drafts */
|
||||
esc_html( _n( '%d draft', '%d drafts', $query->found_posts, 'autoblogger' ) ),
|
||||
esc_html( _n( '%d draft', '%d drafts', $query->found_posts, 'prautoblogger' ) ),
|
||||
$query->found_posts
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<table class="widefat striped autoblogger-queue-table">
|
||||
<table class="widefat striped prautoblogger-queue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="check-column"><span class="screen-reader-text"><?php esc_html_e( 'Select', 'autoblogger' ); ?></span></th>
|
||||
<th><?php esc_html_e( 'Title', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Topic', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Editor Verdict', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Quality', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Generated', 'autoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Actions', 'autoblogger' ); ?></th>
|
||||
<th class="check-column"><span class="screen-reader-text"><?php esc_html_e( 'Select', 'prautoblogger' ); ?></span></th>
|
||||
<th><?php esc_html_e( 'Title', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Topic', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Editor Verdict', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Quality', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Generated', 'prautoblogger' ); ?></th>
|
||||
<th><?php esc_html_e( 'Actions', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
|
||||
<?php
|
||||
$post_id = get_the_ID();
|
||||
$verdict = get_post_meta( $post_id, '_autoblogger_editor_verdict', true );
|
||||
$topic = get_post_meta( $post_id, '_autoblogger_topic', true );
|
||||
$article_type = get_post_meta( $post_id, '_autoblogger_article_type', true );
|
||||
$quality_score = get_post_meta( $post_id, '_autoblogger_quality_score', true );
|
||||
$generated_at = get_post_meta( $post_id, '_autoblogger_generated_at', true );
|
||||
$editor_notes = get_post_meta( $post_id, '_autoblogger_editor_notes', true );
|
||||
$verdict_class = 'autoblogger-verdict-' . sanitize_html_class( $verdict ?: 'pending' );
|
||||
$verdict = get_post_meta( $post_id, '_prautoblogger_editor_verdict', true );
|
||||
$topic = get_post_meta( $post_id, '_prautoblogger_topic', true );
|
||||
$article_type = get_post_meta( $post_id, '_prautoblogger_article_type', true );
|
||||
$quality_score = get_post_meta( $post_id, '_prautoblogger_quality_score', true );
|
||||
$generated_at = get_post_meta( $post_id, '_prautoblogger_generated_at', true );
|
||||
$editor_notes = get_post_meta( $post_id, '_prautoblogger_editor_notes', true );
|
||||
$verdict_class = 'prautoblogger-verdict-' . sanitize_html_class( $verdict ?: 'pending' );
|
||||
?>
|
||||
<tr data-post-id="<?php echo esc_attr( (string) $post_id ); ?>">
|
||||
<td class="check-column">
|
||||
<input type="checkbox" name="autoblogger_post_ids[]" value="<?php echo esc_attr( (string) $post_id ); ?>" />
|
||||
<input type="checkbox" name="prautoblogger_post_ids[]" value="<?php echo esc_attr( (string) $post_id ); ?>" />
|
||||
</td>
|
||||
<td>
|
||||
<strong><?php the_title(); ?></strong>
|
||||
<?php if ( $editor_notes ) : ?>
|
||||
<div class="autoblogger-editor-notes"><?php echo esc_html( $editor_notes ); ?></div>
|
||||
<div class="prautoblogger-editor-notes"><?php echo esc_html( $editor_notes ); ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo esc_html( $topic ?: '—' ); ?></td>
|
||||
@@ -112,18 +112,18 @@ $bulk_type = isset( $_GET['autoblogger_bulk_type'] ) ? sanitize_text_field( wp_u
|
||||
<td><span class="<?php echo esc_attr( $verdict_class ); ?>"><?php echo esc_html( ucfirst( $verdict ?: 'pending' ) ); ?></span></td>
|
||||
<td><?php echo '' !== $quality_score ? esc_html( $quality_score . '/10' ) : '—'; ?></td>
|
||||
<td><?php echo $generated_at ? esc_html( wp_date( 'M j, g:i A', strtotime( $generated_at ) ) ) : '—'; ?></td>
|
||||
<td class="autoblogger-queue-actions">
|
||||
<button type="button" class="button button-small button-primary autoblogger-approve-btn" data-post-id="<?php echo esc_attr( (string) $post_id ); ?>">
|
||||
<?php esc_html_e( 'Approve', 'autoblogger' ); ?>
|
||||
<td class="prautoblogger-queue-actions">
|
||||
<button type="button" class="button button-small button-primary prautoblogger-approve-btn" data-post-id="<?php echo esc_attr( (string) $post_id ); ?>">
|
||||
<?php esc_html_e( 'Approve', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $post_id ) ); ?>" class="button button-small">
|
||||
<?php esc_html_e( 'Edit', 'autoblogger' ); ?>
|
||||
<?php esc_html_e( 'Edit', 'prautoblogger' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( get_preview_post_link( $post_id ) ); ?>" class="button button-small" target="_blank">
|
||||
<?php esc_html_e( 'Preview', 'autoblogger' ); ?>
|
||||
<?php esc_html_e( 'Preview', 'prautoblogger' ); ?>
|
||||
</a>
|
||||
<button type="button" class="button button-small autoblogger-reject-btn" data-post-id="<?php echo esc_attr( (string) $post_id ); ?>">
|
||||
<?php esc_html_e( 'Reject', 'autoblogger' ); ?>
|
||||
<button type="button" class="button button-small prautoblogger-reject-btn" data-post-id="<?php echo esc_attr( (string) $post_id ); ?>">
|
||||
<?php esc_html_e( 'Reject', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -138,7 +138,7 @@ $bulk_type = isset( $_GET['autoblogger_bulk_type'] ) ? sanitize_text_field( wp_u
|
||||
$total_pages = $query->max_num_pages;
|
||||
if ( $total_pages > 1 ) :
|
||||
$current_page = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
|
||||
echo '<div class="autoblogger-pagination">';
|
||||
echo '<div class="prautoblogger-pagination">';
|
||||
echo wp_kses_post( paginate_links( [
|
||||
'base' => add_query_arg( 'paged', '%#%' ),
|
||||
'format' => '',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Settings page template — modern tabbed UI.
|
||||
*
|
||||
* Variables from Autoblogger_Admin_Page::render_page():
|
||||
* Variables from PRAutoBlogger_Admin_Page::render_page():
|
||||
* $sections — tab definitions from Settings_Fields::get_sections().
|
||||
* $fields — all field definitions (unused here, WP renders via do_settings_fields).
|
||||
*
|
||||
@@ -13,12 +13,12 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'autoblogger_api';
|
||||
$cost_tracker = new Autoblogger_Cost_Tracker();
|
||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'prautoblogger_api';
|
||||
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
|
||||
$monthly_spend = $cost_tracker->get_monthly_spend();
|
||||
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
|
||||
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
|
||||
$utilization = $budget > 0 ? ( $monthly_spend / $budget ) * 100.0 : 0;
|
||||
$next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
$next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
|
||||
?>
|
||||
<div class="wrap ab-wrap">
|
||||
|
||||
@@ -27,18 +27,18 @@ $next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
<div class="ab-header-left">
|
||||
<span class="dashicons dashicons-edit-page ab-header-icon"></span>
|
||||
<div>
|
||||
<h1 class="ab-header-title"><?php esc_html_e( 'AutoBlogger', 'autoblogger' ); ?></h1>
|
||||
<span class="ab-header-version">v<?php echo esc_html( AUTOBLOGGER_VERSION ); ?></span>
|
||||
<h1 class="ab-header-title"><?php esc_html_e( 'PRAutoBlogger', 'prautoblogger' ); ?></h1>
|
||||
<span class="ab-header-version">v<?php echo esc_html( PRAUTOBLOGGER_VERSION ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ab-header-actions">
|
||||
<button type="button" id="autoblogger-test-connection" class="ab-btn ab-btn-outline">
|
||||
<button type="button" id="prautoblogger-test-connection" class="ab-btn ab-btn-outline">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( 'Test Connections', 'autoblogger' ); ?>
|
||||
<?php esc_html_e( 'Test Connections', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
<button type="button" id="autoblogger-generate-now" class="ab-btn ab-btn-primary">
|
||||
<button type="button" id="prautoblogger-generate-now" class="ab-btn ab-btn-primary">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( 'Generate Now', 'autoblogger' ); ?>
|
||||
<?php esc_html_e( 'Generate Now', 'prautoblogger' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,27 +46,27 @@ $next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
<!-- Quick stats bar -->
|
||||
<div class="ab-stats-bar">
|
||||
<div class="ab-stat">
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Monthly Spend', 'autoblogger' ); ?></span>
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Monthly Spend', 'prautoblogger' ); ?></span>
|
||||
<span class="ab-stat-value <?php echo $utilization >= 90 ? 'ab-text-danger' : ( $utilization >= 70 ? 'ab-text-warning' : '' ); ?>">
|
||||
$<?php echo esc_html( number_format( $monthly_spend, 2 ) ); ?>
|
||||
<small>/ $<?php echo esc_html( number_format( $budget, 2 ) ); ?></small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ab-stat">
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Next Run', 'autoblogger' ); ?></span>
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Next Run', 'prautoblogger' ); ?></span>
|
||||
<span class="ab-stat-value">
|
||||
<?php echo false !== $next_run ? esc_html( wp_date( 'M j, g:i A', $next_run ) ) : '<em>' . esc_html__( 'Not scheduled', 'autoblogger' ) . '</em>'; ?>
|
||||
<?php echo false !== $next_run ? esc_html( wp_date( 'M j, g:i A', $next_run ) ) : '<em>' . esc_html__( 'Not scheduled', 'prautoblogger' ) . '</em>'; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ab-stat">
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Review Queue', 'autoblogger' ); ?></span>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=autoblogger-review-queue' ) ); ?>" class="ab-stat-value ab-stat-link">
|
||||
<?php esc_html_e( 'View Drafts →', 'autoblogger' ); ?>
|
||||
<span class="ab-stat-label"><?php esc_html_e( 'Review Queue', 'prautoblogger' ); ?></span>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=prautoblogger-review-queue' ) ); ?>" class="ab-stat-value ab-stat-link">
|
||||
<?php esc_html_e( 'View Drafts →', 'prautoblogger' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="autoblogger-status-message" class="hidden"></div>
|
||||
<div id="prautoblogger-status-message" class="hidden"></div>
|
||||
|
||||
<!-- Tab navigation + settings form -->
|
||||
<div class="ab-layout">
|
||||
@@ -84,7 +84,7 @@ $next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
|
||||
<div class="ab-content">
|
||||
<form method="post" action="options.php" id="ab-settings-form">
|
||||
<?php settings_fields( 'autoblogger_settings_group' ); ?>
|
||||
<?php settings_fields( 'prautoblogger_settings_group' ); ?>
|
||||
|
||||
<?php foreach ( $sections as $sid => $sec ) : ?>
|
||||
<div class="ab-panel <?php echo $active_tab === $sid ? 'ab-panel-active' : ''; ?>"
|
||||
@@ -97,13 +97,13 @@ $next_run = wp_next_scheduled( 'autoblogger_daily_generation' );
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<table class="form-table ab-form-table" role="presentation">
|
||||
<?php do_settings_fields( 'autoblogger-settings', $sid ); ?>
|
||||
<?php do_settings_fields( 'prautoblogger-settings', $sid ); ?>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="ab-save-bar">
|
||||
<?php submit_button( __( 'Save Settings', 'autoblogger' ), 'ab-btn ab-btn-primary', 'submit', false ); ?>
|
||||
<?php submit_button( __( 'Save Settings', 'prautoblogger' ), 'ab-btn ab-btn-primary', 'submit', false ); ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPUnit bootstrap for AutoBlogger unit tests.
|
||||
* PHPUnit bootstrap for PRAutoBlogger unit tests.
|
||||
*
|
||||
* Uses Brain Monkey to mock WordPress functions so tests run without
|
||||
* a full WordPress installation. Only loads the classes under test.
|
||||
@@ -29,24 +29,24 @@ if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
|
||||
define( 'HOUR_IN_SECONDS', 3600 );
|
||||
}
|
||||
|
||||
// AutoBlogger constants from the main plugin file.
|
||||
if ( ! defined( 'AUTOBLOGGER_VERSION' ) ) {
|
||||
define( 'AUTOBLOGGER_VERSION', '0.1.0' );
|
||||
// PRAutoBlogger constants from the main plugin file.
|
||||
if ( ! defined( 'PRAUTOBLOGGER_VERSION' ) ) {
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.1.0' );
|
||||
}
|
||||
if ( ! defined( 'AUTOBLOGGER_MAX_RETRIES' ) ) {
|
||||
define( 'AUTOBLOGGER_MAX_RETRIES', 3 );
|
||||
if ( ! defined( 'PRAUTOBLOGGER_MAX_RETRIES' ) ) {
|
||||
define( 'PRAUTOBLOGGER_MAX_RETRIES', 3 );
|
||||
}
|
||||
if ( ! defined( 'AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS' ) ) {
|
||||
define( 'AUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
if ( ! defined( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS' ) ) {
|
||||
define( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
}
|
||||
if ( ! defined( 'AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL' ) ) {
|
||||
define( 'AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL', 'anthropic/claude-3.5-haiku' );
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL' ) ) {
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL', 'anthropic/claude-3.5-haiku' );
|
||||
}
|
||||
if ( ! defined( 'AUTOBLOGGER_DEFAULT_WRITING_MODEL' ) ) {
|
||||
define( 'AUTOBLOGGER_DEFAULT_WRITING_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_WRITING_MODEL' ) ) {
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_WRITING_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
}
|
||||
if ( ! defined( 'AUTOBLOGGER_DEFAULT_EDITOR_MODEL' ) ) {
|
||||
define( 'AUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL' ) ) {
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'anthropic/claude-sonnet-4' );
|
||||
}
|
||||
|
||||
// Load plugin class files (no autoloader in tests — explicit includes).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Unit tests for Autoblogger_Article_Idea value object.
|
||||
* Unit tests for PRAutoBlogger_Article_Idea value object.
|
||||
*
|
||||
* Article ideas flow from the scorer to the generator to the editor —
|
||||
* data must survive the full pipeline without mutation or loss.
|
||||
@@ -22,7 +22,7 @@ class ArticleIdeaTest extends \PHPUnit\Framework\TestCase {
|
||||
'target_keywords' => [ 'bpc-157 dosage', 'bpc-157 protocol' ],
|
||||
];
|
||||
|
||||
$idea = new Autoblogger_Article_Idea( $data );
|
||||
$idea = new PRAutoBlogger_Article_Idea( $data );
|
||||
|
||||
$this->assertSame( 'BPC-157 dosage protocols', $idea->get_topic() );
|
||||
$this->assertSame( 'question', $idea->get_article_type() );
|
||||
@@ -36,7 +36,7 @@ class ArticleIdeaTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_optional_fields_default_to_empty(): void {
|
||||
$idea = new Autoblogger_Article_Idea( [
|
||||
$idea = new PRAutoBlogger_Article_Idea( [
|
||||
'topic' => 'Test',
|
||||
'article_type' => 'comparison',
|
||||
'suggested_title' => 'Test Title',
|
||||
@@ -51,7 +51,7 @@ class ArticleIdeaTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_score_cast_from_string(): void {
|
||||
$idea = new Autoblogger_Article_Idea( [
|
||||
$idea = new PRAutoBlogger_Article_Idea( [
|
||||
'topic' => 'Test',
|
||||
'article_type' => 'question',
|
||||
'suggested_title' => 'Test',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Unit tests for Autoblogger_Editorial_Review value object.
|
||||
* Unit tests for PRAutoBlogger_Editorial_Review value object.
|
||||
*
|
||||
* The editorial review verdict drives the publish/draft/reject decision.
|
||||
* A null revised_content on a "revised" verdict caused a bug (now fixed) —
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
class EditorialReviewTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
public function test_approved_review(): void {
|
||||
$review = new Autoblogger_Editorial_Review( [
|
||||
$review = new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'approved',
|
||||
'notes' => 'Good article, ready to publish.',
|
||||
'quality_score' => 0.85,
|
||||
@@ -28,7 +28,7 @@ class EditorialReviewTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_revised_review_with_content(): void {
|
||||
$review = new Autoblogger_Editorial_Review( [
|
||||
$review = new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'revised',
|
||||
'notes' => 'Fixed heading structure.',
|
||||
'revised_content' => '<h1>Improved Article</h1><p>Better content.</p>',
|
||||
@@ -49,7 +49,7 @@ class EditorialReviewTest extends \PHPUnit\Framework\TestCase {
|
||||
* The pipeline handles this with a null-coalesce fallback.
|
||||
*/
|
||||
public function test_revised_verdict_with_null_content(): void {
|
||||
$review = new Autoblogger_Editorial_Review( [
|
||||
$review = new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'revised',
|
||||
'notes' => 'Supposedly revised but no content provided.',
|
||||
'revised_content' => null,
|
||||
@@ -60,7 +60,7 @@ class EditorialReviewTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_rejected_review(): void {
|
||||
$review = new Autoblogger_Editorial_Review( [
|
||||
$review = new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'rejected',
|
||||
'notes' => 'Off-topic and factually inaccurate.',
|
||||
'quality_score' => 0.2,
|
||||
@@ -74,7 +74,7 @@ class EditorialReviewTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_default_scores_are_zero(): void {
|
||||
$review = new Autoblogger_Editorial_Review( [
|
||||
$review = new PRAutoBlogger_Editorial_Review( [
|
||||
'verdict' => 'rejected',
|
||||
'notes' => 'Minimal data.',
|
||||
] );
|
||||
|
||||
@@ -5,7 +5,7 @@ use Brain\Monkey;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
/**
|
||||
* Unit tests for Autoblogger_Generation_Log value object.
|
||||
* Unit tests for PRAutoBlogger_Generation_Log value object.
|
||||
*
|
||||
* Verifies construction, getters, and to_db_row() serialization.
|
||||
* These are the records that power the cost dashboard — data integrity is critical.
|
||||
@@ -42,7 +42,7 @@ class GenerationLogTest extends \PHPUnit\Framework\TestCase {
|
||||
'created_at' => '2026-04-10 10:00:00',
|
||||
];
|
||||
|
||||
$log = new Autoblogger_Generation_Log( $data );
|
||||
$log = new PRAutoBlogger_Generation_Log( $data );
|
||||
|
||||
$this->assertSame( 42, $log->get_id() );
|
||||
$this->assertSame( 100, $log->get_post_id() );
|
||||
@@ -59,7 +59,7 @@ class GenerationLogTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_constructor_defaults_for_missing_fields(): void {
|
||||
$log = new Autoblogger_Generation_Log( [] );
|
||||
$log = new PRAutoBlogger_Generation_Log( [] );
|
||||
|
||||
$this->assertSame( 0, $log->get_id() );
|
||||
$this->assertNull( $log->get_post_id() );
|
||||
@@ -73,7 +73,7 @@ class GenerationLogTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_to_db_row_excludes_id(): void {
|
||||
$log = new Autoblogger_Generation_Log( [
|
||||
$log = new PRAutoBlogger_Generation_Log( [
|
||||
'id' => 99,
|
||||
'post_id' => 50,
|
||||
'run_id' => 'run-abc',
|
||||
@@ -95,7 +95,7 @@ class GenerationLogTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_to_db_row_has_all_expected_keys(): void {
|
||||
$log = new Autoblogger_Generation_Log( [ 'stage' => 'analysis' ] );
|
||||
$log = new PRAutoBlogger_Generation_Log( [ 'stage' => 'analysis' ] );
|
||||
$row = $log->to_db_row();
|
||||
|
||||
$expected_keys = [
|
||||
@@ -110,7 +110,7 @@ class GenerationLogTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_numeric_fields_are_cast_correctly(): void {
|
||||
$log = new Autoblogger_Generation_Log( [
|
||||
$log = new PRAutoBlogger_Generation_Log( [
|
||||
'id' => '42',
|
||||
'post_id' => '100',
|
||||
'prompt_tokens' => '1500',
|
||||
|
||||
@@ -5,7 +5,7 @@ use Brain\Monkey;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
/**
|
||||
* Unit tests for Autoblogger_OpenRouter_Pricing cost estimation.
|
||||
* Unit tests for PRAutoBlogger_OpenRouter_Pricing cost estimation.
|
||||
*
|
||||
* Cost calculation accuracy directly affects budget enforcement and the
|
||||
* metrics dashboard. Getting this wrong means either overspending or
|
||||
@@ -24,7 +24,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
// Mock the Logger singleton so calls don't break.
|
||||
// The pricing class calls Logger::instance()->warning() for unknown models.
|
||||
if ( ! class_exists( 'Autoblogger_Logger' ) ) {
|
||||
if ( ! class_exists( 'PRAutoBlogger_Logger' ) ) {
|
||||
require_once dirname( __DIR__, 2 ) . '/includes/core/class-logger.php';
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
Functions\when( 'current_time' )->justReturn( '2026-04-10 12:00:00' );
|
||||
|
||||
// Mock Encryption class (used by get_api_key).
|
||||
if ( ! class_exists( 'Autoblogger_Encryption' ) ) {
|
||||
if ( ! class_exists( 'PRAutoBlogger_Encryption' ) ) {
|
||||
require_once dirname( __DIR__, 2 ) . '/includes/class-encryption.php';
|
||||
}
|
||||
|
||||
// Load the class under test.
|
||||
if ( ! class_exists( 'Autoblogger_OpenRouter_Pricing' ) ) {
|
||||
if ( ! class_exists( 'PRAutoBlogger_OpenRouter_Pricing' ) ) {
|
||||
require_once dirname( __DIR__, 2 ) . '/includes/providers/class-openrouter-pricing.php';
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_known_model_cost_calculation(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// Claude 3.5 Haiku: $0.80/M prompt, $4.00/M completion
|
||||
// 1000 prompt tokens = 1000 * 0.80 / 1_000_000 = $0.0008
|
||||
@@ -59,7 +59,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_claude_sonnet_cost(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// Claude Sonnet 4: $3.00/M prompt, $15.00/M completion
|
||||
// 2000 prompt + 1000 completion
|
||||
@@ -70,7 +70,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_gpt4o_mini_cost(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// GPT-4o-mini: $0.15/M prompt, $0.60/M completion
|
||||
// 5000 prompt + 2000 completion
|
||||
@@ -81,13 +81,13 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_zero_tokens_returns_zero_cost(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
$cost = $pricing->estimate_cost( 'anthropic/claude-3.5-haiku', 0, 0 );
|
||||
$this->assertEqualsWithDelta( 0.0, $cost, 0.00001 );
|
||||
}
|
||||
|
||||
public function test_unknown_model_uses_conservative_estimate(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// Unknown model: $10.00/M prompt, $30.00/M completion (conservative fallback)
|
||||
// 1000 prompt + 1000 completion
|
||||
@@ -97,7 +97,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_cost_scales_linearly_with_tokens(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
$cost_1x = $pricing->estimate_cost( 'anthropic/claude-3.5-haiku', 1000, 500 );
|
||||
$cost_2x = $pricing->estimate_cost( 'anthropic/claude-3.5-haiku', 2000, 1000 );
|
||||
@@ -107,7 +107,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
}
|
||||
|
||||
public function test_prompt_and_completion_priced_separately(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// For a model where prompt and completion prices differ (all of them),
|
||||
// swapping prompt/completion tokens should give a different cost.
|
||||
@@ -127,7 +127,7 @@ class OpenRouterPricingTest extends \PHPUnit\Framework\TestCase {
|
||||
* This is a sanity check: if this ever fails, pricing data may be stale.
|
||||
*/
|
||||
public function test_full_article_cost_sanity_check(): void {
|
||||
$pricing = new Autoblogger_OpenRouter_Pricing();
|
||||
$pricing = new PRAutoBlogger_OpenRouter_Pricing();
|
||||
|
||||
// Typical multi-step article: analysis + outline + draft + polish + review
|
||||
// Using Haiku for analysis, Sonnet for the rest.
|
||||
|
||||
@@ -23,7 +23,7 @@ global $wpdb;
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$prefix = $wpdb->prefix . 'autoblogger_';
|
||||
$prefix = $wpdb->prefix . 'prautoblogger_';
|
||||
$tables = [
|
||||
$prefix . 'source_data',
|
||||
$prefix . 'analysis_results',
|
||||
@@ -44,7 +44,7 @@ foreach ( $tables as $table ) {
|
||||
*/
|
||||
|
||||
$options = $wpdb->get_col(
|
||||
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'autoblogger\_%'"
|
||||
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'prautoblogger\_%'"
|
||||
);
|
||||
|
||||
foreach ( $options as $option ) {
|
||||
@@ -58,7 +58,7 @@ foreach ( $options as $option ) {
|
||||
*/
|
||||
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\\_autoblogger\\_%'"
|
||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\\_prautoblogger\\_%'"
|
||||
);
|
||||
|
||||
/*
|
||||
@@ -68,7 +68,7 @@ $wpdb->query(
|
||||
*/
|
||||
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_autoblogger\\_%' OR option_name LIKE '\\_transient\\_timeout\\_autoblogger\\_%'"
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_prautoblogger\\_%' OR option_name LIKE '\\_transient\\_timeout\\_prautoblogger\\_%'"
|
||||
);
|
||||
|
||||
/*
|
||||
@@ -78,8 +78,8 @@ $wpdb->query(
|
||||
*/
|
||||
|
||||
$hooks = [
|
||||
'autoblogger_daily_generation',
|
||||
'autoblogger_collect_metrics',
|
||||
'prautoblogger_daily_generation',
|
||||
'prautoblogger_collect_metrics',
|
||||
];
|
||||
|
||||
foreach ( $hooks as $hook ) {
|
||||
|
||||
Reference in New Issue
Block a user