neuroalpha 0.5.17 → 0.5.19
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/package.json +61 -60
- package/public/admin.html +603 -571
- package/public/app.js +96 -18
- package/public/landing.html +299 -73
- package/public/splat3d.html +671 -0
- package/public/styles.css +12 -3
- package/scripts/set-admin-password.mjs +30 -0
- package/server/api/routes.js +41 -14
- package/server/auth/index.js +35 -4
- package/server/billing/yookassa.js +142 -133
- package/server/brokers/index.js +81 -50
- package/server/data/moex.js +6 -5
- package/server/db.js +1 -1
- package/server/index.js +3 -3
- package/server/instance/central.js +514 -505
- package/server/model/dataset.js +8 -9
- package/server/model/edge.js +41 -19
- package/server/model/forecast.js +111 -17
- package/server/model/live.js +16 -18
- package/server/model/research.js +41 -15
- package/server/strategist/index.js +758 -757
- package/server/strategist/tools.js +707 -675
package/public/landing.html
CHANGED
|
@@ -333,10 +333,13 @@
|
|
|
333
333
|
<a href="#paas-sec">Как работает PaaS</a>
|
|
334
334
|
<a href="#brokers">Брокеры и мессенджеры</a>
|
|
335
335
|
<a href="#pricing">Тарифы</a>
|
|
336
|
+
<a href="/splat3d.html">3D-демо</a>
|
|
336
337
|
</nav>
|
|
337
|
-
<div style="display:flex;gap:10px" id="nav-auth">
|
|
338
|
-
<button class="btn" id="btn-login-open">Войти</button>
|
|
339
|
-
<
|
|
338
|
+
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap" id="nav-auth">
|
|
339
|
+
<button class="btn" id="btn-login-open" type="button">Войти</button>
|
|
340
|
+
<button class="btn primary" id="btn-reg-open" type="button">Регистрация</button>
|
|
341
|
+
<a class="btn" href="#pricing" id="btn-nav-cta">Тарифы →</a>
|
|
342
|
+
<button class="btn" id="btn-nav-logout" type="button" hidden style="color:#f87171;border-color:rgba(248,113,113,0.35)">Выйти</button>
|
|
340
343
|
</div>
|
|
341
344
|
</div>
|
|
342
345
|
</header>
|
|
@@ -600,7 +603,12 @@
|
|
|
600
603
|
|
|
601
604
|
<div class="modal" id="auth-modal" hidden>
|
|
602
605
|
<div class="modal-box">
|
|
603
|
-
<
|
|
606
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;background:#070b14;padding:4px;border-radius:10px;border:1px solid var(--line);margin-bottom:14px">
|
|
607
|
+
<button type="button" class="btn" id="tab-auth-reg" style="padding:8px 10px;font-size:13px;justify-content:center;border:0">Регистрация</button>
|
|
608
|
+
<button type="button" class="btn" id="tab-auth-login" style="padding:8px 10px;font-size:13px;justify-content:center;border:0">Вход в аккаунт</button>
|
|
609
|
+
</div>
|
|
610
|
+
<div id="auth-plan-banner" hidden style="margin-bottom:12px;padding:10px 12px;border-radius:10px;background:rgba(59,130,246,0.14);border:1px solid rgba(96,165,250,0.4);font-size:13px;color:#bfdbfe"></div>
|
|
611
|
+
<h3 id="auth-title" style="margin-top:0">Вход в НейроАльфа</h3>
|
|
604
612
|
<div id="tg-bot-login-box" hidden style="margin-bottom:8px">
|
|
605
613
|
<button type="button" class="oauth-btn full" id="btn-tg-bot-login" style="width:100%;background:rgba(36, 161, 222, 0.16);border-color:rgba(36, 161, 222, 0.45);color:#7dd3fc">
|
|
606
614
|
<span class="oauth-ico" style="background:#24a1de;color:#fff">✈</span>
|
|
@@ -637,10 +645,10 @@
|
|
|
637
645
|
</div>
|
|
638
646
|
<div class="or-sep" id="oauth-or-sep" hidden>или по Email и паролю</div>
|
|
639
647
|
<form id="auth-form">
|
|
640
|
-
<input name="name" id="auth-name" placeholder="Ваше имя" hidden>
|
|
641
|
-
<input name="email" type="email" required placeholder="Email">
|
|
648
|
+
<input name="name" id="auth-name" placeholder="Ваше имя (например, Иван)" hidden>
|
|
649
|
+
<input name="email" type="email" required placeholder="Ваш Email">
|
|
642
650
|
<input name="password" type="password" required minlength="6" placeholder="Пароль (не короче 6 символов)">
|
|
643
|
-
<label style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--muted)"><input type="checkbox" required checked style="width:16px;height:16px;margin-top:2px"><span>Принимаю условия <a href="/legal#offer" target="_blank" style="color:#93c5fd">Публичной оферты</a>, <a href="/legal#privacy" target="_blank" style="color:#93c5fd">Политики конфиденциальности</a> и <a href="/legal#risks" target="_blank" style="color:#93c5fd">Декларации о рисках</a></span></label>
|
|
651
|
+
<label id="auth-offer-row" style="display:flex;gap:8px;align-items:flex-start;font-size:12.5px;color:var(--muted)"><input type="checkbox" id="auth-offer-chk" required checked style="width:16px;height:16px;margin-top:2px"><span>Принимаю условия <a href="/legal#offer" target="_blank" style="color:#93c5fd">Публичной оферты</a>, <a href="/legal#privacy" target="_blank" style="color:#93c5fd">Политики конфиденциальности</a> и <a href="/legal#risks" target="_blank" style="color:#93c5fd">Декларации о рисках</a></span></label>
|
|
644
652
|
<div class="err" id="auth-err"></div>
|
|
645
653
|
<button class="btn primary" type="submit" id="auth-submit">Войти</button>
|
|
646
654
|
<button class="btn" type="button" id="auth-toggle">Нет аккаунта? Зарегистрироваться</button>
|
|
@@ -653,6 +661,7 @@
|
|
|
653
661
|
<div class="modal" id="offer-modal" hidden>
|
|
654
662
|
<div class="modal-box" style="max-width:660px">
|
|
655
663
|
<h3>Договор публичной оферты и Декларация о рисках</h3>
|
|
664
|
+
<div id="offer-account-banner" style="margin-bottom:12px"></div>
|
|
656
665
|
<div style="background:#070b14;border:1px solid var(--line);border-radius:10px;padding:14px 16px;max-height:260px;overflow-y:auto;font-size:13px;line-height:1.55;color:#cbd5e1;margin-bottom:14px">
|
|
657
666
|
<p style="margin-top:0"><b>1. Предмет договора:</b> Настоящая Публичная оферта регулирует предоставление доступа к облачной аналитической платформе «НейроАльфа» (PaaS/SaaS) согласно выбранному тарифному плану («Инвестор», «Профи», «VIP»).</p>
|
|
658
667
|
<p><b>2. Правовой статус (отсутствие ИИР и ДУ):</b> Сервис является исключительно программно-техническим средством (калькулятором статистических закономерностей). Исполнитель <b>не осуществляет инвестиционное консультирование</b>, не даёт индивидуальных инвестиционных рекомендаций (ФЗ № 39-ФЗ) и не осуществляет доверительное управление. Все сделки совершаются Пользователем самостоятельно на его личном брокерском счёте.</p>
|
|
@@ -809,56 +818,168 @@
|
|
|
809
818
|
</svg>`;
|
|
810
819
|
}
|
|
811
820
|
|
|
821
|
+
let lastMeMeta = {};
|
|
822
|
+
let paymentSuccessBanner = '';
|
|
823
|
+
const PLAN_TITLES = { investor: 'Инвестор (3 900 ₽/мес)', pro: 'Профи (9 900 ₽/мес)', vip: 'VIP', owner: 'Хозяин', none: 'Без подписки' };
|
|
824
|
+
const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
825
|
+
|
|
826
|
+
function syncNavAuth() {
|
|
827
|
+
const loginBtn = document.getElementById('btn-login-open');
|
|
828
|
+
const regBtn = document.getElementById('btn-reg-open');
|
|
829
|
+
const logoutBtn = document.getElementById('btn-nav-logout');
|
|
830
|
+
const navCta = document.getElementById('btn-nav-cta');
|
|
831
|
+
if (currentUser) {
|
|
832
|
+
if (loginBtn) loginBtn.textContent = `👤 ${currentUser.name} · ${currentUser.planName}`;
|
|
833
|
+
if (regBtn) regBtn.hidden = true;
|
|
834
|
+
if (logoutBtn) logoutBtn.hidden = false;
|
|
835
|
+
if (navCta) {
|
|
836
|
+
if (currentUser.role === 'admin') {
|
|
837
|
+
navCta.href = '/admin';
|
|
838
|
+
navCta.textContent = 'Консоль админа →';
|
|
839
|
+
navCta.hidden = false;
|
|
840
|
+
} else {
|
|
841
|
+
navCta.href = '#cabinet-box';
|
|
842
|
+
navCta.textContent = 'Личный кабинет →';
|
|
843
|
+
navCta.hidden = false;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
if (loginBtn) loginBtn.textContent = 'Войти';
|
|
848
|
+
if (regBtn) regBtn.hidden = false;
|
|
849
|
+
if (logoutBtn) logoutBtn.hidden = true;
|
|
850
|
+
if (navCta) {
|
|
851
|
+
navCta.href = '#pricing';
|
|
852
|
+
navCta.textContent = 'Тарифы →';
|
|
853
|
+
navCta.hidden = false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function doLogout(reopenRegWithPlan = null) {
|
|
859
|
+
await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
|
|
860
|
+
currentUser = null;
|
|
861
|
+
paymentSuccessBanner = '';
|
|
862
|
+
syncNavAuth();
|
|
863
|
+
renderCabinet();
|
|
864
|
+
if (reopenRegWithPlan) {
|
|
865
|
+
pendingPlan = reopenRegWithPlan;
|
|
866
|
+
openAuth(true);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
812
870
|
function renderCabinet() {
|
|
813
871
|
const cab = document.getElementById('cabinet-box');
|
|
872
|
+
syncNavAuth();
|
|
814
873
|
if (!cab) return;
|
|
815
874
|
if (!currentUser) {
|
|
816
875
|
cab.hidden = true;
|
|
817
876
|
return;
|
|
818
877
|
}
|
|
819
878
|
cab.hidden = false;
|
|
820
|
-
const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
821
|
-
// Терминал клиента работает на его машине (облако naXXXXXX.odant.org или свой компьютер) — см. блок «Где работать»
|
|
822
|
-
const navCta = document.getElementById('btn-nav-cta');
|
|
823
|
-
if (navCta) {
|
|
824
|
-
if (currentUser.role === 'admin') {
|
|
825
|
-
navCta.href = '/admin';
|
|
826
|
-
navCta.textContent = 'Панель админа →';
|
|
827
|
-
navCta.hidden = false;
|
|
828
|
-
} else {
|
|
829
|
-
navCta.hidden = true;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
879
|
const untilStr = currentUser.planUntil > Date.now()
|
|
833
880
|
? `оплачен до ${new Date(currentUser.planUntil).toLocaleDateString('ru-RU')}`
|
|
834
|
-
: 'бессрочно';
|
|
881
|
+
: (currentUser.plan && currentUser.plan !== 'none' ? 'бессрочно' : 'подписка не активна');
|
|
835
882
|
const capLim = currentUser.limits?.maxCapitalPerAccountRub
|
|
836
883
|
? (currentUser.limits.maxCapitalPerAccountRub >= 100000000 ? 'без ограничений' : `до ${(currentUser.limits.maxCapitalPerAccountRub / 1000000).toLocaleString('ru-RU')} млн ₽/счёт`)
|
|
837
|
-
: '
|
|
884
|
+
: 'требуется выбор тарифа';
|
|
885
|
+
const offerStr = currentUser.offerAcceptedAt
|
|
886
|
+
? `<span class="pos">подписана ✓ (${new Date(currentUser.offerAcceptedAt).toLocaleDateString('ru-RU')})</span>`
|
|
887
|
+
: '<span style="color:var(--muted)">подписывается при оформлении тарифа</span>';
|
|
888
|
+
const nt = lastMeMeta.notify || {};
|
|
889
|
+
const code = currentUser.notifyCode || '';
|
|
890
|
+
const tgUrl = nt.tgBotName && code ? `https://t.me/${encodeURIComponent(nt.tgBotName)}?start=${encodeURIComponent(code)}` : '';
|
|
891
|
+
const maxUrl = nt.maxBotName && code ? `https://max.ru/${encodeURIComponent(nt.maxBotName)}?start=${encodeURIComponent(code)}` : '';
|
|
892
|
+
const payments = Array.isArray(currentUser.payments) ? currentUser.payments : [];
|
|
893
|
+
const payRowsHtml = payments.length
|
|
894
|
+
? `<table style="width:100%;border-collapse:collapse;font-size:13px;margin-top:8px">
|
|
895
|
+
<thead><tr style="border-bottom:1px solid var(--line);color:var(--muted);text-align:left">
|
|
896
|
+
<th style="padding:6px 8px">Дата</th><th style="padding:6px 8px">Тариф</th><th style="padding:6px 8px">Период</th><th style="padding:6px 8px">Сумма</th><th style="padding:6px 8px">Статус</th>
|
|
897
|
+
</tr></thead>
|
|
898
|
+
<tbody>${payments.map((p) => {
|
|
899
|
+
const dt = p.created_at ? new Date(p.created_at).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
|
|
900
|
+
const stBadge = p.status === 'succeeded'
|
|
901
|
+
? '<span class="pos" style="font-weight:700">Оплачен ✓</span>'
|
|
902
|
+
: p.status === 'canceled'
|
|
903
|
+
? '<span class="neg">Отменён</span>'
|
|
904
|
+
: `<span style="color:#fbbf24">Ожидает подтверждения</span> <button type="button" class="btn" data-verify-pid="${escH(p.id)}" style="padding:2px 8px;font-size:11.5px;margin-left:6px">Проверить</button>`;
|
|
905
|
+
return `<tr style="border-bottom:1px solid rgba(148,163,184,0.1)">
|
|
906
|
+
<td style="padding:6px 8px;color:var(--muted)">${escH(dt)}</td>
|
|
907
|
+
<td style="padding:6px 8px"><b>${escH(PLAN_TITLES[p.plan] || p.plan)}</b></td>
|
|
908
|
+
<td style="padding:6px 8px">${Number(p.months || 1)} мес.</td>
|
|
909
|
+
<td style="padding:6px 8px"><b>${Number(p.amount_rub || 0).toLocaleString('ru-RU')} ₽</b></td>
|
|
910
|
+
<td style="padding:6px 8px">${stBadge}</td>
|
|
911
|
+
</tr>`;
|
|
912
|
+
}).join('')}</tbody>
|
|
913
|
+
</table>`
|
|
914
|
+
: '<div style="font-size:13px;color:var(--muted);margin-top:6px">Платежей по этому аккаунту пока не было. Выберите тариф ниже или активируйте промокод.</div>';
|
|
915
|
+
|
|
838
916
|
cab.innerHTML = `<div class="cab-card">
|
|
917
|
+
${paymentSuccessBanner ? `<div style="margin-bottom:16px;padding:12px 16px;border-radius:12px;background:rgba(16,185,129,0.16);border:1px solid rgba(16,185,129,0.45);color:#a7f3d0;font-size:14px;display:flex;justify-content:space-between;align-items:center;gap:12px"><span>${escH(paymentSuccessBanner)}</span><button type="button" class="btn" id="btn-close-pay-banner" style="padding:4px 10px;font-size:12px">ОК</button></div>` : ''}
|
|
839
918
|
<div class="cab-grid">
|
|
840
919
|
<div>
|
|
841
|
-
<div class="sec-tag" style="margin-bottom:4px">Личный кабинет
|
|
920
|
+
<div class="sec-tag" style="margin-bottom:4px">Личный кабинет клиента · ${currentUser.role === 'admin' ? 'Администратор платформы' : 'Аккаунт активен'}</div>
|
|
842
921
|
<h3 style="margin:0 0 8px;font-size:22px">${escH(currentUser.name)} <span style="color:var(--muted);font-weight:400;font-size:15px">(${escH(currentUser.email)})</span></h3>
|
|
843
|
-
<div style="margin-bottom:
|
|
844
|
-
Тариф: <b class="pos">${currentUser.planName}</b> (${untilStr}) · Брокерских счетов: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> (${capLim}) · Режим: <b>${currentUser.limits?.allowShorts ? 'в рост и в падение (с шортом)' : 'только в рост (без шорта)'}</b
|
|
845
|
-
|
|
846
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
847
|
-
<input id="cab-promo-in" placeholder="Промокод или VIP-токен" style="padding:8px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text);font-size:13.5px;width:220px">
|
|
848
|
-
<button type="button" class="btn" id="cab-promo-btn" style="padding:8px 14px;font-size:13px">Активировать код</button>
|
|
922
|
+
<div style="margin-bottom:10px;font-size:14.5px;color:#cbd5e1;line-height:1.55">
|
|
923
|
+
Тариф: <b class="${currentUser.plan === 'none' ? '' : 'pos'}">${escH(currentUser.planName)}</b> (${escH(untilStr)}) · Брокерских счетов: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> (${escH(capLim)}) · Режим: <b>${currentUser.limits?.allowShorts ? 'в рост и в падение (с шортом)' : 'только в рост (без шорта)'}</b><br>
|
|
924
|
+
Договор-оферта: ${offerStr}
|
|
849
925
|
</div>
|
|
850
926
|
</div>
|
|
851
|
-
<div style="display:flex;flex-direction:column;gap:
|
|
852
|
-
<div style="display:flex;gap:
|
|
853
|
-
<a class="btn" href="#pricing" style="flex:1"
|
|
854
|
-
|
|
927
|
+
<div style="display:flex;flex-direction:column;gap:8px;min-width:240px">
|
|
928
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
929
|
+
<a class="btn primary" href="#pricing" style="flex:1;justify-content:center">${currentUser.plan === 'none' ? 'Выбрать и оплатить тариф' : 'Продлить / Сменить тариф'}</a>
|
|
930
|
+
<button type="button" class="btn" id="cab-verify-btn" title="Проверить статус последнего платежа в ЮKassa">Проверить оплату</button>
|
|
931
|
+
</div>
|
|
932
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
933
|
+
${currentUser.role === 'admin' ? '<a class="btn" href="/admin" style="flex:1;justify-content:center">Консоль админа (/admin)</a><a class="btn" href="/?terminal=1" style="flex:1;justify-content:center">Терминал центра →</a>' : ''}
|
|
934
|
+
<button type="button" class="btn" id="cab-logout-btn" style="color:#f87171;border-color:rgba(248,113,113,0.35);justify-content:center">Выйти из аккаунта</button>
|
|
855
935
|
</div>
|
|
856
936
|
</div>
|
|
857
937
|
</div>
|
|
938
|
+
|
|
858
939
|
<div id="cab-workspace" style="margin-top:18px;padding-top:16px;border-top:1px solid var(--line)"></div>
|
|
940
|
+
|
|
941
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;margin-top:16px;padding-top:16px;border-top:1px solid var(--line)">
|
|
942
|
+
<div style="border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--bg)">
|
|
943
|
+
<div style="font-weight:700;margin-bottom:6px">📲 Уведомления и ИИ-Стратег в Telegram и MAX</div>
|
|
944
|
+
<div style="font-size:13px;color:#cbd5e1;line-height:1.5;margin-bottom:10px">
|
|
945
|
+
Ваш персональный код привязки: <b style="font-family:monospace;font-size:14px;color:#93c5fd">${escH(code || '—')}</b><br>
|
|
946
|
+
Статус: Telegram — ${currentUser.tgConnected ? '<b class="pos">привязан ✓</b>' : '<span style="color:var(--muted)">не привязан</span>'} · MAX — ${currentUser.maxConnected ? '<b class="pos">привязан ✓</b>' : '<span style="color:var(--muted)">не привязан</span>'}
|
|
947
|
+
</div>
|
|
948
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
949
|
+
${tgUrl ? `<a class="btn" href="${escH(tgUrl)}" target="_blank" rel="noopener" style="padding:7px 12px;font-size:12.5px">Подключить Telegram</a>` : ''}
|
|
950
|
+
${maxUrl ? `<a class="btn" href="${escH(maxUrl)}" target="_blank" rel="noopener" style="padding:7px 12px;font-size:12.5px">Подключить MAX</a>` : ''}
|
|
951
|
+
<button type="button" class="btn" id="cab-notify-test" style="padding:7px 12px;font-size:12.5px">Тест уведомления</button>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
<div style="border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--bg)">
|
|
956
|
+
<div style="font-weight:700;margin-bottom:6px">💳 Биллинг, промокоды и история платежей</div>
|
|
957
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:8px">
|
|
958
|
+
<input id="cab-promo-in" placeholder="Промокод или VIP-токен (VIP-…)" style="padding:7px 12px;border-radius:9px;border:1px solid var(--line);background:#070b14;color:var(--text);font-size:13px;flex:1;min-width:180px">
|
|
959
|
+
<button type="button" class="btn" id="cab-promo-btn" style="padding:7px 14px;font-size:12.5px">Активировать код</button>
|
|
960
|
+
</div>
|
|
961
|
+
${payRowsHtml}
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
859
964
|
</div>`;
|
|
860
965
|
renderWorkspace();
|
|
861
966
|
|
|
967
|
+
document.getElementById('btn-close-pay-banner')?.addEventListener('click', () => {
|
|
968
|
+
paymentSuccessBanner = '';
|
|
969
|
+
renderCabinet();
|
|
970
|
+
});
|
|
971
|
+
document.getElementById('cab-logout-btn')?.addEventListener('click', () => doLogout());
|
|
972
|
+
document.getElementById('cab-verify-btn')?.addEventListener('click', () => verifyPaymentNow());
|
|
973
|
+
cab.querySelectorAll('[data-verify-pid]').forEach((b) => b.addEventListener('click', () => verifyPaymentNow(b.dataset.verifyPid)));
|
|
974
|
+
document.getElementById('cab-notify-test')?.addEventListener('click', async () => {
|
|
975
|
+
try {
|
|
976
|
+
const r = await fetch('/api/auth/notify-test', { method: 'POST' });
|
|
977
|
+
const j = await r.json();
|
|
978
|
+
if (!r.ok) throw new Error(j.error || 'Не удалось отправить тест');
|
|
979
|
+
alert('Тестовое уведомление отправлено в привязанный мессенджер!');
|
|
980
|
+
} catch (e) { alert(e.message); }
|
|
981
|
+
});
|
|
982
|
+
|
|
862
983
|
document.getElementById('cab-promo-btn')?.addEventListener('click', () => {
|
|
863
984
|
const code = document.getElementById('cab-promo-in')?.value.trim();
|
|
864
985
|
if (!code) return;
|
|
@@ -871,48 +992,91 @@
|
|
|
871
992
|
const j = await r.json();
|
|
872
993
|
if (!r.ok) return alert(j.error || 'Ошибка активации кода');
|
|
873
994
|
currentUser = j.user;
|
|
995
|
+
paymentSuccessBanner = `Код успешно активирован! Включён тариф «${currentUser.planName}».`;
|
|
874
996
|
renderCabinet();
|
|
875
|
-
|
|
876
|
-
});
|
|
997
|
+
}, `Активация промокода / VIP-токена (${code})`);
|
|
877
998
|
});
|
|
878
999
|
}
|
|
879
1000
|
|
|
880
|
-
|
|
1001
|
+
async function verifyPaymentNow(paymentId = null, silent = false) {
|
|
1002
|
+
try {
|
|
1003
|
+
const r = await fetch('/api/billing/verify', {
|
|
1004
|
+
method: 'POST',
|
|
1005
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1006
|
+
body: JSON.stringify(paymentId ? { paymentId } : {}),
|
|
1007
|
+
});
|
|
1008
|
+
const j = await r.json();
|
|
1009
|
+
if (!r.ok) throw new Error(j.error || 'Не удалось проверить статус платежа');
|
|
1010
|
+
if (j.user) currentUser = j.user;
|
|
1011
|
+
if (j.status === 'succeeded') {
|
|
1012
|
+
paymentSuccessBanner = `Оплата прошла успешно! Активен тариф «${currentUser?.planName || ''}». Ваша персональная виртуальная машина PaaS подготовлена ниже.`;
|
|
1013
|
+
renderCabinet();
|
|
1014
|
+
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1015
|
+
} else if (!silent) {
|
|
1016
|
+
renderCabinet();
|
|
1017
|
+
alert(j.status === 'none' ? 'Неоплаченных счетов в ожидании не найдено.' : `Текущий статус платежа в ЮKassa: ${j.status}`);
|
|
1018
|
+
}
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
if (!silent) alert(e.message);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ---------- «Где работать»: персональная виртуальная машина PaaS или установка на свой компьютер (VIP по договору) ----------
|
|
881
1025
|
async function renderWorkspace() {
|
|
882
1026
|
const box = document.getElementById('cab-workspace');
|
|
883
1027
|
if (!box || !currentUser) return;
|
|
884
|
-
const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
885
1028
|
let me = null;
|
|
886
|
-
try {
|
|
887
|
-
|
|
1029
|
+
try {
|
|
1030
|
+
me = await (await fetch('/api/auth/me')).json();
|
|
1031
|
+
if (me?.user) {
|
|
1032
|
+
currentUser = me.user;
|
|
1033
|
+
lastMeMeta = me;
|
|
1034
|
+
}
|
|
1035
|
+
} catch { /* ignore */ }
|
|
1036
|
+
const list = me?.user?.instances || currentUser.instances || [];
|
|
888
1037
|
const dl = me?.downloads || {};
|
|
889
1038
|
const paid = currentUser.plan && currentUser.plan !== 'none' && currentUser.plan !== 'free';
|
|
890
1039
|
const canDesktop = Boolean(currentUser.limits?.allowDesktop);
|
|
891
1040
|
const ago = (ts) => {
|
|
892
|
-
if (!ts) return '
|
|
1041
|
+
if (!ts) return 'ожидает первого запуска в PaaS';
|
|
893
1042
|
const s = Math.round((Date.now() - ts) / 1000);
|
|
894
1043
|
return s < 120 ? `${s} с назад` : s < 7200 ? `${Math.round(s / 60)} мин назад` : `${Math.round(s / 3600)} ч назад`;
|
|
895
1044
|
};
|
|
896
1045
|
const card = (title, body) => `<div style="flex:1 1 300px;border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--bg)"><div style="font-weight:700;margin-bottom:8px">${title}</div>${body}</div>`;
|
|
897
1046
|
|
|
898
1047
|
const clouds = list.filter((i) => i.kind === 'cloud');
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1048
|
+
let cloudBody;
|
|
1049
|
+
if (currentUser.role === 'admin' && !clouds.length) {
|
|
1050
|
+
cloudBody = `<div style="font-size:13.5px;color:#cbd5e1;margin-bottom:10px">Для администратора («Хозяин») торговый терминал работает прямо на центральном сервере, а клиентские PaaS-машины управляются в консоли администратора.</div>
|
|
1051
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
1052
|
+
<a class="btn primary" href="/?terminal=1">Открыть торговый терминал →</a>
|
|
1053
|
+
<a class="btn" href="/admin">Управление машинами клиентов (/admin) →</a>
|
|
1054
|
+
</div>`;
|
|
1055
|
+
} else if (clouds.length) {
|
|
1056
|
+
cloudBody = clouds.map((i) => {
|
|
1057
|
+
const state = i.online
|
|
1058
|
+
? '<span class="pos" style="font-weight:700">● работает в облаке PaaS</span>'
|
|
1059
|
+
: ['pending', 'deploying', 'deployed'].includes(i.provisionState)
|
|
1060
|
+
? '<span style="color:#fbbf24">● выделена и готовится к запуску в PaaS</span>'
|
|
1061
|
+
: i.provisionState === 'error'
|
|
1062
|
+
? '<span class="neg">● ожидает повторного развёртывания в PaaS</span>'
|
|
1063
|
+
: `<span style="color:var(--muted)">● ${ago(i.lastSeen)}</span>`;
|
|
1064
|
+
return `<div style="font-size:14px;margin-bottom:10px">Персональная машина: <b>${escH(i.host || i.id)}</b> · ${state}</div>
|
|
1065
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
1066
|
+
<a class="btn primary" href="/api/instances/open?i=${encodeURIComponent(i.id)}">Перейти в торговый терминал →</a>
|
|
1067
|
+
</div>
|
|
1068
|
+
<div style="font-size:12.5px;color:var(--muted);margin-top:8px">При нажатии вы автоматически войдёте в свой изолированный торговый терминал на виртуальной машине PaaS по защищённому одноразовому ключу.</div>`;
|
|
1069
|
+
}).join('<hr style="border:0;border-top:1px solid var(--line);margin:10px 0">');
|
|
1070
|
+
} else if (paid) {
|
|
1071
|
+
cloudBody = '<div style="font-size:14px;color:var(--muted)">Ваша персональная виртуальная машина PaaS создаётся автоматически после подтверждения подписки — нажмите «Проверить оплату» или обновите страницу через минуту.</div>';
|
|
1072
|
+
} else {
|
|
1073
|
+
cloudBody = '<div style="font-size:13.5px;color:var(--muted);margin-bottom:10px">Оформите стандартную подписку («Инвестор» или «Профи») — в облаке PaaS автоматически создастся ваша персональная виртуальная машина с торговым терминалом и ИИ-Стратегом.</div><a class="btn primary" href="#pricing">Выбрать тариф →</a>';
|
|
1074
|
+
}
|
|
911
1075
|
|
|
912
1076
|
const desks = list.filter((i) => i.kind === 'desktop');
|
|
913
1077
|
let deskBody;
|
|
914
1078
|
if (!canDesktop) {
|
|
915
|
-
deskBody = '<div style="font-size:
|
|
1079
|
+
deskBody = '<div style="font-size:13.5px;color:var(--muted);margin-bottom:10px">Установка автономного терминала на собственный компьютер (Windows, Linux или Docker) предоставляется по индивидуальному договору на тарифе VIP.</div><a class="btn" href="#pricing">Обсудить тариф VIP</a>';
|
|
916
1080
|
} else {
|
|
917
1081
|
const links = [
|
|
918
1082
|
dl.windows ? `<a class="btn" href="${escH(dl.windows)}">⬇ Windows</a>` : '',
|
|
@@ -927,8 +1091,8 @@
|
|
|
927
1091
|
<button type="button" class="btn primary" id="cab-desktop-code">Получить код активации</button>
|
|
928
1092
|
<div id="cab-desktop-code-out" style="margin-top:8px;font-size:14px"></div>`;
|
|
929
1093
|
}
|
|
930
|
-
box.innerHTML = `<div class="sec-tag" style="margin-bottom:10px"
|
|
931
|
-
<div style="display:flex;gap:14px;flex-wrap:wrap">${card('☁
|
|
1094
|
+
box.innerHTML = `<div class="sec-tag" style="margin-bottom:10px">Ваша торговая машина и вход в терминал</div>
|
|
1095
|
+
<div style="display:flex;gap:14px;flex-wrap:wrap">${card('☁ Персональная виртуальная машина в PaaS', cloudBody)}${card('💻 Установка на свой компьютер (VIP)', deskBody)}</div>`;
|
|
932
1096
|
|
|
933
1097
|
document.getElementById('cab-desktop-code')?.addEventListener('click', async () => {
|
|
934
1098
|
const out = document.getElementById('cab-desktop-code-out');
|
|
@@ -972,16 +1136,17 @@
|
|
|
972
1136
|
const pr = await fetch(`/api/auth/telegram/poll?code=${encodeURIComponent(j.code)}`).then((r) => r.json()).catch(() => ({}));
|
|
973
1137
|
if (pr.status === 'done' && pr.user) {
|
|
974
1138
|
clearInterval(tgPollTimer);
|
|
975
|
-
|
|
1139
|
+
const me = await (await fetch('/api/auth/me')).json().catch(() => ({ user: pr.user }));
|
|
1140
|
+
currentUser = me.user || pr.user;
|
|
1141
|
+
lastMeMeta = me;
|
|
976
1142
|
document.getElementById('auth-modal').hidden = true;
|
|
977
|
-
document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
|
|
978
1143
|
renderCabinet();
|
|
979
1144
|
if (pendingPlan) {
|
|
980
1145
|
const p = pendingPlan;
|
|
981
1146
|
pendingPlan = null;
|
|
982
1147
|
await startCheckout(p);
|
|
983
1148
|
} else {
|
|
984
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: '
|
|
1149
|
+
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
985
1150
|
}
|
|
986
1151
|
} else if (pr.status === 'expired') {
|
|
987
1152
|
clearInterval(tgPollTimer);
|
|
@@ -998,6 +1163,7 @@
|
|
|
998
1163
|
async function loadShowcase() {
|
|
999
1164
|
try {
|
|
1000
1165
|
const me = await (await fetch('/api/auth/me')).json();
|
|
1166
|
+
lastMeMeta = me;
|
|
1001
1167
|
currentUser = me.user;
|
|
1002
1168
|
let hasAnyFastLogin = false;
|
|
1003
1169
|
if (me.notify?.tgBotName && me.notify?.hasTgToken) {
|
|
@@ -1025,14 +1191,24 @@
|
|
|
1025
1191
|
if (up.get('oauth_error')) {
|
|
1026
1192
|
openAuth(false);
|
|
1027
1193
|
document.getElementById('auth-err').textContent = up.get('oauth_error');
|
|
1194
|
+
} else if (!currentUser && up.get('register') === '1') {
|
|
1195
|
+
openAuth(true);
|
|
1196
|
+
} else if (!currentUser && up.get('login') === '1') {
|
|
1197
|
+
openAuth(false);
|
|
1028
1198
|
}
|
|
1199
|
+
syncNavAuth();
|
|
1029
1200
|
if (currentUser) {
|
|
1030
|
-
document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
|
|
1031
1201
|
renderCabinet();
|
|
1032
|
-
|
|
1202
|
+
const hasPendingPay = Array.isArray(currentUser.payments) && currentUser.payments.some((p) => p.status === 'pending');
|
|
1203
|
+
if (up.get('payment_check') === '1' || hasPendingPay) {
|
|
1204
|
+
await verifyPaymentNow(null, up.get('payment_check') !== '1');
|
|
1205
|
+
if (up.get('payment_check') === '1' && history.replaceState) {
|
|
1206
|
+
history.replaceState(null, '', location.pathname);
|
|
1207
|
+
}
|
|
1208
|
+
} else if (up.get('buy_plan')) {
|
|
1033
1209
|
startCheckout(up.get('buy_plan'));
|
|
1034
1210
|
} else if (up.get('oauth_ok') === '1') {
|
|
1035
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: '
|
|
1211
|
+
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1036
1212
|
}
|
|
1037
1213
|
}
|
|
1038
1214
|
const { runs } = await (await fetch('/api/public/showcase')).json();
|
|
@@ -1058,7 +1234,6 @@
|
|
|
1058
1234
|
const tColor = r.tierColor || (isLong ? 'blue' : 'indigo');
|
|
1059
1235
|
const tBadge = r.tierBadge || (isLong ? 'Для тарифа «Инвестор»' : 'Для тарифов «Профи» и «VIP»');
|
|
1060
1236
|
const tNote = r.tierNote || (isLong ? 'Только покупки (без шорта)' : 'Покупки и продажи без покрытия (шорт)');
|
|
1061
|
-
// Главная цифра — реалистичная оценка стресс-теста (исполнение как в живом контуре, издержки 0,10% на сторону)
|
|
1062
1237
|
const rl = r.realistic;
|
|
1063
1238
|
const badgeStyle = tColor === 'gold'
|
|
1064
1239
|
? 'background:rgba(245,158,11,0.18);border:1px solid rgba(245,158,11,0.5);color:#fbbf24'
|
|
@@ -1066,6 +1241,7 @@
|
|
|
1066
1241
|
? 'background:rgba(99,102,241,0.2);border:1px solid rgba(129,140,248,0.5);color:#a5b4fc'
|
|
1067
1242
|
: 'background:rgba(59,130,246,0.16);border:1px solid rgba(96,165,250,0.45);color:#93c5fd';
|
|
1068
1243
|
const cardBorder = tColor === 'gold' ? 'border-color:rgba(245,158,11,0.4)' : '';
|
|
1244
|
+
void cagr;
|
|
1069
1245
|
return `<div class="sc-card" style="${cardBorder}">
|
|
1070
1246
|
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
1071
1247
|
<span style="display:inline-block;padding:3px 10px;border-radius:999px;font-size:11.5px;font-weight:700;letter-spacing:0.3px;${badgeStyle}">${tBadge}</span>
|
|
@@ -1116,15 +1292,46 @@
|
|
|
1116
1292
|
isReg = reg;
|
|
1117
1293
|
document.getElementById('auth-modal').hidden = false;
|
|
1118
1294
|
document.getElementById('auth-name').hidden = !reg;
|
|
1119
|
-
document.getElementById('auth-
|
|
1120
|
-
document.getElementById('auth-
|
|
1295
|
+
const offerRow = document.getElementById('auth-offer-row');
|
|
1296
|
+
const offerChk = document.getElementById('auth-offer-chk');
|
|
1297
|
+
if (offerRow) offerRow.style.display = reg ? 'flex' : 'none';
|
|
1298
|
+
if (offerChk) offerChk.required = Boolean(reg);
|
|
1299
|
+
const tabReg = document.getElementById('tab-auth-reg');
|
|
1300
|
+
const tabLogin = document.getElementById('tab-auth-login');
|
|
1301
|
+
if (tabReg) tabReg.className = reg ? 'btn primary' : 'btn';
|
|
1302
|
+
if (tabLogin) tabLogin.className = !reg ? 'btn primary' : 'btn';
|
|
1303
|
+
const planBanner = document.getElementById('auth-plan-banner');
|
|
1304
|
+
if (planBanner) {
|
|
1305
|
+
if (pendingPlan) {
|
|
1306
|
+
planBanner.hidden = false;
|
|
1307
|
+
planBanner.innerHTML = `<b>Шаг 1 из 2:</b> ${reg ? 'Создайте личный аккаунт клиента' : 'Войдите в свой аккаунт'}, чтобы перейти к оплате тарифа <b>«${escH(PLAN_TITLES[pendingPlan] || pendingPlan)}»</b>.`;
|
|
1308
|
+
} else {
|
|
1309
|
+
planBanner.hidden = true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
document.getElementById('auth-title').textContent = reg ? 'Регистрация нового клиента' : 'Вход в личный кабинет';
|
|
1313
|
+
document.getElementById('auth-submit').textContent = reg ? (pendingPlan ? 'Создать аккаунт и перейти к оплате →' : 'Создать аккаунт') : (pendingPlan ? 'Войти и перейти к оплате →' : 'Войти');
|
|
1121
1314
|
document.getElementById('auth-toggle').textContent = reg ? 'Уже есть аккаунт? Войти' : 'Нет аккаунта? Зарегистрироваться';
|
|
1122
1315
|
document.getElementById('auth-err').textContent = '';
|
|
1123
1316
|
}
|
|
1124
1317
|
|
|
1125
|
-
document.getElementById('
|
|
1126
|
-
document.getElementById('
|
|
1127
|
-
document.getElementById('btn-
|
|
1318
|
+
document.getElementById('tab-auth-reg')?.addEventListener('click', () => openAuth(true));
|
|
1319
|
+
document.getElementById('tab-auth-login')?.addEventListener('click', () => openAuth(false));
|
|
1320
|
+
document.getElementById('btn-login-open').addEventListener('click', () => {
|
|
1321
|
+
if (currentUser) {
|
|
1322
|
+
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1323
|
+
} else {
|
|
1324
|
+
pendingPlan = null;
|
|
1325
|
+
openAuth(false);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
document.getElementById('btn-reg-open')?.addEventListener('click', () => {
|
|
1329
|
+
pendingPlan = null;
|
|
1330
|
+
openAuth(true);
|
|
1331
|
+
});
|
|
1332
|
+
document.getElementById('btn-nav-logout')?.addEventListener('click', () => doLogout());
|
|
1333
|
+
document.getElementById('btn-hero-start').addEventListener('click', () => currentUser ? document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) : openAuth(true));
|
|
1334
|
+
document.getElementById('btn-free-start')?.addEventListener('click', () => currentUser ? document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) : openAuth(true));
|
|
1128
1335
|
document.getElementById('auth-close').addEventListener('click', () => { document.getElementById('auth-modal').hidden = true; });
|
|
1129
1336
|
document.getElementById('auth-toggle').addEventListener('click', () => openAuth(!isReg));
|
|
1130
1337
|
|
|
@@ -1140,11 +1347,27 @@
|
|
|
1140
1347
|
offerChk1?.addEventListener('change', syncOfferBtn);
|
|
1141
1348
|
offerChk2?.addEventListener('change', syncOfferBtn);
|
|
1142
1349
|
|
|
1143
|
-
function requireOfferAgreement(actionFn) {
|
|
1350
|
+
function requireOfferAgreement(actionFn, planLabel = '') {
|
|
1144
1351
|
onOfferAccepted = actionFn;
|
|
1145
1352
|
if (offerChk1) offerChk1.checked = false;
|
|
1146
1353
|
if (offerChk2) offerChk2.checked = false;
|
|
1147
1354
|
syncOfferBtn();
|
|
1355
|
+
const accBanner = document.getElementById('offer-account-banner');
|
|
1356
|
+
if (accBanner && currentUser) {
|
|
1357
|
+
const isAdmin = currentUser.role === 'admin';
|
|
1358
|
+
accBanner.innerHTML = `<div style="padding:10px 14px;border-radius:10px;background:${isAdmin ? 'rgba(245,158,11,0.16)' : 'rgba(59,130,246,0.12)'};border:1px solid ${isAdmin ? 'rgba(245,158,11,0.5)' : 'rgba(96,165,250,0.35)'};font-size:13px;line-height:1.5">
|
|
1359
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
|
|
1360
|
+
<span>Аккаунт покупателя: <b>${escH(currentUser.name)}</b> (<code>${escH(currentUser.email)}</code>)${planLabel ? ` · <b>${escH(planLabel)}</b>` : ''}</span>
|
|
1361
|
+
<button type="button" class="btn" id="btn-offer-switch-user" style="padding:4px 10px;font-size:12px">Выйти и зарегистрировать другого клиента</button>
|
|
1362
|
+
</div>
|
|
1363
|
+
${isAdmin ? '<div style="margin-top:6px;color:#fde68a">⚠ <b>Внимание:</b> вы сейчас авторизованы под учётной записью администратора («Хозяин»). Чтобы протестировать регистрацию и покупку подписки как новый клиент, нажмите кнопку <b>«Выйти и зарегистрировать другого клиента»</b>.</div>' : ''}
|
|
1364
|
+
</div>`;
|
|
1365
|
+
document.getElementById('btn-offer-switch-user')?.addEventListener('click', () => {
|
|
1366
|
+
offerModal.hidden = true;
|
|
1367
|
+
onOfferAccepted = null;
|
|
1368
|
+
doLogout(pendingPlan || 'investor');
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1148
1371
|
offerModal.hidden = false;
|
|
1149
1372
|
}
|
|
1150
1373
|
|
|
@@ -1165,6 +1388,8 @@
|
|
|
1165
1388
|
});
|
|
1166
1389
|
|
|
1167
1390
|
async function startCheckout(plan) {
|
|
1391
|
+
pendingPlan = plan;
|
|
1392
|
+
const planLabel = `Тариф «${PLAN_TITLES[plan] || plan}» (${selectedMonths} мес.)`;
|
|
1168
1393
|
requireOfferAgreement(async () => {
|
|
1169
1394
|
try {
|
|
1170
1395
|
const res = await fetch('/api/billing/checkout', {
|
|
@@ -1178,7 +1403,7 @@
|
|
|
1178
1403
|
} catch (e) {
|
|
1179
1404
|
alert(e.message);
|
|
1180
1405
|
}
|
|
1181
|
-
});
|
|
1406
|
+
}, planLabel);
|
|
1182
1407
|
}
|
|
1183
1408
|
|
|
1184
1409
|
document.getElementById('btn-vip-req')?.addEventListener('click', () => {
|
|
@@ -1224,9 +1449,9 @@
|
|
|
1224
1449
|
const j = await r.json();
|
|
1225
1450
|
if (!r.ok) return alert(j.error || 'Не удалось активировать VIP-токен');
|
|
1226
1451
|
currentUser = j.user;
|
|
1452
|
+
paymentSuccessBanner = `Персональный VIP-токен активирован! Ваш тариф: «${currentUser.planName}».`;
|
|
1227
1453
|
renderCabinet();
|
|
1228
|
-
|
|
1229
|
-
});
|
|
1454
|
+
}, `Активация VIP-токена (${code})`);
|
|
1230
1455
|
});
|
|
1231
1456
|
|
|
1232
1457
|
document.querySelectorAll('[data-buy]').forEach((b) => b.addEventListener('click', () => {
|
|
@@ -1251,16 +1476,17 @@
|
|
|
1251
1476
|
});
|
|
1252
1477
|
const j = await res.json();
|
|
1253
1478
|
if (!res.ok) throw new Error(j.error || 'Ошибка авторизации');
|
|
1254
|
-
|
|
1479
|
+
const me = await (await fetch('/api/auth/me')).json().catch(() => ({ user: j.user }));
|
|
1480
|
+
currentUser = me.user || j.user;
|
|
1481
|
+
lastMeMeta = me;
|
|
1255
1482
|
document.getElementById('auth-modal').hidden = true;
|
|
1256
|
-
document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
|
|
1257
1483
|
renderCabinet();
|
|
1258
1484
|
if (pendingPlan) {
|
|
1259
1485
|
const p = pendingPlan;
|
|
1260
1486
|
pendingPlan = null;
|
|
1261
1487
|
await startCheckout(p);
|
|
1262
1488
|
} else {
|
|
1263
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: '
|
|
1489
|
+
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1264
1490
|
}
|
|
1265
1491
|
} catch (err) {
|
|
1266
1492
|
document.getElementById('auth-err').textContent = err.message;
|