6556479417
Features: - Full sync of NorthStar MLS properties via MLS Grid API v2 - Incremental sync using ModificationTimestamp - Local media download and storage - Rate limit compliance (2 req/sec, 7200/hr, 40000/day) - Sync state tracking with resume capability - WP-CLI commands: test, sync, status, stats, cache - Admin settings page with manual sync triggers - Public API functions: mls_get_properties, mls_get_property, etc. Database tables: - mls_properties: Listing data with full field mapping - mls_media: Downloaded images - mls_sync_state: Sync progress tracking - mls_rate_limits: API usage tracking - mls_sync_log: Debug logging Documentation: - docs/CLAUDE.md: AI development guide - docs/API.md: MLS Grid API reference - docs/USAGE.md: User documentation Tested: Connection, auth, sync 10 records, media download verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
387 lines
17 KiB
PHP
387 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* MLS Admin Settings Page
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class MLS_Admin {
|
|
|
|
/**
|
|
* Plugin instance
|
|
*/
|
|
private $plugin;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct($plugin) {
|
|
$this->plugin = $plugin;
|
|
|
|
add_action('admin_menu', array($this, 'add_menu'));
|
|
add_action('admin_init', array($this, 'register_settings'));
|
|
add_action('wp_ajax_mls_test_connection', array($this, 'ajax_test_connection'));
|
|
add_action('wp_ajax_mls_run_sync', array($this, 'ajax_run_sync'));
|
|
}
|
|
|
|
/**
|
|
* Add admin menu
|
|
*/
|
|
public function add_menu() {
|
|
add_options_page(
|
|
'MLS Settings',
|
|
'MLS Settings',
|
|
'manage_options',
|
|
'mls-settings',
|
|
array($this, 'render_settings_page')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register settings
|
|
*/
|
|
public function register_settings() {
|
|
register_setting('mls_settings', MLS_Options::OPTION_KEY, array(
|
|
'sanitize_callback' => array($this, 'sanitize_options'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Sanitize options
|
|
*/
|
|
public function sanitize_options($input) {
|
|
$sanitized = array();
|
|
|
|
if (isset($input['api_url'])) {
|
|
$sanitized['api_url'] = esc_url_raw($input['api_url']);
|
|
}
|
|
|
|
if (isset($input['api_token'])) {
|
|
$sanitized['api_token'] = sanitize_text_field($input['api_token']);
|
|
}
|
|
|
|
if (isset($input['originating_system'])) {
|
|
$sanitized['originating_system'] = sanitize_text_field($input['originating_system']);
|
|
}
|
|
|
|
$sanitized['auto_sync_enabled'] = !empty($input['auto_sync_enabled']);
|
|
$sanitized['sync_media'] = !empty($input['sync_media']);
|
|
|
|
if (isset($input['sync_interval'])) {
|
|
$allowed = array('every_30_minutes', 'hourly', 'every_2_hours', 'every_6_hours', 'every_12_hours', 'daily');
|
|
$sanitized['sync_interval'] = in_array($input['sync_interval'], $allowed)
|
|
? $input['sync_interval']
|
|
: 'hourly';
|
|
}
|
|
|
|
// Preserve timestamps
|
|
$existing = get_option(MLS_Options::OPTION_KEY, array());
|
|
if (isset($existing['last_full_sync'])) {
|
|
$sanitized['last_full_sync'] = $existing['last_full_sync'];
|
|
}
|
|
if (isset($existing['last_incremental_sync'])) {
|
|
$sanitized['last_incremental_sync'] = $existing['last_incremental_sync'];
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Render settings page
|
|
*/
|
|
public function render_settings_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
return;
|
|
}
|
|
|
|
$options = $this->plugin->get_options();
|
|
$db = $this->plugin->get_db();
|
|
$stats = $db->get_stats();
|
|
$sync_engine = $this->plugin->get_sync_engine();
|
|
$sync_status = $sync_engine->get_status();
|
|
$rate_limiter = $this->plugin->get_rate_limiter();
|
|
$rate_status = $rate_limiter->get_status();
|
|
|
|
// Check if using wp-config constants
|
|
$using_config_url = defined('MLSGRID_API_URL') && MLSGRID_API_URL;
|
|
$using_config_token = defined('MLSGRID_ACCESS_TOKEN') && MLSGRID_ACCESS_TOKEN;
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1>MLS Settings</h1>
|
|
|
|
<div class="mls-admin-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
|
|
|
<!-- Settings Form -->
|
|
<div class="mls-settings-form">
|
|
<form method="post" action="options.php">
|
|
<?php settings_fields('mls_settings'); ?>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th scope="row">API URL</th>
|
|
<td>
|
|
<?php if ($using_config_url): ?>
|
|
<input type="text" value="<?php echo esc_attr($options->get_api_url()); ?>" class="regular-text" disabled />
|
|
<p class="description">Set via MLSGRID_API_URL in wp-config.php</p>
|
|
<?php else: ?>
|
|
<input type="url" name="<?php echo MLS_Options::OPTION_KEY; ?>[api_url]"
|
|
value="<?php echo esc_attr($options->get('api_url')); ?>"
|
|
class="regular-text" />
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th scope="row">API Token</th>
|
|
<td>
|
|
<?php if ($using_config_token): ?>
|
|
<input type="password" value="••••••••••••" class="regular-text" disabled />
|
|
<p class="description">Set via MLSGRID_ACCESS_TOKEN in wp-config.php</p>
|
|
<?php else: ?>
|
|
<input type="password" name="<?php echo MLS_Options::OPTION_KEY; ?>[api_token]"
|
|
value="<?php echo esc_attr($options->get('api_token')); ?>"
|
|
class="regular-text" />
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th scope="row">Originating System</th>
|
|
<td>
|
|
<input type="text" name="<?php echo MLS_Options::OPTION_KEY; ?>[originating_system]"
|
|
value="<?php echo esc_attr($options->get('originating_system', 'northstar')); ?>"
|
|
class="regular-text" />
|
|
<p class="description">MLS system identifier (e.g., northstar)</p>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th scope="row">Auto Sync</th>
|
|
<td>
|
|
<label>
|
|
<input type="checkbox" name="<?php echo MLS_Options::OPTION_KEY; ?>[auto_sync_enabled]"
|
|
value="1" <?php checked($options->get('auto_sync_enabled')); ?> />
|
|
Enable automatic sync
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th scope="row">Sync Interval</th>
|
|
<td>
|
|
<select name="<?php echo MLS_Options::OPTION_KEY; ?>[sync_interval]">
|
|
<option value="every_30_minutes" <?php selected($options->get('sync_interval'), 'every_30_minutes'); ?>>Every 30 minutes</option>
|
|
<option value="hourly" <?php selected($options->get('sync_interval'), 'hourly'); ?>>Hourly</option>
|
|
<option value="every_2_hours" <?php selected($options->get('sync_interval'), 'every_2_hours'); ?>>Every 2 hours</option>
|
|
<option value="every_6_hours" <?php selected($options->get('sync_interval'), 'every_6_hours'); ?>>Every 6 hours</option>
|
|
<option value="every_12_hours" <?php selected($options->get('sync_interval'), 'every_12_hours'); ?>>Every 12 hours</option>
|
|
<option value="daily" <?php selected($options->get('sync_interval'), 'daily'); ?>>Daily</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th scope="row">Sync Media</th>
|
|
<td>
|
|
<label>
|
|
<input type="checkbox" name="<?php echo MLS_Options::OPTION_KEY; ?>[sync_media]"
|
|
value="1" <?php checked($options->get('sync_media', true)); ?> />
|
|
Download listing photos
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<?php submit_button(); ?>
|
|
</form>
|
|
|
|
<hr />
|
|
|
|
<h2>Manual Actions</h2>
|
|
<p>
|
|
<button type="button" class="button" id="mls-test-connection">Test Connection</button>
|
|
<span id="mls-test-result"></span>
|
|
</p>
|
|
<p>
|
|
<button type="button" class="button button-primary" id="mls-run-sync">Run Incremental Sync</button>
|
|
<button type="button" class="button" id="mls-run-full-sync">Run Full Sync</button>
|
|
</p>
|
|
<div id="mls-sync-progress" style="display:none; margin-top: 10px;">
|
|
<span class="spinner is-active" style="float: none;"></span>
|
|
<span id="mls-sync-status">Syncing...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Panel -->
|
|
<div class="mls-status-panel">
|
|
<div class="card" style="max-width: none; padding: 15px;">
|
|
<h2 style="margin-top: 0;">Database Statistics</h2>
|
|
<table class="widefat">
|
|
<tr>
|
|
<td>Total Properties</td>
|
|
<td><strong><?php echo number_format($stats['total_properties']); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Active</td>
|
|
<td><?php echo number_format($stats['active_properties']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Pending</td>
|
|
<td><?php echo number_format($stats['pending_properties']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Sold/Closed</td>
|
|
<td><?php echo number_format($stats['sold_properties']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Media Files</td>
|
|
<td><?php echo number_format($stats['downloaded_media']); ?> / <?php echo number_format($stats['total_media']); ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card" style="max-width: none; padding: 15px; margin-top: 15px;">
|
|
<h2 style="margin-top: 0;">Last Sync</h2>
|
|
<?php if ($sync_status['last_sync']): ?>
|
|
<p>
|
|
<strong>Type:</strong> <?php echo esc_html($sync_status['last_sync']->sync_type); ?><br />
|
|
<strong>Completed:</strong> <?php echo esc_html($sync_status['last_sync']->completed_at); ?><br />
|
|
<strong>Records:</strong> <?php echo number_format($sync_status['last_sync']->records_processed); ?> processed
|
|
</p>
|
|
<?php else: ?>
|
|
<p>No sync completed yet.</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="card" style="max-width: none; padding: 15px; margin-top: 15px;">
|
|
<h2 style="margin-top: 0;">Rate Limits</h2>
|
|
<table class="widefat">
|
|
<tr>
|
|
<td>Hourly</td>
|
|
<td><?php echo number_format($rate_status['hourly']['used']); ?> / <?php echo number_format($rate_status['hourly']['limit']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Daily</td>
|
|
<td><?php echo number_format($rate_status['daily']['used']); ?> / <?php echo number_format($rate_status['daily']['limit']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Data This Hour</td>
|
|
<td><?php echo size_format($rate_status['bytes_this_hour']); ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
jQuery(document).ready(function($) {
|
|
$('#mls-test-connection').on('click', function() {
|
|
var $btn = $(this);
|
|
var $result = $('#mls-test-result');
|
|
|
|
$btn.prop('disabled', true);
|
|
$result.text('Testing...');
|
|
|
|
$.post(ajaxurl, {
|
|
action: 'mls_test_connection',
|
|
_wpnonce: '<?php echo wp_create_nonce('mls_admin'); ?>'
|
|
}, function(response) {
|
|
$btn.prop('disabled', false);
|
|
if (response.success) {
|
|
$result.html('<span style="color:green;">Connected! (' + response.data.response_time + 'ms)</span>');
|
|
} else {
|
|
$result.html('<span style="color:red;">Failed: ' + response.data + '</span>');
|
|
}
|
|
});
|
|
});
|
|
|
|
$('#mls-run-sync, #mls-run-full-sync').on('click', function() {
|
|
var syncType = $(this).attr('id') === 'mls-run-full-sync' ? 'full' : 'incremental';
|
|
|
|
if (syncType === 'full' && !confirm('Run full sync? This may take a while.')) {
|
|
return;
|
|
}
|
|
|
|
$('#mls-sync-progress').show();
|
|
$('#mls-sync-status').text('Starting ' + syncType + ' sync...');
|
|
|
|
$.post(ajaxurl, {
|
|
action: 'mls_run_sync',
|
|
sync_type: syncType,
|
|
_wpnonce: '<?php echo wp_create_nonce('mls_admin'); ?>'
|
|
}, function(response) {
|
|
$('#mls-sync-progress').hide();
|
|
if (response.success) {
|
|
alert('Sync completed! ' + response.data.message);
|
|
location.reload();
|
|
} else {
|
|
alert('Sync failed: ' + response.data);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* AJAX: Test connection
|
|
*/
|
|
public function ajax_test_connection() {
|
|
check_ajax_referer('mls_admin', '_wpnonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error('Permission denied');
|
|
}
|
|
|
|
$api_client = $this->plugin->get_api_client();
|
|
$result = $api_client->test_connection();
|
|
|
|
if ($result['success']) {
|
|
wp_send_json_success($result);
|
|
} else {
|
|
wp_send_json_error($result['error']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX: Run sync
|
|
*/
|
|
public function ajax_run_sync() {
|
|
check_ajax_referer('mls_admin', '_wpnonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error('Permission denied');
|
|
}
|
|
|
|
$sync_type = isset($_POST['sync_type']) ? sanitize_text_field($_POST['sync_type']) : 'incremental';
|
|
$sync_engine = $this->plugin->get_sync_engine();
|
|
|
|
// Run sync with a reasonable limit for AJAX
|
|
if ($sync_type === 'full') {
|
|
$result = $sync_engine->run_full_sync(false, 500);
|
|
} else {
|
|
$result = $sync_engine->run_incremental_sync(false);
|
|
}
|
|
|
|
if ($result['success']) {
|
|
wp_send_json_success(array(
|
|
'message' => sprintf(
|
|
'%d processed, %d created, %d updated',
|
|
$result['stats']['processed'],
|
|
$result['stats']['created'],
|
|
$result['stats']['updated']
|
|
),
|
|
'stats' => $result['stats'],
|
|
));
|
|
} else {
|
|
wp_send_json_error($result['error']);
|
|
}
|
|
}
|
|
}
|