| @@ -2873,8 +2873,15 @@ | ||
| 2873 | 2873 | |
| 2874 | 2874 | // Success: reshape the list into autocomplete {label,value} pairs. |
| 2875 | 2875 | if ( isset( $answer['success'] ) && true === $answer['success'] ) { |
| 2876 | 2876 | $mls_data = $answer['mls_list']; |
| 2877 | + | |
| 2878 | + // Fresh catalogue in hand: refresh registered connections' display | |
| 2879 | + // names so an MLS renamed upstream shows its new name everywhere | |
| 2880 | + // (Connections table, import task picker) after a cache clear. | |
| 2881 | + if ( is_array( $mls_data ) && class_exists( 'Mlsimport_Connections' ) ) { | |
| 2882 | + Mlsimport_Connections::sync_names( $mls_data ); | |
| 2883 | + } | |
| 2877 | 2884 | // Prepend the "not listed" opt-out choice. |
| 2878 | 2885 | $mls_data['0'] = esc_html__( 'My MLS is not on this list', 'mlsimport' ); |
| 2879 | 2886 | |
| 2880 | 2887 | $autofill_array = array(); |
| @@ -2952,11 +2959,17 @@ | ||
| 2952 | 2959 | /** |
| 2953 | 2960 | * Cron entry point for the daily SaaS reconciliation pass. |
| 2954 | 2961 | * |
| 2955 | 2962 | * Bails out early unless at least one non-trashed import task has a title, then |
| 2956 | - * kicks off reconciliation when an MLS name is configured. | |
| 2963 | + * hands off to the per-connection runner (#279) when an MLS name is configured: | |
| 2964 | + * one sub-run of the deep reconciliation module per registered connection, | |
| 2965 | + * or the single legacy unscoped run while no connections are registered. | |
| 2957 | 2966 | * |
| 2958 | - * @return void | |
| 2967 | + * The same function handles the deduplicated one-hour retry hook — a retry | |
| 2968 | + * re-runs all connections, and completed ones converge to no-op keeps. Trigger | |
| 2969 | + * code intentionally owns no snapshot, status, batching, or deletion decisions. | |
| 2970 | + * | |
| 2971 | + * @return array<int, array<string, int|string>>|null Outcome per connection, or null when ineligible. | |
| 2959 | 2972 | */ |
| 2960 | 2973 | function mlsimport_saas_reconciliation_event_function() { |
| 2961 | 2974 | |
| 2962 | 2975 | // Pull the API token (side effect: ensures a fresh token) and plugin options. |
| @@ -2973,15 +2986,19 @@ | ||
| 2973 | 2986 | LIMIT 1" |
| 2974 | 2987 | ); |
| 2975 | 2988 | // No titled task -> nothing to reconcile, abort. |
| 2976 | 2989 | if ( ! $has_titled_task ) { |
| 2977 | - return; | |
| 2990 | + return null; | |
| 2978 | 2991 | } |
| 2979 | 2992 | |
| 2980 | - // Only reconcile when an MLS name is configured. | |
| 2993 | + // Only reconcile when an MLS name is configured. The runner sequences one | |
| 2994 | + // scoped sub-run per registered connection (legacy unscoped run when the | |
| 2995 | + // registry is empty) and records/logs every outcome itself. | |
| 2981 | 2996 | if ( isset( $options['mlsimport_mls_name'] ) && '' !== $options['mlsimport_mls_name'] ) { |
| 2982 | - $mlsimport->admin->mlsimport_saas_start_doing_reconciliation(); | |
| 2997 | + return mlsimport_reconciliation_run_connections(); | |
| 2983 | 2998 | } |
| 2999 | + | |
| 3000 | + return null; | |
| 2984 | 3001 | } |
| 2985 | 3002 | |
| 2986 | 3003 | /* |
| 2987 | 3004 | * Admin extra columns for MlsImport Items |
| @@ -3017,8 +3034,9 @@ | ||
| 3017 | 3034 | $splice = array_splice( $columns, 2 ); |
| 3018 | 3035 | |
| 3019 | 3036 | // Append the plugin's own columns. |
| 3020 | 3037 | $columns['mlsimport_items_params'] = esc_html__( 'Import Parameters', 'mlsimport' ); |
| 3038 | + $columns['mlsimport_task_health'] = esc_html__( 'Status', 'mlsimport' ); | |
| 3021 | 3039 | $columns['mlsimport_last_action'] = esc_html__( 'Last action', 'mlsimport' ); |
| 3022 | 3040 | $columns['mlsimport_autoupdates'] = esc_html__( 'Auto Update Enabled', 'mlsimport' ); |
| 3023 | 3041 | |
| 3024 | 3042 | // Return the plugin columns followed by the preserved slice (reversed). |
| @@ -3081,11 +3099,12 @@ | ||
| 3081 | 3099 | * @param int $postID Import Task post ID. |
| 3082 | 3100 | * @return void Output is echoed directly. |
| 3083 | 3101 | */ |
| 3084 | 3102 | function mlsimport_populate_columns_params_display( $postID ) { |
| 3085 | - // Field definitions come from the admin class. | |
| 3103 | + // Field definitions come from the admin class, scoped to the TASK's own | |
| 3104 | + // connection (#277) so each row's parameters render against its MLS. | |
| 3086 | 3105 | global $mlsimport; |
| 3087 | - $field_import = $mlsimport->admin->mlsimport_saas_return_mls_fields(); | |
| 3106 | + $field_import = $mlsimport->admin->mlsimport_saas_return_mls_fields( mlsimport_task_mls_id( (int) $postID ) ); | |
| 3088 | 3107 | |
| 3089 | 3108 | // Fields whose stored value is always shown verbatim (never collapsed to "ALL"). |
| 3090 | 3109 | $select_all_none = array( |
| 3091 | 3110 | 'InternetAddressDisplayYN', |
| @@ -3201,8 +3220,36 @@ | ||
| 3201 | 3220 | <?php |
| 3202 | 3221 | |
| 3203 | 3222 | // Then the per-field import parameters. |
| 3204 | 3223 | mlsimport_populate_columns_params_display( $post->ID ); |
| 3224 | + } elseif ( 'mlsimport_task_health' === $column ) { | |
| 3225 | + // Status column (GitHub issue #200): surface stuck, failed, and | |
| 3226 | + // sync-overdue tasks directly in the list. All inputs are already | |
| 3227 | + // recorded — the run status meta (state, progress, heartbeat, error), | |
| 3228 | + // the sync watermark, and the auto-sync flag. The badge decision | |
| 3229 | + // itself lives in the pure, unit-tested mlsimport_task_health(). | |
| 3230 | + $health_status = get_post_meta( $post->ID, 'mlsimport_import_run_status', true ); | |
| 3231 | + $health = mlsimport_task_health( | |
| 3232 | + is_array( $health_status ) ? $health_status : array(), | |
| 3233 | + (string) get_post_meta( $post->ID, 'mlsimport_last_date', true ), | |
| 3234 | + 1 === (int) get_post_meta( $post->ID, 'mlsimport_item_stat_cron', true ), | |
| 3235 | + time(), | |
| 3236 | + // Watermarks are stored with wp_date() in site-local time, so | |
| 3237 | + // the overdue cutoff must be built the same way to compare. | |
| 3238 | + wp_date( 'Y-m-d\TH:i', time() - MLSIMPORT_TASK_HEALTH_OVERDUE_AFTER ), | |
| 3239 | + // The hourly runner's own eligibility rule (GitHub issue #330): | |
| 3240 | + // a task it will never pick up must not read as merely overdue. | |
| 3241 | + mlsimport_cron_task_is_eligible( | |
| 3242 | + (int) get_post_meta( $post->ID, 'mlsimport_initial_import_completed', true ), | |
| 3243 | + (string) get_post_meta( $post->ID, 'mlsimport_spawn_status', true ) | |
| 3244 | + ) | |
| 3245 | + ); | |
| 3246 | + ?> | |
| 3247 | + <span class="mlsimport-task-health mlsimport-task-health--<?php echo esc_attr( $health['level'] ); ?>"> | |
| 3248 | + <?php echo esc_html( $health['label'] ); ?> | |
| 3249 | + </span> | |
| 3250 | + <p class="mlsimport-task-health__message"><?php echo esc_html( $health['message'] ); ?></p> | |
| 3251 | + <?php | |
| 3205 | 3252 | } elseif ( 'mlsimport_last_action' === $column ) { |
| 3206 | 3253 | // Last action column: date of the most recent import activity. |
| 3207 | 3254 | $last_date = get_post_meta( $post->ID, 'mlsimport_last_date', true ); |
| 3208 | 3255 | if ( '' !== $last_date ) { |