Add verbose mode, progress indicators, and missing media log

- Add --verbose flag to sync commands for detailed API request/response output
- Add progress indicators (.=#xPpE|) for compact sync output
- Implement exponential backoff (1s, 2s, 4s, 8s, 16s) for media downloads
- Log failed media downloads to wp-content/uploads/mls-missing-media.log
- Add 'wp mls cache missing' command to view/clear the log
- Retry on rate limit (429) and server errors (5xx)
- Update documentation with new features

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-14 22:20:41 -06:00
parent 6556479417
commit 5e4ebfb99e
5 changed files with 626 additions and 48 deletions
@@ -228,11 +228,18 @@ class MLS_CLI {
* [--force]
* : Force re-download of media (for media command)
*
* [--quiet]
* : Suppress progress output
*
* [--verbose]
* : Show detailed output including API requests and responses
*
* ## EXAMPLES
*
* wp mls sync full
* wp mls sync full --dry-run --limit=10
* wp mls sync incremental
* wp mls sync incremental --verbose
* wp mls sync media --limit=100
* wp mls sync resume --id=5
*
@@ -242,16 +249,58 @@ class MLS_CLI {
$type = isset($args[0]) ? $args[0] : 'incremental';
$dry_run = isset($assoc_args['dry-run']);
$limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : null;
$quiet = isset($assoc_args['quiet']);
$verbose = isset($assoc_args['verbose']);
$sync_engine = $this->plugin->get_sync_engine();
// Progress callback for CLI
$progress = null;
$progress_callback = function($stats) use (&$progress) {
if ($progress) {
$progress->tick();
}
};
$progress_callback = null;
if (!$quiet) {
$progress_callback = function($event, $data = array()) use ($verbose) {
if ($verbose) {
// Verbose mode: full line output
$this->output_verbose_event($event, $data);
} else {
// Compact mode: single character symbols
switch ($event) {
case 'property_created':
echo '.';
break;
case 'property_updated':
echo '#';
break;
case 'property_deleted':
echo 'x';
break;
case 'property_skipped':
echo '-';
break;
case 'property_error':
echo '!';
break;
case 'media_downloaded':
echo 'P';
break;
case 'media_skipped':
echo 'p';
break;
case 'media_error':
echo 'E';
break;
case 'page_complete':
echo '|';
break;
case 'api_request':
// Silent in compact mode
break;
case 'api_response':
// Silent in compact mode
break;
}
}
};
}
switch ($type) {
case 'full':
@@ -259,8 +308,14 @@ class MLS_CLI {
if ($dry_run) {
WP_CLI::line('DRY RUN - No changes will be made');
}
if (!$quiet) {
$this->print_progress_legend($verbose);
}
$result = $sync_engine->run_full_sync($dry_run, $limit, $progress_callback);
if (!$quiet) {
echo "\n";
}
$this->output_sync_result($result);
break;
@@ -269,17 +324,30 @@ class MLS_CLI {
if ($dry_run) {
WP_CLI::line('DRY RUN - No changes will be made');
}
if (!$quiet) {
$this->print_progress_legend($verbose);
}
$result = $sync_engine->run_incremental_sync($dry_run, $progress_callback);
if (!$quiet) {
echo "\n";
}
$this->output_sync_result($result);
break;
case 'media':
WP_CLI::line('Downloading pending media...');
if (!$quiet) {
WP_CLI::line('Legend: P=downloaded p=skipped E=error');
echo "\n";
}
$media_handler = $this->plugin->get_media_handler();
$result = $media_handler->download_pending($limit ?: 100);
$result = $media_handler->download_pending($limit ?: 100, $progress_callback);
if (!$quiet) {
echo "\n";
}
WP_CLI::line(sprintf(
'Media download complete: %d success, %d failed out of %d total',
$result['success'],
@@ -293,6 +361,14 @@ class MLS_CLI {
WP_CLI::success('No pending media to download.');
} else {
WP_CLI::warning('Some media failed to download.');
$missing_count = $media_handler->get_missing_count();
if ($missing_count > 0) {
WP_CLI::line(sprintf(
'Missing media log: %s (%d entries)',
$media_handler->get_missing_log_path(),
$missing_count
));
}
}
break;
@@ -303,8 +379,14 @@ class MLS_CLI {
}
WP_CLI::line("Resuming sync #{$sync_id}...");
if (!$quiet) {
$this->print_progress_legend($verbose);
}
$result = $sync_engine->resume_sync($sync_id, $progress_callback);
if (!$quiet) {
echo "\n";
}
$this->output_sync_result($result);
break;
@@ -313,6 +395,95 @@ class MLS_CLI {
}
}
/**
* Print progress legend
*
* @param bool $verbose Whether verbose mode is enabled
*/
private function print_progress_legend($verbose = false) {
if (!$verbose) {
WP_CLI::line('Legend: .=new #=updated x=deleted -=skipped !=error P=photo p=photo-skip E=photo-error |=page');
echo "\n";
}
}
/**
* Output verbose event information
*
* @param string $event Event name
* @param array $data Event data
*/
private function output_verbose_event($event, $data) {
$timestamp = date('H:i:s');
switch ($event) {
case 'api_request':
WP_CLI::line("[{$timestamp}] API REQUEST: {$data['method']} {$data['url']}");
if (!empty($data['params'])) {
WP_CLI::line(" Params: " . json_encode($data['params']));
}
break;
case 'api_response':
$status = $data['success'] ? 'OK' : 'ERROR';
WP_CLI::line("[{$timestamp}] API RESPONSE: {$status} ({$data['status_code']}) - {$data['record_count']} records, {$data['response_time']}ms");
if (!$data['success'] && !empty($data['error'])) {
WP_CLI::warning(" Error: {$data['error']}");
}
break;
case 'property_created':
$key = $data['listing_key'] ?? 'unknown';
$address = $data['address'] ?? '';
WP_CLI::line("[{$timestamp}] CREATED: {$key} {$address}");
break;
case 'property_updated':
$key = $data['listing_key'] ?? 'unknown';
$address = $data['address'] ?? '';
WP_CLI::line("[{$timestamp}] UPDATED: {$key} {$address}");
break;
case 'property_deleted':
$key = $data['listing_key'] ?? 'unknown';
WP_CLI::line("[{$timestamp}] DELETED: {$key}");
break;
case 'property_skipped':
$key = $data['listing_key'] ?? 'unknown';
WP_CLI::line("[{$timestamp}] SKIPPED: {$key} (dry-run)");
break;
case 'property_error':
$key = $data['listing_key'] ?? 'unknown';
$error = $data['error'] ?? 'Unknown error';
WP_CLI::warning("[{$timestamp}] ERROR: {$key} - {$error}");
break;
case 'media_downloaded':
$key = $data['media_key'] ?? 'unknown';
$listing = $data['listing_key'] ?? '';
WP_CLI::line("[{$timestamp}] MEDIA OK: {$key}" . ($listing ? " ({$listing})" : ""));
break;
case 'media_skipped':
$key = $data['media_key'] ?? 'unknown';
WP_CLI::line("[{$timestamp}] MEDIA SKIP: {$key} (already downloaded)");
break;
case 'media_error':
$key = $data['media_key'] ?? 'unknown';
$error = $data['error'] ?? 'Download failed';
WP_CLI::warning("[{$timestamp}] MEDIA ERROR: {$key} - {$error}");
break;
case 'page_complete':
$processed = $data['processed'] ?? 0;
WP_CLI::line("[{$timestamp}] PAGE COMPLETE: {$processed} records processed so far");
break;
}
}
/**
* Output sync result
*/
@@ -332,6 +503,17 @@ class MLS_CLI {
$stats['deleted'],
$stats['errors']
));
// Show missing media log info if there are failures
$media_handler = $this->plugin->get_media_handler();
$missing_count = $media_handler->get_missing_count();
if ($missing_count > 0) {
WP_CLI::line(sprintf(
'Missing media log: %s (%d entries)',
$media_handler->get_missing_log_path(),
$missing_count
));
}
}
/**
@@ -380,7 +562,7 @@ class MLS_CLI {
* ## OPTIONS
*
* <action>
* : Action: clear, clear-listing, cleanup
* : Action: clear, clear-listing, cleanup, missing
*
* [--confirm]
* : Confirm destructive operations
@@ -388,11 +570,20 @@ class MLS_CLI {
* [--listing=<key>]
* : Listing key for clear-listing
*
* [--clear]
* : Clear the missing media log (for missing action)
*
* [--limit=<n>]
* : Limit output lines (for missing action)
*
* ## EXAMPLES
*
* wp mls cache clear --confirm
* wp mls cache clear-listing --listing=NST123456
* wp mls cache cleanup
* wp mls cache missing
* wp mls cache missing --limit=20
* wp mls cache missing --clear
*
* @subcommand cache
*/
@@ -421,6 +612,9 @@ class MLS_CLI {
wp_mkdir_p($upload_dir);
}
// Also clear missing log
$media_handler->clear_missing_log();
WP_CLI::success('Cache cleared successfully.');
break;
@@ -449,8 +643,44 @@ class MLS_CLI {
WP_CLI::success("Cleaned up {$deleted} orphaned directories.");
break;
case 'missing':
$media_handler = $this->plugin->get_media_handler();
$log_file = $media_handler->get_missing_log_path();
if (isset($assoc_args['clear'])) {
$media_handler->clear_missing_log();
WP_CLI::success('Missing media log cleared.');
break;
}
if (!file_exists($log_file)) {
WP_CLI::success('No missing media logged.');
break;
}
$limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : null;
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$total = count($lines);
WP_CLI::line(sprintf('Missing media log: %s', $log_file));
WP_CLI::line(sprintf('Total entries: %d', $total));
WP_CLI::line('');
WP_CLI::line('Format: [timestamp] listing_key | media_key | error | url');
WP_CLI::line(str_repeat('-', 80));
$display = $limit ? array_slice($lines, 0, $limit) : $lines;
foreach ($display as $line) {
WP_CLI::line($line);
}
if ($limit && $total > $limit) {
WP_CLI::line('');
WP_CLI::line(sprintf('... and %d more entries', $total - $limit));
}
break;
default:
WP_CLI::error("Unknown action: {$action}. Use 'clear', 'clear-listing', or 'cleanup'.");
WP_CLI::error("Unknown action: {$action}. Use 'clear', 'clear-listing', 'cleanup', or 'missing'.");
}
}