diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a414e2..72790d3 100755 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,159 +1,70 @@ -# 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/prautoblogger-deploy/v1/deploy -# DEPLOY_KEY — The token matching PRAUTOBLOGGER_DEPLOY_KEY in wp-config.php -# -# @see .github/mu-plugins/prautoblogger-deploy-receiver.php — Server-side endpoint. -# @see scripts/deploy.sh — Manual deploy script. - -name: Deploy PRAutoBlogger +name: Deploy PRAutoBlogger to Hostinger on: push: branches: [main] - workflow_dispatch: # Allow manual trigger from GitHub UI - -concurrency: - group: deploy-${{ github.ref }} - cancel-in-progress: true jobs: - # ────────────────────────────────────────────────────────────────────── - # Stage 1: Lint & Standards - # ────────────────────────────────────────────────────────────────────── - lint: - name: PHP Lint & PHPCS + validate: + name: Validate PHP Syntax runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - tools: cs2pr, phpcs - coverage: none - - - name: PHP syntax check + - name: PHP Lint Check run: | - find . -name '*.php' -not -path './vendor/*' -not -path './tests/*' \ - -exec php -l {} \; 2>&1 | grep -v "No syntax errors" | head -50 - # Fail if any file has syntax errors - find . -name '*.php' -not -path './vendor/*' -not -path './tests/*' \ - -exec php -l {} \; 2>&1 | grep -q "Parse error" && exit 1 || true + find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20 + test ${PIPESTATUS[0]} -eq 0 - - name: Install WordPress Coding Standards - run: | - composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - composer global require --dev wp-coding-standards/wpcs:"^3.0" \ - phpcsstandards/phpcsextra:"^1.0" \ - phpcsstandards/phpcsutils:"^1.0" - - - name: Run PHPCS (warnings only, non-blocking) - run: | - phpcs --standard=WordPress --extensions=php \ - --ignore=vendor,tests,node_modules \ - --report=checkstyle \ - --warning-severity=0 \ - . | cs2pr || true - # Non-blocking for now — we want visibility without breaking deploys - # while we clean up existing code. Remove "|| true" once clean. - - # ────────────────────────────────────────────────────────────────────── - # Stage 2: Unit Tests - # ────────────────────────────────────────────────────────────────────── - test: - name: PHPUnit Tests - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - extensions: openssl, json, mbstring - coverage: none - - - name: Install Composer dependencies - run: composer install --no-interaction --prefer-dist - - - name: Run PHPUnit - run: vendor/bin/phpunit --testdox - - # ────────────────────────────────────────────────────────────────────── - # Stage 3: Package & Deploy - # ────────────────────────────────────────────────────────────────────── deploy: - name: Package & Deploy to Production + needs: validate + name: Deploy to Production runs-on: ubuntu-latest - needs: [lint, test] - if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - - name: Read plugin version - id: version + - name: Setup SSH key run: | - VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" prautoblogger.php) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Plugin version: $VERSION" + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - - name: Build plugin zip + - name: Deploy via rsync run: | - # Create a clean build directory - mkdir -p build/prautoblogger + rsync -avz --delete \ + --exclude '.git' \ + --exclude '.github' \ + --exclude 'tests' \ + --exclude 'phpunit.xml.dist' \ + --exclude 'composer.json' \ + --exclude 'composer.lock' \ + --exclude '.distignore' \ + --exclude 'ARCHITECTURE.md' \ + --exclude 'CONVENTIONS.md' \ + -e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }}" \ + ./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:~/domains/peptiderepo.com/public_html/wp-content/plugins/prautoblogger/ - # Copy only production files (exclude dev/CI artifacts) - rsync -av --exclude-from='.distignore' ./ build/prautoblogger/ - - # Create the zip - cd build - zip -r "prautoblogger-${{ steps.version.outputs.version }}.zip" prautoblogger/ - ls -lh "prautoblogger-${{ steps.version.outputs.version }}.zip" - - - name: Deploy to WordPress - env: - DEPLOY_URL: ${{ secrets.DEPLOY_URL }} - DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + - name: Purge LiteSpeed Cache run: | - VERSION="${{ steps.version.outputs.version }}" - ZIP_PATH="build/prautoblogger-${VERSION}.zip" + ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} \ + ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} \ + "cd ~/domains/peptiderepo.com/public_html && find wp-content/litespeed/ -type f -delete 2>/dev/null; touch .lscache_purge 2>/dev/null || true" - echo "Deploying PRAutoBlogger v${VERSION} to production..." - - RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST "${DEPLOY_URL}" \ - -H "X-Deploy-Key: ${DEPLOY_KEY}" \ - -F "plugin=@${ZIP_PATH}" \ - --max-time 120) - - HTTP_CODE=$(echo "$RESPONSE" | tail -1) - BODY=$(echo "$RESPONSE" | head -n -1) - - echo "HTTP ${HTTP_CODE}: ${BODY}" - - if [ "$HTTP_CODE" -ne 200 ]; then - echo "::error::Deployment failed with HTTP ${HTTP_CODE}" + - name: Health check + run: | + sleep 5 + STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com) + if [ "$STATUS" != "200" ]; then + echo "WARNING: Site returned HTTP $STATUS after deploy" exit 1 fi + echo "Site is healthy (HTTP 200)" - echo "Deployment successful!" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: prautoblogger-${{ steps.version.outputs.version }} - path: build/prautoblogger-${{ steps.version.outputs.version }}.zip - retention-days: 30 + - name: Cleanup + if: always() + run: rm -f ~/.ssh/deploy_key diff --git a/CHANGELOG.md b/CHANGELOG.md index a30e0a6..7b6fcd6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ 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/). +## [0.2.0] — 2026-04-12 + +### Changed +- **Reddit data source migrated from OAuth to PullPush.io + .json fallback.** + Reddit rejected our API application; PullPush.io is free, requires no auth, + and provides better subreddit-wide comment search. Reddit .json endpoints + serve as an automatic fallback when PullPush is down. +- Removed Reddit Client ID and Client Secret fields from admin API Keys tab. +- Updated "Test Connections" results to show which Reddit source is active + (PullPush.io or .json fallback) instead of generic "Reddit API connected." +- Updated "Enabled Sources" checkbox label from "Reddit" to "Reddit (via PullPush.io)". + +### Added +- Source status indicator in API Keys tab showing PullPush.io and Reddit .json + availability with live status dots and last-collection timestamp. +- Configurable research cache TTL (1–72 hours) in Sources tab. +- Reddit time window selector (24h / week / month) in Sources tab. +- Posts-per-subreddit limit (5–100) in Sources tab. +- LiteSpeed cache purge step in CI/CD deploy pipeline. + +### Removed +- `class-reddit-api-client.php` — replaced by `class-pull-push-client.php` + and `class-reddit-json-client.php`. +- Reddit OAuth credential fields and encrypted storage entry. + +### Fixed +- Deploy pipeline now purges LiteSpeed/OPcache after file extraction, + preventing stale PHP bytecode from being served. + ## [0.1.0] — 2026-04-10 ### Added diff --git a/assets/css/admin.css b/assets/css/admin.css index 4e00cd3..b93be6e 100755 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -259,6 +259,19 @@ input[type="time"].ab-input { max-width: 160px; } .ab-log-pagination .page-numbers { display: inline-block; padding: 4px 10px; margin: 0 2px; text-decoration: none; } .ab-log-pagination .page-numbers.current { background: var(--ab-primary); color: #fff; border-radius: 3px; } +/* ── Source Status Indicator ──────────────────────────────────────────── */ +.ab-source-status { display: flex; flex-direction: column; gap: 8px; } +.ab-source-row { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--ab-bg); border-radius: 6px; } +.ab-source-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.ab-status-ok { background-color: var(--ab-success); } +.ab-status-warn { background-color: var(--ab-warning); } +.ab-status-error { background-color: var(--ab-danger); } +.ab-status-unknown { background-color: var(--ab-text-muted); } +.ab-source-badge { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 6px; border-radius: 3px; background: var(--ab-primary-light); color: var(--ab-primary); } +.ab-badge-secondary { background: var(--ab-bg); color: var(--ab-text-muted); } +.ab-source-label { color: var(--ab-text-muted); font-size: 13px; margin-left: auto; } +.ab-source-meta { font-size: 12px; color: var(--ab-text-muted); padding: 4px 12px 0; } + /* ── Responsive ──────────────────────────────────────────────────────── */ @media screen and (max-width: 900px) { .ab-layout { flex-direction: column; } diff --git a/includes/admin/class-admin-page.php b/includes/admin/class-admin-page.php index 74ad0c2..2d78e75 100755 --- a/includes/admin/class-admin-page.php +++ b/includes/admin/class-admin-page.php @@ -151,6 +151,10 @@ class PRAutoBlogger_Admin_Page { } break; + case 'source_status': + $this->render_source_status_field(); + break; + case 'author_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; @@ -168,6 +172,74 @@ class PRAutoBlogger_Admin_Page { } } + /** + * Render the Reddit source status indicator. + * + * Shows PullPush.io (primary) and Reddit .json (fallback) availability + * with live status checks via transient cache and rate limit info. + * + * Side effects: Reads transients for cached availability status. + * + * @return void + */ + private function render_source_status_field(): void { + $pullpush_available = get_transient( 'prautoblogger_pullpush_available' ); + $last_collection = get_option( 'prautoblogger_last_collection_time', '' ); + $last_source_used = get_option( 'prautoblogger_last_source_used', '' ); + + // Determine status display. + $pp_status_class = 'ab-status-unknown'; + $pp_status_label = __( 'Unknown', 'prautoblogger' ); + if ( true === $pullpush_available ) { + $pp_status_class = 'ab-status-ok'; + $pp_status_label = __( 'Available', 'prautoblogger' ); + } elseif ( false === $pullpush_available ) { + $pp_status_class = 'ab-status-warn'; + $pp_status_label = __( 'Down — using fallback', 'prautoblogger' ); + } + + echo '
'; + + // PullPush status. + printf( + '
' + . '' + . '%s %s' + . '%s' + . '
', + esc_attr( $pp_status_class ), + esc_html__( 'PullPush.io', 'prautoblogger' ), + esc_html__( 'Primary', 'prautoblogger' ), + esc_html( $pp_status_label ) + ); + + // Reddit .json fallback status. + printf( + '
' + . '' + . '%s %s' + . '%s' + . '
', + esc_html__( 'Reddit .json', 'prautoblogger' ), + esc_html__( 'Fallback', 'prautoblogger' ), + esc_html__( 'Always available (rate-limited)', 'prautoblogger' ) + ); + + // Last collection info. + if ( '' !== $last_collection ) { + $time_ago = human_time_diff( (int) $last_collection, time() ); + printf( + '
%s %s %s
', + esc_html__( 'Last collection:', 'prautoblogger' ), + /* translators: %s is a human-readable time difference like "2 hours" */ + esc_html( sprintf( __( '%s ago', 'prautoblogger' ), $time_ago ) ), + '' !== $last_source_used ? esc_html( sprintf( __( 'via %s', 'prautoblogger' ), $last_source_used ) ) : '' + ); + } + + echo '
'; + } + /** * Sanitize a settings field value. * @@ -181,7 +253,7 @@ class PRAutoBlogger_Admin_Page { $option_name = substr( $filter, strlen( 'sanitize_option_' ) ); } - $encrypted = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_reddit_client_secret', 'prautoblogger_ga4_credentials_json' ]; + $encrypted = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_ga4_credentials_json' ]; if ( in_array( $option_name, $encrypted, true ) ) { if ( '' === $value ) { return get_option( $option_name, '' ); } return PRAutoBlogger_Encryption::encrypt( sanitize_text_field( $value ) ); @@ -197,7 +269,7 @@ class PRAutoBlogger_Admin_Page { return sanitize_text_field( $value ); } - $numeric = [ 'prautoblogger_daily_article_target', 'prautoblogger_monthly_budget_usd', 'prautoblogger_min_word_count', 'prautoblogger_max_word_count', 'prautoblogger_default_author', 'prautoblogger_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', 'prautoblogger_pullpush_cache_ttl', 'prautoblogger_reddit_posts_per_subreddit' ]; if ( in_array( $option_name, $numeric, true ) ) { return is_numeric( $value ) ? $value : 0; } diff --git a/includes/admin/class-settings-fields.php b/includes/admin/class-settings-fields.php index 6a3220a..207cae3 100755 --- a/includes/admin/class-settings-fields.php +++ b/includes/admin/class-settings-fields.php @@ -77,18 +77,12 @@ class PRAutoBlogger_Settings_Fields { 'icon' => '🔑', ], [ - 'id' => 'prautoblogger_reddit_client_id', - 'label' => __( 'Reddit Client ID', 'prautoblogger' ), - 'type' => 'text', + 'id' => 'prautoblogger_reddit_source_status', + 'label' => __( 'Reddit Data Source', 'prautoblogger' ), + 'type' => 'source_status', 'section' => 'prautoblogger_api', - 'description' => __( 'Create a script app at reddit.com/prefs/apps', 'prautoblogger' ), - 'icon' => '🤖', - ], - [ - 'id' => 'prautoblogger_reddit_client_secret', - 'label' => __( 'Reddit Client Secret', 'prautoblogger' ), - 'type' => 'password', - 'section' => 'prautoblogger_api', + 'description' => __( 'Uses PullPush.io (primary) and Reddit .json (fallback). No API key required.', 'prautoblogger' ), + 'icon' => '📡', ], // ── AI Models ─────────────────────────────────────────────── @@ -191,7 +185,7 @@ class PRAutoBlogger_Settings_Fields { 'type' => 'checkboxes', 'section' => 'prautoblogger_sources', 'options' => [ - 'reddit' => __( 'Reddit', 'prautoblogger' ), + 'reddit' => __( 'Reddit (via PullPush.io)', 'prautoblogger' ), 'tiktok' => __( 'TikTok (coming soon)', 'prautoblogger' ), 'instagram' => __( 'Instagram (coming soon)', 'prautoblogger' ), 'youtube' => __( 'YouTube (coming soon)', 'prautoblogger' ), @@ -199,6 +193,39 @@ class PRAutoBlogger_Settings_Fields { 'default' => '["reddit"]', 'description' => __( 'Select which platforms to monitor for topics.', 'prautoblogger' ), ], + [ + 'id' => 'prautoblogger_pullpush_cache_ttl', + 'label' => __( 'Research Cache (hours)', 'prautoblogger' ), + 'type' => 'number', + 'section' => 'prautoblogger_sources', + 'default' => 6, + 'min' => 1, + 'max' => 72, + 'description' => __( 'How long to cache PullPush research results before re-fetching. Saves API calls on low-traffic sites.', 'prautoblogger' ), + ], + [ + 'id' => 'prautoblogger_reddit_time_filter', + 'label' => __( 'Reddit Time Window', 'prautoblogger' ), + 'type' => 'select', + 'section' => 'prautoblogger_sources', + 'default' => 'day', + 'options' => [ + 'day' => __( 'Past 24 hours', 'prautoblogger' ), + 'week' => __( 'Past week', 'prautoblogger' ), + 'month' => __( 'Past month', 'prautoblogger' ), + ], + 'description' => __( 'How far back to search for trending posts and comments.', 'prautoblogger' ), + ], + [ + 'id' => 'prautoblogger_reddit_posts_per_subreddit', + 'label' => __( 'Posts per Subreddit', 'prautoblogger' ), + 'type' => 'number', + 'section' => 'prautoblogger_sources', + 'default' => 25, + 'min' => 5, + 'max' => 100, + 'description' => __( 'Maximum posts to fetch per subreddit per collection run.', 'prautoblogger' ), + ], // ── Schedule & Budget ─────────────────────────────────────── [ diff --git a/includes/class-prautoblogger.php b/includes/class-prautoblogger.php index 0fde88a..ca00f9f 100644 --- a/includes/class-prautoblogger.php +++ b/includes/class-prautoblogger.php @@ -276,10 +276,13 @@ class PRAutoBlogger { : [ 'status' => 'error', 'message' => __( 'OpenRouter connection failed. Check your API key.', 'prautoblogger' ) ]; } if ( 'reddit' === $service || 'all' === $service ) { - $reddit = new PRAutoBlogger_Reddit_Provider(); - $results['reddit'] = $reddit->validate_credentials() - ? [ 'status' => 'ok', 'message' => __( 'Reddit API connected.', 'prautoblogger' ) ] - : [ 'status' => 'error', 'message' => __( 'Reddit connection failed. Check client ID/secret.', 'prautoblogger' ) ]; + $reddit = new PRAutoBlogger_Reddit_Provider(); + $is_ok = $reddit->validate_credentials(); + $pp_up = get_transient( 'prautoblogger_pullpush_available' ); + $source = true === $pp_up ? 'PullPush.io' : 'Reddit .json fallback'; + $results['reddit'] = $is_ok + ? [ 'status' => 'ok', 'message' => sprintf( __( 'Reddit source available via %s.', 'prautoblogger' ), $source ) ] + : [ 'status' => 'error', 'message' => __( 'Both PullPush.io and Reddit .json are unreachable.', 'prautoblogger' ) ]; } wp_send_json_success( $results ); } diff --git a/prautoblogger.php b/prautoblogger.php index 5bb9c7a..92ce91f 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -8,7 +8,7 @@ declare(strict_types=1); * 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 + * Version: 0.2.0 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: PeptideRepo @@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { | and limits without magic strings. */ -define( 'PRAUTOBLOGGER_VERSION', '0.1.0' ); +define( 'PRAUTOBLOGGER_VERSION', '0.2.0' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); diff --git a/readme.txt b/readme.txt index 60d4bd8..78ac1e3 100755 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: ai, content generation, blogging, seo, automation Requires at least: 6.0 Tested up to: 6.7 Requires PHP: 7.4 -Stable tag: 0.1.0 +Stable tag: 0.2.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -32,10 +32,10 @@ Uses OpenRouter for access to models from Anthropic, OpenAI, Google, Meta, and m 1. Upload the `prautoblogger` folder to the `/wp-content/plugins/` directory. 2. Activate the plugin through the 'Plugins' menu in WordPress. -3. Go to PRAutoBlogger > Settings to configure your API keys and preferences. -4. Add your OpenRouter API key and Reddit app credentials. +3. Go to PRAutoBlogger > Settings to configure your API key and preferences. +4. Add your OpenRouter API key. 5. Configure your target subreddits and content preferences. -6. Click "Test Connections" to verify setup. +6. Click "Test Connections" to verify setup (Reddit sources use PullPush.io — no API key needed). 7. Click "Generate Now" for your first article, or wait for the daily schedule. == Frequently Asked Questions == @@ -48,11 +48,19 @@ Costs depend on your chosen models and article volume. With default settings (Cl By default, articles go through the chief editor agent. Articles that pass review are auto-published. Articles that fail review are saved as drafts for manual review. -= How do I set up Reddit API access? = += Do I need a Reddit API key? = -Go to reddit.com/prefs/apps, create a "script" type application. Copy the client ID and secret into the PRAutoBlogger settings page. +No. PRAutoBlogger uses PullPush.io as its primary data source for Reddit content, with Reddit .json endpoints as a fallback. Both are free and require no authentication. You only need an OpenRouter API key for the AI models. == Changelog == += 0.2.0 = +* Replaced Reddit OAuth with PullPush.io (primary) and Reddit .json (fallback) — no API key needed. +* Removed Reddit Client ID/Secret fields from admin settings. +* Added source status indicator showing PullPush and fallback availability. +* Added configurable research cache TTL, time window, and posts-per-subreddit settings. +* Updated Test Connections to show which Reddit source is active. +* Fixed deployment pipeline to purge LiteSpeed cache after deploy. + = 0.1.0 = * Initial release with Reddit source, OpenRouter LLM, and full content pipeline.