The monolithic Peptide_News_LLM class mixed HTTP transport, prompt
construction, batch orchestration, and AJAX handling. Split into:
- class-peptide-news-llm-client.php (199 lines) — OpenRouter HTTP
transport with retry logic and response parsing
- class-peptide-news-llm-prompt-builder.php (142 lines) — keyword
and summary prompt construction, article text fetching
- class-peptide-news-llm-ajax.php (87 lines) — bulk-generate AJAX
handler
- class-peptide-news-llm.php (300 lines) — orchestrator with
backward-compatible proxies for is_valid_model(), call_openrouter(),
and call_openrouter_with_usage()
Extracted run_llm_task() helper to deduplicate the keyword/summary
processing blocks. All public API preserved — no test changes needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New Cost_Tracker class: logs every API call (model, tokens, cost), enforces
monthly budgets with hard-stop mode, provides aggregated reporting
- New llm_costs database table with indexes on created_at, model, operation
- Modified LLM class: call_openrouter_with_usage() extracts token usage from
OpenRouter responses; budget gate checks before each API call
- Admin settings: monthly budget (USD), enforcement mode (disabled/warn/hard-stop),
cost log retention period
- Admin cost dashboard: budget progress bar, daily cost chart (Chart.js),
per-model and per-operation breakdowns, recent API call log
- PHPUnit tests: 16 tests covering cost calculation, pricing lookups,
budget enforcement logic, and model validation
- Updated ARCHITECTURE.md with new table schema, data flow, and key decisions
- Updated CONVENTIONS.md with "how to add cost tracking for a new operation"
- Version bump to 2.4.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WordPress coding standards require tab indentation. All 12 PHP files
in includes/, admin/, and public/ were using 4-space indentation.
Converted to tabs to pass the Generic.WhiteSpace.DisallowSpaceIndent
PHPCS rule. Whitespace-only change — no logic modifications.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Google News RSS items often have no description or content — just a
title. This left the LLM with insufficient context, resulting in empty
ai_summary fields.
When the stored content is sparse (<100 chars or matches the title),
build_summary_prompt() now fetches the actual article page via
wp_remote_get(), extracts text from <article>/<main> tags, and feeds
it to the LLM. Gracefully falls back to title-only if the fetch fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security (7 fixes):
- SEC-001: Use wpdb->prepare() for COUNT query in REST API
- SEC-002: Set httponly=true on session cookie to prevent XSS
- SEC-003: Mask API keys in admin settings (show last 4 chars only)
- SEC-004: Check capability before nonce in manual fetch handler
- SEC-005: Validate redirect domains to prevent open redirects
- SEC-006: Escape all article fields before REST API response
- SEC-007: Add capability check to AJAX summary handler
Infrastructure (7 fixes):
- WPS-002: Clear cron schedule on plugin deactivation
- WPS-003: Bundle Chart.js locally instead of CDN without SRI
- WPS-004: Add uninstall.php for clean plugin removal
- DB-001: Add foreign key constraints with CASCADE deletes
- DB-002: Clamp pagination to valid page range
- CI-001: Use StrictHostKeyChecking=accept-new in deploy
- CI-002: Add security scanning step to CI pipeline
- CI-003: Add rollback workflow via workflow_dispatch
Performance & Quality (8 fixes):
- PHP-001: Validate widget $instance as array before access
- PHP-002: Return null from get_client_ip() for invalid IPs
- PHP-003: Process RSS feeds in chunked batches of 10
- PHP-004: Wire up clear_article_cache() in fetch cycle
- PERF-001: Cache source names in transient for strip_source_suffix
- PERF-002: Add server-side transient caching for REST API
- ARCH-002: Make LLM API URL configurable via WordPress options
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces Peptide_News_Logger class with leveled logging (info, warning,
error, debug) to a custom DB table, auto-pruning at 2000 rows. Adds a
filterable, paginated log viewer to the admin settings page. Instruments
the LLM and Fetcher classes with structured log calls for fetch cycles,
AI processing, and error tracking. Version bumped to 2.1.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The qwen/qwen3.6-plus-preview:free model returns "No endpoint"
errors. Switch to google/gemma-3-27b-it:free which is confirmed
working on OpenRouter's free tier.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Surface actual API error messages in the admin UI instead of just
showing a generic failure count. This helps diagnose issues with
the OpenRouter API configuration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The configured model qwen/qwen3.6-plus:free doesn't exist on
OpenRouter — the correct ID is qwen/qwen3.6-plus-preview:free.
Add auto-correction in the bulk generation handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Free OpenRouter models are too slow for batch processing on shared
hosting. Reduce batch size from 5 to 1, API timeout from 30s to 15s,
and max retries from 3 to 1 to stay within PHP max_execution_time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous batch size of 50 exceeded PHP max_execution_time on
Hostinger shared hosting (30s). Each article needs ~3s for the
OpenRouter API call, so 5 articles per AJAX request keeps each
batch under 20s. The JS loop still processes all articles
automatically — just in more, smaller batches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a "Generate AI Summaries" button to the Peptide News settings page
that processes all articles missing summaries in batches of 50. The UI
loops automatically until every article has been summarized, showing
real-time progress. The admin batch overrides the per-cycle limit so it
isn't capped at 10 — the existing 120-second timeout guard still applies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all AI thumbnail generation infrastructure (too expensive,
poor results) and rewrite the public-facing article feed as a React
SPA mounted via WordPress shortcode.
Changes:
- Remove thumbnail generation, OG scraping, and image download code
from fetcher, LLM, and admin classes
- Replace server-rendered PHP templates with React component
(PeptideNewsFeed.jsx) using WordPress-bundled React/ReactDOM
- Add Babel build pipeline for JSX transpilation
- Update REST API to serve ai_summary instead of thumbnail fields
- Redesign public CSS: clean card layout, loading skeletons,
dark mode, responsive, no thumbnail imagery
- Remove dead admin settings (llm_image_model, thumbnail_fallback)
- Fix call_openrouter() visibility for Content Filter cross-class use
- Bump version to 1.3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DuckDuckGo's HTML search endpoint now requires vqd session tokens,
making server-side search-based URL resolution unreliable. Simplified
the thumbnail pipeline:
- OG scrape only for non-aggregator URLs (direct publisher pages)
- AI image generation via OpenRouter for aggregator articles (Google News)
- Added Image Model settings field in admin UI
- Added thumbnail_local column for AI-generated images
- Updated public/admin display to check thumbnail_local as fallback
- Removed all DuckDuckGo search code (find_article_url_via_search,
extract_ddg_url, resolve_article_for_scraping)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use $wpdb->prepare() with esc_like() in clear_article_cache()
- Add strict base64_decode() validation with error handling
- Add isset()/is_array()/is_string() checks for nested array access
- Add wp_http_validate_url() SSRF prevention on all image downloads
- Add limit_response_size (5MB) on image URL downloads
- Set chmod 0644 after file_put_contents()