Integrate MLS listings with property map and add smart sync

Property Map:
- Replace ACF-based property display with MLS database queries
- Use real lat/lng coordinates from MLS (100% coverage)
- Create property-card-mls.php template for MLS property cards
- Update AJAX handler to filter MLS properties

MLS Plugin Enhancements:
- Add 'wp mls run' smart sync command (auto-detects full/incremental/resume)
- Add database index migrations for lat/lng and composite search indexes
- Add comprehensive README.md documentation

Documentation:
- Update site README.md with sysadmin quick reference
- Add FEATURES_PENDING_12_15.md tracking client feature requests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-15 22:32:41 -06:00
parent b9cddd2f64
commit fc018ca604
13 changed files with 2346 additions and 308 deletions
@@ -937,4 +937,113 @@ class MLS_Sync_Engine {
return $this->resume_sync($resumable->id, $progress_callback);
}
/**
* Smart sync - autonomous self-healing sync that handles all scenarios
*
* Decision logic:
* 1. If a sync is currently running (and not stale), abort
* 2. If there's a resumable failed/interrupted sync, resume it
* 3. If no data exists, run full sync
* 4. Otherwise, run incremental sync
*
* On failure, the sync state is preserved for future resume.
*
* @param callable|null $progress_callback Progress callback
* @param callable|null $status_callback Callback for status messages: function(string $message, string $level)
* @return array Sync results with 'action' key indicating what was done
*/
public function smart_sync($progress_callback = null, $status_callback = null) {
// Helper to emit status messages
$status = function($message, $level = 'info') use ($status_callback) {
if ($status_callback) {
call_user_func($status_callback, $message, $level);
}
$this->logger->log($level, $message);
};
// Step 1: Clean up stale syncs (running > 1 hour = probably dead)
$stale_cleaned = $this->cleanup_stale_syncs();
if ($stale_cleaned > 0) {
$status("Cleaned up {$stale_cleaned} stale sync(s)", 'info');
}
// Step 2: Check if a sync is actively running
$running = $this->get_running_sync();
if ($running) {
$status("Sync #{$running->id} is already running (started {$running->started_at})", 'warning');
return array(
'success' => false,
'action' => 'aborted',
'reason' => 'Sync already running',
'running_sync' => $running,
);
}
// Step 3: Check for resumable syncs
$resumable = $this->get_latest_resumable();
if ($resumable) {
$status("Found resumable sync #{$resumable->id} ({$resumable->sync_type}), processed {$resumable->records_processed} records", 'info');
$status("Resuming...", 'info');
$result = $this->resume_sync($resumable->id, $progress_callback);
$result['action'] = 'resumed';
$result['resumed_sync_id'] = $resumable->id;
return $result;
}
// Step 4: Check if we have any data
$has_data = $this->has_synced_data();
if (!$has_data) {
// No data - need full sync
$status("No existing data found, starting full sync", 'info');
$result = $this->run_full_sync(false, null, $progress_callback);
$result['action'] = 'full';
return $result;
}
// Step 5: We have data - run incremental sync
$last_timestamp = $this->get_last_modification_timestamp();
$status("Running incremental sync (changes since {$last_timestamp})", 'info');
$result = $this->run_incremental_sync(false, $progress_callback);
$result['action'] = 'incremental';
return $result;
}
/**
* Check if there's a currently running sync (not stale)
*
* @return object|null Running sync state or null
*/
public function get_running_sync() {
global $wpdb;
$one_hour_ago = date('Y-m-d H:i:s', strtotime('-1 hour'));
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->db->sync_state_table()}
WHERE status = 'running' AND updated_at >= %s
ORDER BY started_at DESC
LIMIT 1",
$one_hour_ago
));
}
/**
* Check if we have any synced property data
*
* @return bool
*/
public function has_synced_data() {
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE mlg_can_view = 1"
);
return (int) $count > 0;
}
}