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