neuroalpha 0.6.13 → 0.7.0
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/README.md +14 -10
- package/bin/cli.js +79 -69
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -2
- package/package.json +4 -5
- package/public/activate.html +41 -19
- package/public/activate.js +81 -10
- package/public/app.js +300 -152
- package/public/index.html +4 -37
- package/server/api/routes.js +570 -906
- package/server/auth/index.js +576 -761
- package/server/brokers/index.js +1 -2
- package/server/config.js +73 -53
- package/server/index.js +186 -212
- package/server/llm/local-engine.js +843 -0
- package/server/llm/providers.js +45 -34
- package/server/notify/index.js +304 -466
- package/deploy/docs/01-central.md +0 -96
- package/deploy/docs/02-cloud-instances.md +0 -130
- package/deploy/docs/03-desktop-build.md +0 -92
- package/deploy/docs/04-client-guide.md +0 -101
- package/deploy/docs/05-operations.md +0 -109
- package/deploy/docs/06-env-reference.md +0 -91
- package/deploy/docs/07-stress-test.md +0 -253
- package/deploy/docs/pdf/01-central.pdf +0 -0
- package/deploy/docs/pdf/02-cloud-instances.pdf +0 -0
- package/deploy/docs/pdf/03-desktop-build.pdf +0 -0
- package/deploy/docs/pdf/04-client-guide.pdf +0 -0
- package/deploy/docs/pdf/05-operations.pdf +0 -0
- package/deploy/docs/pdf/06-env-reference.pdf +0 -0
- package/deploy/docs/pdf/07-stress-test.pdf +0 -0
- package/deploy/install.sh +0 -128
- package/deploy/k8s/instance.yaml +0 -147
- package/public/admin.html +0 -604
- package/public/landing.html +0 -1207
- package/public/legal.html +0 -134
- package/scripts/set-admin-password.mjs +0 -30
- package/server/billing/yookassa.js +0 -142
- package/server/instance/agent.js +0 -259
- package/server/instance/central.js +0 -514
- package/server/instance/identity.js +0 -79
- package/server/instance/provision.js +0 -166
- package/server/instance/signing.js +0 -71
- package/server/role.js +0 -44
package/public/landing.html
DELETED
|
@@ -1,1207 +0,0 @@
|
|
|
1
|
-
<!-- Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software. -->
|
|
2
|
-
<!doctype html>
|
|
3
|
-
<html lang="ru">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="utf-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
-
<title>НейроАльфа — Аренда биржевого программного обеспечения (PaaS)</title>
|
|
8
|
-
<style>
|
|
9
|
-
:root {
|
|
10
|
-
color-scheme: dark;
|
|
11
|
-
--bg: #060911;
|
|
12
|
-
--bg2: #0b101d;
|
|
13
|
-
--panel: rgba(17, 24, 39, 0.82);
|
|
14
|
-
--panel-solid: #111827;
|
|
15
|
-
--line: rgba(255, 255, 255, 0.09);
|
|
16
|
-
--line-bright: rgba(96, 165, 250, 0.4);
|
|
17
|
-
--text: #f8fafc;
|
|
18
|
-
--muted: #94a3b8;
|
|
19
|
-
--accent: #3b82f6;
|
|
20
|
-
--accent2: #6366f1;
|
|
21
|
-
--green: #10b981;
|
|
22
|
-
--red: #f43f5e;
|
|
23
|
-
}
|
|
24
|
-
* { box-sizing: border-box; }
|
|
25
|
-
[hidden] { display: none !important; }
|
|
26
|
-
html { scroll-behavior: smooth; }
|
|
27
|
-
body {
|
|
28
|
-
margin: 0;
|
|
29
|
-
background: var(--bg);
|
|
30
|
-
color: var(--text);
|
|
31
|
-
font: 15.5px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
32
|
-
overflow-x: hidden;
|
|
33
|
-
}
|
|
34
|
-
a, a:visited { color: inherit; text-decoration: none; }
|
|
35
|
-
.wrap { max-width: 1140px; margin: 0 auto; padding: 0 22px; position: relative; z-index: 2; }
|
|
36
|
-
|
|
37
|
-
/* ---------- Шапка ---------- */
|
|
38
|
-
header {
|
|
39
|
-
position: sticky; top: 0; z-index: 40;
|
|
40
|
-
background: rgba(6, 9, 17, 0.88);
|
|
41
|
-
backdrop-filter: blur(14px);
|
|
42
|
-
border-bottom: 1px solid var(--line);
|
|
43
|
-
}
|
|
44
|
-
.nav {
|
|
45
|
-
min-height: 62px; padding: 8px 0;
|
|
46
|
-
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
|
47
|
-
}
|
|
48
|
-
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 19px; letter-spacing: -0.3px; white-space: nowrap; }
|
|
49
|
-
.brand-icon {
|
|
50
|
-
width: 32px; height: 32px; border-radius: 8px;
|
|
51
|
-
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
52
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
53
|
-
color: #fff; font-size: 16px;
|
|
54
|
-
}
|
|
55
|
-
.nav-links { display: flex; gap: 22px; font-size: 14px; color: var(--muted); font-weight: 500; }
|
|
56
|
-
.nav-links a { transition: color .15s; white-space: nowrap; }
|
|
57
|
-
.nav-links a:hover { color: #fff; }
|
|
58
|
-
@media (max-width: 860px) { .nav-links { display: none; } }
|
|
59
|
-
|
|
60
|
-
.nav-auth { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
|
|
61
|
-
|
|
62
|
-
/* ---------- Кнопки ---------- */
|
|
63
|
-
.btn {
|
|
64
|
-
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
65
|
-
padding: 9px 18px; border-radius: 9px; border: 1px solid var(--line);
|
|
66
|
-
background: rgba(255, 255, 255, 0.04); color: var(--text);
|
|
67
|
-
font-weight: 600; font-size: 13.5px; cursor: pointer; white-space: nowrap;
|
|
68
|
-
transition: border-color .15s ease, background .15s ease;
|
|
69
|
-
}
|
|
70
|
-
.btn:hover { border-color: var(--line-bright); background: rgba(255, 255, 255, 0.08); }
|
|
71
|
-
.btn.primary {
|
|
72
|
-
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
73
|
-
border-color: transparent; color: #fff;
|
|
74
|
-
}
|
|
75
|
-
.btn.primary:hover { filter: brightness(1.08); }
|
|
76
|
-
.btn.lg { padding: 12px 22px; font-size: 14.5px; border-radius: 10px; }
|
|
77
|
-
|
|
78
|
-
/* ---------- Компактный главный блок ---------- */
|
|
79
|
-
.hero { padding: 36px 0 28px; border-bottom: 1px solid var(--line); }
|
|
80
|
-
.hero-grid {
|
|
81
|
-
display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 32px; align-items: start;
|
|
82
|
-
}
|
|
83
|
-
@media (max-width: 940px) { .hero-grid { grid-template-columns: 1fr; } }
|
|
84
|
-
.pill-badge {
|
|
85
|
-
display: inline-flex; align-items: center; gap: 8px;
|
|
86
|
-
padding: 5px 12px; border-radius: 999px;
|
|
87
|
-
background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.3);
|
|
88
|
-
color: #93c5fd; font-size: 12.5px; font-weight: 500; margin-bottom: 14px;
|
|
89
|
-
}
|
|
90
|
-
h1 {
|
|
91
|
-
font-size: clamp(26px, 3.4vw, 38px); line-height: 1.15; margin: 0 0 14px;
|
|
92
|
-
letter-spacing: -0.8px; font-weight: 800;
|
|
93
|
-
}
|
|
94
|
-
.hero-sub { font-size: 15.5px; color: #cbd5e1; margin: 0 0 18px; line-height: 1.55; }
|
|
95
|
-
.legal-notice {
|
|
96
|
-
background: rgba(30, 41, 59, 0.65);
|
|
97
|
-
border: 1px solid rgba(96, 165, 250, 0.32);
|
|
98
|
-
border-left: 3px solid var(--accent);
|
|
99
|
-
border-radius: 10px;
|
|
100
|
-
padding: 12px 15px;
|
|
101
|
-
font-size: 13px;
|
|
102
|
-
color: #cbd5e1;
|
|
103
|
-
line-height: 1.5;
|
|
104
|
-
margin-bottom: 20px;
|
|
105
|
-
}
|
|
106
|
-
.legal-notice b { color: #fff; }
|
|
107
|
-
.hero-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
108
|
-
|
|
109
|
-
/* ---------- Технический паспорт ПО ---------- */
|
|
110
|
-
.spec-card {
|
|
111
|
-
background: var(--panel);
|
|
112
|
-
border: 1px solid var(--line);
|
|
113
|
-
border-radius: 14px;
|
|
114
|
-
padding: 20px 22px;
|
|
115
|
-
}
|
|
116
|
-
.spec-title {
|
|
117
|
-
font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.7px;
|
|
118
|
-
color: #60a5fa; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line);
|
|
119
|
-
}
|
|
120
|
-
.spec-list { display: grid; gap: 10px; font-size: 13.5px; }
|
|
121
|
-
.spec-row { display: grid; grid-template-columns: 150px 1fr; gap: 10px; align-items: baseline; border-bottom: 1px solid rgba(255,255,255,0.04); padding-bottom: 8px; }
|
|
122
|
-
.spec-row:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
123
|
-
.spec-lbl { color: var(--muted); font-size: 12.5px; }
|
|
124
|
-
.spec-val { color: var(--text); font-weight: 500; }
|
|
125
|
-
@media (max-width: 560px) { .spec-row { grid-template-columns: 1fr; gap: 2px; } }
|
|
126
|
-
|
|
127
|
-
/* ---------- Секции и карточки ---------- */
|
|
128
|
-
section { padding: 38px 0; border-bottom: 1px solid var(--line); }
|
|
129
|
-
.sec-head { margin-bottom: 24px; }
|
|
130
|
-
.sec-tag { color: #60a5fa; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 4px; }
|
|
131
|
-
h2 { font-size: clamp(21px, 2.6vw, 28px); margin: 0 0 6px; letter-spacing: -0.5px; line-height: 1.2; }
|
|
132
|
-
.sec-sub { color: var(--muted); margin: 0; font-size: 14.5px; max-width: 780px; }
|
|
133
|
-
|
|
134
|
-
.bento { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
|
135
|
-
@media (max-width: 960px) { .bento { grid-template-columns: repeat(2, 1fr); } }
|
|
136
|
-
@media (max-width: 600px) { .bento { grid-template-columns: 1fr; } }
|
|
137
|
-
.bcard {
|
|
138
|
-
background: var(--panel); border: 1px solid var(--line);
|
|
139
|
-
border-radius: 12px; padding: 18px 20px;
|
|
140
|
-
}
|
|
141
|
-
.bcard h3 { margin: 0 0 6px; font-size: 16px; letter-spacing: -0.2px; color: #fff; }
|
|
142
|
-
.bcard p { margin: 0; color: var(--muted); font-size: 13.5px; line-height: 1.5; }
|
|
143
|
-
|
|
144
|
-
/* ---------- Тарифы лицензирования ---------- */
|
|
145
|
-
.period-wrap { display: flex; justify-content: flex-start; margin-bottom: 20px; }
|
|
146
|
-
.period-seg { display: inline-flex; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 3px; gap: 4px; flex-wrap: wrap; }
|
|
147
|
-
.period-seg button {
|
|
148
|
-
border: 0; background: transparent; color: var(--muted);
|
|
149
|
-
padding: 7px 14px; border-radius: 7px; cursor: pointer; font-weight: 600; font-size: 13px; transition: .15s;
|
|
150
|
-
}
|
|
151
|
-
.period-seg button.active { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; }
|
|
152
|
-
.plans { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; align-items: stretch; }
|
|
153
|
-
@media (max-width: 820px) { .plans { grid-template-columns: 1fr; } }
|
|
154
|
-
.plan {
|
|
155
|
-
background: var(--panel); border: 1px solid var(--line);
|
|
156
|
-
border-radius: 14px; padding: 22px 24px;
|
|
157
|
-
display: flex; flex-direction: column; position: relative;
|
|
158
|
-
}
|
|
159
|
-
.plan.featured {
|
|
160
|
-
background: linear-gradient(180deg, rgba(30, 58, 138, 0.25), rgba(17, 24, 39, 0.9));
|
|
161
|
-
border-color: rgba(96, 165, 250, 0.5);
|
|
162
|
-
}
|
|
163
|
-
.plan-vip-wide {
|
|
164
|
-
margin-top: 18px;
|
|
165
|
-
background: linear-gradient(135deg, rgba(120, 53, 15, 0.18), rgba(17, 24, 39, 0.94));
|
|
166
|
-
border: 1px solid rgba(245, 158, 11, 0.45);
|
|
167
|
-
border-radius: 14px; padding: 22px 24px;
|
|
168
|
-
display: grid; grid-template-columns: 1fr 1.4fr auto; gap: 22px; align-items: center;
|
|
169
|
-
position: relative;
|
|
170
|
-
}
|
|
171
|
-
@media (max-width: 920px) { .plan-vip-wide { grid-template-columns: 1fr; } }
|
|
172
|
-
.pop-ribbon {
|
|
173
|
-
position: absolute; top: 14px; right: 16px;
|
|
174
|
-
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
175
|
-
color: #fff; font-size: 10.5px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
|
|
176
|
-
text-transform: uppercase; letter-spacing: 0.4px;
|
|
177
|
-
}
|
|
178
|
-
.plan h3, .plan-vip-wide h3 { margin: 0 0 4px; font-size: 19px; }
|
|
179
|
-
.plan-desc { font-size: 13px; color: var(--muted); margin-bottom: 10px; }
|
|
180
|
-
.price { font-size: 32px; font-weight: 800; margin: 4px 0 14px; letter-spacing: -0.8px; }
|
|
181
|
-
.price span { font-size: 13.5px; font-weight: 400; color: var(--muted); letter-spacing: 0; }
|
|
182
|
-
.plan ul, .plan-vip-wide ul { list-style: none; padding: 0; margin: 0 0 20px; font-size: 13.5px; color: #cbd5e1; flex: 1; }
|
|
183
|
-
.plan-vip-wide ul { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 8px 18px; }
|
|
184
|
-
@media (max-width: 720px) { .plan-vip-wide ul { grid-template-columns: 1fr; } }
|
|
185
|
-
.plan li, .plan-vip-wide li { margin: 7px 0; position: relative; padding-left: 20px; line-height: 1.45; }
|
|
186
|
-
.plan-vip-wide li { margin: 0; }
|
|
187
|
-
.plan li::before, .plan-vip-wide li::before { content: "✓"; color: var(--green); font-weight: 800; position: absolute; left: 0; top: 0; }
|
|
188
|
-
|
|
189
|
-
.pos { color: var(--green); }
|
|
190
|
-
.neg { color: var(--red); }
|
|
191
|
-
|
|
192
|
-
footer { padding: 28px 0 36px; color: var(--muted); font-size: 12.5px; line-height: 1.5; }
|
|
193
|
-
.modal { position: fixed; inset: 0; background: rgba(3, 6, 12, 0.8); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 16px; }
|
|
194
|
-
.modal-box { background: var(--panel-solid); border: 1px solid var(--line-bright); border-radius: 14px; padding: 24px; width: 100%; max-width: 430px; box-shadow: 0 24px 60px #000a; }
|
|
195
|
-
.modal-box h3 { margin: 0 0 14px; font-size: 19px; }
|
|
196
|
-
.modal-box form { display: grid; gap: 11px; }
|
|
197
|
-
.modal-box input[type=email], .modal-box input[type=password], .modal-box input[name=name] {
|
|
198
|
-
width: 100%; padding: 10px 13px; border-radius: 9px; border: 1px solid var(--line); background: var(--bg); color: var(--text); font: inherit;
|
|
199
|
-
}
|
|
200
|
-
.err { color: var(--red); font-size: 13px; min-height: 18px; }
|
|
201
|
-
.cab-card {
|
|
202
|
-
background: linear-gradient(135deg, rgba(30, 58, 138, 0.24), rgba(15, 23, 42, 0.94));
|
|
203
|
-
border: 1px solid var(--line-bright); border-radius: 14px; padding: 22px 24px; margin: 22px 0 8px;
|
|
204
|
-
}
|
|
205
|
-
.cab-grid { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 20px; align-items: center; }
|
|
206
|
-
@media (max-width: 860px) { .cab-grid { grid-template-columns: 1fr; } }
|
|
207
|
-
.oauth-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
|
|
208
|
-
.oauth-btn {
|
|
209
|
-
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
210
|
-
padding: 9px 12px; border-radius: 9px; border: 1px solid var(--line);
|
|
211
|
-
background: rgba(255, 255, 255, 0.04); color: var(--text); font-weight: 600; font-size: 13px;
|
|
212
|
-
cursor: pointer; transition: border-color .15s, background .15s;
|
|
213
|
-
}
|
|
214
|
-
.oauth-btn:hover { border-color: var(--line-bright); background: rgba(255, 255, 255, 0.09); }
|
|
215
|
-
.oauth-btn.full { grid-column: span 2; }
|
|
216
|
-
.oauth-ico { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: 800; font-size: 11px; flex: none; }
|
|
217
|
-
.or-sep { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
|
|
218
|
-
.or-sep::before, .or-sep::after { content: ''; flex: 1; height: 1px; background: var(--line); }
|
|
219
|
-
</style>
|
|
220
|
-
</head>
|
|
221
|
-
<body>
|
|
222
|
-
<header>
|
|
223
|
-
<div class="wrap nav">
|
|
224
|
-
<a class="brand" href="/landing"><span class="brand-icon">◈</span><span>НейроАльфа</span></a>
|
|
225
|
-
<nav class="nav-links">
|
|
226
|
-
<a href="#software">Состав ПО</a>
|
|
227
|
-
<a href="#architecture">Инфраструктура PaaS</a>
|
|
228
|
-
<a href="#pricing">Лицензионные тарифы</a>
|
|
229
|
-
<a href="/legal">Правовые документы</a>
|
|
230
|
-
</nav>
|
|
231
|
-
<div class="nav-auth" id="nav-auth">
|
|
232
|
-
<button class="btn" id="btn-login-open" type="button">Войти</button>
|
|
233
|
-
<button class="btn primary" id="btn-reg-open" type="button">Регистрация</button>
|
|
234
|
-
<a class="btn" href="/admin" id="btn-nav-cta" hidden>Консоль админа</a>
|
|
235
|
-
<button class="btn" id="btn-nav-logout" type="button" hidden style="color:#f87171;border-color:rgba(248,113,113,0.35)">Выйти</button>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</header>
|
|
239
|
-
|
|
240
|
-
<div class="wrap">
|
|
241
|
-
<div id="cabinet-box" hidden></div>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<div class="hero">
|
|
245
|
-
<div class="wrap hero-grid">
|
|
246
|
-
<div>
|
|
247
|
-
<div class="pill-badge"><span>Прикладное биржевое ПО (PaaS / Desktop) · Интеграция по API с брокерами РФ</span></div>
|
|
248
|
-
<h1>Аренда биржевого программного обеспечения «НейроАльфа»</h1>
|
|
249
|
-
<p class="hero-sub">Программно-вычислительный комплекс для автоматизации статистической обработки биржевых котировок, технического расчёта параметров лимитных заявок в лотах и обмена командами с брокерскими счетами через официальные протоколы API.</p>
|
|
250
|
-
<div class="legal-notice">
|
|
251
|
-
<b>Назначение продукта и предмет оплаты:</b> «НейроАльфа» <b>не является сервисом по зарабатыванию денег</b>, доверительным управлением, инвестиционным консультантом или источником индивидуальных инвестиционных рекомендаций. Пользователю предоставляется исключительно <b>техническое программное обеспечение в аренду</b>. Оплата взимается <b>исключительно за простую (неисключительную) лицензию на право использования ПО</b> и выделенные вычислительные ресурсы виртуальной машины PaaS.
|
|
252
|
-
</div>
|
|
253
|
-
<div class="hero-actions">
|
|
254
|
-
<button class="btn primary lg" id="btn-hero-start" type="button">Регистрация в личном кабинете</button>
|
|
255
|
-
<a class="btn lg" href="#pricing">Тарифы на лицензию ПО →</a>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<div class="spec-card">
|
|
260
|
-
<div class="spec-title">Технический паспорт программного комплекса</div>
|
|
261
|
-
<div class="spec-list">
|
|
262
|
-
<div class="spec-row">
|
|
263
|
-
<div class="spec-lbl">Вид продукта</div>
|
|
264
|
-
<div class="spec-val">Программное обеспечение для ЭВМ (веб-терминал, вычислительное ядро и шлюз API)</div>
|
|
265
|
-
</div>
|
|
266
|
-
<div class="spec-row">
|
|
267
|
-
<div class="spec-lbl">Правовое основание</div>
|
|
268
|
-
<div class="spec-val">Предоставление неисключительной лицензии на использование ПО на срок подписки</div>
|
|
269
|
-
</div>
|
|
270
|
-
<div class="spec-row">
|
|
271
|
-
<div class="spec-lbl">Среда исполнения</div>
|
|
272
|
-
<div class="spec-val">Изолированная виртуальная машина PaaS (<code>naXXXXXX.odant.org</code>) или локальная установка (VIP)</div>
|
|
273
|
-
</div>
|
|
274
|
-
<div class="spec-row">
|
|
275
|
-
<div class="spec-lbl">Поддерживаемые API</div>
|
|
276
|
-
<div class="spec-val">Т-Банк (T-Invest API), БКС, Финам, Алор, Цифра брокер, ITI Capital</div>
|
|
277
|
-
</div>
|
|
278
|
-
<div class="spec-row">
|
|
279
|
-
<div class="spec-lbl">Защита ключей API</div>
|
|
280
|
-
<div class="spec-val">Шифрование AES-256-GCM; отсутствие у ПО доступа к неторговым операциям и выводу средств</div>
|
|
281
|
-
</div>
|
|
282
|
-
<div class="spec-row">
|
|
283
|
-
<div class="spec-lbl">Управление и контроль</div>
|
|
284
|
-
<div class="spec-val">Веб-интерфейс терминала, боты технического подтверждения заявок в MAX и Telegram</div>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
<div class="wrap">
|
|
292
|
-
<section id="software">
|
|
293
|
-
<div class="sec-head">
|
|
294
|
-
<div class="sec-tag">Архитектура программного обеспечения</div>
|
|
295
|
-
<h2>Функциональные модули комплекса «НейроАльфа»</h2>
|
|
296
|
-
<p class="sec-sub">Программный комплекс выполняет технические операции по обработке рыночных данных и маршрутизации заявок строго в рамках параметров и лимитов, заданных самим пользователем.</p>
|
|
297
|
-
</div>
|
|
298
|
-
<div class="bento">
|
|
299
|
-
<div class="bcard">
|
|
300
|
-
<h3>1. Вычислительное ядро</h3>
|
|
301
|
-
<p>Автоматический сбор и нормализация потоковых котировок Московской биржи, макроиндикаторов и календаря корпоративных событий для локальных статистических расчётов.</p>
|
|
302
|
-
</div>
|
|
303
|
-
<div class="bcard">
|
|
304
|
-
<h3>2. Шлюз брокерских API</h3>
|
|
305
|
-
<p>Технический расчёт количества целых лотов с учётом шага цены биржевого стакана, формирование и передача лимитных и защитных стоп-заявок через API выбранного брокера.</p>
|
|
306
|
-
</div>
|
|
307
|
-
<div class="bcard">
|
|
308
|
-
<h3>3. Программные предохранители</h3>
|
|
309
|
-
<p>Контроль выделенного пользователем лимита денежных средств, расписания торговых сессий и автоматическая техническая блокировка подачи поручений при достижении заданных порогов.</p>
|
|
310
|
-
</div>
|
|
311
|
-
<div class="bcard">
|
|
312
|
-
<h3>4. Диалоговый модуль и боты</h3>
|
|
313
|
-
<p>Управление параметрами ПО через встроенный веб-интерфейс, получение технических карточек сформированных заявок и их подтверждение в мессенджере MAX или Telegram.</p>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
</section>
|
|
317
|
-
|
|
318
|
-
<section id="architecture">
|
|
319
|
-
<div class="sec-head">
|
|
320
|
-
<div class="sec-tag">Модель развёртывания PaaS</div>
|
|
321
|
-
<h2>Порядок предоставления лицензии и работы с ПО</h2>
|
|
322
|
-
<p class="sec-sub">Все операции со счётом происходят напрямую между выделенной машиной пользователя и сервером брокера по API-токену.</p>
|
|
323
|
-
</div>
|
|
324
|
-
<div class="bento" style="grid-template-columns:repeat(3,1fr)">
|
|
325
|
-
<div class="bcard">
|
|
326
|
-
<h3>1. Регистрация и выпуск лицензии</h3>
|
|
327
|
-
<p>После регистрации и оплаты выбранного тарифа лицензиату автоматически выделяется персональная изолированная виртуальная машина PaaS с предустановленным ПО «НейроАльфа».</p>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="bcard">
|
|
330
|
-
<h3>2. Настройка подключения по API</h3>
|
|
331
|
-
<p>Пользователь самостоятельно размещает в терминале свой токен API брокера и выбирает технический режим: «Только чтение», «Отправка с подтверждением в мессенджере» или «Автоматическая отправка».</p>
|
|
332
|
-
</div>
|
|
333
|
-
<div class="bcard">
|
|
334
|
-
<h3>3. Локальная поставка (VIP)</h3>
|
|
335
|
-
<p>Для корпоративных и VIP-лицензиатов по отдельному договору доступна поставка дистрибутива ПО для автономной работы на собственном сервере или рабочей станции (Windows, Linux, Docker).</p>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
</section>
|
|
339
|
-
|
|
340
|
-
<section id="pricing" style="border-bottom:0">
|
|
341
|
-
<div class="sec-head">
|
|
342
|
-
<div class="sec-tag">Лицензионные тарифы</div>
|
|
343
|
-
<h2>Стоимость лицензии на аренду биржевого ПО</h2>
|
|
344
|
-
<p class="sec-sub">Оплата взимается исключительно за право использования программного обеспечения (простую неисключительную лицензию) и аренду виртуальной машины PaaS. Тарифы различаются только техническими квотами ПО и не определяют финансовый результат торговли.</p>
|
|
345
|
-
</div>
|
|
346
|
-
<div class="period-wrap">
|
|
347
|
-
<div class="period-seg" id="months-seg">
|
|
348
|
-
<button data-m="1" class="active" type="button">1 месяц</button>
|
|
349
|
-
<button data-m="3" type="button">3 месяца (квартальная лицензия)</button>
|
|
350
|
-
<button data-m="12" type="button">12 месяцев (годовая лицензия)</button>
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
<div class="plans">
|
|
354
|
-
<div class="plan">
|
|
355
|
-
<h3>Лицензия «Инвестор»</h3>
|
|
356
|
-
<div class="plan-desc">Базовая комплектация ПО для одного брокерского счёта (режим длинных позиций)</div>
|
|
357
|
-
<div class="price" id="price-investor">3 900 ₽ <span>/ мес</span></div>
|
|
358
|
-
<ul>
|
|
359
|
-
<li>Выделенная виртуальная машина PaaS с круглосуточной работой служб</li>
|
|
360
|
-
<li>Квота подключений по API: <b>1 брокерский счёт</b> (Т-Банк, БКС, Финам, Алор и др.)</li>
|
|
361
|
-
<li>Технический лимит рассчитываемой стартовой базы в ПО: <b>до 1 000 000 ₽</b></li>
|
|
362
|
-
<li>Активный модуль направления: <b>«Только длинные позиции (Long)»</b> и тестовая песочница API</li>
|
|
363
|
-
<li>Модуль программных предохранителей и подтверждение заявок в <b>MAX и Telegram</b></li>
|
|
364
|
-
</ul>
|
|
365
|
-
<button class="btn primary" data-buy="investor" type="button">Оплатить лицензию «Инвестор»</button>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<div class="plan featured">
|
|
369
|
-
<div class="pop-ribbon">Полный функционал PaaS</div>
|
|
370
|
-
<h3>Лицензия «Профи»</h3>
|
|
371
|
-
<div class="plan-desc">Расширенная комплектация ПО до 5 брокерских счетов (длинные и короткие позиции)</div>
|
|
372
|
-
<div class="price" id="price-pro">9 900 ₽ <span>/ мес</span></div>
|
|
373
|
-
<ul>
|
|
374
|
-
<li>Выделенная виртуальная машина PaaS с круглосуточной работой служб</li>
|
|
375
|
-
<li>Квота подключений по API: до <b>5 брокерских счетов</b> параллельно</li>
|
|
376
|
-
<li>Технический лимит рассчитываемой стартовой базы в ПО: <b>до 1 000 000 ₽</b> на каждый счёт</li>
|
|
377
|
-
<li>Активный модуль направления: <b>«Длинные и короткие позиции (Long / Short)»</b></li>
|
|
378
|
-
<li>Модуль серверных стоп-заявок через API брокера (Т-Банк) и поддержка песочницы</li>
|
|
379
|
-
</ul>
|
|
380
|
-
<button class="btn primary" data-buy="pro" type="button">Оплатить лицензию «Профи»</button>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
|
|
384
|
-
<div class="plan-vip-wide">
|
|
385
|
-
<div>
|
|
386
|
-
<span class="pop-ribbon" style="position:static;display:inline-block;margin-bottom:8px;background:linear-gradient(135deg,#d97706,#f59e0b)">Индивидуальная лицензия</span>
|
|
387
|
-
<h3 style="font-size:21px;margin-bottom:4px">Лицензия «VIP»</h3>
|
|
388
|
-
<div class="plan-desc" style="margin-bottom:8px">Индивидуальные технические квоты счетов и возможность локальной установки ПО</div>
|
|
389
|
-
<div style="font-size:23px;font-weight:800;color:#fbbf24">По договору</div>
|
|
390
|
-
</div>
|
|
391
|
-
<ul>
|
|
392
|
-
<li><b>Индивидуальная квота</b> количества подключаемых счетов и технического лимита объёма</li>
|
|
393
|
-
<li>Выделенный сервер PaaS или <b>дистрибутив для установки на собственный компьютер</b> (Windows, Linux, Docker)</li>
|
|
394
|
-
<li>Полный комплект вычислительных модулей ПО (Long / Short, настраиваемые параметры слотов)</li>
|
|
395
|
-
<li>Персональный лицензионный токен активации от администратора платформы</li>
|
|
396
|
-
</ul>
|
|
397
|
-
<div style="display:flex;flex-direction:column;gap:8px;min-width:210px">
|
|
398
|
-
<button class="btn primary" type="button" id="btn-vip-req" style="background:linear-gradient(135deg,#d97706,#f59e0b);justify-content:center">Запросить условия VIP</button>
|
|
399
|
-
<button class="btn" type="button" id="btn-vip-token-open" style="font-size:13px;padding:8px 14px;justify-content:center">Ввести VIP-токен лицензии</button>
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
</section>
|
|
403
|
-
|
|
404
|
-
<footer>
|
|
405
|
-
<div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px">
|
|
406
|
-
<div>© 2026 Автор и правообладатель ПО: Перепелкин Р.А. · Оператор платформы: ООО «БИС» («Бизнес Интер Софт», г. Рязань) · Все права защищены</div>
|
|
407
|
-
<div style="display:flex;gap:18px;flex-wrap:wrap">
|
|
408
|
-
<a href="/legal#offer" style="color:#93c5fd">Лицензионная публичная оферта</a>
|
|
409
|
-
<a href="/legal#privacy" style="color:#93c5fd">Политика конфиденциальности (152-ФЗ)</a>
|
|
410
|
-
<a href="/legal#risks" style="color:#93c5fd">Декларация о рисках</a>
|
|
411
|
-
</div>
|
|
412
|
-
</div>
|
|
413
|
-
<div style="margin-top:8px">Проприетарное программное обеспечение для ЭВМ. Оплата по тарифам является исключительно лицензионным вознаграждением за предоставление права использования ПО и аренду вычислительной инфраструктуры PaaS. Платформа не является сервисом по зарабатыванию денег, не осуществляет доверительное управление или инвестиционное консультирование и не даёт индивидуальных инвестиционных рекомендаций (ФЗ № 39-ФЗ).</div>
|
|
414
|
-
</footer>
|
|
415
|
-
</div>
|
|
416
|
-
|
|
417
|
-
<!-- Модальное окно входа и регистрации -->
|
|
418
|
-
<div class="modal" id="auth-modal" hidden>
|
|
419
|
-
<div class="modal-box">
|
|
420
|
-
<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">
|
|
421
|
-
<button type="button" class="btn" id="tab-auth-reg" style="padding:8px 10px;font-size:13px;justify-content:center;border:0">Регистрация</button>
|
|
422
|
-
<button type="button" class="btn" id="tab-auth-login" style="padding:8px 10px;font-size:13px;justify-content:center;border:0">Вход в аккаунт</button>
|
|
423
|
-
</div>
|
|
424
|
-
<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>
|
|
425
|
-
<h3 id="auth-title" style="margin-top:0">Вход в личный кабинет</h3>
|
|
426
|
-
<div id="max-bot-login-box" hidden style="margin-bottom:8px">
|
|
427
|
-
<button type="button" class="oauth-btn full" id="btn-max-bot-login" style="width:100%;background:rgba(139, 92, 246, 0.16);border-color:rgba(139, 92, 246, 0.45);color:#c4b5fd">
|
|
428
|
-
<span class="oauth-ico" style="background:#7c3aed;color:#fff">M</span>
|
|
429
|
-
<span id="btn-max-bot-lbl">Войти через российский мессенджер MAX</span>
|
|
430
|
-
</button>
|
|
431
|
-
</div>
|
|
432
|
-
<div id="tg-bot-login-box" data-foreign-auth="1" hidden style="margin-bottom:10px">
|
|
433
|
-
<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">
|
|
434
|
-
<span class="oauth-ico" style="background:#24a1de;color:#fff">✈</span>
|
|
435
|
-
<span id="btn-tg-bot-lbl">Войти в 1 клик через Telegram-бота</span>
|
|
436
|
-
</button>
|
|
437
|
-
</div>
|
|
438
|
-
<div class="oauth-grid" id="oauth-btns" hidden>
|
|
439
|
-
<button type="button" class="oauth-btn" data-oauth="yandex" hidden>
|
|
440
|
-
<span class="oauth-ico" style="background:#fc3f1d;color:#fff">Я</span>
|
|
441
|
-
<span>Яндекс ID</span>
|
|
442
|
-
</button>
|
|
443
|
-
<button type="button" class="oauth-btn" data-oauth="sber" hidden>
|
|
444
|
-
<span class="oauth-ico" style="background:#21a038;color:#fff">С</span>
|
|
445
|
-
<span>Сбер ID</span>
|
|
446
|
-
</button>
|
|
447
|
-
<button type="button" class="oauth-btn" data-oauth="tbank" hidden>
|
|
448
|
-
<span class="oauth-ico" style="background:#ffdd2d;color:#111">Т</span>
|
|
449
|
-
<span>T-ID (Т-Банк)</span>
|
|
450
|
-
</button>
|
|
451
|
-
<button type="button" class="oauth-btn" data-oauth="google" data-foreign-auth="1" hidden>
|
|
452
|
-
<span class="oauth-ico" style="background:#fff;color:#2563eb">G</span>
|
|
453
|
-
<span>Google</span>
|
|
454
|
-
</button>
|
|
455
|
-
<button type="button" class="oauth-btn full" data-oauth="vk" hidden>
|
|
456
|
-
<span class="oauth-ico" style="background:#0077ff;color:#fff">VK</span>
|
|
457
|
-
<span>Войти через VK ID</span>
|
|
458
|
-
</button>
|
|
459
|
-
</div>
|
|
460
|
-
<div class="or-sep" id="oauth-or-sep" hidden>или по Email и паролю</div>
|
|
461
|
-
<form id="auth-form">
|
|
462
|
-
<input name="name" id="auth-name" placeholder="Ваше имя (например, Иван)" hidden>
|
|
463
|
-
<input name="email" type="email" required placeholder="Ваш Email">
|
|
464
|
-
<input name="password" type="password" required minlength="6" placeholder="Пароль (не короче 6 символов)">
|
|
465
|
-
<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>
|
|
466
|
-
<div class="err" id="auth-err"></div>
|
|
467
|
-
<button class="btn primary" type="submit" id="auth-submit">Войти</button>
|
|
468
|
-
<button class="btn" type="button" id="auth-toggle">Нет аккаунта? Зарегистрироваться</button>
|
|
469
|
-
<button class="btn" type="button" id="auth-close" style="border:0;color:var(--muted)">Закрыть</button>
|
|
470
|
-
</form>
|
|
471
|
-
</div>
|
|
472
|
-
</div>
|
|
473
|
-
|
|
474
|
-
<!-- Модальное окно подписания Лицензионного договора-оферты перед оплатой лицензии ПО -->
|
|
475
|
-
<div class="modal" id="offer-modal" hidden>
|
|
476
|
-
<div class="modal-box" style="max-width:660px">
|
|
477
|
-
<h3>Лицензионный договор-оферта на использование ПО</h3>
|
|
478
|
-
<div id="offer-account-banner" style="margin-bottom:12px"></div>
|
|
479
|
-
<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">
|
|
480
|
-
<p style="margin-top:0"><b>1. Предмет договора (аренда и лицензирование ПО):</b> Настоящая Публичная оферта регулирует предоставление Пользователю простой (неисключительной) лицензии на право использования программного обеспечения «НейроАльфа» и аренду выделенной виртуальной машины PaaS согласно выбранному тарифу («Инвестор», «Профи», «VIP»). Оплата взимается исключительно за лицензию на ПО.</p>
|
|
481
|
-
<p><b>2. Правовой статус (отсутствие услуг по зарабатыванию денег, ИИР и ДУ):</b> «НейроАльфа» является исключительно техническим программным обеспечением для ЭВМ. Правообладатель и Оператор <b>не оказывают услуг по зарабатыванию денег</b>, не осуществляют инвестиционное консультирование, не дают индивидуальных инвестиционных рекомендаций (ФЗ № 39-ФЗ) и не принимают средства в доверительное управление. Все операции совершаются Пользователем самостоятельно на его личном брокерском счёте.</p>
|
|
482
|
-
<p><b>3. Технические квоты лицензионных тарифов:</b>
|
|
483
|
-
<br>• <b>Лицензия «Инвестор» (3 900 ₽/мес)</b> — 1 брокерский счёт, технический лимит стартовой базы в ПО до 1 000 000 ₽, модуль «Только длинные позиции (Long)»;
|
|
484
|
-
<br>• <b>Лицензия «Профи» (9 900 ₽/мес)</b> — до 5 брокерских счетов, технический лимит стартовой базы в ПО до 1 000 000 ₽ на каждом, модуль «Длинные и короткие позиции (Long / Short)»;
|
|
485
|
-
<br>• <b>Лицензия «VIP»</b> — по индивидуальному договору: квота счетов, лимит объёма, облачная машина PaaS или локальная установка ПО на компьютер.
|
|
486
|
-
</p>
|
|
487
|
-
<p><b>4. Запрет автоследования и сублицензирования:</b> Пользователю запрещается подключать к ПО счета, участвующие в сервисах автоследования или копирования сделок, а также передавать доступ к ПО или формируемые программой данные третьим лицам.</p>
|
|
488
|
-
<p style="margin-bottom:0"><b>5. Декларация о рисках:</b> Использование биржевого ПО на рынке ценных бумаг сопряжено с риском финансовых потерь. Полный текст документов: <a href="/legal" target="_blank" style="color:#93c5fd">открыть в новой вкладке →</a></p>
|
|
489
|
-
</div>
|
|
490
|
-
<div style="display:grid;gap:10px;font-size:13px;color:#e2e8f0;margin-bottom:16px">
|
|
491
|
-
<label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
|
|
492
|
-
<input type="checkbox" id="offer-chk-1" style="width:17px;height:17px;margin-top:2px;flex:none">
|
|
493
|
-
<span>Я прочитал(а) и безоговорочно принимаю условия <a href="/legal#offer" target="_blank" style="color:#93c5fd">Лицензионного договора публичной оферты</a> и <a href="/legal#privacy" target="_blank" style="color:#93c5fd">Политики обработки персональных данных</a>.</span>
|
|
494
|
-
</label>
|
|
495
|
-
<label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
|
|
496
|
-
<input type="checkbox" id="offer-chk-2" style="width:17px;height:17px;margin-top:2px;flex:none">
|
|
497
|
-
<span>Я понимаю, что оплачиваю исключительно лицензию на использование биржевого ПО, ознакомлен(а) с <a href="/legal#risks" target="_blank" style="color:#93c5fd">Декларацией о рисках</a> и обязуюсь <b>не использовать автоследование</b> на подключаемых счетах.</span>
|
|
498
|
-
</label>
|
|
499
|
-
</div>
|
|
500
|
-
<div style="display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap">
|
|
501
|
-
<button class="btn" type="button" id="btn-offer-decline">Отмена</button>
|
|
502
|
-
<button class="btn primary" type="button" id="btn-offer-accept" disabled>Принять условия лицензии и продолжить →</button>
|
|
503
|
-
</div>
|
|
504
|
-
</div>
|
|
505
|
-
</div>
|
|
506
|
-
|
|
507
|
-
<!-- Модальное окно согласования VIP и ввода VIP-токена -->
|
|
508
|
-
<div class="modal" id="vip-modal" hidden>
|
|
509
|
-
<div class="modal-box" style="max-width:480px">
|
|
510
|
-
<h3>Лицензия VIP · Индивидуальные условия</h3>
|
|
511
|
-
<p style="font-size:13px;color:var(--muted);margin:0 0 14px">Лицензия VIP оформляется по индивидуальному договору: квота брокерских счетов, технический лимит объёма, облачная машина PaaS или локальная установка ПО на ваш компьютер.</p>
|
|
512
|
-
<form id="vip-req-form" style="margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)">
|
|
513
|
-
<div style="font-size:13px;font-weight:600;color:#f59e0b">1. Оставить заявку на согласование лицензии VIP:</div>
|
|
514
|
-
<input name="name" placeholder="Ваше имя или организация" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
|
|
515
|
-
<input name="contact" required placeholder="Ваш телефон, email или мессенджер для связи" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
|
|
516
|
-
<input name="capitalRub" placeholder="Требуемая квота счетов / конфигурация (например, локальная установка)" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
|
|
517
|
-
<button class="btn primary" type="submit" style="background:linear-gradient(135deg,#d97706,#f59e0b)">Отправить заявку администратору</button>
|
|
518
|
-
</form>
|
|
519
|
-
<form id="vip-token-form">
|
|
520
|
-
<div style="font-size:13px;font-weight:600;color:#93c5fd">2. Уже получили персональный VIP-токен лицензии?</div>
|
|
521
|
-
<div style="display:flex;gap:8px">
|
|
522
|
-
<input name="code" required placeholder="VIP-…" style="flex:1;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
|
|
523
|
-
<button class="btn primary" type="submit">Активировать VIP</button>
|
|
524
|
-
</div>
|
|
525
|
-
</form>
|
|
526
|
-
<div style="margin-top:12px;text-align:right">
|
|
527
|
-
<button class="btn" type="button" id="btn-vip-close" style="border:0;color:var(--muted)">Закрыть</button>
|
|
528
|
-
</div>
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
<script>
|
|
533
|
-
let selectedMonths = 1;
|
|
534
|
-
let isReg = false;
|
|
535
|
-
let pendingPlan = null;
|
|
536
|
-
let currentUser = null;
|
|
537
|
-
let lastMeMeta = {};
|
|
538
|
-
let paymentSuccessBanner = '';
|
|
539
|
-
let authSeq = 0;
|
|
540
|
-
|
|
541
|
-
const PLAN_TITLES = {
|
|
542
|
-
investor: 'Лицензия «Инвестор» (3 900 ₽/мес)',
|
|
543
|
-
pro: 'Лицензия «Профи» (9 900 ₽/мес)',
|
|
544
|
-
vip: 'Лицензия «VIP»',
|
|
545
|
-
owner: 'Хозяин',
|
|
546
|
-
none: 'Без активной лицензии',
|
|
547
|
-
};
|
|
548
|
-
const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
549
|
-
|
|
550
|
-
const RU_TIMEZONES = new Set([
|
|
551
|
-
'Europe/Kaliningrad', 'Europe/Moscow', 'Europe/Simferopol', 'Europe/Kirov',
|
|
552
|
-
'Europe/Volgograd', 'Europe/Astrakhan', 'Europe/Saratov', 'Europe/Ulyanovsk',
|
|
553
|
-
'Europe/Samara', 'Asia/Yekaterinburg', 'Asia/Omsk', 'Asia/Novosibirsk',
|
|
554
|
-
'Asia/Barnaul', 'Asia/Tomsk', 'Asia/Novokuznetsk', 'Asia/Krasnoyarsk',
|
|
555
|
-
'Asia/Irkutsk', 'Asia/Chita', 'Asia/Yakutsk', 'Asia/Khandyga',
|
|
556
|
-
'Asia/Vladivostok', 'Asia/Ust-Nera', 'Asia/Magadan', 'Asia/Sakhalin',
|
|
557
|
-
'Asia/Srednekolymsk', 'Asia/Kamchatka', 'Asia/Anadyr',
|
|
558
|
-
]);
|
|
559
|
-
|
|
560
|
-
function isRussianUserSegment(me) {
|
|
561
|
-
const up = new URLSearchParams(location.search);
|
|
562
|
-
if (up.get('foreign_auth') === '1') return false;
|
|
563
|
-
if (up.get('foreign_auth') === '0') return true;
|
|
564
|
-
if (me && typeof me.isRuSegment === 'boolean') {
|
|
565
|
-
if (me.isRuSegment) return true;
|
|
566
|
-
}
|
|
567
|
-
try {
|
|
568
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
|
569
|
-
if (RU_TIMEZONES.has(tz)) return true;
|
|
570
|
-
} catch { /* ignore */ }
|
|
571
|
-
const langs = (navigator.languages?.length ? navigator.languages : [navigator.language || '']).map((l) => String(l).toLowerCase());
|
|
572
|
-
if (langs.some((l) => l === 'ru' || l.startsWith('ru-'))) return true;
|
|
573
|
-
return Boolean(me?.isRuSegment ?? true);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function syncNavAuth() {
|
|
577
|
-
const loginBtn = document.getElementById('btn-login-open');
|
|
578
|
-
const regBtn = document.getElementById('btn-reg-open');
|
|
579
|
-
const logoutBtn = document.getElementById('btn-nav-logout');
|
|
580
|
-
const navCta = document.getElementById('btn-nav-cta');
|
|
581
|
-
const heroStart = document.getElementById('btn-hero-start');
|
|
582
|
-
if (currentUser) {
|
|
583
|
-
if (loginBtn) {
|
|
584
|
-
loginBtn.textContent = `👤 ${currentUser.name} · ${currentUser.planName}`;
|
|
585
|
-
loginBtn.title = 'Перейти в личный кабинет';
|
|
586
|
-
}
|
|
587
|
-
if (regBtn) regBtn.hidden = true;
|
|
588
|
-
if (logoutBtn) logoutBtn.hidden = false;
|
|
589
|
-
if (navCta) {
|
|
590
|
-
if (currentUser.role === 'admin') {
|
|
591
|
-
navCta.href = '/admin';
|
|
592
|
-
navCta.textContent = 'Консоль админа';
|
|
593
|
-
navCta.hidden = false;
|
|
594
|
-
} else {
|
|
595
|
-
navCta.hidden = true;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
if (heroStart) heroStart.textContent = 'Открыть личный кабинет';
|
|
599
|
-
} else {
|
|
600
|
-
if (loginBtn) {
|
|
601
|
-
loginBtn.textContent = 'Войти';
|
|
602
|
-
loginBtn.title = 'Войти в личный кабинет';
|
|
603
|
-
}
|
|
604
|
-
if (regBtn) regBtn.hidden = false;
|
|
605
|
-
if (logoutBtn) logoutBtn.hidden = true;
|
|
606
|
-
if (navCta) navCta.hidden = true;
|
|
607
|
-
if (heroStart) heroStart.textContent = 'Регистрация в личном кабинете';
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function doLogout(reopenRegWithPlan = null) {
|
|
612
|
-
authSeq++;
|
|
613
|
-
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
|
|
614
|
-
currentUser = null;
|
|
615
|
-
paymentSuccessBanner = '';
|
|
616
|
-
syncNavAuth();
|
|
617
|
-
renderCabinet();
|
|
618
|
-
if (reopenRegWithPlan) {
|
|
619
|
-
pendingPlan = reopenRegWithPlan;
|
|
620
|
-
openAuth(true);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function renderCabinet() {
|
|
625
|
-
const cab = document.getElementById('cabinet-box');
|
|
626
|
-
syncNavAuth();
|
|
627
|
-
if (!cab) return;
|
|
628
|
-
if (!currentUser) {
|
|
629
|
-
cab.hidden = true;
|
|
630
|
-
cab.innerHTML = '';
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
cab.hidden = false;
|
|
634
|
-
const untilStr = currentUser.planUntil > Date.now()
|
|
635
|
-
? `лицензия активна до ${new Date(currentUser.planUntil).toLocaleDateString('ru-RU')}`
|
|
636
|
-
: (currentUser.plan && currentUser.plan !== 'none' ? 'бессрочная лицензия' : 'лицензия не оплачена');
|
|
637
|
-
const capLim = currentUser.limits?.maxCapitalPerAccountRub
|
|
638
|
-
? (currentUser.limits.maxCapitalPerAccountRub >= 100000000 ? 'без ограничений' : `до ${(currentUser.limits.maxCapitalPerAccountRub / 1000000).toLocaleString('ru-RU')} млн ₽/счёт`)
|
|
639
|
-
: 'требуется выбор тарифа лицензии';
|
|
640
|
-
const offerStr = currentUser.offerAcceptedAt
|
|
641
|
-
? `<span class="pos">подписан ✓ (${new Date(currentUser.offerAcceptedAt).toLocaleDateString('ru-RU')})</span>`
|
|
642
|
-
: '<span style="color:var(--muted)">акцептуется при оформлении лицензии</span>';
|
|
643
|
-
const nt = lastMeMeta.notify || {};
|
|
644
|
-
const code = currentUser.notifyCode || '';
|
|
645
|
-
const tgUrl = nt.tgBotName && code ? `https://t.me/${encodeURIComponent(nt.tgBotName)}?start=${encodeURIComponent(code)}` : '';
|
|
646
|
-
const maxUrl = nt.maxBotName && code ? `https://max.ru/${encodeURIComponent(nt.maxBotName)}?start=${encodeURIComponent(code)}` : '';
|
|
647
|
-
const payments = Array.isArray(currentUser.payments) ? currentUser.payments : [];
|
|
648
|
-
const payRowsHtml = payments.length
|
|
649
|
-
? `<table style="width:100%;border-collapse:collapse;font-size:13px;margin-top:8px">
|
|
650
|
-
<thead><tr style="border-bottom:1px solid var(--line);color:var(--muted);text-align:left">
|
|
651
|
-
<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>
|
|
652
|
-
</tr></thead>
|
|
653
|
-
<tbody>${payments.map((p) => {
|
|
654
|
-
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' }) : '—';
|
|
655
|
-
const stBadge = p.status === 'succeeded'
|
|
656
|
-
? '<span class="pos" style="font-weight:700">Оплачен ✓</span>'
|
|
657
|
-
: p.status === 'canceled'
|
|
658
|
-
? '<span class="neg">Отменён</span>'
|
|
659
|
-
: `<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>`;
|
|
660
|
-
return `<tr style="border-bottom:1px solid rgba(148,163,184,0.1)">
|
|
661
|
-
<td style="padding:6px 8px;color:var(--muted)">${escH(dt)}</td>
|
|
662
|
-
<td style="padding:6px 8px"><b>${escH(PLAN_TITLES[p.plan] || p.plan)}</b></td>
|
|
663
|
-
<td style="padding:6px 8px">${Number(p.months || 1)} мес.</td>
|
|
664
|
-
<td style="padding:6px 8px"><b>${Number(p.amount_rub || 0).toLocaleString('ru-RU')} ₽</b></td>
|
|
665
|
-
<td style="padding:6px 8px">${stBadge}</td>
|
|
666
|
-
</tr>`;
|
|
667
|
-
}).join('')}</tbody>
|
|
668
|
-
</table>`
|
|
669
|
-
: '<div style="font-size:13px;color:var(--muted);margin-top:6px">Платежей по этому аккаунту пока не было. Выберите тариф лицензии ниже или активируйте промокод.</div>';
|
|
670
|
-
|
|
671
|
-
cab.innerHTML = `<div class="cab-card">
|
|
672
|
-
${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>` : ''}
|
|
673
|
-
<div class="cab-grid">
|
|
674
|
-
<div>
|
|
675
|
-
<div class="sec-tag" style="margin-bottom:4px">Личный кабинет лицензиата · ${currentUser.role === 'admin' ? 'Администратор платформы' : 'Учётная запись активна'}</div>
|
|
676
|
-
<h3 style="margin:0 0 8px;font-size:21px">${escH(currentUser.name)} <span style="color:var(--muted);font-weight:400;font-size:14.5px">(${escH(currentUser.email)})</span></h3>
|
|
677
|
-
<div style="margin-bottom:10px;font-size:14px;color:#cbd5e1;line-height:1.55">
|
|
678
|
-
Лицензия ПО: <b class="${currentUser.plan === 'none' ? '' : 'pos'}">${escH(currentUser.planName)}</b> (${escH(untilStr)}) · Квота счетов: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> (${escH(capLim)}) · Модуль: <b>${currentUser.limits?.allowShorts ? 'Long / Short' : 'только Long'}</b><br>
|
|
679
|
-
Лицензионный договор-оферта: ${offerStr}
|
|
680
|
-
</div>
|
|
681
|
-
</div>
|
|
682
|
-
<div style="display:flex;flex-direction:column;gap:8px;min-width:240px">
|
|
683
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
684
|
-
<a class="btn primary" href="#pricing" style="flex:1;justify-content:center">${currentUser.plan === 'none' ? 'Выбрать и оплатить лицензию' : 'Продлить / Сменить лицензию'}</a>
|
|
685
|
-
<button type="button" class="btn" id="cab-verify-btn" title="Проверить статус последнего платежа в ЮKassa">Проверить оплату</button>
|
|
686
|
-
</div>
|
|
687
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
688
|
-
${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>' : ''}
|
|
689
|
-
<button type="button" class="btn" id="cab-logout-btn" style="color:#f87171;border-color:rgba(248,113,113,0.35);justify-content:center">Выйти из аккаунта</button>
|
|
690
|
-
</div>
|
|
691
|
-
</div>
|
|
692
|
-
</div>
|
|
693
|
-
|
|
694
|
-
<div id="cab-workspace" style="margin-top:16px;padding-top:14px;border-top:1px solid var(--line)"></div>
|
|
695
|
-
|
|
696
|
-
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line)">
|
|
697
|
-
<div style="border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--bg)">
|
|
698
|
-
<div style="font-weight:700;margin-bottom:6px">📲 Технические уведомления в MAX и Telegram</div>
|
|
699
|
-
<div style="font-size:13px;color:#cbd5e1;line-height:1.5;margin-bottom:10px">
|
|
700
|
-
Ваш персональный код привязки: <b style="font-family:monospace;font-size:14px;color:#93c5fd">${escH(code || '—')}</b><br>
|
|
701
|
-
Статус: MAX — ${currentUser.maxConnected ? '<b class="pos">привязан ✓</b>' : '<span style="color:var(--muted)">не привязан</span>'} · Telegram — ${currentUser.tgConnected ? '<b class="pos">привязан ✓</b>' : '<span style="color:var(--muted)">не привязан</span>'}
|
|
702
|
-
</div>
|
|
703
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
704
|
-
${maxUrl ? `<a class="btn" href="${escH(maxUrl)}" target="_blank" rel="noopener" style="padding:7px 12px;font-size:12.5px">Подключить MAX</a>` : ''}
|
|
705
|
-
${tgUrl ? `<a class="btn" href="${escH(tgUrl)}" target="_blank" rel="noopener" style="padding:7px 12px;font-size:12.5px">Подключить Telegram</a>` : ''}
|
|
706
|
-
<button type="button" class="btn" id="cab-notify-test" style="padding:7px 12px;font-size:12.5px">Тест уведомления</button>
|
|
707
|
-
</div>
|
|
708
|
-
</div>
|
|
709
|
-
|
|
710
|
-
<div style="border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:var(--bg)">
|
|
711
|
-
<div style="font-weight:700;margin-bottom:6px">💳 Оплата лицензии, промокоды и история счетов</div>
|
|
712
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:8px">
|
|
713
|
-
<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">
|
|
714
|
-
<button type="button" class="btn" id="cab-promo-btn" style="padding:7px 14px;font-size:12.5px">Активировать код</button>
|
|
715
|
-
</div>
|
|
716
|
-
${payRowsHtml}
|
|
717
|
-
</div>
|
|
718
|
-
</div>
|
|
719
|
-
</div>`;
|
|
720
|
-
renderWorkspace();
|
|
721
|
-
|
|
722
|
-
document.getElementById('btn-close-pay-banner')?.addEventListener('click', () => {
|
|
723
|
-
paymentSuccessBanner = '';
|
|
724
|
-
renderCabinet();
|
|
725
|
-
});
|
|
726
|
-
document.getElementById('cab-logout-btn')?.addEventListener('click', () => doLogout());
|
|
727
|
-
document.getElementById('cab-verify-btn')?.addEventListener('click', () => verifyPaymentNow());
|
|
728
|
-
cab.querySelectorAll('[data-verify-pid]').forEach((b) => b.addEventListener('click', () => verifyPaymentNow(b.dataset.verifyPid)));
|
|
729
|
-
document.getElementById('cab-notify-test')?.addEventListener('click', async () => {
|
|
730
|
-
try {
|
|
731
|
-
const r = await fetch('/api/auth/notify-test', { method: 'POST', credentials: 'same-origin' });
|
|
732
|
-
const j = await r.json();
|
|
733
|
-
if (!r.ok) throw new Error(j.error || 'Не удалось отправить тест');
|
|
734
|
-
alert('Тестовое уведомление отправлено в привязанный мессенджер!');
|
|
735
|
-
} catch (e) { alert(e.message); }
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
document.getElementById('cab-promo-btn')?.addEventListener('click', () => {
|
|
739
|
-
const code = document.getElementById('cab-promo-in')?.value.trim();
|
|
740
|
-
if (!code) return;
|
|
741
|
-
requireOfferAgreement(async () => {
|
|
742
|
-
const r = await fetch('/api/auth/redeem-promo', {
|
|
743
|
-
method: 'POST',
|
|
744
|
-
credentials: 'same-origin',
|
|
745
|
-
headers: { 'Content-Type': 'application/json' },
|
|
746
|
-
body: JSON.stringify({ code }),
|
|
747
|
-
});
|
|
748
|
-
const j = await r.json();
|
|
749
|
-
if (!r.ok) return alert(j.error || 'Ошибка активации кода');
|
|
750
|
-
currentUser = j.user;
|
|
751
|
-
paymentSuccessBanner = `Лицензионный код активирован! Действует тариф «${currentUser.planName}».`;
|
|
752
|
-
renderCabinet();
|
|
753
|
-
}, `Активация лицензионного кода (${code})`);
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
async function verifyPaymentNow(paymentId = null, silent = false) {
|
|
758
|
-
try {
|
|
759
|
-
const r = await fetch('/api/billing/verify', {
|
|
760
|
-
method: 'POST',
|
|
761
|
-
credentials: 'same-origin',
|
|
762
|
-
headers: { 'Content-Type': 'application/json' },
|
|
763
|
-
body: JSON.stringify(paymentId ? { paymentId } : {}),
|
|
764
|
-
});
|
|
765
|
-
const j = await r.json();
|
|
766
|
-
if (!r.ok) throw new Error(j.error || 'Не удалось проверить статус платежа');
|
|
767
|
-
if (j.user) currentUser = j.user;
|
|
768
|
-
if (j.status === 'succeeded') {
|
|
769
|
-
paymentSuccessBanner = `Оплата лицензии подтверждена! Активен тариф «${currentUser?.planName || ''}». Ваша персональная виртуальная машина PaaS готова к работе ниже.`;
|
|
770
|
-
renderCabinet();
|
|
771
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
772
|
-
} else if (!silent) {
|
|
773
|
-
renderCabinet();
|
|
774
|
-
alert(j.status === 'none' ? 'Неоплаченных счетов в ожидании не найдено.' : `Текущий статус платежа в ЮKassa: ${j.status}`);
|
|
775
|
-
}
|
|
776
|
-
} catch (e) {
|
|
777
|
-
if (!silent) alert(e.message);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function renderWorkspace() {
|
|
782
|
-
const box = document.getElementById('cab-workspace');
|
|
783
|
-
if (!box || !currentUser) return;
|
|
784
|
-
const list = currentUser.instances || [];
|
|
785
|
-
const dl = lastMeMeta?.downloads || {};
|
|
786
|
-
const paid = currentUser.plan && currentUser.plan !== 'none' && currentUser.plan !== 'free';
|
|
787
|
-
const canDesktop = Boolean(currentUser.limits?.allowDesktop);
|
|
788
|
-
const ago = (ts) => {
|
|
789
|
-
if (!ts) return 'ожидает первого запуска в PaaS';
|
|
790
|
-
const s = Math.round((Date.now() - ts) / 1000);
|
|
791
|
-
return s < 120 ? `${s} с назад` : s < 7200 ? `${Math.round(s / 60)} мин назад` : `${Math.round(s / 3600)} ч назад`;
|
|
792
|
-
};
|
|
793
|
-
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>`;
|
|
794
|
-
|
|
795
|
-
const clouds = list.filter((i) => i.kind === 'cloud');
|
|
796
|
-
let cloudBody;
|
|
797
|
-
if (currentUser.role === 'admin' && !clouds.length) {
|
|
798
|
-
cloudBody = `<div style="font-size:13.5px;color:#cbd5e1;margin-bottom:10px">Для администратора («Хозяин») программный комплекс работает на центральном сервере, а клиентские PaaS-машины управляются в консоли администратора.</div>
|
|
799
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
800
|
-
<a class="btn primary" href="/?terminal=1">Открыть веб-терминал ПО →</a>
|
|
801
|
-
<a class="btn" href="/admin">Управление машинами клиентов (/admin) →</a>
|
|
802
|
-
</div>`;
|
|
803
|
-
} else if (clouds.length) {
|
|
804
|
-
cloudBody = clouds.map((i) => {
|
|
805
|
-
const state = i.online
|
|
806
|
-
? '<span class="pos" style="font-weight:700">● работает в облаке PaaS</span>'
|
|
807
|
-
: ['pending', 'deploying', 'deployed'].includes(i.provisionState)
|
|
808
|
-
? '<span style="color:#fbbf24">● выделена и готовится к запуску в PaaS</span>'
|
|
809
|
-
: i.provisionState === 'error'
|
|
810
|
-
? '<span class="neg">● ожидает повторного развёртывания в PaaS</span>'
|
|
811
|
-
: `<span style="color:var(--muted)">● ${ago(i.lastSeen)}</span>`;
|
|
812
|
-
return `<div style="font-size:14px;margin-bottom:10px">Выделенная машина: <b>${escH(i.host || i.id)}</b> · ${state}</div>
|
|
813
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
814
|
-
<a class="btn primary" href="/api/instances/open?i=${encodeURIComponent(i.id)}">Перейти в веб-терминал ПО →</a>
|
|
815
|
-
</div>
|
|
816
|
-
<div style="font-size:12.5px;color:var(--muted);margin-top:8px">При нажатии выполняется автоматический вход в ваш изолированный экземпляр ПО на виртуальной машине PaaS по одноразовому ключу.</div>`;
|
|
817
|
-
}).join('<hr style="border:0;border-top:1px solid var(--line);margin:10px 0">');
|
|
818
|
-
} else if (paid) {
|
|
819
|
-
cloudBody = '<div style="font-size:13.5px;color:var(--muted)">Ваша персональная виртуальная машина PaaS создаётся автоматически после подтверждения лицензии — нажмите «Проверить оплату» или обновите страницу через минуту.</div>';
|
|
820
|
-
} else {
|
|
821
|
-
cloudBody = '<div style="font-size:13.5px;color:var(--muted);margin-bottom:10px">Оформите лицензию («Инвестор» или «Профи») — в облаке PaaS автоматически создастся ваша персональная виртуальная машина с установленным ПО «НейроАльфа».</div><a class="btn primary" href="#pricing">Выбрать тариф лицензии →</a>';
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const desks = list.filter((i) => i.kind === 'desktop');
|
|
825
|
-
let deskBody;
|
|
826
|
-
if (!canDesktop) {
|
|
827
|
-
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>';
|
|
828
|
-
} else {
|
|
829
|
-
const links = [
|
|
830
|
-
dl.windows ? `<a class="btn" href="${escH(dl.windows)}">⬇ Windows</a>` : '',
|
|
831
|
-
dl.linux ? `<a class="btn" href="${escH(dl.linux)}">⬇ Linux</a>` : '',
|
|
832
|
-
].filter(Boolean).join(' ');
|
|
833
|
-
const bound = desks.filter((i) => i.installBound);
|
|
834
|
-
deskBody = `${links ? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">${links}</div>` : '<div style="font-size:13px;color:var(--muted);margin-bottom:8px">Ссылки на дистрибутив ПО предоставляются администратором.</div>'}
|
|
835
|
-
${dl.docker ? `<div style="font-size:12.5px;color:var(--muted);margin-bottom:8px">Docker: <code>docker run -d -p 127.0.0.1:3000:3000 -v neuroalpha:/data ${escH(dl.docker)}</code></div>` : ''}
|
|
836
|
-
${bound.map((i) => `<div style="font-size:13.5px;margin:6px 0">💻 <b>${escH(i.hostname || i.id)}</b> · ${i.online ? '<span class="pos">на связи</span>' : ago(i.lastSeen)}${i.version ? ` · v${escH(i.version)}` : ''}
|
|
837
|
-
<a class="btn" href="/api/instances/open?i=${encodeURIComponent(i.id)}" style="padding:4px 10px;font-size:12.5px">Открыть</a>
|
|
838
|
-
<button type="button" class="btn" data-unbind="${escH(i.id)}" style="padding:4px 10px;font-size:12.5px">Отвязать</button></div>`).join('')}
|
|
839
|
-
<button type="button" class="btn primary" id="cab-desktop-code">Получить код активации ПО</button>
|
|
840
|
-
<div id="cab-desktop-code-out" style="margin-top:8px;font-size:14px"></div>`;
|
|
841
|
-
}
|
|
842
|
-
box.innerHTML = `<div class="sec-tag" style="margin-bottom:10px">Доступ к экземпляру ПО и веб-терминалу</div>
|
|
843
|
-
<div style="display:flex;gap:14px;flex-wrap:wrap">${card('☁ Выделенная виртуальная машина в PaaS', cloudBody)}${card('💻 Локальная установка ПО на компьютер (VIP)', deskBody)}</div>`;
|
|
844
|
-
|
|
845
|
-
document.getElementById('cab-desktop-code')?.addEventListener('click', async () => {
|
|
846
|
-
const out = document.getElementById('cab-desktop-code-out');
|
|
847
|
-
try {
|
|
848
|
-
const r = await fetch('/api/instances/desktop-code', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } });
|
|
849
|
-
const j = await r.json();
|
|
850
|
-
if (!r.ok) throw new Error(j.error || `Ошибка ${r.status}`);
|
|
851
|
-
out.innerHTML = `Код: <b style="font-family:monospace;font-size:18px;letter-spacing:1px">${escH(j.code)}</b> — введите его в локальной программе в течение 10 минут.`;
|
|
852
|
-
} catch (e) { out.textContent = e.message; }
|
|
853
|
-
});
|
|
854
|
-
box.querySelectorAll('[data-unbind]').forEach((b) => b.addEventListener('click', async () => {
|
|
855
|
-
if (!confirm('Отвязать этот компьютер? На новом компьютере понадобится новый код активации.')) return;
|
|
856
|
-
const r = await fetch(`/api/instances/${encodeURIComponent(b.dataset.unbind)}/unbind`, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } });
|
|
857
|
-
if (!r.ok) return alert((await r.json().catch(() => ({}))).error || 'Ошибка');
|
|
858
|
-
const me = await (await fetch('/api/auth/me', { credentials: 'same-origin' })).json().catch(() => null);
|
|
859
|
-
if (me?.user) { currentUser = me.user; lastMeMeta = me; }
|
|
860
|
-
renderCabinet();
|
|
861
|
-
}));
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
document.getElementById('oauth-btns')?.addEventListener('click', (e) => {
|
|
865
|
-
const b = e.target.closest('button[data-oauth]');
|
|
866
|
-
if (!b) return;
|
|
867
|
-
const prov = b.dataset.oauth;
|
|
868
|
-
location.href = `/api/auth/oauth/${encodeURIComponent(prov)}/start${pendingPlan ? `?plan=${encodeURIComponent(pendingPlan)}` : ''}`;
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
let tgPollTimer = null;
|
|
872
|
-
async function startBotLogin(channel) {
|
|
873
|
-
try {
|
|
874
|
-
const res = await fetch('/api/auth/telegram/start', {
|
|
875
|
-
method: 'POST',
|
|
876
|
-
credentials: 'same-origin',
|
|
877
|
-
headers: { 'Content-Type': 'application/json' },
|
|
878
|
-
body: JSON.stringify({ channel }),
|
|
879
|
-
});
|
|
880
|
-
const j = await res.json();
|
|
881
|
-
if (!res.ok) throw new Error(j.error || 'Не удалось запустить вход через мессенджер');
|
|
882
|
-
window.open(j.botUrl, '_blank');
|
|
883
|
-
const lbl = document.getElementById(channel === 'max' ? 'btn-max-bot-lbl' : 'btn-tg-bot-lbl');
|
|
884
|
-
if (lbl) lbl.textContent = `Нажмите «Начать / Запустить» в открывшемся ${channel === 'max' ? 'MAX' : 'Telegram'}…`;
|
|
885
|
-
clearInterval(tgPollTimer);
|
|
886
|
-
tgPollTimer = setInterval(async () => {
|
|
887
|
-
const pr = await fetch(`/api/auth/telegram/poll?code=${encodeURIComponent(j.code)}`, { credentials: 'same-origin' }).then((r) => r.json()).catch(() => ({}));
|
|
888
|
-
if (pr.status === 'done' && pr.user) {
|
|
889
|
-
clearInterval(tgPollTimer);
|
|
890
|
-
const me = await (await fetch('/api/auth/me', { credentials: 'same-origin' })).json().catch(() => ({ user: pr.user }));
|
|
891
|
-
currentUser = me.user || pr.user;
|
|
892
|
-
lastMeMeta = me;
|
|
893
|
-
document.getElementById('auth-modal').hidden = true;
|
|
894
|
-
renderCabinet();
|
|
895
|
-
if (pendingPlan) {
|
|
896
|
-
const p = pendingPlan;
|
|
897
|
-
pendingPlan = null;
|
|
898
|
-
await startCheckout(p);
|
|
899
|
-
} else {
|
|
900
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
901
|
-
}
|
|
902
|
-
} else if (pr.status === 'expired') {
|
|
903
|
-
clearInterval(tgPollTimer);
|
|
904
|
-
if (lbl) lbl.textContent = channel === 'max' ? 'Войти через российский мессенджер MAX' : 'Войти в 1 клик через Telegram-бота';
|
|
905
|
-
}
|
|
906
|
-
}, 2000);
|
|
907
|
-
} catch (e) {
|
|
908
|
-
document.getElementById('auth-err').textContent = e.message;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
document.getElementById('btn-tg-bot-login')?.addEventListener('click', () => startBotLogin('tg'));
|
|
912
|
-
document.getElementById('btn-max-bot-login')?.addEventListener('click', () => startBotLogin('max'));
|
|
913
|
-
|
|
914
|
-
async function initLanding() {
|
|
915
|
-
const mySeq = ++authSeq;
|
|
916
|
-
try {
|
|
917
|
-
const me = await (await fetch('/api/auth/me', { credentials: 'same-origin' })).json();
|
|
918
|
-
if (mySeq !== authSeq) return;
|
|
919
|
-
lastMeMeta = me;
|
|
920
|
-
currentUser = me.user;
|
|
921
|
-
|
|
922
|
-
// Для пользователей из российского сегмента скрываем авторизацию через иностранные сервисы (ФЗ № 406-ФЗ / 149-ФЗ)
|
|
923
|
-
const hideForeignAuth = isRussianUserSegment(me);
|
|
924
|
-
let hasAnyFastLogin = false;
|
|
925
|
-
|
|
926
|
-
if (me.notify?.maxBotName && me.notify?.hasMaxToken) {
|
|
927
|
-
document.getElementById('max-bot-login-box').hidden = false;
|
|
928
|
-
hasAnyFastLogin = true;
|
|
929
|
-
}
|
|
930
|
-
const tgBox = document.getElementById('tg-bot-login-box');
|
|
931
|
-
if (tgBox) {
|
|
932
|
-
const showTg = Boolean(me.notify?.tgBotName && me.notify?.hasTgToken && !hideForeignAuth);
|
|
933
|
-
tgBox.hidden = !showTg;
|
|
934
|
-
if (showTg) hasAnyFastLogin = true;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const oa = me.oauth || {};
|
|
938
|
-
let enabledOauthCount = 0;
|
|
939
|
-
document.querySelectorAll('#oauth-btns [data-oauth]').forEach((b) => {
|
|
940
|
-
const prov = b.dataset.oauth;
|
|
941
|
-
const isForeign = b.dataset.foreignAuth === '1' || Boolean(oa[prov]?.foreign);
|
|
942
|
-
const on = Boolean(oa[prov]?.enabled) && !(hideForeignAuth && isForeign);
|
|
943
|
-
b.hidden = !on;
|
|
944
|
-
if (on) enabledOauthCount++;
|
|
945
|
-
});
|
|
946
|
-
const oauthGrid = document.getElementById('oauth-btns');
|
|
947
|
-
if (oauthGrid) oauthGrid.hidden = enabledOauthCount === 0;
|
|
948
|
-
if (enabledOauthCount > 0) hasAnyFastLogin = true;
|
|
949
|
-
const orSep = document.getElementById('oauth-or-sep');
|
|
950
|
-
if (orSep) orSep.hidden = !hasAnyFastLogin;
|
|
951
|
-
|
|
952
|
-
const up = new URLSearchParams(location.search);
|
|
953
|
-
if (up.get('oauth_error')) {
|
|
954
|
-
openAuth(false);
|
|
955
|
-
document.getElementById('auth-err').textContent = up.get('oauth_error');
|
|
956
|
-
} else if (!currentUser && up.get('register') === '1') {
|
|
957
|
-
openAuth(true);
|
|
958
|
-
} else if (!currentUser && up.get('login') === '1') {
|
|
959
|
-
openAuth(false);
|
|
960
|
-
}
|
|
961
|
-
syncNavAuth();
|
|
962
|
-
if (currentUser) {
|
|
963
|
-
renderCabinet();
|
|
964
|
-
const hasPendingPay = Array.isArray(currentUser.payments) && currentUser.payments.some((p) => p.status === 'pending');
|
|
965
|
-
if (up.get('payment_check') === '1' || hasPendingPay) {
|
|
966
|
-
await verifyPaymentNow(null, up.get('payment_check') !== '1');
|
|
967
|
-
if (up.get('payment_check') === '1' && history.replaceState) {
|
|
968
|
-
history.replaceState(null, '', location.pathname);
|
|
969
|
-
}
|
|
970
|
-
} else if (up.get('buy_plan')) {
|
|
971
|
-
startCheckout(up.get('buy_plan'));
|
|
972
|
-
} else if (up.get('oauth_ok') === '1') {
|
|
973
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
} catch { /* ignore */ }
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function updatePrices() {
|
|
980
|
-
const disc = selectedMonths >= 12 ? 0.8 : selectedMonths >= 3 ? 0.9 : 1;
|
|
981
|
-
const inv = Math.round(3900 * selectedMonths * disc);
|
|
982
|
-
const pro = Math.round(9900 * selectedMonths * disc);
|
|
983
|
-
const lbl = selectedMonths === 1 ? '/ мес' : `за ${selectedMonths} мес.`;
|
|
984
|
-
document.getElementById('price-investor').innerHTML = `${inv.toLocaleString('ru-RU')} ₽ <span>${lbl}</span>`;
|
|
985
|
-
document.getElementById('price-pro').innerHTML = `${pro.toLocaleString('ru-RU')} ₽ <span>${lbl}</span>`;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
document.getElementById('months-seg').addEventListener('click', (e) => {
|
|
989
|
-
const b = e.target.closest('button[data-m]');
|
|
990
|
-
if (!b) return;
|
|
991
|
-
selectedMonths = Number(b.dataset.m);
|
|
992
|
-
document.querySelectorAll('#months-seg button').forEach((x) => x.classList.toggle('active', x === b));
|
|
993
|
-
updatePrices();
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
function openAuth(reg = false) {
|
|
997
|
-
isReg = reg;
|
|
998
|
-
document.getElementById('auth-modal').hidden = false;
|
|
999
|
-
document.getElementById('auth-name').hidden = !reg;
|
|
1000
|
-
const offerRow = document.getElementById('auth-offer-row');
|
|
1001
|
-
const offerChk = document.getElementById('auth-offer-chk');
|
|
1002
|
-
if (offerRow) offerRow.hidden = !reg;
|
|
1003
|
-
if (offerChk) offerChk.required = Boolean(reg);
|
|
1004
|
-
const tabReg = document.getElementById('tab-auth-reg');
|
|
1005
|
-
const tabLogin = document.getElementById('tab-auth-login');
|
|
1006
|
-
if (tabReg) tabReg.className = reg ? 'btn primary' : 'btn';
|
|
1007
|
-
if (tabLogin) tabLogin.className = !reg ? 'btn primary' : 'btn';
|
|
1008
|
-
const planBanner = document.getElementById('auth-plan-banner');
|
|
1009
|
-
if (planBanner) {
|
|
1010
|
-
if (pendingPlan) {
|
|
1011
|
-
planBanner.hidden = false;
|
|
1012
|
-
planBanner.innerHTML = `<b>Шаг 1 из 2:</b> ${reg ? 'Создайте учётную запись лицензиата' : 'Войдите в свою учётную запись'}, чтобы перейти к оплате тарифа <b>«${escH(PLAN_TITLES[pendingPlan] || pendingPlan)}»</b>.`;
|
|
1013
|
-
} else {
|
|
1014
|
-
planBanner.hidden = true;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
document.getElementById('auth-title').textContent = reg ? 'Регистрация учётной записи' : 'Вход в личный кабинет';
|
|
1018
|
-
document.getElementById('auth-submit').textContent = reg ? (pendingPlan ? 'Создать аккаунт и перейти к оплате →' : 'Создать аккаунт') : (pendingPlan ? 'Войти и перейти к оплате →' : 'Войти');
|
|
1019
|
-
document.getElementById('auth-toggle').textContent = reg ? 'Уже есть аккаунт? Войти' : 'Нет аккаунта? Зарегистрироваться';
|
|
1020
|
-
document.getElementById('auth-err').textContent = '';
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
document.getElementById('tab-auth-reg')?.addEventListener('click', () => openAuth(true));
|
|
1024
|
-
document.getElementById('tab-auth-login')?.addEventListener('click', () => openAuth(false));
|
|
1025
|
-
document.getElementById('btn-login-open').addEventListener('click', () => {
|
|
1026
|
-
if (currentUser) {
|
|
1027
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1028
|
-
} else {
|
|
1029
|
-
pendingPlan = null;
|
|
1030
|
-
openAuth(false);
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
document.getElementById('btn-reg-open')?.addEventListener('click', () => {
|
|
1034
|
-
pendingPlan = null;
|
|
1035
|
-
openAuth(true);
|
|
1036
|
-
});
|
|
1037
|
-
document.getElementById('btn-nav-logout')?.addEventListener('click', () => doLogout());
|
|
1038
|
-
document.getElementById('btn-hero-start').addEventListener('click', () => currentUser ? document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' }) : openAuth(true));
|
|
1039
|
-
document.getElementById('auth-close').addEventListener('click', () => { document.getElementById('auth-modal').hidden = true; });
|
|
1040
|
-
document.getElementById('auth-toggle').addEventListener('click', () => openAuth(!isReg));
|
|
1041
|
-
|
|
1042
|
-
let onOfferAccepted = null;
|
|
1043
|
-
const offerModal = document.getElementById('offer-modal');
|
|
1044
|
-
const offerChk1 = document.getElementById('offer-chk-1');
|
|
1045
|
-
const offerChk2 = document.getElementById('offer-chk-2');
|
|
1046
|
-
const offerAcceptBtn = document.getElementById('btn-offer-accept');
|
|
1047
|
-
|
|
1048
|
-
function syncOfferBtn() {
|
|
1049
|
-
if (offerAcceptBtn) offerAcceptBtn.disabled = !(offerChk1?.checked && offerChk2?.checked);
|
|
1050
|
-
}
|
|
1051
|
-
offerChk1?.addEventListener('change', syncOfferBtn);
|
|
1052
|
-
offerChk2?.addEventListener('change', syncOfferBtn);
|
|
1053
|
-
|
|
1054
|
-
function requireOfferAgreement(actionFn, planLabel = '') {
|
|
1055
|
-
onOfferAccepted = actionFn;
|
|
1056
|
-
if (offerChk1) offerChk1.checked = false;
|
|
1057
|
-
if (offerChk2) offerChk2.checked = false;
|
|
1058
|
-
syncOfferBtn();
|
|
1059
|
-
const accBanner = document.getElementById('offer-account-banner');
|
|
1060
|
-
if (accBanner && currentUser) {
|
|
1061
|
-
const isAdmin = currentUser.role === 'admin';
|
|
1062
|
-
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">
|
|
1063
|
-
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
|
|
1064
|
-
<span>Лицензиат: <b>${escH(currentUser.name)}</b> (<code>${escH(currentUser.email)}</code>)${planLabel ? ` · <b>${escH(planLabel)}</b>` : ''}</span>
|
|
1065
|
-
<button type="button" class="btn" id="btn-offer-switch-user" style="padding:4px 10px;font-size:12px">Выйти и зарегистрировать другого клиента</button>
|
|
1066
|
-
</div>
|
|
1067
|
-
${isAdmin ? '<div style="margin-top:6px;color:#fde68a">⚠ <b>Внимание:</b> вы сейчас авторизованы под учётной записью администратора («Хозяин»). Чтобы протестировать регистрацию и покупку лицензии как новый клиент, нажмите кнопку <b>«Выйти и зарегистрировать другого клиента»</b>.</div>' : ''}
|
|
1068
|
-
</div>`;
|
|
1069
|
-
document.getElementById('btn-offer-switch-user')?.addEventListener('click', () => {
|
|
1070
|
-
offerModal.hidden = true;
|
|
1071
|
-
onOfferAccepted = null;
|
|
1072
|
-
doLogout(pendingPlan || 'investor');
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
offerModal.hidden = false;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
document.getElementById('btn-offer-decline')?.addEventListener('click', () => {
|
|
1079
|
-
offerModal.hidden = true;
|
|
1080
|
-
onOfferAccepted = null;
|
|
1081
|
-
});
|
|
1082
|
-
offerAcceptBtn?.addEventListener('click', async () => {
|
|
1083
|
-
offerAcceptBtn.disabled = true;
|
|
1084
|
-
try {
|
|
1085
|
-
await fetch('/api/auth/accept-offer', { method: 'POST', credentials: 'same-origin' }).catch(() => {});
|
|
1086
|
-
offerModal.hidden = true;
|
|
1087
|
-
if (onOfferAccepted) await onOfferAccepted();
|
|
1088
|
-
} finally {
|
|
1089
|
-
onOfferAccepted = null;
|
|
1090
|
-
syncOfferBtn();
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
async function startCheckout(plan) {
|
|
1095
|
-
pendingPlan = plan;
|
|
1096
|
-
const planLabel = `${PLAN_TITLES[plan] || plan} (${selectedMonths} мес.)`;
|
|
1097
|
-
requireOfferAgreement(async () => {
|
|
1098
|
-
try {
|
|
1099
|
-
const res = await fetch('/api/billing/checkout', {
|
|
1100
|
-
method: 'POST',
|
|
1101
|
-
credentials: 'same-origin',
|
|
1102
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1103
|
-
body: JSON.stringify({ plan, months: selectedMonths, returnOrigin: location.origin }),
|
|
1104
|
-
});
|
|
1105
|
-
const j = await res.json();
|
|
1106
|
-
if (!res.ok) throw new Error(j.error || 'Ошибка перехода к оплате лицензии');
|
|
1107
|
-
if (j.confirmationUrl) location.href = j.confirmationUrl;
|
|
1108
|
-
} catch (e) {
|
|
1109
|
-
alert(e.message);
|
|
1110
|
-
}
|
|
1111
|
-
}, planLabel);
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
document.getElementById('btn-vip-req')?.addEventListener('click', () => {
|
|
1115
|
-
document.getElementById('vip-modal').hidden = false;
|
|
1116
|
-
});
|
|
1117
|
-
document.getElementById('btn-vip-token-open')?.addEventListener('click', () => {
|
|
1118
|
-
if (!currentUser) { openAuth(true); return; }
|
|
1119
|
-
document.getElementById('vip-modal').hidden = false;
|
|
1120
|
-
document.querySelector('#vip-token-form input[name=code]')?.focus();
|
|
1121
|
-
});
|
|
1122
|
-
document.getElementById('btn-vip-close')?.addEventListener('click', () => {
|
|
1123
|
-
document.getElementById('vip-modal').hidden = true;
|
|
1124
|
-
});
|
|
1125
|
-
document.getElementById('vip-req-form')?.addEventListener('submit', async (e) => {
|
|
1126
|
-
e.preventDefault();
|
|
1127
|
-
const f = e.target.elements;
|
|
1128
|
-
const res = await fetch('/api/auth/vip-request', {
|
|
1129
|
-
method: 'POST',
|
|
1130
|
-
credentials: 'same-origin',
|
|
1131
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1132
|
-
body: JSON.stringify({ name: f.name.value, contact: f.contact.value, capitalRub: f.capitalRub.value }),
|
|
1133
|
-
});
|
|
1134
|
-
const j = await res.json();
|
|
1135
|
-
if (!res.ok) return alert(j.error || 'Ошибка отправки заявки');
|
|
1136
|
-
document.getElementById('vip-modal').hidden = true;
|
|
1137
|
-
alert('Заявка на согласование лицензии VIP отправлена администратору.');
|
|
1138
|
-
});
|
|
1139
|
-
document.getElementById('vip-token-form')?.addEventListener('submit', (e) => {
|
|
1140
|
-
e.preventDefault();
|
|
1141
|
-
const code = e.target.elements.code.value.trim();
|
|
1142
|
-
if (!code) return;
|
|
1143
|
-
if (!currentUser) {
|
|
1144
|
-
document.getElementById('vip-modal').hidden = true;
|
|
1145
|
-
openAuth(true);
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
document.getElementById('vip-modal').hidden = true;
|
|
1149
|
-
requireOfferAgreement(async () => {
|
|
1150
|
-
const r = await fetch('/api/auth/redeem-promo', {
|
|
1151
|
-
method: 'POST',
|
|
1152
|
-
credentials: 'same-origin',
|
|
1153
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1154
|
-
body: JSON.stringify({ code }),
|
|
1155
|
-
});
|
|
1156
|
-
const j = await r.json();
|
|
1157
|
-
if (!r.ok) return alert(j.error || 'Не удалось активировать VIP-токен');
|
|
1158
|
-
currentUser = j.user;
|
|
1159
|
-
paymentSuccessBanner = `Персональный VIP-токен активирован! Ваша лицензия: «${currentUser.planName}».`;
|
|
1160
|
-
renderCabinet();
|
|
1161
|
-
}, `Активация VIP-токена лицензии (${code})`);
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
document.querySelectorAll('[data-buy]').forEach((b) => b.addEventListener('click', () => {
|
|
1165
|
-
const plan = b.dataset.buy;
|
|
1166
|
-
if (!currentUser) {
|
|
1167
|
-
pendingPlan = plan;
|
|
1168
|
-
openAuth(true);
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
startCheckout(plan);
|
|
1172
|
-
}));
|
|
1173
|
-
|
|
1174
|
-
document.getElementById('auth-form').addEventListener('submit', async (e) => {
|
|
1175
|
-
e.preventDefault();
|
|
1176
|
-
const f = e.target.elements;
|
|
1177
|
-
const url = isReg ? '/api/auth/register' : '/api/auth/login';
|
|
1178
|
-
try {
|
|
1179
|
-
const res = await fetch(url, {
|
|
1180
|
-
method: 'POST',
|
|
1181
|
-
credentials: 'same-origin',
|
|
1182
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1183
|
-
body: JSON.stringify({ name: f.name.value, email: f.email.value, password: f.password.value }),
|
|
1184
|
-
});
|
|
1185
|
-
const j = await res.json();
|
|
1186
|
-
if (!res.ok) throw new Error(j.error || 'Ошибка авторизации');
|
|
1187
|
-
const me = await (await fetch('/api/auth/me', { credentials: 'same-origin' })).json().catch(() => ({ user: j.user }));
|
|
1188
|
-
currentUser = me.user || j.user;
|
|
1189
|
-
lastMeMeta = me;
|
|
1190
|
-
document.getElementById('auth-modal').hidden = true;
|
|
1191
|
-
renderCabinet();
|
|
1192
|
-
if (pendingPlan) {
|
|
1193
|
-
const p = pendingPlan;
|
|
1194
|
-
pendingPlan = null;
|
|
1195
|
-
await startCheckout(p);
|
|
1196
|
-
} else {
|
|
1197
|
-
document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1198
|
-
}
|
|
1199
|
-
} catch (err) {
|
|
1200
|
-
document.getElementById('auth-err').textContent = err.message;
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
initLanding();
|
|
1205
|
-
</script>
|
|
1206
|
-
</body>
|
|
1207
|
-
</html>
|