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/.firebase/hosting.d2Vi.cache +4 -4
- package/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +29 -2
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/commands/audit.d.ts.map +1 -1
- package/bin/src/commands/audit.js +29 -10
- package/bin/src/commands/audit.js.map +1 -1
- package/bin/src/commands/dashboard.d.ts.map +1 -1
- package/bin/src/commands/dashboard.js +32 -18
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/commands/risky.d.ts.map +1 -1
- package/bin/src/commands/risky.js +32 -13
- package/bin/src/commands/risky.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/server/server.d.ts.map +1 -0
- package/bin/src/server/server.js +207 -0
- package/bin/src/server/server.js.map +1 -0
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +68 -0
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/firebase-debug.log +31 -0
- package/package.json +2 -2
- package/web/ScoutFavicon.png +0 -0
- package/web/app.js +563 -660
- package/web/styles.css +928 -526
- package/web/favicon.svg +0 -39
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout —
|
|
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
|
-
//
|
|
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: '
|
|
39
|
+
activeTab: 'account',
|
|
18
40
|
serverConnected: false,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
86
|
+
// Account & Header Controls
|
|
57
87
|
userDisplayName: document.getElementById('user-display-name'),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
148
|
+
loadTierAndCreditsFromStorage();
|
|
149
|
+
loadSessionsFromStorage();
|
|
103
150
|
initEventListeners();
|
|
104
|
-
|
|
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.
|
|
152
|
-
elements.
|
|
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
|
-
//
|
|
169
|
-
elements.
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
elements.
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
386
|
-
elements.tabSignupBtn
|
|
387
|
-
elements.loginForm
|
|
388
|
-
elements.signupForm
|
|
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
|
|
391
|
-
elements.tabLoginBtn
|
|
392
|
-
elements.signupForm
|
|
393
|
-
elements.loginForm
|
|
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(
|
|
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(
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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(
|
|
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.
|
|
1003
|
+
if (state.user) {
|
|
731
1004
|
try {
|
|
732
|
-
await fetch(
|
|
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 ></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, '>')
|
|
1152
1062
|
.replace(/"/g, '"');
|
|
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
|
-
|