Changeset 3461830
- Timestamp:
- 02/15/2026 01:30:17 PM (7 days ago)
- Location:
- apicoid-ghostwriter
- Files:
-
- 18 edited
- 1 copied
-
tags/1.4.0 (copied) (copied from apicoid-ghostwriter/trunk)
-
tags/1.4.0/CHANGELOG.md (modified) (2 diffs)
-
tags/1.4.0/apicoid-ghostwriter.php (modified) (12 diffs)
-
tags/1.4.0/assets/css/admin.css (modified) (2 diffs)
-
tags/1.4.0/assets/js/admin.js (modified) (1 diff)
-
tags/1.4.0/assets/js/article-generator.js (modified) (1 diff)
-
tags/1.4.0/assets/js/post-edit.js (modified) (1 diff)
-
tags/1.4.0/includes/article-optimizer-page.php (modified) (5 diffs)
-
tags/1.4.0/includes/settings-page.php (modified) (1 diff)
-
tags/1.4.0/readme.txt (modified) (4 diffs)
-
trunk/CHANGELOG.md (modified) (2 diffs)
-
trunk/apicoid-ghostwriter.php (modified) (12 diffs)
-
trunk/assets/css/admin.css (modified) (2 diffs)
-
trunk/assets/js/admin.js (modified) (1 diff)
-
trunk/assets/js/article-generator.js (modified) (1 diff)
-
trunk/assets/js/post-edit.js (modified) (1 diff)
-
trunk/includes/article-optimizer-page.php (modified) (5 diffs)
-
trunk/includes/settings-page.php (modified) (1 diff)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
apicoid-ghostwriter/tags/1.4.0/CHANGELOG.md
r3461338 r3461830 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 8 ## [1.4.0] - 2026-02-15 9 10 ### Added 11 - **Auto Schedule**: One-click post scheduling from the post editor (Classic & Block Editor) with a yellow "Auto Schedule" button 12 - **Auto Schedule Settings**: Configurable daily article limits (`Articles Per Day`), publishing time window (`Start Hour` / `End Hour`) on the Settings page 13 - **Rearrange Queue**: Redistribute all scheduled posts according to current settings while preserving their original order 14 - **Smart Related Article Reinjection**: When auto-scheduling, optionally strip and re-generate related article links to include scheduled posts — only links to posts that will be live before the current post (prevents 404s) 15 - **Confirm Dialog with Preview**: Auto Schedule shows a modal with the proposed date and updated related article links before confirming 16 - **Include Scheduled Content Checkbox**: New option in Generate by Category to include `future` posts in category analysis 17 - **Scheduled Posts in Article Generator**: Future posts now appear in the article list with a blue "Scheduled" badge and their scheduled publish date/time 18 - **Create Support Article for Scheduled Posts**: The "Create Support Article" button now appears for both published and scheduled posts 19 20 ### Changed 21 - **Related Article Label from Preset**: `build_related_articles_html()` and auto-schedule reinjection now read the label from the Default preset instead of the legacy standalone `apicoid_gw_article_related_article_label` option 22 - **Article Generator Query**: Post status query updated to include `future` alongside `publish` and `draft` 23 24 ## [1.3.3] - 2026-02-15 25 26 ### Fixed 27 - **Increased API Timeouts to 10 Minutes**: All API calls to api.co.id (article generation, rewriting, suggestions, image generation) now use 600-second timeout to prevent premature request failures 28 - **Improved AJAX Timeout Handling**: Added 10-minute client-side timeout to Generate Article and Rewrite Article forms that were previously missing explicit timeouts 29 - **Better Error Messages for Timeouts**: Generate and Rewrite forms now show informative messages when server timeout (502/504) occurs, letting users know the article may still be generating in the background 30 - **Fixed False "Request Failed" Errors**: Resolved issue where articles were successfully generated but users saw error alerts due to web server gateway timeouts 7 31 8 32 ## [1.3.2] - 2026-02-14 … … 119 143 - Secure API key validation 120 144 145 [1.4.0]: https://github.com/apicoid/ghostwriter/compare/v1.3.3...v1.4.0 146 [1.3.3]: https://github.com/apicoid/ghostwriter/compare/v1.3.2...v1.3.3 121 147 [1.3.2]: https://github.com/apicoid/ghostwriter/compare/v1.3.1...v1.3.2 122 148 [1.3.1]: https://github.com/apicoid/ghostwriter/compare/v1.3.0...v1.3.1 -
apicoid-ghostwriter/tags/1.4.0/apicoid-ghostwriter.php
r3461407 r3461830 4 4 * Plugin URI: https://wordpress.org/plugins/apicoid-ghostwriter/ 5 5 * Description: Connects your WordPress site to Api.co.id to generate content and rewrite content automatically using AI. Features include article generation, content rewriting, automatic related article linking, SEO integration, and image generation. 6 * Version: 1. 3.36 * Version: 1.4.0 7 7 * Author: Api.co.id 8 8 * Author URI: https://api.co.id … … 21 21 22 22 // Define plugin constants 23 define( 'APICOID_GW_VERSION', '1. 3.3' );23 define( 'APICOID_GW_VERSION', '1.4.0' ); 24 24 define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) ); … … 129 129 // Handle Google Index log clear 130 130 add_action( 'wp_ajax_apicoid_gw_clear_google_index_logs', array( $this, 'ajax_clear_google_index_logs' ) ); 131 132 // Handle Auto Schedule 133 add_action( 'wp_ajax_apicoid_gw_save_auto_schedule_settings', array( $this, 'ajax_save_auto_schedule_settings' ) ); 134 add_action( 'wp_ajax_apicoid_gw_auto_schedule_post', array( $this, 'ajax_auto_schedule_post' ) ); 135 add_action( 'wp_ajax_apicoid_gw_rearrange_schedule_queue', array( $this, 'ajax_rearrange_schedule_queue' ) ); 131 136 132 137 // Auto-submit URL to Google on post publish/update … … 564 569 ); 565 570 571 // Register Auto Schedule settings 572 register_setting( 573 'apicoid_gw_article_settings', 574 'apicoid_gw_auto_schedule_per_day', 575 array( 576 'sanitize_callback' => 'absint', 577 'default' => 3, 578 ) 579 ); 580 581 register_setting( 582 'apicoid_gw_article_settings', 583 'apicoid_gw_auto_schedule_start_hour', 584 array( 585 'sanitize_callback' => 'sanitize_text_field', 586 'default' => '08:00', 587 ) 588 ); 589 590 register_setting( 591 'apicoid_gw_article_settings', 592 'apicoid_gw_auto_schedule_end_hour', 593 array( 594 'sanitize_callback' => 'sanitize_text_field', 595 'default' => '20:00', 596 ) 597 ); 598 566 599 // Register Google Index settings 567 600 register_setting( … … 1183 1216 * @return string Content with related articles inserted. 1184 1217 */ 1185 private function insert_related_articles( $content, $category_ids, $word_count, $current_post_id = 0, $pillar_article_id = 0, $related_article_label = '' ) {1218 private function insert_related_articles( $content, $category_ids, $word_count, $current_post_id = 0, $pillar_article_id = 0, $related_article_label = '', $scheduled_date = '' ) { 1186 1219 if ( empty( $category_ids ) || ! is_array( $category_ids ) || $word_count <= 0 ) { 1187 1220 return $content; … … 1197 1230 1198 1231 if ( $pillar_article_id > 0 ) { 1199 // Verify pillar article exists and is published 1232 // Verify pillar article exists and is published (or scheduled before scheduled_date) 1200 1233 $pillar_post = get_post( $pillar_article_id ); 1201 if ( $pillar_post && 'publish' === $pillar_post->post_status && $pillar_article_id !== $current_post_id ) { 1234 $pillar_valid = false; 1235 if ( $pillar_post && $pillar_article_id !== $current_post_id ) { 1236 if ( 'publish' === $pillar_post->post_status ) { 1237 $pillar_valid = true; 1238 } elseif ( ! empty( $scheduled_date ) && 'future' === $pillar_post->post_status && $pillar_post->post_date < $scheduled_date ) { 1239 $pillar_valid = true; 1240 } 1241 } 1242 if ( $pillar_valid ) { 1202 1243 $related_posts[] = $pillar_article_id; 1203 1244 $pillar_included = true; … … 1213 1254 } 1214 1255 1215 $additional_posts = get_posts( 1216 array( 1217 'post_type' => 'post', 1218 'post_status' => 'publish', 1219 'posts_per_page' => $num_related * 3, // Get more to ensure we have enough 1220 'category__in' => $category_ids, 1221 'post__not_in' => $exclude_ids, 1222 'orderby' => 'rand', 1223 'fields' => 'ids', 1224 ) 1225 ); 1256 $related_query_args = array( 1257 'post_type' => 'post', 1258 'posts_per_page' => $num_related * 3, // Get more to ensure we have enough 1259 'category__in' => $category_ids, 1260 'post__not_in' => $exclude_ids, 1261 'orderby' => 'rand', 1262 'fields' => 'ids', 1263 ); 1264 1265 if ( ! empty( $scheduled_date ) ) { 1266 $related_query_args['post_status'] = array( 'publish', 'future' ); 1267 $related_query_args['date_query'] = array( array( 'before' => $scheduled_date ) ); 1268 } else { 1269 $related_query_args['post_status'] = 'publish'; 1270 } 1271 1272 $additional_posts = get_posts( $related_query_args ); 1226 1273 1227 1274 // Merge pillar article with additional posts … … 1343 1390 1344 1391 if ( empty( $label ) ) { 1345 $label = get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 1392 // Try to get label from the default preset first 1393 $presets = get_option( 'apicoid_gw_article_presets', array() ); 1394 if ( ! empty( $presets['default']['related_article_label'] ) ) { 1395 $label = $presets['default']['related_article_label']; 1396 } else { 1397 $label = get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 1398 } 1346 1399 } 1347 1400 if ( '' === trim( $label ) ) { … … 1747 1800 } 1748 1801 1749 // Get published posts from this category 1802 // Get posts from this category (optionally include scheduled) 1803 $include_scheduled = isset( $_POST['include_scheduled'] ) && '1' === $_POST['include_scheduled']; 1804 $post_status = $include_scheduled ? array( 'publish', 'future' ) : 'publish'; 1805 1750 1806 $args = array( 1751 1807 'post_type' => 'post', 1752 'post_status' => 'publish',1808 'post_status' => $post_status, 1753 1809 'posts_per_page' => 50, // Limit to 50 articles 1754 1810 'cat' => $category_id, … … 2736 2792 'ajax_url' => admin_url( 'admin-ajax.php' ), 2737 2793 'nonce' => wp_create_nonce( 'apicoid_gw_generate_featured_image' ), 2794 'auto_schedule_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_post' ), 2738 2795 'is_block_editor' => $is_block_editor, 2739 2796 'strings' => array( … … 2878 2935 ) 2879 2936 ); 2937 2938 wp_localize_script( 2939 'apicoid-gw-admin-script', 2940 'apicoidGwAutoSchedule', 2941 array( 2942 'ajax_url' => admin_url( 'admin-ajax.php' ), 2943 'save_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_settings' ), 2944 'strings' => array( 2945 'saving' => __( 'Saving...', 'apicoid-ghostwriter' ), 2946 'rearranging' => __( 'Rearranging...', 'apicoid-ghostwriter' ), 2947 'confirm_rearrange' => __( 'This will rearrange all scheduled posts according to current settings. Continue?', 'apicoid-ghostwriter' ), 2948 ), 2949 ) 2950 ); 2880 2951 } 2881 2952 … … 3430 3501 3431 3502 /** 3503 * Count posts on a given date (published + scheduled). 3504 * 3505 * @param string $date_str Date in Y-m-d format. 3506 * @return int 3507 */ 3508 private function count_posts_on_date( $date_str ) { 3509 global $wpdb; 3510 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Lightweight count query for scheduling 3511 return (int) $wpdb->get_var( 3512 $wpdb->prepare( 3513 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status IN ('publish','future') AND DATE(post_date) = %s", 3514 $date_str 3515 ) 3516 ); 3517 } 3518 3519 /** 3520 * Get related articles preview data for the auto-schedule confirm dialog. 3521 * 3522 * @param int $post_id Post ID. 3523 * @param string $scheduled_date Scheduled datetime string. 3524 * @return array Array of [ 'title' => ..., 'url' => ... ]. 3525 */ 3526 private function get_related_articles_for_preview( $post_id, $scheduled_date ) { 3527 $post = get_post( $post_id ); 3528 if ( ! $post ) { 3529 return array(); 3530 } 3531 3532 $categories = wp_get_post_categories( $post_id ); 3533 if ( empty( $categories ) ) { 3534 return array(); 3535 } 3536 3537 $word_count = str_word_count( wp_strip_all_tags( $post->post_content ) ); 3538 if ( $word_count <= 0 ) { 3539 return array(); 3540 } 3541 3542 $num_related = ceil( $word_count / 500 ); 3543 3544 $related = get_posts( array( 3545 'post_type' => 'post', 3546 'post_status' => array( 'publish', 'future' ), 3547 'posts_per_page' => $num_related * 3, 3548 'category__in' => $categories, 3549 'post__not_in' => array( $post_id ), 3550 'date_query' => array( array( 'before' => $scheduled_date ) ), 3551 'orderby' => 'rand', 3552 'fields' => 'ids', 3553 ) ); 3554 3555 $result = array(); 3556 foreach ( array_slice( $related, 0, $num_related ) as $rid ) { 3557 $result[] = array( 3558 'title' => get_the_title( $rid ), 3559 'url' => get_permalink( $rid ), 3560 ); 3561 } 3562 return $result; 3563 } 3564 3565 /** 3566 * Strip existing related article blocks from content. 3567 * 3568 * @param string $content Post content. 3569 * @return string Cleaned content. 3570 */ 3571 private function strip_related_articles_from_content( $content ) { 3572 // Match pattern from build_related_articles_html(): <p>LABEL: <a href="...">...</a></p> 3573 $content = preg_replace( '/<p>[^<]*:\s*<a [^>]*>.*?<\/a><\/p>/s', '', $content ); 3574 // Clean up leftover double blank lines 3575 $content = preg_replace( '/\n{3,}/', "\n\n", $content ); 3576 return trim( $content ); 3577 } 3578 3579 /** 3580 * AJAX handler: save auto schedule settings. 3581 */ 3582 public function ajax_save_auto_schedule_settings() { 3583 check_ajax_referer( 'apicoid_gw_auto_schedule_settings', 'nonce' ); 3584 3585 if ( ! current_user_can( 'manage_options' ) ) { 3586 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3587 } 3588 3589 $per_day = isset( $_POST['per_day'] ) ? absint( $_POST['per_day'] ) : 3; 3590 $start_hour = isset( $_POST['start_hour'] ) ? sanitize_text_field( wp_unslash( $_POST['start_hour'] ) ) : '08:00'; 3591 $end_hour = isset( $_POST['end_hour'] ) ? sanitize_text_field( wp_unslash( $_POST['end_hour'] ) ) : '20:00'; 3592 3593 // Validate 3594 if ( $per_day < 1 ) { 3595 $per_day = 1; 3596 } 3597 3598 if ( ! preg_match( '/^\d{2}:\d{2}$/', $start_hour ) || ! preg_match( '/^\d{2}:\d{2}$/', $end_hour ) ) { 3599 wp_send_json_error( array( 'message' => __( 'Invalid time format. Use HH:MM.', 'apicoid-ghostwriter' ) ) ); 3600 } 3601 3602 if ( $start_hour >= $end_hour ) { 3603 wp_send_json_error( array( 'message' => __( 'Start hour must be before end hour.', 'apicoid-ghostwriter' ) ) ); 3604 } 3605 3606 update_option( 'apicoid_gw_auto_schedule_per_day', $per_day ); 3607 update_option( 'apicoid_gw_auto_schedule_start_hour', $start_hour ); 3608 update_option( 'apicoid_gw_auto_schedule_end_hour', $end_hour ); 3609 3610 wp_send_json_success( array( 'message' => __( 'Auto Schedule settings saved.', 'apicoid-ghostwriter' ) ) ); 3611 } 3612 3613 /** 3614 * AJAX handler: auto schedule a post. 3615 */ 3616 public function ajax_auto_schedule_post() { 3617 check_ajax_referer( 'apicoid_gw_auto_schedule_post', 'nonce' ); 3618 3619 if ( ! current_user_can( 'edit_posts' ) ) { 3620 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3621 } 3622 3623 $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; 3624 $confirmed = isset( $_POST['confirmed'] ) && '1' === $_POST['confirmed']; 3625 3626 if ( ! $post_id || ! get_post( $post_id ) ) { 3627 wp_send_json_error( array( 'message' => __( 'Invalid post ID.', 'apicoid-ghostwriter' ) ) ); 3628 } 3629 3630 $per_day = (int) get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 3631 $start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 3632 $end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 3633 3634 if ( ! $confirmed ) { 3635 // Phase 1: Calculate date and return preview 3636 $date = current_time( 'Y-m-d' ); 3637 $max_days = 365; 3638 for ( $i = 0; $i < $max_days; $i++ ) { 3639 $check_date = gmdate( 'Y-m-d', strtotime( $date . ' +' . $i . ' days' ) ); 3640 if ( $this->count_posts_on_date( $check_date ) < $per_day ) { 3641 $date = $check_date; 3642 break; 3643 } 3644 } 3645 3646 // Generate random time within window 3647 list( $sh, $sm ) = explode( ':', $start_hour ); 3648 list( $eh, $em ) = explode( ':', $end_hour ); 3649 $start_minutes = (int) $sh * 60 + (int) $sm; 3650 $end_minutes = (int) $eh * 60 + (int) $em; 3651 $rand_minutes = wp_rand( $start_minutes, $end_minutes ); 3652 $hour = str_pad( (string) floor( $rand_minutes / 60 ), 2, '0', STR_PAD_LEFT ); 3653 $minute = str_pad( (string) ( $rand_minutes % 60 ), 2, '0', STR_PAD_LEFT ); 3654 $scheduled_date = $date . ' ' . $hour . ':' . $minute . ':00'; 3655 3656 // Get related articles preview 3657 $related_articles = $this->get_related_articles_for_preview( $post_id, $scheduled_date ); 3658 3659 $formatted = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $scheduled_date ) ); 3660 3661 wp_send_json_success( array( 3662 'scheduled_date' => $scheduled_date, 3663 'formatted_date' => $formatted, 3664 'related_articles' => $related_articles, 3665 ) ); 3666 } 3667 3668 // Phase 2: Confirm and schedule 3669 $scheduled_date = isset( $_POST['scheduled_date'] ) ? sanitize_text_field( wp_unslash( $_POST['scheduled_date'] ) ) : ''; 3670 $reinject_related = isset( $_POST['reinject_related'] ) && '1' === $_POST['reinject_related']; 3671 3672 if ( empty( $scheduled_date ) ) { 3673 wp_send_json_error( array( 'message' => __( 'Missing scheduled date.', 'apicoid-ghostwriter' ) ) ); 3674 } 3675 3676 $post = get_post( $post_id ); 3677 3678 // Reinject related articles if requested 3679 if ( $reinject_related ) { 3680 $categories = wp_get_post_categories( $post_id ); 3681 if ( ! empty( $categories ) ) { 3682 $content = $this->strip_related_articles_from_content( $post->post_content ); 3683 $word_count = str_word_count( wp_strip_all_tags( $content ) ); 3684 3685 if ( $word_count > 0 ) { 3686 // Get label from default preset, fallback to standalone option 3687 $presets = get_option( 'apicoid_gw_article_presets', array() ); 3688 $related_article_label = ! empty( $presets['default']['related_article_label'] ) 3689 ? $presets['default']['related_article_label'] 3690 : get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 3691 $content = $this->insert_related_articles( $content, $categories, $word_count, $post_id, 0, $related_article_label, $scheduled_date ); 3692 3693 wp_update_post( array( 3694 'ID' => $post_id, 3695 'post_content' => $content, 3696 ) ); 3697 } 3698 } 3699 } 3700 3701 // Schedule the post 3702 $gmt_date = get_gmt_from_date( $scheduled_date ); 3703 wp_update_post( array( 3704 'ID' => $post_id, 3705 'post_status' => 'future', 3706 'post_date' => $scheduled_date, 3707 'post_date_gmt' => $gmt_date, 3708 'edit_date' => true, 3709 ) ); 3710 3711 $formatted = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $scheduled_date ) ); 3712 3713 wp_send_json_success( array( 3714 'message' => sprintf( 3715 /* translators: %s: formatted scheduled date */ 3716 __( 'Post scheduled for %s.', 'apicoid-ghostwriter' ), 3717 $formatted 3718 ), 3719 ) ); 3720 } 3721 3722 /** 3723 * AJAX handler: rearrange schedule queue. 3724 */ 3725 public function ajax_rearrange_schedule_queue() { 3726 check_ajax_referer( 'apicoid_gw_auto_schedule_settings', 'nonce' ); 3727 3728 if ( ! current_user_can( 'manage_options' ) ) { 3729 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3730 } 3731 3732 $per_day = (int) get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 3733 $start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 3734 $end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 3735 3736 // Get all future posts ordered by date ASC (preserve original order) 3737 $future_posts = get_posts( array( 3738 'post_type' => 'post', 3739 'post_status' => 'future', 3740 'posts_per_page' => -1, 3741 'orderby' => 'date', 3742 'order' => 'ASC', 3743 ) ); 3744 3745 if ( empty( $future_posts ) ) { 3746 wp_send_json_error( array( 'message' => __( 'No scheduled posts to rearrange.', 'apicoid-ghostwriter' ) ) ); 3747 } 3748 3749 list( $sh, $sm ) = explode( ':', $start_hour ); 3750 list( $eh, $em ) = explode( ':', $end_hour ); 3751 $start_minutes = (int) $sh * 60 + (int) $sm; 3752 $end_minutes = (int) $eh * 60 + (int) $em; 3753 3754 $current_date = current_time( 'Y-m-d' ); 3755 $assigned = 0; 3756 3757 foreach ( $future_posts as $fp ) { 3758 // Find next available date from current_date 3759 for ( $i = 0; $i < 365; $i++ ) { 3760 $check_date = gmdate( 'Y-m-d', strtotime( $current_date . ' +' . $i . ' days' ) ); 3761 $existing = $this->count_posts_on_date( $check_date ); 3762 // Subtract future posts we've already assigned to this date in this loop 3763 // We need to count how many we've assigned to this date so far 3764 if ( $existing < $per_day ) { 3765 // Generate random time 3766 $rand_minutes = wp_rand( $start_minutes, $end_minutes ); 3767 $hour = str_pad( (string) floor( $rand_minutes / 60 ), 2, '0', STR_PAD_LEFT ); 3768 $minute = str_pad( (string) ( $rand_minutes % 60 ), 2, '0', STR_PAD_LEFT ); 3769 $new_date = $check_date . ' ' . $hour . ':' . $minute . ':00'; 3770 $gmt_date = get_gmt_from_date( $new_date ); 3771 3772 wp_update_post( array( 3773 'ID' => $fp->ID, 3774 'post_status' => 'future', 3775 'post_date' => $new_date, 3776 'post_date_gmt' => $gmt_date, 3777 'edit_date' => true, 3778 ) ); 3779 3780 $assigned++; 3781 break; 3782 } 3783 } 3784 } 3785 3786 wp_send_json_success( array( 3787 'message' => sprintf( 3788 /* translators: %d: number of posts rearranged */ 3789 __( '%d posts rearranged successfully.', 'apicoid-ghostwriter' ), 3790 $assigned 3791 ), 3792 ) ); 3793 } 3794 3795 /** 3432 3796 * Auto-submit URL to Google when a post/page is published or updated 3433 3797 * -
apicoid-ghostwriter/tags/1.4.0/assets/css/admin.css
r3461407 r3461830 478 478 height: 16px; 479 479 vertical-align: text-bottom; 480 } 481 482 .apicoid-gw-index-log { 480 }.apicoid-gw-index-log { 483 481 margin-top: 20px; 484 482 padding: 15px 20px; … … 671 669 pointer-events: none; 672 670 } 671 672 /* Auto Schedule Button */ 673 .apicoid-gw-auto-schedule-btn { 674 background: #f0b849 !important; 675 border-color: #d4a23a !important; 676 color: #1d2327 !important; 677 font-weight: 600; 678 } 679 680 .apicoid-gw-auto-schedule-btn:hover { 681 background: #e5a930 !important; 682 border-color: #c49530 !important; 683 } 684 685 .apicoid-gw-auto-schedule-btn:disabled { 686 opacity: 0.6; 687 cursor: not-allowed; 688 } 689 690 /* Future status badge */ 691 .apicoid-gw-status.status-future { 692 background: #00a0d2; 693 color: #fff; 694 } -
apicoid-ghostwriter/tags/1.4.0/assets/js/admin.js
r3461338 r3461830 360 360 } 361 361 } // end apicoidGwPresets guard 362 363 // --- Auto Schedule Settings --- 364 if (typeof apicoidGwAutoSchedule !== 'undefined') { 365 var $autoScheduleNotice = $('#apicoid-gw-auto-schedule-notice'); 366 367 function showAutoScheduleNotice(message, type) { 368 var cls = type === 'error' ? 'notice-error' : 'notice-success'; 369 $autoScheduleNotice.html('<div class="notice ' + cls + ' is-dismissible inline"><p>' + message + '</p></div>'); 370 setTimeout(function() { $autoScheduleNotice.html(''); }, 4000); 371 } 372 373 // Save Auto Schedule settings 374 $('#apicoid-gw-auto-schedule-save').on('click', function() { 375 var $btn = $(this); 376 $btn.prop('disabled', true).text(apicoidGwAutoSchedule.strings.saving); 377 378 $.ajax({ 379 url: apicoidGwAutoSchedule.ajax_url, 380 type: 'POST', 381 data: { 382 action: 'apicoid_gw_save_auto_schedule_settings', 383 nonce: apicoidGwAutoSchedule.save_nonce, 384 per_day: $('#apicoid-gw-auto-schedule-per-day').val(), 385 start_hour: $('#apicoid-gw-auto-schedule-start-hour').val(), 386 end_hour: $('#apicoid-gw-auto-schedule-end-hour').val() 387 }, 388 success: function(response) { 389 $btn.prop('disabled', false).text('Save'); 390 if (response.success) { 391 showAutoScheduleNotice(response.data.message, 'success'); 392 } else { 393 showAutoScheduleNotice(response.data.message || 'Save failed.', 'error'); 394 } 395 }, 396 error: function() { 397 $btn.prop('disabled', false).text('Save'); 398 showAutoScheduleNotice('Request failed. Please try again.', 'error'); 399 } 400 }); 401 }); 402 403 // Rearrange Schedule Queue 404 $('#apicoid-gw-auto-schedule-rearrange').on('click', function() { 405 if (!confirm(apicoidGwAutoSchedule.strings.confirm_rearrange)) { 406 return; 407 } 408 409 var $btn = $(this); 410 $btn.prop('disabled', true).text(apicoidGwAutoSchedule.strings.rearranging); 411 412 $.ajax({ 413 url: apicoidGwAutoSchedule.ajax_url, 414 type: 'POST', 415 data: { 416 action: 'apicoid_gw_rearrange_schedule_queue', 417 nonce: apicoidGwAutoSchedule.save_nonce 418 }, 419 success: function(response) { 420 $btn.prop('disabled', false).text('Rearrange Queue'); 421 if (response.success) { 422 showAutoScheduleNotice(response.data.message, 'success'); 423 } else { 424 showAutoScheduleNotice(response.data.message || 'Rearrange failed.', 'error'); 425 } 426 }, 427 error: function() { 428 $btn.prop('disabled', false).text('Rearrange Queue'); 429 showAutoScheduleNotice('Request failed. Please try again.', 'error'); 430 } 431 }); 432 }); 433 } // end apicoidGwAutoSchedule guard 362 434 }); 363 435 364 436 })(jQuery); 365 437 -
apicoid-ghostwriter/tags/1.4.0/assets/js/article-generator.js
r3461407 r3461830 701 701 action: 'apicoid_gw_generate_article_by_category', 702 702 nonce: apicoidGwArticleAjax.nonce, 703 category: [selectedCategory] // Send as array for compatibility with backend 703 category: [selectedCategory], // Send as array for compatibility with backend 704 include_scheduled: $('#apicoid-gw-category-include-scheduled').is(':checked') ? 1 : 0 704 705 }, 705 706 dataType: 'json', -
apicoid-ghostwriter/tags/1.4.0/assets/js/post-edit.js
r3457596 r3461830 684 684 } 685 685 686 // === Auto Schedule Button === 687 window.apicoidGwAutoScheduleBtnAdded = window.apicoidGwAutoScheduleBtnAdded || false; 688 689 function addAutoScheduleModal() { 690 if ($('#apicoid-gw-auto-schedule-confirm-modal').length > 0) { 691 return; 692 } 693 var modalHtml = '<div id="apicoid-gw-auto-schedule-confirm-modal" class="apicoid-gw-modal">' + 694 '<div class="apicoid-gw-modal-content" style="max-width:500px;">' + 695 '<div class="apicoid-gw-modal-header">' + 696 '<h2>Auto Schedule</h2>' + 697 '<span class="apicoid-gw-modal-close">×</span>' + 698 '</div>' + 699 '<div class="apicoid-gw-modal-body">' + 700 '<p>GhostWriter will schedule this post at <strong id="gw-schedule-date"></strong></p>' + 701 '<label><input type="checkbox" id="gw-reinject-related" checked /> Reinject related articles</label>' + 702 '<div id="gw-related-preview" style="margin-top:10px;"></div>' + 703 '</div>' + 704 '<div class="apicoid-gw-modal-footer">' + 705 '<button class="button button-secondary apicoid-gw-modal-close">Cancel</button>' + 706 '<button class="button button-primary" id="gw-confirm-schedule">Confirm</button>' + 707 '</div>' + 708 '</div>' + 709 '</div>'; 710 $('body').append(modalHtml); 711 712 // Close modal handlers 713 $(document).on('click', '#apicoid-gw-auto-schedule-confirm-modal .apicoid-gw-modal-close', function() { 714 $('#apicoid-gw-auto-schedule-confirm-modal').hide(); 715 }); 716 717 // Toggle related preview visibility 718 $(document).on('change', '#gw-reinject-related', function() { 719 $('#gw-related-preview').toggle($(this).is(':checked')); 720 }); 721 } 722 723 function handleAutoScheduleClick(postId) { 724 addAutoScheduleModal(); 725 726 // Phase 1: Get schedule preview 727 $.ajax({ 728 url: apicoidGwPostEdit.ajax_url, 729 type: 'POST', 730 data: { 731 action: 'apicoid_gw_auto_schedule_post', 732 nonce: apicoidGwPostEdit.auto_schedule_nonce, 733 post_id: postId 734 }, 735 beforeSend: function() { 736 $('.apicoid-gw-auto-schedule-btn').prop('disabled', true).text('Calculating...'); 737 }, 738 success: function(response) { 739 $('.apicoid-gw-auto-schedule-btn').prop('disabled', false).text('Auto Schedule'); 740 if (!response.success) { 741 alert(response.data && response.data.message ? response.data.message : 'Failed to calculate schedule.'); 742 return; 743 } 744 745 var data = response.data; 746 $('#gw-schedule-date').text(data.formatted_date); 747 748 // Build related articles preview 749 var previewHtml = ''; 750 if (data.related_articles && data.related_articles.length > 0) { 751 previewHtml = '<p><strong>Related articles will be updated to:</strong></p><ul style="margin-left:20px;">'; 752 $.each(data.related_articles, function(i, article) { 753 previewHtml += '<li><a href="' + article.url + '" target="_blank">' + $('<span>').text(article.title).html() + '</a></li>'; 754 }); 755 previewHtml += '</ul>'; 756 } else { 757 previewHtml = '<p><em>No related articles available for this post.</em></p>'; 758 } 759 $('#gw-related-preview').html(previewHtml).show(); 760 $('#gw-reinject-related').prop('checked', true); 761 762 // Store scheduled_date for Phase 2 763 $('#gw-confirm-schedule').data('scheduled_date', data.scheduled_date); 764 $('#gw-confirm-schedule').data('post_id', postId); 765 766 // Show modal 767 $('#apicoid-gw-auto-schedule-confirm-modal').show(); 768 }, 769 error: function() { 770 $('.apicoid-gw-auto-schedule-btn').prop('disabled', false).text('Auto Schedule'); 771 alert('Request failed. Please try again.'); 772 } 773 }); 774 } 775 776 // Phase 2: Confirm handler 777 $(document).on('click', '#gw-confirm-schedule', function() { 778 var $btn = $(this); 779 var postId = $btn.data('post_id'); 780 var scheduledDate = $btn.data('scheduled_date'); 781 var reinjectRelated = $('#gw-reinject-related').is(':checked') ? 1 : 0; 782 783 $btn.prop('disabled', true).text('Scheduling...'); 784 785 $.ajax({ 786 url: apicoidGwPostEdit.ajax_url, 787 type: 'POST', 788 data: { 789 action: 'apicoid_gw_auto_schedule_post', 790 nonce: apicoidGwPostEdit.auto_schedule_nonce, 791 post_id: postId, 792 confirmed: '1', 793 scheduled_date: scheduledDate, 794 reinject_related: reinjectRelated ? '1' : '0' 795 }, 796 success: function(response) { 797 $btn.prop('disabled', false).text('Confirm'); 798 $('#apicoid-gw-auto-schedule-confirm-modal').hide(); 799 800 if (response.success) { 801 // Show success notice 802 if (apicoidGwPostEdit.is_block_editor && typeof wp !== 'undefined' && wp.data && wp.data.dispatch('core/notices')) { 803 wp.data.dispatch('core/notices').createSuccessNotice( 804 response.data.message || 'Post scheduled successfully!', 805 { type: 'snackbar', isDismissible: true } 806 ); 807 } else { 808 alert(response.data.message || 'Post scheduled successfully!'); 809 } 810 // Reload page to reflect new status 811 setTimeout(function() { location.reload(); }, 1000); 812 } else { 813 alert(response.data && response.data.message ? response.data.message : 'Failed to schedule post.'); 814 } 815 }, 816 error: function() { 817 $btn.prop('disabled', false).text('Confirm'); 818 alert('Request failed. Please try again.'); 819 } 820 }); 821 }); 822 823 // Add Auto Schedule button for Classic Editor 824 function addAutoScheduleButtonClassic() { 825 if (window.apicoidGwAutoScheduleBtnAdded) return; 826 827 var $publishingAction = $('#publishing-action'); 828 if ($publishingAction.length === 0) return; 829 830 var postId = $('#post_ID').val(); 831 if (!postId) return; 832 833 var $container = $('<div>', { style: 'padding-top:15px; margin-top:10px; clear:both;' }); 834 var $button = $('<button>', { 835 type: 'button', 836 class: 'button apicoid-gw-auto-schedule-btn', 837 text: 'Auto Schedule', 838 style: 'width:100%' 839 }); 840 841 $button.on('click', function(e) { 842 e.preventDefault(); 843 handleAutoScheduleClick(postId); 844 }); 845 846 $container.append($button); 847 $publishingAction.after($container); 848 window.apicoidGwAutoScheduleBtnAdded = true; 849 } 850 851 // Add Auto Schedule button for Block Editor 852 function addAutoScheduleButtonBlock() { 853 if (window.apicoidGwAutoScheduleBtnAdded) return; 854 if ($('#apicoid-gw-auto-schedule-btn-container').length > 0) { 855 window.apicoidGwAutoScheduleBtnAdded = true; 856 return; 857 } 858 859 // Find the header bar actions area 860 var $headerBar = $('.edit-post-header__settings, .editor-header__settings').first(); 861 if ($headerBar.length === 0) return; 862 863 var postId = 0; 864 var urlMatch = window.location.search.match(/post=(\d+)/); 865 if (urlMatch) { 866 postId = parseInt(urlMatch[1]); 867 } else if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) { 868 try { postId = wp.data.select('core/editor').getCurrentPostId(); } catch(e) {} 869 } 870 if (!postId) return; 871 872 var $button = $('<button>', { 873 type: 'button', 874 id: 'apicoid-gw-auto-schedule-btn-container', 875 class: 'components-button apicoid-gw-auto-schedule-btn', 876 text: 'Auto Schedule', 877 style: 'height:32px; margin-right:8px;' 878 }); 879 880 $button.on('click', function(e) { 881 e.preventDefault(); 882 handleAutoScheduleClick(postId); 883 }); 884 885 $headerBar.prepend($button); 886 window.apicoidGwAutoScheduleBtnAdded = true; 887 } 888 889 if (apicoidGwPostEdit.is_block_editor) { 890 setTimeout(addAutoScheduleButtonBlock, 1000); 891 setTimeout(addAutoScheduleButtonBlock, 2000); 892 setTimeout(addAutoScheduleButtonBlock, 3000); 893 } else { 894 setTimeout(addAutoScheduleButtonClassic, 500); 895 setTimeout(addAutoScheduleButtonClassic, 1000); 896 setTimeout(addAutoScheduleButtonClassic, 2000); 897 } 898 686 899 // Classic Editor: Use MutationObserver to detect when featured image box is added/updated 687 900 if (!apicoidGwPostEdit.is_block_editor && typeof MutationObserver !== 'undefined') { -
apicoid-ghostwriter/tags/1.4.0/includes/article-optimizer-page.php
r3461338 r3461830 18 18 $args = array( 19 19 'post_type' => 'post', 20 'post_status' => array( 'publish', 'draft' ),20 'post_status' => array( 'publish', 'draft', 'future' ), 21 21 'posts_per_page' => $pagination_size, 22 22 'paged' => $paged, … … 129 129 $metadata_array = ! empty( $metadata_json ) ? json_decode( $metadata_json, true ) : array(); 130 130 $status = get_post_status( $post->ID ); 131 $status_label = 'publish' === $status ? __( 'Published', 'apicoid-ghostwriter' ) : __( 'Draft', 'apicoid-ghostwriter' ); 132 $status_class = 'publish' === $status ? 'status-publish' : 'status-draft'; 131 if ( 'publish' === $status ) { 132 $status_label = __( 'Published', 'apicoid-ghostwriter' ); 133 $status_class = 'status-publish'; 134 } elseif ( 'future' === $status ) { 135 $status_label = __( 'Scheduled', 'apicoid-ghostwriter' ); 136 $status_class = 'status-future'; 137 } else { 138 $status_label = __( 'Draft', 'apicoid-ghostwriter' ); 139 $status_class = 'status-draft'; 140 } 133 141 ?> 134 142 <tr> … … 145 153 <?php echo esc_html( $status_label ); ?> 146 154 </span> 155 <?php if ( 'future' === $status ) : ?> 156 <br><small style="color:#666;"><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $post->post_date ) ) ); ?></small> 157 <?php endif; ?> 147 158 </td> 148 159 <td> … … 185 196 <?php esc_html_e( 'View Article', 'apicoid-ghostwriter' ); ?> 186 197 </a> 187 <?php if ( 'publish' === $status ) :198 <?php if ( 'publish' === $status || 'future' === $status ) : 188 199 $post_permalink = get_permalink( $post->ID ); 189 200 ?> … … 811 822 <?php endforeach; ?> 812 823 </div> 824 <label style="display:block; margin-top:10px;"> 825 <input type="checkbox" id="apicoid-gw-category-include-scheduled" value="1" /> 826 <?php esc_html_e( 'Include scheduled content', 'apicoid-ghostwriter' ); ?> 827 </label> 813 828 <p class="description"><?php esc_html_e( 'Select one category to get article suggestions', 'apicoid-ghostwriter' ); ?></p> 814 829 </td> -
apicoid-ghostwriter/tags/1.4.0/includes/settings-page.php
r3461338 r3461830 253 253 </div> 254 254 </div> 255 256 <!-- Auto Schedule Settings --> 257 <?php 258 $auto_schedule_per_day = get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 259 $auto_schedule_start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 260 $auto_schedule_end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 261 ?> 262 <div class="apicoid-gw-admin"> 263 <div class="apicoid-gw-header"> 264 <h2><?php esc_html_e( 'Auto Schedule', 'apicoid-ghostwriter' ); ?></h2> 265 <p><?php esc_html_e( 'Configure automatic post scheduling. Set daily article limits and publishing time windows, then auto-schedule posts from the post editor with one click.', 'apicoid-ghostwriter' ); ?></p> 266 </div> 267 <div class="apicoid-gw-content"> 268 <div id="apicoid-gw-auto-schedule-notice"></div> 269 <table class="form-table"> 270 <tr> 271 <th scope="row"> 272 <label for="apicoid-gw-auto-schedule-per-day"><?php esc_html_e( 'Articles Per Day', 'apicoid-ghostwriter' ); ?></label> 273 </th> 274 <td> 275 <input type="number" id="apicoid-gw-auto-schedule-per-day" value="<?php echo esc_attr( $auto_schedule_per_day ); ?>" min="1" max="50" class="small-text" /> 276 <p class="description"><?php esc_html_e( 'Maximum number of articles to schedule per day (default: 3)', 'apicoid-ghostwriter' ); ?></p> 277 </td> 278 </tr> 279 <tr> 280 <th scope="row"> 281 <label for="apicoid-gw-auto-schedule-start-hour"><?php esc_html_e( 'Start Hour', 'apicoid-ghostwriter' ); ?></label> 282 </th> 283 <td> 284 <input type="time" id="apicoid-gw-auto-schedule-start-hour" value="<?php echo esc_attr( $auto_schedule_start_hour ); ?>" /> 285 <p class="description"><?php esc_html_e( 'Earliest time to schedule posts (default: 08:00)', 'apicoid-ghostwriter' ); ?></p> 286 </td> 287 </tr> 288 <tr> 289 <th scope="row"> 290 <label for="apicoid-gw-auto-schedule-end-hour"><?php esc_html_e( 'End Hour', 'apicoid-ghostwriter' ); ?></label> 291 </th> 292 <td> 293 <input type="time" id="apicoid-gw-auto-schedule-end-hour" value="<?php echo esc_attr( $auto_schedule_end_hour ); ?>" /> 294 <p class="description"><?php esc_html_e( 'Latest time to schedule posts (default: 20:00)', 'apicoid-ghostwriter' ); ?></p> 295 </td> 296 </tr> 297 </table> 298 <div style="display:flex; gap:10px; margin-top:10px;"> 299 <button type="button" id="apicoid-gw-auto-schedule-save" class="button button-primary"><?php esc_html_e( 'Save', 'apicoid-ghostwriter' ); ?></button> 300 <button type="button" id="apicoid-gw-auto-schedule-rearrange" class="button button-secondary"><?php esc_html_e( 'Rearrange Queue', 'apicoid-ghostwriter' ); ?></button> 301 </div> 302 </div> 303 </div> 255 304 </div> -
apicoid-ghostwriter/tags/1.4.0/readme.txt
r3461407 r3461830 5 5 Requires at least: 6.2 6 6 Tested up to: 6.9 7 Stable tag: 1. 3.37 Stable tag: 1.4.0 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 35 35 36 36 * **Secure API Key Validation**: Secure API key validation system ensures only valid keys can be used. 37 38 * **Auto Schedule**: Schedule draft posts with one click. Configure how many articles to publish per day and a time window (e.g. 08:00–20:00), then hit the "Auto Schedule" button on any post editor. GhostWriter finds the next available slot, picks a random time within your window, and shows you a preview before confirming. It can also re-generate related article links so they only point to posts that will already be live when the scheduled post publishes — no broken links. Use "Rearrange Queue" on the Settings page to redistribute all scheduled posts whenever you change your limits. 37 39 38 40 = How It Works = … … 129 131 130 132 == Changelog == 133 134 = 1.4.0 = 135 * Auto Schedule: New feature to automatically schedule posts with one click from the post editor (Classic & Block Editor) 136 * Configurable daily article limits and publishing time windows (Settings > Auto Schedule) 137 * Rearrange Queue: Redistribute all scheduled posts according to current settings while preserving order 138 * Smart related article reinjection: When auto-scheduling, optionally re-generate related article links to include scheduled posts (prevents 404s by only linking to posts that will be live before the current post) 139 * Related article label now reads from Default preset instead of legacy standalone option 140 * Include Scheduled Content: New checkbox in Generate by Category to include scheduled posts in category analysis 141 * Scheduled posts now visible in Article Generator list with blue "Scheduled" badge and publish date 142 * Create Support Article now available for scheduled posts (not just published) 143 * Future post status added to Article Generator list query 131 144 132 145 = 1.3.3 = … … 208 221 == Upgrade Notice == 209 222 223 = 1.4.0 = 224 This update introduces Auto Schedule, allowing you to schedule posts with one click from the post editor. Configure daily article limits and time windows in Settings, then auto-schedule drafts with smart related article reinjection that prevents 404 links. Scheduled posts are now visible in the Article Generator list and can be used to generate support articles. 225 210 226 = 1.3.1 = 211 227 This update introduces Auto Google Index feature that automatically submits your post and page URLs to Google Indexing API when published or updated. Includes comprehensive submission logging, encrypted service account storage, and test connection functionality. See the new "Auto Google Index" menu for setup instructions. -
apicoid-ghostwriter/trunk/CHANGELOG.md
r3461338 r3461830 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 8 ## [1.4.0] - 2026-02-15 9 10 ### Added 11 - **Auto Schedule**: One-click post scheduling from the post editor (Classic & Block Editor) with a yellow "Auto Schedule" button 12 - **Auto Schedule Settings**: Configurable daily article limits (`Articles Per Day`), publishing time window (`Start Hour` / `End Hour`) on the Settings page 13 - **Rearrange Queue**: Redistribute all scheduled posts according to current settings while preserving their original order 14 - **Smart Related Article Reinjection**: When auto-scheduling, optionally strip and re-generate related article links to include scheduled posts — only links to posts that will be live before the current post (prevents 404s) 15 - **Confirm Dialog with Preview**: Auto Schedule shows a modal with the proposed date and updated related article links before confirming 16 - **Include Scheduled Content Checkbox**: New option in Generate by Category to include `future` posts in category analysis 17 - **Scheduled Posts in Article Generator**: Future posts now appear in the article list with a blue "Scheduled" badge and their scheduled publish date/time 18 - **Create Support Article for Scheduled Posts**: The "Create Support Article" button now appears for both published and scheduled posts 19 20 ### Changed 21 - **Related Article Label from Preset**: `build_related_articles_html()` and auto-schedule reinjection now read the label from the Default preset instead of the legacy standalone `apicoid_gw_article_related_article_label` option 22 - **Article Generator Query**: Post status query updated to include `future` alongside `publish` and `draft` 23 24 ## [1.3.3] - 2026-02-15 25 26 ### Fixed 27 - **Increased API Timeouts to 10 Minutes**: All API calls to api.co.id (article generation, rewriting, suggestions, image generation) now use 600-second timeout to prevent premature request failures 28 - **Improved AJAX Timeout Handling**: Added 10-minute client-side timeout to Generate Article and Rewrite Article forms that were previously missing explicit timeouts 29 - **Better Error Messages for Timeouts**: Generate and Rewrite forms now show informative messages when server timeout (502/504) occurs, letting users know the article may still be generating in the background 30 - **Fixed False "Request Failed" Errors**: Resolved issue where articles were successfully generated but users saw error alerts due to web server gateway timeouts 7 31 8 32 ## [1.3.2] - 2026-02-14 … … 119 143 - Secure API key validation 120 144 145 [1.4.0]: https://github.com/apicoid/ghostwriter/compare/v1.3.3...v1.4.0 146 [1.3.3]: https://github.com/apicoid/ghostwriter/compare/v1.3.2...v1.3.3 121 147 [1.3.2]: https://github.com/apicoid/ghostwriter/compare/v1.3.1...v1.3.2 122 148 [1.3.1]: https://github.com/apicoid/ghostwriter/compare/v1.3.0...v1.3.1 -
apicoid-ghostwriter/trunk/apicoid-ghostwriter.php
r3461407 r3461830 4 4 * Plugin URI: https://wordpress.org/plugins/apicoid-ghostwriter/ 5 5 * Description: Connects your WordPress site to Api.co.id to generate content and rewrite content automatically using AI. Features include article generation, content rewriting, automatic related article linking, SEO integration, and image generation. 6 * Version: 1. 3.36 * Version: 1.4.0 7 7 * Author: Api.co.id 8 8 * Author URI: https://api.co.id … … 21 21 22 22 // Define plugin constants 23 define( 'APICOID_GW_VERSION', '1. 3.3' );23 define( 'APICOID_GW_VERSION', '1.4.0' ); 24 24 define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) ); … … 129 129 // Handle Google Index log clear 130 130 add_action( 'wp_ajax_apicoid_gw_clear_google_index_logs', array( $this, 'ajax_clear_google_index_logs' ) ); 131 132 // Handle Auto Schedule 133 add_action( 'wp_ajax_apicoid_gw_save_auto_schedule_settings', array( $this, 'ajax_save_auto_schedule_settings' ) ); 134 add_action( 'wp_ajax_apicoid_gw_auto_schedule_post', array( $this, 'ajax_auto_schedule_post' ) ); 135 add_action( 'wp_ajax_apicoid_gw_rearrange_schedule_queue', array( $this, 'ajax_rearrange_schedule_queue' ) ); 131 136 132 137 // Auto-submit URL to Google on post publish/update … … 564 569 ); 565 570 571 // Register Auto Schedule settings 572 register_setting( 573 'apicoid_gw_article_settings', 574 'apicoid_gw_auto_schedule_per_day', 575 array( 576 'sanitize_callback' => 'absint', 577 'default' => 3, 578 ) 579 ); 580 581 register_setting( 582 'apicoid_gw_article_settings', 583 'apicoid_gw_auto_schedule_start_hour', 584 array( 585 'sanitize_callback' => 'sanitize_text_field', 586 'default' => '08:00', 587 ) 588 ); 589 590 register_setting( 591 'apicoid_gw_article_settings', 592 'apicoid_gw_auto_schedule_end_hour', 593 array( 594 'sanitize_callback' => 'sanitize_text_field', 595 'default' => '20:00', 596 ) 597 ); 598 566 599 // Register Google Index settings 567 600 register_setting( … … 1183 1216 * @return string Content with related articles inserted. 1184 1217 */ 1185 private function insert_related_articles( $content, $category_ids, $word_count, $current_post_id = 0, $pillar_article_id = 0, $related_article_label = '' ) {1218 private function insert_related_articles( $content, $category_ids, $word_count, $current_post_id = 0, $pillar_article_id = 0, $related_article_label = '', $scheduled_date = '' ) { 1186 1219 if ( empty( $category_ids ) || ! is_array( $category_ids ) || $word_count <= 0 ) { 1187 1220 return $content; … … 1197 1230 1198 1231 if ( $pillar_article_id > 0 ) { 1199 // Verify pillar article exists and is published 1232 // Verify pillar article exists and is published (or scheduled before scheduled_date) 1200 1233 $pillar_post = get_post( $pillar_article_id ); 1201 if ( $pillar_post && 'publish' === $pillar_post->post_status && $pillar_article_id !== $current_post_id ) { 1234 $pillar_valid = false; 1235 if ( $pillar_post && $pillar_article_id !== $current_post_id ) { 1236 if ( 'publish' === $pillar_post->post_status ) { 1237 $pillar_valid = true; 1238 } elseif ( ! empty( $scheduled_date ) && 'future' === $pillar_post->post_status && $pillar_post->post_date < $scheduled_date ) { 1239 $pillar_valid = true; 1240 } 1241 } 1242 if ( $pillar_valid ) { 1202 1243 $related_posts[] = $pillar_article_id; 1203 1244 $pillar_included = true; … … 1213 1254 } 1214 1255 1215 $additional_posts = get_posts( 1216 array( 1217 'post_type' => 'post', 1218 'post_status' => 'publish', 1219 'posts_per_page' => $num_related * 3, // Get more to ensure we have enough 1220 'category__in' => $category_ids, 1221 'post__not_in' => $exclude_ids, 1222 'orderby' => 'rand', 1223 'fields' => 'ids', 1224 ) 1225 ); 1256 $related_query_args = array( 1257 'post_type' => 'post', 1258 'posts_per_page' => $num_related * 3, // Get more to ensure we have enough 1259 'category__in' => $category_ids, 1260 'post__not_in' => $exclude_ids, 1261 'orderby' => 'rand', 1262 'fields' => 'ids', 1263 ); 1264 1265 if ( ! empty( $scheduled_date ) ) { 1266 $related_query_args['post_status'] = array( 'publish', 'future' ); 1267 $related_query_args['date_query'] = array( array( 'before' => $scheduled_date ) ); 1268 } else { 1269 $related_query_args['post_status'] = 'publish'; 1270 } 1271 1272 $additional_posts = get_posts( $related_query_args ); 1226 1273 1227 1274 // Merge pillar article with additional posts … … 1343 1390 1344 1391 if ( empty( $label ) ) { 1345 $label = get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 1392 // Try to get label from the default preset first 1393 $presets = get_option( 'apicoid_gw_article_presets', array() ); 1394 if ( ! empty( $presets['default']['related_article_label'] ) ) { 1395 $label = $presets['default']['related_article_label']; 1396 } else { 1397 $label = get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 1398 } 1346 1399 } 1347 1400 if ( '' === trim( $label ) ) { … … 1747 1800 } 1748 1801 1749 // Get published posts from this category 1802 // Get posts from this category (optionally include scheduled) 1803 $include_scheduled = isset( $_POST['include_scheduled'] ) && '1' === $_POST['include_scheduled']; 1804 $post_status = $include_scheduled ? array( 'publish', 'future' ) : 'publish'; 1805 1750 1806 $args = array( 1751 1807 'post_type' => 'post', 1752 'post_status' => 'publish',1808 'post_status' => $post_status, 1753 1809 'posts_per_page' => 50, // Limit to 50 articles 1754 1810 'cat' => $category_id, … … 2736 2792 'ajax_url' => admin_url( 'admin-ajax.php' ), 2737 2793 'nonce' => wp_create_nonce( 'apicoid_gw_generate_featured_image' ), 2794 'auto_schedule_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_post' ), 2738 2795 'is_block_editor' => $is_block_editor, 2739 2796 'strings' => array( … … 2878 2935 ) 2879 2936 ); 2937 2938 wp_localize_script( 2939 'apicoid-gw-admin-script', 2940 'apicoidGwAutoSchedule', 2941 array( 2942 'ajax_url' => admin_url( 'admin-ajax.php' ), 2943 'save_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_settings' ), 2944 'strings' => array( 2945 'saving' => __( 'Saving...', 'apicoid-ghostwriter' ), 2946 'rearranging' => __( 'Rearranging...', 'apicoid-ghostwriter' ), 2947 'confirm_rearrange' => __( 'This will rearrange all scheduled posts according to current settings. Continue?', 'apicoid-ghostwriter' ), 2948 ), 2949 ) 2950 ); 2880 2951 } 2881 2952 … … 3430 3501 3431 3502 /** 3503 * Count posts on a given date (published + scheduled). 3504 * 3505 * @param string $date_str Date in Y-m-d format. 3506 * @return int 3507 */ 3508 private function count_posts_on_date( $date_str ) { 3509 global $wpdb; 3510 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Lightweight count query for scheduling 3511 return (int) $wpdb->get_var( 3512 $wpdb->prepare( 3513 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status IN ('publish','future') AND DATE(post_date) = %s", 3514 $date_str 3515 ) 3516 ); 3517 } 3518 3519 /** 3520 * Get related articles preview data for the auto-schedule confirm dialog. 3521 * 3522 * @param int $post_id Post ID. 3523 * @param string $scheduled_date Scheduled datetime string. 3524 * @return array Array of [ 'title' => ..., 'url' => ... ]. 3525 */ 3526 private function get_related_articles_for_preview( $post_id, $scheduled_date ) { 3527 $post = get_post( $post_id ); 3528 if ( ! $post ) { 3529 return array(); 3530 } 3531 3532 $categories = wp_get_post_categories( $post_id ); 3533 if ( empty( $categories ) ) { 3534 return array(); 3535 } 3536 3537 $word_count = str_word_count( wp_strip_all_tags( $post->post_content ) ); 3538 if ( $word_count <= 0 ) { 3539 return array(); 3540 } 3541 3542 $num_related = ceil( $word_count / 500 ); 3543 3544 $related = get_posts( array( 3545 'post_type' => 'post', 3546 'post_status' => array( 'publish', 'future' ), 3547 'posts_per_page' => $num_related * 3, 3548 'category__in' => $categories, 3549 'post__not_in' => array( $post_id ), 3550 'date_query' => array( array( 'before' => $scheduled_date ) ), 3551 'orderby' => 'rand', 3552 'fields' => 'ids', 3553 ) ); 3554 3555 $result = array(); 3556 foreach ( array_slice( $related, 0, $num_related ) as $rid ) { 3557 $result[] = array( 3558 'title' => get_the_title( $rid ), 3559 'url' => get_permalink( $rid ), 3560 ); 3561 } 3562 return $result; 3563 } 3564 3565 /** 3566 * Strip existing related article blocks from content. 3567 * 3568 * @param string $content Post content. 3569 * @return string Cleaned content. 3570 */ 3571 private function strip_related_articles_from_content( $content ) { 3572 // Match pattern from build_related_articles_html(): <p>LABEL: <a href="...">...</a></p> 3573 $content = preg_replace( '/<p>[^<]*:\s*<a [^>]*>.*?<\/a><\/p>/s', '', $content ); 3574 // Clean up leftover double blank lines 3575 $content = preg_replace( '/\n{3,}/', "\n\n", $content ); 3576 return trim( $content ); 3577 } 3578 3579 /** 3580 * AJAX handler: save auto schedule settings. 3581 */ 3582 public function ajax_save_auto_schedule_settings() { 3583 check_ajax_referer( 'apicoid_gw_auto_schedule_settings', 'nonce' ); 3584 3585 if ( ! current_user_can( 'manage_options' ) ) { 3586 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3587 } 3588 3589 $per_day = isset( $_POST['per_day'] ) ? absint( $_POST['per_day'] ) : 3; 3590 $start_hour = isset( $_POST['start_hour'] ) ? sanitize_text_field( wp_unslash( $_POST['start_hour'] ) ) : '08:00'; 3591 $end_hour = isset( $_POST['end_hour'] ) ? sanitize_text_field( wp_unslash( $_POST['end_hour'] ) ) : '20:00'; 3592 3593 // Validate 3594 if ( $per_day < 1 ) { 3595 $per_day = 1; 3596 } 3597 3598 if ( ! preg_match( '/^\d{2}:\d{2}$/', $start_hour ) || ! preg_match( '/^\d{2}:\d{2}$/', $end_hour ) ) { 3599 wp_send_json_error( array( 'message' => __( 'Invalid time format. Use HH:MM.', 'apicoid-ghostwriter' ) ) ); 3600 } 3601 3602 if ( $start_hour >= $end_hour ) { 3603 wp_send_json_error( array( 'message' => __( 'Start hour must be before end hour.', 'apicoid-ghostwriter' ) ) ); 3604 } 3605 3606 update_option( 'apicoid_gw_auto_schedule_per_day', $per_day ); 3607 update_option( 'apicoid_gw_auto_schedule_start_hour', $start_hour ); 3608 update_option( 'apicoid_gw_auto_schedule_end_hour', $end_hour ); 3609 3610 wp_send_json_success( array( 'message' => __( 'Auto Schedule settings saved.', 'apicoid-ghostwriter' ) ) ); 3611 } 3612 3613 /** 3614 * AJAX handler: auto schedule a post. 3615 */ 3616 public function ajax_auto_schedule_post() { 3617 check_ajax_referer( 'apicoid_gw_auto_schedule_post', 'nonce' ); 3618 3619 if ( ! current_user_can( 'edit_posts' ) ) { 3620 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3621 } 3622 3623 $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; 3624 $confirmed = isset( $_POST['confirmed'] ) && '1' === $_POST['confirmed']; 3625 3626 if ( ! $post_id || ! get_post( $post_id ) ) { 3627 wp_send_json_error( array( 'message' => __( 'Invalid post ID.', 'apicoid-ghostwriter' ) ) ); 3628 } 3629 3630 $per_day = (int) get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 3631 $start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 3632 $end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 3633 3634 if ( ! $confirmed ) { 3635 // Phase 1: Calculate date and return preview 3636 $date = current_time( 'Y-m-d' ); 3637 $max_days = 365; 3638 for ( $i = 0; $i < $max_days; $i++ ) { 3639 $check_date = gmdate( 'Y-m-d', strtotime( $date . ' +' . $i . ' days' ) ); 3640 if ( $this->count_posts_on_date( $check_date ) < $per_day ) { 3641 $date = $check_date; 3642 break; 3643 } 3644 } 3645 3646 // Generate random time within window 3647 list( $sh, $sm ) = explode( ':', $start_hour ); 3648 list( $eh, $em ) = explode( ':', $end_hour ); 3649 $start_minutes = (int) $sh * 60 + (int) $sm; 3650 $end_minutes = (int) $eh * 60 + (int) $em; 3651 $rand_minutes = wp_rand( $start_minutes, $end_minutes ); 3652 $hour = str_pad( (string) floor( $rand_minutes / 60 ), 2, '0', STR_PAD_LEFT ); 3653 $minute = str_pad( (string) ( $rand_minutes % 60 ), 2, '0', STR_PAD_LEFT ); 3654 $scheduled_date = $date . ' ' . $hour . ':' . $minute . ':00'; 3655 3656 // Get related articles preview 3657 $related_articles = $this->get_related_articles_for_preview( $post_id, $scheduled_date ); 3658 3659 $formatted = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $scheduled_date ) ); 3660 3661 wp_send_json_success( array( 3662 'scheduled_date' => $scheduled_date, 3663 'formatted_date' => $formatted, 3664 'related_articles' => $related_articles, 3665 ) ); 3666 } 3667 3668 // Phase 2: Confirm and schedule 3669 $scheduled_date = isset( $_POST['scheduled_date'] ) ? sanitize_text_field( wp_unslash( $_POST['scheduled_date'] ) ) : ''; 3670 $reinject_related = isset( $_POST['reinject_related'] ) && '1' === $_POST['reinject_related']; 3671 3672 if ( empty( $scheduled_date ) ) { 3673 wp_send_json_error( array( 'message' => __( 'Missing scheduled date.', 'apicoid-ghostwriter' ) ) ); 3674 } 3675 3676 $post = get_post( $post_id ); 3677 3678 // Reinject related articles if requested 3679 if ( $reinject_related ) { 3680 $categories = wp_get_post_categories( $post_id ); 3681 if ( ! empty( $categories ) ) { 3682 $content = $this->strip_related_articles_from_content( $post->post_content ); 3683 $word_count = str_word_count( wp_strip_all_tags( $content ) ); 3684 3685 if ( $word_count > 0 ) { 3686 // Get label from default preset, fallback to standalone option 3687 $presets = get_option( 'apicoid_gw_article_presets', array() ); 3688 $related_article_label = ! empty( $presets['default']['related_article_label'] ) 3689 ? $presets['default']['related_article_label'] 3690 : get_option( 'apicoid_gw_article_related_article_label', 'Related Article' ); 3691 $content = $this->insert_related_articles( $content, $categories, $word_count, $post_id, 0, $related_article_label, $scheduled_date ); 3692 3693 wp_update_post( array( 3694 'ID' => $post_id, 3695 'post_content' => $content, 3696 ) ); 3697 } 3698 } 3699 } 3700 3701 // Schedule the post 3702 $gmt_date = get_gmt_from_date( $scheduled_date ); 3703 wp_update_post( array( 3704 'ID' => $post_id, 3705 'post_status' => 'future', 3706 'post_date' => $scheduled_date, 3707 'post_date_gmt' => $gmt_date, 3708 'edit_date' => true, 3709 ) ); 3710 3711 $formatted = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $scheduled_date ) ); 3712 3713 wp_send_json_success( array( 3714 'message' => sprintf( 3715 /* translators: %s: formatted scheduled date */ 3716 __( 'Post scheduled for %s.', 'apicoid-ghostwriter' ), 3717 $formatted 3718 ), 3719 ) ); 3720 } 3721 3722 /** 3723 * AJAX handler: rearrange schedule queue. 3724 */ 3725 public function ajax_rearrange_schedule_queue() { 3726 check_ajax_referer( 'apicoid_gw_auto_schedule_settings', 'nonce' ); 3727 3728 if ( ! current_user_can( 'manage_options' ) ) { 3729 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'apicoid-ghostwriter' ) ) ); 3730 } 3731 3732 $per_day = (int) get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 3733 $start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 3734 $end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 3735 3736 // Get all future posts ordered by date ASC (preserve original order) 3737 $future_posts = get_posts( array( 3738 'post_type' => 'post', 3739 'post_status' => 'future', 3740 'posts_per_page' => -1, 3741 'orderby' => 'date', 3742 'order' => 'ASC', 3743 ) ); 3744 3745 if ( empty( $future_posts ) ) { 3746 wp_send_json_error( array( 'message' => __( 'No scheduled posts to rearrange.', 'apicoid-ghostwriter' ) ) ); 3747 } 3748 3749 list( $sh, $sm ) = explode( ':', $start_hour ); 3750 list( $eh, $em ) = explode( ':', $end_hour ); 3751 $start_minutes = (int) $sh * 60 + (int) $sm; 3752 $end_minutes = (int) $eh * 60 + (int) $em; 3753 3754 $current_date = current_time( 'Y-m-d' ); 3755 $assigned = 0; 3756 3757 foreach ( $future_posts as $fp ) { 3758 // Find next available date from current_date 3759 for ( $i = 0; $i < 365; $i++ ) { 3760 $check_date = gmdate( 'Y-m-d', strtotime( $current_date . ' +' . $i . ' days' ) ); 3761 $existing = $this->count_posts_on_date( $check_date ); 3762 // Subtract future posts we've already assigned to this date in this loop 3763 // We need to count how many we've assigned to this date so far 3764 if ( $existing < $per_day ) { 3765 // Generate random time 3766 $rand_minutes = wp_rand( $start_minutes, $end_minutes ); 3767 $hour = str_pad( (string) floor( $rand_minutes / 60 ), 2, '0', STR_PAD_LEFT ); 3768 $minute = str_pad( (string) ( $rand_minutes % 60 ), 2, '0', STR_PAD_LEFT ); 3769 $new_date = $check_date . ' ' . $hour . ':' . $minute . ':00'; 3770 $gmt_date = get_gmt_from_date( $new_date ); 3771 3772 wp_update_post( array( 3773 'ID' => $fp->ID, 3774 'post_status' => 'future', 3775 'post_date' => $new_date, 3776 'post_date_gmt' => $gmt_date, 3777 'edit_date' => true, 3778 ) ); 3779 3780 $assigned++; 3781 break; 3782 } 3783 } 3784 } 3785 3786 wp_send_json_success( array( 3787 'message' => sprintf( 3788 /* translators: %d: number of posts rearranged */ 3789 __( '%d posts rearranged successfully.', 'apicoid-ghostwriter' ), 3790 $assigned 3791 ), 3792 ) ); 3793 } 3794 3795 /** 3432 3796 * Auto-submit URL to Google when a post/page is published or updated 3433 3797 * -
apicoid-ghostwriter/trunk/assets/css/admin.css
r3461407 r3461830 478 478 height: 16px; 479 479 vertical-align: text-bottom; 480 } 481 482 .apicoid-gw-index-log { 480 }.apicoid-gw-index-log { 483 481 margin-top: 20px; 484 482 padding: 15px 20px; … … 671 669 pointer-events: none; 672 670 } 671 672 /* Auto Schedule Button */ 673 .apicoid-gw-auto-schedule-btn { 674 background: #f0b849 !important; 675 border-color: #d4a23a !important; 676 color: #1d2327 !important; 677 font-weight: 600; 678 } 679 680 .apicoid-gw-auto-schedule-btn:hover { 681 background: #e5a930 !important; 682 border-color: #c49530 !important; 683 } 684 685 .apicoid-gw-auto-schedule-btn:disabled { 686 opacity: 0.6; 687 cursor: not-allowed; 688 } 689 690 /* Future status badge */ 691 .apicoid-gw-status.status-future { 692 background: #00a0d2; 693 color: #fff; 694 } -
apicoid-ghostwriter/trunk/assets/js/admin.js
r3461338 r3461830 360 360 } 361 361 } // end apicoidGwPresets guard 362 363 // --- Auto Schedule Settings --- 364 if (typeof apicoidGwAutoSchedule !== 'undefined') { 365 var $autoScheduleNotice = $('#apicoid-gw-auto-schedule-notice'); 366 367 function showAutoScheduleNotice(message, type) { 368 var cls = type === 'error' ? 'notice-error' : 'notice-success'; 369 $autoScheduleNotice.html('<div class="notice ' + cls + ' is-dismissible inline"><p>' + message + '</p></div>'); 370 setTimeout(function() { $autoScheduleNotice.html(''); }, 4000); 371 } 372 373 // Save Auto Schedule settings 374 $('#apicoid-gw-auto-schedule-save').on('click', function() { 375 var $btn = $(this); 376 $btn.prop('disabled', true).text(apicoidGwAutoSchedule.strings.saving); 377 378 $.ajax({ 379 url: apicoidGwAutoSchedule.ajax_url, 380 type: 'POST', 381 data: { 382 action: 'apicoid_gw_save_auto_schedule_settings', 383 nonce: apicoidGwAutoSchedule.save_nonce, 384 per_day: $('#apicoid-gw-auto-schedule-per-day').val(), 385 start_hour: $('#apicoid-gw-auto-schedule-start-hour').val(), 386 end_hour: $('#apicoid-gw-auto-schedule-end-hour').val() 387 }, 388 success: function(response) { 389 $btn.prop('disabled', false).text('Save'); 390 if (response.success) { 391 showAutoScheduleNotice(response.data.message, 'success'); 392 } else { 393 showAutoScheduleNotice(response.data.message || 'Save failed.', 'error'); 394 } 395 }, 396 error: function() { 397 $btn.prop('disabled', false).text('Save'); 398 showAutoScheduleNotice('Request failed. Please try again.', 'error'); 399 } 400 }); 401 }); 402 403 // Rearrange Schedule Queue 404 $('#apicoid-gw-auto-schedule-rearrange').on('click', function() { 405 if (!confirm(apicoidGwAutoSchedule.strings.confirm_rearrange)) { 406 return; 407 } 408 409 var $btn = $(this); 410 $btn.prop('disabled', true).text(apicoidGwAutoSchedule.strings.rearranging); 411 412 $.ajax({ 413 url: apicoidGwAutoSchedule.ajax_url, 414 type: 'POST', 415 data: { 416 action: 'apicoid_gw_rearrange_schedule_queue', 417 nonce: apicoidGwAutoSchedule.save_nonce 418 }, 419 success: function(response) { 420 $btn.prop('disabled', false).text('Rearrange Queue'); 421 if (response.success) { 422 showAutoScheduleNotice(response.data.message, 'success'); 423 } else { 424 showAutoScheduleNotice(response.data.message || 'Rearrange failed.', 'error'); 425 } 426 }, 427 error: function() { 428 $btn.prop('disabled', false).text('Rearrange Queue'); 429 showAutoScheduleNotice('Request failed. Please try again.', 'error'); 430 } 431 }); 432 }); 433 } // end apicoidGwAutoSchedule guard 362 434 }); 363 435 364 436 })(jQuery); 365 437 -
apicoid-ghostwriter/trunk/assets/js/article-generator.js
r3461407 r3461830 701 701 action: 'apicoid_gw_generate_article_by_category', 702 702 nonce: apicoidGwArticleAjax.nonce, 703 category: [selectedCategory] // Send as array for compatibility with backend 703 category: [selectedCategory], // Send as array for compatibility with backend 704 include_scheduled: $('#apicoid-gw-category-include-scheduled').is(':checked') ? 1 : 0 704 705 }, 705 706 dataType: 'json', -
apicoid-ghostwriter/trunk/assets/js/post-edit.js
r3457596 r3461830 684 684 } 685 685 686 // === Auto Schedule Button === 687 window.apicoidGwAutoScheduleBtnAdded = window.apicoidGwAutoScheduleBtnAdded || false; 688 689 function addAutoScheduleModal() { 690 if ($('#apicoid-gw-auto-schedule-confirm-modal').length > 0) { 691 return; 692 } 693 var modalHtml = '<div id="apicoid-gw-auto-schedule-confirm-modal" class="apicoid-gw-modal">' + 694 '<div class="apicoid-gw-modal-content" style="max-width:500px;">' + 695 '<div class="apicoid-gw-modal-header">' + 696 '<h2>Auto Schedule</h2>' + 697 '<span class="apicoid-gw-modal-close">×</span>' + 698 '</div>' + 699 '<div class="apicoid-gw-modal-body">' + 700 '<p>GhostWriter will schedule this post at <strong id="gw-schedule-date"></strong></p>' + 701 '<label><input type="checkbox" id="gw-reinject-related" checked /> Reinject related articles</label>' + 702 '<div id="gw-related-preview" style="margin-top:10px;"></div>' + 703 '</div>' + 704 '<div class="apicoid-gw-modal-footer">' + 705 '<button class="button button-secondary apicoid-gw-modal-close">Cancel</button>' + 706 '<button class="button button-primary" id="gw-confirm-schedule">Confirm</button>' + 707 '</div>' + 708 '</div>' + 709 '</div>'; 710 $('body').append(modalHtml); 711 712 // Close modal handlers 713 $(document).on('click', '#apicoid-gw-auto-schedule-confirm-modal .apicoid-gw-modal-close', function() { 714 $('#apicoid-gw-auto-schedule-confirm-modal').hide(); 715 }); 716 717 // Toggle related preview visibility 718 $(document).on('change', '#gw-reinject-related', function() { 719 $('#gw-related-preview').toggle($(this).is(':checked')); 720 }); 721 } 722 723 function handleAutoScheduleClick(postId) { 724 addAutoScheduleModal(); 725 726 // Phase 1: Get schedule preview 727 $.ajax({ 728 url: apicoidGwPostEdit.ajax_url, 729 type: 'POST', 730 data: { 731 action: 'apicoid_gw_auto_schedule_post', 732 nonce: apicoidGwPostEdit.auto_schedule_nonce, 733 post_id: postId 734 }, 735 beforeSend: function() { 736 $('.apicoid-gw-auto-schedule-btn').prop('disabled', true).text('Calculating...'); 737 }, 738 success: function(response) { 739 $('.apicoid-gw-auto-schedule-btn').prop('disabled', false).text('Auto Schedule'); 740 if (!response.success) { 741 alert(response.data && response.data.message ? response.data.message : 'Failed to calculate schedule.'); 742 return; 743 } 744 745 var data = response.data; 746 $('#gw-schedule-date').text(data.formatted_date); 747 748 // Build related articles preview 749 var previewHtml = ''; 750 if (data.related_articles && data.related_articles.length > 0) { 751 previewHtml = '<p><strong>Related articles will be updated to:</strong></p><ul style="margin-left:20px;">'; 752 $.each(data.related_articles, function(i, article) { 753 previewHtml += '<li><a href="' + article.url + '" target="_blank">' + $('<span>').text(article.title).html() + '</a></li>'; 754 }); 755 previewHtml += '</ul>'; 756 } else { 757 previewHtml = '<p><em>No related articles available for this post.</em></p>'; 758 } 759 $('#gw-related-preview').html(previewHtml).show(); 760 $('#gw-reinject-related').prop('checked', true); 761 762 // Store scheduled_date for Phase 2 763 $('#gw-confirm-schedule').data('scheduled_date', data.scheduled_date); 764 $('#gw-confirm-schedule').data('post_id', postId); 765 766 // Show modal 767 $('#apicoid-gw-auto-schedule-confirm-modal').show(); 768 }, 769 error: function() { 770 $('.apicoid-gw-auto-schedule-btn').prop('disabled', false).text('Auto Schedule'); 771 alert('Request failed. Please try again.'); 772 } 773 }); 774 } 775 776 // Phase 2: Confirm handler 777 $(document).on('click', '#gw-confirm-schedule', function() { 778 var $btn = $(this); 779 var postId = $btn.data('post_id'); 780 var scheduledDate = $btn.data('scheduled_date'); 781 var reinjectRelated = $('#gw-reinject-related').is(':checked') ? 1 : 0; 782 783 $btn.prop('disabled', true).text('Scheduling...'); 784 785 $.ajax({ 786 url: apicoidGwPostEdit.ajax_url, 787 type: 'POST', 788 data: { 789 action: 'apicoid_gw_auto_schedule_post', 790 nonce: apicoidGwPostEdit.auto_schedule_nonce, 791 post_id: postId, 792 confirmed: '1', 793 scheduled_date: scheduledDate, 794 reinject_related: reinjectRelated ? '1' : '0' 795 }, 796 success: function(response) { 797 $btn.prop('disabled', false).text('Confirm'); 798 $('#apicoid-gw-auto-schedule-confirm-modal').hide(); 799 800 if (response.success) { 801 // Show success notice 802 if (apicoidGwPostEdit.is_block_editor && typeof wp !== 'undefined' && wp.data && wp.data.dispatch('core/notices')) { 803 wp.data.dispatch('core/notices').createSuccessNotice( 804 response.data.message || 'Post scheduled successfully!', 805 { type: 'snackbar', isDismissible: true } 806 ); 807 } else { 808 alert(response.data.message || 'Post scheduled successfully!'); 809 } 810 // Reload page to reflect new status 811 setTimeout(function() { location.reload(); }, 1000); 812 } else { 813 alert(response.data && response.data.message ? response.data.message : 'Failed to schedule post.'); 814 } 815 }, 816 error: function() { 817 $btn.prop('disabled', false).text('Confirm'); 818 alert('Request failed. Please try again.'); 819 } 820 }); 821 }); 822 823 // Add Auto Schedule button for Classic Editor 824 function addAutoScheduleButtonClassic() { 825 if (window.apicoidGwAutoScheduleBtnAdded) return; 826 827 var $publishingAction = $('#publishing-action'); 828 if ($publishingAction.length === 0) return; 829 830 var postId = $('#post_ID').val(); 831 if (!postId) return; 832 833 var $container = $('<div>', { style: 'padding-top:15px; margin-top:10px; clear:both;' }); 834 var $button = $('<button>', { 835 type: 'button', 836 class: 'button apicoid-gw-auto-schedule-btn', 837 text: 'Auto Schedule', 838 style: 'width:100%' 839 }); 840 841 $button.on('click', function(e) { 842 e.preventDefault(); 843 handleAutoScheduleClick(postId); 844 }); 845 846 $container.append($button); 847 $publishingAction.after($container); 848 window.apicoidGwAutoScheduleBtnAdded = true; 849 } 850 851 // Add Auto Schedule button for Block Editor 852 function addAutoScheduleButtonBlock() { 853 if (window.apicoidGwAutoScheduleBtnAdded) return; 854 if ($('#apicoid-gw-auto-schedule-btn-container').length > 0) { 855 window.apicoidGwAutoScheduleBtnAdded = true; 856 return; 857 } 858 859 // Find the header bar actions area 860 var $headerBar = $('.edit-post-header__settings, .editor-header__settings').first(); 861 if ($headerBar.length === 0) return; 862 863 var postId = 0; 864 var urlMatch = window.location.search.match(/post=(\d+)/); 865 if (urlMatch) { 866 postId = parseInt(urlMatch[1]); 867 } else if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) { 868 try { postId = wp.data.select('core/editor').getCurrentPostId(); } catch(e) {} 869 } 870 if (!postId) return; 871 872 var $button = $('<button>', { 873 type: 'button', 874 id: 'apicoid-gw-auto-schedule-btn-container', 875 class: 'components-button apicoid-gw-auto-schedule-btn', 876 text: 'Auto Schedule', 877 style: 'height:32px; margin-right:8px;' 878 }); 879 880 $button.on('click', function(e) { 881 e.preventDefault(); 882 handleAutoScheduleClick(postId); 883 }); 884 885 $headerBar.prepend($button); 886 window.apicoidGwAutoScheduleBtnAdded = true; 887 } 888 889 if (apicoidGwPostEdit.is_block_editor) { 890 setTimeout(addAutoScheduleButtonBlock, 1000); 891 setTimeout(addAutoScheduleButtonBlock, 2000); 892 setTimeout(addAutoScheduleButtonBlock, 3000); 893 } else { 894 setTimeout(addAutoScheduleButtonClassic, 500); 895 setTimeout(addAutoScheduleButtonClassic, 1000); 896 setTimeout(addAutoScheduleButtonClassic, 2000); 897 } 898 686 899 // Classic Editor: Use MutationObserver to detect when featured image box is added/updated 687 900 if (!apicoidGwPostEdit.is_block_editor && typeof MutationObserver !== 'undefined') { -
apicoid-ghostwriter/trunk/includes/article-optimizer-page.php
r3461338 r3461830 18 18 $args = array( 19 19 'post_type' => 'post', 20 'post_status' => array( 'publish', 'draft' ),20 'post_status' => array( 'publish', 'draft', 'future' ), 21 21 'posts_per_page' => $pagination_size, 22 22 'paged' => $paged, … … 129 129 $metadata_array = ! empty( $metadata_json ) ? json_decode( $metadata_json, true ) : array(); 130 130 $status = get_post_status( $post->ID ); 131 $status_label = 'publish' === $status ? __( 'Published', 'apicoid-ghostwriter' ) : __( 'Draft', 'apicoid-ghostwriter' ); 132 $status_class = 'publish' === $status ? 'status-publish' : 'status-draft'; 131 if ( 'publish' === $status ) { 132 $status_label = __( 'Published', 'apicoid-ghostwriter' ); 133 $status_class = 'status-publish'; 134 } elseif ( 'future' === $status ) { 135 $status_label = __( 'Scheduled', 'apicoid-ghostwriter' ); 136 $status_class = 'status-future'; 137 } else { 138 $status_label = __( 'Draft', 'apicoid-ghostwriter' ); 139 $status_class = 'status-draft'; 140 } 133 141 ?> 134 142 <tr> … … 145 153 <?php echo esc_html( $status_label ); ?> 146 154 </span> 155 <?php if ( 'future' === $status ) : ?> 156 <br><small style="color:#666;"><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $post->post_date ) ) ); ?></small> 157 <?php endif; ?> 147 158 </td> 148 159 <td> … … 185 196 <?php esc_html_e( 'View Article', 'apicoid-ghostwriter' ); ?> 186 197 </a> 187 <?php if ( 'publish' === $status ) :198 <?php if ( 'publish' === $status || 'future' === $status ) : 188 199 $post_permalink = get_permalink( $post->ID ); 189 200 ?> … … 811 822 <?php endforeach; ?> 812 823 </div> 824 <label style="display:block; margin-top:10px;"> 825 <input type="checkbox" id="apicoid-gw-category-include-scheduled" value="1" /> 826 <?php esc_html_e( 'Include scheduled content', 'apicoid-ghostwriter' ); ?> 827 </label> 813 828 <p class="description"><?php esc_html_e( 'Select one category to get article suggestions', 'apicoid-ghostwriter' ); ?></p> 814 829 </td> -
apicoid-ghostwriter/trunk/includes/settings-page.php
r3461338 r3461830 253 253 </div> 254 254 </div> 255 256 <!-- Auto Schedule Settings --> 257 <?php 258 $auto_schedule_per_day = get_option( 'apicoid_gw_auto_schedule_per_day', 3 ); 259 $auto_schedule_start_hour = get_option( 'apicoid_gw_auto_schedule_start_hour', '08:00' ); 260 $auto_schedule_end_hour = get_option( 'apicoid_gw_auto_schedule_end_hour', '20:00' ); 261 ?> 262 <div class="apicoid-gw-admin"> 263 <div class="apicoid-gw-header"> 264 <h2><?php esc_html_e( 'Auto Schedule', 'apicoid-ghostwriter' ); ?></h2> 265 <p><?php esc_html_e( 'Configure automatic post scheduling. Set daily article limits and publishing time windows, then auto-schedule posts from the post editor with one click.', 'apicoid-ghostwriter' ); ?></p> 266 </div> 267 <div class="apicoid-gw-content"> 268 <div id="apicoid-gw-auto-schedule-notice"></div> 269 <table class="form-table"> 270 <tr> 271 <th scope="row"> 272 <label for="apicoid-gw-auto-schedule-per-day"><?php esc_html_e( 'Articles Per Day', 'apicoid-ghostwriter' ); ?></label> 273 </th> 274 <td> 275 <input type="number" id="apicoid-gw-auto-schedule-per-day" value="<?php echo esc_attr( $auto_schedule_per_day ); ?>" min="1" max="50" class="small-text" /> 276 <p class="description"><?php esc_html_e( 'Maximum number of articles to schedule per day (default: 3)', 'apicoid-ghostwriter' ); ?></p> 277 </td> 278 </tr> 279 <tr> 280 <th scope="row"> 281 <label for="apicoid-gw-auto-schedule-start-hour"><?php esc_html_e( 'Start Hour', 'apicoid-ghostwriter' ); ?></label> 282 </th> 283 <td> 284 <input type="time" id="apicoid-gw-auto-schedule-start-hour" value="<?php echo esc_attr( $auto_schedule_start_hour ); ?>" /> 285 <p class="description"><?php esc_html_e( 'Earliest time to schedule posts (default: 08:00)', 'apicoid-ghostwriter' ); ?></p> 286 </td> 287 </tr> 288 <tr> 289 <th scope="row"> 290 <label for="apicoid-gw-auto-schedule-end-hour"><?php esc_html_e( 'End Hour', 'apicoid-ghostwriter' ); ?></label> 291 </th> 292 <td> 293 <input type="time" id="apicoid-gw-auto-schedule-end-hour" value="<?php echo esc_attr( $auto_schedule_end_hour ); ?>" /> 294 <p class="description"><?php esc_html_e( 'Latest time to schedule posts (default: 20:00)', 'apicoid-ghostwriter' ); ?></p> 295 </td> 296 </tr> 297 </table> 298 <div style="display:flex; gap:10px; margin-top:10px;"> 299 <button type="button" id="apicoid-gw-auto-schedule-save" class="button button-primary"><?php esc_html_e( 'Save', 'apicoid-ghostwriter' ); ?></button> 300 <button type="button" id="apicoid-gw-auto-schedule-rearrange" class="button button-secondary"><?php esc_html_e( 'Rearrange Queue', 'apicoid-ghostwriter' ); ?></button> 301 </div> 302 </div> 303 </div> 255 304 </div> -
apicoid-ghostwriter/trunk/readme.txt
r3461407 r3461830 5 5 Requires at least: 6.2 6 6 Tested up to: 6.9 7 Stable tag: 1. 3.37 Stable tag: 1.4.0 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 35 35 36 36 * **Secure API Key Validation**: Secure API key validation system ensures only valid keys can be used. 37 38 * **Auto Schedule**: Schedule draft posts with one click. Configure how many articles to publish per day and a time window (e.g. 08:00–20:00), then hit the "Auto Schedule" button on any post editor. GhostWriter finds the next available slot, picks a random time within your window, and shows you a preview before confirming. It can also re-generate related article links so they only point to posts that will already be live when the scheduled post publishes — no broken links. Use "Rearrange Queue" on the Settings page to redistribute all scheduled posts whenever you change your limits. 37 39 38 40 = How It Works = … … 129 131 130 132 == Changelog == 133 134 = 1.4.0 = 135 * Auto Schedule: New feature to automatically schedule posts with one click from the post editor (Classic & Block Editor) 136 * Configurable daily article limits and publishing time windows (Settings > Auto Schedule) 137 * Rearrange Queue: Redistribute all scheduled posts according to current settings while preserving order 138 * Smart related article reinjection: When auto-scheduling, optionally re-generate related article links to include scheduled posts (prevents 404s by only linking to posts that will be live before the current post) 139 * Related article label now reads from Default preset instead of legacy standalone option 140 * Include Scheduled Content: New checkbox in Generate by Category to include scheduled posts in category analysis 141 * Scheduled posts now visible in Article Generator list with blue "Scheduled" badge and publish date 142 * Create Support Article now available for scheduled posts (not just published) 143 * Future post status added to Article Generator list query 131 144 132 145 = 1.3.3 = … … 208 221 == Upgrade Notice == 209 222 223 = 1.4.0 = 224 This update introduces Auto Schedule, allowing you to schedule posts with one click from the post editor. Configure daily article limits and time windows in Settings, then auto-schedule drafts with smart related article reinjection that prevents 404 links. Scheduled posts are now visible in the Article Generator list and can be used to generate support articles. 225 210 226 = 1.3.1 = 211 227 This update introduces Auto Google Index feature that automatically submits your post and page URLs to Google Indexing API when published or updated. Includes comprehensive submission logging, encrypted service account storage, and test connection functionality. See the new "Auto Google Index" menu for setup instructions.
Note: See TracChangeset
for help on using the changeset viewer.