chore: initial snapshot of 2meet-data-optimizer v0.1.0

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
This commit is contained in:
2026-07-31 05:06:36 +08:00
commit d36bb954d1
206 changed files with 66538 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
/**
* 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 );
} )();