ft-scout 8.0.7 → 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/web/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Multi-Page Application Engine v8.0.7
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
- state.renewAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
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';
@@ -590,6 +730,13 @@ function initEventListeners() {
590
730
  getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
591
731
  getEl('btn-razorpay-checkout')?.addEventListener('click', handleRazorpayCheckout);
592
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
+
593
740
  getEl('btn-confirm-checkout')?.addEventListener('click', async () => {
594
741
  if (!pendingCheckoutTier) return;
595
742
 
@@ -727,18 +874,28 @@ function initEventListeners() {
727
874
  const targetTier = btn.getAttribute('data-tier');
728
875
  if (!targetTier || !TIER_DETAILS[targetTier]) return;
729
876
 
730
- if (targetTier === 'free') {
731
- state.currentTier = 'free';
732
- state.creditsTotal = 100;
733
- saveSessionToStorage();
734
- syncAuthWithServer();
735
- renderCreditUsageUI();
736
- renderTiersUI();
737
- updateUserUI();
738
- showToast('Switched to Developer Free Tier.', 'info');
739
- } else {
740
- openCheckoutModal(targetTier);
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;
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;
741
895
  }
896
+
897
+ // 4. Regular Upgrade via Checkout Modal
898
+ openCheckoutModal(targetTier);
742
899
  });
743
900
  });
744
901
  }
@@ -823,7 +980,7 @@ function initTerminalSimulator() {
823
980
  `;
824
981
  } else if (lower.includes('status')) {
825
982
  response.innerHTML = `
826
- <span class="term-green">✔ Scout CLI Engine: v8.0.7 Online</span><br>
983
+ <span class="term-green">✔ Scout CLI Engine: v8.0.8 Online</span><br>
827
984
  ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
828
985
  ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
829
986
  ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
@@ -882,6 +1039,21 @@ function saveSessionsToStorage() {
882
1039
 
883
1040
  function renderSessionsTable() {
884
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
+
885
1057
  if (!tbody) return;
886
1058
 
887
1059
  if (!state.sessions || state.sessions.length === 0) {
@@ -1005,18 +1177,52 @@ function renderCreditUsageUI() {
1005
1177
  renewDateStr = `${nextMonth.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} at 00:00:00 UTC`;
1006
1178
  }
1007
1179
 
1008
- if (valRenew) valRenew.innerText = renewDateStr;
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
+ }
1009
1187
  }
1010
1188
 
1011
1189
  // Render Paid Tiers UI
1012
1190
  function renderTiersUI() {
1013
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);
1014
1194
 
1015
1195
  if (getEl('current-plan-title')) getEl('current-plan-title').innerText = current.name;
1016
- if (getEl('current-plan-badge')) getEl('current-plan-badge').innerText = current.badge;
1017
- if (getEl('current-plan-desc')) getEl('current-plan-desc').innerText = current.desc;
1018
1196
  if (getEl('tier-active-icon')) getEl('tier-active-icon').innerHTML = current.icon;
1019
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
+
1020
1226
  const isAnnual = state.billingCycle === 'annual';
1021
1227
  if (getEl('price-pro-val')) getEl('price-pro-val').innerText = isAnnual ? '15' : '19';
1022
1228
  if (getEl('price-pro-period')) getEl('price-pro-period').innerText = isAnnual ? '/ month (billed annually)' : '/ month';
@@ -1031,18 +1237,57 @@ function renderTiersUI() {
1031
1237
 
1032
1238
  if (t === state.currentTier) {
1033
1239
  card.classList.add('active-tier-card');
1034
- btn.className = 'btn btn-success btn-block btn-select-tier';
1035
- btn.innerText = 'Current Plan';
1036
- btn.disabled = true;
1037
- } else {
1038
- card.classList.remove('active-tier-card');
1039
- btn.disabled = false;
1040
- if (t === 'pro') {
1240
+ if (isScheduledCancel) {
1041
1241
  btn.className = 'btn btn-primary btn-block btn-select-tier';
1042
- btn.innerText = 'Upgrade to Pro';
1242
+ btn.innerHTML = `<i class="fa-solid fa-rotate-left"></i> Resume ${current.name.split(' ')[0]}`;
1243
+ btn.disabled = false;
1043
1244
  } else {
1044
- btn.className = 'btn btn-outline btn-block btn-select-tier';
1045
- btn.innerText = t === 'enterprise' ? 'Upgrade to Enterprise' : 'Switch to Free';
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
+ }
1046
1291
  }
1047
1292
  }
1048
1293
  });
@@ -1094,6 +1339,9 @@ async function checkServerStatusAndSession() {
1094
1339
  if (typeof data.auth.creditsTotal === 'number') state.creditsTotal = data.auth.creditsTotal;
1095
1340
  if (data.auth.currentTier) state.currentTier = data.auth.currentTier;
1096
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;
1097
1345
  if (typeof data.auth.byokEnabled === 'boolean') state.byokEnabled = data.auth.byokEnabled;
1098
1346
  if (data.auth.customLlmProvider) state.customLlmProvider = data.auth.customLlmProvider;
1099
1347
  if (typeof data.auth.customApiKey === 'string') state.customApiKey = data.auth.customApiKey;
@@ -1432,6 +1680,15 @@ function updateUserUI() {
1432
1680
  if (getEl('input-byok-endpoint')) getEl('input-byok-endpoint').value = state.customBaseUrl || '';
1433
1681
  if (getEl('input-byok-key')) getEl('input-byok-key').value = state.customApiKey || '';
1434
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
+ }
1435
1692
  }
1436
1693
 
1437
1694
  async function handleLogout() {
@@ -1461,6 +1718,9 @@ function saveSessionToStorage() {
1461
1718
  localStorage.setItem('scout_credits_used', String(state.creditsUsed));
1462
1719
  localStorage.setItem('scout_credits_total', String(state.creditsTotal));
1463
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 || '');
1464
1724
  localStorage.setItem('scout_byok_enabled', String(state.byokEnabled));
1465
1725
  localStorage.setItem('scout_byok_provider', state.customLlmProvider);
1466
1726
  localStorage.setItem('scout_byok_key', state.customApiKey);
@@ -1481,6 +1741,9 @@ async function syncAuthWithServer() {
1481
1741
  creditsTotal: state.creditsTotal,
1482
1742
  currentTier: state.currentTier,
1483
1743
  renewAt: state.renewAt,
1744
+ cancelAtPeriodEnd: state.cancelAtPeriodEnd,
1745
+ pendingDowngrade: state.pendingDowngrade,
1746
+ paidTier: state.paidTier,
1484
1747
  byokEnabled: state.byokEnabled,
1485
1748
  customLlmProvider: state.customLlmProvider,
1486
1749
  customApiKey: state.customApiKey,
package/web/styles.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Design System & Visual Engine v8.0.7
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.7
1066
+ Visual Perception & Live Screen Sharing Engine v8.0.8
1067
1067
  ========================================================================== */
1068
1068
  .vision-banner {
1069
1069
  margin-bottom: 28px;
@@ -1,103 +0,0 @@
1
- [debug] [2026-09-11T13:38:39.856Z] ----------------------------------------------------------------------
2
- [debug] [2026-09-11T13:38:39.860Z] Command: C:\Program Files\nodejs\node.exe C:\Users\rishi\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js deploy --only hosting
3
- [debug] [2026-09-11T13:38:39.860Z] CLI Version: 15.30.0
4
- [debug] [2026-09-11T13:38:39.860Z] Platform: win32
5
- [debug] [2026-09-11T13:38:39.861Z] Node Version: v22.20.0
6
- [debug] [2026-09-11T13:38:39.861Z] Detected Agent: unknown
7
- [debug] [2026-09-11T13:38:39.861Z] Time: Fri Sep 11 2026 19:08:39 GMT+0530 (India Standard Time)
8
- [debug] [2026-09-11T13:38:39.861Z] ----------------------------------------------------------------------
9
- [debug]
10
- [debug] [2026-09-11T13:38:44.368Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
11
- [debug] [2026-09-11T13:38:44.369Z] > authorizing via signed-in user (rishishandilya40@gmail.com)
12
- [debug] [2026-09-11T13:38:44.369Z] [iam] checking project frontterrain for permissions ["firebase.projects.get","firebasehosting.sites.update"]
13
- [debug] [2026-09-11T13:38:44.390Z] Checked if tokens are valid: true, expires at: 1789136234965
14
- [debug] [2026-09-11T13:38:44.391Z] Checked if tokens are valid: true, expires at: 1789136234965
15
- [debug] [2026-09-11T13:38:44.392Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions [none]
16
- [debug] [2026-09-11T13:38:44.393Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions x-goog-user-project=frontterrain
17
- [debug] [2026-09-11T13:38:44.393Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions {"permissions":["firebase.projects.get","firebasehosting.sites.update"]}
18
- [debug] [2026-09-11T13:39:05.264Z] ----------------------------------------------------------------------
19
- [debug] [2026-09-11T13:39:05.268Z] Command: C:\Program Files\nodejs\node.exe C:\Users\rishi\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js deploy --only hosting
20
- [debug] [2026-09-11T13:39:05.269Z] CLI Version: 15.30.0
21
- [debug] [2026-09-11T13:39:05.269Z] Platform: win32
22
- [debug] [2026-09-11T13:39:05.269Z] Node Version: v22.20.0
23
- [debug] [2026-09-11T13:39:05.270Z] Detected Agent: unknown
24
- [debug] [2026-09-11T13:39:05.270Z] Time: Fri Sep 11 2026 19:09:05 GMT+0530 (India Standard Time)
25
- [debug] [2026-09-11T13:39:05.270Z] ----------------------------------------------------------------------
26
- [debug]
27
- [debug] [2026-09-11T13:39:08.785Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
28
- [debug] [2026-09-11T13:39:08.786Z] > authorizing via signed-in user (rishishandilya40@gmail.com)
29
- [debug] [2026-09-11T13:39:08.786Z] [iam] checking project frontterrain for permissions ["firebase.projects.get","firebasehosting.sites.update"]
30
- [debug] [2026-09-11T13:39:08.804Z] Checked if tokens are valid: true, expires at: 1789136234965
31
- [debug] [2026-09-11T13:39:08.804Z] Checked if tokens are valid: true, expires at: 1789136234965
32
- [debug] [2026-09-11T13:39:08.807Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions [none]
33
- [debug] [2026-09-11T13:39:08.807Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions x-goog-user-project=frontterrain
34
- [debug] [2026-09-11T13:39:08.808Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions {"permissions":["firebase.projects.get","firebasehosting.sites.update"]}
35
- [debug] [2026-09-11T13:39:10.817Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions 200
36
- [debug] [2026-09-11T13:39:10.817Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/frontterrain:testIamPermissions {"permissions":["firebase.projects.get","firebasehosting.sites.update"]}
37
- [debug] [2026-09-11T13:39:10.818Z] Checked if tokens are valid: true, expires at: 1789136234965
38
- [debug] [2026-09-11T13:39:10.818Z] Checked if tokens are valid: true, expires at: 1789136234965
39
- [debug] [2026-09-11T13:39:10.819Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/frontterrain [none]
40
- [debug] [2026-09-11T13:39:11.602Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/frontterrain 200
41
- [debug] [2026-09-11T13:39:11.602Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/frontterrain {"projectId":"frontterrain","projectNumber":"443977559241","displayName":"frontterrain","name":"projects/frontterrain","resources":{"hostingSite":"frontterrain","realtimeDatabaseInstance":"frontterrain-default-rtdb"},"state":"ACTIVE","labels":{"firebase":"enabled","firebase/storage-default-bucket":"created","firebaseworkflows/environment":"production"},"etag":"1_78a6722f-2aff-4818-b8b5-030f66204476"}
42
- [info]
43
- [info] === Deploying to 'frontterrain'...
44
- [info]
45
- [info] i deploying hosting
46
- [debug] [2026-09-11T13:39:11.607Z] Checked if tokens are valid: true, expires at: 1789136234965
47
- [debug] [2026-09-11T13:39:11.607Z] Checked if tokens are valid: true, expires at: 1789136234965
48
- [debug] [2026-09-11T13:39:11.607Z] >>> [apiv2][query] GET https://firebasehosting.googleapis.com/v1beta1/projects/frontterrain/sites/tryscout [none]
49
- [debug] [2026-09-11T13:39:13.065Z] <<< [apiv2][status] GET https://firebasehosting.googleapis.com/v1beta1/projects/frontterrain/sites/tryscout 200
50
- [debug] [2026-09-11T13:39:13.065Z] <<< [apiv2][body] GET https://firebasehosting.googleapis.com/v1beta1/projects/frontterrain/sites/tryscout {"name":"projects/frontterrain/sites/tryscout","defaultUrl":"https://tryscout.web.app","type":"USER_SITE"}
51
- [debug] [2026-09-11T13:39:13.069Z] Checked if tokens are valid: true, expires at: 1789136234965
52
- [debug] [2026-09-11T13:39:13.069Z] Checked if tokens are valid: true, expires at: 1789136234965
53
- [debug] [2026-09-11T13:39:13.069Z] >>> [apiv2][query] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/tryscout/versions [none]
54
- [debug] [2026-09-11T13:39:13.069Z] >>> [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/tryscout/versions {"status":"CREATED","labels":{"deployment-tool":"cli-firebase"}}
55
- [debug] [2026-09-11T13:39:14.161Z] <<< [apiv2][status] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/tryscout/versions 200
56
- [debug] [2026-09-11T13:39:14.161Z] <<< [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/tryscout/versions {"name":"projects/443977559241/sites/tryscout/versions/4bdc9aa79fd6b3f1","status":"CREATED","config":{},"labels":{"deployment-tool":"cli-firebase"}}
57
- [info] i hosting[tryscout]: beginning deploy...
58
- [info] i hosting[tryscout]: found 7 files in web
59
- [debug] [2026-09-11T13:39:14.179Z] [hosting] uploading with 200 concurrency
60
- [debug] [2026-09-11T13:39:14.212Z] Checked if tokens are valid: true, expires at: 1789136234965
61
- [debug] [2026-09-11T13:39:14.213Z] Checked if tokens are valid: true, expires at: 1789136234965
62
- [debug] [2026-09-11T13:39:14.214Z] [hosting] hash cache [d2Vi] stored for 7 files
63
- [debug] [2026-09-11T13:39:14.215Z] [hosting][hash queue][FINAL] {"max":26,"min":1,"avg":20.714285714285715,"active":0,"complete":7,"success":7,"errored":0,"retried":0,"total":7,"elapsed":32}
64
- [debug] [2026-09-11T13:39:14.216Z] >>> [apiv2][query] POST https://firebasehosting.googleapis.com/v1beta1/projects/443977559241/sites/tryscout/versions/4bdc9aa79fd6b3f1:populateFiles [none]
65
- [debug] [2026-09-11T13:39:14.216Z] >>> [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/443977559241/sites/tryscout/versions/4bdc9aa79fd6b3f1:populateFiles {"files":{"/ScoutFavicon.png":"41a9ca9abf7acd2976e78c72a72285b49cc0e64b2b5eeb2c2b88c69628fc618a","/paid_plans.html":"4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3","/styles.css":"802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee","/authentication.html":"8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c","/web_cli.html":"1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2","/index.html":"2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61","/app.js":"ff780163c1af103d5c054e226db5bee558df5e08d8684b97ae9dcfaf8f94a1d0"}}
66
- [debug] [2026-09-11T13:39:15.164Z] <<< [apiv2][status] POST https://firebasehosting.googleapis.com/v1beta1/projects/443977559241/sites/tryscout/versions/4bdc9aa79fd6b3f1:populateFiles 200
67
- [debug] [2026-09-11T13:39:15.164Z] <<< [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/443977559241/sites/tryscout/versions/4bdc9aa79fd6b3f1:populateFiles {"uploadRequiredHashes":["4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3","802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee","8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c","1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2","2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61","ff780163c1af103d5c054e226db5bee558df5e08d8684b97ae9dcfaf8f94a1d0"],"uploadUrl":"https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files"}
68
- [debug] [2026-09-11T13:39:15.165Z] Checked if tokens are valid: true, expires at: 1789136234965
69
- [debug] [2026-09-11T13:39:15.165Z] Checked if tokens are valid: true, expires at: 1789136234965
70
- [debug] [2026-09-11T13:39:15.165Z] Checked if tokens are valid: true, expires at: 1789136234965
71
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
72
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
73
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
74
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
75
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
76
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
77
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
78
- [debug] [2026-09-11T13:39:15.166Z] Checked if tokens are valid: true, expires at: 1789136234965
79
- [debug] [2026-09-11T13:39:15.167Z] Checked if tokens are valid: true, expires at: 1789136234965
80
- [debug] [2026-09-11T13:39:15.167Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3 [none]
81
- [debug] [2026-09-11T13:39:15.167Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3 [stream]
82
- [debug] [2026-09-11T13:39:15.169Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee [none]
83
- [debug] [2026-09-11T13:39:15.169Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee [stream]
84
- [debug] [2026-09-11T13:39:15.170Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c [none]
85
- [debug] [2026-09-11T13:39:15.170Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c [stream]
86
- [debug] [2026-09-11T13:39:15.171Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2 [none]
87
- [debug] [2026-09-11T13:39:15.171Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2 [stream]
88
- [debug] [2026-09-11T13:39:15.172Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61 [none]
89
- [debug] [2026-09-11T13:39:15.172Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61 [stream]
90
- [debug] [2026-09-11T13:39:15.173Z] >>> [apiv2][query] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/ff780163c1af103d5c054e226db5bee558df5e08d8684b97ae9dcfaf8f94a1d0 [none]
91
- [debug] [2026-09-11T13:39:15.173Z] >>> [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/ff780163c1af103d5c054e226db5bee558df5e08d8684b97ae9dcfaf8f94a1d0 [stream]
92
- [debug] [2026-09-11T13:39:15.180Z] [hosting][populate queue][FINAL] {"max":955,"min":955,"avg":955,"active":0,"complete":1,"success":1,"errored":0,"retried":0,"total":1,"elapsed":968}
93
- [debug] [2026-09-11T13:39:15.181Z] [hosting] uploads queued: 6
94
- [debug] [2026-09-11T13:39:17.113Z] <<< [apiv2][status] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee 200
95
- [debug] [2026-09-11T13:39:17.113Z] <<< [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/802a23ff4b01e6897011490ebe76ad7943e2ec4e7a0e0d31a0fb8277ece237ee [stream]
96
- [debug] [2026-09-11T13:39:17.560Z] <<< [apiv2][status] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2 200
97
- [debug] [2026-09-11T13:39:17.560Z] <<< [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/1257e1531714ba94225ad9d61fd469ba3c224052b16b2f15a93be930712e09f2 [stream]
98
- [debug] [2026-09-11T13:39:17.789Z] <<< [apiv2][status] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61 200
99
- [debug] [2026-09-11T13:39:17.789Z] <<< [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/2ebcbf5fffc488928cef4ed75745a81314776e005560e33c3870bb4d14068c61 [stream]
100
- [debug] [2026-09-11T13:39:17.795Z] <<< [apiv2][status] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c 200
101
- [debug] [2026-09-11T13:39:17.795Z] <<< [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/8c072cd5ef13845d7eea447685fc4cf1ca5c34ff1e8b8ebf1ae2b9e69c6c8b8c [stream]
102
- [debug] [2026-09-11T13:39:17.911Z] <<< [apiv2][status] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3 200
103
- [debug] [2026-09-11T13:39:17.911Z] <<< [apiv2][body] POST https://upload-firebasehosting.googleapis.com/upload/sites/tryscout/versions/4bdc9aa79fd6b3f1/files/4575f51ff7a5d36601fe9a1aa2f0e164d1621daa4befca152c97338ef6b802b3 [stream]