// sidepanel/sidepanel.js — Side Panel logic
const STATUS_ICONS = {
pending: '\u2B1A', // ⬚
running: '\u23F3', // ⏳
completed: '\u2705', // ✅
failed: '\u274C', // ❌
};
const logArea = document.getElementById('log-area');
const displayOauthUrl = document.getElementById('display-oauth-url');
const displayLocalhostUrl = document.getElementById('display-localhost-url');
const displayStatus = document.getElementById('display-status');
const inputEmail = document.getElementById('input-email');
const btnReset = document.getElementById('btn-reset');
// ============================================================
// State Restore on load
// ============================================================
async function restoreState() {
try {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
// Restore data fields
if (state.oauthUrl) {
displayOauthUrl.textContent = state.oauthUrl;
displayOauthUrl.classList.add('has-value');
}
if (state.localhostUrl) {
displayLocalhostUrl.textContent = state.localhostUrl;
displayLocalhostUrl.classList.add('has-value');
}
if (state.email) {
inputEmail.value = state.email;
}
// Restore step statuses
if (state.stepStatuses) {
for (const [step, status] of Object.entries(state.stepStatuses)) {
updateStepUI(Number(step), status);
}
}
// Restore logs
if (state.logs) {
for (const entry of state.logs) {
appendLog(entry);
}
}
updateStatusDisplay(state);
} catch (err) {
console.error('Failed to restore state:', err);
}
}
// ============================================================
// UI Updates
// ============================================================
function updateStepUI(step, status) {
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '\u2B1A';
// Interlock logic
updateButtonStates();
}
function updateButtonStates() {
// Get all current statuses from DOM
const statuses = {};
document.querySelectorAll('.step-status').forEach(el => {
const step = Number(el.dataset.step);
const icon = el.textContent;
const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending';
statuses[step] = status;
});
// Find if any step is running
const anyRunning = Object.values(statuses).some(s => s === 'running');
for (let step = 1; step <= 9; step++) {
const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
if (!btn) continue;
if (anyRunning) {
// When any step is running, disable all buttons
btn.disabled = true;
} else if (step === 1) {
// Step 1 is always available (unless running)
btn.disabled = false;
} else {
// Steps 2-9: enabled if previous step completed (or current step failed for retry)
const prevStatus = statuses[step - 1];
const currentStatus = statuses[step];
btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed');
}
}
}
function updateStatusDisplay(state) {
if (!state || !state.stepStatuses) return;
const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
if (running) {
displayStatus.textContent = `Step ${running[0]} running...`;
displayStatus.classList.add('has-value');
} else {
const lastCompleted = Object.entries(state.stepStatuses)
.filter(([, s]) => s === 'completed')
.map(([k]) => Number(k))
.sort((a, b) => b - a)[0];
if (lastCompleted === 9) {
displayStatus.textContent = 'All steps completed!';
displayStatus.classList.add('has-value');
} else if (lastCompleted) {
displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`;
displayStatus.classList.add('has-value');
} else {
displayStatus.textContent = 'Waiting';
displayStatus.classList.remove('has-value');
}
}
}
function appendLog(entry) {
const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
const levelLabel = entry.level.toUpperCase().padEnd(5);
const line = document.createElement('div');
line.className = `log-${entry.level}`;
// Extract step number from message (e.g., "Step 4: ..." or "[vps-panel] Step 1: ...")
const stepMatch = entry.message.match(/Step (\d)/);
const stepNum = stepMatch ? stepMatch[1] : null;
// Build rich HTML
let html = `${time} `;
html += `[${levelLabel}] `;
if (stepNum) {
html += `S${stepNum}`;
}
html += `${escapeHtml(entry.message)}`;
line.innerHTML = html;
logArea.appendChild(line);
logArea.scrollTop = logArea.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============================================================
// Button Handlers
// ============================================================
document.querySelectorAll('.step-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const step = Number(btn.dataset.step);
// Save email if step 3 and email input has value
if (step === 3) {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
return;
}
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step, email },
});
} else {
await chrome.runtime.sendMessage({
type: 'EXECUTE_STEP',
source: 'sidepanel',
payload: { step },
});
}
});
});
// Auto Run button
const btnAutoRun = document.getElementById('btn-auto-run');
const btnAutoContinue = document.getElementById('btn-auto-continue');
const autoContinueBar = document.getElementById('auto-continue-bar');
btnAutoRun.addEventListener('click', async () => {
btnAutoRun.disabled = true;
btnAutoRun.textContent = 'Running...';
await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
});
btnAutoContinue.addEventListener('click', async () => {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
return;
}
autoContinueBar.style.display = 'none';
btnAutoRun.textContent = 'Running...';
await chrome.runtime.sendMessage({
type: 'RESUME_AUTO_RUN',
source: 'sidepanel',
payload: { email },
});
});
// Reset button
btnReset.addEventListener('click', async () => {
if (confirm('Reset all steps and data?')) {
await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
// Clear UI
displayOauthUrl.textContent = 'Not obtained';
displayOauthUrl.classList.remove('has-value');
displayLocalhostUrl.textContent = 'Not captured';
displayLocalhostUrl.classList.remove('has-value');
inputEmail.value = '';
displayStatus.textContent = 'Waiting';
displayStatus.classList.remove('has-value');
logArea.innerHTML = '';
document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A');
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
updateButtonStates();
}
});
// Save email when user types/pastes
inputEmail.addEventListener('change', async () => {
const email = inputEmail.value.trim();
if (email) {
await chrome.runtime.sendMessage({
type: 'SAVE_EMAIL',
source: 'sidepanel',
payload: { email },
});
}
});
// ============================================================
// Listen for Background broadcasts
// ============================================================
chrome.runtime.onMessage.addListener((message) => {
switch (message.type) {
case 'LOG_ENTRY':
appendLog(message.payload);
break;
case 'STEP_STATUS_CHANGED': {
const { step, status } = message.payload;
updateStepUI(step, status);
// Update status display
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
break;
}
case 'DATA_UPDATED': {
if (message.payload.oauthUrl) {
displayOauthUrl.textContent = message.payload.oauthUrl;
displayOauthUrl.classList.add('has-value');
}
if (message.payload.localhostUrl) {
displayLocalhostUrl.textContent = message.payload.localhostUrl;
displayLocalhostUrl.classList.add('has-value');
}
break;
}
case 'AUTO_RUN_STATUS': {
const { phase } = message.payload;
switch (phase) {
case 'waiting_email':
autoContinueBar.style.display = 'flex';
btnAutoRun.textContent = 'Waiting...';
break;
case 'complete':
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
break;
case 'stopped':
btnAutoRun.disabled = false;
btnAutoRun.textContent = 'Auto Run';
autoContinueBar.style.display = 'none';
break;
}
break;
}
}
});
// ============================================================
// Init
// ============================================================
restoreState().then(() => {
updateButtonStates();
});