ft-scout 7.0.7 → 7.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Account & Paid Tiers Dashboard Controller
3
- ========================================================================== */
2
+ FrontTerrain Scout — Multi-Page Application Engine v7.0.8
3
+ ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
6
6
  apiKey: 'AIzaSyCMVNDgR8EejfXofUy87L7hsKxi3E_C3lg',
@@ -15,19 +15,19 @@ const TIER_DETAILS = {
15
15
  name: 'Developer Free Tier',
16
16
  badge: 'ACTIVE',
17
17
  desc: 'Standard access to Scout CLI features & local execution.',
18
- icon: '<i class="fa-solid fa-star text-warning" style="font-size: 24px;"></i>',
18
+ icon: '<i class="fa-solid fa-star text-warning" style="font-size: 28px;"></i>',
19
19
  },
20
20
  pro: {
21
21
  name: 'Pro Developer Tier',
22
22
  badge: 'PRO ACTIVE',
23
23
  desc: 'High-power AI co-pilot capabilities, priority quota & 5 active CLI sessions.',
24
- icon: '<i class="fa-solid fa-bolt text-primary" style="font-size: 24px;"></i>',
24
+ icon: '<i class="fa-solid fa-bolt text-primary" style="font-size: 28px;"></i>',
25
25
  },
26
26
  enterprise: {
27
27
  name: 'Team & Enterprise Tier',
28
28
  badge: 'ENTERPRISE ACTIVE',
29
29
  desc: 'Organization controls, security policies & dedicated scale.',
30
- icon: '<i class="fa-solid fa-building text-warning" style="font-size: 24px;"></i>',
30
+ icon: '<i class="fa-solid fa-building text-warning" style="font-size: 28px;"></i>',
31
31
  },
32
32
  };
33
33
 
@@ -36,7 +36,6 @@ let state = {
36
36
  user: null,
37
37
  token: null,
38
38
  refreshToken: null,
39
- activeTab: 'account',
40
39
  serverConnected: false,
41
40
  apiBaseUrl: '',
42
41
  sessions: [],
@@ -53,103 +52,73 @@ let state = {
53
52
 
54
53
  let pendingCheckoutTier = null;
55
54
 
56
- // DOM Elements Reference
57
- const elements = {
58
- // Views
59
- authSection: document.getElementById('auth-section'),
60
- dashboardSection: document.getElementById('dashboard-section'),
61
-
62
- // Auth Controls
63
- tabLoginBtn: document.getElementById('tab-login-btn'),
64
- tabSignupBtn: document.getElementById('tab-signup-btn'),
65
- loginForm: document.getElementById('login-form'),
66
- signupForm: document.getElementById('signup-form'),
67
- btnOpenAuth: document.getElementById('btn-open-auth'),
68
- btnDemoMode: document.getElementById('btn-demo-mode'),
69
- btnForgotPwd: document.getElementById('btn-forgot-pwd'),
70
-
71
- // Navigation & User Menu
72
- mainNav: document.getElementById('main-nav'),
73
- userMenu: document.getElementById('user-menu'),
74
- userMenuToggle: document.getElementById('user-menu-toggle'),
75
- userDropdown: document.getElementById('user-dropdown'),
76
- navAvatar: document.getElementById('nav-avatar'),
77
- navUserName: document.getElementById('nav-user-name'),
78
- dropdownName: document.getElementById('dropdown-name'),
79
- dropdownEmail: document.getElementById('dropdown-email'),
80
- btnDropdownLogout: document.getElementById('btn-dropdown-logout'),
81
- btnDropdownProfile: document.getElementById('btn-dropdown-profile'),
82
- btnDropdownTiers: document.getElementById('btn-dropdown-tiers'),
83
- navTenantTag: document.getElementById('nav-tenant-tag'),
84
- navTenantName: document.getElementById('nav-tenant-name'),
85
-
86
- // Account & Header Controls
87
- userDisplayName: document.getElementById('user-display-name'),
88
- btnNavTiersHero: document.getElementById('btn-nav-tiers-hero'),
89
- sessionTableBody: document.getElementById('session-table-body'),
90
-
91
- // Credit Quota & Usage Controls
92
- valCreditsUsed: document.getElementById('val-credits-used'),
93
- valCreditsRemaining: document.getElementById('val-credits-remaining'),
94
- valCreditsTotal: document.getElementById('val-credits-total'),
95
- valCreditsPct: document.getElementById('val-credits-pct'),
96
- creditProgressFill: document.getElementById('credit-progress-fill'),
97
- valRenewDatetime: document.getElementById('val-renew-datetime'),
98
- btnUpgradeCreditsHero: document.getElementById('btn-upgrade-credits-hero'),
99
- creditStatusBadge: document.getElementById('credit-status-badge'),
100
- labelCreditConsumption: document.getElementById('label-credit-consumption'),
101
- byokBannerNote: document.getElementById('byok-banner-note'),
102
-
103
- // BYOK & Custom Proxy Endpoint Inputs
104
- byokForm: document.getElementById('byok-form'),
105
- selectByokProvider: document.getElementById('select-byok-provider'),
106
- inputByokEndpoint: document.getElementById('input-byok-endpoint'),
107
- inputByokKey: document.getElementById('input-byok-key'),
108
- checkboxEnableByok: document.getElementById('checkbox-enable-byok'),
109
- byokStatusBadge: document.getElementById('byok-status-badge'),
110
-
111
- // Profile Settings Inputs
112
- settingsDisplayName: document.getElementById('settings-display-name'),
113
- settingsEmail: document.getElementById('settings-email'),
114
- settingsUid: document.getElementById('settings-uid'),
115
- settingsActiveTier: document.getElementById('settings-active-tier'),
116
- profileForm: document.getElementById('profile-form'),
117
- btnAccountLogout: document.getElementById('btn-account-logout'),
118
-
119
- // Paid Tiers UI
120
- currentPlanTitle: document.getElementById('current-plan-title'),
121
- currentPlanBadge: document.getElementById('current-plan-badge'),
122
- currentPlanDesc: document.getElementById('current-plan-desc'),
123
- tierActiveIcon: document.getElementById('tier-active-icon'),
124
- toggleBillingCycle: document.getElementById('toggle-billing-cycle'),
125
- priceProVal: document.getElementById('price-pro-val'),
126
- priceProPeriod: document.getElementById('price-pro-period'),
127
- priceEntVal: document.getElementById('price-ent-val'),
128
- priceEntPeriod: document.getElementById('price-ent-period'),
129
-
130
- // Checkout Modal Controls
131
- checkoutModal: document.getElementById('checkout-modal'),
132
- checkoutModalTitle: document.getElementById('checkout-modal-title'),
133
- checkoutPlanName: document.getElementById('checkout-plan-name'),
134
- checkoutBillingCycle: document.getElementById('checkout-billing-cycle'),
135
- checkoutPlanPrice: document.getElementById('checkout-plan-price'),
136
- checkoutPricePeriod: document.getElementById('checkout-price-period'),
137
- checkoutQuotaVal: document.getElementById('checkout-quota-val'),
138
- btnCloseCheckout: document.getElementById('btn-close-checkout'),
139
- btnCancelCheckout: document.getElementById('btn-cancel-checkout'),
140
- btnConfirmCheckout: document.getElementById('btn-confirm-checkout'),
141
-
142
- // Toast Container
143
- toastContainer: document.getElementById('toast-container'),
144
- };
55
+ // Audio Synthesizer via Web Audio API
56
+ let audioCtx = null;
57
+
58
+ function playRetroBeep(freq = 440, duration = 0.08, type = 'square') {
59
+ try {
60
+ if (!audioCtx) {
61
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
62
+ audioCtx = new AudioContext();
63
+ }
64
+ if (audioCtx.state === 'suspended') audioCtx.resume();
65
+
66
+ const osc = audioCtx.createOscillator();
67
+ const gain = audioCtx.createGain();
68
+ osc.type = type;
69
+ osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
70
+
71
+ gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
72
+ gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
73
+
74
+ osc.connect(gain);
75
+ gain.connect(audioCtx.destination);
76
+ osc.start();
77
+ osc.stop(audioCtx.currentTime + duration);
78
+ } catch { }
79
+ }
80
+
81
+ // DOM Helper Getter
82
+ function getEl(id) {
83
+ return document.getElementById(id);
84
+ }
85
+
86
+ // Helper to determine current page basename
87
+ function getCurrentPage() {
88
+ const path = window.location.pathname.toLowerCase();
89
+ if (path.includes('/auth')) return '/auth';
90
+ if (path.includes('/plans')) return '/plans';
91
+ if (path.includes('/cli')) return '/cli';
92
+ return 'index.html';
93
+ }
94
+
95
+ // Highlight current active nav link in navbar
96
+ function highlightActiveNav() {
97
+ const currentPage = getCurrentPage();
98
+ document.querySelectorAll('.nav-links .nav-btn').forEach((link) => {
99
+ const href = link.getAttribute('href');
100
+ if (href === currentPage || (currentPage === 'index.html' && (href === 'index.html' || href === './'))) {
101
+ link.classList.add('active');
102
+ } else {
103
+ link.classList.remove('active');
104
+ }
105
+ });
106
+ }
145
107
 
146
108
  // Initialize Application
147
109
  document.addEventListener('DOMContentLoaded', async () => {
110
+ highlightActiveNav();
148
111
  loadTierAndCreditsFromStorage();
149
112
  loadSessionsFromStorage();
150
113
  initEventListeners();
114
+ initFAQAccordions();
115
+ initTerminalSimulator();
151
116
  renderAllDynamicComponents();
152
117
  await checkServerStatusAndSession();
118
+
119
+ if (getEl('pacman-canvas')) {
120
+ initPacmanGame();
121
+ }
153
122
  });
154
123
 
155
124
  // Time Helper Function for Dynamic Relative Timestamps
@@ -169,7 +138,7 @@ function timeAgo(dateInput) {
169
138
  return `${days}d ago`;
170
139
  }
171
140
 
172
- // Load Subscription Tier & Dynamic Credits from Local Storage / Config
141
+ // Load Subscription Tier & Dynamic Credits from Local Storage
173
142
  function loadTierAndCreditsFromStorage() {
174
143
  const savedTier = localStorage.getItem('scout_current_tier');
175
144
  if (savedTier && TIER_DETAILS[savedTier]) {
@@ -177,11 +146,7 @@ function loadTierAndCreditsFromStorage() {
177
146
  }
178
147
 
179
148
  const savedUsed = localStorage.getItem('scout_credits_used');
180
- if (savedUsed !== null) {
181
- state.creditsUsed = parseInt(savedUsed, 10) || 0;
182
- } else {
183
- state.creditsUsed = 0;
184
- }
149
+ state.creditsUsed = savedUsed !== null ? parseInt(savedUsed, 10) || 0 : 0;
185
150
 
186
151
  const savedTotal = localStorage.getItem('scout_credits_total');
187
152
  if (savedTotal !== null) {
@@ -209,6 +174,7 @@ function renderAllDynamicComponents() {
209
174
  renderSessionsTable();
210
175
  renderCreditUsageUI();
211
176
  renderTiersUI();
177
+ updateUserUI();
212
178
  }
213
179
 
214
180
  // Open / Close Checkout Modal
@@ -228,67 +194,71 @@ function openCheckoutModal(targetTier) {
228
194
  quota = '10,000 Credits / mo';
229
195
  }
230
196
 
231
- if (elements.checkoutPlanName) elements.checkoutPlanName.innerText = tierInfo.name;
232
- if (elements.checkoutBillingCycle) elements.checkoutBillingCycle.innerText = isAnnual ? 'Billed Annually (20% Off)' : 'Billed Monthly';
233
- if (elements.checkoutPlanPrice) elements.checkoutPlanPrice.innerText = price;
234
- if (elements.checkoutPricePeriod) elements.checkoutPricePeriod.innerText = isAnnual ? '/ mo (annual)' : '/ mo';
235
- if (elements.checkoutQuotaVal) elements.checkoutQuotaVal.innerText = quota;
197
+ if (getEl('checkout-plan-name')) getEl('checkout-plan-name').innerText = tierInfo.name;
198
+ if (getEl('checkout-billing-cycle')) getEl('checkout-billing-cycle').innerText = isAnnual ? 'Billed Annually (20% Off)' : 'Billed Monthly';
199
+ if (getEl('checkout-plan-price')) getEl('checkout-plan-price').innerText = price;
200
+ if (getEl('checkout-price-period')) getEl('checkout-price-period').innerText = isAnnual ? '/ mo (annual)' : '/ mo';
201
+ if (getEl('checkout-quota-val')) getEl('checkout-quota-val').innerText = quota;
236
202
 
237
- if (elements.checkoutModal) elements.checkoutModal.classList.remove('hidden');
203
+ const modal = getEl('checkout-modal');
204
+ if (modal) modal.classList.remove('hidden');
238
205
  }
239
206
 
240
207
  function closeCheckoutModal() {
241
208
  pendingCheckoutTier = null;
242
- if (elements.checkoutModal) elements.checkoutModal.classList.add('hidden');
209
+ const modal = getEl('checkout-modal');
210
+ if (modal) modal.classList.add('hidden');
243
211
  }
244
212
 
245
213
  // Setup Event Handlers
246
214
  function initEventListeners() {
247
215
  // Auth Form Tabs Switcher
248
- elements.tabLoginBtn?.addEventListener('click', () => switchAuthForm('login'));
249
- elements.tabSignupBtn?.addEventListener('click', () => switchAuthForm('signup'));
216
+ getEl('tab-login-btn')?.addEventListener('click', () => switchAuthForm('login'));
217
+ getEl('tab-signup-btn')?.addEventListener('click', () => switchAuthForm('signup'));
250
218
 
251
219
  // Form Submissions
252
- elements.loginForm?.addEventListener('submit', handleSignIn);
253
- elements.signupForm?.addEventListener('submit', handleSignUp);
254
- elements.btnDemoMode?.addEventListener('click', launchDemoSession);
220
+ getEl('login-form')?.addEventListener('submit', handleSignIn);
221
+ getEl('signup-form')?.addEventListener('submit', handleSignUp);
222
+ getEl('btn-demo-mode')?.addEventListener('click', launchDemoSession);
223
+
224
+ // Password Strength Checker
225
+ getEl('signup-password')?.addEventListener('input', (e) => {
226
+ const val = e.target.value;
227
+ const bar = getEl('pwd-strength-bar');
228
+ if (!bar) return;
229
+
230
+ let strength = 0;
231
+ if (val.length >= 6) strength += 33;
232
+ if (/[A-Z]/.test(val) && /[0-9]/.test(val)) strength += 33;
233
+ if (/[^A-Za-z0-9]/.test(val) && val.length >= 10) strength += 34;
234
+
235
+ bar.style.width = `${strength}%`;
236
+ bar.style.backgroundColor = strength < 40 ? '#ef4444' : strength < 80 ? '#eab308' : '#22c55e';
237
+ });
255
238
 
256
- elements.btnForgotPwd?.addEventListener('click', (e) => {
239
+ getEl('btn-forgot-pwd')?.addEventListener('click', (e) => {
257
240
  e.preventDefault();
258
241
  handleForgotPassword();
259
242
  });
260
243
 
261
- // Navigation Tabs Switcher
262
- document.querySelectorAll('.nav-btn').forEach((btn) => {
263
- btn.addEventListener('click', () => {
264
- const targetTab = btn.getAttribute('data-tab');
265
- if (targetTab) switchDashboardTab(targetTab);
266
- });
267
- });
268
-
269
244
  // User Dropdown Menu
270
- elements.userMenuToggle?.addEventListener('click', (e) => {
245
+ getEl('user-menu-toggle')?.addEventListener('click', (e) => {
271
246
  e.stopPropagation();
272
- elements.userDropdown?.classList.toggle('show');
247
+ getEl('user-dropdown')?.classList.toggle('show');
273
248
  });
274
249
 
275
250
  document.addEventListener('click', () => {
276
- elements.userDropdown?.classList.remove('show');
251
+ getEl('user-dropdown')?.classList.remove('show');
277
252
  });
278
253
 
279
- elements.btnDropdownLogout?.addEventListener('click', handleLogout);
280
- elements.btnAccountLogout?.addEventListener('click', handleLogout);
281
- elements.btnDropdownProfile?.addEventListener('click', () => switchDashboardTab('account'));
282
- elements.btnDropdownTiers?.addEventListener('click', () => switchDashboardTab('tiers'));
283
- elements.btnNavTiersHero?.addEventListener('click', () => switchDashboardTab('tiers'));
284
- elements.btnUpgradeCreditsHero?.addEventListener('click', () => switchDashboardTab('tiers'));
285
- elements.btnOpenAuth?.addEventListener('click', showAuthSection);
254
+ getEl('btn-dropdown-logout')?.addEventListener('click', handleLogout);
255
+ getEl('btn-account-logout')?.addEventListener('click', handleLogout);
286
256
 
287
257
  // Checkout Modal Events
288
- elements.btnCloseCheckout?.addEventListener('click', closeCheckoutModal);
289
- elements.btnCancelCheckout?.addEventListener('click', closeCheckoutModal);
258
+ getEl('btn-close-checkout')?.addEventListener('click', closeCheckoutModal);
259
+ getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
290
260
 
291
- elements.btnConfirmCheckout?.addEventListener('click', () => {
261
+ getEl('btn-confirm-checkout')?.addEventListener('click', () => {
292
262
  if (!pendingCheckoutTier) return;
293
263
 
294
264
  const targetTier = pendingCheckoutTier;
@@ -314,7 +284,7 @@ function initEventListeners() {
314
284
  document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
315
285
  btn.addEventListener('click', () => {
316
286
  const targetId = btn.getAttribute('data-target');
317
- const input = document.getElementById(targetId);
287
+ const input = getEl(targetId);
318
288
  if (input) {
319
289
  const isPwd = input.type === 'password';
320
290
  input.type = isPwd ? 'text' : 'password';
@@ -324,9 +294,10 @@ function initEventListeners() {
324
294
  });
325
295
 
326
296
  // Profile Form Submission
327
- elements.profileForm?.addEventListener('submit', (e) => {
297
+ getEl('profile-form')?.addEventListener('submit', (e) => {
328
298
  e.preventDefault();
329
- const newName = elements.settingsDisplayName.value.trim();
299
+ const inputDisp = getEl('settings-display-name');
300
+ const newName = inputDisp ? inputDisp.value.trim() : '';
330
301
  if (newName && state.user) {
331
302
  state.user.name = newName;
332
303
  saveSessionToStorage();
@@ -336,7 +307,7 @@ function initEventListeners() {
336
307
  }
337
308
  });
338
309
 
339
- elements.selectByokProvider?.addEventListener('change', (e) => {
310
+ getEl('select-byok-provider')?.addEventListener('change', (e) => {
340
311
  const provider = e.target.value;
341
312
  const defaultEndpoints = {
342
313
  nvidia: 'https://integrate.api.nvidia.com/v1',
@@ -345,18 +316,19 @@ function initEventListeners() {
345
316
  openai: 'https://api.openai.com/v1',
346
317
  custom: 'http://localhost:11434/v1',
347
318
  };
348
- if (elements.inputByokEndpoint && defaultEndpoints[provider]) {
349
- elements.inputByokEndpoint.value = defaultEndpoints[provider];
319
+ const epInput = getEl('input-byok-endpoint');
320
+ if (epInput && defaultEndpoints[provider]) {
321
+ epInput.value = defaultEndpoints[provider];
350
322
  }
351
323
  });
352
324
 
353
325
  // BYOK & Custom Proxy Form Submission
354
- elements.byokForm?.addEventListener('submit', (e) => {
326
+ getEl('byok-form')?.addEventListener('submit', (e) => {
355
327
  e.preventDefault();
356
- state.customLlmProvider = elements.selectByokProvider?.value || 'nvidia';
357
- state.customBaseUrl = elements.inputByokEndpoint?.value.trim() || '';
358
- state.customApiKey = elements.inputByokKey?.value.trim() || '';
359
- state.byokEnabled = elements.checkboxEnableByok?.checked || false;
328
+ state.customLlmProvider = getEl('select-byok-provider')?.value || 'nvidia';
329
+ state.customBaseUrl = getEl('input-byok-endpoint')?.value.trim() || '';
330
+ state.customApiKey = getEl('input-byok-key')?.value.trim() || '';
331
+ state.byokEnabled = getEl('checkbox-enable-byok')?.checked || false;
360
332
 
361
333
  localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
362
334
  localStorage.setItem('scout_byok_provider', state.customLlmProvider);
@@ -374,7 +346,7 @@ function initEventListeners() {
374
346
  }
375
347
  });
376
348
 
377
- elements.checkboxEnableByok?.addEventListener('change', (e) => {
349
+ getEl('checkbox-enable-byok')?.addEventListener('change', (e) => {
378
350
  state.byokEnabled = e.target.checked;
379
351
  localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
380
352
  syncAuthWithServer();
@@ -382,7 +354,7 @@ function initEventListeners() {
382
354
  });
383
355
 
384
356
  // Billing Cycle Toggle Switcher
385
- elements.toggleBillingCycle?.addEventListener('change', (e) => {
357
+ getEl('toggle-billing-cycle')?.addEventListener('change', (e) => {
386
358
  state.billingCycle = e.target.checked ? 'annual' : 'monthly';
387
359
  renderTiersUI();
388
360
  });
@@ -409,7 +381,105 @@ function initEventListeners() {
409
381
  });
410
382
  }
411
383
 
412
- // Data Storage & Session Loaders
384
+ // FAQ Accordion Handler
385
+ function initFAQAccordions() {
386
+ document.querySelectorAll('.faq-question').forEach((btn) => {
387
+ btn.addEventListener('click', () => {
388
+ const item = btn.closest('.faq-item');
389
+ if (item) item.classList.toggle('open');
390
+ });
391
+ });
392
+ }
393
+
394
+ // Interactive Web Terminal Simulator
395
+ function initTerminalSimulator() {
396
+ const form = getEl('cli-terminal-form');
397
+ const input = getEl('terminal-input-field');
398
+ const logs = getEl('terminal-logs');
399
+
400
+ if (!form || !input || !logs) return;
401
+
402
+ // Quick Command Chip Click Handlers
403
+ document.querySelectorAll('.btn-cli-chip').forEach((chip) => {
404
+ chip.addEventListener('click', () => {
405
+ const cmd = chip.getAttribute('data-cmd');
406
+ if (cmd) {
407
+ input.value = cmd;
408
+ runTerminalCommand(cmd);
409
+ }
410
+ });
411
+ });
412
+
413
+ form.addEventListener('submit', (e) => {
414
+ e.preventDefault();
415
+ const cmd = input.value.trim();
416
+ if (cmd) {
417
+ runTerminalCommand(cmd);
418
+ input.value = '';
419
+ }
420
+ });
421
+
422
+ function runTerminalCommand(cmd) {
423
+ const line = document.createElement('div');
424
+ line.className = 'terminal-line';
425
+ line.innerHTML = `<span class="prompt-user">developer@scout</span>:$ <span class="cmd-text">${escapeHtml(cmd)}</span>`;
426
+ logs.appendChild(line);
427
+
428
+ const response = document.createElement('div');
429
+ response.style.margin = '4px 0 12px 0';
430
+
431
+ const lower = cmd.toLowerCase().trim();
432
+ if (lower === 'clear') {
433
+ logs.innerHTML = '';
434
+ return;
435
+ } else if (lower === 'help' || lower === 'scout help' || lower === 'scout --help' || lower === 'scout') {
436
+ response.innerHTML = `
437
+ <span class="term-yellow">Available Scout Commands:</span><br>
438
+ ↳ <code>scout agent</code> - Launch autonomous AI co-pilot agent & reasoning loop<br>
439
+ ↳ <code>scout status</code> - Inspect CLI authentication, active workspace & credit balance<br>
440
+ ↳ <code>scout analyze</code> - Run instant AST codebase teardown and entry point indexing<br>
441
+ ↳ <code>scout teardown</code> - Generate strategic onboarding map & architectural moat report<br>
442
+ ↳ <code>scout byok</code> - Verify custom LLM proxy endpoint connection<br>
443
+ ↳ <code>clear</code> - Clear terminal buffer<br>
444
+ `;
445
+ } else if (lower.includes('agent')) {
446
+ response.innerHTML = `
447
+ <span class="term-bg-purple"> SCOUT AUTONOMOUS CO-PILOT AGENT </span><br>
448
+ <span class="term-green">✔ Agent Orchestrator active (Gemini 2.5 Flash / Deep Reasoning Engine)</span><br>
449
+ <span class="term-cyan">↳ Workspace Vector Memory:</span> 124 files indexed and loaded<br>
450
+ <span class="term-yellow">↳ Pair-Programming Status:</span> Ready for interactive tasks & refactoring loops<br>
451
+ <span class="term-dim">↳ Try typing 'scout analyze' or 'scout teardown' to execute automated codebase operations.</span><br>
452
+ `;
453
+ } else if (lower.includes('status')) {
454
+ response.innerHTML = `
455
+ <span class="term-green">✔ Scout CLI Engine: v7.0.8 Online</span><br>
456
+ ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
457
+ ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
458
+ ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
459
+ `;
460
+ } else if (lower.includes('analyze') || lower.includes('teardown')) {
461
+ response.innerHTML = `
462
+ <span class="term-bg-cyan"> FRONTTERRAIN SCOUT AST ENGINE </span><br>
463
+ <span class="term-green">✔ Indexed 124 workspace files in 42ms.</span><br>
464
+ <span class="term-yellow">↳ Primary Entry Point:</span> src/index.ts<br>
465
+ <span class="term-purple">↳ Core Dependencies:</span> TypeScript, Commander, Clack, Express, Firebase SDK<br>
466
+ `;
467
+ } else if (lower.includes('byok')) {
468
+ response.innerHTML = `
469
+ <span class="term-yellow">↳ BYOK Mode Status:</span> ${state.byokEnabled ? '<span class="term-green">ACTIVE (Scout Credits Bypassed)</span>' : '<span class="term-dim">DISABLED (Using Scout Default Key)</span>'}<br>
470
+ ↳ Provider: ${escapeHtml(state.customLlmProvider)}<br>
471
+ ↳ Endpoint: ${escapeHtml(state.customBaseUrl || 'Default Proxy')}<br>
472
+ `;
473
+ } else {
474
+ response.innerHTML = `<span class="term-purple">scout: command not found: '${escapeHtml(cmd)}'. Type 'help' for available commands.</span>`;
475
+ }
476
+
477
+ logs.appendChild(response);
478
+ logs.scrollTop = logs.scrollHeight;
479
+ }
480
+ }
481
+
482
+ // Sessions Storage Loader
413
483
  function loadSessionsFromStorage() {
414
484
  try {
415
485
  const stored = localStorage.getItem('scout_cli_sessions');
@@ -440,10 +510,11 @@ function saveSessionsToStorage() {
440
510
  }
441
511
 
442
512
  function renderSessionsTable() {
443
- if (!elements.sessionTableBody) return;
513
+ const tbody = getEl('session-table-body');
514
+ if (!tbody) return;
444
515
 
445
516
  if (!state.sessions || state.sessions.length === 0) {
446
- elements.sessionTableBody.innerHTML = `
517
+ tbody.innerHTML = `
447
518
  <tr>
448
519
  <td colspan="4" class="empty-table-notice">No active CLI authorization sessions found.</td>
449
520
  </tr>
@@ -451,12 +522,12 @@ function renderSessionsTable() {
451
522
  return;
452
523
  }
453
524
 
454
- elements.sessionTableBody.innerHTML = state.sessions
525
+ tbody.innerHTML = state.sessions
455
526
  .map((sess) => {
456
527
  return `
457
528
  <tr data-id="${escapeHtml(sess.id)}">
458
529
  <td>
459
- <div class="flex-center-gap">
530
+ <div class="flex-center-gap" style="display: flex; align-items: center; gap: 8px;">
460
531
  <i class="fa-solid fa-terminal text-primary"></i>
461
532
  <div>
462
533
  <strong>${escapeHtml(sess.host)}</strong>
@@ -482,40 +553,49 @@ function revokeSession(sessionId) {
482
553
 
483
554
  // Render Dynamic Credit Usage & BYOK Status UI
484
555
  function renderCreditUsageUI() {
556
+ const byokStatusBadge = getEl('byok-status-badge');
557
+ const creditStatusBadge = getEl('credit-status-badge');
558
+ const valUsed = getEl('val-credits-used');
559
+ const valRemaining = getEl('val-credits-remaining');
560
+ const valTotal = getEl('val-credits-total');
561
+ const valPct = getEl('val-credits-pct');
562
+ const progressFill = getEl('credit-progress-fill');
563
+ const labelConsumption = getEl('label-credit-consumption');
564
+ const byokNote = getEl('byok-banner-note');
565
+ const valRenew = getEl('val-renew-datetime');
566
+
485
567
  if (state.byokEnabled) {
486
- if (elements.byokStatusBadge) {
487
- elements.byokStatusBadge.innerText = 'BYOK Mode Active';
488
- elements.byokStatusBadge.className = 'badge badge-accent';
568
+ if (byokStatusBadge) {
569
+ byokStatusBadge.innerText = 'BYOK Mode Active';
570
+ byokStatusBadge.className = 'badge badge-accent';
489
571
  }
490
- if (elements.creditStatusBadge) {
491
- elements.creditStatusBadge.innerText = 'Custom Endpoint Active';
492
- elements.creditStatusBadge.className = 'badge badge-accent';
572
+ if (creditStatusBadge) {
573
+ creditStatusBadge.innerText = 'Custom Endpoint Active';
574
+ creditStatusBadge.className = 'badge badge-accent';
493
575
  }
494
- if (elements.valCreditsUsed) elements.valCreditsUsed.innerText = 'Unmonitored';
495
- if (elements.valCreditsRemaining) elements.valCreditsRemaining.innerText = 'Unlimited (BYOK)';
496
- if (elements.valCreditsTotal) elements.valCreditsTotal.innerText = 'Custom Provider';
497
- if (elements.valCreditsPct) elements.valCreditsPct.innerText = 'BYOK';
498
-
499
- if (elements.creditProgressFill) {
500
- elements.creditProgressFill.style.width = '100%';
501
- elements.creditProgressFill.style.background = 'linear-gradient(90deg, #a855f7 0%, #ec4899 100%)';
576
+ if (valUsed) valUsed.innerText = 'Unmonitored';
577
+ if (valRemaining) valRemaining.innerText = 'Unlimited (BYOK)';
578
+ if (valTotal) valTotal.innerText = 'Custom Provider';
579
+ if (valPct) valPct.innerText = 'BYOK';
580
+
581
+ if (progressFill) {
582
+ progressFill.style.width = '100%';
583
+ progressFill.style.background = 'linear-gradient(90deg, #a855f7 0%, #ec4899 100%)';
502
584
  }
503
585
 
504
- if (elements.labelCreditConsumption) {
505
- elements.labelCreditConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
586
+ if (labelConsumption) {
587
+ labelConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
506
588
  }
507
589
 
508
- if (elements.byokBannerNote) {
509
- elements.byokBannerNote.classList.remove('hidden');
510
- }
590
+ if (byokNote) byokNote.classList.remove('hidden');
511
591
  } else {
512
- if (elements.byokStatusBadge) {
513
- elements.byokStatusBadge.innerText = 'Scout Default';
514
- elements.byokStatusBadge.className = 'badge';
592
+ if (byokStatusBadge) {
593
+ byokStatusBadge.innerText = 'Scout Default';
594
+ byokStatusBadge.className = 'badge';
515
595
  }
516
- if (elements.creditStatusBadge) {
517
- elements.creditStatusBadge.innerText = 'Quota Active';
518
- elements.creditStatusBadge.className = 'badge badge-success';
596
+ if (creditStatusBadge) {
597
+ creditStatusBadge.innerText = 'Quota Active';
598
+ creditStatusBadge.className = 'badge badge-success';
519
599
  }
520
600
 
521
601
  const defaultTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
@@ -524,23 +604,21 @@ function renderCreditUsageUI() {
524
604
  const remaining = Math.max(0, total - used);
525
605
  const pct = Math.min(100, Math.round((used / total) * 100));
526
606
 
527
- if (elements.valCreditsUsed) elements.valCreditsUsed.innerText = used.toLocaleString();
528
- if (elements.valCreditsRemaining) elements.valCreditsRemaining.innerText = remaining.toLocaleString();
529
- if (elements.valCreditsTotal) elements.valCreditsTotal.innerText = total.toLocaleString();
530
- if (elements.valCreditsPct) elements.valCreditsPct.innerText = `${pct}%`;
607
+ if (valUsed) valUsed.innerText = used.toLocaleString();
608
+ if (valRemaining) valRemaining.innerText = remaining.toLocaleString();
609
+ if (valTotal) valTotal.innerText = total.toLocaleString();
610
+ if (valPct) valPct.innerText = `${pct}%`;
531
611
 
532
- if (elements.creditProgressFill) {
533
- elements.creditProgressFill.style.width = `${pct}%`;
534
- elements.creditProgressFill.style.background = 'linear-gradient(90deg, #6366f1 0%, #38bdf8 60%, #34d399 100%)';
612
+ if (progressFill) {
613
+ progressFill.style.width = `${pct}%`;
614
+ progressFill.style.background = 'linear-gradient(90deg, #6366f1 0%, #38bdf8 60%, #34d399 100%)';
535
615
  }
536
616
 
537
- if (elements.labelCreditConsumption) {
538
- elements.labelCreditConsumption.innerText = 'Monthly Credit Consumption';
617
+ if (labelConsumption) {
618
+ labelConsumption.innerText = 'Monthly Credit Consumption';
539
619
  }
540
620
 
541
- if (elements.byokBannerNote) {
542
- elements.byokBannerNote.classList.add('hidden');
543
- }
621
+ if (byokNote) byokNote.classList.add('hidden');
544
622
  }
545
623
 
546
624
  let renewDateStr = '';
@@ -556,32 +634,28 @@ function renderCreditUsageUI() {
556
634
  renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
557
635
  }
558
636
 
559
- if (elements.valRenewDatetime) {
560
- elements.valRenewDatetime.innerText = renewDateStr;
561
- }
637
+ if (valRenew) valRenew.innerText = renewDateStr;
562
638
  }
563
639
 
564
640
  // Render Paid Tiers UI
565
641
  function renderTiersUI() {
566
642
  const current = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
567
643
 
568
- if (elements.currentPlanTitle) elements.currentPlanTitle.innerText = current.name;
569
- if (elements.currentPlanBadge) elements.currentPlanBadge.innerText = current.badge;
570
- if (elements.currentPlanDesc) elements.currentPlanDesc.innerText = current.desc;
571
- if (elements.tierActiveIcon) elements.tierActiveIcon.innerHTML = current.icon;
644
+ if (getEl('current-plan-title')) getEl('current-plan-title').innerText = current.name;
645
+ if (getEl('current-plan-badge')) getEl('current-plan-badge').innerText = current.badge;
646
+ if (getEl('current-plan-desc')) getEl('current-plan-desc').innerText = current.desc;
647
+ if (getEl('tier-active-icon')) getEl('tier-active-icon').innerHTML = current.icon;
572
648
 
573
- // Billing Cycle Pricing Adjustments
574
649
  const isAnnual = state.billingCycle === 'annual';
575
- if (elements.priceProVal) elements.priceProVal.innerText = isAnnual ? '15' : '19';
576
- if (elements.priceProPeriod) elements.priceProPeriod.innerText = isAnnual ? '/ month (billed annually)' : '/ month';
650
+ if (getEl('price-pro-val')) getEl('price-pro-val').innerText = isAnnual ? '15' : '19';
651
+ if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
577
652
 
578
- if (elements.priceEntVal) elements.priceEntVal.innerText = isAnnual ? '39' : '49';
579
- if (elements.priceEntPeriod) elements.priceEntPeriod.innerText = isAnnual ? '/ month (billed annually)' : '/ month';
653
+ if (getEl('price-ent-val')) getEl('price-ent-val').innerText = isAnnual ? '39' : '49';
654
+ if (getEl('price-ent-period')) getEl('price-ent-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
580
655
 
581
- // Highlight Active Tier Card
582
656
  ['free', 'pro', 'enterprise'].forEach((t) => {
583
- const card = document.getElementById(`tier-card-${t}`);
584
- const btn = document.getElementById(`btn-tier-${t}`);
657
+ const card = getEl(`tier-card-${t}`);
658
+ const btn = getEl(`btn-tier-${t}`);
585
659
  if (!card || !btn) return;
586
660
 
587
661
  if (t === state.currentTier) {
@@ -635,10 +709,6 @@ async function checkServerStatusAndSession() {
635
709
  if (connected && data) {
636
710
  state.serverConnected = true;
637
711
 
638
- if (elements.navTenantName) {
639
- elements.navTenantName.innerText = `CLI Local Sync Active (${data.repoName || 'Workspace'})`;
640
- }
641
-
642
712
  if (data.isLoggedIn && data.auth?.user) {
643
713
  state.user = data.auth.user;
644
714
  state.token = data.auth.token;
@@ -653,19 +723,6 @@ async function checkServerStatusAndSession() {
653
723
  if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
654
724
  if (typeof data.auth.customBaseUrl === 'string') state.customBaseUrl = data.auth.customBaseUrl;
655
725
 
656
- const localCliSession = {
657
- id: 'sess_cli_local',
658
- host: `Local Workstation (${data.repoName || 'FT-CLI'})`,
659
- platform: `Node.js CLI (~/.ft/auth.json)`,
660
- lastActive: data.auth.loggedInAt || new Date().toISOString(),
661
- ip: '127.0.2.1 (Local Sync)',
662
- active: true,
663
- };
664
-
665
- const otherSessions = state.sessions.filter(s => s.id !== 'sess_cli_local');
666
- state.sessions = [localCliSession, ...otherSessions];
667
- saveSessionsToStorage();
668
-
669
726
  saveSessionToStorage();
670
727
  showDashboard();
671
728
  updateUserUI();
@@ -676,10 +733,6 @@ async function checkServerStatusAndSession() {
676
733
  }
677
734
 
678
735
  state.serverConnected = false;
679
- if (elements.navTenantName) {
680
- elements.navTenantName.innerText = FIREBASE_CONFIG.tenantId;
681
- }
682
-
683
736
  checkLocalStorageSession();
684
737
  }
685
738
 
@@ -707,27 +760,31 @@ function checkLocalStorageSession() {
707
760
  // Switch Auth View Forms
708
761
  function switchAuthForm(mode) {
709
762
  if (mode === 'login') {
710
- elements.tabLoginBtn?.classList.add('active');
711
- elements.tabSignupBtn?.classList.remove('active');
712
- elements.loginForm?.classList.remove('hidden');
713
- elements.signupForm?.classList.add('hidden');
763
+ getEl('tab-login-btn')?.classList.add('active');
764
+ getEl('tab-signup-btn')?.classList.remove('active');
765
+ getEl('login-form')?.classList.remove('hidden');
766
+ getEl('signup-form')?.classList.add('hidden');
714
767
  } else {
715
- elements.tabSignupBtn?.classList.add('active');
716
- elements.tabLoginBtn?.classList.remove('active');
717
- elements.signupForm?.classList.remove('hidden');
718
- elements.loginForm?.classList.add('hidden');
768
+ getEl('tab-signup-btn')?.classList.add('active');
769
+ getEl('tab-login-btn')?.classList.remove('active');
770
+ getEl('signup-form')?.classList.remove('hidden');
771
+ getEl('login-form')?.classList.add('hidden');
719
772
  }
720
773
  }
721
774
 
722
775
  // Handle Sign In Submission
723
776
  async function handleSignIn(e) {
724
777
  e.preventDefault();
725
- const email = document.getElementById('login-email').value.trim();
726
- const password = document.getElementById('login-password').value;
727
- const btnSubmit = document.getElementById('btn-submit-login');
778
+ const email = getEl('login-email')?.value.trim();
779
+ const password = getEl('login-password')?.value;
780
+ const btnSubmit = getEl('btn-submit-login');
728
781
 
729
- btnSubmit.disabled = true;
730
- btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
782
+ if (!email || !password) return;
783
+
784
+ if (btnSubmit) {
785
+ btnSubmit.disabled = true;
786
+ btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
787
+ }
731
788
 
732
789
  try {
733
790
  let authRes;
@@ -757,33 +814,42 @@ async function handleSignIn(e) {
757
814
 
758
815
  saveSessionToStorage();
759
816
  syncAuthWithServer();
760
- showDashboard();
761
817
  updateUserUI();
762
818
  showToast(`Signed in successfully as ${state.user.name}!`, 'success');
819
+
820
+ if (getCurrentPage() === '/auth') {
821
+ setTimeout(() => { window.location.href = 'index.html'; }, 400);
822
+ } else {
823
+ showDashboard();
824
+ }
763
825
  } catch (error) {
764
826
  showToast(error.message || 'Login failed.', 'error');
765
827
  } finally {
766
- btnSubmit.disabled = false;
767
- btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
828
+ if (btnSubmit) {
829
+ btnSubmit.disabled = false;
830
+ btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
831
+ }
768
832
  }
769
833
  }
770
834
 
771
835
  // Handle Sign Up Submission
772
836
  async function handleSignUp(e) {
773
837
  e.preventDefault();
774
- const name = document.getElementById('signup-name').value.trim();
775
- const email = document.getElementById('signup-email').value.trim();
776
- const password = document.getElementById('signup-password').value;
777
- const confirmPassword = document.getElementById('signup-confirm-password').value;
778
- const btnSubmit = document.getElementById('btn-submit-signup');
838
+ const name = getEl('signup-name')?.value.trim();
839
+ const email = getEl('signup-email')?.value.trim();
840
+ const password = getEl('signup-password')?.value;
841
+ const confirmPassword = getEl('signup-confirm-password')?.value;
842
+ const btnSubmit = getEl('btn-submit-signup');
779
843
 
780
844
  if (password !== confirmPassword) {
781
845
  showToast('Passwords do not match.', 'error');
782
846
  return;
783
847
  }
784
848
 
785
- btnSubmit.disabled = true;
786
- btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
849
+ if (btnSubmit) {
850
+ btnSubmit.disabled = true;
851
+ btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
852
+ }
787
853
 
788
854
  try {
789
855
  let authRes;
@@ -813,18 +879,24 @@ async function handleSignUp(e) {
813
879
 
814
880
  saveSessionToStorage();
815
881
  syncAuthWithServer();
816
- showDashboard();
817
882
  updateUserUI();
818
883
  showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
884
+
885
+ if (getCurrentPage() === '/auth') {
886
+ setTimeout(() => { window.location.href = 'index.html'; }, 400);
887
+ } else {
888
+ showDashboard();
889
+ }
819
890
  } catch (error) {
820
891
  showToast(error.message || 'Registration failed.', 'error');
821
892
  } finally {
822
- btnSubmit.disabled = false;
823
- btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
893
+ if (btnSubmit) {
894
+ btnSubmit.disabled = false;
895
+ btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
896
+ }
824
897
  }
825
898
  }
826
899
 
827
- // Forgot Password Flow
828
900
  function handleForgotPassword() {
829
901
  const email = prompt('Enter your registered email address for password reset:');
830
902
  if (email && email.includes('@')) {
@@ -834,7 +906,7 @@ function handleForgotPassword() {
834
906
  }
835
907
  }
836
908
 
837
- // Demo Mode Session Launcher
909
+ // Demo Session Launcher
838
910
  function launchDemoSession() {
839
911
  state.token = 'ft_demo_token_' + Date.now().toString(36);
840
912
  state.user = {
@@ -846,9 +918,14 @@ function launchDemoSession() {
846
918
 
847
919
  saveSessionToStorage();
848
920
  syncAuthWithServer();
849
- showDashboard();
850
921
  updateUserUI();
851
922
  showToast('Demo Developer Session Active!', 'info');
923
+
924
+ if (getCurrentPage() === '/auth') {
925
+ setTimeout(() => { window.location.href = 'index.html'; }, 300);
926
+ } else {
927
+ showDashboard();
928
+ }
852
929
  }
853
930
 
854
931
  // Firebase Direct REST API
@@ -920,64 +997,65 @@ function parseFirebaseError(errData, defaultMsg) {
920
997
 
921
998
  // UI State Renderers
922
999
  function showAuthSection() {
923
- elements.authSection?.classList.remove('hidden');
924
- elements.dashboardSection?.classList.add('hidden');
925
- elements.userMenu?.classList.add('hidden');
926
- elements.btnOpenAuth?.classList.remove('hidden');
1000
+ const authSec = getEl('auth-section');
1001
+ const dashSec = getEl('dashboard-section');
1002
+ const userMenu = getEl('user-menu');
1003
+ const btnOpenAuth = getEl('btn-open-auth');
1004
+
1005
+ if (authSec) authSec.classList.remove('hidden');
1006
+ if (dashSec) dashSec.classList.add('hidden');
1007
+ if (userMenu) userMenu.classList.add('hidden');
1008
+ if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
927
1009
  }
928
1010
 
929
1011
  function showDashboard() {
930
- elements.authSection?.classList.add('hidden');
931
- elements.dashboardSection?.classList.remove('hidden');
932
- elements.userMenu?.classList.remove('hidden');
933
- elements.btnOpenAuth?.classList.add('hidden');
1012
+ const authSec = getEl('auth-section');
1013
+ const dashSec = getEl('dashboard-section');
1014
+ const userMenu = getEl('user-menu');
1015
+ const btnOpenAuth = getEl('btn-open-auth');
1016
+
1017
+ if (authSec) authSec.classList.add('hidden');
1018
+ if (dashSec) dashSec.classList.remove('hidden');
1019
+ if (userMenu) userMenu.classList.remove('hidden');
1020
+ if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
934
1021
 
935
1022
  renderAllDynamicComponents();
936
1023
  }
937
1024
 
938
1025
  function updateUserUI() {
939
- if (!state.user) return;
1026
+ if (!state.user) {
1027
+ const userMenu = getEl('user-menu');
1028
+ const btnOpenAuth = getEl('btn-open-auth');
1029
+ if (userMenu) userMenu.classList.add('hidden');
1030
+ if (btnOpenAuth) btnOpenAuth.classList.remove('hidden');
1031
+ return;
1032
+ }
1033
+
1034
+ const userMenu = getEl('user-menu');
1035
+ const btnOpenAuth = getEl('btn-open-auth');
1036
+ if (userMenu) userMenu.classList.remove('hidden');
1037
+ if (btnOpenAuth) btnOpenAuth.classList.add('hidden');
940
1038
 
941
1039
  const name = state.user.name || 'Developer';
942
1040
  const initial = name.charAt(0).toUpperCase();
943
1041
 
944
- if (elements.navAvatar) elements.navAvatar.innerText = initial;
945
- if (elements.navUserName) elements.navUserName.innerText = name;
946
- if (elements.userDisplayName) elements.userDisplayName.innerText = name;
947
- if (elements.dropdownName) elements.dropdownName.innerText = name;
948
- if (elements.dropdownEmail) elements.dropdownEmail.innerText = state.user.email;
1042
+ if (getEl('nav-avatar')) getEl('nav-avatar').innerText = initial;
1043
+ if (getEl('nav-user-name')) getEl('nav-user-name').innerText = name;
1044
+ if (getEl('user-display-name')) getEl('user-display-name').innerText = name;
1045
+ if (getEl('dropdown-name')) getEl('dropdown-name').innerText = name;
1046
+ if (getEl('dropdown-email')) getEl('dropdown-email').innerText = state.user.email;
949
1047
 
950
- if (elements.settingsDisplayName) elements.settingsDisplayName.value = name;
951
- if (elements.settingsEmail) elements.settingsEmail.value = state.user.email;
952
- if (elements.settingsUid) elements.settingsUid.value = state.user.id;
1048
+ if (getEl('settings-display-name')) getEl('settings-display-name').value = name;
1049
+ if (getEl('settings-email')) getEl('settings-email').value = state.user.email;
1050
+ if (getEl('settings-uid')) getEl('settings-uid').value = state.user.id;
953
1051
 
954
1052
  const tierInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
955
- if (elements.settingsActiveTier) elements.settingsActiveTier.value = tierInfo.name;
956
-
957
- if (elements.selectByokProvider) elements.selectByokProvider.value = state.customLlmProvider || 'nvidia';
958
- if (elements.inputByokEndpoint) elements.inputByokEndpoint.value = state.customBaseUrl || '';
959
- if (elements.inputByokKey) elements.inputByokKey.value = state.customApiKey || '';
960
- if (elements.checkboxEnableByok) elements.checkboxEnableByok.checked = Boolean(state.byokEnabled);
961
- }
1053
+ if (getEl('settings-active-tier')) getEl('settings-active-tier').value = tierInfo.name;
962
1054
 
963
- function switchDashboardTab(tabId) {
964
- state.activeTab = tabId;
965
-
966
- document.querySelectorAll('.nav-btn').forEach((btn) => {
967
- if (btn.getAttribute('data-tab') === tabId) {
968
- btn.classList.add('active');
969
- } else {
970
- btn.classList.remove('active');
971
- }
972
- });
973
-
974
- document.querySelectorAll('.tab-pane').forEach((pane) => {
975
- if (pane.id === `tab-${tabId}`) {
976
- pane.classList.add('active');
977
- } else {
978
- pane.classList.remove('active');
979
- }
980
- });
1055
+ if (getEl('select-byok-provider')) getEl('select-byok-provider').value = state.customLlmProvider || 'nvidia';
1056
+ if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
1057
+ if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
1058
+ if (getEl('checkbox-enable-byok')) getEl('checkbox-enable-byok').checked = Boolean(state.byokEnabled);
981
1059
  }
982
1060
 
983
1061
  async function handleLogout() {
@@ -995,8 +1073,8 @@ async function handleLogout() {
995
1073
  localStorage.removeItem('scout_auth_token');
996
1074
  localStorage.removeItem('scout_refresh_token');
997
1075
 
998
- showAuthSection();
999
1076
  showToast('Signed out of Scout session.', 'info');
1077
+ window.location.href = '/auth';
1000
1078
  }
1001
1079
 
1002
1080
  function saveSessionToStorage() {
@@ -1039,23 +1117,24 @@ async function syncAuthWithServer() {
1039
1117
 
1040
1118
  // Copy Utility
1041
1119
  function copyText(text, successMsg = 'Copied to clipboard!') {
1042
- navigator.clipboard.writeText(text).then(
1043
- () => showToast(successMsg, 'success'),
1044
- () => showToast('Failed to copy to clipboard', 'error')
1045
- );
1120
+ navigator.clipboard.writeText(text).then(
1121
+ () => showToast(successMsg, 'success'),
1122
+ () => showToast('Failed to copy to clipboard', 'error')
1123
+ );
1046
1124
  }
1047
1125
 
1048
1126
  document.addEventListener('click', (e) => {
1049
- const copyBtn = e.target.closest('#btn-copy-terminal-cmd');
1050
- if (copyBtn) {
1051
- const cmdText = document.getElementById('terminal-cmd-display')?.innerText || 'npx -y ft-scout@latest';
1052
- copyText(cmdText, 'CLI Command copied to clipboard!');
1053
- }
1127
+ const copyBtn = e.target.closest('#btn-copy-terminal-cmd');
1128
+ if (copyBtn) {
1129
+ const cmdText = getEl('terminal-cmd-display')?.innerText || 'npx -y ft-scout@latest';
1130
+ copyText(cmdText, 'CLI Command copied to clipboard!');
1131
+ }
1054
1132
  });
1055
1133
 
1056
1134
  // Toast System
1057
1135
  function showToast(message, type = 'info') {
1058
- if (!elements.toastContainer) return;
1136
+ const container = getEl('toast-container');
1137
+ if (!container) return;
1059
1138
 
1060
1139
  const toast = document.createElement('div');
1061
1140
  toast.className = `toast toast-${type}`;
@@ -1064,8 +1143,8 @@ function showToast(message, type = 'info') {
1064
1143
  if (type === 'success') iconClass = 'fa-circle-check';
1065
1144
  if (type === 'error') iconClass = 'fa-circle-exclamation';
1066
1145
 
1067
- toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${message}</span>`;
1068
- elements.toastContainer.appendChild(toast);
1146
+ toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${escapeHtml(message)}</span>`;
1147
+ container.appendChild(toast);
1069
1148
 
1070
1149
  setTimeout(() => {
1071
1150
  toast.style.opacity = '0';
@@ -1083,3 +1162,382 @@ function escapeHtml(str) {
1083
1162
  .replace(/>/g, '&gt;')
1084
1163
  .replace(/"/g, '&quot;');
1085
1164
  }
1165
+
1166
+ /* ==========================================================================
1167
+ Interactive 60 FPS Pacman Retro Arcade Game Loop Engine
1168
+ ========================================================================== */
1169
+ function initPacmanGame() {
1170
+ const canvas = getEl('pacman-canvas');
1171
+ if (!canvas) return;
1172
+
1173
+ const ctx = canvas.getContext('2d');
1174
+ const overlay = getEl('arcade-overlay');
1175
+ const overlayTitle = getEl('overlay-title');
1176
+ const overlaySubtitle = getEl('overlay-subtitle');
1177
+ const btnAction = getEl('btn-arcade-action');
1178
+ const scoreValEl = getEl('pacman-score');
1179
+ const highScoreValEl = getEl('pacman-highscore');
1180
+
1181
+ let gameLoopId = null;
1182
+ let score = 0;
1183
+ let highScore = parseInt(localStorage.getItem('scout_pacman_highscore') || '12400', 10);
1184
+ let level = 1;
1185
+ let lives = 3;
1186
+ let gameState = 'STOPPED'; // STOPPED, PLAYING, PAUSED, GAMEOVER
1187
+ let crtEnabled = true;
1188
+ let soundEnabled = true;
1189
+
1190
+ if (highScoreValEl) highScoreValEl.innerText = String(highScore).padStart(5, '0');
1191
+
1192
+ // Classic Grid Map (1: wall, 0: pellet, 2: power pellet, 3: empty)
1193
+ const originalGrid = [
1194
+ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
1195
+ [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
1196
+ [1, 2, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 1],
1197
+ [1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
1198
+ [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
1199
+ [1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1],
1200
+ [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
1201
+ [1, 1, 1, 1, 0, 1, 1, 1, 3, 1, 3, 1, 1, 1, 0, 1, 1, 1, 1],
1202
+ [3, 3, 3, 1, 0, 1, 3, 3, 3, 3, 3, 3, 3, 1, 0, 1, 3, 3, 3],
1203
+ [1, 1, 1, 1, 0, 1, 3, 1, 1, 3, 1, 1, 3, 1, 0, 1, 1, 1, 1],
1204
+ [3, 3, 3, 3, 0, 3, 3, 1, 3, 3, 3, 1, 3, 3, 0, 3, 3, 3, 3],
1205
+ [1, 1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1, 3, 1, 0, 1, 1, 1, 1],
1206
+ [3, 3, 3, 1, 0, 1, 3, 3, 3, 3, 3, 3, 3, 1, 0, 1, 3, 3, 3],
1207
+ [1, 1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1, 3, 1, 0, 1, 1, 1, 1],
1208
+ [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
1209
+ [1, 2, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 2, 1],
1210
+ [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
1211
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1],
1212
+ [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
1213
+ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
1214
+ ];
1215
+
1216
+ let grid = JSON.parse(JSON.stringify(originalGrid));
1217
+ const tileSize = 22;
1218
+ const offsetX = (canvas.width - 19 * tileSize) / 2;
1219
+ const offsetY = (canvas.height - 20 * tileSize) / 2;
1220
+
1221
+ const pacman = {
1222
+ x: 9,
1223
+ y: 14,
1224
+ px: 9 * tileSize + tileSize / 2,
1225
+ py: 14 * tileSize + tileSize / 2,
1226
+ dirX: 0,
1227
+ dirY: 0,
1228
+ nextDirX: 0,
1229
+ nextDirY: 0,
1230
+ speed: 2,
1231
+ mouthAngle: 0.2,
1232
+ mouthSpeed: 0.03,
1233
+ };
1234
+
1235
+ const ghosts = [
1236
+ { name: 'Blinky', color: '#ef4444', x: 9, y: 9, px: 9 * tileSize + tileSize / 2, py: 9 * tileSize + tileSize / 2, dirX: 1, dirY: 0, scared: 0 },
1237
+ { name: 'Pinky', color: '#ec4899', x: 9, y: 10, px: 9 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: -1, dirY: 0, scared: 0 },
1238
+ { name: 'Inky', color: '#38bdf8', x: 8, y: 10, px: 8 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: 0, dirY: -1, scared: 0 },
1239
+ { name: 'Clyde', color: '#fbbf24', x: 10, y: 10, px: 10 * tileSize + tileSize / 2, py: 10 * tileSize + tileSize / 2, dirX: 0, dirY: 1, scared: 0 },
1240
+ ];
1241
+
1242
+ function resetPositions() {
1243
+ pacman.x = 9;
1244
+ pacman.y = 14;
1245
+ pacman.px = 9 * tileSize + tileSize / 2;
1246
+ pacman.py = 14 * tileSize + tileSize / 2;
1247
+ pacman.dirX = 0;
1248
+ pacman.dirY = 0;
1249
+ pacman.nextDirX = 0;
1250
+ pacman.nextDirY = 0;
1251
+
1252
+ ghosts[0].px = 9 * tileSize + tileSize / 2; ghosts[0].py = 8 * tileSize + tileSize / 2; ghosts[0].dirX = 1; ghosts[0].dirY = 0; ghosts[0].scared = 0;
1253
+ ghosts[1].px = 9 * tileSize + tileSize / 2; ghosts[1].py = 10 * tileSize + tileSize / 2; ghosts[1].dirX = -1; ghosts[1].dirY = 0; ghosts[1].scared = 0;
1254
+ ghosts[2].px = 8 * tileSize + tileSize / 2; ghosts[2].py = 10 * tileSize + tileSize / 2; ghosts[2].dirX = 0; ghosts[2].dirY = -1; ghosts[2].scared = 0;
1255
+ ghosts[3].px = 10 * tileSize + tileSize / 2; ghosts[3].py = 10 * tileSize + tileSize / 2; ghosts[3].dirX = 0; ghosts[3].dirY = 1; ghosts[3].scared = 0;
1256
+ }
1257
+
1258
+ function startGame() {
1259
+ score = 0;
1260
+ lives = 3;
1261
+ level = 1;
1262
+ grid = JSON.parse(JSON.stringify(originalGrid));
1263
+ resetPositions();
1264
+ gameState = 'PLAYING';
1265
+ if (overlay) overlay.classList.add('hidden');
1266
+ if (scoreValEl) scoreValEl.innerText = '00000';
1267
+ updateLivesUI();
1268
+ if (!gameLoopId) loop();
1269
+ }
1270
+
1271
+ function togglePause() {
1272
+ if (gameState === 'PLAYING') {
1273
+ gameState = 'PAUSED';
1274
+ if (overlayTitle) overlayTitle.innerText = 'PAUSED';
1275
+ if (overlaySubtitle) overlaySubtitle.innerText = 'Press SPACE to Resume';
1276
+ if (btnAction) btnAction.innerHTML = '<i class="fa-solid fa-play"></i> RESUME';
1277
+ if (overlay) overlay.classList.remove('hidden');
1278
+ } else if (gameState === 'PAUSED') {
1279
+ gameState = 'PLAYING';
1280
+ if (overlay) overlay.classList.add('hidden');
1281
+ }
1282
+ }
1283
+
1284
+ function updateLivesUI() {
1285
+ const livesEl = getEl('pacman-lives');
1286
+ if (livesEl) {
1287
+ livesEl.innerHTML = Array(Math.max(0, lives)).fill('<span class="life-pacman">🟡</span>').join('');
1288
+ }
1289
+ }
1290
+
1291
+ function canMoveTo(gx, gy) {
1292
+ if (gy < 0 || gy >= grid.length || gx < 0 || gx >= grid[0].length) return false;
1293
+ return grid[gy][gx] !== 1;
1294
+ }
1295
+
1296
+ function update() {
1297
+ if (gameState !== 'PLAYING') return;
1298
+
1299
+ // Pacman mouth animation
1300
+ pacman.mouthAngle += pacman.mouthSpeed;
1301
+ if (pacman.mouthAngle > 0.4 || pacman.mouthAngle < 0.05) pacman.mouthSpeed = -pacman.mouthSpeed;
1302
+
1303
+ const currentTileX = Math.floor(pacman.px / tileSize);
1304
+ const currentTileY = Math.floor(pacman.py / tileSize);
1305
+ const centerX = currentTileX * tileSize + tileSize / 2;
1306
+ const centerY = currentTileY * tileSize + tileSize / 2;
1307
+
1308
+ if (Math.abs(pacman.px - centerX) < pacman.speed && Math.abs(pacman.py - centerY) < pacman.speed) {
1309
+ pacman.px = centerX;
1310
+ pacman.py = centerY;
1311
+
1312
+ if (pacman.nextDirX !== 0 || pacman.nextDirY !== 0) {
1313
+ if (canMoveTo(currentTileX + pacman.nextDirX, currentTileY + pacman.nextDirY)) {
1314
+ pacman.dirX = pacman.nextDirX;
1315
+ pacman.dirY = pacman.nextDirY;
1316
+ }
1317
+ }
1318
+
1319
+ if (!canMoveTo(currentTileX + pacman.dirX, currentTileY + pacman.dirY)) {
1320
+ pacman.dirX = 0;
1321
+ pacman.dirY = 0;
1322
+ }
1323
+
1324
+ // Eat Pellets
1325
+ if (grid[currentTileY] && grid[currentTileY][currentTileX] === 0) {
1326
+ grid[currentTileY][currentTileX] = 3;
1327
+ score += 10;
1328
+ if (soundEnabled) playRetroBeep(580, 0.04, 'triangle');
1329
+ } else if (grid[currentTileY] && grid[currentTileY][currentTileX] === 2) {
1330
+ grid[currentTileY][currentTileX] = 3;
1331
+ score += 50;
1332
+ ghosts.forEach(g => g.scared = 300);
1333
+ if (soundEnabled) playRetroBeep(880, 0.12, 'sawtooth');
1334
+ }
1335
+
1336
+ if (score > highScore) {
1337
+ highScore = score;
1338
+ localStorage.setItem('scout_pacman_highscore', String(highScore));
1339
+ if (highScoreValEl) highScoreValEl.innerText = String(highScore).padStart(5, '0');
1340
+ }
1341
+
1342
+ if (scoreValEl) scoreValEl.innerText = String(score).padStart(5, '0');
1343
+ }
1344
+
1345
+ pacman.px += pacman.dirX * pacman.speed;
1346
+ pacman.py += pacman.dirY * pacman.speed;
1347
+
1348
+ // Ghosts move
1349
+ ghosts.forEach((g) => {
1350
+ if (g.scared > 0) g.scared--;
1351
+ const gSpeed = g.scared > 0 ? 1 : 2;
1352
+
1353
+ const gtx = Math.floor(g.px / tileSize);
1354
+ const gty = Math.floor(g.py / tileSize);
1355
+ const gcx = gtx * tileSize + tileSize / 2;
1356
+ const gcy = gty * tileSize + tileSize / 2;
1357
+
1358
+ if (Math.abs(g.px - gcx) < gSpeed && Math.abs(g.py - gcy) < gSpeed) {
1359
+ g.px = gcx;
1360
+ g.py = gcy;
1361
+
1362
+ const allDirs = [
1363
+ { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }
1364
+ ];
1365
+ let validDirs = allDirs.filter(d =>
1366
+ (d.x !== -g.dirX || d.y !== -g.dirY) && canMoveTo(gtx + d.x, gty + d.y)
1367
+ );
1368
+ if (validDirs.length === 0) {
1369
+ validDirs = allDirs.filter(d => canMoveTo(gtx + d.x, gty + d.y));
1370
+ }
1371
+
1372
+ if (validDirs.length > 0) {
1373
+ const chosen = validDirs[Math.floor(Math.random() * validDirs.length)];
1374
+ g.dirX = chosen.x;
1375
+ g.dirY = chosen.y;
1376
+ }
1377
+ }
1378
+
1379
+ g.px += g.dirX * gSpeed;
1380
+ g.py += g.dirY * gSpeed;
1381
+
1382
+ // Collision with Pacman
1383
+ const dist = Math.hypot(pacman.px - g.px, pacman.py - g.py);
1384
+ if (dist < tileSize * 0.85) {
1385
+ if (g.scared > 0) {
1386
+ score += 200;
1387
+ g.px = 9 * tileSize + tileSize / 2;
1388
+ g.py = 8 * tileSize + tileSize / 2;
1389
+ g.scared = 0;
1390
+ if (soundEnabled) playRetroBeep(1200, 0.15, 'sine');
1391
+ } else {
1392
+ lives--;
1393
+ updateLivesUI();
1394
+ if (soundEnabled) playRetroBeep(220, 0.3, 'sawtooth');
1395
+ if (lives <= 0) {
1396
+ gameState = 'GAMEOVER';
1397
+ if (overlayTitle) overlayTitle.innerText = 'GAME OVER';
1398
+ if (overlaySubtitle) overlaySubtitle.innerText = `Final Score: ${score}`;
1399
+ if (btnAction) btnAction.innerHTML = '<i class="fa-solid fa-rotate-right"></i> PLAY AGAIN';
1400
+ if (overlay) overlay.classList.remove('hidden');
1401
+ } else {
1402
+ resetPositions();
1403
+ }
1404
+ }
1405
+ }
1406
+ });
1407
+ }
1408
+
1409
+ function render() {
1410
+ ctx.fillStyle = '#08090c';
1411
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1412
+
1413
+ ctx.save();
1414
+ ctx.translate(offsetX, offsetY);
1415
+
1416
+ // Draw Map Grid
1417
+ for (let r = 0; r < grid.length; r++) {
1418
+ for (let c = 0; c < grid[r].length; c++) {
1419
+ const tile = grid[r][c];
1420
+ const x = c * tileSize;
1421
+ const y = r * tileSize;
1422
+
1423
+ if (tile === 1) {
1424
+ ctx.fillStyle = '#1c202d';
1425
+ ctx.strokeStyle = '#38bdf8';
1426
+ ctx.lineWidth = 1.5;
1427
+ ctx.fillRect(x, y, tileSize, tileSize);
1428
+ ctx.strokeRect(x + 2, y + 2, tileSize - 4, tileSize - 4);
1429
+ } else if (tile === 0) {
1430
+ ctx.fillStyle = '#fbbf24';
1431
+ ctx.beginPath();
1432
+ ctx.arc(x + tileSize / 2, y + tileSize / 2, 3, 0, Math.PI * 2);
1433
+ ctx.fill();
1434
+ } else if (tile === 2) {
1435
+ ctx.fillStyle = '#f8fafc';
1436
+ ctx.beginPath();
1437
+ ctx.arc(x + tileSize / 2, y + tileSize / 2, 7, 0, Math.PI * 2);
1438
+ ctx.fill();
1439
+ }
1440
+ }
1441
+ }
1442
+
1443
+ // Draw Pacman
1444
+ let startAngle = pacman.mouthAngle;
1445
+ let endAngle = Math.PI * 2 - pacman.mouthAngle;
1446
+ if (pacman.dirX === -1) { startAngle += Math.PI; endAngle += Math.PI; }
1447
+ if (pacman.dirY === 1) { startAngle += Math.PI / 2; endAngle += Math.PI / 2; }
1448
+ if (pacman.dirY === -1) { startAngle -= Math.PI / 2; endAngle -= Math.PI / 2; }
1449
+
1450
+ ctx.fillStyle = '#facc15';
1451
+ ctx.beginPath();
1452
+ ctx.arc(pacman.px, pacman.py, tileSize / 2 - 2, startAngle, endAngle);
1453
+ ctx.lineTo(pacman.px, pacman.py);
1454
+ ctx.fill();
1455
+
1456
+ // Draw Ghosts
1457
+ ghosts.forEach((g) => {
1458
+ ctx.fillStyle = g.scared > 0 ? '#3b82f6' : g.color;
1459
+ ctx.beginPath();
1460
+ ctx.arc(g.px, g.py, tileSize / 2 - 2, Math.PI, 0, false);
1461
+ ctx.lineTo(g.px + tileSize / 2 - 2, g.py + tileSize / 2 - 2);
1462
+ ctx.lineTo(g.px - tileSize / 2 + 2, g.py + tileSize / 2 - 2);
1463
+ ctx.fill();
1464
+
1465
+ // Eyes
1466
+ ctx.fillStyle = '#ffffff';
1467
+ ctx.beginPath();
1468
+ ctx.arc(g.px - 3, g.py - 3, 3, 0, Math.PI * 2);
1469
+ ctx.arc(g.px + 3, g.py - 3, 3, 0, Math.PI * 2);
1470
+ ctx.fill();
1471
+
1472
+ ctx.fillStyle = '#1e1b4b';
1473
+ ctx.beginPath();
1474
+ ctx.arc(g.px - 3 + g.dirX * 1.5, g.py - 3 + g.dirY * 1.5, 1.5, 0, Math.PI * 2);
1475
+ ctx.arc(g.px + 3 + g.dirX * 1.5, g.py - 3 + g.dirY * 1.5, 1.5, 0, Math.PI * 2);
1476
+ ctx.fill();
1477
+ });
1478
+
1479
+ ctx.restore();
1480
+ }
1481
+
1482
+ function loop() {
1483
+ update();
1484
+ render();
1485
+ gameLoopId = requestAnimationFrame(loop);
1486
+ }
1487
+
1488
+ // Input Listeners
1489
+ window.addEventListener('keydown', (e) => {
1490
+ const active = document.activeElement;
1491
+ if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.tagName === 'SELECT' || active.isContentEditable)) {
1492
+ return;
1493
+ }
1494
+
1495
+ if (['ArrowUp', 'KeyW'].includes(e.code)) { pacman.nextDirX = 0; pacman.nextDirY = -1; }
1496
+ if (['ArrowDown', 'KeyS'].includes(e.code)) { pacman.nextDirX = 0; pacman.nextDirY = 1; }
1497
+ if (['ArrowLeft', 'KeyA'].includes(e.code)) { pacman.nextDirX = -1; pacman.nextDirY = 0; }
1498
+ if (['ArrowRight', 'KeyD'].includes(e.code)) { pacman.nextDirX = 1; pacman.nextDirY = 0; }
1499
+ if (e.code === 'Space') {
1500
+ e.preventDefault();
1501
+ if (gameState === 'STOPPED' || gameState === 'GAMEOVER') startGame();
1502
+ else togglePause();
1503
+ }
1504
+ });
1505
+
1506
+ btnAction?.addEventListener('click', startGame);
1507
+ getEl('btn-pacman-pause')?.addEventListener('click', togglePause);
1508
+ getEl('btn-pacman-restart')?.addEventListener('click', startGame);
1509
+
1510
+ const handleDir = (dx, dy) => (e) => {
1511
+ if (e.type === 'touchstart') e.preventDefault();
1512
+ pacman.nextDirX = dx;
1513
+ pacman.nextDirY = dy;
1514
+ };
1515
+
1516
+ ['click', 'pointerdown', 'touchstart'].forEach((evt) => {
1517
+ getEl('dpad-up')?.addEventListener(evt, handleDir(0, -1));
1518
+ getEl('dpad-down')?.addEventListener(evt, handleDir(0, 1));
1519
+ getEl('dpad-left')?.addEventListener(evt, handleDir(-1, 0));
1520
+ getEl('dpad-right')?.addEventListener(evt, handleDir(1, 0));
1521
+ });
1522
+
1523
+ getEl('btn-toggle-crt')?.addEventListener('click', () => {
1524
+ const screen = getEl('arcade-screen');
1525
+ if (screen) {
1526
+ crtEnabled = !crtEnabled;
1527
+ screen.classList.toggle('crt-active', crtEnabled);
1528
+ showToast(`CRT Filter ${crtEnabled ? 'ENABLED' : 'DISABLED'}`, 'info');
1529
+ }
1530
+ });
1531
+
1532
+ getEl('btn-toggle-sound')?.addEventListener('click', () => {
1533
+ soundEnabled = !soundEnabled;
1534
+ const btn = getEl('btn-toggle-sound');
1535
+ if (btn) {
1536
+ btn.innerHTML = soundEnabled
1537
+ ? '<i class="fa-solid fa-volume-high text-success"></i> Sound ON'
1538
+ : '<i class="fa-solid fa-volume-xmark text-danger"></i> Sound OFF';
1539
+ }
1540
+ });
1541
+
1542
+ render();
1543
+ }