Plugin Directory

Changeset 3461830


Ignore:
Timestamp:
02/15/2026 01:30:17 PM (7 days ago)
Author:
rifaldye
Message:

Release 1.4.0 - Auto Schedule feature: one-click post scheduling, configurable daily limits and time windows, smart related article reinjection, rearrange queue, scheduled posts in article list with support article generation

Location:
apicoid-ghostwriter
Files:
18 edited
1 copied

Legend:

Unmodified
Added
Removed
  • apicoid-ghostwriter/tags/1.4.0/CHANGELOG.md

    r3461338 r3461830  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
    66and 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
    731
    832## [1.3.2] - 2026-02-14
     
    119143- Secure API key validation
    120144
     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
    121147[1.3.2]: https://github.com/apicoid/ghostwriter/compare/v1.3.1...v1.3.2
    122148[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  
    44 * Plugin URI:  https://wordpress.org/plugins/apicoid-ghostwriter/
    55 * 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.3
     6 * Version:     1.4.0
    77 * Author:      Api.co.id
    88 * Author URI:  https://api.co.id
     
    2121
    2222// Define plugin constants
    23 define( 'APICOID_GW_VERSION', '1.3.3' );
     23define( 'APICOID_GW_VERSION', '1.4.0' );
    2424define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) );
    2525define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) );
     
    129129        // Handle Google Index log clear
    130130        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' ) );
    131136
    132137        // Auto-submit URL to Google on post publish/update
     
    564569        );
    565570       
     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
    566599        // Register Google Index settings
    567600        register_setting(
     
    11831216     * @return string Content with related articles inserted.
    11841217     */
    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 = '' ) {
    11861219        if ( empty( $category_ids ) || ! is_array( $category_ids ) || $word_count <= 0 ) {
    11871220            return $content;
     
    11971230       
    11981231        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)
    12001233            $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 ) {
    12021243                $related_posts[] = $pillar_article_id;
    12031244                $pillar_included = true;
     
    12131254        }
    12141255       
    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 );
    12261273       
    12271274        // Merge pillar article with additional posts
     
    13431390
    13441391        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            }
    13461399        }
    13471400        if ( '' === trim( $label ) ) {
     
    17471800        }
    17481801       
    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
    17501806        $args = array(
    17511807            'post_type'      => 'post',
    1752             'post_status'    => 'publish',
     1808            'post_status'    => $post_status,
    17531809            'posts_per_page' => 50, // Limit to 50 articles
    17541810            'cat'            => $category_id,
     
    27362792                    'ajax_url' => admin_url( 'admin-ajax.php' ),
    27372793                    'nonce'    => wp_create_nonce( 'apicoid_gw_generate_featured_image' ),
     2794                    'auto_schedule_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_post' ),
    27382795                    'is_block_editor' => $is_block_editor,
    27392796                    'strings'  => array(
     
    28782935                )
    28792936            );
     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            );
    28802951        }
    28812952
     
    34303501
    34313502    /**
     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    /**
    34323796     * Auto-submit URL to Google when a post/page is published or updated
    34333797     *
  • apicoid-ghostwriter/tags/1.4.0/assets/css/admin.css

    r3461407 r3461830  
    478478    height: 16px;
    479479    vertical-align: text-bottom;
    480 }
    481 
    482 .apicoid-gw-index-log {
     480}.apicoid-gw-index-log {
    483481    margin-top: 20px;
    484482    padding: 15px 20px;
     
    671669    pointer-events: none;
    672670}
     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  
    360360            }
    361361        } // 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
    362434    });
    363    
     435
    364436})(jQuery);
    365437
  • apicoid-ghostwriter/tags/1.4.0/assets/js/article-generator.js

    r3461407 r3461830  
    701701                    action: 'apicoid_gw_generate_article_by_category',
    702702                    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
    704705                },
    705706                dataType: 'json',
  • apicoid-ghostwriter/tags/1.4.0/assets/js/post-edit.js

    r3457596 r3461830  
    684684    }
    685685
     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">&times;</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
    686899    // Classic Editor: Use MutationObserver to detect when featured image box is added/updated
    687900    if (!apicoidGwPostEdit.is_block_editor && typeof MutationObserver !== 'undefined') {
  • apicoid-ghostwriter/tags/1.4.0/includes/article-optimizer-page.php

    r3461338 r3461830  
    1818$args = array(
    1919    'post_type'      => 'post',
    20     'post_status'    => array( 'publish', 'draft' ),
     20    'post_status'    => array( 'publish', 'draft', 'future' ),
    2121    'posts_per_page' => $pagination_size,
    2222    'paged'          => $paged,
     
    129129                            $metadata_array = ! empty( $metadata_json ) ? json_decode( $metadata_json, true ) : array();
    130130                            $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                            }
    133141                        ?>
    134142                            <tr>
     
    145153                                        <?php echo esc_html( $status_label ); ?>
    146154                                    </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; ?>
    147158                                </td>
    148159                                <td>
     
    185196                                            <?php esc_html_e( 'View Article', 'apicoid-ghostwriter' ); ?>
    186197                                        </a>
    187                                         <?php if ( 'publish' === $status ) :
     198                                        <?php if ( 'publish' === $status || 'future' === $status ) :
    188199                                            $post_permalink = get_permalink( $post->ID );
    189200                                        ?>
     
    811822                                        <?php endforeach; ?>
    812823                                    </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>
    813828                                    <p class="description"><?php esc_html_e( 'Select one category to get article suggestions', 'apicoid-ghostwriter' ); ?></p>
    814829                                </td>
  • apicoid-ghostwriter/tags/1.4.0/includes/settings-page.php

    r3461338 r3461830  
    253253        </div>
    254254    </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>
    255304</div>
  • apicoid-ghostwriter/tags/1.4.0/readme.txt

    r3461407 r3461830  
    55Requires at least: 6.2
    66Tested up to: 6.9
    7 Stable tag: 1.3.3
     7Stable tag: 1.4.0
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    3535
    3636* **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.
    3739
    3840= How It Works =
     
    129131
    130132== 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
    131144
    132145= 1.3.3 =
     
    208221== Upgrade Notice ==
    209222
     223= 1.4.0 =
     224This 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
    210226= 1.3.1 =
    211227This 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  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
    66and 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
    731
    832## [1.3.2] - 2026-02-14
     
    119143- Secure API key validation
    120144
     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
    121147[1.3.2]: https://github.com/apicoid/ghostwriter/compare/v1.3.1...v1.3.2
    122148[1.3.1]: https://github.com/apicoid/ghostwriter/compare/v1.3.0...v1.3.1
  • apicoid-ghostwriter/trunk/apicoid-ghostwriter.php

    r3461407 r3461830  
    44 * Plugin URI:  https://wordpress.org/plugins/apicoid-ghostwriter/
    55 * 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.3
     6 * Version:     1.4.0
    77 * Author:      Api.co.id
    88 * Author URI:  https://api.co.id
     
    2121
    2222// Define plugin constants
    23 define( 'APICOID_GW_VERSION', '1.3.3' );
     23define( 'APICOID_GW_VERSION', '1.4.0' );
    2424define( 'APICOID_GW_DIR', plugin_dir_path( __FILE__ ) );
    2525define( 'APICOID_GW_URL', plugin_dir_url( __FILE__ ) );
     
    129129        // Handle Google Index log clear
    130130        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' ) );
    131136
    132137        // Auto-submit URL to Google on post publish/update
     
    564569        );
    565570       
     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
    566599        // Register Google Index settings
    567600        register_setting(
     
    11831216     * @return string Content with related articles inserted.
    11841217     */
    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 = '' ) {
    11861219        if ( empty( $category_ids ) || ! is_array( $category_ids ) || $word_count <= 0 ) {
    11871220            return $content;
     
    11971230       
    11981231        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)
    12001233            $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 ) {
    12021243                $related_posts[] = $pillar_article_id;
    12031244                $pillar_included = true;
     
    12131254        }
    12141255       
    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 );
    12261273       
    12271274        // Merge pillar article with additional posts
     
    13431390
    13441391        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            }
    13461399        }
    13471400        if ( '' === trim( $label ) ) {
     
    17471800        }
    17481801       
    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
    17501806        $args = array(
    17511807            'post_type'      => 'post',
    1752             'post_status'    => 'publish',
     1808            'post_status'    => $post_status,
    17531809            'posts_per_page' => 50, // Limit to 50 articles
    17541810            'cat'            => $category_id,
     
    27362792                    'ajax_url' => admin_url( 'admin-ajax.php' ),
    27372793                    'nonce'    => wp_create_nonce( 'apicoid_gw_generate_featured_image' ),
     2794                    'auto_schedule_nonce' => wp_create_nonce( 'apicoid_gw_auto_schedule_post' ),
    27382795                    'is_block_editor' => $is_block_editor,
    27392796                    'strings'  => array(
     
    28782935                )
    28792936            );
     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            );
    28802951        }
    28812952
     
    34303501
    34313502    /**
     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    /**
    34323796     * Auto-submit URL to Google when a post/page is published or updated
    34333797     *
  • apicoid-ghostwriter/trunk/assets/css/admin.css

    r3461407 r3461830  
    478478    height: 16px;
    479479    vertical-align: text-bottom;
    480 }
    481 
    482 .apicoid-gw-index-log {
     480}.apicoid-gw-index-log {
    483481    margin-top: 20px;
    484482    padding: 15px 20px;
     
    671669    pointer-events: none;
    672670}
     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  
    360360            }
    361361        } // 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
    362434    });
    363    
     435
    364436})(jQuery);
    365437
  • apicoid-ghostwriter/trunk/assets/js/article-generator.js

    r3461407 r3461830  
    701701                    action: 'apicoid_gw_generate_article_by_category',
    702702                    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
    704705                },
    705706                dataType: 'json',
  • apicoid-ghostwriter/trunk/assets/js/post-edit.js

    r3457596 r3461830  
    684684    }
    685685
     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">&times;</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
    686899    // Classic Editor: Use MutationObserver to detect when featured image box is added/updated
    687900    if (!apicoidGwPostEdit.is_block_editor && typeof MutationObserver !== 'undefined') {
  • apicoid-ghostwriter/trunk/includes/article-optimizer-page.php

    r3461338 r3461830  
    1818$args = array(
    1919    'post_type'      => 'post',
    20     'post_status'    => array( 'publish', 'draft' ),
     20    'post_status'    => array( 'publish', 'draft', 'future' ),
    2121    'posts_per_page' => $pagination_size,
    2222    'paged'          => $paged,
     
    129129                            $metadata_array = ! empty( $metadata_json ) ? json_decode( $metadata_json, true ) : array();
    130130                            $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                            }
    133141                        ?>
    134142                            <tr>
     
    145153                                        <?php echo esc_html( $status_label ); ?>
    146154                                    </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; ?>
    147158                                </td>
    148159                                <td>
     
    185196                                            <?php esc_html_e( 'View Article', 'apicoid-ghostwriter' ); ?>
    186197                                        </a>
    187                                         <?php if ( 'publish' === $status ) :
     198                                        <?php if ( 'publish' === $status || 'future' === $status ) :
    188199                                            $post_permalink = get_permalink( $post->ID );
    189200                                        ?>
     
    811822                                        <?php endforeach; ?>
    812823                                    </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>
    813828                                    <p class="description"><?php esc_html_e( 'Select one category to get article suggestions', 'apicoid-ghostwriter' ); ?></p>
    814829                                </td>
  • apicoid-ghostwriter/trunk/includes/settings-page.php

    r3461338 r3461830  
    253253        </div>
    254254    </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>
    255304</div>
  • apicoid-ghostwriter/trunk/readme.txt

    r3461407 r3461830  
    55Requires at least: 6.2
    66Tested up to: 6.9
    7 Stable tag: 1.3.3
     7Stable tag: 1.4.0
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    3535
    3636* **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.
    3739
    3840= How It Works =
     
    129131
    130132== 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
    131144
    132145= 1.3.3 =
     
    208221== Upgrade Notice ==
    209222
     223= 1.4.0 =
     224This 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
    210226= 1.3.1 =
    211227This 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.