diff --git a/CHANGELOG.md b/CHANGELOG.md index c03b950..c2862de 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ 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.4.1] — 2026-04-19 + +### Added +- **Posts list columns: Writing Model, Image Model, Cost.** Three new columns + on the WordPress Posts admin page. Writing Model shows the LLM that wrote the + article, Image Model shows the model that generated the featured image, and + Cost shows the total API spend for generating that article (summed from the + generation log). All columns show "—" for non-generated posts. Writing Model + is sortable. Model names are shortened (provider prefix stripped) with full + ID in a tooltip. + ## [0.4.0] — 2026-04-19 ### Added diff --git a/includes/admin/class-post-list-columns.php b/includes/admin/class-post-list-columns.php new file mode 100644 index 0000000..7cc296c --- /dev/null +++ b/includes/admin/class-post-list-columns.php @@ -0,0 +1,186 @@ + $columns Existing columns. + * @return array Modified columns. + */ + public function filter_add_columns( array $columns ): array { + $new = []; + foreach ( $columns as $key => $label ) { + $new[ $key ] = $label; + if ( 'title' === $key ) { + $new['prab_writing_model'] = __( 'Writing Model', 'prautoblogger' ); + $new['prab_image_model'] = __( 'Image Model', 'prautoblogger' ); + $new['prab_cost'] = __( 'Cost', 'prautoblogger' ); + } + } + return $new; + } + + /** + * Render cell content for our custom columns. + * + * @param string $column Column identifier. + * @param int $post_id Current post ID. + */ + public function on_render_column( string $column, int $post_id ): void { + switch ( $column ) { + case 'prab_writing_model': + $this->render_model_cell( + (string) get_post_meta( $post_id, '_prautoblogger_model_used', true ) + ); + break; + + case 'prab_image_model': + $this->render_image_model_cell( $post_id ); + break; + + case 'prab_cost': + $this->render_cost_cell( $post_id ); + break; + } + } + + /** + * Make the writing model column sortable. + * + * @param array $columns Sortable column map. + * @return array + */ + public function filter_sortable_columns( array $columns ): array { + $columns['prab_writing_model'] = 'prab_writing_model'; + return $columns; + } + + /** + * Handle sorting by writing model meta value. + * + * @param \WP_Query $query The admin posts query. + */ + public function on_sort_by_model( \WP_Query $query ): void { + if ( ! is_admin() || ! $query->is_main_query() ) { + return; + } + if ( 'prab_writing_model' !== $query->get( 'orderby' ) ) { + return; + } + $query->set( 'meta_key', '_prautoblogger_model_used' ); + $query->set( 'orderby', 'meta_value' ); + } + + // ── Private helpers ───────────────────────────────────────────────── + + /** + * Render a model name cell with shortened display and full tooltip. + * + * @param string $model Full model identifier (may be empty). + */ + private function render_model_cell( string $model ): void { + if ( '' === $model ) { + echo '—'; + return; + } + printf( + '%s', + esc_attr( $model ), + esc_html( $this->shorten( $model ) ) + ); + } + + /** + * Look up the featured image's model and render it. + * + * @param int $post_id The post ID. + */ + private function render_image_model_cell( int $post_id ): void { + $thumbnail_id = (int) get_post_thumbnail_id( $post_id ); + if ( 0 === $thumbnail_id ) { + echo '—'; + return; + } + $model = (string) get_post_meta( $thumbnail_id, '_prautoblogger_image_model', true ); + $this->render_model_cell( $model ); + } + + /** + * Sum all generation_log costs for this post and render. + * + * @param int $post_id The post ID. + */ + private function render_cost_cell( int $post_id ): void { + // Only query for posts we generated — skip the DB hit for regular posts. + $is_generated = get_post_meta( $post_id, '_prautoblogger_generated', true ); + if ( '1' !== $is_generated ) { + echo '—'; + return; + } + + global $wpdb; + $table = $wpdb->prefix . 'prautoblogger_generation_log'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $total = (float) $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM(estimated_cost) FROM {$table} WHERE post_id = %d", + $post_id + ) + ); + + if ( $total <= 0.0 ) { + echo '$0.00'; + return; + } + + // Show 4 decimals for sub-cent precision, 2 decimals for larger amounts. + $formatted = $total < 0.01 + ? sprintf( '$%.4f', $total ) + : sprintf( '$%.2f', $total ); + + printf( + '%s', + esc_html( $formatted ) + ); + } + + /** + * Strip provider prefix for compact display. + * + * @param string $model Full model identifier. + * @return string Shortened name. + */ + private function shorten( string $model ): string { + $pos = strpos( $model, '/' ); + return false !== $pos ? substr( $model, $pos + 1 ) : $model; + } +} diff --git a/includes/class-prautoblogger.php b/includes/class-prautoblogger.php index d15a12f..f9b884c 100644 --- a/includes/class-prautoblogger.php +++ b/includes/class-prautoblogger.php @@ -75,6 +75,8 @@ class PRAutoBlogger { add_action( 'admin_menu', [ new PRAutoBlogger_Log_Viewer(), 'on_register_menu' ] ); + ( new PRAutoBlogger_Post_List_Columns() )->register(); + // Block false update notifications — our slug collides with another plugin. add_filter( 'site_transient_update_plugins', [ $this, 'filter_block_false_updates' ] ); } diff --git a/prautoblogger.php b/prautoblogger.php index 6c5a9d2..6722a3b 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.4.0 + * Version: 0.4.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.4.0' ); +define( 'PRAUTOBLOGGER_VERSION', '0.4.1' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );