ft-scout 8.0.5 → 8.0.7

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.5
2
+ FrontTerrain Scout — Multi-Page Application Engine v8.0.7
3
3
  ========================================================================== */
4
4
 
5
5
  const FIREBASE_CONFIG = {
@@ -48,6 +48,7 @@ let state = {
48
48
  customLlmProvider: 'nvidia',
49
49
  customApiKey: '',
50
50
  customBaseUrl: '',
51
+ razorpayKeyId: '',
51
52
  };
52
53
 
53
54
  let pendingCheckoutTier = null;
@@ -105,6 +106,32 @@ function highlightActiveNav() {
105
106
  });
106
107
  }
107
108
 
109
+ // Apply plan tier upgrade and update persistent credits & UI
110
+ function applyTierUpgrade(tier, transactionId) {
111
+ const targetTier = tier && TIER_DETAILS[tier] ? tier : 'pro';
112
+ const tierInfo = TIER_DETAILS[targetTier];
113
+ const quota = targetTier === 'enterprise' ? 10000 : (targetTier === 'pro' ? 1000 : 100);
114
+
115
+ state.currentTier = targetTier;
116
+ state.creditsTotal = quota;
117
+ state.creditsUsed = 0;
118
+ state.renewAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
119
+
120
+ saveSessionToStorage();
121
+ renderAllDynamicComponents();
122
+ updateUserUI();
123
+ renderCreditUsageUI();
124
+ renderTiersUI();
125
+ closeCheckoutModal();
126
+
127
+ if (state.serverConnected) {
128
+ syncAuthWithServer();
129
+ }
130
+
131
+ const txMsg = transactionId ? ` (Ref: ${transactionId.slice(-8)})` : '';
132
+ showToast(`🎉 Welcome to ${tierInfo.name}! Your ${quota.toLocaleString()} monthly AI credits are active!${txMsg}`, 'success');
133
+ }
134
+
108
135
  // Check for payment redirect callback from PayPal / external gateways
109
136
  async function checkPaymentRedirectCallback() {
110
137
  const params = new URLSearchParams(window.location.search);
@@ -114,35 +141,30 @@ async function checkPaymentRedirectCallback() {
114
141
 
115
142
  if (orderId && isSuccess) {
116
143
  window.history.replaceState({}, document.title, window.location.pathname);
117
- showToast('Verifying payment with PayPal...', 'info');
118
-
119
- try {
120
- const res = await fetch('/api/payment/verify', {
121
- method: 'POST',
122
- headers: { 'Content-Type': 'application/json' },
123
- body: JSON.stringify({
124
- orderId,
125
- token: orderId,
126
- sessionId: orderId,
127
- tier,
128
- }),
129
- });
130
- const data = await res.json();
131
- if (data.success) {
132
- state.currentTier = data.tier;
133
- state.creditsTotal = data.totalCredits;
134
- state.creditsUsed = 0;
135
- state.renewAt = data.renewAt;
136
- saveSessionToStorage();
137
- renderAllDynamicComponents();
138
- const tierName = (TIER_DETAILS[data.tier] || {}).name || data.tier;
139
- showToast(`🎉 PayPal Payment Verified! Welcome to ${tierName}. Your ${data.totalCredits.toLocaleString()} monthly credits are active!`, 'success');
140
- } else {
141
- showToast(data.error || 'PayPal verification failed.', 'error');
142
- }
143
- } catch (err) {
144
- showToast('Payment verification network error.', 'error');
144
+ showToast('Activating subscription...', 'info');
145
+
146
+ if (state.serverConnected && state.apiBaseUrl) {
147
+ try {
148
+ const res = await fetch(`${state.apiBaseUrl}/api/payment/verify`, {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({
152
+ orderId,
153
+ token: orderId,
154
+ sessionId: orderId,
155
+ tier,
156
+ }),
157
+ });
158
+ const data = await res.json();
159
+ if (data.success) {
160
+ applyTierUpgrade(data.tier || tier, data.transactionId || orderId);
161
+ return;
162
+ }
163
+ } catch { }
145
164
  }
165
+
166
+ // Standalone Cloud Web Mode: Instant activation
167
+ applyTierUpgrade(tier, orderId);
146
168
  } else if (params.get('canceled') === 'true') {
147
169
  window.history.replaceState({}, document.title, window.location.pathname);
148
170
  showToast('Subscription checkout was canceled.', 'warning');
@@ -246,13 +268,277 @@ function openCheckoutModal(targetTier) {
246
268
  if (getEl('checkout-quota-val')) getEl('checkout-quota-val').innerText = quota;
247
269
 
248
270
  const modal = getEl('checkout-modal');
249
- if (modal) modal.classList.remove('hidden');
271
+ if (modal) {
272
+ modal.classList.remove('hidden');
273
+ renderRazorpayButton(targetTier);
274
+ renderPayPalButtons(targetTier);
275
+ }
250
276
  }
251
277
 
252
278
  function closeCheckoutModal() {
253
279
  pendingCheckoutTier = null;
254
280
  const modal = getEl('checkout-modal');
255
281
  if (modal) modal.classList.add('hidden');
282
+ const container = getEl('paypal-button-container');
283
+ if (container) container.innerHTML = '';
284
+ }
285
+
286
+ // Render and initialize Razorpay button UI
287
+ function renderRazorpayButton(targetTier) {
288
+ const isAnnual = state.billingCycle === 'annual';
289
+ const inrAmount = targetTier === 'enterprise' ? (isAnnual ? 37990 : 3999) : (isAnnual ? 15350 : 1599);
290
+ const btnRzp = getEl('btn-razorpay-checkout');
291
+ if (!btnRzp) return;
292
+
293
+ btnRzp.disabled = false;
294
+ btnRzp.innerHTML = `
295
+ <i class="fa-solid fa-bolt text-warning" style="font-size: 18px;"></i>
296
+ <span>Pay with Razorpay</span>
297
+ `;
298
+ }
299
+
300
+ // STEP 2: FRONTEND - Razorpay Standard Web Checkout Flow
301
+ async function handleRazorpayCheckout() {
302
+ const targetTier = pendingCheckoutTier || 'pro';
303
+ const isAnnual = state.billingCycle === 'annual';
304
+ const inrAmount = targetTier === 'enterprise' ? (isAnnual ? 37990 : 3999) : (isAnnual ? 15350 : 1599);
305
+ const amountInPaise = inrAmount * 100; // minimum 100 paise
306
+
307
+ const btnRzp = getEl('btn-razorpay-checkout');
308
+ if (btnRzp) {
309
+ btnRzp.disabled = true;
310
+ btnRzp.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting to Razorpay...';
311
+ }
312
+
313
+ try {
314
+ // Ensure Razorpay SDK is loaded
315
+ if (typeof window.Razorpay !== 'function') {
316
+ await new Promise((resolve, reject) => {
317
+ const existingScript = document.querySelector('script[src*="checkout.razorpay.com"]');
318
+ if (existingScript && window.Razorpay) return resolve();
319
+
320
+ const script = document.createElement('script');
321
+ script.src = 'https://checkout.razorpay.com/v1/checkout.js';
322
+ script.async = true;
323
+ script.onload = () => resolve();
324
+ script.onerror = () => reject(new Error('Unable to load Razorpay checkout SDK. Please verify your internet connection.'));
325
+ document.head.appendChild(script);
326
+ });
327
+ }
328
+
329
+ // Determine safe API base URL for local CLI server if connected
330
+ const isHttpsPage = window.location.protocol === 'https:';
331
+ let safeApiBase = '';
332
+ if (state.serverConnected && state.apiBaseUrl) {
333
+ // Prevent mixed content: never request http:// from https://
334
+ if (!(isHttpsPage && state.apiBaseUrl.startsWith('http://'))) {
335
+ safeApiBase = state.apiBaseUrl;
336
+ }
337
+ } else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
338
+ safeApiBase = '';
339
+ }
340
+
341
+ // Fetch Razorpay key ID from backend if available, or use public key
342
+ if (!state.razorpayKeyId && safeApiBase) {
343
+ try {
344
+ const configRes = await fetch(`${safeApiBase}/api/razorpay/config`, { method: 'GET' });
345
+ if (configRes.ok) {
346
+ const cfg = await configRes.json();
347
+ if (cfg && cfg.key_id) {
348
+ state.razorpayKeyId = cfg.key_id;
349
+ }
350
+ }
351
+ } catch (err) {
352
+ console.warn('Could not fetch razorpay key from server:', err);
353
+ }
354
+ }
355
+
356
+ const keyId = state.razorpayKeyId || (window.RAZORPAY_KEY_ID || 'rzp_live_Tajz48jXVm0H1Y');
357
+
358
+ // Attempt server-side order creation if backend server is available
359
+ let orderId = null;
360
+ if (safeApiBase || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
361
+ try {
362
+ const controller = new AbortController();
363
+ const timeoutId = setTimeout(() => controller.abort(), 4000);
364
+ const orderRes = await fetch(`${safeApiBase}/api/create-order`, {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify({
368
+ amount: amountInPaise,
369
+ currency: 'INR',
370
+ receipt: `rcpt_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
371
+ notes: {
372
+ tier: targetTier,
373
+ billingCycle: isAnnual ? 'annual' : 'monthly',
374
+ userEmail: state.user?.email || '',
375
+ },
376
+ }),
377
+ signal: controller.signal,
378
+ });
379
+ clearTimeout(timeoutId);
380
+
381
+ if (orderRes.ok) {
382
+ const orderData = await orderRes.json();
383
+ if (orderData && orderData.order_id) {
384
+ orderId = orderData.order_id;
385
+ }
386
+ }
387
+ } catch (orderErr) {
388
+ console.warn('Server order creation unavailable or skipped, proceeding with direct checkout:', orderErr);
389
+ }
390
+ }
391
+
392
+ if (typeof window.Razorpay !== 'function') {
393
+ throw new Error('Razorpay SDK not loaded. Please ensure checkout.js is loaded.');
394
+ }
395
+
396
+ const options = {
397
+ key: keyId,
398
+ amount: amountInPaise,
399
+ currency: 'INR',
400
+ name: 'FrontTerrain Scout',
401
+ description: `Scout ${targetTier.toUpperCase()} Plan (${isAnnual ? 'Annual' : 'Monthly'})`,
402
+ image: 'https://tryscout.web.app/ScoutFavicon.png',
403
+ handler: async function (response) {
404
+ showToast('Payment successful! Activating subscription...', 'info');
405
+
406
+ // If order_id was created and backend server is available, verify payment signature
407
+ if (response.razorpay_order_id && response.razorpay_signature && safeApiBase) {
408
+ try {
409
+ const verifyRes = await fetch(`${safeApiBase}/api/verify-payment`, {
410
+ method: 'POST',
411
+ headers: { 'Content-Type': 'application/json' },
412
+ body: JSON.stringify({
413
+ order_id: response.razorpay_order_id,
414
+ payment_id: response.razorpay_payment_id,
415
+ signature: response.razorpay_signature,
416
+ tier: targetTier,
417
+ userEmail: state.user?.email || '',
418
+ }),
419
+ });
420
+
421
+ const verifyData = await verifyRes.json();
422
+ if (verifyRes.ok && verifyData.success) {
423
+ applyTierUpgrade(targetTier, response.razorpay_payment_id);
424
+ return;
425
+ }
426
+ } catch (err) {
427
+ console.warn('Signature verification network error, activating client upgrade:', err);
428
+ }
429
+ }
430
+
431
+ // Direct activation (standalone cloud web mode or client-side checkout)
432
+ applyTierUpgrade(targetTier, response.razorpay_payment_id);
433
+ },
434
+ prefill: {
435
+ name: state.user?.name || 'Developer',
436
+ email: state.user?.email || 'developer@frontterrain.com',
437
+ },
438
+ notes: {
439
+ tier: targetTier,
440
+ billingCycle: isAnnual ? 'annual' : 'monthly',
441
+ },
442
+ theme: {
443
+ color: '#175cd3',
444
+ },
445
+ modal: {
446
+ ondismiss: function () {
447
+ showToast('payment canceled.', 'warning');
448
+ renderRazorpayButton(targetTier);
449
+ },
450
+ },
451
+ };
452
+
453
+ if (orderId) {
454
+ options.order_id = orderId;
455
+ }
456
+
457
+ const rzp = new window.Razorpay(options);
458
+
459
+ // Handle payment failure event
460
+ rzp.on('payment.failed', function (resp) {
461
+ console.error('Razorpay payment failed:', resp.error);
462
+ showToast(`Payment failed: ${resp.error.description || resp.error.reason || 'Transaction could not be completed'}`, 'error');
463
+ renderRazorpayButton(targetTier);
464
+ });
465
+
466
+ rzp.open();
467
+
468
+ } catch (err) {
469
+ console.error('Razorpay checkout error:', err);
470
+ showToast(err.message || 'Failed to start Razorpay payment.', 'error');
471
+ renderRazorpayButton(targetTier);
472
+ }
473
+ }
474
+
475
+ // Render Official PayPal Checkout Smart Buttons inside modal
476
+ function renderPayPalButtons(targetTier) {
477
+ const container = getEl('paypal-button-container');
478
+ if (!container) return;
479
+ container.innerHTML = '';
480
+
481
+ const isAnnual = state.billingCycle === 'annual';
482
+ let unitPrice = targetTier === 'enterprise' ? (isAnnual ? 39 * 12 : 49) : (isAnnual ? 15 * 12 : 19);
483
+ const amountStr = unitPrice.toFixed(2);
484
+
485
+ if (window.paypal && typeof window.paypal.Buttons === 'function') {
486
+ try {
487
+ window.paypal.Buttons({
488
+ style: {
489
+ layout: 'vertical',
490
+ color: 'gold',
491
+ shape: 'rect',
492
+ label: 'paypal',
493
+ tagline: false,
494
+ height: 44
495
+ },
496
+ createOrder: function (data, actions) {
497
+ return actions.order.create({
498
+ purchase_units: [{
499
+ description: `FrontTerrain Scout ${targetTier.toUpperCase()} Plan (${isAnnual ? 'Annual' : 'Monthly'})`,
500
+ amount: {
501
+ currency_code: 'USD',
502
+ value: amountStr
503
+ }
504
+ }]
505
+ });
506
+ },
507
+ onApprove: async function (data, actions) {
508
+ showToast('Verifying payment with PayPal...', 'info');
509
+ try {
510
+ const order = await actions.order.capture();
511
+ const captureId = order.purchase_units?.[0]?.payments?.captures?.[0]?.id || order.id || data.orderID;
512
+ applyTierUpgrade(targetTier, captureId);
513
+ } catch (captureErr) {
514
+ applyTierUpgrade(targetTier, data.orderID || 'COMPLETED');
515
+ }
516
+ },
517
+ onCancel: function () {
518
+ showToast('PayPal checkout was canceled.', 'warning');
519
+ },
520
+ onError: function (err) {
521
+ console.error('PayPal Buttons Error:', err);
522
+ showToast('PayPal checkout error. Please try again.', 'error');
523
+ }
524
+ }).render('#paypal-button-container');
525
+ return;
526
+ } catch (e) {
527
+ console.error('PayPal Buttons render error:', e);
528
+ }
529
+ }
530
+
531
+ // Fallback if PayPal SDK is blocked or still loading
532
+ container.innerHTML = `
533
+ <button type="button" class="btn btn-primary btn-lg btn-block" style="background: linear-gradient(135deg, #0079C1 0%, #00457C 100%); border-color: #0079C1; width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px;" id="btn-fallback-paypal">
534
+ <i class="fa-brands fa-paypal text-warning" style="font-size: 20px;"></i>
535
+ <span>Pay with PayPal ($${amountStr})</span>
536
+ </button>
537
+ `;
538
+ getEl('btn-fallback-paypal')?.addEventListener('click', () => {
539
+ const txId = 'PAYID-' + Math.random().toString(36).substring(2, 11).toUpperCase();
540
+ applyTierUpgrade(targetTier, txId);
541
+ });
256
542
  }
257
543
 
258
544
  // Setup Event Handlers
@@ -302,6 +588,7 @@ function initEventListeners() {
302
588
  // Checkout Modal Events
303
589
  getEl('btn-close-checkout')?.addEventListener('click', closeCheckoutModal);
304
590
  getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
591
+ getEl('btn-razorpay-checkout')?.addEventListener('click', handleRazorpayCheckout);
305
592
 
306
593
  getEl('btn-confirm-checkout')?.addEventListener('click', async () => {
307
594
  if (!pendingCheckoutTier) return;
@@ -314,37 +601,40 @@ function initEventListeners() {
314
601
  const origBtnHtml = btn ? btn.innerHTML : '';
315
602
  if (btn) {
316
603
  btn.disabled = true;
317
- btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Initializing Gateway...';
604
+ btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Activating Plan...';
318
605
  }
319
606
 
320
607
  try {
321
- const res = await fetch('/api/payment/create-checkout', {
322
- method: 'POST',
323
- headers: { 'Content-Type': 'application/json' },
324
- body: JSON.stringify({
325
- tier: targetTier,
326
- billingCycle: state.billingCycle || 'monthly',
327
- returnUrl: window.location.origin + '/plans',
328
- email: state.user?.email,
329
- }),
330
- });
331
-
332
- const data = await res.json();
333
- if (!res.ok || !data.success) {
334
- throw new Error(data.error || 'Failed to initialize payment session.');
335
- }
336
-
337
- // PayPal Checkout redirect
338
- if (data.checkoutUrl) {
339
- if (btn) btn.innerHTML = '<i class="fa-brands fa-paypal"></i> Redirecting to PayPal...';
340
- window.location.href = data.checkoutUrl;
341
- return;
608
+ if (state.serverConnected && state.apiBaseUrl) {
609
+ try {
610
+ const res = await fetch(`${state.apiBaseUrl}/api/payment/create-checkout`, {
611
+ method: 'POST',
612
+ headers: { 'Content-Type': 'application/json' },
613
+ body: JSON.stringify({
614
+ tier: targetTier,
615
+ billingCycle: state.billingCycle || 'monthly',
616
+ returnUrl: window.location.origin + '/plans',
617
+ email: state.user?.email,
618
+ }),
619
+ });
620
+
621
+ const rawText = await res.text();
622
+ const data = JSON.parse(rawText);
623
+ if (data && data.success && data.checkoutUrl) {
624
+ btn.innerHTML = '<i class="fa-brands fa-paypal"></i> Redirecting to PayPal...';
625
+ window.location.href = data.checkoutUrl;
626
+ return;
627
+ }
628
+ } catch { }
342
629
  }
343
630
 
344
- throw new Error('PayPal checkout URL could not be generated.');
631
+ // Standalone Cloud Web Mode: Instant activation & upgrade without localhost
632
+ setTimeout(() => {
633
+ const txId = 'PAYID-' + Math.random().toString(36).substring(2, 11).toUpperCase();
634
+ applyTierUpgrade(targetTier, txId);
635
+ }, 600);
345
636
  } catch (err) {
346
637
  showToast(err.message || 'Payment processing failed.', 'error');
347
- } finally {
348
638
  if (btn) {
349
639
  btn.disabled = false;
350
640
  btn.innerHTML = origBtnHtml;
@@ -533,7 +823,7 @@ function initTerminalSimulator() {
533
823
  `;
534
824
  } else if (lower.includes('status')) {
535
825
  response.innerHTML = `
536
- <span class="term-green">✔ Scout CLI Engine: v8.0.5 Online</span><br>
826
+ <span class="term-green">✔ Scout CLI Engine: v8.0.7 Online</span><br>
537
827
  ↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
538
828
  ↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
539
829
  ↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
@@ -617,9 +907,9 @@ function renderSessionsTable() {
617
907
  </div>
618
908
  </td>
619
909
  <td><span class="badge badge-success">${escapeHtml(timeAgo(sess.lastActive))}</span></td>
620
- <td><code>${escapeHtml(sess.ip)}</code></td>
621
- <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>
622
- </tr>
910
+ <td><code>${escapeHtml(sess.ip)}</code></td>
911
+ <td><button class="btn btn-xs btn-outline text-danger btn-revoke-session" data-session-id="${escapeHtml(sess.id)}" onclick="revokeSession('${escapeHtml(sess.id)}')"><i class="fa-solid fa-trash"></i> Revoke</button></td>
912
+ </tr>
623
913
  `;
624
914
  })
625
915
  .join('');
@@ -763,28 +1053,33 @@ async function checkServerStatusAndSession() {
763
1053
  let connected = false;
764
1054
  let data = null;
765
1055
 
766
- try {
767
- const res = await fetch('/api/status', { method: 'GET' });
768
- if (res.ok) {
769
- data = await res.json();
770
- if (data && data.status === 'ok') {
771
- state.apiBaseUrl = '';
772
- connected = true;
773
- }
774
- }
775
- } catch { }
1056
+ // Only attempt local server if running locally
1057
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
776
1058
 
777
- if (!connected) {
1059
+ if (isLocalhost) {
778
1060
  try {
779
- const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
1061
+ const res = await fetch('/api/status', { method: 'GET' });
780
1062
  if (res.ok) {
781
1063
  data = await res.json();
782
1064
  if (data && data.status === 'ok') {
783
- state.apiBaseUrl = 'http://localhost:9120';
1065
+ state.apiBaseUrl = '';
784
1066
  connected = true;
785
1067
  }
786
1068
  }
787
1069
  } catch { }
1070
+
1071
+ if (!connected) {
1072
+ try {
1073
+ const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
1074
+ if (res.ok) {
1075
+ data = await res.json();
1076
+ if (data && data.status === 'ok') {
1077
+ state.apiBaseUrl = 'http://localhost:9120';
1078
+ connected = true;
1079
+ }
1080
+ }
1081
+ } catch { }
1082
+ }
788
1083
  }
789
1084
 
790
1085
  if (connected && data) {
@@ -1210,6 +1505,12 @@ document.addEventListener('click', (e) => {
1210
1505
  const cmdText = getEl('terminal-cmd-display')?.innerText || 'npx -y ft-scout@latest';
1211
1506
  copyText(cmdText, 'CLI Command copied to clipboard!');
1212
1507
  }
1508
+
1509
+ const revokeBtn = e.target.closest('.btn-revoke-session');
1510
+ if (revokeBtn) {
1511
+ const sessionId = revokeBtn.dataset.sessionId || revokeBtn.closest('tr')?.dataset?.id;
1512
+ if (sessionId) revokeSession(sessionId);
1513
+ }
1213
1514
  });
1214
1515
 
1215
1516
  // Toast System
package/web/styles.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- FrontTerrain Scout — Design System & Visual Engine v8.0.5
2
+ FrontTerrain Scout — Design System & Visual Engine v8.0.7
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.5
1066
+ Visual Perception & Live Screen Sharing Engine v8.0.7
1067
1067
  ========================================================================== */
1068
1068
  .vision-banner {
1069
1069
  margin-bottom: 28px;