d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
296 lines
9.6 KiB
JavaScript
296 lines
9.6 KiB
JavaScript
/**
|
|
* WPDO Entity Bridge — Admin UI controller
|
|
*
|
|
* Features:
|
|
* - Polls /wp-json/wpdo/v1/entity-bridge/health every 5 s during active backfill
|
|
* - Updates coverage progress bars and migration badges in real time
|
|
* - Handles Backfill / Promote / Demote button clicks with confirmation
|
|
*
|
|
* Depends on: wpdoEntityBridge (wp_localize_script data)
|
|
*/
|
|
|
|
/* global wpdoEntityBridge */
|
|
( function () {
|
|
'use strict';
|
|
|
|
var cfg = window.wpdoEntityBridge || {};
|
|
var restUrl = cfg.restUrl || '';
|
|
var nonce = cfg.nonce || '';
|
|
var pollInterval = 5000; // ms between polls
|
|
var timer = null;
|
|
var activeBackfills = {}; // {entity_type: {group_name: true}} — track in-progress
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
function apiFetch( method, path, body ) {
|
|
var url = restUrl + path;
|
|
var opts = {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-WP-Nonce': nonce,
|
|
},
|
|
};
|
|
if ( body ) {
|
|
opts.body = JSON.stringify( body );
|
|
}
|
|
return fetch( url, opts ).then( function ( res ) {
|
|
if ( ! res.ok ) {
|
|
return res.json().then( function ( e ) { throw new Error( e.message || e.error || res.status ); } );
|
|
}
|
|
return res.json();
|
|
} );
|
|
}
|
|
|
|
function modeBadgeClass( mode ) {
|
|
return {
|
|
disabled: 'wpdo-mode-disabled',
|
|
dual_write: 'wpdo-mode-dual-write',
|
|
shadow_read: 'wpdo-mode-shadow-read',
|
|
aeav_only: 'wpdo-mode-aeav-only',
|
|
}[ mode ] || 'wpdo-mode-disabled';
|
|
}
|
|
|
|
function modeLabel( mode ) {
|
|
return {
|
|
disabled: 'disabled',
|
|
dual_write: 'dual_write',
|
|
shadow_read: 'shadow_read',
|
|
aeav_only: 'aeav_only',
|
|
}[ mode ] || mode;
|
|
}
|
|
|
|
function pct( n ) {
|
|
return Math.min( 100, Math.max( 0, parseFloat( n ) || 0 ) ).toFixed( 1 );
|
|
}
|
|
|
|
// ─── Update UI from health data ──────────────────────────────────────────
|
|
|
|
function updateAll( data ) {
|
|
var hasActive = false;
|
|
|
|
Object.keys( data ).forEach( function ( type ) {
|
|
var card = document.querySelector( '[data-entity-type="' + type + '"]' );
|
|
if ( ! card ) return;
|
|
|
|
var info = data[ type ];
|
|
|
|
// Mode badge
|
|
var badge = card.querySelector( '.wpdo-mode-badge' );
|
|
if ( badge ) {
|
|
badge.textContent = modeLabel( info.mode );
|
|
badge.className = 'wpdo-mode-badge ' + modeBadgeClass( info.mode );
|
|
}
|
|
|
|
// Pipeline dots
|
|
var pipeline = card.querySelector( '.wpdo-pipeline' );
|
|
if ( pipeline ) {
|
|
pipeline.innerHTML = buildPipeline( info.mode );
|
|
}
|
|
|
|
// Mode days
|
|
var daysEl = card.querySelector( '.wpdo-mode-days' );
|
|
if ( daysEl ) {
|
|
daysEl.textContent = info.mode_days + ' 天';
|
|
}
|
|
|
|
// Groups
|
|
( info.groups || [] ).forEach( function ( g ) {
|
|
var groupEl = card.querySelector( '[data-group="' + g.name + '"]' );
|
|
if ( ! groupEl ) return;
|
|
|
|
// Progress bar
|
|
var bar = groupEl.querySelector( '.wpdo-cov-bar-fill' );
|
|
if ( bar ) bar.style.width = pct( g.coverage_pct ) + '%';
|
|
|
|
var pctEl = groupEl.querySelector( '.wpdo-cov-pct' );
|
|
if ( pctEl ) pctEl.textContent = pct( g.coverage_pct ) + '%';
|
|
|
|
var rowsEl = groupEl.querySelector( '.wpdo-cov-rows' );
|
|
if ( rowsEl ) rowsEl.textContent = g.flat_rows + ' / ' + g.eav_rows;
|
|
|
|
// Migration badge
|
|
var migBadge = groupEl.querySelector( '.wpdo-mig-status' );
|
|
if ( migBadge ) {
|
|
migBadge.textContent = g.migration_status;
|
|
migBadge.className = 'wpdo-mig-status wpdo-mig-' + g.migration_status.replace( /_/g, '-' );
|
|
}
|
|
|
|
// Is this group's backfill active?
|
|
if ( g.migration_status === 'running' ) {
|
|
hasActive = true;
|
|
}
|
|
} );
|
|
|
|
// Shadow diffs
|
|
var diffsEl = card.querySelector( '.wpdo-shadow-diffs' );
|
|
if ( diffsEl ) diffsEl.textContent = info.shadow_diffs;
|
|
|
|
// Recommendation
|
|
var recEl = card.querySelector( '.wpdo-recommendation' );
|
|
if ( recEl ) recEl.textContent = info.recommendation;
|
|
|
|
// Auto-promote eligible badge
|
|
var apEl = card.querySelector( '.wpdo-auto-promote-eligible' );
|
|
if ( apEl ) {
|
|
if ( info.auto_promote && info.auto_promote.eligible ) {
|
|
apEl.textContent = '✓ 可升級';
|
|
apEl.style.color = '#155724';
|
|
} else {
|
|
apEl.textContent = '— ' + ( ( info.auto_promote || {} ).reason || '' );
|
|
apEl.style.color = '#856404';
|
|
}
|
|
}
|
|
|
|
// Button states — update promote/demote targets
|
|
var promoteBtn = card.querySelector( '.wpdo-btn-promote' );
|
|
if ( promoteBtn ) {
|
|
promoteBtn.disabled = ! info.next_mode;
|
|
promoteBtn.dataset.nextMode = info.next_mode || '';
|
|
promoteBtn.title = info.next_mode ? '升級到 ' + info.next_mode : '已在最高模式';
|
|
}
|
|
|
|
var demoteBtn = card.querySelector( '.wpdo-btn-demote' );
|
|
if ( demoteBtn ) {
|
|
demoteBtn.disabled = ! info.prev_mode;
|
|
demoteBtn.dataset.prevMode = info.prev_mode || '';
|
|
demoteBtn.title = info.prev_mode ? '降級到 ' + info.prev_mode : '已在最低模式';
|
|
}
|
|
|
|
// Backfill active indicator
|
|
if ( info.backfill_active ) hasActive = true;
|
|
} );
|
|
|
|
// Manage poll timer
|
|
if ( hasActive ) {
|
|
startPolling();
|
|
}
|
|
}
|
|
|
|
function buildPipeline( currentMode ) {
|
|
var modes = [ 'disabled', 'dual_write', 'shadow_read', 'aeav_only' ];
|
|
var labels = { disabled: 'disabled', dual_write: 'dual_write', shadow_read: 'shadow_read', aeav_only: 'aeav_only' };
|
|
return modes.map( function ( m ) {
|
|
var active = m === currentMode ? ' wpdo-pipeline-active' : '';
|
|
return '<span class="wpdo-pipeline-dot' + active + '" title="' + labels[ m ] + '">' +
|
|
'<span class="wpdo-dot"></span>' +
|
|
'<span class="wpdo-dot-label">' + labels[ m ] + '</span>' +
|
|
'</span>';
|
|
} ).join( '<span class="wpdo-pipeline-arrow">→</span>' );
|
|
}
|
|
|
|
// ─── Polling ─────────────────────────────────────────────────────────────
|
|
|
|
function poll() {
|
|
apiFetch( 'GET', '/entity-bridge/health' )
|
|
.then( updateAll )
|
|
.catch( function ( e ) { console.warn( '[WPDO] Health poll error:', e ); } );
|
|
}
|
|
|
|
function startPolling() {
|
|
if ( timer ) return;
|
|
timer = setInterval( poll, pollInterval );
|
|
}
|
|
|
|
function stopPolling() {
|
|
if ( timer ) { clearInterval( timer ); timer = null; }
|
|
}
|
|
|
|
// ─── Button handlers ─────────────────────────────────────────────────────
|
|
|
|
function handleBackfill( btn ) {
|
|
var entityType = btn.dataset.entityType;
|
|
var groupName = btn.dataset.groupName;
|
|
if ( ! confirm( '確定要啟動 ' + entityType + '/' + groupName + ' 的 Backfill 遷移嗎?這將清除現有進度並重新開始。' ) ) return;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '排程中…';
|
|
|
|
apiFetch( 'POST', '/entity-bridge/backfill', { entity_type: entityType, group_name: groupName } )
|
|
.then( function () {
|
|
btn.textContent = '已排程 ✓';
|
|
startPolling();
|
|
// Refresh once immediately
|
|
setTimeout( poll, 1000 );
|
|
} )
|
|
.catch( function ( e ) {
|
|
alert( '啟動 Backfill 失敗:' + e.message );
|
|
btn.disabled = false;
|
|
btn.textContent = '啟動 Backfill';
|
|
} );
|
|
}
|
|
|
|
function handlePromote( btn ) {
|
|
var entityType = btn.dataset.entityType;
|
|
var nextMode = btn.dataset.nextMode;
|
|
if ( ! nextMode ) return;
|
|
|
|
if ( ! confirm( '確定要將 ' + entityType + ' 升級到 ' + nextMode + ' 嗎?' ) ) return;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '更新中…';
|
|
|
|
apiFetch( 'POST', '/entity-bridge/promote', { entity_type: entityType } )
|
|
.then( function () {
|
|
poll(); // immediate refresh
|
|
} )
|
|
.catch( function ( e ) {
|
|
alert( '升級失敗:' + e.message );
|
|
btn.disabled = false;
|
|
btn.textContent = '升級模式';
|
|
} );
|
|
}
|
|
|
|
function handleDemote( btn ) {
|
|
var entityType = btn.dataset.entityType;
|
|
var prevMode = btn.dataset.prevMode;
|
|
if ( ! prevMode ) return;
|
|
|
|
if ( ! confirm( '確定要將 ' + entityType + ' 降級到 ' + prevMode + ' 嗎?降級後讀取會切回 EAV。' ) ) return;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '更新中…';
|
|
|
|
apiFetch( 'POST', '/entity-bridge/demote', { entity_type: entityType } )
|
|
.then( function () {
|
|
poll();
|
|
} )
|
|
.catch( function ( e ) {
|
|
alert( '降級失敗:' + e.message );
|
|
btn.disabled = false;
|
|
btn.textContent = '降級模式';
|
|
} );
|
|
}
|
|
|
|
// ─── Event delegation ────────────────────────────────────────────────────
|
|
|
|
document.addEventListener( 'click', function ( e ) {
|
|
var btn = e.target.closest( 'button[data-wpdo-action]' );
|
|
if ( ! btn ) return;
|
|
|
|
var action = btn.dataset.wpdoAction;
|
|
|
|
if ( action === 'backfill' ) {
|
|
handleBackfill( btn );
|
|
} else if ( action === 'promote' ) {
|
|
handlePromote( btn );
|
|
} else if ( action === 'demote' ) {
|
|
handleDemote( btn );
|
|
}
|
|
} );
|
|
|
|
// ─── Init ────────────────────────────────────────────────────────────────
|
|
|
|
// Initial poll on page load if we're on the entity-bridge tab.
|
|
if ( document.querySelector( '.wpdo-entity-bridge-tab' ) ) {
|
|
poll();
|
|
|
|
// Auto-start polling if any backfill is already running (check server state).
|
|
// A subsequent poll() response will call startPolling() if needed.
|
|
}
|
|
|
|
// Stop polling when navigating away.
|
|
window.addEventListener( 'beforeunload', stopPolling );
|
|
|
|
} )();
|