ft-scout 8.0.6 → 8.0.8
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 +6 -6
- package/README.md +2 -2
- package/bin/src/commands/dashboard.js +1 -1
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/engine/liveScreenEngine.d.ts.map +1 -1
- package/bin/src/engine/liveScreenEngine.js +31 -0
- package/bin/src/engine/liveScreenEngine.js.map +1 -1
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +33 -2
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +61 -7
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/auditLogger.d.ts.map +1 -0
- package/bin/src/utils/auditLogger.js +70 -0
- package/bin/src/utils/auditLogger.js.map +1 -0
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js +166 -30
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/firebase.json +1 -1
- package/package.json +1 -1
- package/web/app.js +385 -84
- package/web/styles.css +2 -2
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout — Multi-Page Application Engine v8.0.
|
|
2
|
+
FrontTerrain Scout — Multi-Page Application Engine v8.0.8
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
@@ -44,6 +44,9 @@ let state = {
|
|
|
44
44
|
creditsUsed: 0,
|
|
45
45
|
creditsTotal: 100,
|
|
46
46
|
renewAt: null,
|
|
47
|
+
cancelAtPeriodEnd: false,
|
|
48
|
+
pendingDowngrade: null,
|
|
49
|
+
paidTier: null,
|
|
47
50
|
byokEnabled: false,
|
|
48
51
|
customLlmProvider: 'nvidia',
|
|
49
52
|
customApiKey: '',
|
|
@@ -52,6 +55,7 @@ let state = {
|
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
let pendingCheckoutTier = null;
|
|
58
|
+
let pendingDowngradeTier = null;
|
|
55
59
|
|
|
56
60
|
// Audio Synthesizer via Web Audio API
|
|
57
61
|
let audioCtx = null;
|
|
@@ -113,9 +117,13 @@ function applyTierUpgrade(tier, transactionId) {
|
|
|
113
117
|
const quota = targetTier === 'enterprise' ? 10000 : (targetTier === 'pro' ? 1000 : 100);
|
|
114
118
|
|
|
115
119
|
state.currentTier = targetTier;
|
|
120
|
+
state.paidTier = targetTier;
|
|
121
|
+
state.cancelAtPeriodEnd = false;
|
|
122
|
+
state.pendingDowngrade = null;
|
|
116
123
|
state.creditsTotal = quota;
|
|
117
124
|
state.creditsUsed = 0;
|
|
118
|
-
|
|
125
|
+
const durationDays = state.billingCycle === 'annual' ? 365 : 30;
|
|
126
|
+
state.renewAt = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000).toISOString();
|
|
119
127
|
|
|
120
128
|
saveSessionToStorage();
|
|
121
129
|
renderAllDynamicComponents();
|
|
@@ -205,6 +213,59 @@ function timeAgo(dateInput) {
|
|
|
205
213
|
return `${days}d ago`;
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
// Date and Expiration Helpers
|
|
217
|
+
function formatRenewDate(isoDateStr) {
|
|
218
|
+
if (!isoDateStr) return 'End of billing period';
|
|
219
|
+
try {
|
|
220
|
+
const d = new Date(isoDateStr);
|
|
221
|
+
if (isNaN(d.getTime())) return 'End of billing period';
|
|
222
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
223
|
+
} catch {
|
|
224
|
+
return 'End of billing period';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isSubscriptionExpired() {
|
|
229
|
+
if (!state.renewAt) return true;
|
|
230
|
+
const renewDate = new Date(state.renewAt);
|
|
231
|
+
if (isNaN(renewDate.getTime())) return true;
|
|
232
|
+
return Date.now() >= renewDate.getTime();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function checkSubscriptionExpiration() {
|
|
236
|
+
if (!state.renewAt) return;
|
|
237
|
+
const renewDate = new Date(state.renewAt);
|
|
238
|
+
if (isNaN(renewDate.getTime())) return;
|
|
239
|
+
|
|
240
|
+
const now = new Date();
|
|
241
|
+
if (now >= renewDate) {
|
|
242
|
+
if (state.cancelAtPeriodEnd) {
|
|
243
|
+
// Expiration date has arrived! Transition gracefully to pending downgrade tier
|
|
244
|
+
const targetTier = state.pendingDowngrade || 'free';
|
|
245
|
+
state.currentTier = targetTier;
|
|
246
|
+
state.paidTier = targetTier === 'free' ? null : targetTier;
|
|
247
|
+
state.cancelAtPeriodEnd = false;
|
|
248
|
+
state.pendingDowngrade = null;
|
|
249
|
+
state.creditsTotal = targetTier === 'pro' ? 1000 : (targetTier === 'enterprise' ? 10000 : 100);
|
|
250
|
+
state.creditsUsed = 0;
|
|
251
|
+
state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
|
|
252
|
+
saveSessionToStorage();
|
|
253
|
+
if (state.serverConnected) syncAuthWithServer();
|
|
254
|
+
showToast(`Your subscription period has ended. Account transitioned to ${TIER_DETAILS[targetTier].name}.`, 'info');
|
|
255
|
+
} else if (state.currentTier !== 'free') {
|
|
256
|
+
// Auto-renew monthly credits cycle
|
|
257
|
+
const nextRenewal = new Date(renewDate);
|
|
258
|
+
while (nextRenewal <= now) {
|
|
259
|
+
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
|
|
260
|
+
}
|
|
261
|
+
state.renewAt = nextRenewal.toISOString();
|
|
262
|
+
state.creditsUsed = 0;
|
|
263
|
+
saveSessionToStorage();
|
|
264
|
+
if (state.serverConnected) syncAuthWithServer();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
208
269
|
// Load Subscription Tier & Dynamic Credits from Local Storage
|
|
209
270
|
function loadTierAndCreditsFromStorage() {
|
|
210
271
|
const savedTier = localStorage.getItem('scout_current_tier');
|
|
@@ -230,10 +291,17 @@ function loadTierAndCreditsFromStorage() {
|
|
|
230
291
|
state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
|
|
231
292
|
}
|
|
232
293
|
|
|
294
|
+
state.cancelAtPeriodEnd = localStorage.getItem('scout_cancel_at_period_end') === 'true';
|
|
295
|
+
state.pendingDowngrade = localStorage.getItem('scout_pending_downgrade') || null;
|
|
296
|
+
const savedPaidTier = localStorage.getItem('scout_paid_tier');
|
|
297
|
+
state.paidTier = (savedPaidTier && TIER_DETAILS[savedPaidTier]) ? savedPaidTier : (state.currentTier !== 'free' ? state.currentTier : null);
|
|
298
|
+
|
|
233
299
|
state.byokEnabled = localStorage.getItem('scout_byok_enabled') === 'true';
|
|
234
300
|
state.customLlmProvider = localStorage.getItem('scout_byok_provider') || 'nvidia';
|
|
235
301
|
state.customApiKey = localStorage.getItem('scout_byok_key') || '';
|
|
236
302
|
state.customBaseUrl = localStorage.getItem('scout_byok_endpoint') || '';
|
|
303
|
+
|
|
304
|
+
checkSubscriptionExpiration();
|
|
237
305
|
}
|
|
238
306
|
|
|
239
307
|
// Render All Dynamic Components
|
|
@@ -283,6 +351,78 @@ function closeCheckoutModal() {
|
|
|
283
351
|
if (container) container.innerHTML = '';
|
|
284
352
|
}
|
|
285
353
|
|
|
354
|
+
// Downgrade & Cancellation Modal Handlers
|
|
355
|
+
function openDowngradeModal(targetTier = 'free') {
|
|
356
|
+
pendingDowngradeTier = targetTier;
|
|
357
|
+
const currentInfo = TIER_DETAILS[state.currentTier] || TIER_DETAILS.pro;
|
|
358
|
+
const renewDateFormatted = formatRenewDate(state.renewAt);
|
|
359
|
+
|
|
360
|
+
if (getEl('downgrade-current-plan')) {
|
|
361
|
+
getEl('downgrade-current-plan').innerText = currentInfo.name;
|
|
362
|
+
}
|
|
363
|
+
if (getEl('downgrade-expiration-date')) {
|
|
364
|
+
getEl('downgrade-expiration-date').innerText = renewDateFormatted;
|
|
365
|
+
}
|
|
366
|
+
if (getEl('downgrade-modal-desc')) {
|
|
367
|
+
getEl('downgrade-modal-desc').innerHTML = `You are currently subscribed to <strong class="text-bright">${escapeHtml(currentInfo.name)}</strong>.`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const modal = getEl('downgrade-modal');
|
|
371
|
+
if (modal) modal.classList.remove('hidden');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function closeDowngradeModal() {
|
|
375
|
+
pendingDowngradeTier = null;
|
|
376
|
+
const modal = getEl('downgrade-modal');
|
|
377
|
+
if (modal) modal.classList.add('hidden');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function confirmScheduleDowngrade() {
|
|
381
|
+
const targetTier = pendingDowngradeTier || 'free';
|
|
382
|
+
state.cancelAtPeriodEnd = true;
|
|
383
|
+
state.pendingDowngrade = targetTier;
|
|
384
|
+
saveSessionToStorage();
|
|
385
|
+
if (state.serverConnected) syncAuthWithServer();
|
|
386
|
+
closeDowngradeModal();
|
|
387
|
+
renderAllDynamicComponents();
|
|
388
|
+
showToast(`Subscription will cancel on ${formatRenewDate(state.renewAt)}. You retain all ${TIER_DETAILS[state.currentTier].name} features until then!`, 'warning');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function confirmImmediateDowngrade() {
|
|
392
|
+
const targetTier = pendingDowngradeTier || 'free';
|
|
393
|
+
state.currentTier = targetTier;
|
|
394
|
+
state.paidTier = targetTier === 'free' ? null : targetTier;
|
|
395
|
+
state.cancelAtPeriodEnd = false;
|
|
396
|
+
state.pendingDowngrade = null;
|
|
397
|
+
state.creditsTotal = targetTier === 'pro' ? 1000 : (targetTier === 'enterprise' ? 10000 : 100);
|
|
398
|
+
state.creditsUsed = 0;
|
|
399
|
+
const now = new Date();
|
|
400
|
+
state.renewAt = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0).toISOString();
|
|
401
|
+
saveSessionToStorage();
|
|
402
|
+
if (state.serverConnected) syncAuthWithServer();
|
|
403
|
+
closeDowngradeModal();
|
|
404
|
+
renderAllDynamicComponents();
|
|
405
|
+
showToast(`Downgraded to ${TIER_DETAILS[targetTier].name} immediately.`, 'info');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function resumeSubscription() {
|
|
409
|
+
if (!state.currentTier || state.currentTier === 'free') {
|
|
410
|
+
if (state.paidTier && state.paidTier !== 'free' && !isSubscriptionExpired()) {
|
|
411
|
+
state.currentTier = state.paidTier;
|
|
412
|
+
state.creditsTotal = state.currentTier === 'pro' ? 1000 : 10000;
|
|
413
|
+
} else {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
state.cancelAtPeriodEnd = false;
|
|
418
|
+
state.pendingDowngrade = null;
|
|
419
|
+
state.paidTier = state.currentTier;
|
|
420
|
+
saveSessionToStorage();
|
|
421
|
+
if (state.serverConnected) syncAuthWithServer();
|
|
422
|
+
renderAllDynamicComponents();
|
|
423
|
+
showToast(`🎉 Subscription resumed! Your ${TIER_DETAILS[state.currentTier].name} is active and will auto-renew.`, 'success');
|
|
424
|
+
}
|
|
425
|
+
|
|
286
426
|
// Render and initialize Razorpay button UI
|
|
287
427
|
function renderRazorpayButton(targetTier) {
|
|
288
428
|
const isAnnual = state.billingCycle === 'annual';
|
|
@@ -293,7 +433,7 @@ function renderRazorpayButton(targetTier) {
|
|
|
293
433
|
btnRzp.disabled = false;
|
|
294
434
|
btnRzp.innerHTML = `
|
|
295
435
|
<i class="fa-solid fa-bolt text-warning" style="font-size: 18px;"></i>
|
|
296
|
-
<span>Pay
|
|
436
|
+
<span>Pay with Razorpay</span>
|
|
297
437
|
`;
|
|
298
438
|
}
|
|
299
439
|
|
|
@@ -310,13 +450,38 @@ async function handleRazorpayCheckout() {
|
|
|
310
450
|
btnRzp.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting to Razorpay...';
|
|
311
451
|
}
|
|
312
452
|
|
|
313
|
-
const apiBase = state.apiBaseUrl || (window.location.origin.includes('localhost') || window.location.origin.includes('127.0.0.1') ? '' : 'http://localhost:9120');
|
|
314
|
-
|
|
315
453
|
try {
|
|
316
|
-
//
|
|
317
|
-
if (
|
|
454
|
+
// Ensure Razorpay SDK is loaded
|
|
455
|
+
if (typeof window.Razorpay !== 'function') {
|
|
456
|
+
await new Promise((resolve, reject) => {
|
|
457
|
+
const existingScript = document.querySelector('script[src*="checkout.razorpay.com"]');
|
|
458
|
+
if (existingScript && window.Razorpay) return resolve();
|
|
459
|
+
|
|
460
|
+
const script = document.createElement('script');
|
|
461
|
+
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
|
462
|
+
script.async = true;
|
|
463
|
+
script.onload = () => resolve();
|
|
464
|
+
script.onerror = () => reject(new Error('Unable to load Razorpay checkout SDK. Please verify your internet connection.'));
|
|
465
|
+
document.head.appendChild(script);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Determine safe API base URL for local CLI server if connected
|
|
470
|
+
const isHttpsPage = window.location.protocol === 'https:';
|
|
471
|
+
let safeApiBase = '';
|
|
472
|
+
if (state.serverConnected && state.apiBaseUrl) {
|
|
473
|
+
// Prevent mixed content: never request http:// from https://
|
|
474
|
+
if (!(isHttpsPage && state.apiBaseUrl.startsWith('http://'))) {
|
|
475
|
+
safeApiBase = state.apiBaseUrl;
|
|
476
|
+
}
|
|
477
|
+
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
478
|
+
safeApiBase = '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Fetch Razorpay key ID from backend if available, or use public key
|
|
482
|
+
if (!state.razorpayKeyId && safeApiBase) {
|
|
318
483
|
try {
|
|
319
|
-
const configRes = await fetch(`${
|
|
484
|
+
const configRes = await fetch(`${safeApiBase}/api/razorpay/config`, { method: 'GET' });
|
|
320
485
|
if (configRes.ok) {
|
|
321
486
|
const cfg = await configRes.json();
|
|
322
487
|
if (cfg && cfg.key_id) {
|
|
@@ -330,72 +495,81 @@ async function handleRazorpayCheckout() {
|
|
|
330
495
|
|
|
331
496
|
const keyId = state.razorpayKeyId || (window.RAZORPAY_KEY_ID || 'rzp_live_Tajz48jXVm0H1Y');
|
|
332
497
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
498
|
+
// Attempt server-side order creation if backend server is available
|
|
499
|
+
let orderId = null;
|
|
500
|
+
if (safeApiBase || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
|
501
|
+
try {
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
504
|
+
const orderRes = await fetch(`${safeApiBase}/api/create-order`, {
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: { 'Content-Type': 'application/json' },
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
amount: amountInPaise,
|
|
509
|
+
currency: 'INR',
|
|
510
|
+
receipt: `rcpt_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
|
511
|
+
notes: {
|
|
512
|
+
tier: targetTier,
|
|
513
|
+
billingCycle: isAnnual ? 'annual' : 'monthly',
|
|
514
|
+
userEmail: state.user?.email || '',
|
|
515
|
+
},
|
|
516
|
+
}),
|
|
517
|
+
signal: controller.signal,
|
|
518
|
+
});
|
|
519
|
+
clearTimeout(timeoutId);
|
|
353
520
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
521
|
+
if (orderRes.ok) {
|
|
522
|
+
const orderData = await orderRes.json();
|
|
523
|
+
if (orderData && orderData.order_id) {
|
|
524
|
+
orderId = orderData.order_id;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} catch (orderErr) {
|
|
528
|
+
console.warn('Server order creation unavailable or skipped, proceeding with direct checkout:', orderErr);
|
|
529
|
+
}
|
|
357
530
|
}
|
|
358
531
|
|
|
359
|
-
// STEP 2: FRONTEND - Open Razorpay Checkout Modal
|
|
360
532
|
if (typeof window.Razorpay !== 'function') {
|
|
361
533
|
throw new Error('Razorpay SDK not loaded. Please ensure checkout.js is loaded.');
|
|
362
534
|
}
|
|
363
535
|
|
|
364
536
|
const options = {
|
|
365
537
|
key: keyId,
|
|
366
|
-
amount:
|
|
367
|
-
currency:
|
|
538
|
+
amount: amountInPaise,
|
|
539
|
+
currency: 'INR',
|
|
368
540
|
name: 'FrontTerrain Scout',
|
|
369
541
|
description: `Scout ${targetTier.toUpperCase()} Plan (${isAnnual ? 'Annual' : 'Monthly'})`,
|
|
370
542
|
image: 'https://tryscout.web.app/ScoutFavicon.png',
|
|
371
|
-
order_id: order.order_id,
|
|
372
543
|
handler: async function (response) {
|
|
373
|
-
|
|
374
|
-
showToast('Verifying payment signature with backend...', 'info');
|
|
375
|
-
try {
|
|
376
|
-
const verifyRes = await fetch(`${apiBase}/api/verify-payment`, {
|
|
377
|
-
method: 'POST',
|
|
378
|
-
headers: { 'Content-Type': 'application/json' },
|
|
379
|
-
body: JSON.stringify({
|
|
380
|
-
order_id: response.razorpay_order_id,
|
|
381
|
-
payment_id: response.razorpay_payment_id,
|
|
382
|
-
signature: response.razorpay_signature,
|
|
383
|
-
tier: targetTier,
|
|
384
|
-
userEmail: state.user?.email || '',
|
|
385
|
-
}),
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
const verifyData = await verifyRes.json();
|
|
544
|
+
showToast('Payment successful! Activating subscription...', 'info');
|
|
389
545
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
546
|
+
// If order_id was created and backend server is available, verify payment signature
|
|
547
|
+
if (response.razorpay_order_id && response.razorpay_signature && safeApiBase) {
|
|
548
|
+
try {
|
|
549
|
+
const verifyRes = await fetch(`${safeApiBase}/api/verify-payment`, {
|
|
550
|
+
method: 'POST',
|
|
551
|
+
headers: { 'Content-Type': 'application/json' },
|
|
552
|
+
body: JSON.stringify({
|
|
553
|
+
order_id: response.razorpay_order_id,
|
|
554
|
+
payment_id: response.razorpay_payment_id,
|
|
555
|
+
signature: response.razorpay_signature,
|
|
556
|
+
tier: targetTier,
|
|
557
|
+
userEmail: state.user?.email || '',
|
|
558
|
+
}),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const verifyData = await verifyRes.json();
|
|
562
|
+
if (verifyRes.ok && verifyData.success) {
|
|
563
|
+
applyTierUpgrade(targetTier, response.razorpay_payment_id);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
} catch (err) {
|
|
567
|
+
console.warn('Signature verification network error, activating client upgrade:', err);
|
|
394
568
|
}
|
|
395
|
-
} catch (err) {
|
|
396
|
-
console.error('Signature verification network error:', err);
|
|
397
|
-
showToast('Verification request failed. Please check your network connection.', 'error');
|
|
398
569
|
}
|
|
570
|
+
|
|
571
|
+
// Direct activation (standalone cloud web mode or client-side checkout)
|
|
572
|
+
applyTierUpgrade(targetTier, response.razorpay_payment_id);
|
|
399
573
|
},
|
|
400
574
|
prefill: {
|
|
401
575
|
name: state.user?.name || 'Developer',
|
|
@@ -410,12 +584,16 @@ async function handleRazorpayCheckout() {
|
|
|
410
584
|
},
|
|
411
585
|
modal: {
|
|
412
586
|
ondismiss: function () {
|
|
413
|
-
showToast('
|
|
587
|
+
showToast('payment canceled.', 'warning');
|
|
414
588
|
renderRazorpayButton(targetTier);
|
|
415
589
|
},
|
|
416
590
|
},
|
|
417
591
|
};
|
|
418
592
|
|
|
593
|
+
if (orderId) {
|
|
594
|
+
options.order_id = orderId;
|
|
595
|
+
}
|
|
596
|
+
|
|
419
597
|
const rzp = new window.Razorpay(options);
|
|
420
598
|
|
|
421
599
|
// Handle payment failure event
|
|
@@ -552,6 +730,13 @@ function initEventListeners() {
|
|
|
552
730
|
getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
|
|
553
731
|
getEl('btn-razorpay-checkout')?.addEventListener('click', handleRazorpayCheckout);
|
|
554
732
|
|
|
733
|
+
// Downgrade Modal Events
|
|
734
|
+
getEl('btn-close-downgrade')?.addEventListener('click', closeDowngradeModal);
|
|
735
|
+
getEl('btn-cancel-downgrade-modal')?.addEventListener('click', closeDowngradeModal);
|
|
736
|
+
getEl('btn-confirm-schedule-downgrade')?.addEventListener('click', confirmScheduleDowngrade);
|
|
737
|
+
getEl('btn-confirm-immediate-downgrade')?.addEventListener('click', confirmImmediateDowngrade);
|
|
738
|
+
getEl('btn-banner-resume-plan')?.addEventListener('click', resumeSubscription);
|
|
739
|
+
|
|
555
740
|
getEl('btn-confirm-checkout')?.addEventListener('click', async () => {
|
|
556
741
|
if (!pendingCheckoutTier) return;
|
|
557
742
|
|
|
@@ -689,18 +874,28 @@ function initEventListeners() {
|
|
|
689
874
|
const targetTier = btn.getAttribute('data-tier');
|
|
690
875
|
if (!targetTier || !TIER_DETAILS[targetTier]) return;
|
|
691
876
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
state.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
877
|
+
// 1. Clicked Current Active Tier
|
|
878
|
+
if (targetTier === state.currentTier) {
|
|
879
|
+
if (state.cancelAtPeriodEnd) {
|
|
880
|
+
resumeSubscription();
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// 2. Clicked to Downgrade (from Pro/Enterprise to Free, or Enterprise to Pro)
|
|
886
|
+
if (state.currentTier !== 'free' && (targetTier === 'free' || (state.currentTier === 'enterprise' && targetTier === 'pro'))) {
|
|
887
|
+
openDowngradeModal(targetTier);
|
|
888
|
+
return;
|
|
703
889
|
}
|
|
890
|
+
|
|
891
|
+
// 3. User already has an active, unexpired paid subscription for this tier
|
|
892
|
+
if (state.paidTier === targetTier && !isSubscriptionExpired()) {
|
|
893
|
+
resumeSubscription();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 4. Regular Upgrade via Checkout Modal
|
|
898
|
+
openCheckoutModal(targetTier);
|
|
704
899
|
});
|
|
705
900
|
});
|
|
706
901
|
}
|
|
@@ -785,7 +980,7 @@ function initTerminalSimulator() {
|
|
|
785
980
|
`;
|
|
786
981
|
} else if (lower.includes('status')) {
|
|
787
982
|
response.innerHTML = `
|
|
788
|
-
<span class="term-green">✔ Scout CLI Engine: v8.0.
|
|
983
|
+
<span class="term-green">✔ Scout CLI Engine: v8.0.8 Online</span><br>
|
|
789
984
|
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
790
985
|
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
791
986
|
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
@@ -844,6 +1039,21 @@ function saveSessionsToStorage() {
|
|
|
844
1039
|
|
|
845
1040
|
function renderSessionsTable() {
|
|
846
1041
|
const tbody = getEl('session-table-body');
|
|
1042
|
+
const slotCounter = getEl('session-slot-counter');
|
|
1043
|
+
|
|
1044
|
+
const tier = (state.currentTier || 'free').toLowerCase();
|
|
1045
|
+
const maxSlots = tier === 'enterprise' ? 'Unlimited' : (tier === 'pro' ? 5 : 1);
|
|
1046
|
+
const sessionCount = state.sessions && state.sessions.length > 0 ? state.sessions.length : 1;
|
|
1047
|
+
|
|
1048
|
+
if (slotCounter) {
|
|
1049
|
+
slotCounter.innerText = `${sessionCount} / ${maxSlots} Slots Used`;
|
|
1050
|
+
if (typeof maxSlots === 'number' && sessionCount >= maxSlots) {
|
|
1051
|
+
slotCounter.className = 'badge badge-warning';
|
|
1052
|
+
} else {
|
|
1053
|
+
slotCounter.className = 'badge badge-primary';
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
847
1057
|
if (!tbody) return;
|
|
848
1058
|
|
|
849
1059
|
if (!state.sessions || state.sessions.length === 0) {
|
|
@@ -967,18 +1177,52 @@ function renderCreditUsageUI() {
|
|
|
967
1177
|
renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
|
|
968
1178
|
}
|
|
969
1179
|
|
|
970
|
-
if (valRenew)
|
|
1180
|
+
if (valRenew) {
|
|
1181
|
+
if (state.cancelAtPeriodEnd && state.currentTier !== 'free') {
|
|
1182
|
+
valRenew.innerHTML = `${renewDateStr} <span style="color: #eab308; font-weight: 600; margin-left: 6px;">(Cancels at period end)</span>`;
|
|
1183
|
+
} else {
|
|
1184
|
+
valRenew.innerText = renewDateStr;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
971
1187
|
}
|
|
972
1188
|
|
|
973
1189
|
// Render Paid Tiers UI
|
|
974
1190
|
function renderTiersUI() {
|
|
975
1191
|
const current = TIER_DETAILS[state.currentTier] || TIER_DETAILS.free;
|
|
1192
|
+
const isScheduledCancel = Boolean(state.cancelAtPeriodEnd && state.currentTier !== 'free');
|
|
1193
|
+
const formattedDate = formatRenewDate(state.renewAt);
|
|
976
1194
|
|
|
977
1195
|
if (getEl('current-plan-title')) getEl('current-plan-title').innerText = current.name;
|
|
978
|
-
if (getEl('current-plan-badge')) getEl('current-plan-badge').innerText = current.badge;
|
|
979
|
-
if (getEl('current-plan-desc')) getEl('current-plan-desc').innerText = current.desc;
|
|
980
1196
|
if (getEl('tier-active-icon')) getEl('tier-active-icon').innerHTML = current.icon;
|
|
981
1197
|
|
|
1198
|
+
if (getEl('current-plan-badge')) {
|
|
1199
|
+
if (isScheduledCancel) {
|
|
1200
|
+
getEl('current-plan-badge').innerText = `CANCELS ON ${formattedDate.toUpperCase()}`;
|
|
1201
|
+
getEl('current-plan-badge').className = 'badge badge-warning';
|
|
1202
|
+
} else {
|
|
1203
|
+
getEl('current-plan-badge').innerText = current.badge;
|
|
1204
|
+
getEl('current-plan-badge').className = 'badge badge-success';
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (getEl('current-plan-desc')) {
|
|
1209
|
+
if (isScheduledCancel) {
|
|
1210
|
+
const targetTierName = TIER_DETAILS[state.pendingDowngrade || 'free']?.name || 'Developer Free Tier';
|
|
1211
|
+
getEl('current-plan-desc').innerText = `Your plan is active until ${formattedDate}. It will automatically switch to ${targetTierName} on this date. You retain all features and credits until then.`;
|
|
1212
|
+
} else {
|
|
1213
|
+
getEl('current-plan-desc').innerText = current.desc;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const bannerResumeBtn = getEl('btn-banner-resume-plan');
|
|
1218
|
+
if (bannerResumeBtn) {
|
|
1219
|
+
if (isScheduledCancel) {
|
|
1220
|
+
bannerResumeBtn.classList.remove('hidden');
|
|
1221
|
+
} else {
|
|
1222
|
+
bannerResumeBtn.classList.add('hidden');
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
982
1226
|
const isAnnual = state.billingCycle === 'annual';
|
|
983
1227
|
if (getEl('price-pro-val')) getEl('price-pro-val').innerText = isAnnual ? '15' : '19';
|
|
984
1228
|
if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
|
|
@@ -993,18 +1237,57 @@ function renderTiersUI() {
|
|
|
993
1237
|
|
|
994
1238
|
if (t === state.currentTier) {
|
|
995
1239
|
card.classList.add('active-tier-card');
|
|
996
|
-
|
|
997
|
-
btn.innerText = 'Current Plan';
|
|
998
|
-
btn.disabled = true;
|
|
999
|
-
} else {
|
|
1000
|
-
card.classList.remove('active-tier-card');
|
|
1001
|
-
btn.disabled = false;
|
|
1002
|
-
if (t === 'pro') {
|
|
1240
|
+
if (isScheduledCancel) {
|
|
1003
1241
|
btn.className = 'btn btn-primary btn-block btn-select-tier';
|
|
1004
|
-
btn.
|
|
1242
|
+
btn.innerHTML = `<i class="fa-solid fa-rotate-left"></i> Resume ${current.name.split(' ')[0]}`;
|
|
1243
|
+
btn.disabled = false;
|
|
1005
1244
|
} else {
|
|
1006
|
-
btn.className = 'btn btn-
|
|
1007
|
-
btn.innerText =
|
|
1245
|
+
btn.className = 'btn btn-success btn-block btn-select-tier';
|
|
1246
|
+
btn.innerText = 'Current Plan';
|
|
1247
|
+
btn.disabled = true;
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
card.classList.remove('active-tier-card');
|
|
1251
|
+
if (t === 'free') {
|
|
1252
|
+
if (isScheduledCancel && (state.pendingDowngrade === 'free' || !state.pendingDowngrade)) {
|
|
1253
|
+
btn.className = 'btn btn-outline btn-block btn-select-tier';
|
|
1254
|
+
btn.innerText = `Downgrade Scheduled (${formattedDate})`;
|
|
1255
|
+
btn.disabled = true;
|
|
1256
|
+
} else {
|
|
1257
|
+
btn.disabled = false;
|
|
1258
|
+
btn.className = 'btn btn-outline btn-block btn-select-tier';
|
|
1259
|
+
btn.innerText = state.currentTier !== 'free' ? 'Switch to Free' : 'Current Plan';
|
|
1260
|
+
}
|
|
1261
|
+
} else if (t === 'pro') {
|
|
1262
|
+
if (state.paidTier === 'pro' && !isSubscriptionExpired()) {
|
|
1263
|
+
btn.className = 'btn btn-primary btn-block btn-select-tier';
|
|
1264
|
+
btn.innerHTML = `<i class="fa-solid fa-rotate-left"></i> Reactivate Pro (Active until ${formattedDate})`;
|
|
1265
|
+
btn.disabled = false;
|
|
1266
|
+
} else if (state.currentTier === 'enterprise') {
|
|
1267
|
+
if (isScheduledCancel && state.pendingDowngrade === 'pro') {
|
|
1268
|
+
btn.className = 'btn btn-outline btn-block btn-select-tier';
|
|
1269
|
+
btn.innerText = `Downgrade Scheduled (${formattedDate})`;
|
|
1270
|
+
btn.disabled = true;
|
|
1271
|
+
} else {
|
|
1272
|
+
btn.className = 'btn btn-outline btn-block btn-select-tier';
|
|
1273
|
+
btn.innerText = 'Downgrade to Pro';
|
|
1274
|
+
btn.disabled = false;
|
|
1275
|
+
}
|
|
1276
|
+
} else {
|
|
1277
|
+
btn.className = 'btn btn-primary btn-block btn-select-tier';
|
|
1278
|
+
btn.innerText = 'Upgrade to Pro';
|
|
1279
|
+
btn.disabled = false;
|
|
1280
|
+
}
|
|
1281
|
+
} else if (t === 'enterprise') {
|
|
1282
|
+
if (state.paidTier === 'enterprise' && !isSubscriptionExpired()) {
|
|
1283
|
+
btn.className = 'btn btn-warning btn-block btn-select-tier';
|
|
1284
|
+
btn.innerHTML = `<i class="fa-solid fa-rotate-left"></i> Reactivate Enterprise (Active until ${formattedDate})`;
|
|
1285
|
+
btn.disabled = false;
|
|
1286
|
+
} else {
|
|
1287
|
+
btn.className = 'btn btn-outline btn-block btn-select-tier';
|
|
1288
|
+
btn.innerText = 'Upgrade to Enterprise';
|
|
1289
|
+
btn.disabled = false;
|
|
1290
|
+
}
|
|
1008
1291
|
}
|
|
1009
1292
|
}
|
|
1010
1293
|
});
|
|
@@ -1056,6 +1339,9 @@ async function checkServerStatusAndSession() {
|
|
|
1056
1339
|
if (typeof data.auth.creditsTotal === 'number') state.creditsTotal = data.auth.creditsTotal;
|
|
1057
1340
|
if (data.auth.currentTier) state.currentTier = data.auth.currentTier;
|
|
1058
1341
|
if (data.auth.renewAt) state.renewAt = data.auth.renewAt;
|
|
1342
|
+
if (typeof data.auth.cancelAtPeriodEnd === 'boolean') state.cancelAtPeriodEnd = data.auth.cancelAtPeriodEnd;
|
|
1343
|
+
if (data.auth.pendingDowngrade !== undefined) state.pendingDowngrade = data.auth.pendingDowngrade;
|
|
1344
|
+
if (data.auth.paidTier !== undefined) state.paidTier = data.auth.paidTier;
|
|
1059
1345
|
if (typeof data.auth.byokEnabled === 'boolean') state.byokEnabled = data.auth.byokEnabled;
|
|
1060
1346
|
if (data.auth.customLlmProvider) state.customLlmProvider = data.auth.customLlmProvider;
|
|
1061
1347
|
if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
|
|
@@ -1394,6 +1680,15 @@ function updateUserUI() {
|
|
|
1394
1680
|
if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
|
|
1395
1681
|
if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
|
|
1396
1682
|
if (getEl('checkbox-enable-byok')) getEl('checkbox-enable-byok').checked = Boolean(state.byokEnabled);
|
|
1683
|
+
|
|
1684
|
+
const inceptionBadge = getEl('nvidia-inception-badge');
|
|
1685
|
+
if (inceptionBadge) {
|
|
1686
|
+
if (state.currentTier === 'pro' || state.currentTier === 'enterprise') {
|
|
1687
|
+
inceptionBadge.style.display = 'inline-flex';
|
|
1688
|
+
} else {
|
|
1689
|
+
inceptionBadge.style.display = 'none';
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1397
1692
|
}
|
|
1398
1693
|
|
|
1399
1694
|
async function handleLogout() {
|
|
@@ -1423,6 +1718,9 @@ function saveSessionToStorage() {
|
|
|
1423
1718
|
localStorage.setItem('scout_credits_used', String(state.creditsUsed));
|
|
1424
1719
|
localStorage.setItem('scout_credits_total', String(state.creditsTotal));
|
|
1425
1720
|
if (state.renewAt) localStorage.setItem('scout_renew_at', state.renewAt);
|
|
1721
|
+
localStorage.setItem('scout_cancel_at_period_end', String(Boolean(state.cancelAtPeriodEnd)));
|
|
1722
|
+
localStorage.setItem('scout_pending_downgrade', state.pendingDowngrade || '');
|
|
1723
|
+
localStorage.setItem('scout_paid_tier', state.paidTier || '');
|
|
1426
1724
|
localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
|
|
1427
1725
|
localStorage.setItem('scout_byok_provider', state.customLlmProvider);
|
|
1428
1726
|
localStorage.setItem('scout_byok_key', state.customApiKey);
|
|
@@ -1443,6 +1741,9 @@ async function syncAuthWithServer() {
|
|
|
1443
1741
|
creditsTotal: state.creditsTotal,
|
|
1444
1742
|
currentTier: state.currentTier,
|
|
1445
1743
|
renewAt: state.renewAt,
|
|
1744
|
+
cancelAtPeriodEnd: state.cancelAtPeriodEnd,
|
|
1745
|
+
pendingDowngrade: state.pendingDowngrade,
|
|
1746
|
+
paidTier: state.paidTier,
|
|
1446
1747
|
byokEnabled: state.byokEnabled,
|
|
1447
1748
|
customLlmProvider: state.customLlmProvider,
|
|
1448
1749
|
customApiKey: state.customApiKey,
|
package/web/styles.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout — Design System & Visual Engine v8.0.
|
|
2
|
+
FrontTerrain Scout — Design System & Visual Engine v8.0.8
|
|
3
3
|
Inspired by HappyRobot, Linear.app & Vercel aesthetics
|
|
4
4
|
========================================================================== */
|
|
5
5
|
|
|
@@ -1063,7 +1063,7 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
1063
1063
|
.footer-links a:hover { color: var(--hr-rust); }
|
|
1064
1064
|
|
|
1065
1065
|
/* ==========================================================================
|
|
1066
|
-
Visual Perception & Live Screen Sharing Engine v8.0.
|
|
1066
|
+
Visual Perception & Live Screen Sharing Engine v8.0.8
|
|
1067
1067
|
========================================================================== */
|
|
1068
1068
|
.vision-banner {
|
|
1069
1069
|
margin-bottom: 28px;
|