sidepanel.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. // sidepanel/sidepanel.js — Side Panel logic
  2. const STATUS_ICONS = {
  3. pending: '\u2B1A', // ⬚
  4. running: '\u23F3', // ⏳
  5. completed: '\u2705', // ✅
  6. failed: '\u274C', // ❌
  7. };
  8. const logArea = document.getElementById('log-area');
  9. const displayOauthUrl = document.getElementById('display-oauth-url');
  10. const displayLocalhostUrl = document.getElementById('display-localhost-url');
  11. const displayStatus = document.getElementById('display-status');
  12. const inputEmail = document.getElementById('input-email');
  13. const btnReset = document.getElementById('btn-reset');
  14. // ============================================================
  15. // State Restore on load
  16. // ============================================================
  17. async function restoreState() {
  18. try {
  19. const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
  20. // Restore data fields
  21. if (state.oauthUrl) {
  22. displayOauthUrl.textContent = state.oauthUrl;
  23. displayOauthUrl.classList.add('has-value');
  24. }
  25. if (state.localhostUrl) {
  26. displayLocalhostUrl.textContent = state.localhostUrl;
  27. displayLocalhostUrl.classList.add('has-value');
  28. }
  29. if (state.email) {
  30. inputEmail.value = state.email;
  31. }
  32. // Restore step statuses
  33. if (state.stepStatuses) {
  34. for (const [step, status] of Object.entries(state.stepStatuses)) {
  35. updateStepUI(Number(step), status);
  36. }
  37. }
  38. // Restore logs
  39. if (state.logs) {
  40. for (const entry of state.logs) {
  41. appendLog(entry);
  42. }
  43. }
  44. updateStatusDisplay(state);
  45. } catch (err) {
  46. console.error('Failed to restore state:', err);
  47. }
  48. }
  49. // ============================================================
  50. // UI Updates
  51. // ============================================================
  52. function updateStepUI(step, status) {
  53. const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
  54. if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '\u2B1A';
  55. // Interlock logic
  56. updateButtonStates();
  57. }
  58. function updateButtonStates() {
  59. // Get all current statuses from DOM
  60. const statuses = {};
  61. document.querySelectorAll('.step-status').forEach(el => {
  62. const step = Number(el.dataset.step);
  63. const icon = el.textContent;
  64. const status = Object.entries(STATUS_ICONS).find(([, v]) => v === icon)?.[0] || 'pending';
  65. statuses[step] = status;
  66. });
  67. // Find if any step is running
  68. const anyRunning = Object.values(statuses).some(s => s === 'running');
  69. for (let step = 1; step <= 9; step++) {
  70. const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
  71. if (!btn) continue;
  72. if (anyRunning) {
  73. // When any step is running, disable all buttons
  74. btn.disabled = true;
  75. } else if (step === 1) {
  76. // Step 1 is always available (unless running)
  77. btn.disabled = false;
  78. } else {
  79. // Steps 2-9: enabled if previous step completed (or current step failed for retry)
  80. const prevStatus = statuses[step - 1];
  81. const currentStatus = statuses[step];
  82. btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed');
  83. }
  84. }
  85. }
  86. function updateStatusDisplay(state) {
  87. if (!state || !state.stepStatuses) return;
  88. const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
  89. if (running) {
  90. displayStatus.textContent = `Step ${running[0]} running...`;
  91. displayStatus.classList.add('has-value');
  92. } else {
  93. const lastCompleted = Object.entries(state.stepStatuses)
  94. .filter(([, s]) => s === 'completed')
  95. .map(([k]) => Number(k))
  96. .sort((a, b) => b - a)[0];
  97. if (lastCompleted === 9) {
  98. displayStatus.textContent = 'All steps completed!';
  99. displayStatus.classList.add('has-value');
  100. } else if (lastCompleted) {
  101. displayStatus.textContent = `Step ${lastCompleted} done. Ready for step ${lastCompleted + 1}.`;
  102. displayStatus.classList.add('has-value');
  103. } else {
  104. displayStatus.textContent = 'Waiting';
  105. displayStatus.classList.remove('has-value');
  106. }
  107. }
  108. }
  109. function appendLog(entry) {
  110. const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
  111. const levelLabel = entry.level.toUpperCase().padEnd(5);
  112. const line = document.createElement('div');
  113. line.className = `log-${entry.level}`;
  114. // Extract step number from message (e.g., "Step 4: ..." or "[vps-panel] Step 1: ...")
  115. const stepMatch = entry.message.match(/Step (\d)/);
  116. const stepNum = stepMatch ? stepMatch[1] : null;
  117. // Build rich HTML
  118. let html = `<span class="log-time">${time}</span> `;
  119. html += `<span class="log-level log-level-${entry.level}">[${levelLabel}]</span> `;
  120. if (stepNum) {
  121. html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
  122. }
  123. html += `<span>${escapeHtml(entry.message)}</span>`;
  124. line.innerHTML = html;
  125. logArea.appendChild(line);
  126. logArea.scrollTop = logArea.scrollHeight;
  127. }
  128. function escapeHtml(text) {
  129. const div = document.createElement('div');
  130. div.textContent = text;
  131. return div.innerHTML;
  132. }
  133. // ============================================================
  134. // Button Handlers
  135. // ============================================================
  136. document.querySelectorAll('.step-btn').forEach(btn => {
  137. btn.addEventListener('click', async () => {
  138. const step = Number(btn.dataset.step);
  139. // Save email if step 3 and email input has value
  140. if (step === 3) {
  141. const email = inputEmail.value.trim();
  142. if (!email) {
  143. appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
  144. return;
  145. }
  146. await chrome.runtime.sendMessage({
  147. type: 'EXECUTE_STEP',
  148. source: 'sidepanel',
  149. payload: { step, email },
  150. });
  151. } else {
  152. await chrome.runtime.sendMessage({
  153. type: 'EXECUTE_STEP',
  154. source: 'sidepanel',
  155. payload: { step },
  156. });
  157. }
  158. });
  159. });
  160. // Auto Run button
  161. const btnAutoRun = document.getElementById('btn-auto-run');
  162. const btnAutoContinue = document.getElementById('btn-auto-continue');
  163. const autoContinueBar = document.getElementById('auto-continue-bar');
  164. btnAutoRun.addEventListener('click', async () => {
  165. btnAutoRun.disabled = true;
  166. btnAutoRun.textContent = 'Running...';
  167. await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
  168. });
  169. btnAutoContinue.addEventListener('click', async () => {
  170. const email = inputEmail.value.trim();
  171. if (!email) {
  172. appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
  173. return;
  174. }
  175. autoContinueBar.style.display = 'none';
  176. btnAutoRun.textContent = 'Running...';
  177. await chrome.runtime.sendMessage({
  178. type: 'RESUME_AUTO_RUN',
  179. source: 'sidepanel',
  180. payload: { email },
  181. });
  182. });
  183. // Reset button
  184. btnReset.addEventListener('click', async () => {
  185. if (confirm('Reset all steps and data?')) {
  186. await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
  187. // Clear UI
  188. displayOauthUrl.textContent = 'Not obtained';
  189. displayOauthUrl.classList.remove('has-value');
  190. displayLocalhostUrl.textContent = 'Not captured';
  191. displayLocalhostUrl.classList.remove('has-value');
  192. inputEmail.value = '';
  193. displayStatus.textContent = 'Waiting';
  194. displayStatus.classList.remove('has-value');
  195. logArea.innerHTML = '';
  196. document.querySelectorAll('.step-status').forEach(el => el.textContent = '\u2B1A');
  197. btnAutoRun.disabled = false;
  198. btnAutoRun.textContent = 'Auto Run';
  199. autoContinueBar.style.display = 'none';
  200. updateButtonStates();
  201. }
  202. });
  203. // Save email when user types/pastes
  204. inputEmail.addEventListener('change', async () => {
  205. const email = inputEmail.value.trim();
  206. if (email) {
  207. await chrome.runtime.sendMessage({
  208. type: 'SAVE_EMAIL',
  209. source: 'sidepanel',
  210. payload: { email },
  211. });
  212. }
  213. });
  214. // ============================================================
  215. // Listen for Background broadcasts
  216. // ============================================================
  217. chrome.runtime.onMessage.addListener((message) => {
  218. switch (message.type) {
  219. case 'LOG_ENTRY':
  220. appendLog(message.payload);
  221. break;
  222. case 'STEP_STATUS_CHANGED': {
  223. const { step, status } = message.payload;
  224. updateStepUI(step, status);
  225. // Update status display
  226. chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
  227. break;
  228. }
  229. case 'DATA_UPDATED': {
  230. if (message.payload.oauthUrl) {
  231. displayOauthUrl.textContent = message.payload.oauthUrl;
  232. displayOauthUrl.classList.add('has-value');
  233. }
  234. if (message.payload.localhostUrl) {
  235. displayLocalhostUrl.textContent = message.payload.localhostUrl;
  236. displayLocalhostUrl.classList.add('has-value');
  237. }
  238. break;
  239. }
  240. case 'AUTO_RUN_STATUS': {
  241. const { phase } = message.payload;
  242. switch (phase) {
  243. case 'waiting_email':
  244. autoContinueBar.style.display = 'flex';
  245. btnAutoRun.textContent = 'Waiting...';
  246. break;
  247. case 'complete':
  248. btnAutoRun.disabled = false;
  249. btnAutoRun.textContent = 'Auto Run';
  250. autoContinueBar.style.display = 'none';
  251. break;
  252. case 'stopped':
  253. btnAutoRun.disabled = false;
  254. btnAutoRun.textContent = 'Auto Run';
  255. autoContinueBar.style.display = 'none';
  256. break;
  257. }
  258. break;
  259. }
  260. }
  261. });
  262. // ============================================================
  263. // Init
  264. // ============================================================
  265. restoreState().then(() => {
  266. updateButtonStates();
  267. });