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
@@ -41,16 +41,31 @@ class MLS_Cluster {
/**
* Minimum properties before any grouping kicks in
* Below this, always show individual markers
* Below this, always show individual markers regardless of zoom
*/
const MIN_FOR_GROUPING = 30;
/**
* Viewport-aware marker threshold
* If viewport contains fewer than this many properties AND zoom >= 9,
* show individual markers instead of clusters.
* This helps mobile viewports which show smaller geographic areas.
*/
const VIEWPORT_MARKER_THRESHOLD = 120;
/**
* Minimum zoom level for viewport-aware marker display
* Below this zoom, always use density/cluster mode even with few properties
* (prevents showing 100+ scattered markers across entire state)
*/
const MIN_ZOOM_FOR_VIEWPORT_MARKERS = 9;
/**
* Zoom thresholds for visualization modes
*/
const ZOOM_DENSE_MAX = 5; // 1-5: density dots (40% more dense)
const ZOOM_DENSITY_MAX = 8; // 6-8: density dots (normal)
const ZOOM_CLUSTER_MAX = 15; // 9-15: numbered clusters
const ZOOM_CLUSTER_MAX = 15; // 9-15: numbered clusters (unless viewport threshold met)
// 16+: individual markers
/**
@@ -269,8 +284,15 @@ class MLS_Cluster {
}
if ($args['property_type']) {
$where[] = 'property_type = %s';
$values[] = $args['property_type'];
$types = array_filter(array_map('trim', explode(',', $args['property_type'])));
if (count($types) === 1) {
$where[] = 'property_type = %s';
$values[] = $types[0];
} elseif (count($types) > 1) {
$placeholders = implode(',', array_fill(0, count($types), '%s'));
$where[] = "property_type IN ({$placeholders})";
$values = array_merge($values, $types);
}
}
if ($args['city']) {
@@ -314,7 +336,7 @@ class MLS_Cluster {
$total = (int) $wpdb->get_var($count_sql);
}
// If few properties, always show individual markers (no grouping)
// If very few properties, always show individual markers (no grouping)
if ($total <= self::MIN_FOR_GROUPING) {
return $this->get_individual_markers($where_sql, $values, $total);
}
@@ -327,19 +349,34 @@ class MLS_Cluster {
$zoom = (int) $args['zoom'];
// Determine visualization mode based on zoom level
// Zoom 1-5: Density dots (40% more dense)
// Determine visualization mode based on zoom level AND viewport property count
//
// Priority order:
// 1. Very zoomed out (zoom 1-5): Always density dots (dense)
// 2. Zoomed out (zoom 6-8): Always density dots (normal)
// 3. Medium zoom (9-15) with FEW properties in viewport: Individual markers
// 4. Medium zoom (9-15) with MANY properties: Clusters
// 5. Very zoomed in (16+): Always individual markers
// Zoom 1-5: Density dots (40% more dense) - always, regardless of count
if ($zoom <= self::ZOOM_DENSE_MAX) {
return $this->get_density_data($where_sql, $values, $zoom, $center_lat, $total, self::DENSITY_DOT_SPACING_DENSE);
}
// Zoom 6-11: Density dots (normal spacing)
// Zoom 6-8: Density dots (normal spacing) - always, regardless of count
if ($zoom <= self::ZOOM_DENSITY_MAX) {
return $this->get_density_data($where_sql, $values, $zoom, $center_lat, $total, self::DENSITY_DOT_SPACING);
}
// Zoom 9-15: Always use server-side clusters (let server handle grouping)
// Zoom 9-15: Use viewport-aware threshold
// If viewport has relatively few properties, show individual markers
// This helps mobile viewports which show smaller geographic areas at same zoom level
if ($zoom <= self::ZOOM_CLUSTER_MAX) {
if ($total <= self::VIEWPORT_MARKER_THRESHOLD) {
// Few enough properties in viewport - show individual markers
return $this->get_individual_markers($where_sql, $values, $total);
}
// Many properties - use clusters
return $this->get_cluster_data($where_sql, $values, $zoom, $center_lat, $total);
}