ft-scout 6.0.9 → 7.0.1

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
@@ -3,443 +3,457 @@
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
6
- apiKey: 'AIzaSyCMVNDgR8EejfXofUy87L7hsKxi3E_C3lg',
7
- projectId: 'frontterrain',
8
- tenantId: 'ft-scout-auth-gc2v5',
9
- baseAuthUrl: 'https://identitytoolkit.googleapis.com/v1/accounts',
6
+ apiKey: 'AIzaSyCMVNDgR8EejfXofUy87L7hsKxi3E_C3lg',
7
+ projectId: 'frontterrain',
8
+ tenantId: 'ft-scout-auth-gc2v5',
9
+ baseAuthUrl: 'https://identitytoolkit.googleapis.com/v1/accounts',
10
10
  };
11
11
 
12
12
  // Tier Configuration Details
13
13
  const TIER_DETAILS = {
14
- free: {
15
- name: 'Developer Free Tier',
16
- badge: 'ACTIVE',
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>',
19
- },
20
- pro: {
21
- name: 'Pro Developer Tier',
22
- badge: 'PRO ACTIVE',
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>',
25
- },
26
- enterprise: {
27
- name: 'Team & Enterprise Tier',
28
- badge: 'ENTERPRISE ACTIVE',
29
- desc: 'Organization controls, security policies & dedicated scale.',
30
- icon: '<i class="fa-solid fa-building text-warning" style="font-size: 24px;"></i>',
31
- },
14
+ free: {
15
+ name: 'Developer Free Tier',
16
+ badge: 'ACTIVE',
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>',
19
+ },
20
+ pro: {
21
+ name: 'Pro Developer Tier',
22
+ badge: 'PRO ACTIVE',
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>',
25
+ },
26
+ enterprise: {
27
+ name: 'Team & Enterprise Tier',
28
+ badge: 'ENTERPRISE ACTIVE',
29
+ desc: 'Organization controls, security policies & dedicated scale.',
30
+ icon: '<i class="fa-solid fa-building text-warning" style="font-size: 24px;"></i>',
31
+ },
32
32
  };
33
33
 
34
34
  // Global Application State
35
35
  let state = {
36
- user: null,
37
- token: null,
38
- refreshToken: null,
39
- activeTab: 'account',
40
- serverConnected: false,
41
- apiBaseUrl: '',
42
- sessions: [],
43
- currentTier: 'free',
44
- billingCycle: 'monthly',
45
- creditsUsed: 0,
46
- creditsTotal: 100,
47
- renewAt: null,
48
- byokEnabled: false,
49
- customLlmProvider: 'gemini',
50
- customApiKey: '',
51
- customBaseUrl: '',
36
+ user: null,
37
+ token: null,
38
+ refreshToken: null,
39
+ activeTab: 'account',
40
+ serverConnected: false,
41
+ apiBaseUrl: '',
42
+ sessions: [],
43
+ currentTier: 'free',
44
+ billingCycle: 'monthly',
45
+ creditsUsed: 0,
46
+ creditsTotal: 100,
47
+ renewAt: null,
48
+ byokEnabled: false,
49
+ customLlmProvider: 'nvidia',
50
+ customApiKey: '',
51
+ customBaseUrl: '',
52
52
  };
53
53
 
54
54
  let pendingCheckoutTier = null;
55
55
 
56
56
  // DOM Elements Reference
57
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'),
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
144
  };
145
145
 
146
146
  // Initialize Application
147
147
  document.addEventListener('DOMContentLoaded', async () => {
148
- loadTierAndCreditsFromStorage();
149
- loadSessionsFromStorage();
150
- initEventListeners();
151
- renderAllDynamicComponents();
152
- await checkServerStatusAndSession();
148
+ loadTierAndCreditsFromStorage();
149
+ loadSessionsFromStorage();
150
+ initEventListeners();
151
+ renderAllDynamicComponents();
152
+ await checkServerStatusAndSession();
153
153
  });
154
154
 
155
155
  // Time Helper Function for Dynamic Relative Timestamps
156
156
  function timeAgo(dateInput) {
157
- if (!dateInput) return 'Just now';
158
- const date = new Date(dateInput);
159
- if (isNaN(date.getTime())) return String(dateInput);
160
-
161
- const now = new Date();
162
- const seconds = Math.floor((now - date) / 1000);
163
- if (seconds < 45) return 'Just now';
164
- const minutes = Math.floor(seconds / 60);
165
- if (minutes < 60) return `${minutes}m ago`;
166
- const hours = Math.floor(minutes / 60);
167
- if (hours < 24) return `${hours}h ago`;
168
- const days = Math.floor(hours / 24);
169
- return `${days}d ago`;
157
+ if (!dateInput) return 'Just now';
158
+ const date = new Date(dateInput);
159
+ if (isNaN(date.getTime())) return String(dateInput);
160
+
161
+ const now = new Date();
162
+ const seconds = Math.floor((now - date) / 1000);
163
+ if (seconds < 45) return 'Just now';
164
+ const minutes = Math.floor(seconds / 60);
165
+ if (minutes < 60) return `${minutes}m ago`;
166
+ const hours = Math.floor(minutes / 60);
167
+ if (hours < 24) return `${hours}h ago`;
168
+ const days = Math.floor(hours / 24);
169
+ return `${days}d ago`;
170
170
  }
171
171
 
172
172
  // Load Subscription Tier & Dynamic Credits from Local Storage / Config
173
173
  function loadTierAndCreditsFromStorage() {
174
- const savedTier = localStorage.getItem('scout_current_tier');
175
- if (savedTier && TIER_DETAILS[savedTier]) {
176
- state.currentTier = savedTier;
177
- }
178
-
179
- 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
- }
185
-
186
- const savedTotal = localStorage.getItem('scout_credits_total');
187
- if (savedTotal !== null) {
188
- state.creditsTotal = parseInt(savedTotal, 10) || 100;
189
- } else {
190
- state.creditsTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
191
- }
192
-
193
- const savedRenew = localStorage.getItem('scout_renew_at');
194
- if (savedRenew) {
195
- state.renewAt = savedRenew;
196
- } else {
197
- const now = new Date();
198
- state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
199
- }
200
-
201
- state.byokEnabled = localStorage.getItem('scout_byok_enabled') === 'true';
202
- state.customLlmProvider = localStorage.getItem('scout_byok_provider') || 'gemini';
203
- state.customApiKey = localStorage.getItem('scout_byok_key') || '';
204
- state.customBaseUrl = localStorage.getItem('scout_byok_endpoint') || '';
174
+ const savedTier = localStorage.getItem('scout_current_tier');
175
+ if (savedTier && TIER_DETAILS[savedTier]) {
176
+ state.currentTier = savedTier;
177
+ }
178
+
179
+ 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
+ }
185
+
186
+ const savedTotal = localStorage.getItem('scout_credits_total');
187
+ if (savedTotal !== null) {
188
+ state.creditsTotal = parseInt(savedTotal, 10) || 100;
189
+ } else {
190
+ state.creditsTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
191
+ }
192
+
193
+ const savedRenew = localStorage.getItem('scout_renew_at');
194
+ if (savedRenew) {
195
+ state.renewAt = savedRenew;
196
+ } else {
197
+ const now = new Date();
198
+ state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
199
+ }
200
+
201
+ state.byokEnabled = localStorage.getItem('scout_byok_enabled') === 'true';
202
+ state.customLlmProvider = localStorage.getItem('scout_byok_provider') || 'nvidia';
203
+ state.customApiKey = localStorage.getItem('scout_byok_key') || '';
204
+ state.customBaseUrl = localStorage.getItem('scout_byok_endpoint') || '';
205
205
  }
206
206
 
207
207
  // Render All Dynamic Components
208
208
  function renderAllDynamicComponents() {
209
- renderSessionsTable();
210
- renderCreditUsageUI();
211
- renderTiersUI();
209
+ renderSessionsTable();
210
+ renderCreditUsageUI();
211
+ renderTiersUI();
212
212
  }
213
213
 
214
214
  // Open / Close Checkout Modal
215
215
  function openCheckoutModal(targetTier) {
216
- pendingCheckoutTier = targetTier;
217
- const isAnnual = state.billingCycle === 'annual';
218
- const tierInfo = TIER_DETAILS[targetTier] || TIER_DETAILS.pro;
219
-
220
- let price = '0';
221
- let quota = '100 Credits / mo';
222
-
223
- if (targetTier === 'pro') {
224
- price = isAnnual ? '15' : '19';
225
- quota = '1,000 Credits / mo';
226
- } else if (targetTier === 'enterprise') {
227
- price = isAnnual ? '39' : '49';
228
- quota = '10,000 Credits / mo';
229
- }
230
-
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;
236
-
237
- if (elements.checkoutModal) elements.checkoutModal.classList.remove('hidden');
216
+ pendingCheckoutTier = targetTier;
217
+ const isAnnual = state.billingCycle === 'annual';
218
+ const tierInfo = TIER_DETAILS[targetTier] || TIER_DETAILS.pro;
219
+
220
+ let price = '0';
221
+ let quota = '100 Credits / mo';
222
+
223
+ if (targetTier === 'pro') {
224
+ price = isAnnual ? '15' : '19';
225
+ quota = '1,000 Credits / mo';
226
+ } else if (targetTier === 'enterprise') {
227
+ price = isAnnual ? '39' : '49';
228
+ quota = '10,000 Credits / mo';
229
+ }
230
+
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;
236
+
237
+ if (elements.checkoutModal) elements.checkoutModal.classList.remove('hidden');
238
238
  }
239
239
 
240
240
  function closeCheckoutModal() {
241
- pendingCheckoutTier = null;
242
- if (elements.checkoutModal) elements.checkoutModal.classList.add('hidden');
241
+ pendingCheckoutTier = null;
242
+ if (elements.checkoutModal) elements.checkoutModal.classList.add('hidden');
243
243
  }
244
244
 
245
245
  // Setup Event Handlers
246
246
  function initEventListeners() {
247
- // Auth Form Tabs Switcher
248
- elements.tabLoginBtn?.addEventListener('click', () => switchAuthForm('login'));
249
- elements.tabSignupBtn?.addEventListener('click', () => switchAuthForm('signup'));
250
-
251
- // Form Submissions
252
- elements.loginForm?.addEventListener('submit', handleSignIn);
253
- elements.signupForm?.addEventListener('submit', handleSignUp);
254
- elements.btnDemoMode?.addEventListener('click', launchDemoSession);
255
-
256
- elements.btnForgotPwd?.addEventListener('click', (e) => {
257
- e.preventDefault();
258
- handleForgotPassword();
259
- });
260
-
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
- // User Dropdown Menu
270
- elements.userMenuToggle?.addEventListener('click', (e) => {
271
- e.stopPropagation();
272
- elements.userDropdown?.classList.toggle('show');
273
- });
274
-
275
- document.addEventListener('click', () => {
276
- elements.userDropdown?.classList.remove('show');
277
- });
278
-
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);
286
-
287
- // Checkout Modal Events
288
- elements.btnCloseCheckout?.addEventListener('click', closeCheckoutModal);
289
- elements.btnCancelCheckout?.addEventListener('click', closeCheckoutModal);
290
-
291
- elements.btnConfirmCheckout?.addEventListener('click', () => {
292
- if (!pendingCheckoutTier) return;
293
-
294
- const targetTier = pendingCheckoutTier;
295
- const tierInfo = TIER_DETAILS[targetTier];
296
- if (!tierInfo) return;
297
-
298
- state.currentTier = targetTier;
299
- state.creditsTotal = targetTier === 'pro' ? 1000 : targetTier === 'enterprise' ? 10000 : 100;
300
-
301
- const now = new Date();
302
- state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
303
-
304
- saveSessionToStorage();
305
- syncAuthWithServer();
306
- closeCheckoutModal();
307
- renderCreditUsageUI();
308
- renderTiersUI();
309
- updateUserUI();
310
- showToast(`Subscription activated! Welcome to ${tierInfo.name}. Quota upgraded to ${state.creditsTotal.toLocaleString()} credits.`, 'success');
311
- });
312
-
313
- // Password Visibility Toggle Buttons
314
- document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
315
- btn.addEventListener('click', () => {
316
- const targetId = btn.getAttribute('data-target');
317
- const input = document.getElementById(targetId);
318
- if (input) {
319
- const isPwd = input.type === 'password';
320
- input.type = isPwd ? 'text' : 'password';
321
- btn.innerHTML = isPwd ? '<i class="fa-solid fa-eye-slash"></i>' : '<i class="fa-solid fa-eye"></i>';
322
- }
323
- });
324
- });
325
-
326
- // Profile Form Submission
327
- elements.profileForm?.addEventListener('submit', (e) => {
328
- e.preventDefault();
329
- const newName = elements.settingsDisplayName.value.trim();
330
- if (newName && state.user) {
331
- state.user.name = newName;
332
- saveSessionToStorage();
333
- syncAuthWithServer();
334
- updateUserUI();
335
- showToast('Profile display name updated successfully!', 'success');
336
- }
337
- });
338
-
339
- // BYOK & Custom Proxy Form Submission
340
- elements.byokForm?.addEventListener('submit', (e) => {
341
- e.preventDefault();
342
- state.customLlmProvider = elements.selectByokProvider?.value || 'gemini';
343
- state.customBaseUrl = elements.inputByokEndpoint?.value.trim() || '';
344
- state.customApiKey = elements.inputByokKey?.value.trim() || '';
345
- state.byokEnabled = elements.checkboxEnableByok?.checked || false;
346
-
347
- localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
348
- localStorage.setItem('scout_byok_provider', state.customLlmProvider);
349
- localStorage.setItem('scout_byok_key', state.customApiKey);
350
- localStorage.setItem('scout_byok_endpoint', state.customBaseUrl);
351
-
352
- syncAuthWithServer();
353
- renderCreditUsageUI();
354
- updateUserUI();
355
-
356
- if (state.byokEnabled) {
357
- showToast('BYOK Mode Active! Requests now route through your custom API key/proxy endpoint (Scout credits bypassed).', 'success');
358
- } else {
359
- showToast('Endpoint configuration saved. Using standard Scout key.', 'info');
360
- }
361
- });
362
-
363
- elements.checkboxEnableByok?.addEventListener('change', (e) => {
364
- state.byokEnabled = e.target.checked;
365
- localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
366
- syncAuthWithServer();
367
- renderCreditUsageUI();
368
- });
369
-
370
- // Billing Cycle Toggle Switcher
371
- elements.toggleBillingCycle?.addEventListener('change', (e) => {
372
- state.billingCycle = e.target.checked ? 'annual' : 'monthly';
373
- renderTiersUI();
374
- });
375
-
376
- // Tier Selection Buttons
377
- document.querySelectorAll('.btn-select-tier').forEach((btn) => {
378
- btn.addEventListener('click', () => {
379
- const targetTier = btn.getAttribute('data-tier');
380
- if (!targetTier || !TIER_DETAILS[targetTier]) return;
381
-
382
- if (targetTier === 'free') {
383
- state.currentTier = 'free';
384
- state.creditsTotal = 100;
385
- saveSessionToStorage();
386
- syncAuthWithServer();
387
- renderCreditUsageUI();
388
- renderTiersUI();
389
- updateUserUI();
390
- showToast('Switched to Developer Free Tier.', 'info');
391
- } else {
392
- openCheckoutModal(targetTier);
393
- }
394
- });
395
- });
247
+ // Auth Form Tabs Switcher
248
+ elements.tabLoginBtn?.addEventListener('click', () => switchAuthForm('login'));
249
+ elements.tabSignupBtn?.addEventListener('click', () => switchAuthForm('signup'));
250
+
251
+ // Form Submissions
252
+ elements.loginForm?.addEventListener('submit', handleSignIn);
253
+ elements.signupForm?.addEventListener('submit', handleSignUp);
254
+ elements.btnDemoMode?.addEventListener('click', launchDemoSession);
255
+
256
+ elements.btnForgotPwd?.addEventListener('click', (e) => {
257
+ e.preventDefault();
258
+ handleForgotPassword();
259
+ });
260
+
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
+ // User Dropdown Menu
270
+ elements.userMenuToggle?.addEventListener('click', (e) => {
271
+ e.stopPropagation();
272
+ elements.userDropdown?.classList.toggle('show');
273
+ });
274
+
275
+ document.addEventListener('click', () => {
276
+ elements.userDropdown?.classList.remove('show');
277
+ });
278
+
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);
286
+
287
+ // Checkout Modal Events
288
+ elements.btnCloseCheckout?.addEventListener('click', closeCheckoutModal);
289
+ elements.btnCancelCheckout?.addEventListener('click', closeCheckoutModal);
290
+
291
+ elements.btnConfirmCheckout?.addEventListener('click', () => {
292
+ if (!pendingCheckoutTier) return;
293
+
294
+ const targetTier = pendingCheckoutTier;
295
+ const tierInfo = TIER_DETAILS[targetTier];
296
+ if (!tierInfo) return;
297
+
298
+ state.currentTier = targetTier;
299
+ state.creditsTotal = targetTier === 'pro' ? 1000 : targetTier === 'enterprise' ? 10000 : 100;
300
+
301
+ const now = new Date();
302
+ state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
303
+
304
+ saveSessionToStorage();
305
+ syncAuthWithServer();
306
+ closeCheckoutModal();
307
+ renderCreditUsageUI();
308
+ renderTiersUI();
309
+ updateUserUI();
310
+ showToast(`Subscription activated! Welcome to ${tierInfo.name}. Quota upgraded to ${state.creditsTotal.toLocaleString()} credits.`, 'success');
311
+ });
312
+
313
+ // Password Visibility Toggle Buttons
314
+ document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
315
+ btn.addEventListener('click', () => {
316
+ const targetId = btn.getAttribute('data-target');
317
+ const input = document.getElementById(targetId);
318
+ if (input) {
319
+ const isPwd = input.type === 'password';
320
+ input.type = isPwd ? 'text' : 'password';
321
+ btn.innerHTML = isPwd ? '<i class="fa-solid fa-eye-slash"></i>' : '<i class="fa-solid fa-eye"></i>';
322
+ }
323
+ });
324
+ });
325
+
326
+ // Profile Form Submission
327
+ elements.profileForm?.addEventListener('submit', (e) => {
328
+ e.preventDefault();
329
+ const newName = elements.settingsDisplayName.value.trim();
330
+ if (newName && state.user) {
331
+ state.user.name = newName;
332
+ saveSessionToStorage();
333
+ syncAuthWithServer();
334
+ updateUserUI();
335
+ showToast('Profile display name updated successfully!', 'success');
336
+ }
337
+ });
338
+
339
+ elements.selectByokProvider?.addEventListener('change', (e) => {
340
+ const provider = e.target.value;
341
+ const defaultEndpoints = {
342
+ nvidia: 'https://integrate.api.nvidia.com/v1',
343
+ groq: 'https://api.groq.com/openai/v1',
344
+ gemini: 'https://generativelanguage.googleapis.com/v1beta/openai',
345
+ openai: 'https://api.openai.com/v1',
346
+ custom: 'http://localhost:11434/v1',
347
+ };
348
+ if (elements.inputByokEndpoint && defaultEndpoints[provider]) {
349
+ elements.inputByokEndpoint.value = defaultEndpoints[provider];
350
+ }
351
+ });
352
+
353
+ // BYOK & Custom Proxy Form Submission
354
+ elements.byokForm?.addEventListener('submit', (e) => {
355
+ 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;
360
+
361
+ localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
362
+ localStorage.setItem('scout_byok_provider', state.customLlmProvider);
363
+ localStorage.setItem('scout_byok_key', state.customApiKey);
364
+ localStorage.setItem('scout_byok_endpoint', state.customBaseUrl);
365
+
366
+ syncAuthWithServer();
367
+ renderCreditUsageUI();
368
+ updateUserUI();
369
+
370
+ if (state.byokEnabled) {
371
+ showToast('BYOK Mode Active! Requests now route through your custom API key/proxy endpoint (Scout credits bypassed).', 'success');
372
+ } else {
373
+ showToast('Endpoint configuration saved. Using standard Scout key.', 'info');
374
+ }
375
+ });
376
+
377
+ elements.checkboxEnableByok?.addEventListener('change', (e) => {
378
+ state.byokEnabled = e.target.checked;
379
+ localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
380
+ syncAuthWithServer();
381
+ renderCreditUsageUI();
382
+ });
383
+
384
+ // Billing Cycle Toggle Switcher
385
+ elements.toggleBillingCycle?.addEventListener('change', (e) => {
386
+ state.billingCycle = e.target.checked ? 'annual' : 'monthly';
387
+ renderTiersUI();
388
+ });
389
+
390
+ // Tier Selection Buttons
391
+ document.querySelectorAll('.btn-select-tier').forEach((btn) => {
392
+ btn.addEventListener('click', () => {
393
+ const targetTier = btn.getAttribute('data-tier');
394
+ if (!targetTier || !TIER_DETAILS[targetTier]) return;
395
+
396
+ if (targetTier === 'free') {
397
+ state.currentTier = 'free';
398
+ state.creditsTotal = 100;
399
+ saveSessionToStorage();
400
+ syncAuthWithServer();
401
+ renderCreditUsageUI();
402
+ renderTiersUI();
403
+ updateUserUI();
404
+ showToast('Switched to Developer Free Tier.', 'info');
405
+ } else {
406
+ openCheckoutModal(targetTier);
407
+ }
408
+ });
409
+ });
396
410
  }
397
411
 
398
412
  // Data Storage & Session Loaders
399
413
  function loadSessionsFromStorage() {
400
- try {
401
- const stored = localStorage.getItem('scout_cli_sessions');
402
- if (stored) {
403
- state.sessions = JSON.parse(stored);
404
- } else {
405
- state.sessions = [
406
- {
407
- id: 'sess_local_1',
408
- host: 'Local Workstation (FT-CLI)',
409
- platform: 'Windows x64 Node.js',
410
- lastActive: new Date().toISOString(),
411
- ip: '127.0.0.1',
412
- active: true,
413
- },
414
- ];
415
- saveSessionsToStorage();
416
- }
417
- } catch {
418
- state.sessions = [];
419
- }
414
+ try {
415
+ const stored = localStorage.getItem('scout_cli_sessions');
416
+ if (stored) {
417
+ state.sessions = JSON.parse(stored);
418
+ } else {
419
+ state.sessions = [
420
+ {
421
+ id: 'sess_local_1',
422
+ host: 'Local Workstation (FT-CLI)',
423
+ platform: 'Windows x64 Node.js',
424
+ lastActive: new Date().toISOString(),
425
+ ip: '127.0.1.1',
426
+ active: true,
427
+ },
428
+ ];
429
+ saveSessionsToStorage();
430
+ }
431
+ } catch {
432
+ state.sessions = [];
433
+ }
420
434
  }
421
435
 
422
436
  function saveSessionsToStorage() {
423
- try {
424
- localStorage.setItem('scout_cli_sessions', JSON.stringify(state.sessions));
425
- } catch {}
437
+ try {
438
+ localStorage.setItem('scout_cli_sessions', JSON.stringify(state.sessions));
439
+ } catch { }
426
440
  }
427
441
 
428
442
  function renderSessionsTable() {
429
- if (!elements.sessionTableBody) return;
430
-
431
- if (!state.sessions || state.sessions.length === 0) {
432
- elements.sessionTableBody.innerHTML = `
443
+ if (!elements.sessionTableBody) return;
444
+
445
+ if (!state.sessions || state.sessions.length === 0) {
446
+ elements.sessionTableBody.innerHTML = `
433
447
  <tr>
434
448
  <td colspan="4" class="empty-table-notice">No active CLI authorization sessions found.</td>
435
449
  </tr>
436
450
  `;
437
- return;
438
- }
451
+ return;
452
+ }
439
453
 
440
- elements.sessionTableBody.innerHTML = state.sessions
441
- .map((sess) => {
442
- return `
454
+ elements.sessionTableBody.innerHTML = state.sessions
455
+ .map((sess) => {
456
+ return `
443
457
  <tr data-id="${escapeHtml(sess.id)}">
444
458
  <td>
445
459
  <div class="flex-center-gap">
@@ -455,609 +469,617 @@ function renderSessionsTable() {
455
469
  <td><button class="btn btn-xs btn-outline text-danger btn-revoke-session" onclick="revokeSession('${escapeHtml(sess.id)}')"><i class="fa-solid fa-trash"></i> Revoke</button></td>
456
470
  </tr>
457
471
  `;
458
- })
459
- .join('');
472
+ })
473
+ .join('');
460
474
  }
461
475
 
462
476
  function revokeSession(sessionId) {
463
- state.sessions = state.sessions.filter(s => s.id !== sessionId);
464
- saveSessionsToStorage();
465
- renderSessionsTable();
466
- showToast('CLI access session revoked.', 'info');
477
+ state.sessions = state.sessions.filter(s => s.id !== sessionId);
478
+ saveSessionsToStorage();
479
+ renderSessionsTable();
480
+ showToast('CLI access session revoked.', 'info');
467
481
  }
468
482
 
469
483
  // Render Dynamic Credit Usage & BYOK Status UI
470
484
  function renderCreditUsageUI() {
471
- if (state.byokEnabled) {
472
- if (elements.byokStatusBadge) {
473
- elements.byokStatusBadge.innerText = 'BYOK Mode Active';
474
- elements.byokStatusBadge.className = 'badge badge-accent';
475
- }
476
- if (elements.creditStatusBadge) {
477
- elements.creditStatusBadge.innerText = 'Custom Endpoint Active';
478
- elements.creditStatusBadge.className = 'badge badge-accent';
479
- }
480
- if (elements.valCreditsUsed) elements.valCreditsUsed.innerText = 'Unmonitored';
481
- if (elements.valCreditsRemaining) elements.valCreditsRemaining.innerText = 'Unlimited (BYOK)';
482
- if (elements.valCreditsTotal) elements.valCreditsTotal.innerText = 'Custom Provider';
483
- if (elements.valCreditsPct) elements.valCreditsPct.innerText = 'BYOK';
484
-
485
- if (elements.creditProgressFill) {
486
- elements.creditProgressFill.style.width = '100%';
487
- elements.creditProgressFill.style.background = 'linear-gradient(90deg, #a855f7 0%, #ec4899 100%)';
488
- }
489
-
490
- if (elements.labelCreditConsumption) {
491
- elements.labelCreditConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
492
- }
493
-
494
- if (elements.byokBannerNote) {
495
- elements.byokBannerNote.classList.remove('hidden');
496
- }
497
- } else {
498
- if (elements.byokStatusBadge) {
499
- elements.byokStatusBadge.innerText = 'Scout Default';
500
- elements.byokStatusBadge.className = 'badge';
501
- }
502
- if (elements.creditStatusBadge) {
503
- elements.creditStatusBadge.innerText = 'Quota Active';
504
- elements.creditStatusBadge.className = 'badge badge-success';
505
- }
506
-
507
- const defaultTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
508
- const total = state.creditsTotal || defaultTotal;
509
- const used = state.creditsUsed || 0;
510
- const remaining = Math.max(0, total - used);
511
- const pct = Math.min(100, Math.round((used / total) * 100));
512
-
513
- if (elements.valCreditsUsed) elements.valCreditsUsed.innerText = used.toLocaleString();
514
- if (elements.valCreditsRemaining) elements.valCreditsRemaining.innerText = remaining.toLocaleString();
515
- if (elements.valCreditsTotal) elements.valCreditsTotal.innerText = total.toLocaleString();
516
- if (elements.valCreditsPct) elements.valCreditsPct.innerText = `${pct}%`;
517
-
518
- if (elements.creditProgressFill) {
519
- elements.creditProgressFill.style.width = `${pct}%`;
520
- elements.creditProgressFill.style.background = 'linear-gradient(90deg, #6366f1 0%, #38bdf8 60%, #34d399 100%)';
521
- }
522
-
523
- if (elements.labelCreditConsumption) {
524
- elements.labelCreditConsumption.innerText = 'Monthly Credit Consumption';
525
- }
526
-
527
- if (elements.byokBannerNote) {
528
- elements.byokBannerNote.classList.add('hidden');
529
- }
530
- }
531
-
532
- let renewDateStr = '';
533
- if (state.renewAt) {
534
- const d = new Date(state.renewAt);
535
- if (!isNaN(d.getTime())) {
536
- renewDateStr = `${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
537
- }
538
- }
539
- if (!renewDateStr) {
540
- const now = new Date();
541
- const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
542
- renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
543
- }
544
-
545
- if (elements.valRenewDatetime) {
546
- elements.valRenewDatetime.innerText = renewDateStr;
547
- }
485
+ if (state.byokEnabled) {
486
+ if (elements.byokStatusBadge) {
487
+ elements.byokStatusBadge.innerText = 'BYOK Mode Active';
488
+ elements.byokStatusBadge.className = 'badge badge-accent';
489
+ }
490
+ if (elements.creditStatusBadge) {
491
+ elements.creditStatusBadge.innerText = 'Custom Endpoint Active';
492
+ elements.creditStatusBadge.className = 'badge badge-accent';
493
+ }
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%)';
502
+ }
503
+
504
+ if (elements.labelCreditConsumption) {
505
+ elements.labelCreditConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
506
+ }
507
+
508
+ if (elements.byokBannerNote) {
509
+ elements.byokBannerNote.classList.remove('hidden');
510
+ }
511
+ } else {
512
+ if (elements.byokStatusBadge) {
513
+ elements.byokStatusBadge.innerText = 'Scout Default';
514
+ elements.byokStatusBadge.className = 'badge';
515
+ }
516
+ if (elements.creditStatusBadge) {
517
+ elements.creditStatusBadge.innerText = 'Quota Active';
518
+ elements.creditStatusBadge.className = 'badge badge-success';
519
+ }
520
+
521
+ const defaultTotal = state.currentTier === 'pro' ? 1000 : state.currentTier === 'enterprise' ? 10000 : 100;
522
+ const total = state.creditsTotal || defaultTotal;
523
+ const used = state.creditsUsed || 0;
524
+ const remaining = Math.max(0, total - used);
525
+ const pct = Math.min(100, Math.round((used / total) * 100));
526
+
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}%`;
531
+
532
+ if (elements.creditProgressFill) {
533
+ elements.creditProgressFill.style.width = `${pct}%`;
534
+ elements.creditProgressFill.style.background = 'linear-gradient(90deg, #6366f1 0%, #38bdf8 60%, #34d399 100%)';
535
+ }
536
+
537
+ if (elements.labelCreditConsumption) {
538
+ elements.labelCreditConsumption.innerText = 'Monthly Credit Consumption';
539
+ }
540
+
541
+ if (elements.byokBannerNote) {
542
+ elements.byokBannerNote.classList.add('hidden');
543
+ }
544
+ }
545
+
546
+ let renewDateStr = '';
547
+ if (state.renewAt) {
548
+ const d = new Date(state.renewAt);
549
+ if (!isNaN(d.getTime())) {
550
+ renewDateStr = `${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
551
+ }
552
+ }
553
+ if (!renewDateStr) {
554
+ const now = new Date();
555
+ const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
556
+ renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
557
+ }
558
+
559
+ if (elements.valRenewDatetime) {
560
+ elements.valRenewDatetime.innerText = renewDateStr;
561
+ }
548
562
  }
549
563
 
550
564
  // Render Paid Tiers UI
551
565
  function renderTiersUI() {
552
- const current = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
553
-
554
- if (elements.currentPlanTitle) elements.currentPlanTitle.innerText = current.name;
555
- if (elements.currentPlanBadge) elements.currentPlanBadge.innerText = current.badge;
556
- if (elements.currentPlanDesc) elements.currentPlanDesc.innerText = current.desc;
557
- if (elements.tierActiveIcon) elements.tierActiveIcon.innerHTML = current.icon;
558
-
559
- // Billing Cycle Pricing Adjustments
560
- const isAnnual = state.billingCycle === 'annual';
561
- if (elements.priceProVal) elements.priceProVal.innerText = isAnnual ? '15' : '19';
562
- if (elements.priceProPeriod) elements.priceProPeriod.innerText = isAnnual ? '/ month (billed annually)' : '/ month';
563
-
564
- if (elements.priceEntVal) elements.priceEntVal.innerText = isAnnual ? '39' : '49';
565
- if (elements.priceEntPeriod) elements.priceEntPeriod.innerText = isAnnual ? '/ month (billed annually)' : '/ month';
566
-
567
- // Highlight Active Tier Card
568
- ['free', 'pro', 'enterprise'].forEach((t) => {
569
- const card = document.getElementById(`tier-card-${t}`);
570
- const btn = document.getElementById(`btn-tier-${t}`);
571
- if (!card || !btn) return;
572
-
573
- if (t === state.currentTier) {
574
- card.classList.add('active-tier-card');
575
- btn.className = 'btn btn-success btn-block btn-select-tier';
576
- btn.innerText = 'Current Plan';
577
- btn.disabled = true;
578
- } else {
579
- card.classList.remove('active-tier-card');
580
- btn.disabled = false;
581
- if (t === 'pro') {
582
- btn.className = 'btn btn-primary btn-block btn-select-tier';
583
- btn.innerText = 'Upgrade to Pro';
584
- } else {
585
- btn.className = 'btn btn-outline btn-block btn-select-tier';
586
- btn.innerText = t === 'enterprise' ? 'Upgrade to Enterprise' : 'Switch to Free';
587
- }
588
- }
589
- });
566
+ const current = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
567
+
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;
572
+
573
+ // Billing Cycle Pricing Adjustments
574
+ 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';
577
+
578
+ if (elements.priceEntVal) elements.priceEntVal.innerText = isAnnual ? '39' : '49';
579
+ if (elements.priceEntPeriod) elements.priceEntPeriod.innerText = isAnnual ? '/ month (billed annually)' : '/ month';
580
+
581
+ // Highlight Active Tier Card
582
+ ['free', 'pro', 'enterprise'].forEach((t) => {
583
+ const card = document.getElementById(`tier-card-${t}`);
584
+ const btn = document.getElementById(`btn-tier-${t}`);
585
+ if (!card || !btn) return;
586
+
587
+ if (t === state.currentTier) {
588
+ card.classList.add('active-tier-card');
589
+ btn.className = 'btn btn-success btn-block btn-select-tier';
590
+ btn.innerText = 'Current Plan';
591
+ btn.disabled = true;
592
+ } else {
593
+ card.classList.remove('active-tier-card');
594
+ btn.disabled = false;
595
+ if (t === 'pro') {
596
+ btn.className = 'btn btn-primary btn-block btn-select-tier';
597
+ btn.innerText = 'Upgrade to Pro';
598
+ } else {
599
+ btn.className = 'btn btn-outline btn-block btn-select-tier';
600
+ btn.innerText = t === 'enterprise' ? 'Upgrade to Enterprise' : 'Switch to Free';
601
+ }
602
+ }
603
+ });
590
604
  }
591
605
 
592
606
  // Sync with Local Server CLI Session
593
607
  async function checkServerStatusAndSession() {
594
- let connected = false;
595
- let data = null;
596
-
597
- try {
598
- const res = await fetch('/api/status', { method: 'GET' });
599
- if (res.ok) {
600
- data = await res.json();
601
- if (data && data.status === 'ok') {
602
- state.apiBaseUrl = '';
603
- connected = true;
604
- }
605
- }
606
- } catch {}
607
-
608
- if (!connected) {
609
- try {
610
- const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
611
- if (res.ok) {
612
- data = await res.json();
613
- if (data && data.status === 'ok') {
614
- state.apiBaseUrl = 'http://localhost:9120';
615
- connected = true;
616
- }
617
- }
618
- } catch {}
619
- }
620
-
621
- if (connected && data) {
622
- state.serverConnected = true;
623
-
624
- if (elements.navTenantName) {
625
- elements.navTenantName.innerText = `CLI Local Sync Active (${data.repoName || 'Workspace'})`;
626
- }
627
-
628
- if (data.isLoggedIn && data.auth?.user) {
629
- state.user = data.auth.user;
630
- state.token = data.auth.token;
631
- state.refreshToken = data.auth.refreshToken;
632
-
633
- if (typeof data.auth.creditsUsed === 'number') state.creditsUsed = data.auth.creditsUsed;
634
- if (typeof data.auth.creditsTotal === 'number') state.creditsTotal = data.auth.creditsTotal;
635
- if (data.auth.currentTier) state.currentTier = data.auth.currentTier;
636
- if (data.auth.renewAt) state.renewAt = data.auth.renewAt;
637
- if (typeof data.auth.byokEnabled === 'boolean') state.byokEnabled = data.auth.byokEnabled;
638
- if (data.auth.customLlmProvider) state.customLlmProvider = data.auth.customLlmProvider;
639
- if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
640
- if (typeof data.auth.customBaseUrl === 'string') state.customBaseUrl = data.auth.customBaseUrl;
641
-
642
- const localCliSession = {
643
- id: 'sess_cli_local',
644
- host: `Local Workstation (${data.repoName || 'FT-CLI'})`,
645
- platform: `Node.js CLI (~/.ft/auth.json)`,
646
- lastActive: data.auth.loggedInAt || new Date().toISOString(),
647
- ip: '127.0.0.1 (Local Sync)',
648
- active: true,
649
- };
650
-
651
- const otherSessions = state.sessions.filter(s => s.id !== 'sess_cli_local');
652
- state.sessions = [localCliSession, ...otherSessions];
653
- saveSessionsToStorage();
654
-
655
- saveSessionToStorage();
656
- showDashboard();
657
- updateUserUI();
658
- renderCreditUsageUI();
659
- renderSessionsTable();
660
- return;
661
- }
662
- }
663
-
664
- state.serverConnected = false;
665
- if (elements.navTenantName) {
666
- elements.navTenantName.innerText = FIREBASE_CONFIG.tenantId;
667
- }
668
-
669
- checkLocalStorageSession();
608
+ let connected = false;
609
+ let data = null;
610
+
611
+ try {
612
+ const res = await fetch('/api/status', { method: 'GET' });
613
+ if (res.ok) {
614
+ data = await res.json();
615
+ if (data && data.status === 'ok') {
616
+ state.apiBaseUrl = '';
617
+ connected = true;
618
+ }
619
+ }
620
+ } catch { }
621
+
622
+ if (!connected) {
623
+ try {
624
+ const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
625
+ if (res.ok) {
626
+ data = await res.json();
627
+ if (data && data.status === 'ok') {
628
+ state.apiBaseUrl = 'http://localhost:9120';
629
+ connected = true;
630
+ }
631
+ }
632
+ } catch { }
633
+ }
634
+
635
+ if (connected && data) {
636
+ state.serverConnected = true;
637
+
638
+ if (elements.navTenantName) {
639
+ elements.navTenantName.innerText = `CLI Local Sync Active (${data.repoName || 'Workspace'})`;
640
+ }
641
+
642
+ if (data.isLoggedIn && data.auth?.user) {
643
+ state.user = data.auth.user;
644
+ state.token = data.auth.token;
645
+ state.refreshToken = data.auth.refreshToken;
646
+
647
+ if (typeof data.auth.creditsUsed === 'number') state.creditsUsed = data.auth.creditsUsed;
648
+ if (typeof data.auth.creditsTotal === 'number') state.creditsTotal = data.auth.creditsTotal;
649
+ if (data.auth.currentTier) state.currentTier = data.auth.currentTier;
650
+ if (data.auth.renewAt) state.renewAt = data.auth.renewAt;
651
+ if (typeof data.auth.byokEnabled === 'boolean') state.byokEnabled = data.auth.byokEnabled;
652
+ if (data.auth.customLlmProvider) state.customLlmProvider = data.auth.customLlmProvider;
653
+ if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
654
+ if (typeof data.auth.customBaseUrl === 'string') state.customBaseUrl = data.auth.customBaseUrl;
655
+
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.1.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
+ saveSessionToStorage();
670
+ showDashboard();
671
+ updateUserUI();
672
+ renderCreditUsageUI();
673
+ renderSessionsTable();
674
+ return;
675
+ }
676
+ }
677
+
678
+ state.serverConnected = false;
679
+ if (elements.navTenantName) {
680
+ elements.navTenantName.innerText = FIREBASE_CONFIG.tenantId;
681
+ }
682
+
683
+ checkLocalStorageSession();
670
684
  }
671
685
 
672
686
  function checkLocalStorageSession() {
673
- try {
674
- const storedUser = localStorage.getItem('scout_auth_user');
675
- const storedToken = localStorage.getItem('scout_auth_token');
676
-
677
- if (storedUser && storedToken) {
678
- state.user = JSON.parse(storedUser);
679
- state.token = storedToken;
680
- state.refreshToken = localStorage.getItem('scout_refresh_token') || '';
681
-
682
- showDashboard();
683
- updateUserUI();
684
- return;
685
- }
686
- } catch (err) {
687
- console.error('Saved session parse error:', err);
688
- }
689
-
690
- showAuthSection();
687
+ try {
688
+ const storedUser = localStorage.getItem('scout_auth_user');
689
+ const storedToken = localStorage.getItem('scout_auth_token');
690
+
691
+ if (storedUser && storedToken) {
692
+ state.user = JSON.parse(storedUser);
693
+ state.token = storedToken;
694
+ state.refreshToken = localStorage.getItem('scout_refresh_token') || '';
695
+
696
+ showDashboard();
697
+ updateUserUI();
698
+ return;
699
+ }
700
+ } catch (err) {
701
+ console.error('Saved session parse error:', err);
702
+ }
703
+
704
+ showAuthSection();
691
705
  }
692
706
 
693
707
  // Switch Auth View Forms
694
708
  function switchAuthForm(mode) {
695
- if (mode === 'login') {
696
- elements.tabLoginBtn?.classList.add('active');
697
- elements.tabSignupBtn?.classList.remove('active');
698
- elements.loginForm?.classList.remove('hidden');
699
- elements.signupForm?.classList.add('hidden');
700
- } else {
701
- elements.tabSignupBtn?.classList.add('active');
702
- elements.tabLoginBtn?.classList.remove('active');
703
- elements.signupForm?.classList.remove('hidden');
704
- elements.loginForm?.classList.add('hidden');
705
- }
709
+ 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');
714
+ } 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');
719
+ }
706
720
  }
707
721
 
708
722
  // Handle Sign In Submission
709
723
  async function handleSignIn(e) {
710
- e.preventDefault();
711
- const email = document.getElementById('login-email').value.trim();
712
- const password = document.getElementById('login-password').value;
713
- const btnSubmit = document.getElementById('btn-submit-login');
714
-
715
- btnSubmit.disabled = true;
716
- btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
717
-
718
- try {
719
- let authRes;
720
- if (state.serverConnected) {
721
- const res = await fetch(`${state.apiBaseUrl}/api/login`, {
722
- method: 'POST',
723
- headers: { 'Content-Type': 'application/json' },
724
- body: JSON.stringify({ email, password }),
725
- });
726
- const data = await res.json();
727
- if (!res.ok || !data.success) {
728
- throw new Error(data.error || 'Server authentication failed.');
729
- }
730
- authRes = data.auth;
731
- } else {
732
- authRes = await apiFirebaseSignIn(email, password);
733
- }
734
-
735
- state.token = authRes.token || authRes.idToken;
736
- state.refreshToken = authRes.refreshToken;
737
- state.user = authRes.user || {
738
- id: authRes.localId || 'usr_' + Date.now().toString(36),
739
- email: authRes.email || email,
740
- name: authRes.displayName || email.split('@')[0],
741
- createdAt: new Date().toISOString(),
742
- };
743
-
744
- saveSessionToStorage();
745
- syncAuthWithServer();
746
- showDashboard();
747
- updateUserUI();
748
- showToast(`Signed in successfully as ${state.user.name}!`, 'success');
749
- } catch (error) {
750
- showToast(error.message || 'Login failed.', 'error');
751
- } finally {
752
- btnSubmit.disabled = false;
753
- btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
754
- }
724
+ 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');
728
+
729
+ btnSubmit.disabled = true;
730
+ btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
731
+
732
+ try {
733
+ let authRes;
734
+ if (state.serverConnected) {
735
+ const res = await fetch(`${state.apiBaseUrl}/api/login`, {
736
+ method: 'POST',
737
+ headers: { 'Content-Type': 'application/json' },
738
+ body: JSON.stringify({ email, password }),
739
+ });
740
+ const data = await res.json();
741
+ if (!res.ok || !data.success) {
742
+ throw new Error(data.error || 'Server authentication failed.');
743
+ }
744
+ authRes = data.auth;
745
+ } else {
746
+ authRes = await apiFirebaseSignIn(email, password);
747
+ }
748
+
749
+ state.token = authRes.token || authRes.idToken;
750
+ state.refreshToken = authRes.refreshToken;
751
+ state.user = authRes.user || {
752
+ id: authRes.localId || 'usr_' + Date.now().toString(36),
753
+ email: authRes.email || email,
754
+ name: authRes.displayName || email.split('@')[0],
755
+ createdAt: new Date().toISOString(),
756
+ };
757
+
758
+ saveSessionToStorage();
759
+ syncAuthWithServer();
760
+ showDashboard();
761
+ updateUserUI();
762
+ showToast(`Signed in successfully as ${state.user.name}!`, 'success');
763
+ } catch (error) {
764
+ showToast(error.message || 'Login failed.', 'error');
765
+ } finally {
766
+ btnSubmit.disabled = false;
767
+ btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
768
+ }
755
769
  }
756
770
 
757
771
  // Handle Sign Up Submission
758
772
  async function handleSignUp(e) {
759
- e.preventDefault();
760
- const name = document.getElementById('signup-name').value.trim();
761
- const email = document.getElementById('signup-email').value.trim();
762
- const password = document.getElementById('signup-password').value;
763
- const confirmPassword = document.getElementById('signup-confirm-password').value;
764
- const btnSubmit = document.getElementById('btn-submit-signup');
765
-
766
- if (password !== confirmPassword) {
767
- showToast('Passwords do not match.', 'error');
768
- return;
769
- }
770
-
771
- btnSubmit.disabled = true;
772
- btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
773
-
774
- try {
775
- let authRes;
776
- if (state.serverConnected) {
777
- const res = await fetch(`${state.apiBaseUrl}/api/signup`, {
778
- method: 'POST',
779
- headers: { 'Content-Type': 'application/json' },
780
- body: JSON.stringify({ name, email, password }),
781
- });
782
- const data = await res.json();
783
- if (!res.ok || !data.success) {
784
- throw new Error(data.error || 'Server registration failed.');
785
- }
786
- authRes = data.auth;
787
- } else {
788
- authRes = await apiFirebaseSignUp(name, email, password);
789
- }
790
-
791
- state.token = authRes.token || authRes.idToken;
792
- state.refreshToken = authRes.refreshToken;
793
- state.user = authRes.user || {
794
- id: authRes.localId || 'usr_' + Date.now().toString(36),
795
- email: authRes.email || email,
796
- name: name,
797
- createdAt: new Date().toISOString(),
798
- };
799
-
800
- saveSessionToStorage();
801
- syncAuthWithServer();
802
- showDashboard();
803
- updateUserUI();
804
- showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
805
- } catch (error) {
806
- showToast(error.message || 'Registration failed.', 'error');
807
- } finally {
808
- btnSubmit.disabled = false;
809
- btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
810
- }
773
+ 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');
779
+
780
+ if (password !== confirmPassword) {
781
+ showToast('Passwords do not match.', 'error');
782
+ return;
783
+ }
784
+
785
+ btnSubmit.disabled = true;
786
+ btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
787
+
788
+ try {
789
+ let authRes;
790
+ if (state.serverConnected) {
791
+ const res = await fetch(`${state.apiBaseUrl}/api/signup`, {
792
+ method: 'POST',
793
+ headers: { 'Content-Type': 'application/json' },
794
+ body: JSON.stringify({ name, email, password }),
795
+ });
796
+ const data = await res.json();
797
+ if (!res.ok || !data.success) {
798
+ throw new Error(data.error || 'Server registration failed.');
799
+ }
800
+ authRes = data.auth;
801
+ } else {
802
+ authRes = await apiFirebaseSignUp(name, email, password);
803
+ }
804
+
805
+ state.token = authRes.token || authRes.idToken;
806
+ state.refreshToken = authRes.refreshToken;
807
+ state.user = authRes.user || {
808
+ id: authRes.localId || 'usr_' + Date.now().toString(36),
809
+ email: authRes.email || email,
810
+ name: name,
811
+ createdAt: new Date().toISOString(),
812
+ };
813
+
814
+ saveSessionToStorage();
815
+ syncAuthWithServer();
816
+ showDashboard();
817
+ updateUserUI();
818
+ showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
819
+ } catch (error) {
820
+ showToast(error.message || 'Registration failed.', 'error');
821
+ } finally {
822
+ btnSubmit.disabled = false;
823
+ btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
824
+ }
811
825
  }
812
826
 
813
827
  // Forgot Password Flow
814
828
  function handleForgotPassword() {
815
- const email = prompt('Enter your registered email address for password reset:');
816
- if (email && email.includes('@')) {
817
- showToast(`Password reset link dispatched to ${email}`, 'success');
818
- } else if (email) {
819
- showToast('Please enter a valid email address.', 'error');
820
- }
829
+ const email = prompt('Enter your registered email address for password reset:');
830
+ if (email && email.includes('@')) {
831
+ showToast(`Password reset link dispatched to ${email}`, 'success');
832
+ } else if (email) {
833
+ showToast('Please enter a valid email address.', 'error');
834
+ }
821
835
  }
822
836
 
823
837
  // Demo Mode Session Launcher
824
838
  function launchDemoSession() {
825
- state.token = 'ft_demo_token_' + Date.now().toString(36);
826
- state.user = {
827
- id: 'usr_demo_' + Math.floor(1000 + Math.random() * 9000),
828
- email: 'developer@frontterrain.com',
829
- name: 'FrontTerrain Developer',
830
- createdAt: new Date().toISOString(),
831
- };
832
-
833
- saveSessionToStorage();
834
- syncAuthWithServer();
835
- showDashboard();
836
- updateUserUI();
837
- showToast('Demo Developer Session Active!', 'info');
839
+ state.token = 'ft_demo_token_' + Date.now().toString(36);
840
+ state.user = {
841
+ id: 'usr_demo_' + Math.floor(1000 + Math.random() * 9000),
842
+ email: 'developer@frontterrain.com',
843
+ name: 'FrontTerrain Developer',
844
+ createdAt: new Date().toISOString(),
845
+ };
846
+
847
+ saveSessionToStorage();
848
+ syncAuthWithServer();
849
+ showDashboard();
850
+ updateUserUI();
851
+ showToast('Demo Developer Session Active!', 'info');
838
852
  }
839
853
 
840
854
  // Firebase Direct REST API
841
855
  async function apiFirebaseSignIn(email, password) {
842
- const url = `${FIREBASE_CONFIG.baseAuthUrl}:signInWithPassword?key=${FIREBASE_CONFIG.apiKey}`;
843
- const response = await fetch(url, {
844
- method: 'POST',
845
- headers: { 'Content-Type': 'application/json' },
846
- body: JSON.stringify({
847
- email,
848
- password,
849
- returnSecureToken: true,
850
- tenantId: FIREBASE_CONFIG.tenantId,
851
- }),
852
- });
853
-
854
- const data = await response.json();
855
- if (!response.ok) {
856
- throw new Error(parseFirebaseError(data, 'Firebase sign in failed'));
857
- }
858
- return data;
856
+ const url = `${FIREBASE_CONFIG.baseAuthUrl}:signInWithPassword?key=${FIREBASE_CONFIG.apiKey}`;
857
+ const response = await fetch(url, {
858
+ method: 'POST',
859
+ headers: { 'Content-Type': 'application/json' },
860
+ body: JSON.stringify({
861
+ email,
862
+ password,
863
+ returnSecureToken: true,
864
+ tenantId: FIREBASE_CONFIG.tenantId,
865
+ }),
866
+ });
867
+
868
+ const data = await response.json();
869
+ if (!response.ok) {
870
+ throw new Error(parseFirebaseError(data, 'Firebase sign in failed'));
871
+ }
872
+ return data;
859
873
  }
860
874
 
861
875
  async function apiFirebaseSignUp(name, email, password) {
862
- const url = `${FIREBASE_CONFIG.baseAuthUrl}:signUp?key=${FIREBASE_CONFIG.apiKey}`;
863
- const response = await fetch(url, {
864
- method: 'POST',
865
- headers: { 'Content-Type': 'application/json' },
866
- body: JSON.stringify({
867
- email,
868
- password,
869
- returnSecureToken: true,
870
- tenantId: FIREBASE_CONFIG.tenantId,
871
- }),
872
- });
873
-
874
- const data = await response.json();
875
- if (!response.ok) {
876
- throw new Error(parseFirebaseError(data, 'Firebase sign up failed'));
877
- }
878
-
879
- try {
880
- const updateUrl = `${FIREBASE_CONFIG.baseAuthUrl}:update?key=${FIREBASE_CONFIG.apiKey}`;
881
- await fetch(updateUrl, {
882
- method: 'POST',
883
- headers: { 'Content-Type': 'application/json' },
884
- body: JSON.stringify({
885
- idToken: data.idToken,
886
- displayName: name,
887
- tenantId: FIREBASE_CONFIG.tenantId,
888
- }),
889
- });
890
- data.displayName = name;
891
- } catch {}
892
-
893
- return data;
876
+ const url = `${FIREBASE_CONFIG.baseAuthUrl}:signUp?key=${FIREBASE_CONFIG.apiKey}`;
877
+ const response = await fetch(url, {
878
+ method: 'POST',
879
+ headers: { 'Content-Type': 'application/json' },
880
+ body: JSON.stringify({
881
+ email,
882
+ password,
883
+ returnSecureToken: true,
884
+ tenantId: FIREBASE_CONFIG.tenantId,
885
+ }),
886
+ });
887
+
888
+ const data = await response.json();
889
+ if (!response.ok) {
890
+ throw new Error(parseFirebaseError(data, 'Firebase sign up failed'));
891
+ }
892
+
893
+ try {
894
+ const updateUrl = `${FIREBASE_CONFIG.baseAuthUrl}:update?key=${FIREBASE_CONFIG.apiKey}`;
895
+ await fetch(updateUrl, {
896
+ method: 'POST',
897
+ headers: { 'Content-Type': 'application/json' },
898
+ body: JSON.stringify({
899
+ idToken: data.idToken,
900
+ displayName: name,
901
+ tenantId: FIREBASE_CONFIG.tenantId,
902
+ }),
903
+ });
904
+ data.displayName = name;
905
+ } catch { }
906
+
907
+ return data;
894
908
  }
895
909
 
896
910
  function parseFirebaseError(errData, defaultMsg) {
897
- const code = errData?.error?.message || errData?.message || '';
898
- if (code.includes('EMAIL_NOT_FOUND')) return 'No account found with this email.';
899
- if (code.includes('INVALID_PASSWORD') || code.includes('INVALID_LOGIN_CREDENTIALS')) return 'Invalid password entered.';
900
- if (code.includes('EMAIL_EXISTS')) return 'An account already exists with this email.';
901
- if (code.includes('WEAK_PASSWORD')) return 'Password is too weak (min 6 chars).';
902
- if (code.includes('INVALID_EMAIL')) return 'Invalid email address format.';
903
- if (code.includes('TOO_MANY_ATTEMPTS_TRY_LATER')) return 'Too many login attempts. Try later.';
904
- return `${defaultMsg}: ${code || 'Unknown error'}`;
911
+ const code = errData?.error?.message || errData?.message || '';
912
+ if (code.includes('EMAIL_NOT_FOUND')) return 'No account found with this email.';
913
+ if (code.includes('INVALID_PASSWORD') || code.includes('INVALID_LOGIN_CREDENTIALS')) return 'Invalid password entered.';
914
+ if (code.includes('EMAIL_EXISTS')) return 'An account already exists with this email.';
915
+ if (code.includes('WEAK_PASSWORD')) return 'Password is too weak (min 6 chars).';
916
+ if (code.includes('INVALID_EMAIL')) return 'Invalid email address format.';
917
+ if (code.includes('TOO_MANY_ATTEMPTS_TRY_LATER')) return 'Too many login attempts. Try later.';
918
+ return `${defaultMsg}: ${code || 'Unknown error'}`;
905
919
  }
906
920
 
907
921
  // UI State Renderers
908
922
  function showAuthSection() {
909
- elements.authSection?.classList.remove('hidden');
910
- elements.dashboardSection?.classList.add('hidden');
911
- elements.userMenu?.classList.add('hidden');
912
- elements.btnOpenAuth?.classList.remove('hidden');
923
+ elements.authSection?.classList.remove('hidden');
924
+ elements.dashboardSection?.classList.add('hidden');
925
+ elements.userMenu?.classList.add('hidden');
926
+ elements.btnOpenAuth?.classList.remove('hidden');
913
927
  }
914
928
 
915
929
  function showDashboard() {
916
- elements.authSection?.classList.add('hidden');
917
- elements.dashboardSection?.classList.remove('hidden');
918
- elements.userMenu?.classList.remove('hidden');
919
- elements.btnOpenAuth?.classList.add('hidden');
920
-
921
- renderAllDynamicComponents();
930
+ elements.authSection?.classList.add('hidden');
931
+ elements.dashboardSection?.classList.remove('hidden');
932
+ elements.userMenu?.classList.remove('hidden');
933
+ elements.btnOpenAuth?.classList.add('hidden');
934
+
935
+ renderAllDynamicComponents();
922
936
  }
923
937
 
924
938
  function updateUserUI() {
925
- if (!state.user) return;
926
-
927
- const name = state.user.name || 'Developer';
928
- const initial = name.charAt(0).toUpperCase();
929
-
930
- if (elements.navAvatar) elements.navAvatar.innerText = initial;
931
- if (elements.navUserName) elements.navUserName.innerText = name;
932
- if (elements.userDisplayName) elements.userDisplayName.innerText = name;
933
- if (elements.dropdownName) elements.dropdownName.innerText = name;
934
- if (elements.dropdownEmail) elements.dropdownEmail.innerText = state.user.email;
935
-
936
- if (elements.settingsDisplayName) elements.settingsDisplayName.value = name;
937
- if (elements.settingsEmail) elements.settingsEmail.value = state.user.email;
938
- if (elements.settingsUid) elements.settingsUid.value = state.user.id;
939
-
940
- const tierInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
941
- if (elements.settingsActiveTier) elements.settingsActiveTier.value = tierInfo.name;
942
-
943
- if (elements.selectByokProvider) elements.selectByokProvider.value = state.customLlmProvider || 'gemini';
944
- if (elements.inputByokEndpoint) elements.inputByokEndpoint.value = state.customBaseUrl || '';
945
- if (elements.inputByokKey) elements.inputByokKey.value = state.customApiKey || '';
946
- if (elements.checkboxEnableByok) elements.checkboxEnableByok.checked = Boolean(state.byokEnabled);
939
+ if (!state.user) return;
940
+
941
+ const name = state.user.name || 'Developer';
942
+ const initial = name.charAt(0).toUpperCase();
943
+
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;
949
+
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;
953
+
954
+ 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);
947
961
  }
948
962
 
949
963
  function switchDashboardTab(tabId) {
950
- state.activeTab = tabId;
951
-
952
- document.querySelectorAll('.nav-btn').forEach((btn) => {
953
- if (btn.getAttribute('data-tab') === tabId) {
954
- btn.classList.add('active');
955
- } else {
956
- btn.classList.remove('active');
957
- }
958
- });
959
-
960
- document.querySelectorAll('.tab-pane').forEach((pane) => {
961
- if (pane.id === `tab-${tabId}`) {
962
- pane.classList.add('active');
963
- } else {
964
- pane.classList.remove('active');
965
- }
966
- });
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
+ });
967
981
  }
968
982
 
969
983
  async function handleLogout() {
970
- if (state.serverConnected) {
971
- try {
972
- await fetch(`${state.apiBaseUrl}/api/logout`, { method: 'POST' });
973
- } catch {}
974
- }
975
-
976
- state.user = null;
977
- state.token = null;
978
- state.refreshToken = null;
979
-
980
- localStorage.removeItem('scout_auth_user');
981
- localStorage.removeItem('scout_auth_token');
982
- localStorage.removeItem('scout_refresh_token');
983
-
984
- showAuthSection();
985
- showToast('Signed out of Scout session.', 'info');
984
+ if (state.serverConnected) {
985
+ try {
986
+ await fetch(`${state.apiBaseUrl}/api/logout`, { method: 'POST' });
987
+ } catch { }
988
+ }
989
+
990
+ state.user = null;
991
+ state.token = null;
992
+ state.refreshToken = null;
993
+
994
+ localStorage.removeItem('scout_auth_user');
995
+ localStorage.removeItem('scout_auth_token');
996
+ localStorage.removeItem('scout_refresh_token');
997
+
998
+ showAuthSection();
999
+ showToast('Signed out of Scout session.', 'info');
986
1000
  }
987
1001
 
988
1002
  function saveSessionToStorage() {
989
- if (state.user) localStorage.setItem('scout_auth_user', JSON.stringify(state.user));
990
- if (state.token) localStorage.setItem('scout_auth_token', state.token);
991
- if (state.refreshToken) localStorage.setItem('scout_refresh_token', state.refreshToken);
992
- localStorage.setItem('scout_current_tier', state.currentTier);
993
- localStorage.setItem('scout_credits_used', String(state.creditsUsed));
994
- localStorage.setItem('scout_credits_total', String(state.creditsTotal));
995
- if (state.renewAt) localStorage.setItem('scout_renew_at', state.renewAt);
996
- localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
997
- localStorage.setItem('scout_byok_provider', state.customLlmProvider);
998
- localStorage.setItem('scout_byok_key', state.customApiKey);
999
- localStorage.setItem('scout_byok_endpoint', state.customBaseUrl);
1003
+ if (state.user) localStorage.setItem('scout_auth_user', JSON.stringify(state.user));
1004
+ if (state.token) localStorage.setItem('scout_auth_token', state.token);
1005
+ if (state.refreshToken) localStorage.setItem('scout_refresh_token', state.refreshToken);
1006
+ localStorage.setItem('scout_current_tier', state.currentTier);
1007
+ localStorage.setItem('scout_credits_used', String(state.creditsUsed));
1008
+ localStorage.setItem('scout_credits_total', String(state.creditsTotal));
1009
+ if (state.renewAt) localStorage.setItem('scout_renew_at', state.renewAt);
1010
+ localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
1011
+ localStorage.setItem('scout_byok_provider', state.customLlmProvider);
1012
+ localStorage.setItem('scout_byok_key', state.customApiKey);
1013
+ localStorage.setItem('scout_byok_endpoint', state.customBaseUrl);
1000
1014
  }
1001
1015
 
1002
1016
  async function syncAuthWithServer() {
1003
- if (state.user) {
1004
- try {
1005
- await fetch(`${state.apiBaseUrl}/api/save-auth`, {
1006
- method: 'POST',
1007
- headers: { 'Content-Type': 'application/json' },
1008
- body: JSON.stringify({
1009
- user: state.user,
1010
- token: state.token,
1011
- refreshToken: state.refreshToken,
1012
- creditsUsed: state.creditsUsed,
1013
- creditsTotal: state.creditsTotal,
1014
- currentTier: state.currentTier,
1015
- renewAt: state.renewAt,
1016
- byokEnabled: state.byokEnabled,
1017
- customLlmProvider: state.customLlmProvider,
1018
- customApiKey: state.customApiKey,
1019
- customBaseUrl: state.customBaseUrl,
1020
- }),
1021
- });
1022
- } catch {}
1023
- }
1017
+ if (state.user) {
1018
+ try {
1019
+ await fetch(`${state.apiBaseUrl}/api/save-auth`, {
1020
+ method: 'POST',
1021
+ headers: { 'Content-Type': 'application/json' },
1022
+ body: JSON.stringify({
1023
+ user: state.user,
1024
+ token: state.token,
1025
+ refreshToken: state.refreshToken,
1026
+ creditsUsed: state.creditsUsed,
1027
+ creditsTotal: state.creditsTotal,
1028
+ currentTier: state.currentTier,
1029
+ renewAt: state.renewAt,
1030
+ byokEnabled: state.byokEnabled,
1031
+ customLlmProvider: state.customLlmProvider,
1032
+ customApiKey: state.customApiKey,
1033
+ customBaseUrl: state.customBaseUrl,
1034
+ }),
1035
+ });
1036
+ } catch { }
1037
+ }
1024
1038
  }
1025
1039
 
1026
1040
  // Copy Utility
1027
1041
  function copyText(text, successMsg = 'Copied to clipboard!') {
1028
- navigator.clipboard.writeText(text).then(
1029
- () => showToast(successMsg, 'success'),
1030
- () => showToast('Failed to copy to clipboard', 'error')
1031
- );
1042
+ navigator.clipboard.writeText(text).then(
1043
+ () => showToast(successMsg, 'success'),
1044
+ () => showToast('Failed to copy to clipboard', 'error')
1045
+ );
1032
1046
  }
1033
1047
 
1048
+ 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
+ }
1054
+ });
1055
+
1034
1056
  // Toast System
1035
1057
  function showToast(message, type = 'info') {
1036
- if (!elements.toastContainer) return;
1037
-
1038
- const toast = document.createElement('div');
1039
- toast.className = `toast toast-${type}`;
1040
-
1041
- let iconClass = 'fa-circle-info';
1042
- if (type === 'success') iconClass = 'fa-circle-check';
1043
- if (type === 'error') iconClass = 'fa-circle-exclamation';
1044
-
1045
- toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${message}</span>`;
1046
- elements.toastContainer.appendChild(toast);
1047
-
1048
- setTimeout(() => {
1049
- toast.style.opacity = '0';
1050
- toast.style.transform = 'translateX(100%)';
1051
- toast.style.transition = 'all 0.3s ease-out';
1052
- setTimeout(() => toast.remove(), 300);
1053
- }, 3500);
1058
+ if (!elements.toastContainer) return;
1059
+
1060
+ const toast = document.createElement('div');
1061
+ toast.className = `toast toast-${type}`;
1062
+
1063
+ let iconClass = 'fa-circle-info';
1064
+ if (type === 'success') iconClass = 'fa-circle-check';
1065
+ if (type === 'error') iconClass = 'fa-circle-exclamation';
1066
+
1067
+ toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${message}</span>`;
1068
+ elements.toastContainer.appendChild(toast);
1069
+
1070
+ setTimeout(() => {
1071
+ toast.style.opacity = '0';
1072
+ toast.style.transform = 'translateX(100%)';
1073
+ toast.style.transition = 'all 0.3s ease-out';
1074
+ setTimeout(() => toast.remove(), 300);
1075
+ }, 3500);
1054
1076
  }
1055
1077
 
1056
1078
  function escapeHtml(str) {
1057
- if (!str) return '';
1058
- return String(str)
1059
- .replace(/&/g, '&amp;')
1060
- .replace(/</g, '&lt;')
1061
- .replace(/>/g, '&gt;')
1062
- .replace(/"/g, '&quot;');
1079
+ if (!str) return '';
1080
+ return String(str)
1081
+ .replace(/&/g, '&amp;')
1082
+ .replace(/</g, '&lt;')
1083
+ .replace(/>/g, '&gt;')
1084
+ .replace(/"/g, '&quot;');
1063
1085
  }