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