feat: add Ideas browser admin page (v0.5.1)
New submenu page showing all article ideas from the content analyzer. Displays title, type, relevance score, frequency, key points, and keywords with type filtering and pagination. Gives live visibility into the idea pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,7 @@ prautoblogger/
|
||||
│ │ ├── class-dashboard-widget.php # WP Dashboard widget showing generation status
|
||||
│ │ ├── class-post-metabox.php # Metabox on posts showing generation metadata
|
||||
│ │ ├── class-metrics-page.php # Admin page for cost dashboard and content scores
|
||||
│ │ ├── class-ideas-browser.php # Browse all analysis results (article ideas)
|
||||
│ │ ├── class-review-queue.php # Approve/edit/reject queue for generated drafts
|
||||
│ │ └── class-log-viewer.php # Activity log viewer with level filtering
|
||||
│ │
|
||||
|
||||
@@ -5,6 +5,15 @@ 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.5.1] — 2026-04-20
|
||||
|
||||
### Added
|
||||
- **Ideas browser admin page.** New "Ideas" submenu under PRAutoBlogger showing
|
||||
all article ideas collected by the content analyzer. Displays suggested title,
|
||||
topic, type (question/complaint/comparison/news/guide), relevance score bar,
|
||||
frequency, key points, and target keywords. Filterable by type with count
|
||||
badges. Paginated at 30 per page, newest first.
|
||||
|
||||
## [0.5.0] — 2026-04-20
|
||||
|
||||
### Added
|
||||
|
||||
@@ -58,6 +58,7 @@ class PRAutoBlogger_Admin_Page {
|
||||
'toplevel_page_' . self::PAGE_SLUG,
|
||||
'prautoblogger_page_prautoblogger-metrics',
|
||||
'prautoblogger_page_prautoblogger-review-queue',
|
||||
'prautoblogger_page_prautoblogger-ideas',
|
||||
'prautoblogger_page_prautoblogger-logs',
|
||||
];
|
||||
if ( ! in_array( $hook_suffix, $pages, true ) ) {
|
||||
|
||||
135
includes/admin/class-ideas-browser.php
Normal file
135
includes/admin/class-ideas-browser.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Admin page listing analysis results (article ideas) from all pipeline runs.
|
||||
*
|
||||
* Shows every idea the content analyzer has surfaced: topic, type, relevance
|
||||
* score, frequency, source, and metadata (key points, keywords). Filterable
|
||||
* by type and source, with pagination. Gives the CEO a live view of the idea
|
||||
* pipeline without needing to trigger a generation run.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::register_admin_hooks() on `admin_menu`.
|
||||
* Dependencies: WordPress $wpdb for direct table queries.
|
||||
*
|
||||
* @see core/class-content-analyzer.php — Produces the rows this page displays.
|
||||
* @see models/class-analysis-result.php — Data model for analysis results.
|
||||
* @see templates/admin/ideas-browser.php — Renders the HTML.
|
||||
* @see ARCHITECTURE.md — Data flow step 2.
|
||||
*/
|
||||
class PRAutoBlogger_Ideas_Browser {
|
||||
|
||||
private const PER_PAGE = 30;
|
||||
|
||||
/**
|
||||
* Register the Ideas submenu page under PRAutoBlogger.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_menu(): void {
|
||||
add_submenu_page(
|
||||
'prautoblogger-settings',
|
||||
__( 'Ideas', 'prautoblogger' ),
|
||||
__( 'Ideas', 'prautoblogger' ),
|
||||
'manage_options',
|
||||
'prautoblogger-ideas',
|
||||
[ $this, 'render_page' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ideas browser page.
|
||||
*
|
||||
* Reads filter/pagination parameters from $_GET, queries the analysis_results
|
||||
* table, and includes the template.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$paged = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
|
||||
$type_filter = isset( $_GET['type'] ) ? sanitize_key( $_GET['type'] ) : '';
|
||||
$source_filter = isset( $_GET['source'] ) ? sanitize_key( $_GET['source'] ) : '';
|
||||
|
||||
$result = $this->query_ideas( $paged, $type_filter, $source_filter );
|
||||
$rows = $result['rows'];
|
||||
$total = $result['total'];
|
||||
$total_pages = (int) ceil( $total / self::PER_PAGE );
|
||||
$type_counts = $this->get_type_counts();
|
||||
|
||||
include PRAUTOBLOGGER_PLUGIN_DIR . 'templates/admin/ideas-browser.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Query analysis results with optional filtering and pagination.
|
||||
*
|
||||
* Side effects: database SELECT.
|
||||
*
|
||||
* @param int $paged Page number (1-based).
|
||||
* @param string $type Filter by analysis_type (empty = all).
|
||||
* @param string $source Filter: 'llm_research' or 'reddit' based on source_ids content.
|
||||
* @return array{rows: array[], total: int}
|
||||
*/
|
||||
private function query_ideas( int $paged, string $type, string $source ): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ( '' !== $type ) {
|
||||
$where[] = 'analysis_type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
|
||||
$where_sql = empty( $where ) ? '' : 'WHERE ' . implode( ' AND ', $where );
|
||||
$offset = ( $paged - 1 ) * self::PER_PAGE;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$total = (int) $wpdb->get_var(
|
||||
empty( $params )
|
||||
? "SELECT COUNT(*) FROM {$table} {$where_sql}"
|
||||
: $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where_sql}", ...$params )
|
||||
);
|
||||
|
||||
$order_sql = 'ORDER BY analyzed_at DESC, relevance_score DESC';
|
||||
$limit_sql = sprintf( 'LIMIT %d OFFSET %d', self::PER_PAGE, $offset );
|
||||
$full_sql = "SELECT * FROM {$table} {$where_sql} {$order_sql} {$limit_sql}";
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$rows = empty( $params )
|
||||
? $wpdb->get_results( $full_sql, ARRAY_A )
|
||||
: $wpdb->get_results( $wpdb->prepare( $full_sql, ...$params ), ARRAY_A );
|
||||
|
||||
return [
|
||||
'rows' => $rows ?: [],
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get counts per analysis_type for the filter badges.
|
||||
*
|
||||
* Side effects: database SELECT.
|
||||
*
|
||||
* @return array<string, int> Map of analysis_type => count.
|
||||
*/
|
||||
private function get_type_counts(): array {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT analysis_type, COUNT(*) AS cnt FROM {$table} GROUP BY analysis_type ORDER BY cnt DESC",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$counts = [];
|
||||
foreach ( $rows ?: [] as $row ) {
|
||||
$counts[ $row['analysis_type'] ] = (int) $row['cnt'];
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ class PRAutoBlogger {
|
||||
$review_queue = new PRAutoBlogger_Review_Queue();
|
||||
add_action( 'admin_menu', [ $review_queue, 'on_register_menu' ] );
|
||||
|
||||
add_action( 'admin_menu', [ new PRAutoBlogger_Ideas_Browser(), 'on_register_menu' ] );
|
||||
add_action( 'admin_menu', [ new PRAutoBlogger_Log_Viewer(), 'on_register_menu' ] );
|
||||
|
||||
( new PRAutoBlogger_Post_List_Columns() )->register();
|
||||
|
||||
@@ -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.5.0
|
||||
* Version: 0.5.1
|
||||
* 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.5.0' );
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.5.1' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
153
templates/admin/ideas-browser.php
Normal file
153
templates/admin/ideas-browser.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Ideas browser template — lists analysis results (article ideas) from the pipeline.
|
||||
*
|
||||
* Variables available from PRAutoBlogger_Ideas_Browser::render_page():
|
||||
* $rows (array[]) — analysis result rows from the database.
|
||||
* $total (int) — total row count (before pagination).
|
||||
* $total_pages (int) — total number of pages.
|
||||
* $type_filter (string) — active type filter (empty = all).
|
||||
* $type_counts (array) — map of analysis_type => count for filter badges.
|
||||
* $paged (int) — current page number.
|
||||
*
|
||||
* @see admin/class-ideas-browser.php — Renders this template.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$base_url = admin_url( 'admin.php?page=prautoblogger-ideas' );
|
||||
?>
|
||||
<div class="wrap prautoblogger-ideas-browser">
|
||||
<h1><?php esc_html_e( 'PRAutoBlogger — Ideas', 'prautoblogger' ); ?></h1>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: total idea count */
|
||||
esc_html__( '%d ideas collected from analysis runs. Newest first.', 'prautoblogger' ),
|
||||
$total
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php if ( ! empty( $type_counts ) ) : ?>
|
||||
<div class="prautoblogger-ideas-filters" style="margin: 12px 0;">
|
||||
<a href="<?php echo esc_url( $base_url ); ?>"
|
||||
class="button <?php echo '' === $type_filter ? 'button-primary' : ''; ?>"
|
||||
style="margin-right: 4px;">
|
||||
<?php printf( '%s (%d)', esc_html__( 'All', 'prautoblogger' ), $total ); ?>
|
||||
</a>
|
||||
<?php foreach ( $type_counts as $type_name => $count ) : ?>
|
||||
<a href="<?php echo esc_url( add_query_arg( 'type', $type_name, $base_url ) ); ?>"
|
||||
class="button <?php echo $type_filter === $type_name ? 'button-primary' : ''; ?>"
|
||||
style="margin-right: 4px;">
|
||||
<?php echo esc_html( ucfirst( $type_name ) ) . ' (' . (int) $count . ')'; ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( empty( $rows ) ) : ?>
|
||||
<div class="prautoblogger-empty-queue">
|
||||
<p><?php esc_html_e( 'No ideas found. Ideas will appear here after the next generation run collects and analyzes source data.', 'prautoblogger' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped prautoblogger-ideas-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30%"><?php esc_html_e( 'Title / Topic', 'prautoblogger' ); ?></th>
|
||||
<th style="width:8%"><?php esc_html_e( 'Type', 'prautoblogger' ); ?></th>
|
||||
<th style="width:6%"><?php esc_html_e( 'Score', 'prautoblogger' ); ?></th>
|
||||
<th style="width:6%"><?php esc_html_e( 'Freq', 'prautoblogger' ); ?></th>
|
||||
<th style="width:35%"><?php esc_html_e( 'Key Points / Keywords', 'prautoblogger' ); ?></th>
|
||||
<th style="width:15%"><?php esc_html_e( 'Analyzed', 'prautoblogger' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $rows as $row ) : ?>
|
||||
<?php
|
||||
$meta = json_decode( $row['metadata_json'] ?? '{}', true ) ?: [];
|
||||
$suggested_title = $meta['suggested_title'] ?? '';
|
||||
$key_points = $meta['key_points'] ?? [];
|
||||
$keywords = $meta['target_keywords'] ?? [];
|
||||
$score_pct = round( (float) $row['relevance_score'] * 100 );
|
||||
$type_label = ucfirst( $row['analysis_type'] ?? 'unknown' );
|
||||
$type_class = 'prab-type-' . sanitize_html_class( $row['analysis_type'] );
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo esc_html( '' !== $suggested_title ? $suggested_title : $row['topic'] ); ?></strong>
|
||||
<?php if ( '' !== $suggested_title && $suggested_title !== $row['topic'] ) : ?>
|
||||
<div style="color:#666; font-size:12px; margin-top:2px;">
|
||||
<?php echo esc_html( $row['topic'] ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( ! empty( $row['summary'] ) ) : ?>
|
||||
<div style="color:#555; font-size:12px; margin-top:4px; line-height:1.4;">
|
||||
<?php echo esc_html( $row['summary'] ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="<?php echo esc_attr( $type_class ); ?>" style="padding:2px 8px; border-radius:3px; font-size:12px; background:#f0f0f1;">
|
||||
<?php echo esc_html( $type_label ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$bar_color = $score_pct >= 70 ? '#00a32a' : ( $score_pct >= 40 ? '#dba617' : '#d63638' );
|
||||
?>
|
||||
<div style="display:flex; align-items:center; gap:6px;">
|
||||
<div style="width:40px; height:6px; background:#ddd; border-radius:3px; overflow:hidden;">
|
||||
<div style="width:<?php echo (int) $score_pct; ?>%; height:100%; background:<?php echo esc_attr( $bar_color ); ?>;"></div>
|
||||
</div>
|
||||
<span style="font-size:12px;"><?php echo (int) $score_pct; ?>%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center;"><?php echo (int) $row['frequency']; ?></td>
|
||||
<td>
|
||||
<?php if ( ! empty( $key_points ) ) : ?>
|
||||
<ul style="margin:0; padding-left:16px; font-size:12px; line-height:1.5;">
|
||||
<?php foreach ( array_slice( $key_points, 0, 4 ) as $point ) : ?>
|
||||
<li><?php echo esc_html( $point ); ?></li>
|
||||
<?php endforeach; ?>
|
||||
<?php if ( count( $key_points ) > 4 ) : ?>
|
||||
<li style="color:#666;">+<?php echo count( $key_points ) - 4; ?> more</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<?php if ( ! empty( $keywords ) ) : ?>
|
||||
<div style="margin-top:4px;">
|
||||
<?php foreach ( array_slice( $keywords, 0, 5 ) as $kw ) : ?>
|
||||
<span style="display:inline-block; padding:1px 6px; margin:1px 2px; background:#e7f5fe; border-radius:3px; font-size:11px; color:#0073aa;">
|
||||
<?php echo esc_html( $kw ); ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:12px; color:#666;">
|
||||
<?php echo esc_html( wp_date( 'M j, g:i A', strtotime( $row['analyzed_at'] ) ) ); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ( $total_pages > 1 ) : ?>
|
||||
<div class="prautoblogger-pagination" style="margin-top: 12px;">
|
||||
<?php
|
||||
echo wp_kses_post( paginate_links( [
|
||||
'base' => add_query_arg( 'paged', '%#%' ),
|
||||
'format' => '',
|
||||
'current' => $paged,
|
||||
'total' => $total_pages,
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
] ) );
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Reference in New Issue
Block a user