From 0a15e266cd5e0db0f164bc9bda2b482be7f1f969 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sun, 19 Apr 2026 14:21:02 +0700 Subject: [PATCH] feat: add Writing Model, Image Model, and Cost columns to posts list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.4.1 — Three new columns on the WordPress Posts admin page showing which LLM wrote each article, which model generated its featured image, and the total API cost. Model names shortened for compact display with full ID in tooltip. Cost queries the generation_log table, skipping the DB hit entirely for non-generated posts. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 11 ++ includes/admin/class-post-list-columns.php | 186 +++++++++++++++++++++ includes/class-prautoblogger.php | 2 + prautoblogger.php | 4 +- 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 includes/admin/class-post-list-columns.php 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__ ) );