Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -40,6 +40,7 @@ class MLS_CLI {
WP_CLI::add_command('mls recovery', array($instance, 'recovery'));
WP_CLI::add_command('mls media', array($instance, 'media'));
WP_CLI::add_command('mls geo', array($instance, 'geo'));
WP_CLI::add_command('mls property', array($instance, 'property'));
}
/**
@@ -256,7 +257,7 @@ class MLS_CLI {
* ## OPTIONS
*
* <type>
* : Sync type: full, incremental, media, or resume
* : Sync type: full, incremental, media-refresh, or resume
*
* [--dry-run]
* : Show what would be synced without making changes
@@ -267,8 +268,8 @@ class MLS_CLI {
* [--id=<sync_id>]
* : Sync state ID to resume (for resume command)
*
* [--force]
* : Force re-download of media (for media command)
* [--days=<n>]
* : Days ahead to check for expiring media (for incremental and media-refresh, default: 3)
*
* [--quiet]
* : Suppress progress output
@@ -281,8 +282,10 @@ class MLS_CLI {
* wp mls sync full
* wp mls sync full --dry-run --limit=10
* wp mls sync incremental
* wp mls sync incremental --days=7
* wp mls sync incremental --verbose
* wp mls sync media --limit=100
* wp mls sync media-refresh
* wp mls sync media-refresh --days=7
* wp mls sync resume --id=5
*
* @subcommand sync
@@ -375,6 +378,22 @@ class MLS_CLI {
echo "\n";
}
$this->output_sync_result($result);
// Run media refresh after successful incremental sync
if ($result['success'] && !$dry_run) {
$days = isset($assoc_args['days']) ? (int) $assoc_args['days'] : 3;
WP_CLI::line('');
WP_CLI::line("Running media refresh (properties expiring within {$days} days)...");
if (!$quiet) {
$this->print_progress_legend($verbose);
}
$media_result = $sync_engine->run_media_refresh_sync($days, false, $progress_callback);
if (!$quiet) {
echo "\n";
}
$this->output_sync_result($media_result);
}
break;
case 'media':
@@ -384,6 +403,24 @@ class MLS_CLI {
WP_CLI::line('');
WP_CLI::line('Use "wp mls media status" to see cache statistics.');
WP_CLI::line('Use "wp mls media fetch --listing=<key>" to pre-cache a specific listing.');
WP_CLI::line('Use "wp mls sync media-refresh" to proactively refresh expiring media URLs.');
break;
case 'media-refresh':
$days = isset($assoc_args['days']) ? (int) $assoc_args['days'] : 3;
WP_CLI::line("Starting media refresh sync (properties expiring within {$days} days)...");
if ($dry_run) {
WP_CLI::line('DRY RUN - No changes will be made');
}
if (!$quiet) {
$this->print_progress_legend($verbose);
}
$result = $sync_engine->run_media_refresh_sync($days, $dry_run, $progress_callback);
if (!$quiet) {
echo "\n";
}
$this->output_sync_result($result);
break;
case 'resume':
@@ -405,7 +442,7 @@ class MLS_CLI {
break;
default:
WP_CLI::error("Unknown sync type: {$type}. Use 'full', 'incremental', 'media', or 'resume'.");
WP_CLI::error("Unknown sync type: {$type}. Use 'full', 'incremental', 'media-refresh', or 'resume'.");
}
}
@@ -1306,4 +1343,113 @@ class MLS_CLI {
WP_CLI::log("... and " . ($total - 100) . " more.");
}
}
/**
* Fetch a property directly from the MLS API and dump the response.
*
* ## OPTIONS
*
* <listing_id>
* : The MLS listing ID (e.g., NST6755550 or 6755550)
*
* [--format=<format>]
* : Output format: json, table, or fields (default: fields)
*
* [--fields=<fields>]
* : Comma-separated list of fields to show (for fields format)
*
* ## EXAMPLES
*
* wp mls property 6755550
* wp mls property NST6755550 --format=json
* wp mls property 6755550 --fields=StandardStatus,ListPrice,CloseDate
*
* @subcommand property
*/
public function property($args, $assoc_args) {
$listing_id = isset($args[0]) ? $args[0] : null;
if (!$listing_id) {
WP_CLI::error('Please provide a listing ID');
}
// Add NST prefix if not present
if (!preg_match('/^[A-Z]{3}/', $listing_id)) {
$listing_id = 'NST' . $listing_id;
}
WP_CLI::line("Fetching property {$listing_id} from MLS API...");
$api_client = $this->plugin->get_api_client();
$result = $api_client->get_property_media($listing_id);
if (is_wp_error($result)) {
WP_CLI::error('API Error: ' . $result->get_error_message());
}
if (!$result) {
WP_CLI::error("Property {$listing_id} not found in MLS");
}
$format = isset($assoc_args['format']) ? $assoc_args['format'] : 'fields';
switch ($format) {
case 'json':
echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
break;
case 'table':
// Flatten for table display
$flat = array();
foreach ($result as $key => $value) {
if (!is_array($value)) {
$flat[$key] = $value;
}
}
WP_CLI\Utils\format_items('table', array($flat), array_keys($flat));
break;
case 'fields':
default:
// Show key fields
$key_fields = array(
'ListingId',
'ListingKey',
'StandardStatus',
'MlsStatus',
'ListPrice',
'ClosePrice',
'CloseDate',
'ListOfficeName',
'ListAgentFullName',
'StreetNumber',
'StreetName',
'City',
'StateOrProvince',
'ModificationTimestamp',
);
// Allow custom fields
if (isset($assoc_args['fields'])) {
$key_fields = explode(',', $assoc_args['fields']);
}
WP_CLI::line('');
WP_CLI::line('=== Property Details from MLS API ===');
WP_CLI::line('');
foreach ($key_fields as $field) {
$field = trim($field);
$value = isset($result[$field]) ? $result[$field] : '(not set)';
if (is_array($value)) {
$value = json_encode($value);
}
WP_CLI::line(sprintf(' %-25s %s', $field . ':', $value));
}
WP_CLI::line('');
WP_CLI::line('Use --format=json to see full response');
break;
}
}
}