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.
- 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/authCommand.d.ts.map +1 -1
- package/bin/src/commands/authCommand.js +20 -8
- package/bin/src/commands/authCommand.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/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +108 -10
- package/bin/src/commands/login.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/commands/signup.d.ts.map +1 -1
- package/bin/src/commands/signup.js +29 -11
- package/bin/src/commands/signup.js.map +1 -1
- package/bin/src/discovery/messages.js +1 -1
- package/bin/src/discovery/messages.js.map +1 -1
- package/bin/src/index.js +8 -5
- package/bin/src/index.js.map +1 -1
- 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 +70 -0
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/holoseneAuth.d.ts.map +1 -0
- package/bin/src/utils/holoseneAuth.js +179 -0
- package/bin/src/utils/holoseneAuth.js.map +1 -0
- package/firebase-debug.log +31 -0
- package/package.json +2 -2
- package/web/ScoutFavicon.png +0 -0
- package/web/app.js +563 -615
- package/web/styles.css +957 -512
- 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
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
86
|
+
// Account & Header Controls
|
|
55
87
|
userDisplayName: document.getElementById('user-display-name'),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
148
|
+
loadTierAndCreditsFromStorage();
|
|
149
|
+
loadSessionsFromStorage();
|
|
101
150
|
initEventListeners();
|
|
102
|
-
|
|
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.
|
|
145
|
-
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'));
|
|
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
|
-
//
|
|
162
|
-
elements.
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
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';
|
|
255
505
|
}
|
|
256
|
-
});
|
|
257
506
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
}
|
|
347
|
-
|
|
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
|
|
379
|
-
elements.tabSignupBtn
|
|
380
|
-
elements.loginForm
|
|
381
|
-
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');
|
|
382
700
|
} else {
|
|
383
|
-
elements.tabSignupBtn
|
|
384
|
-
elements.tabLoginBtn
|
|
385
|
-
elements.signupForm
|
|
386
|
-
elements.loginForm
|
|
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(
|
|
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(
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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(
|
|
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.
|
|
1003
|
+
if (state.user) {
|
|
686
1004
|
try {
|
|
687
|
-
await fetch(
|
|
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 ></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, '>')
|
|
1107
1062
|
.replace(/"/g, '"');
|
|
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
|
-
|