implement wp testing

This commit is contained in:
southseact-3d
2026-02-08 19:27:26 +00:00
parent 541b6bc946
commit 39136e863f
16 changed files with 954 additions and 8 deletions

View File

@@ -33,6 +33,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -33,6 +33,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -136,8 +136,9 @@
<a class="ghost" href="/admin/accounts">Accounts</a>
<a class="ghost" href="/admin/affiliates">Affiliates</a>
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost active" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin Panel - External Testing</title>
<link rel="stylesheet" href="/styles.css" />
<!-- PostHog Analytics -->
<script src="/posthog.js"></script>
</head>
<body data-page="external-testing">
<div class="sidebar-overlay"></div>
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">A</div>
<div>
<div class="brand-title">Admin</div>
<div class="brand-sub">Site management</div>
</div>
<button id="close-sidebar" class="ghost" style="margin-left: auto; display: none;">&times;</button>
</div>
<div class="sidebar-section">
<div class="section-heading">Navigation</div>
<a class="ghost" href="/admin/build">Build models</a>
<a class="ghost" href="/admin/plan">Plan models</a>
<a class="ghost" href="/admin/plans">Plans</a>
<a class="ghost" href="/admin/accounts">Accounts</a>
<a class="ghost" href="/admin/affiliates">Affiliates</a>
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>
</aside>
<main class="main">
<div class="admin-shell">
<div class="topbar" style="margin-bottom: 12px;">
<button id="menu-toggle">
<span></span><span></span><span></span>
</button>
<div>
<div class="pill">Admin</div>
<div class="title" style="margin-top: 6px;">External WP Testing</div>
<div class="crumb">Run a CLI-only self check without AI calls.</div>
</div>
<div class="admin-actions">
<button id="admin-refresh" class="ghost">Refresh</button>
<button id="admin-logout" class="primary">Logout</button>
</div>
</div>
<div class="admin-card">
<header>
<h3>External testing self-check</h3>
<div class="pill">WP-CLI</div>
</header>
<p class="muted" style="margin-top:0;">Uploads a temporary plugin, activates it, verifies the activation flag, and cleans up. No AI API calls are made.</p>
<div class="admin-actions">
<button id="external-testing-run" class="primary">Run self-test</button>
<div class="status-line" id="external-testing-status"></div>
</div>
<div id="external-testing-output" class="admin-list" style="margin-top: 12px;"></div>
</div>
<div class="admin-card" style="margin-top: 16px;">
<header>
<h3>Current external testing configuration</h3>
<div class="pill">Config</div>
</header>
<div id="external-testing-config" class="admin-list"></div>
</div>
</div>
</main>
</div>
<script src="/admin.js"></script>
</body>
</html>

View File

@@ -31,6 +31,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -31,6 +31,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -224,7 +224,8 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost active" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>
</aside>

View File

@@ -184,6 +184,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -33,6 +33,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -43,6 +43,7 @@
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/login">Login</a>
</div>

View File

@@ -91,6 +91,10 @@
opencodeBackupForm: document.getElementById('opencode-backup-form'),
opencodeBackup: document.getElementById('opencode-backup'),
opencodeBackupStatus: document.getElementById('opencode-backup-status'),
externalTestingRun: document.getElementById('external-testing-run'),
externalTestingStatus: document.getElementById('external-testing-status'),
externalTestingOutput: document.getElementById('external-testing-output'),
externalTestingConfig: document.getElementById('external-testing-config'),
};
console.log('Element check - opencodeBackupForm:', el.opencodeBackupForm);
console.log('Element check - opencodeBackup:', el.opencodeBackup);
@@ -178,6 +182,128 @@
el.opencodeBackupStatus.style.color = isError ? 'var(--danger)' : 'inherit';
}
function setExternalTestingStatus(msg, isError = false) {
if (!el.externalTestingStatus) return;
el.externalTestingStatus.textContent = msg || '';
el.externalTestingStatus.style.color = isError ? 'var(--danger)' : 'inherit';
}
function renderExternalTestingConfig(config) {
if (!el.externalTestingConfig) return;
el.externalTestingConfig.innerHTML = '';
if (!config) return;
const rows = [
['WP host', config.wpHost || '—'],
['WP path', config.wpPath || '—'],
['Base URL', config.wpBaseUrl || '—'],
['Multisite enabled', config.enableMultisite ? 'Yes' : 'No'],
['Subsite mode', config.subsiteMode || '—'],
['Subsite domain', config.subsiteDomain || '—'],
['Max concurrent tests', String(config.maxConcurrentTests ?? '—')],
['Auto cleanup', config.autoCleanup ? 'Yes' : 'No'],
['Cleanup delay (ms)', String(config.cleanupDelayMs ?? '—')],
['SSH key configured', config.sshKeyConfigured ? 'Yes' : 'No'],
];
rows.forEach(([label, value]) => {
const row = document.createElement('div');
row.className = 'admin-row';
const labelWrap = document.createElement('div');
labelWrap.style.minWidth = '180px';
const strong = document.createElement('strong');
strong.textContent = label;
labelWrap.appendChild(strong);
const valueWrap = document.createElement('div');
valueWrap.textContent = value;
row.appendChild(labelWrap);
row.appendChild(valueWrap);
el.externalTestingConfig.appendChild(row);
});
}
function renderExternalTestingOutput(result) {
if (!el.externalTestingOutput) return;
el.externalTestingOutput.innerHTML = '';
if (!result) return;
const summary = document.createElement('div');
summary.className = 'admin-row';
const summaryLabel = document.createElement('div');
summaryLabel.style.minWidth = '180px';
const summaryStrong = document.createElement('strong');
summaryStrong.textContent = 'Overall result';
summaryLabel.appendChild(summaryStrong);
const summaryValue = document.createElement('div');
summaryValue.textContent = result.ok ? 'Passed' : 'Failed';
summary.appendChild(summaryLabel);
summary.appendChild(summaryValue);
el.externalTestingOutput.appendChild(summary);
const detailRows = [
['Subsite URL', result.subsite_url || '—'],
['Duration', typeof result.duration === 'number' ? `${(result.duration / 1000).toFixed(1)}s` : '—'],
];
detailRows.forEach(([label, value]) => {
const row = document.createElement('div');
row.className = 'admin-row';
const labelWrap = document.createElement('div');
labelWrap.style.minWidth = '180px';
const strong = document.createElement('strong');
strong.textContent = label;
labelWrap.appendChild(strong);
const valueWrap = document.createElement('div');
valueWrap.textContent = value;
row.appendChild(labelWrap);
row.appendChild(valueWrap);
el.externalTestingOutput.appendChild(row);
});
const scenarioResults = result?.test_results?.cli_tests?.results || [];
if (scenarioResults.length) {
scenarioResults.forEach((scenario) => {
const row = document.createElement('div');
row.className = 'admin-row';
const labelWrap = document.createElement('div');
labelWrap.style.minWidth = '180px';
const strong = document.createElement('strong');
strong.textContent = scenario.name || 'Scenario';
labelWrap.appendChild(strong);
const valueWrap = document.createElement('div');
valueWrap.textContent = scenario.status === 'passed' ? 'Passed' : 'Failed';
if (scenario.status !== 'passed') {
valueWrap.style.color = 'var(--danger)';
}
row.appendChild(labelWrap);
row.appendChild(valueWrap);
el.externalTestingOutput.appendChild(row);
});
}
const errors = Array.isArray(result.errors) ? result.errors : [];
if (errors.length) {
errors.forEach((err) => {
const row = document.createElement('div');
row.className = 'admin-row';
const labelWrap = document.createElement('div');
labelWrap.style.minWidth = '180px';
const strong = document.createElement('strong');
strong.textContent = 'Error';
labelWrap.appendChild(strong);
const valueWrap = document.createElement('div');
valueWrap.textContent = err;
valueWrap.style.color = 'var(--danger)';
row.appendChild(labelWrap);
row.appendChild(valueWrap);
el.externalTestingOutput.appendChild(row);
});
}
}
async function loadExternalTestingStatus() {
const data = await api('/api/admin/external-testing-status');
renderExternalTestingConfig(data.config || {});
}
async function api(path, options = {}) {
const res = await fetch(path, {
credentials: 'same-origin',
@@ -1844,6 +1970,7 @@
() => (el.planTokensTable ? loadPlanTokens() : null),
() => ((el.tokenRateUsd || el.tokenRateGbp || el.tokenRateEur) ? loadTokenRates() : null),
() => ((el.providerUsage || el.providerLimitForm) ? loadProviderLimits() : null),
() => (el.externalTestingConfig ? loadExternalTestingStatus() : null),
];
await Promise.all(loaders.map((fn) => fn()).filter(Boolean));
// Always try to load provider limits if not already loaded (needed for backup dropdown)
@@ -2139,6 +2266,22 @@
});
}
if (el.externalTestingRun) {
el.externalTestingRun.addEventListener('click', async () => {
el.externalTestingRun.disabled = true;
setExternalTestingStatus('Running self-test...');
try {
const data = await api('/api/admin/external-testing-self-test', { method: 'POST' });
renderExternalTestingOutput(data.result || null);
setExternalTestingStatus(data.result && data.result.ok ? 'Self-test passed.' : 'Self-test failed.', !data.result || !data.result.ok);
} catch (err) {
setExternalTestingStatus(err.message || 'Self-test failed.', true);
} finally {
el.externalTestingRun.disabled = false;
}
});
}
if (el.logout) {
el.logout.addEventListener('click', async () => {
await api('/api/admin/logout', { method: 'POST' }).catch(() => { });

View File

@@ -1118,13 +1118,21 @@
</div>
<button class="primary action-link" id="export-zip-btn" title="Download as ZIP">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:8px;">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download ZIP
</button>
<div style="display:flex; align-items:center; gap:8px;">
<label style="display:inline-flex; align-items:center; gap:8px; font-weight:600; font-size:13px;">
<input type="checkbox" id="external-testing-toggle" style="width:18px; height:18px;" />
<span id="external-testing-label" style="font-weight:600; font-size:13px;">External WP Tests</span>
</label>
<button id="external-testing-info" class="action-link" title="Run the plugin through CLI tests on an external WP site" style="padding:6px 8px; border-radius:8px;">i</button>
<div id="external-testing-usage" style="font-size:12px; color:var(--muted); min-width:140px; text-align:right;"></div>
</div>
</div>
</header>
@@ -1437,6 +1445,21 @@
</div>
<!-- Confirm Build Modal -->
<!-- External Testing Limit Modal -->
<div id="external-testing-limit-modal" class="modal" style="display:none;">
<div class="modal-card" style="max-width:520px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong style="color:var(--ink); font-size:20px;">External Testing Limit Reached</strong>
<button id="external-testing-limit-close" style="background:none; border:none; font-size:22px;">&times;</button>
</div>
<p style="color:var(--muted); margin-top:12px;">Your current plan limits the number of external WP CLI tests you can run per month. To continue using external testing, upgrade your plan or purchase additional test credits.</p>
<div class="admin-actions" style="margin-top:18px; justify-content:flex-end; gap:8px;">
<button id="external-testing-limit-upgrade" class="primary">Upgrade Plan</button>
<button id="external-testing-limit-cancel" class="action-link">Cancel</button>
</div>
</div>
</div>
<div id="confirm-build-modal" class="modal">
<div class="modal-content">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">

View File

@@ -32,7 +32,8 @@ const builderState = savedState || {
lastUserRequest: '',
lastPlanText: '',
pluginPrompt: '',
subsequentPrompt: ''
subsequentPrompt: '',
externalTestingEnabled: false
};
// Auto-save builderState changes to localStorage
@@ -117,6 +118,21 @@ async function proceedWithBuild(planContent) {
async function executeBuild(planContent) {
console.log('executeBuild called with planContent:', planContent ? planContent.substring(0, 100) + '...' : 'null');
// Ensure external testing is still allowed if enabled
if (builderState.externalTestingEnabled) {
await loadUsageSummary();
const summary = state.usageSummary?.externalTesting || null;
const limit = summary ? summary.limit : null;
const used = summary ? summary.used : 0;
if (Number.isFinite(limit) && used >= limit) {
// show modal and abort
const modal = document.getElementById('external-testing-limit-modal');
if (modal) modal.style.display = 'flex';
setStatus('External testing limit reached. Disable external testing or upgrade your plan.');
return;
}
}
builderState.mode = 'build';
builderState.planApproved = true;
updateBuildModeUI();
@@ -160,7 +176,8 @@ async function executeBuild(planContent) {
model: selectedModel,
cli: 'opencode',
isProceedWithBuild: true,
planContent: planContent
planContent: planContent,
externalTestingEnabled: !!builderState.externalTestingEnabled
};
// Preserve opencodeSessionId for session continuity
if (session && session.opencodeSessionId) {
@@ -188,7 +205,16 @@ async function executeBuild(planContent) {
console.log('Build process initiated successfully');
} catch (e) {
console.error('Failed to start build:', e);
alert('Failed to start build: ' + (e.message || 'Unknown error'));
const msg = (e && e.message) ? e.message : 'Unknown error';
if (msg && msg.toLowerCase().includes('external wp cli testing')) {
// Show upgrade modal
const modal = document.getElementById('external-testing-limit-modal');
if (modal) modal.style.display = 'flex';
setStatus(msg, true);
} else {
alert('Failed to start build: ' + msg);
}
builderState.mode = 'plan'; // Revert
updateBuildModeUI();
hideLoadingIndicator();
@@ -603,6 +629,9 @@ async function loadUsageSummary() {
usageMeterTrack: !!el.usageMeterTrack
});
updateUsageProgressBar(state.usageSummary);
if (typeof window.updateExternalTestingUI === 'function') {
try { updateExternalTestingUI(); } catch (e) { console.warn('external testing UI update failed', e); }
}
if (typeof window.checkTokenLimitAndShowModal === 'function') {
setTimeout(() => window.checkTokenLimitAndShowModal(), 500);
}
@@ -618,6 +647,92 @@ async function loadUsageSummary() {
// Expose for builder.html
window.loadUsageSummary = loadUsageSummary;
// --- External testing UI helpers ---
function updateExternalTestingUI() {
const elToggle = document.getElementById('external-testing-toggle');
const elUsage = document.getElementById('external-testing-usage');
const infoBtn = document.getElementById('external-testing-info');
if (!elToggle || !elUsage) return;
const et = state.usageSummary?.externalTesting || null;
if (!et) {
elUsage.textContent = 'Not configured';
elToggle.disabled = true;
elToggle.checked = false;
builderState.externalTestingEnabled = false;
saveBuilderState(builderState);
return;
}
const used = Number(et.used || 0);
const limit = Number.isFinite(Number(et.limit)) ? et.limit : 'unlimited';
elUsage.textContent = Number.isFinite(Number(et.limit)) ? `${used} / ${limit}` : `${used} / ∞`;
if (typeof builderState.externalTestingEnabled === 'boolean') {
elToggle.checked = !!builderState.externalTestingEnabled;
} else {
elToggle.checked = false;
builderState.externalTestingEnabled = false;
saveBuilderState(builderState);
}
if (infoBtn) {
infoBtn.onclick = (e) => {
e.preventDefault();
alert('External WP tests run a series of WP-CLI checks on an external WordPress site. Tests are counted against your monthly allowance.');
};
}
elToggle.addEventListener('change', async (e) => {
const wantOn = e.target.checked === true;
if (!wantOn) {
builderState.externalTestingEnabled = false;
saveBuilderState(builderState);
elToggle.checked = false;
return;
}
// Re-check usage before enabling
await loadUsageSummary();
const summary = state.usageSummary?.externalTesting || null;
const limit = summary ? summary.limit : null;
const used = summary ? summary.used : 0;
if (Number.isFinite(limit) && used >= limit) {
// show modal suggesting upgrade
const modal = document.getElementById('external-testing-limit-modal');
if (modal) modal.style.display = 'flex';
elToggle.checked = false;
return;
}
builderState.externalTestingEnabled = true;
saveBuilderState(builderState);
elToggle.checked = true;
});
}
(function wireExternalTestingModal() {
const modal = document.getElementById('external-testing-limit-modal');
if (!modal) return;
const closeBtn = document.getElementById('external-testing-limit-close');
const cancelBtn = document.getElementById('external-testing-limit-cancel');
const upgradeBtn = document.getElementById('external-testing-limit-upgrade');
const upgradeHeaderBtn = document.getElementById('upgrade-header-btn');
const closeModal = () => { modal.style.display = 'none'; };
if (closeBtn) closeBtn.addEventListener('click', closeModal);
if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
if (upgradeBtn) {
upgradeBtn.addEventListener('click', () => {
closeModal();
if (upgradeHeaderBtn) upgradeHeaderBtn.click(); else window.location.href = '/topup';
});
}
})();
// Expose to global scope
window.updateExternalTestingUI = updateExternalTestingUI;
function checkTokenLimitAndShowModal() {
const remaining = state.usageSummary?.remaining || 0;
if (remaining <= 5000) {
@@ -1273,6 +1388,8 @@ async function hydrateUserIdFromServerSession() {
// Load usage summary on page load
loadUsageSummary().catch(err => {
console.warn('[USAGE] Initial loadUsageSummary failed:', err.message);
}).then(() => {
try { if (typeof window.updateExternalTestingUI === 'function') window.updateExternalTestingUI(); } catch (e) { console.warn('updateExternalTestingUI failed on init', e); }
});
})();
@@ -2065,7 +2182,9 @@ window.renderMessages = renderMessages;
function renderContentWithTodos(text) {
const wrapper = document.createElement('div');
if (!text) return document.createTextNode('');
const processedText = String(text).replace(/([.:])\s+(?=[A-Z])/g, '$1\n\n');
const processedText = String(text)
.replace(/:\s*(?=[A-Z])/g, ':\n\n')
.replace(/([.])\s+(?=[A-Z])/g, '$1\n\n');
const lines = processedText.split(/\r?\n/);
let currentList = null;
let inCodeBlock = false;