Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to the Rivian Tire Guide plugin will be documented in this f

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.24.2] - 2026-03-02

### Added
- **Link check progress bar** — The "Check Links Now" button now shows a live progress bar with status text ("Checking link 12 of 38...") and a running count of broken links found. Links are checked in batches of 5 via sequential AJAX calls, replacing the single long-running request. Page auto-reloads after 1.5 seconds with a summary message.

### Fixed
- **Network error alert on page leave** — Navigating away during a link check no longer shows a "Network error" alert. An `isUnloading` flag suppresses error callbacks from cancelled AJAX requests, and a `beforeunload` confirmation warns the user that a check is still running.

### Changed
- **Plugin version** — Bumped to 1.24.2.

## [1.24.0] - 2026-03-02

### Added
Expand Down
151 changes: 111 additions & 40 deletions admin/views/affiliate-links.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ function rtg_truncate_url( $url ) {
</div>
<button type="button" id="rtg-check-links-btn" class="rtg-btn rtg-btn-secondary">Check Links Now</button>
</div>
<div id="rtg-link-check-progress" style="display:none;padding:0 16px 16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">
<span id="rtg-link-check-status" style="font-size:13px;color:var(--rtg-text-muted);">Preparing...</span>
<span id="rtg-link-check-count" style="font-size:13px;color:var(--rtg-text-muted);margin-left:auto;"></span>
</div>
<div style="width:100%;height:8px;background:var(--rtg-border, #2a3548);border-radius:4px;overflow:hidden;">
<div id="rtg-link-check-bar" style="width:0%;height:100%;background:var(--rtg-accent, #fba919);border-radius:4px;transition:width 0.3s ease;"></div>
</div>
</div>
</div>

<!-- Filter Tabs + Search -->
Expand Down Expand Up @@ -403,59 +412,121 @@ function escHtml(str) {
return div.innerHTML;
}

// Check Links Now button.
$(document).on('click', '#rtg-check-links-btn', function() {
var $btn = $(this);
$btn.prop('disabled', true).text('Checking...');
// Track whether a link check is running and whether the page is unloading.
var linkCheckRunning = false;
var isUnloading = false;

$.post(ajaxUrl, {
action: 'rtg_check_links',
nonce: nonce
}, function(response) {
$btn.prop('disabled', false).text('Check Links Now');
$(window).on('beforeunload', function() {
if (linkCheckRunning) {
isUnloading = true;
return 'A link check is still running. Are you sure you want to leave?';
}
});

if (response.success) {
var data = response.data;
var brokenCount = (data.broken || []).length;

if (brokenCount > 0) {
// Mark broken rows.
var brokenIds = {};
for (var i = 0; i < data.broken.length; i++) {
brokenIds[data.broken[i].tire_id] = data.broken[i].reason;
}
// Check Links Now button — batched with progress bar.
$(document).on('click', '#rtg-check-links-btn', function() {
var $btn = $(this);
var $progress = $('#rtg-link-check-progress');
var $bar = $('#rtg-link-check-bar');
var $status = $('#rtg-link-check-status');
var $count = $('#rtg-link-check-count');

$('tr[data-tire-id]').each(function() {
var $row = $(this);
var tireId = $row.data('tire-id');
var $statusCell = $row.find('td:eq(1)');
linkCheckRunning = true;
$btn.prop('disabled', true).text('Checking...');
$bar.css('width', '0%');
$status.text('Fetching link list...');
$count.text('');
$progress.slideDown(200);

// Step 1: Get the list of tires to check.
$.post(ajaxUrl, { action: 'rtg_check_links_start', nonce: nonce }, function(startResp) {
if (!startResp.success) {
linkCheckRunning = false;
$btn.prop('disabled', false).text('Check Links Now');
$progress.slideUp(200);
alert('Error: ' + (startResp.data || 'Could not fetch link list.'));
return;
}

// Remove any existing broken badge.
$statusCell.find('.rtg-broken-badge').remove();
var allTires = startResp.data.tires;
var total = startResp.data.total;
var batchSize = startResp.data.batch_size;
var checked = 0;
var allBroken = [];

if (total === 0) {
linkCheckRunning = false;
$btn.prop('disabled', false).text('Check Links Now');
$progress.slideUp(200);
alert('No tires with purchase links to check.');
return;
}

if (brokenIds[tireId]) {
$row.attr('data-broken', '1');
$statusCell.append('<span class="rtg-badge rtg-badge-error rtg-broken-badge" title="' + escHtml(brokenIds[tireId]) + '" style="margin-top:4px;display:inline-block;background:#ef4444;color:#fff;font-size:11px;">Broken</span>');
// Step 2: Process batches sequentially.
function nextBatch(offset) {
if (isUnloading) { return; }

if (offset >= total) {
// Step 3: Finalize — save results and send notification.
$status.text('Saving results...');
$bar.css('width', '100%');

$.post(ajaxUrl, {
action: 'rtg_check_links_finish',
nonce: nonce,
total: checked,
broken: allBroken
}, function() {
linkCheckRunning = false;
$btn.prop('disabled', false).text('Check Links Now');
var brokenCount = allBroken.length;
if (brokenCount > 0) {
$status.html('<span style="color:var(--rtg-error);font-weight:600;">Done — ' + brokenCount + ' broken link' + (brokenCount !== 1 ? 's' : '') + ' found</span>');
} else {
$row.removeAttr('data-broken');
$status.html('<span style="color:var(--rtg-success);font-weight:600;">Done — all ' + checked + ' links healthy!</span>');
}
setTimeout(function() { location.reload(); }, 1500);
}).fail(function() {
if (isUnloading) { return; }
linkCheckRunning = false;
$btn.prop('disabled', false).text('Check Links Now');
$status.text('Error saving results.');
});

alert('Check complete: ' + brokenCount + ' broken link' + (brokenCount !== 1 ? 's' : '') + ' found. An email notification has been sent.');
} else {
// Remove all broken badges.
$('tr[data-tire-id]').removeAttr('data-broken');
$('.rtg-broken-badge').remove();
alert('Check complete: All ' + data.total + ' links are healthy!');
return;
}

// Reload page to update counts.
location.reload();
} else {
alert('Error: ' + (response.data || 'Link check failed.'));
var batch = allTires.slice(offset, offset + batchSize);
var pct = Math.round((offset / total) * 100);
$bar.css('width', pct + '%');
$status.text('Checking link ' + (offset + 1) + ' of ' + total + '...');
$count.text(allBroken.length + ' broken so far');

$.post(ajaxUrl, {
action: 'rtg_check_links_batch',
nonce: nonce,
tires: batch
}, function(batchResp) {
if (batchResp.success) {
checked += batchResp.data.checked;
if (batchResp.data.broken.length) {
allBroken = allBroken.concat(batchResp.data.broken);
$count.text(allBroken.length + ' broken so far');
}
}
nextBatch(offset + batchSize);
}).fail(function() {
if (isUnloading) { return; }
// Skip failed batch and continue.
nextBatch(offset + batchSize);
});
}

nextBatch(0);
}).fail(function() {
if (isUnloading) { return; }
linkCheckRunning = false;
$btn.prop('disabled', false).text('Check Links Now');
$progress.slideUp(200);
alert('Network error. Please try again.');
});
});
Expand Down
95 changes: 95 additions & 0 deletions includes/class-rtg-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public function __construct() {

// Link health check — admin only.
add_action( 'wp_ajax_rtg_check_links', array( $this, 'check_links' ) );
add_action( 'wp_ajax_rtg_check_links_start', array( $this, 'check_links_start' ) );
add_action( 'wp_ajax_rtg_check_links_batch', array( $this, 'check_links_batch' ) );
add_action( 'wp_ajax_rtg_check_links_finish', array( $this, 'check_links_finish' ) );

// AI tire recommendations — public.
add_action( 'wp_ajax_rtg_ai_recommend', array( $this, 'ai_recommend' ) );
Expand Down Expand Up @@ -824,6 +827,98 @@ public function check_links() {
wp_send_json_success( $results );
}

/**
* Start a batched link check — returns the list of tires to check.
*/
public function check_links_start() {
if ( ! check_ajax_referer( 'rtg_affiliate_links_nonce', 'nonce', false ) ) {
wp_send_json_error( 'Security check failed.' );
}

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized.' );
}

$tires = RTG_Link_Checker::get_linkable_tires();

wp_send_json_success( array(
'tires' => $tires,
'total' => count( $tires ),
'batch_size' => RTG_Link_Checker::PROGRESS_BATCH_SIZE,
) );
}

/**
* Check a single batch of links by tire IDs.
*/
public function check_links_batch() {
if ( ! check_ajax_referer( 'rtg_affiliate_links_nonce', 'nonce', false ) ) {
wp_send_json_error( 'Security check failed.' );
}

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized.' );
}

$tires = isset( $_POST['tires'] ) ? $_POST['tires'] : array();
if ( ! is_array( $tires ) || empty( $tires ) ) {
wp_send_json_error( 'No tires provided.' );
}

// Sanitize the batch.
$batch = array();
foreach ( $tires as $tire ) {
$batch[] = array(
'tire_id' => sanitize_text_field( $tire['tire_id'] ?? '' ),
'brand' => sanitize_text_field( $tire['brand'] ?? '' ),
'model' => sanitize_text_field( $tire['model'] ?? '' ),
'link' => esc_url_raw( $tire['link'] ?? '' ),
);
}

set_time_limit( 120 );

$result = RTG_Link_Checker::check_batch( $batch );

wp_send_json_success( $result );
}

/**
* Finalize a batched link check — save results and send notification.
*/
public function check_links_finish() {
if ( ! check_ajax_referer( 'rtg_affiliate_links_nonce', 'nonce', false ) ) {
wp_send_json_error( 'Security check failed.' );
}

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized.' );
}

$total = isset( $_POST['total'] ) ? absint( $_POST['total'] ) : 0;
$broken = isset( $_POST['broken'] ) ? $_POST['broken'] : array();

// Sanitize broken entries.
$clean_broken = array();
if ( is_array( $broken ) ) {
foreach ( $broken as $entry ) {
$clean_broken[] = array(
'tire_id' => sanitize_text_field( $entry['tire_id'] ?? '' ),
'brand' => sanitize_text_field( $entry['brand'] ?? '' ),
'model' => sanitize_text_field( $entry['model'] ?? '' ),
'url' => esc_url_raw( $entry['url'] ?? '' ),
'status' => sanitize_text_field( $entry['status'] ?? '' ),
'reason' => sanitize_text_field( $entry['reason'] ?? '' ),
'http' => absint( $entry['http'] ?? 0 ),
);
}
}

$results = RTG_Link_Checker::save_results( $total, $clean_broken );

wp_send_json_success( $results );
}

/**
* Get analytics data for the admin dashboard.
* Requires manage_options capability.
Expand Down
Loading
Loading