ft-scout 5.0.1 → 5.0.2

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,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Dashboard & Firebase Auth Controller (Full Stack)
2
+ FrontTerrain Scout — Account & Paid Tiers Dashboard Controller
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
@@ -9,19 +9,50 @@ const FIREBASE_CONFIG = {
9
9
  baseAuthUrl: 'https://identitytoolkit.googleapis.com/v1/accounts',
10
10
  };
11
11
 
12
- // Global State
12
+ // Tier Configuration Details
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
+ },
32
+ };
33
+
34
+ // Global Application State
13
35
  let state = {
14
36
  user: null,
15
37
  token: null,
16
38
  refreshToken: null,
17
- activeTab: 'overview',
39
+ activeTab: 'account',
18
40
  serverConnected: false,
19
- scanReport: null,
20
- repoPath: '',
21
- activities: [],
22
- healthBreakdown: [],
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: '',
23
52
  };
24
53
 
54
+ let pendingCheckoutTier = null;
55
+
25
56
  // DOM Elements Reference
26
57
  const elements = {
27
58
  // Views
@@ -31,8 +62,6 @@ const elements = {
31
62
  // Auth Controls
32
63
  tabLoginBtn: document.getElementById('tab-login-btn'),
33
64
  tabSignupBtn: document.getElementById('tab-signup-btn'),
34
- btnHoloseneLogin: document.getElementById('btn-holosene-login'),
35
- btnHoloseneSignup: document.getElementById('btn-holosene-signup'),
36
65
  loginForm: document.getElementById('login-form'),
37
66
  signupForm: document.getElementById('signup-form'),
38
67
  btnOpenAuth: document.getElementById('btn-open-auth'),
@@ -50,75 +79,175 @@ const elements = {
50
79
  dropdownEmail: document.getElementById('dropdown-email'),
51
80
  btnDropdownLogout: document.getElementById('btn-dropdown-logout'),
52
81
  btnDropdownProfile: document.getElementById('btn-dropdown-profile'),
53
- btnDropdownTokens: document.getElementById('btn-dropdown-tokens'),
82
+ btnDropdownTiers: document.getElementById('btn-dropdown-tiers'),
54
83
  navTenantTag: document.getElementById('nav-tenant-tag'),
84
+ navTenantName: document.getElementById('nav-tenant-name'),
55
85
 
56
- // Dashboard Metrics & Headers
86
+ // Account & Header Controls
57
87
  userDisplayName: document.getElementById('user-display-name'),
58
- inputAuthToken: document.getElementById('input-auth-token'),
59
- cliCommandPreview: document.getElementById('cli-command-preview'),
60
- btnCopyToken: document.getElementById('btn-copy-token'),
61
- btnCopyCliSnippet: document.getElementById('btn-copy-cli-snippet'),
62
- btnCopyCliCommand: document.getElementById('btn-copy-cli-command'),
63
- btnGenerateToken: document.getElementById('btn-generate-token'),
64
- btnRefreshStats: document.getElementById('btn-refresh-stats'),
65
- btnRunAudit: document.getElementById('btn-run-audit'),
66
- btnClearActivity: document.getElementById('btn-clear-activity'),
67
- activityList: document.getElementById('activity-list'),
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'),
68
102
 
69
- // Settings Inputs
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
70
112
  settingsDisplayName: document.getElementById('settings-display-name'),
71
113
  settingsEmail: document.getElementById('settings-email'),
72
114
  settingsUid: document.getElementById('settings-uid'),
115
+ settingsActiveTier: document.getElementById('settings-active-tier'),
73
116
  profileForm: document.getElementById('profile-form'),
74
- btnSaveEngineSettings: document.getElementById('btn-save-engine-settings'),
75
- selectLlmProvider: document.getElementById('select-llm-provider'),
76
- selectLanguage: document.getElementById('select-language'),
77
- inputCustomApiKey: document.getElementById('input-custom-api-key'),
78
-
79
- // Mobile Gate Overlay
80
- mobileGateOverlay: document.getElementById('mobile-gate-overlay'),
81
- btnDismissMobileGate: document.getElementById('btn-dismiss-mobile-gate'),
82
-
83
- // Embedded CLI Terminal
84
- terminalForm: document.getElementById('terminal-form'),
85
- terminalInput: document.getElementById('terminal-input'),
86
- terminalOutput: document.getElementById('terminal-output'),
87
- btnClearTerminal: document.getElementById('btn-clear-terminal'),
88
- btnCopyTermOutput: document.getElementById('btn-copy-term-output'),
89
- termRepoPath: document.getElementById('term-repo-path'),
90
- termConnectionStatus: document.getElementById('term-connection-status'),
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'),
91
141
 
92
142
  // Toast Container
93
143
  toastContainer: document.getElementById('toast-container'),
94
144
  };
95
145
 
96
- // Global Terminal State
97
- let terminalHistory = [];
98
- let historyIndex = -1;
99
-
100
146
  // Initialize Application
101
147
  document.addEventListener('DOMContentLoaded', async () => {
102
- checkMobileDevice();
148
+ loadTierAndCreditsFromStorage();
149
+ loadSessionsFromStorage();
103
150
  initEventListeners();
104
- loadActivitiesFromStorage();
105
- loadHealthBreakdownFromStorage();
106
- renderActivityList();
107
- renderHealthBreakdown();
108
- checkHoloseneOAuthCallback();
151
+ renderAllDynamicComponents();
109
152
  await checkServerStatusAndSession();
110
153
  });
111
154
 
155
+ // Time Helper Function for Dynamic Relative Timestamps
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`;
170
+ }
171
+
172
+ // Load Subscription Tier & Dynamic Credits from Local Storage / Config
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') || '';
205
+ }
206
+
207
+ // Render All Dynamic Components
208
+ function renderAllDynamicComponents() {
209
+ renderSessionsTable();
210
+ renderCreditUsageUI();
211
+ renderTiersUI();
212
+ }
213
+
214
+ // Open / Close Checkout Modal
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');
238
+ }
239
+
240
+ function closeCheckoutModal() {
241
+ pendingCheckoutTier = null;
242
+ if (elements.checkoutModal) elements.checkoutModal.classList.add('hidden');
243
+ }
244
+
112
245
  // Setup Event Handlers
113
246
  function initEventListeners() {
114
247
  // Auth Form Tabs Switcher
115
248
  elements.tabLoginBtn?.addEventListener('click', () => switchAuthForm('login'));
116
249
  elements.tabSignupBtn?.addEventListener('click', () => switchAuthForm('signup'));
117
250
 
118
- // Holosene OAuth Handlers
119
- elements.btnHoloseneLogin?.addEventListener('click', handleHoloseneOAuth);
120
- elements.btnHoloseneSignup?.addEventListener('click', handleHoloseneOAuth);
121
-
122
251
  // Form Submissions
123
252
  elements.loginForm?.addEventListener('submit', handleSignIn);
124
253
  elements.signupForm?.addEventListener('submit', handleSignUp);
@@ -148,10 +277,39 @@ function initEventListeners() {
148
277
  });
149
278
 
150
279
  elements.btnDropdownLogout?.addEventListener('click', handleLogout);
151
- elements.btnDropdownProfile?.addEventListener('click', () => switchDashboardTab('settings'));
152
- elements.btnDropdownTokens?.addEventListener('click', () => switchDashboardTab('tokens'));
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'));
153
285
  elements.btnOpenAuth?.addEventListener('click', showAuthSection);
154
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
+
155
313
  // Password Visibility Toggle Buttons
156
314
  document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
157
315
  btn.addEventListener('click', () => {
@@ -165,196 +323,349 @@ function initEventListeners() {
165
323
  });
166
324
  });
167
325
 
168
- // Copy Actions
169
- elements.btnCopyToken?.addEventListener('click', () => {
170
- if (state.token) copyText(state.token, 'Scout Auth Token copied to clipboard!');
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
+ }
171
337
  });
172
-
173
- elements.btnCopyCliSnippet?.addEventListener('click', () => {
174
- const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
175
- const cmd = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
176
- copyText(cmd, 'CLI Login Command copied to clipboard!');
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
+ }
177
361
  });
178
-
179
- elements.btnCopyCliCommand?.addEventListener('click', () => {
180
- const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
181
- const cmd = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
182
- copyText(cmd, 'CLI Auth Command copied!');
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();
183
368
  });
184
-
185
- elements.btnGenerateToken?.addEventListener('click', generateNewToken);
186
-
187
- elements.btnRefreshStats?.addEventListener('click', async () => {
188
- showToast('Refreshing telemetry with Scout CLI engine...', 'info');
189
- await checkServerStatusAndSession();
190
- animateStats();
369
+
370
+ // Billing Cycle Toggle Switcher
371
+ elements.toggleBillingCycle?.addEventListener('change', (e) => {
372
+ state.billingCycle = e.target.checked ? 'annual' : 'monthly';
373
+ renderTiersUI();
191
374
  });
192
-
193
- elements.btnRunAudit?.addEventListener('click', () => {
194
- state.healthBreakdown = state.healthBreakdown.map((item) => {
195
- const newScore = Math.min(100, Math.max(70, item.score + Math.floor(Math.random() * 7) - 3));
196
- let badgeClass = 'score-high';
197
- let status = 'High Health';
198
- let fillClass = 'bg-success';
199
- if (newScore < 80) {
200
- badgeClass = 'score-warn';
201
- status = 'Warning';
202
- fillClass = 'bg-warning';
203
- } else if (newScore < 90) {
204
- badgeClass = 'score-med';
205
- status = 'Optimal';
206
- fillClass = 'bg-info';
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);
207
393
  }
208
- return { ...item, score: newScore, status, badgeClass, fillClass };
209
394
  });
210
- saveHealthBreakdownToStorage();
211
- renderHealthBreakdown();
212
- showToast('Triggered architecture & fragility scan!', 'success');
213
- addActivityLog('Codebase Fragility Scan Executed', 'Scanned high-churn files, risk ratings, and test coverage across modules.', 'warning', 'fa-solid fa-shield-halved');
214
- animateStats();
215
395
  });
396
+ }
397
+
398
+ // Data Storage & Session Loaders
399
+ 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
+ }
420
+ }
421
+
422
+ function saveSessionsToStorage() {
423
+ try {
424
+ localStorage.setItem('scout_cli_sessions', JSON.stringify(state.sessions));
425
+ } catch {}
426
+ }
427
+
428
+ function renderSessionsTable() {
429
+ if (!elements.sessionTableBody) return;
216
430
 
217
- elements.btnClearActivity?.addEventListener('click', () => {
218
- state.activities = [];
219
- localStorage.removeItem('scout_activity_logs');
220
- renderActivityList();
221
- showToast('Activity log cleared.', 'info');
222
- });
431
+ if (!state.sessions || state.sessions.length === 0) {
432
+ elements.sessionTableBody.innerHTML = `
433
+ <tr>
434
+ <td colspan="4" class="empty-table-notice">No active CLI authorization sessions found.</td>
435
+ </tr>
436
+ `;
437
+ return;
438
+ }
223
439
 
224
- document.querySelectorAll('.btn-revoke-session').forEach((btn) => {
225
- btn.addEventListener('click', (e) => {
226
- const row = e.target.closest('tr');
227
- if (row) {
228
- row.style.opacity = '0.3';
229
- row.style.pointerEvents = 'none';
230
- showToast('Session revoked.', 'info');
231
- }
232
- });
233
- });
440
+ elements.sessionTableBody.innerHTML = state.sessions
441
+ .map((sess) => {
442
+ return `
443
+ <tr data-id="${escapeHtml(sess.id)}">
444
+ <td>
445
+ <div class="flex-center-gap">
446
+ <i class="fa-solid fa-terminal text-primary"></i>
447
+ <div>
448
+ <strong>${escapeHtml(sess.host)}</strong>
449
+ <small class="block text-muted">${escapeHtml(sess.platform)}</small>
450
+ </div>
451
+ </div>
452
+ </td>
453
+ <td><span class="badge badge-success">${escapeHtml(timeAgo(sess.lastActive))}</span></td>
454
+ <td><code>${escapeHtml(sess.ip)}</code></td>
455
+ <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
+ </tr>
457
+ `;
458
+ })
459
+ .join('');
460
+ }
234
461
 
235
- // Mobile Gate Dismissal Handler
236
- elements.btnDismissMobileGate?.addEventListener('click', () => {
237
- elements.mobileGateOverlay?.classList.add('hidden');
238
- sessionStorage.setItem('scout_mobile_dismissed', 'true');
239
- showToast('Dismissed mobile gate. Displaying desktop preview layout.', 'info');
240
- });
462
+ function revokeSession(sessionId) {
463
+ state.sessions = state.sessions.filter(s => s.id !== sessionId);
464
+ saveSessionsToStorage();
465
+ renderSessionsTable();
466
+ showToast('CLI access session revoked.', 'info');
467
+ }
241
468
 
242
- window.addEventListener('resize', checkMobileDevice);
469
+ // Render Dynamic Credit Usage & BYOK Status UI
470
+ 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
+ }
243
489
 
244
- // Terminal Quick Command Chips
245
- document.querySelectorAll('.btn-chip').forEach((chip) => {
246
- chip.addEventListener('click', () => {
247
- const cmd = chip.getAttribute('data-cmd');
248
- if (cmd && elements.terminalInput) {
249
- elements.terminalInput.value = cmd;
250
- elements.terminalForm?.dispatchEvent(new Event('submit'));
251
- }
252
- });
253
- });
490
+ if (elements.labelCreditConsumption) {
491
+ elements.labelCreditConsumption.innerText = 'Direct Custom Endpoint Routing (Scout Credits Bypassed)';
492
+ }
254
493
 
255
- // Terminal Form Submit
256
- elements.terminalForm?.addEventListener('submit', (e) => {
257
- e.preventDefault();
258
- const cmd = elements.terminalInput?.value.trim();
259
- if (cmd) {
260
- runWebTerminalCommand(cmd);
261
- elements.terminalInput.value = '';
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';
262
505
  }
263
- });
264
506
 
265
- // Terminal History Navigation (Up / Down Keys)
266
- elements.terminalInput?.addEventListener('keydown', (e) => {
267
- if (e.key === 'ArrowUp') {
268
- e.preventDefault();
269
- if (terminalHistory.length > 0) {
270
- if (historyIndex < terminalHistory.length - 1) {
271
- historyIndex++;
272
- elements.terminalInput.value = terminalHistory[terminalHistory.length - 1 - historyIndex];
273
- }
274
- }
275
- } else if (e.key === 'ArrowDown') {
276
- e.preventDefault();
277
- if (historyIndex > 0) {
278
- historyIndex--;
279
- elements.terminalInput.value = terminalHistory[terminalHistory.length - 1 - historyIndex];
280
- } else if (historyIndex === 0) {
281
- historyIndex = -1;
282
- elements.terminalInput.value = '';
283
- }
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%)';
284
521
  }
285
- });
286
522
 
287
- // Terminal Actions: Clear & Copy
288
- elements.btnClearTerminal?.addEventListener('click', () => {
289
- if (elements.terminalOutput) {
290
- elements.terminalOutput.innerHTML = `
291
- <div class="term-line term-welcome"><span class="term-brand">FrontTerrain Scout AI Engine v5.0.1</span> — Onboarding Co-Pilot Shell</div>
292
- <div class="term-line term-info">Terminal cleared. Type <code class="term-code">scout help</code> for available commands.</div>
293
- <div class="term-line term-dim">--------------------------------------------------------------------------------</div>
294
- `;
295
- showToast('Terminal cleared.', 'info');
523
+ if (elements.labelCreditConsumption) {
524
+ elements.labelCreditConsumption.innerText = 'Monthly Credit Consumption';
296
525
  }
297
- });
298
526
 
299
- elements.btnCopyTermOutput?.addEventListener('click', () => {
300
- if (elements.terminalOutput) {
301
- copyText(elements.terminalOutput.innerText, 'Terminal output copied to clipboard!');
527
+ if (elements.byokBannerNote) {
528
+ elements.byokBannerNote.classList.add('hidden');
302
529
  }
303
- });
304
-
305
- // Settings Form Submissions
306
- elements.profileForm?.addEventListener('submit', (e) => {
307
- e.preventDefault();
308
- const newName = elements.settingsDisplayName.value.trim();
309
- if (newName && state.user) {
310
- state.user.name = newName;
311
- saveSessionToStorage();
312
- syncAuthWithServer();
313
- updateUserUI();
314
- showToast('Profile display name updated successfully!', 'success');
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`;
315
537
  }
316
- });
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
+ }
317
544
 
318
- elements.btnSaveEngineSettings?.addEventListener('click', () => {
319
- const provider = elements.selectLlmProvider?.value;
320
- const lang = elements.selectLanguage?.value;
321
- localStorage.setItem('scout_provider', provider || 'gemini');
322
- localStorage.setItem('scout_lang', lang || 'english');
323
- showToast(`Scout Engine updated (${provider}, ${lang})`, 'success');
545
+ if (elements.valRenewDatetime) {
546
+ elements.valRenewDatetime.innerText = renewDateStr;
547
+ }
548
+ }
549
+
550
+ // Render Paid Tiers UI
551
+ 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
+ }
324
589
  });
325
590
  }
326
591
 
327
592
  // Sync with Local Server CLI Session
328
593
  async function checkServerStatusAndSession() {
594
+ let connected = false;
595
+ let data = null;
596
+
329
597
  try {
330
598
  const res = await fetch('/api/status', { method: 'GET' });
331
599
  if (res.ok) {
332
- const data = await res.json();
333
- state.serverConnected = true;
334
- state.scanReport = data.scanReport;
335
- state.repoPath = data.repoPath;
336
-
337
- // Update tenant tag to show server connected
338
- if (elements.navTenantTag) {
339
- elements.navTenantTag.innerHTML = `<span class="pulse-dot"></span> <span class="tenant-name">CLI Local Sync Active</span>`;
600
+ data = await res.json();
601
+ if (data && data.status === 'ok') {
602
+ state.apiBaseUrl = '';
603
+ connected = true;
340
604
  }
341
-
342
- if (data.isLoggedIn && data.auth?.user) {
343
- state.user = data.auth.user;
344
- state.token = data.auth.token;
345
- state.refreshToken = data.auth.refreshToken;
346
-
347
- saveSessionToStorage();
348
- showDashboard();
349
- updateUserUI();
350
- return;
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
+ }
351
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'})`;
352
626
  }
353
- } catch (err) {
354
- state.serverConnected = false;
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;
355
667
  }
356
668
 
357
- // Fallback to LocalStorage
358
669
  checkLocalStorageSession();
359
670
  }
360
671
 
@@ -382,53 +693,15 @@ function checkLocalStorageSession() {
382
693
  // Switch Auth View Forms
383
694
  function switchAuthForm(mode) {
384
695
  if (mode === 'login') {
385
- elements.tabLoginBtn.classList.add('active');
386
- elements.tabSignupBtn.classList.remove('active');
387
- elements.loginForm.classList.remove('hidden');
388
- elements.signupForm.classList.add('hidden');
696
+ elements.tabLoginBtn?.classList.add('active');
697
+ elements.tabSignupBtn?.classList.remove('active');
698
+ elements.loginForm?.classList.remove('hidden');
699
+ elements.signupForm?.classList.add('hidden');
389
700
  } else {
390
- elements.tabSignupBtn.classList.add('active');
391
- elements.tabLoginBtn.classList.remove('active');
392
- elements.signupForm.classList.remove('hidden');
393
- elements.loginForm.classList.add('hidden');
394
- }
395
- }
396
-
397
- // Holosene OAuth & Account Integration
398
- function handleHoloseneOAuth() {
399
- const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
400
- const holoseneAuthUrl = `https://holosene.frontterrain.com/oauth/authorize?client_id=ft-scout-web&response_type=code&redirect_uri=${redirectUri}&scope=openid+profile+email`;
401
- showToast('Redirecting to Holosene OAuth Consent Screen...', 'info');
402
- setTimeout(() => {
403
- window.location.href = holoseneAuthUrl;
404
- }, 600);
405
- }
406
-
407
- function checkHoloseneOAuthCallback() {
408
- const params = new URLSearchParams(window.location.search);
409
- const code = params.get('code');
410
- const token = params.get('token') || params.get('access_token');
411
- if (code || token) {
412
- const activeToken = token || code;
413
- const email = params.get('email') || '';
414
- const name = params.get('name') || (email && email.includes('@') ? email.split('@')[0] : 'Holosene Developer');
415
-
416
- state.token = activeToken;
417
- state.provider = 'holosene';
418
- state.user = {
419
- id: 'hls_' + Date.now().toString(36),
420
- email: email,
421
- name: name,
422
- username: email && email.includes('@') ? email.split('@')[0] : 'From Holosene',
423
- createdAt: new Date().toISOString(),
424
- };
425
- saveSessionToStorage();
426
- showDashboard();
427
- updateUserUI();
428
- showToast(`Successfully authenticated via Holosene Account as ${name}!`, 'success');
429
- addActivityLog('Holosene Authenticated', `Signed in via Holosene platform (${email})`, 'success', 'fa-solid fa-atom');
430
-
431
- window.history.replaceState({}, document.title, window.location.pathname);
701
+ elements.tabSignupBtn?.classList.add('active');
702
+ elements.tabLoginBtn?.classList.remove('active');
703
+ elements.signupForm?.classList.remove('hidden');
704
+ elements.loginForm?.classList.add('hidden');
432
705
  }
433
706
  }
434
707
 
@@ -445,7 +718,7 @@ async function handleSignIn(e) {
445
718
  try {
446
719
  let authRes;
447
720
  if (state.serverConnected) {
448
- const res = await fetch('/api/login', {
721
+ const res = await fetch(`${state.apiBaseUrl}/api/login`, {
449
722
  method: 'POST',
450
723
  headers: { 'Content-Type': 'application/json' },
451
724
  body: JSON.stringify({ email, password }),
@@ -462,17 +735,17 @@ async function handleSignIn(e) {
462
735
  state.token = authRes.token || authRes.idToken;
463
736
  state.refreshToken = authRes.refreshToken;
464
737
  state.user = authRes.user || {
465
- id: authRes.localId,
738
+ id: authRes.localId || 'usr_' + Date.now().toString(36),
466
739
  email: authRes.email || email,
467
740
  name: authRes.displayName || email.split('@')[0],
468
741
  createdAt: new Date().toISOString(),
469
742
  };
470
743
 
471
744
  saveSessionToStorage();
745
+ syncAuthWithServer();
472
746
  showDashboard();
473
747
  updateUserUI();
474
748
  showToast(`Signed in successfully as ${state.user.name}!`, 'success');
475
- addActivityLog('User Account Authenticated', `Signed in as ${state.user.name} (${state.user.email})`, 'success', 'fa-solid fa-user-check');
476
749
  } catch (error) {
477
750
  showToast(error.message || 'Login failed.', 'error');
478
751
  } finally {
@@ -501,7 +774,7 @@ async function handleSignUp(e) {
501
774
  try {
502
775
  let authRes;
503
776
  if (state.serverConnected) {
504
- const res = await fetch('/api/signup', {
777
+ const res = await fetch(`${state.apiBaseUrl}/api/signup`, {
505
778
  method: 'POST',
506
779
  headers: { 'Content-Type': 'application/json' },
507
780
  body: JSON.stringify({ name, email, password }),
@@ -518,17 +791,17 @@ async function handleSignUp(e) {
518
791
  state.token = authRes.token || authRes.idToken;
519
792
  state.refreshToken = authRes.refreshToken;
520
793
  state.user = authRes.user || {
521
- id: authRes.localId,
794
+ id: authRes.localId || 'usr_' + Date.now().toString(36),
522
795
  email: authRes.email || email,
523
796
  name: name,
524
797
  createdAt: new Date().toISOString(),
525
798
  };
526
799
 
527
800
  saveSessionToStorage();
801
+ syncAuthWithServer();
528
802
  showDashboard();
529
803
  updateUserUI();
530
804
  showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
531
- addActivityLog('Account Registration Completed', `Created new Scout developer account for ${name}`, 'success', 'fa-solid fa-user-plus');
532
805
  } catch (error) {
533
806
  showToast(error.message || 'Registration failed.', 'error');
534
807
  } finally {
@@ -551,7 +824,7 @@ function handleForgotPassword() {
551
824
  function launchDemoSession() {
552
825
  state.token = 'ft_demo_token_' + Date.now().toString(36);
553
826
  state.user = {
554
- id: 'usr_demo_7749',
827
+ id: 'usr_demo_' + Math.floor(1000 + Math.random() * 9000),
555
828
  email: 'developer@frontterrain.com',
556
829
  name: 'FrontTerrain Developer',
557
830
  createdAt: new Date().toISOString(),
@@ -603,7 +876,6 @@ async function apiFirebaseSignUp(name, email, password) {
603
876
  throw new Error(parseFirebaseError(data, 'Firebase sign up failed'));
604
877
  }
605
878
 
606
- // Set Display Name
607
879
  try {
608
880
  const updateUrl = `${FIREBASE_CONFIG.baseAuthUrl}:update?key=${FIREBASE_CONFIG.apiKey}`;
609
881
  await fetch(updateUrl, {
@@ -646,9 +918,7 @@ function showDashboard() {
646
918
  elements.userMenu?.classList.remove('hidden');
647
919
  elements.btnOpenAuth?.classList.add('hidden');
648
920
 
649
- renderActivityList();
650
- renderHealthBreakdown();
651
- animateStats();
921
+ renderAllDynamicComponents();
652
922
  }
653
923
 
654
924
  function updateUserUI() {
@@ -667,9 +937,13 @@ function updateUserUI() {
667
937
  if (elements.settingsEmail) elements.settingsEmail.value = state.user.email;
668
938
  if (elements.settingsUid) elements.settingsUid.value = state.user.id;
669
939
 
670
- if (elements.inputAuthToken) elements.inputAuthToken.value = state.token || 'No token active';
671
- const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
672
- if (elements.cliCommandPreview) elements.cliCommandPreview.innerText = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
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);
673
947
  }
674
948
 
675
949
  function switchDashboardTab(tabId) {
@@ -692,19 +966,10 @@ function switchDashboardTab(tabId) {
692
966
  });
693
967
  }
694
968
 
695
- function generateNewToken() {
696
- state.token = 'ft_scout_tk_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
697
- saveSessionToStorage();
698
- syncAuthWithServer();
699
- updateUserUI();
700
- showToast('Generated new single-use CLI access token!', 'success');
701
- addActivityLog('Generated CLI Access Token', `Token issued: ${state.token.substring(0, 15)}...`);
702
- }
703
-
704
969
  async function handleLogout() {
705
970
  if (state.serverConnected) {
706
971
  try {
707
- await fetch('/api/logout', { method: 'POST' });
972
+ await fetch(`${state.apiBaseUrl}/api/logout`, { method: 'POST' });
708
973
  } catch {}
709
974
  }
710
975
 
@@ -724,200 +989,40 @@ function saveSessionToStorage() {
724
989
  if (state.user) localStorage.setItem('scout_auth_user', JSON.stringify(state.user));
725
990
  if (state.token) localStorage.setItem('scout_auth_token', state.token);
726
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);
727
1000
  }
728
1001
 
729
1002
  async function syncAuthWithServer() {
730
- if (state.serverConnected && state.user && state.token) {
1003
+ if (state.user) {
731
1004
  try {
732
- await fetch('/api/save-auth', {
1005
+ await fetch(`${state.apiBaseUrl}/api/save-auth`, {
733
1006
  method: 'POST',
734
1007
  headers: { 'Content-Type': 'application/json' },
735
1008
  body: JSON.stringify({
736
1009
  user: state.user,
737
1010
  token: state.token,
738
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,
739
1020
  }),
740
1021
  });
741
1022
  } catch {}
742
1023
  }
743
1024
  }
744
1025
 
745
- // Activity Logs & Health Breakdown Helper Functions
746
- function loadActivitiesFromStorage() {
747
- try {
748
- const stored = localStorage.getItem('scout_activity_logs');
749
- if (stored) {
750
- state.activities = JSON.parse(stored);
751
- } else {
752
- state.activities = [
753
- {
754
- id: 'act_init_1',
755
- title: 'FrontTerrain Scout Co-Pilot Active',
756
- description: 'Telemetry sync active and awaiting repository commands.',
757
- timeAgo: 'Just now',
758
- type: 'success',
759
- icon: 'fa-solid fa-shield-check',
760
- },
761
- {
762
- id: 'act_init_2',
763
- title: 'Repository Scan Completed',
764
- description: 'Indexed source files across TypeScript modules.',
765
- timeAgo: '15 minutes ago',
766
- type: 'info',
767
- icon: 'fa-solid fa-wand-magic-sparkles',
768
- },
769
- {
770
- id: 'act_init_3',
771
- title: 'Codebase Fragility Check',
772
- description: 'Evaluated module risk ratings and high-churn dependencies.',
773
- timeAgo: '1 hour ago',
774
- type: 'warning',
775
- icon: 'fa-solid fa-triangle-exclamation',
776
- },
777
- ];
778
- saveActivitiesToStorage();
779
- }
780
- } catch {
781
- state.activities = [];
782
- }
783
- }
784
-
785
- function saveActivitiesToStorage() {
786
- try {
787
- localStorage.setItem('scout_activity_logs', JSON.stringify(state.activities));
788
- } catch {}
789
- }
790
-
791
- function addActivityLog(title, description, type = 'success', icon = 'fa-solid fa-circle-check') {
792
- const newLog = {
793
- id: `act_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 6)}`,
794
- title,
795
- description,
796
- timeAgo: 'Just now',
797
- type,
798
- icon,
799
- };
800
- state.activities.unshift(newLog);
801
- if (state.activities.length > 25) {
802
- state.activities = state.activities.slice(0, 25);
803
- }
804
- saveActivitiesToStorage();
805
- renderActivityList();
806
- }
807
-
808
- function renderActivityList() {
809
- if (!elements.activityList) return;
810
- if (!state.activities || state.activities.length === 0) {
811
- elements.activityList.innerHTML = `
812
- <li class="timeline-item">
813
- <div class="timeline-content">
814
- <p class="text-muted" style="margin:0;">No recent co-pilot activity. Run Scout commands to log events.</p>
815
- </div>
816
- </li>
817
- `;
818
- return;
819
- }
820
-
821
- elements.activityList.innerHTML = state.activities
822
- .map((item) => {
823
- let dotBg = 'bg-info';
824
- if (item.type === 'success') dotBg = 'bg-success';
825
- if (item.type === 'warning') dotBg = 'bg-warning';
826
- if (item.type === 'error') dotBg = 'bg-danger';
827
-
828
- return `
829
- <li class="timeline-item">
830
- <div class="timeline-dot ${dotBg}"><i class="${escapeHtml(item.icon)}"></i></div>
831
- <div class="timeline-content">
832
- <strong>${escapeHtml(item.title)}</strong>
833
- <p>${escapeHtml(item.description)}</p>
834
- <span class="time-ago">${escapeHtml(item.timeAgo)}</span>
835
- </div>
836
- </li>
837
- `;
838
- })
839
- .join('');
840
- }
841
-
842
- function loadHealthBreakdownFromStorage() {
843
- try {
844
- const stored = localStorage.getItem('scout_health_breakdown');
845
- if (stored) {
846
- state.healthBreakdown = JSON.parse(stored);
847
- } else {
848
- state.healthBreakdown = [
849
- {
850
- id: 'h_1',
851
- name: 'FT-Check / Scout CLI Core',
852
- score: 96,
853
- status: 'High Health',
854
- badgeClass: 'score-high',
855
- fillClass: 'bg-success',
856
- },
857
- {
858
- id: 'h_2',
859
- name: 'LLM & Agent Subsystems',
860
- score: 88,
861
- status: 'Optimal',
862
- badgeClass: 'score-med',
863
- fillClass: 'bg-info',
864
- },
865
- {
866
- id: 'h_3',
867
- name: 'Web Portal & Auth Services',
868
- score: 94,
869
- status: 'High Health',
870
- badgeClass: 'score-high',
871
- fillClass: 'bg-success',
872
- },
873
- ];
874
- saveHealthBreakdownToStorage();
875
- }
876
- } catch {
877
- state.healthBreakdown = [];
878
- }
879
- }
880
-
881
- function saveHealthBreakdownToStorage() {
882
- try {
883
- localStorage.setItem('scout_health_breakdown', JSON.stringify(state.healthBreakdown));
884
- } catch {}
885
- }
886
-
887
- function renderHealthBreakdown() {
888
- const container = document.getElementById('health-breakdown-list');
889
- if (!container) return;
890
-
891
- if (!state.healthBreakdown || state.healthBreakdown.length === 0) {
892
- container.innerHTML = `<p class="text-muted">No health breakdown data available.</p>`;
893
- return;
894
- }
895
-
896
- container.innerHTML = state.healthBreakdown
897
- .map((item) => {
898
- return `
899
- <div class="health-item">
900
- <div class="health-meta">
901
- <span class="repo-name">${escapeHtml(item.name)}</span>
902
- <span class="health-score ${item.badgeClass}">${item.score}% ${escapeHtml(item.status)}</span>
903
- </div>
904
- <div class="progress-bar">
905
- <div class="progress-fill ${item.fillClass}" style="width: ${item.score}%;"></div>
906
- </div>
907
- </div>
908
- `;
909
- })
910
- .join('');
911
-
912
- // Calculate dynamic overall health score average
913
- const totalScore = state.healthBreakdown.reduce((sum, item) => sum + item.score, 0);
914
- const avgHealth = Math.round(totalScore / state.healthBreakdown.length);
915
- const healthEl = document.getElementById('val-health');
916
- if (healthEl) {
917
- healthEl.innerText = `${avgHealth}%`;
918
- }
919
- }
920
-
921
1026
  // Copy Utility
922
1027
  function copyText(text, successMsg = 'Copied to clipboard!') {
923
1028
  navigator.clipboard.writeText(text).then(
@@ -948,201 +1053,6 @@ function showToast(message, type = 'info') {
948
1053
  }, 3500);
949
1054
  }
950
1055
 
951
- // Animated Stat Counters
952
- function animateStats() {
953
- const avgHealth = (state.healthBreakdown && state.healthBreakdown.length > 0)
954
- ? Math.round(state.healthBreakdown.reduce((sum, h) => sum + h.score, 0) / state.healthBreakdown.length)
955
- : 94;
956
- animateCounter('val-health', avgHealth, '%');
957
- animateCounter('val-repos', 12, '');
958
- animateCounter('val-risky', 3, '');
959
- animateCounter('val-queries', 154, '');
960
- }
961
-
962
- function animateCounter(elementId, targetValue, suffix = '') {
963
- const el = document.getElementById(elementId);
964
- if (!el) return;
965
-
966
- let current = 0;
967
- const duration = 750;
968
- const stepTime = 25;
969
- const steps = duration / stepTime;
970
- const increment = targetValue / steps;
971
-
972
- const timer = setInterval(() => {
973
- current += increment;
974
- if (current >= targetValue) {
975
- current = targetValue;
976
- clearInterval(timer);
977
- }
978
- el.innerText = Math.round(current) + suffix;
979
- }, stepTime);
980
- }
981
-
982
- // Mobile Device Gate Detector
983
- function checkMobileDevice() {
984
- const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Windows Phone/i.test(navigator.userAgent);
985
- const isSmallScreen = window.innerWidth < 768;
986
- const userDismissed = sessionStorage.getItem('scout_mobile_dismissed');
987
-
988
- if ((isMobileUA || isSmallScreen) && !userDismissed) {
989
- elements.mobileGateOverlay?.classList.remove('hidden');
990
- } else {
991
- elements.mobileGateOverlay?.classList.add('hidden');
992
- }
993
- }
994
-
995
- // Embedded CLI Terminal Controller
996
- async function runWebTerminalCommand(cmd) {
997
- if (!elements.terminalOutput) return;
998
-
999
- // Save to History
1000
- terminalHistory.push(cmd);
1001
- historyIndex = -1;
1002
-
1003
- // Render Prompt Command Line
1004
- const entryDiv = document.createElement('div');
1005
- entryDiv.className = 'term-line term-cmd-entry';
1006
- entryDiv.innerHTML = `<span class="terminal-prompt-symbol">scout &gt;</span> ${escapeHtml(cmd)}`;
1007
- elements.terminalOutput.appendChild(entryDiv);
1008
-
1009
- if (cmd.toLowerCase() === 'clear' || cmd.toLowerCase() === 'scout clear') {
1010
- elements.btnClearTerminal?.click();
1011
- return;
1012
- }
1013
-
1014
- // Loading Indicator
1015
- const loadingDiv = document.createElement('div');
1016
- loadingDiv.className = 'term-line term-info';
1017
- loadingDiv.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Executing \`${escapeHtml(cmd)}\`...`;
1018
- elements.terminalOutput.appendChild(loadingDiv);
1019
- elements.terminalOutput.scrollTop = elements.terminalOutput.scrollHeight;
1020
-
1021
- try {
1022
- let outputText = '';
1023
- if (state.serverConnected) {
1024
- const res = await fetch('/api/cli/exec', {
1025
- method: 'POST',
1026
- headers: { 'Content-Type': 'application/json' },
1027
- body: JSON.stringify({ command: cmd }),
1028
- });
1029
- const data = await res.json();
1030
- if (data.output) {
1031
- outputText = data.output;
1032
- } else if (!res.ok || data.error) {
1033
- outputText = `Error: ${data.error || 'Server failed to execute command.'}`;
1034
- }
1035
- } else {
1036
- // Client-side execution fallback
1037
- await new Promise((resolve) => setTimeout(resolve, 400));
1038
- outputText = simulateCliOutput(cmd);
1039
- }
1040
-
1041
- loadingDiv.remove();
1042
-
1043
- const outputDiv = document.createElement('div');
1044
- outputDiv.className = 'term-line term-output-text';
1045
- outputDiv.innerHTML = formatTerminalOutput(outputText);
1046
- elements.terminalOutput.appendChild(outputDiv);
1047
- addActivityLog(`Ran CLI Command: ${cmd}`, outputText.substring(0, 80) + '...');
1048
- } catch (err) {
1049
- loadingDiv.remove();
1050
- const errorDiv = document.createElement('div');
1051
- errorDiv.className = 'term-line term-output-text';
1052
- errorDiv.style.color = '#f87171';
1053
- errorDiv.innerText = `Execution Error: ${err.message || 'Failed to communicate with Scout CLI server.'}`;
1054
- elements.terminalOutput.appendChild(errorDiv);
1055
- }
1056
-
1057
- elements.terminalOutput.scrollTop = elements.terminalOutput.scrollHeight;
1058
- }
1059
-
1060
- // Client-side Standalone CLI Output Simulation
1061
- function simulateCliOutput(cmd) {
1062
- const lower = cmd.toLowerCase().trim().replace(/^(scout|ft)\s*/i, '');
1063
-
1064
- if (lower.startsWith('brief')) {
1065
- return `[FrontTerrain Scout Brief]
1066
- Repository: FT-Check / FT-CLI (v5.0.1)
1067
- Architecture: TypeScript / Node.js CLI with Firebase Auth & Web Telemetry Portal.
1068
- Key Modules:
1069
- • bin/src/engine/agentEngine.ts (Core AI agent reasoning engine)
1070
- • bin/src/commands/ (CLI command router & handlers)
1071
- • web/ (Full-stack developer dashboard & auth portal)
1072
- Summary: Week-one onboarding overview ready. FrontTerrain Scout is watching 48 source modules.`;
1073
- }
1074
-
1075
- if (lower.startsWith('risky')) {
1076
- return `[FrontTerrain Fragility Scan]
1077
- Found 3 High-Churn Hotspots:
1078
- 1. bin/src/engine/agentEngine.ts (34k lines, high change velocity)
1079
- 2. bin/src/commands/setup.ts (16k lines, interactive prompt logic)
1080
- 3. bin/src/utils/firebaseAuth.ts (Security & token sync logic)
1081
- Recommendation: Split agentEngine into sub-parsers for better maintainability.`;
1082
- }
1083
-
1084
- if (lower.startsWith('agent') || lower.startsWith('task') || lower.startsWith('goal')) {
1085
- return `[FrontTerrain Scout Autonomous Agent]
1086
- Goal: ${cmd}
1087
- Analyzing repository context...
1088
- [1/3] Scanning codebase structure... OK
1089
- [2/3] Verifying dependencies & imports... OK
1090
- [3/3] Task execution plan formulated.
1091
- Result: Scout agent executed target workflow with high confidence score (96%).`;
1092
- }
1093
-
1094
- if (lower.startsWith('audit')) {
1095
- return `[FrontTerrain Security & Dependency Audit]
1096
- Packages Scanned: 14 dependencies
1097
- Vulnerabilities Found: 0 Critical, 0 High, 1 Low (npm update recommended)
1098
- Code Health Score: 94/100
1099
- Status: Repository meets FrontTerrain security compliance rules.`;
1100
- }
1101
-
1102
- if (lower.startsWith('recommend')) {
1103
- return `[FrontTerrain Architectural Recommendations]
1104
- 1. Modularization: Decouple CLI router from agent execution engine.
1105
- 2. Performance: Cache repository AST tree in .ft/context.json for faster re-scans.
1106
- 3. Auth: Extend single-use tokens to support OAuth webhooks.`;
1107
- }
1108
-
1109
- if (lower.startsWith('setup')) {
1110
- return `[FrontTerrain Environment Setup Diagnostic]
1111
- Node.js Version: v20.x (Pass)
1112
- Git CLI Integration: Available (Pass)
1113
- Firebase Auth Connection: Tenant ft-scout-auth-gc2v5 verified (Pass)
1114
- Status: Environment ready for Scout CLI co-pilot execution.`;
1115
- }
1116
-
1117
- if (lower.startsWith('history')) {
1118
- return `[FrontTerrain Session History]
1119
- [2026-08-07 18:20] scout init --repo FT-CLI
1120
- [2026-08-07 18:21] scout risky
1121
- [2026-08-07 18:22] scout brief
1122
- [2026-08-07 18:23] scout agent --scan`;
1123
- }
1124
-
1125
- if (lower.startsWith('help')) {
1126
- return `FrontTerrain Scout CLI v5.0.1 — Available Commands:
1127
- scout init [repoUrl] Clone & build local repository map
1128
- scout brief Generate week-one onboarding summary
1129
- scout risky Identify high-churn & fragile codebase hotspots
1130
- scout agent [goal...] Run autonomous AI task agent on codebase
1131
- scout audit Run security & dependency supply-chain audit
1132
- scout recommend Get DSA & architectural suggestions
1133
- scout setup Diagnose local dev environment & fix run failures
1134
- scout search <query> Search internet for live docs & solutions
1135
- scout owners <path> Check git ownership history for file
1136
- scout history View log of Scout actions in repo
1137
- scout dashboard Launch this web dashboard & auth portal`;
1138
- }
1139
-
1140
- return `[Scout CLI Output]
1141
- Executed: scout ${lower}
1142
- Status: Command completed.
1143
- (Tip: Type 'scout help' to view all available commands)`;
1144
- }
1145
-
1146
1056
  function escapeHtml(str) {
1147
1057
  if (!str) return '';
1148
1058
  return String(str)
@@ -1151,10 +1061,3 @@ function escapeHtml(str) {
1151
1061
  .replace(/>/g, '&gt;')
1152
1062
  .replace(/"/g, '&quot;');
1153
1063
  }
1154
-
1155
- function formatTerminalOutput(text) {
1156
- if (!text) return '';
1157
- let cleanText = text.replace(/\u001b\[[0-9;]*m/g, '');
1158
- return escapeHtml(cleanText);
1159
- }
1160
-